5.2 用户定义原语 UDP:真值表造单元 本节摘要:用户定义原语用一张真值表定义电路单元的行为,组合 UDP 按输入查表出输出,时序 UDP 还能响应边沿、内部保存状态。它是语言里唯一的「自定义基本单元」机制,能力有限但定位独特:建模特殊单元与加速仿真。 门原语清点完,这一节看语言的自留地:用户定义原语(User Defined Primitive)。当内置门不够用、又不想动用完整模块时,UDP 允许你用真值表「注册」一个新单元——它像门原语一样被实例化,却按你写的表执行。 语法骨架:primitive 加 table UDP 的语法独立成章:primitive 声明、端口声明、table 真值表、endprimitive 收尾。
本节摘要:用户定义原语用一张真值表定义电路单元的行为,组合 UDP 按输入查表出输出,时序 UDP 还能响应边沿、内部保存状态。它是语言里唯一的「自定义基本单元」机制,能力有限但定位独特:建模特殊单元与加速仿真。
门原语清点完,这一节看语言的自留地:用户定义原语(User Defined Primitive)。当内置门不够用、又不想动用完整模块时,UDP 允许你用真值表「注册」一个新单元——它像门原语一样被实例化,却按你写的表执行。
UDP 的语法独立成章:primitive 声明、端口声明、table 真值表、endprimitive 收尾。组合 UDP 的完整示例——一个二选一选择器:
primitive mux2_udp(y, sel, a, b); output y; // 输出必须声明在最前,且只有一个 input sel, a, b; table // sel a b : y 0 0 ? : 0 ; 0 1 ? : 1 ; 1 ? 0 : 0 ; 1 ? 1 : 1 ; x 0 0 : 0 ; x 1 1 : 1 ; endtable endprimitive
真值表的读法:每行「输入序列 : 输出」,列顺序与端口声明顺序一致(输出在冒号右侧)。问号是通配符,匹配 0、1、x;想匹配 z 要显式写。表里特意补了「sel 未知但 a、b 相同」的情形——UDP 的表是穷举语义:查不到行的输入组合,输出就是 x。这是 UDP 与模块建模的重大差别:模块里没写的分支由逻辑结构决定,UDP 里没写的行就是未知。
UDP 还能建模时序单元:输出声明为 reg,真值表多一列「当前状态」,输入栏里可以写边沿记号。用 UDP 写一个带异步复位的 D 触发器:
primitive dff_udp(q, clk, rst_n, d); output reg q; input clk, rst_n, d; table // clk rst_n d : q现态 : q次态 ? 0 ? : ? : 0 ; // 异步复位优先 n ? ? : ? : - ; // 负沿:状态保持 p 1 0 : ? : 0 ; // 正沿采样 p 1 1 : ? : 1 ; p x ? : ? : x ; // 沿上复位未知:输出未知 endtable endprimitive
边沿记号只有四种:p 正沿(0、x 到 1 的跳变)、n 负沿、* 任意跳变、+ 无跳变。冒号分隔「现态」列,最后一列是次态;短横线表示保持现态不变。表的优先级靠行序实现——复位行在前、边沿行在后,从上到下匹配,命中即止。注意这张表刻意没有穷尽 rst_n 为 x 的全部情形:未覆盖的组合输出 x,恰好提醒建模者「复位未知的时段行为未知」,这与真实触发器的物理行为一致。
| 维度 | 表现 |
|---|---|
| 输出数量 | 只能一个 |
| 输入数量 | 组合约十来个封顶,时序更少 |
| 多驱动 | 不支持 |
| 延迟建模 | 实例化时挂,行为与门原语一致 |
| 仿真性能 | 内核查表执行,远快于模块级建模 |
| 综合支持 | 绝大多数综合工具不认 |
能力清单摆出来,定位就清楚了:UDP 是仿真性能工具与单元建模工具。典型用途是建模工艺库单元(很多标准单元库的仿真模型就是 UDP 写的)、给测试台造轻量参考单元、以及把大规模查表逻辑做成 UDP 加速仿真。把它当 RTL 设计设施是错觉——综合器不认它,写了等于白写。
案例展开:用 UDP 给老存储器建仿真模型。 背景:某项目沿用一款老式存储器芯片,厂商只给真值表没有仿真模型,用完整模块建模又拖慢整体仿真速度。操作:把手册真值表逐行转译成时序 UDP——片选、写使能、地址、数据进表,输出 reg 存上次读出的值;手册标注「不允许」的输入组合显式补「输出 z」行;实例化时挂上手册给的访问延迟。结果:存储器行为与手册逐条吻合,仿真速度较模块版模型明显提升,整个回归流程提速。解读:UDP 的穷举语义在这里反而是优点——表上没有的输入组合输出 x,恰好暴露了测试激励里「按手册不允许的时序访问存储器」的错误用例;模块建模会把这种非法访问「合理化」,把 bug 藏进模型里。变式:更大的表超过输入数量限制时,按功能分段拆成几个 UDP 再组合,或退回模块建模加查表数组——工具选型永远是性能与表达力的折中。
写 UDP 的坑几乎全在表上。第一条:显式处理 x 与 z——输入可能为未知的场合(复位释放前后、上电初期)必须有对应行,否则输出无缘无故变 x,排查极费时。第二条:非法组合显式给安全输出,别依赖「查不到出 x」的默认行为替你挡非法输入——挡得住是运气,挡不住是事故。第三条:表行按优先级排列并加注释——时序 UDP 的行序就是优先级,复位行、边沿行、保持行的次序错了,行为天差地别;每行尾部注明它处理什么情形,半年后还有人看得懂。
还有一个容易忽略的细节:UDP 实例化的端口用位置关联,没有命名关联——UDP 不是模块,语法待遇不同。端口写错顺序时编译器按列错位查表,不报错、结果错,看起来像逻辑 bug,实则是端口顺序 bug。
值得,但要摆正动机。直接用 UDP 写设计的日子已经过去,学它的回报在三处。其一,读得懂存量资产:工艺库仿真模型、老项目的自建单元库里 UDP 随处可见,看不懂表就看不懂模型行为。其二,理解仿真器:UDP 是最接近仿真内核的建模层,真值表、边沿记号这些概念在别处学不到这么贴身。其三,特定场景的性能工具:超大查表逻辑、专用译码单元,UDP 模型比行为级模型快得多,验证瓶颈在仿真速度时它是备选武器。
学习路径建议反着走:先读一个现成的库单元 UDP(触发器类最典型),对照手册时序参数理解每一行表的含义,再自己写一个简单组合单元。真值表建模的思维方式——穷举、显式、无中间逻辑——本身就是一种有用的设计训练,它逼你把「未定义行为」当作一等公民来对待,这个习惯在写任何 RTL 时都有用。
自定义单元讲完,下一节处理多个人抢一根线的场景:三态门与多驱动总线。