
简介这份PPT围绕电路描述语言CDL展开面向电路设计、仿真验证及相关课程学习与测试场景也可供需要理解门级电路文本描述方法的读者参考。内容从语法规则入手区分函数/连接语句、说明语句与继续语句并逐一说明AND、OR、NAND、NOR、XOR、NOT、FOUT、IN、OUT、VCC、GND及END等语句的写法与约束。随后以C17电路为例给出完整CDL描述便于对照理解原始输入、输出、内部门输出与扇出分支之间的关系。后半部分介绍CDL编译器如何把文本描述转换为内部结构表涉及编译流程、元素表以及前驱后继扩展表、输入输出表等组织方式能帮助读者建立从语言描述到电路数据结构的整体认识。资源包共1个PPT文件约196KB篇幅紧凑适合课堂阅读报告、实验前预习或自学梳理。已有759人学习可作为CDL入门与复习的轻量材料。1. 第一列的那个空格比关键字更能决定一条 CDL 语句的命运电路描述语言 CDL 的语法表看起来平平无奇AND、NAND、XOR、FOUT、IN、OUT 十来个关键字加斜杠分隔参数半小时就能背完。可真正决定一行文本命运的是它第一列有没有空格。第一列是空格解析器才把它当函数/连接语句去建结点第一列是星号整行算说明第一列是等号才算上一条语句的续行。这个设计把排版和语义绑在了一起代价是一份网表的缩进乱掉就直接失效收益是解析器不用做任何词法回溯读一个字符就能分流。把这一层想明白等于拿到了门级网表从纯文本变成图结构的那把钥匙后面的元素表、前驱后继指针、扇出约束还有测试生成要用的那些统计量全是从这里长出来的。2. 语法即电路一行业务语句如何编码成图的一条边2.1 三类语句靠第一列做词法分流CDL 里所有描述语句被切成三类切法不看关键字看第一列第一列是空格的是函数/连接语句负责建结点、连边第一列是星号的是说明语句解析器直接跳过、当注释使第一列是等号的是继续语句把上一行没写完的内容接上去。这个分层很像汇编里的标号列和指令列——位置本身就是语法的一部分只是 CDL 把它压缩到了单个字符上。除了第一列其余位置的空格一律忽略。这条规则看着随意其实是给手写留的活路IN/ 1GAT,2GAT,...里斜杠后面那一串空格没有任何含义纯粹是为了让人眼对齐电路里同一层的信号。理解了“只有第一列有意义”就不会再纠结为什么抄一份 CDL 到编辑器里一自动缩进就崩了——缩进动了第一列语义就变了。2.2 标号八字符上限表结构反过来约束了语法标号是不超过 8 个字符的字符串不能包含空格、逗号和斜杠。三个禁用字符不用解释它们分别是字段分隔符和参数分隔符出现在标号里会直接把一行切错。八字符上限才是真正有信息量的地方元素表里名字字段是char name[9]8 个字符加一个结尾的\0一个字节都不多留。也就是说这个上限不是语法设计者的审美选择而是被下游结构体的内存布局反向钉死的。这种“语法迁就结构”的做法在早期 EDA 工具里很常见。固定长度名字带来的是整个元素表是一块连续内存按下标随机访问零开销序列化到.tbl文件时也不用处理变长字符串。代价就是你永远不能在标号里写NAND_GATE_001这种带描述性的名字只能1GAT、8FAN地编号。我在看那些例子里1GAT、14FAN、22GAT混着用的时候第一反应是命名混乱第二反应才明白这是把“类型”编进了名字后缀GAT 是门、FAN 是扇出分支线人眼扫一遍就知道这行在建什么。2.3 门语句的字段顺序输入永远在前输出永远在后函数/连接语句的定义高度统一格式都是关键字/输入列表/输出列表/。AND、OR、NAND、NOR 允许n1XOR 强制n2NOT 固定单输入单输出FOUT 反过来一个源、多个分支。这个方向约定不是随意的它让解析器可以在不认识具体门类型的前提下机械地把第二个字段里的每个标号都当成前驱、第三个字段里的标号当成后继。门的种类只影响type字段填什么不影响连边逻辑。XOR 那个n2的限制值得单独说。二输入异或才是基本门的语义三输入以上的 XOR 在电路里其实是一棵二输入异或树的折叠写法。允许n2等于让语法层替你做了一次结构展开的许诺真到仿真阶段多输入 XOR 要么展开成二输入链要么在求值函数里循环累异或。这条约束是在“写得省事”和“语义无歧义”之间取的一个偏向前者的点。提示IN/Pin1,...,Pink/inp1,...,ink/是把外部管脚名和内部线名做一次映射。如果两边完全相同可以简写成IN/inp1,...,ink//第二个字段留空。这个简写是后面最容易踩的坑之一解析器必须能区分“字段为空”和“字段缺失”。2.4 VCC、GND、END给网表补上边界条件VCC/line1,...,linem/和GND/line1,...,linem/不建门只把某些线钉死在逻辑 1 或逻辑 0 上。它们在元素表里是有位置的——类型表里专门留了 6 和 7 两个编码扩展表里也明确写了“存放前驱或后继在*elmtbl中的位置及 VCC 和 GND 在*elmtbl中的位置”。这句话的意思很实在常量线不是特殊情况它就是一个前驱只不过这个前驱没有输入、输出恒定。END则是整份描述的收尾。它的作用不只是礼貌性结束而是给解析器一个确定的停机点——没有END读取循环只能靠 EOF 判断任何一点格式异常都会让错误定位跑到文件末尾去。* 一个半加器的 CDL 描述片段 IN/A,B// OUT/S,C// XOR/A,B/S/ AND/A,B/C/ END上面这段里XOR/A,B/S/建一个异或结点前驱是 A 和 B后继是 SAND/A,B/C/同理。解析器读完三行有效语句元素表里应该有 A、B、S、C 四个带类型的结点外加两个门结点——注意 S 和 C 既在OUT语句里出现过又是门的输出同一个标号只能有一条表项靠名字查表去重是建表阶段必须做的事。3. 从文本到图编译器怎样把一行字符串变成前驱后继指针3.1 编译流程只有三步但每步都在丢信息材料里给的流程图很朴素读入.cdl产出电路表.tbl同时把电路信息和错误信息写进.prn再往屏幕上打一份执行报告。四个通道各司其职——输入是描述、输出是结构、报告是给人看的、错误是给定位用的。真正值得注意的是“电路表”这个产物它不是文本是一组已经连好边的数组后面的仿真、测试生成、等价性检查都直接吃这组数组不再回头看原始 CDL。也就是说CDL 只活在编译期。一旦进了.tbl门的写法AND 还是 NAND、参数的书写顺序、注释的位置全部消失只剩类型码和边的拓扑。这个“信息有损”是特性不是缺陷结构表越小、越规整后面每一轮遍历越快。3.2 ELST一个结构体装下五种语义/* 元素表一个标号一条记录数组下标就是它在图中的编号 */ typedef struct elmtbl { char name[9]; /* 标号名最多 8 字符 结尾 \0 */ int type; /* 元素类型见下方编码表负数用来表示输出取反 */ int nfi; /* fan-in 个数这个结点有几个前驱 */ int fil; /* fan-in 位置nfi1 时是 elmtbl 下标nfi1 时是 flist 起始下标 */ int nfo; /* fan-out 个数这个结点驱动几条线 */ int fol; /* fan-out 位置语义与 fil 完全对称 */ } ELST;name那 9 个字节负责身份type负责“这是个什么”剩下四个整数两两成对负责“它跟谁连着”。关键在于fil和fol这两个字段是复用的当度数等于 1它直接存对方在elmtbl里的下标当度数大于 1它改存对方在扩展表flist里的起始位置。同一个int两种含义靠nfi/nfo的值来消歧。我一般会把这个技巧理解成“手写的 tagged union”省下的不只是 4 个字节还有一次指针解引用和一次额外内存分配。绝大多数门电路的度数都很小就地存能把常见情况走成最快的路径只有扇出点那种真正一对多的结点才升级到扩展表。3.3 nfi/fil 的单值多值复用为什么这样能 work类型码的编码方式也藏着同样的思路type含义type含义0INPUT5XOR1OR6VCC2AND7GND3FOUT8SRL4OUTPUT-1NAND-2NOR-3NOT正数给“不取反”的元件负数给“输出取反”的元件-1是 NOR绝对值 1 对应 OR、-2是 NAND绝对值 2 对应 AND。这套编码的妙处在于求值拿到一个结点先看符号决定要不要在最后取反再用绝对值去查基本门的求值函数一个if (type 0)就把反相器、与非、或非三种情况收编了。-3单独给 NOT 有点破坏对称性——按上面的规律它该对应类型 3而 3 已经被 FOUT 占了所以 NOT 只能作为特例单独走分支。这种“规律留了一点例外”的编码在真实代码里非常常见也是最容易在扩展时踩坑的地方。扩展表本身就是一个int *flist存的全是元素表下标。看注释那句“每一个标号的前驱或后继相对集中”意思是一个结点的所有邻居在flist里占一段连续区间知道起点fil和个数nfi就能顺序读完。这让遍历后继变成一次纯粹的线性扫描没有链表跳转、没有指针追逐。3.4 PIOT 与统计计数器给下游留的接口/* 输入/输出表只存名字和它在元素表里的下标 */ typedef struct piotbl { char name[9]; /* 输入或输出的标号名同样 8 字符上限 */ int ptr; /* 该标号在 *elmtbl 中的位置 */ } PIOT;PIOT是给外部世界用的索引仿真时按顺序给输入赋值、按顺序取输出都靠这两张表。它只存名字和一个下标真正的类型、连接关系全在元素表里避免了两份数据打架。另外还有一批计数器inpnum、numout记输入输出数nfout、nnot、nnor、nnand、nor、nand、nxor、nsr分别统计各类元件个数。这些数字看着像装饰实际是测试生成和故障模拟的入口参数——按类型分桶之后遍历某一类门就不用扫全表。ngnd/lgnd那对变量重复了同样的单值多值技巧只有一个 GND 时lgnd直接是它的下表多个时lgnd变成扩展表起点。同一套模式在一个结构里出现三四次说明这不是灵机一动而是这份实现的统一风格。4. 扇出为什么要单独建模FOUT 语句与那条把图锁成树的约束4.1 一根线驱动多个门物理上本来就有个结点如果只按“门到门”连边一个输出驱动三个输入就得写三条语句元素表里那个门结点凭空多出三条后继看起来也没什么问题。CDL 没这么干它要求你显式写FOUT/inp/out1,...,outm/把扇出源和分支线拆成两层标号。原因得回到电路本身一根线从驱动点出来到分叉点之间是同一个电结点分叉之后才变成多条独立的线。测试生成要给每条分支单独注入故障、时序分析要算分支各自的负载它们本来就是不同的对象。拆开之后元素表里每个标号对应且只对应一个电结点nfi/nfo的语义变得干净门的出度永远是 1或者 0如果它直连输出扇出源的出度是 m分支线的出度是 1。任何遍历都不用再猜“这条边代表一根线还是一束线”。4.2 分支线不能再扇出一条把结构锁成树的约束语法里那句“out1…outm不能是另一个扇出分支的输入”是全套规则里最容易被忽略、也最关键的一条。它禁掉的是这种写法先FOUT/A/B1,B2/再FOUT/B1/C1,C2/。如果允许分支线就成了能继续分叉的普通结点扇出结构从一棵树退化成任意有向无环图FOUT元素的语义一次分叉、分叉点之后不再分叉当场失效。禁掉之后得到的好处很实在从任意一个分支线往上追路径上至多经过一个扇出点追到门就停。这让“这个信号从哪来”的反向遍历有了上界也让故障传播的路径枚举不会爆炸。真需要在分支上再分叉怎么办中间插一个缓冲门——这在物理设计里本来就是标准做法缓冲器要占面积、要算延时不是白给的。语法用一条禁令把“免费分叉”这件事堵死逼你在网表层面就把驱动能力想清楚。注意这条约束是语法级的编译器必须在建表阶段就检查而不是留到后面的图算法里去处理。一旦放进了.tbl结构表里没有任何字段能表达“这条分支线又是扇出源”错误会以“某个门的前驱数不对”这种面目出现在下游。4.3 边是怎么写进 flist 的typedef struct { int src, dst; } EDGE; /* 三遍走先数度数再分配块最后回填。全程零 malloc、零指针 */ static void build_edges(EDGE *e, int ne, ELST *tbl, int ntbl, int *flist, int *lpnt) { int i, *cur; /* 第一遍统计每个结点的出度 */ for (i 0; i ne; i) tbl[e[i].src].nfo; /* 第二遍出度 1 就地存出度 1 在 flist 里领一块连续空间 */ for (i 0; i ntbl; i) { if (tbl[i].nfo 1) { tbl[i].fol *lpnt; /* 记下这一块的起始下标 */ *lpnt tbl[i].nfo; /* 游标前移块与块之间严丝合缝 */ } } /* 第三遍按边的原始顺序回填cur 记录每个结点写到第几格 */ cur calloc(ntbl, sizeof(int)); for (i 0; i ne; i) { int s e[i].src, d e[i].dst; if (tbl[s].nfo 1) tbl[s].fol d; /* 单后继落在字段里 */ else flist[tbl[s].fol cur[s]] d; /* 多后继落在块内 */ } free(cur); }三遍扫描看起来笨换来的是整个边表零动态分配、零指针。lpnt是flist的长度游标第二遍扫完它正好等于所有“出度大于 1”的结点的出度之和flist一次malloc到位。cur只是一个临时写指针数组用完即弃。前驱方向的构建完全对称把src/dst调个个儿、用nfi/fil就行。想省一次遍历也可以在读语句时同时往两个方向推边——代价是两边都得做“单值多值”的分支判断代码会长一截。4.4 拿那份 17 结点的例子对一遍数材料里那份例子给了六个统计量结点数 17、原始输入 5、原始输出 2、内部门输出 4、扇出点 3、扇出分支 6。这六个数字是自洽的5 个输入结点、2 个输出结点、4 个内部与非门输出、6 条扇出分支线加起来正好 17 条元素表记录。三个扇出点各分两路所以分支数是 6。这份账最实用的地方是校验。写完一个解析器把这些数字当成断言跑一遍元素表长度对不对、nfout是不是 3、nnand是不是 6、inpnum和numout是不是 5 和 2。只要哪个数差一问题几乎一定在两类地方——标号重复导致去重没生效或者扇出分支被当成了独立于源结点的普通线而多建了一条表项。这比拿眼睛逐行比对网表快得多也是我做这类解析器时的固定动作。5. 排查清单CDL 描述过不了编译的几种典型翻车5.1 现象第一条语句凭空消失元素表里少了一个门原因几乎总是第一列。用编辑器新建文件时习惯性敲了个 Tab 或者把整段做了自动缩进NAND/1GAT,8FAN/10GAT/的第一列变成空白之外的字符解析器按规则把它归到“说明语句”那一类整行连着后面本属于它的参数一起被丢掉。更隐蔽的情况是文件开头有 BOM 或者不可见字符肉眼看不出第一列被占了。解决办法是把“第一列是空格”写进解析器的最前置判断并且对 Tab 做严格拒绝而不是宽容替换——宽容只会把错误推迟到更难定位的地方。同时给.prn报告加一条“已忽略的说明语句行号”列表被丢掉的行一眼就能看见。5.2 现象两个不同的标号在报告里变成同一个前驱数对不上char name[9]只能装 8 个字符写了 9 个字符的标号会被截断成前 8 个ADDER_OUT1和ADDER_OUT2双双变成ADDER_OU去重逻辑判定为同一个结点后一条语句的前驱直接覆盖了前一条。现象是某个门的输入个数少了一个或者某个输出根本找不到驱动。解决办法是统一在语法分析阶段做长度检查超过 8 个字符直接报错并给出列号绝不允许静默截断。命名习惯上可以把描述性信息压缩成短后缀例如用1GAT、8FAN这类“编号加类型缩写”的组合天然不会超长。5.3 现象两输入的异或门没问题单输入的 XOR 被编译器拒绝XOR 语句的约束是n2而 AND、OR、NAND、NOR 都是n1。手写网表时很容易把某个单输入的情形也写成 XOR尤其是从别的工具导出的网表里冗余门还没被优化掉的时候。编译器报错的位置常常指向输出标号看着像输出有问题其实是输入个数不合法。解决办法是在语法检查里把每种门的最小输入数做成一张表报错时明确写出“XOR 需要至少 2 个输入实际 1 个”。真遇到单输入的异或它在逻辑上就是那根线本身直接删掉这个门、把前后连起来就行。5.4 现象扇出分支又被当成扇出源编译器报错或者图结构错乱FOUT的分支输出不能再作为另一条FOUT的输入这条约束在自由手写时极易违反因为从逻辑上看“这根线再分两路”完全合理。违反之后如果编译器没做检查.tbl里会出现一条既被当作分支线、又有多个后继的结点度量字段自相矛盾做了检查的则会直接拒绝但错误信息常常只写“结点类型冲突”看不出是扇出嵌套。解决办法是建表时维护一张“分支线标号集合”每次处理FOUT语句先查源标号是否在这个集合里。要在某条分支上继续分叉就插入一个缓冲门让分叉发生在门的输出上——这在电路上也更接近真实做法。5.5 现象IN行被判定参数个数不足或者读到文件末尾还没停两种写法在这个坑里会合流。一种是简写IN/A,B,C//第二个字段刻意留空表示管脚名与内部线名相同某些实现把它当成“字段缺失”而报参数不足另一种是漏了END解析循环一直读到 EOF最后一条语句被当成不完整记录吞掉报错位置飘到文件最后一行。解决办法是把“字段为空”和“字段不存在”分开处理连续两个斜杠产生一个空字段这是合法的行尾没有收尾斜杠才是错误。END则必须有缺了就在.prn里明确报告“未找到 END 语句”而不是让 EOF 悄悄收场。6. 取舍为什么是紧凑数组而不是一张指针图把 CDL 编译成一张图最直觉的写法是每个结点挂一个后继链表加边就是malloc一个节点插到表头。这份实现没走这条路它用一个大数组加一个整数池把所有边压平。两种存法摆在一起看差距很清楚维度指针链表图紧凑数组 下标池遍历一个结点的全部后继指针追逐缓存不友好连续区间线性扫描单后继常见情况仍要一次 malloc直接存在fol字段里反向前驱遍历需要额外的反向链表或全表扫描nfi/fil对称支持序列化到.tbl要处理指针重定位整块写出即可动态增删边容易困难基本要求两遍扫描选数组的代价是建图必须分多遍走、边集不能动态改收益是解析完成之后每一次遍历都快而且产物可以直接落盘当成二进制格式用。对这份材料里描述的用法——描述、编译、之后反复做仿真和测试生成——这个取舍几乎不用犹豫建图只发生一次遍历发生无数次。想验证一个实现有没有把图连对我习惯做三件事。第一把inpnum、numout、各类门的计数和源描述手工对一遍数字对不上就别往下走。第二对每个结点检查对称性从 A 的后继里能找到 B那 B 的前驱里就必须能找到 A反向也一样这条断言能抓出绝大多数漏边和错边。第三随便挑一条从原始输入到原始输出的路径顺着fol走一遍再逆着fil走回来看能不能原路返回——能返回说明前驱后继两个方向的构建逻辑是一致的。这套结构还有一个值得往下挖的方向SRL那个类型码说明表里给时序元件留了位置但前驱后继的语义在时序元件上会变得微妙——反馈环让“前驱”不再是良定义的偏序拓扑排序也就不再适用。真要处理带反馈的电路就得在遍历时加访问标记把强连通分量单独拎出来。这已经超出这份材料覆盖的范围但知道边界在哪比以为它能处理一切要有用得多。从那以后我每次写网表解析器都会先造一个只有三个结点的小电路跑通前驱后继的双向断言再往上堆规模——小电路上错一处大电路上会错成一片而错误信息通常指向的是完全无关的那一行。希望帮到你。本文还有配套的精品资源点击获取