CDL网表编译原理:从固定列语法到三表建表机制 简介这份PPT面向数字电路与EDA课程的学习者及电路设计入门人员系统讲解电路描述语言CDL的语法规则与建模方法帮助读者掌握用文本方式描述电路结构、连接关系与逻辑功能的技能。资源包共1个PPT文件约196KB以幻灯片形式组织语法规则、函数/连接语句定义、C17电路描述实例与CDL编译器四部分内容便于课堂讲解或自学时按页梳理要点。其中函数/连接语句部分逐一说明AND、OR、NAND、NOR、XOR、NOT、FOUT、IN、OUT、VCC、GND及END等语句的书写格式并以C17电路为例给出完整描述代码编译器部分介绍日本奈良大学CDL编译器的处理流程与元素表、前驱后继扩展表、输入输出表等内部结构。已有760人学习适合作为电路描述与仿真验证的入门参考。1. 一份网表凭什么能被编译器“读懂”CDL 的固定列语法很多人第一次看到 CDL 的电路描述会觉得它简陋得不像一门语言没有变量声明没有作用域连空格都只在第一列有意义。可正是这种“第一列决定语义”的硬规则让一个用 C 写的编译器能在几百行内把文本变成一张可遍历的电路表。电路描述语言 CDL 解决的核心问题不是“表达逻辑功能”而是把一张手绘或工具导出的门级网表压缩成一种机器能线性扫描、一次建表的结构化文本。搞懂它你就搞懂了 EDA 前端最原始的那一步文本如何变成图。这份 PPT 把语法、实例、编译器内部表结构串成一条线值得逐层拆开看。2. 语法规则为什么第一列比内容更重要2.1 三类语句靠首字符分流CDL 把描述语句分成三类判据只有一个第一列是什么字符。第一列是空格就是函数/连接语句第一列是*就是说明语句第一列是就是继续语句。除第一列外所有空格一律忽略。这个设计看起来粗暴实际上是在为编译器省掉一整个词法分析阶段。常见的实现思路是读入一行后先看line[0]直接 switch 到三个处理分支不需要 tokenizer不需要正则也不需要处理“空格到底算不算分隔符”的歧义。对门级网表这种格式高度规整的输入来说这是用约束换简单性的典型取舍。代价也很明显用户不能随意缩进。你在函数语句前多敲一个空格它就从“连接语句”变成了“说明语句”整行被忽略编译器不会报错只会少一个门。这种静默失败在后面避坑章节会专门讲。标号规则同样克制不超过 8 个字符不能含空格、逗号和斜杠。8 字符上限直接对应元素表里char name[9]的定长数组多一个字符就溢出。逗号和斜杠被禁是因为它们分别是参数分隔符和语句定界符标号里出现就会把一行切错。2.2 函数/连接语句的参数布局所有门语句共享同一个骨架关键字/输入列表/输出/。以 NAND 为例NAND/ 1GAT,8FAN /10GAT斜杠是硬分隔符第一个斜杠后是输入标号列表第二个斜杠后是输出标号。AND、OR、NAND、NOR 的输入个数 n1XOR 要求 n2——因为异或至少要两个输入才有意义单输入 XOR 在语义上退化成缓冲编译器干脆在语法层拒绝。NOT 固定单输入单输出。FOUT 是扇出语句形态是FOUT/inp/out1,…,outm/把源信号inp扇出到多个分支线。这里有一条容易被忽略的约束扇出分支out1…outm不能是另一个扇出分支的输入。换句话说扇出点不能级联。原因在于编译器建表时扇出分支在元素表里是独立条目如果允许级联前驱/后继的扩展表flist会出现一个分支同时被当作“某扇出的后继”和“另一扇出的前驱”索引关系会绕成环遍历时就得加环检测。禁掉级联等于在建表阶段就保证了图的无环性。IN/OUT 语句负责把内部标号和外部管脚名对上IN/Pin1,…,Pink/inp1,…,inpk/ OUT/out1,…,outk/Pin1,…,Pink/如果管脚名和内部标号完全一致可以简写成IN/inp1,…,inpk//第二个斜杠后留空。这个简写不是语法糖它让编译器少建一层映射PIOT表里name和ptr直接指向元素表同一条目。VCC/GND 语句把指定线强制接到电源或地END 收尾。整份语法没有表达式、没有层次、没有参数化就是一张扁平的、带扇出标注的门级连接表。3. 从 C17 实例看建表17 个结点如何变成三张表3.1 实例的规模参数先对账PPT 用 C17 电路做例子先给了一组规模数字结点数 17原始输入 5原始输出 2内部门输出 4扇出点 3扇出分支 6。这组数字不是随便列的它决定了后面每张表的长度。原始输入 5 个1GAT,2GAT,3GAT,6GAT,7GAT。原始输出 2 个22GAT,23GAT。扇出点 3 个3GAT、11GAT、16GAT分别扇出到8FAN,9FAN、14FAN,15FAN、20FAN,21FAN共 6 条分支线。内部门输出是那些既不是原始输入也不是扇出分支的门输出比如10GAT、11GAT、16GAT、19GAT这类中间结点。把 CDL 描述逐行读下来编译器要做的第一件事是给每个出现的标号在元素表elmtbl里分配一个槽位第二件事是记录每个标号的前驱和后继。前驱是“谁驱动我”后继是“我驱动谁”。对门来说输入是前驱输出是后继对扇出点来说源是前驱各分支是后继。3.2 元素表的字段语义元素表条目定义如下typedef struct elmtbl { char name[9]; /* 标号名最长 8 字符 结束符 */ int type; /* 标号类型编码 */ int nfi, fil; /* 前驱个数前驱位置或扩展表起始位置 */ int nfo, fol; /* 后继个数后继位置或扩展表起始位置 */ } ELST;type的编码把门类型和特殊结点压进一个整数0 是 INPUT1 是 OR2 是 AND3 是 FOUT4 是 OUTPUT5 是 XOR6 是 VCC7 是 GND8 是 SRL负数留给带反相的门-1 是 NAND-2 是 NOR-3 是 NOT。用负数表示反相门好处是遍历时判断“是否反相”只需要看符号位不用再查表。nfi/fil和nfo/fol是一对复用字段这是整张表最精巧的地方。当一个标号只有一个前驱时fil直接存那个前驱在elmtbl里的下标当有多个前驱时fil存的是它在扩展表flist里的起始位置nfi是连续存放的个数。后继同理。这种“单值内联、多值外挂”的布局动机很实际门级网表里绝大多数结点的扇入扇出都是 1 到 2把单值情况内联能省掉大量扩展表条目和一次间接寻址。常见的实现思路是先用两遍扫描统计每个标号的前驱后继个数第二遍再决定哪些走内联、哪些走扩展表。3.3 扩展表与前驱后继的定位扩展表就是一个整型数组int *flist; /* 存放前驱/后继在 elmtbl 中的位置以及 VCC/GND 的位置 */PPT 里特别注明“每一个标号的前驱或后继相对集中”所以只要在元素表里拿到fil或fol这个起始位置加上nfi或nfo这个个数就能连续读出全部前驱或后继。这是一个用局部性换随机访问的设计建表时把同一标号的邻居写在一起遍历时顺序读缓存友好。VCC 和 GND 也被塞进flist由ngnd/lgnd记录个数和位置。这样电源和地的连接不需要单独一张表遍历邻居时统一从flist取代码路径只有一条。3.4 输入输出表与统计变量输入输出表用同一个结构typedef struct piotbl { char name[9]; /* 输入或输出标号名 */ int ptr; /* 该标号在 elmtbl 中的位置 */ } PIOT;pitbl和potbl分别指向输入、输出表inpnum/numout记录条数。ptr是元素表下标所以从管脚名到内部结点是一次数组索引不需要字符串查找。其余变量是给后续仿真或检查用的计数器nfout、nnot、nnor、nnand、nor、nand、nxor、nsr分别统计各类标号个数lpnt、slist是元素表和扩展表的长度指针。这些计数在建表过程中顺手累加几乎零成本但让编译器能在执行报告里直接输出电路构成不用再扫一遍表。4. 编译器流程一次扫描如何完成建表4.1 四个输入输出通道PPT 给出的编译器流程图有四个通道#1输入文件是 CDL 描述#2输出文件是电路内部表示#3输出文件是电路信息和错误信息#4是屏幕上的执行报告。对应到文件后缀输入是*.cdl输出是*.tbl和*.prn。这个划分把“机器可读的结构表”和“人可读的报告”分开。*.tbl是给后续仿真器吃的二进制或文本表*.prn是给人看的统计和报错。很多早期 EDA 工具都采用这种双输出原因是建表阶段一旦出错用户需要的是“第几行、哪个标号、什么错”而不是一张看不懂的表。4.2 建表的两遍扫描虽然 PPT 没有贴完整源码但从表结构可以反推出建表流程必然是两遍扫描。第一遍读所有语句给每个标号分配元素表槽位、确定类型、累加前驱后继计数第二遍根据计数决定内联还是写扩展表并填充fil/fol。第一遍里遇到IN语句就把管脚名和内部标号登记进pitbl遇到OUT登记进potbl遇到门语句就给输入标号加前驱计数、给输出标号加后继计数遇到FOUT就给源加后继计数、给每个分支加前驱计数。第二遍再按第一遍统计出的偏移把邻居写进flist。这种两遍结构是定长数组时代的标配没有动态容器只能先数再填。放到今天看它等价于先count再prefix sum再scatter只不过用 C 手写。4.3 错误信息从哪来#3通道的错误信息主要覆盖几类标号超长或含非法字符、门语句输入个数不满足约束比如 XOR 只有一个输入、扇出分支被用作另一扇出的输入、IN/OUT引用了未定义的标号、缺少END。这些检查都发生在建表过程中因为只有建表时才知道标号是否真的存在、扇出是否级联。提示CDL 编译器对“第一列写错”这类格式错误往往不报错而是静默跳过。排查时先确认每行首字符再看标号是否在元素表里出现。5. 避坑与排查CDL 描述里最容易翻车的几处5.1 首列多一个空格整行被当成说明语句现象编译通过但生成的表里少了某个门仿真结果和预期对不上。原因函数/连接语句要求第一列是空格说明语句第一列是*。如果某行开头多敲了一个空格再写NAND它仍然以空格开头看似合法但如果整行被误加了*或者首字符被编辑器改成制表符就会被归到说明语句分支直接忽略。解决用能显示不可见字符的编辑器检查首列确保门语句首字符是单个空格说明语句首字符是*继续语句首字符是。5.2 标号超过 8 字符导致截断现象两个不同标号在表里变成同一个名字前驱后继串线。原因name[9]只能放 8 个字符加结束符超长标号被strncpy截断后 8 位相同的标号撞名。解决命名时控制在 8 字符内或者在建表前加一道长度检查超长直接报错而不是截断。5.3 扇出分支被再次扇出现象编译器报错或表结构异常遍历时出现自环。原因FOUT的分支线不能作为另一个FOUT的输入这是语法层约束。有人为了表达多级扇出写成FOUT/A/B,C/再FOUT/B/D,E/直接违反规则。解决多级扇出拆成显式的门或者重新规划扇出点保证每个分支线只被驱动一次。5.4 XOR 只给一个输入现象编译报错提示输入个数不足。原因XOR 语句要求 n2单输入异或在语义上等于缓冲语法层直接拒绝。解决确认是否真的需要 XOR单输入场景改用直连或 NOT。5.5 IN/OUT 管脚名与内部标号不一致却用了简写现象管脚映射错位外部激励加到了错误的结点上。原因IN/inp1,…,inpk//的简写前提是所有管脚名与内部标号完全相同。一旦有一个不同简写就会让PIOT表里的name和ptr对不上。解决只要管脚名和内部标号不完全一致就写全IN/Pin…/inp…/形式不要图省事用简写。6. 设计取舍定长表、两遍扫描与“够用就好”的编译器CDL 这套设计放到今天看处处是约束8 字符标号、首列定语义、扇出不能级联、两遍扫描建表。但把这些约束放回它诞生的语境——用 C 写一个能在小型机上跑、把门级网表转成仿真器输入的前端——每一个选择都有明确的成本收益。定长name[9]省掉了字符串池和动态分配代价是命名空间被压到 8 字符首列定语义省掉了词法分析器代价是用户必须严格遵守缩进单值内联、多值外挂的fil/fol复用省掉了统一链表带来的指针开销和缓存不友好代价是建表必须两遍扫描禁止扇出级联省掉了图遍历时的环检测代价是表达能力受限。这些取舍的共同点是把复杂度从运行时推到书写时让编译器保持线性扫描和定长数组。如果今天要验证这套机制是否真的按预期工作我会做三件事拿 C17 的 CDL 描述跑一遍编译器检查*.tbl里 17 个结点的前驱后继是否和手算一致故意在门语句前加一个*看它是否被静默忽略把某个标号改成 9 个字符看是报错还是截断。这三步能直接暴露语法分流、错误处理和定长数组三个关键点的真实行为。我自己的习惯是每次拿到一份新的网表描述先不急着编译而是用脚本把每行的首字符和标号长度扫一遍把格式问题挡在建表之前。血泪经验是这类老式编译器的静默失败比报错更可怕报错至少告诉你哪行错了静默跳过只会让你在仿真结果对不上时回头翻半天。从那以后我每次处理这类定列格式的输入都强制先跑一遍格式预检再进编译器。希望帮到你。本文还有配套的精品资源点击获取