12.1 UDP 的基础知识
一、什么是 UDP?
UDP(User-Defined Primitive) 是 Verilog 允许用户自行定义的一种原语类型 ,其调用方式与 and、or 等内置原语完全相同。
UDP 与模块(Module)的对比:
| 对比维度 | 模块(Module) | UDP(User-Defined Primitive) |
|---|---|---|
| 定义关键字 | module ... endmodule |
primitive ... endprimitive |
| 是否可实例化其他模块 | ✅ 可以 | ❌ 不可以(UDP内不能调用任何其他模块或原语) |
| 是否有端口列表 | 可有可无 | 必须有(至少1个输入+1个输出) |
| 是否可以建模时序 | ✅ 可以 | ✅ 可以(输出需声明为 reg) |
| 是否有内部变量 | ✅ 可以有(wire, reg, integer等) | ❌ 不可以(只能有输入、输出和状态表) |
是否需要 initial 初始化 |
视情况 | 时序UDP需要(初始化输出寄存器) |
| 实现方式 | 编译为逻辑网表 | 编译为查找表(LUT) |
| 主要用途 | 大型复杂设计 | 小而常用的功能模块(如特定译码器、锁存器) |
二、UDP 定义的组成部分
┌─────────────────────────────────────────────────────────────────────────────┐
│ UDP 定义的结构 │
│ │
│ primitive <UDP名> ( <输出端口>, <输入端口1>, <输入端口2>, ... ); │
│ │
│ // 1. 端口声明(output / input) │
│ output <输出端口>; │
│ input <输入端口1>; │
│ input <输入端口2>; │
│ ... │
│ │
│ // 2. 时序UDP特有:输出声明为 reg(可选) │
│ reg <输出端口>; │
│ │
│ // 3. 时序UDP特有:初始状态(可选) │
│ initial <输出端口> = <初始值>; │
│ │
│ // 4. 状态表(核心!以 table 开始,以 endtable 结束) │
│ table │
│ // <输入组合> : <当前状态> : <下一状态>; │
│ // 或 <输入组合> : <输出>; │
│ endtable │
│ │
│ endprimitive │
└─────────────────────────────────────────────────────────────────────────────┘
三、UDP 的定义规则(必须遵守)
| 规则编号 | 规则内容 |
|---|---|
| 规则1 | UDP 只能有 1 个 输出端口(标量,不能是向量) |
| 规则2 | UDP 可以有 多个 输入端口(每个都必须是标量,不能是向量) |
| 规则3 | 输出端口必须出现在端口列表的第一个位置 |
| 规则4 | 组合逻辑 UDP 的输出端口中不能 声明为 reg |
| 规则5 | 时序逻辑 UDP 的输出端口必须 声明为 reg |
| 规则6 | 时序逻辑 UDP 可以用 initial 语句初始化输出(可选) |
| 规则7 | UDP 不支持 inout(双向)端口 |
| 规则8 | UDP 不能处理 z 值 ,输入端的 z 值会被当作 x 处理 |
| 规则9 | UDP 中不能 包含 assign、always、initial(除了初始化)等语句,只能有状态表 |
| 规则10 | UDP 的状态表 是最核心的部分,以 table 开始,以 endtable 结束 |
12.2 表示组合逻辑的 UDP
一、基本语法
组合逻辑 UDP 的状态表 格式为:<输入1> <输入2> ... <输入N> : <输出>;
规则:
-
输入的顺序必须与端口列表中的顺序完全一致
-
输入和输出用
:分隔 -
每条表项以
;结束 -
状态表中必须列出所有能产生确定输出值的输入组合,否则未列出的组合输出为
x
二、示例1:自定义与门 udp_and
// ============================================================
// 文件名:udp_and.v
// 功能:用 UDP 实现与门
// ============================================================
primitive udp_and(out, a, b);
output out;
input a, b;
// ---- 状态表 ----
// 注意:输入的顺序必须和端口列表一致(a, b)
table
// a b : out
0 0 : 0;
0 1 : 0;
1 0 : 0;
1 1 : 1;
endtable
endprimitive
测试:
module test;
wire out;
reg a, b;
udp_and u1(out, a, b);
initial begin
$monitor("a=%b b=%b out=%b", a, b, out);
a=0; b=0; #5;
a=0; b=1; #5;
a=1; b=0; #5;
a=1; b=1; #5;
end
endmodule
仿真输出:
a=0 b=0 out=0
a=0 b=1 out=0
a=1 b=0 out=0
a=1 b=1 out=1
三、示例2:完整的或门 udp_or(处理 x 值)
与门的状态表没有覆盖 a=1, b=x 的情况。对于或门,如果任一输入为 1,输出应为 1,即使另一个输入为 x 或 z。
primitive udp_or(out, a, b);
output out;
input a, b;
table
// a b : out
0 0 : 0;
0 1 : 1;
1 0 : 1;
1 1 : 1;
// 处理 x 和 z
x 1 : 1; // 如果b为1,无论a是什么,输出为1
1 x : 1; // 如果a为1,无论b是什么,输出为1
// 其他未列出的组合输出为 x(如 0 x, x 0, x x等)
endtable
endprimitive
关键理解 :UDP 不能处理 z 值,输入的 z 会被自动当作 x 处理。所以上面的 1 x : 1 实际上也覆盖了 1 z : 1 的情况。
四、示例3:使用 ? 简化状态表(无关项)
? 符号表示 0、1 或 x,用于简化状态表中不关心输入值的情况。
primitive udp_or_with_any(out, a, b);
output out;
input a, b;
table
// a b : out
0 0 : 0;
1 ? : 1; // 如果a=1,b为任意值(0/1/x),输出都为1
? 1 : 1; // 如果b=1,a为任意值,输出都为1
// 注意:这两个表项的顺序很重要!
// 如果 ? 1 写在 1 ? 之前,a=1,b=1 时会匹配第一条,输出也是1,结果一样
endtable
endprimitive
匹配规则: UDP 状态表从上到下顺序匹配,第一个匹配的行生效。
五、示例4:四选一多路选择器(UDP 实现)
四选一多路选择器有 6 个输入 (4个数据 + 2个选择)和 1 个输出,适合用 UDP 实现。
// ============================================================
// 文件名:mux4_to_1_udp.v
// 功能:四选一多路选择器(UDP 实现)
// 说明:s1、s0 为选择信号,out 为输出
// ============================================================
primitive mux4_to_1_udp(out, i0, i1, i2, i3, s1, s0);
output out;
input i0, i1, i2, i3;
input s1, s0;
table
// i0 i1 i2 i3 s1 s0 : out
// 选择 s1=0, s0=0 → 输出 i0
0 ? ? ? 0 0 : 0;
1 ? ? ? 0 0 : 1;
// 选择 s1=0, s0=1 → 输出 i1
? 0 ? ? 0 1 : 0;
? 1 ? ? 0 1 : 1;
// 选择 s1=1, s0=0 → 输出 i2
? ? 0 ? 1 0 : 0;
? ? 1 ? 1 0 : 1;
// 选择 s1=1, s0=1 → 输出 i3
? ? ? 0 1 1 : 0;
? ? ? 1 1 1 : 1;
endtable
endprimitive
UDP 的优势 :你看上面的状态表,它完全覆盖了所有组合 。综合工具只需要把这段 UDP 编译成一个 查找表(LUT) ,实现非常高效。
12.3 表示时序逻辑的 UDP
一、时序 UDP 的特点
| 特点 | 说明 |
|---|---|
输出声明为 reg |
必须将输出端口声明为 reg 类型,以保存状态 |
initial 初始化 |
可选,用于设置输出的初始值 |
| 状态表格式不同 | <输入1> <输入2> ... <输入N> : <当前状态> : <下一状态>; |
| 可包含电平敏感和边沿敏感 | 状态表可以混合使用电平输入和边沿输入 |
- 符号表示保持 |
在输出的 下一状态 位置写 - 表示状态不变 |
时序 UDP 状态表格式:
table
// 输入组合(电平/边沿) : 当前状态 : 下一状态
<输入> : <当前状态> : <下一状态>;
endtable
二、电平敏感 UDP:简单锁存器
功能:
-
clear = 1时:q = 0(异步清零) -
clear = 0且clock = 1时:q = d(透明) -
clear = 0且clock = 0时:q保持原值// ============================================================
// 文件名:latch_udp.v
// 功能:带清零端的电平敏感锁存器(UDP 实现)
// ============================================================primitive latch_udp(q, d, clock, clear);
output q;
input d, clock, clear;// ---- 时序 UDP 必须声明输出为 reg ---- reg q; // ---- 初始化输出(可选)---- initial q = 1'b0; // ---- 状态表 ---- table // d clock clear : q : q+ // 清零条件(优先级最高) ? ? 1 : ? : 0; // 锁存数据(透明模式:clock=1) 1 1 0 : ? : 1; 0 1 0 : ? : 0; // 保持模式(clock=0) ? 0 0 : ? : -; // - 表示保持不变 endtableendprimitive
逐行分析:
| 表项 | 解释 |
|---|---|
? ? 1 : ? : 0; |
无论 d 和 clock 是什么,只要 clear=1,输出 q 变为 0 |
1 1 0 : ? : 1; |
clear=0, clock=1, d=1 时,q 变为 1 |
0 1 0 : ? : 0; |
clear=0, clock=1, d=0 时,q 变为 0 |
? 0 0 : ? : -; |
clear=0, clock=0 时,无论 d 是什么,q 保持不变 |
- 符号 :在时序 UDP 的状态表中,- 表示 "保持当前值不变"。它只能在时序 UDP 的输出部分使用。
三、边沿敏感 UDP:下降沿触发的 D 触发器
// ============================================================
// 文件名:edge_dff_udp.v
// 功能:带异步清零的下降沿触发 D 触发器(UDP 实现)
// ============================================================
primitive edge_dff_udp(q, d, clock, clear);
output q;
input d, clock, clear;
// ---- 输出声明为 reg ----
reg q;
// ---- 初始状态 ----
initial q = 1'b0;
// ---- 状态表 ----
table
// d clock clear : q : q+
// 异步清零(最高优先级)
? ? 1 : ? : 0;
// 忽略 clear 的下降沿(防止误触发清零)
? (10) 0 : ? : -;
// 在时钟下降沿锁存数据
1 (10) 0 : ? : 1;
0 (10) 0 : ? : 0;
// 忽略 clock 的其他边沿和变化
? (0?) 0 : ? : -;
? (1x) 0 : ? : -;
? (x1) 0 : ? : -;
// 当 clock 不变时,忽略 d 的变化
(??) ? 0 : ? : -;
endtable
endprimitive
边沿符号说明:
| 符号 | 含义 |
|---|---|
(10) |
从 1 到 0 的下降沿(负跳变) |
(01) |
从 0 到 1 的上升沿(正跳变) |
(1x) |
从 1 到 x 的变化 |
(x1) |
从 x 到 1 的变化 |
(0?) |
从 0 到 0/1/x 的变化(隐含上升沿) |
(??) |
任意变化 |
重要限制 :状态表中每行最多只能有一个输入发生边沿变化。以下写法是非法的:
(01)(10) 0 : ? : 1; // ❌ 非法!一行中有两个边沿跳变
四、边沿敏感 UDP 的优先级规则
当电平敏感输入项 和边沿敏感输入项 在同一个输入端口中同时匹配时,电平敏感的优先级更高。
┌─────────────────────────────────────────────────────────────────────────────┐
│ UDP 优先级规则 │
│ │
│ 在同一时钟边沿,如果: │
│ 1. 输入电平变化(如 clear=1) │
│ 2. 时钟边沿到达(如 posedge clock) │
│ │
│ 那么:电平敏感的匹配优先于边沿敏感的匹配 │
│ 因为:电平控制(如复位)应该是最高优先级的控制信号 │
└─────────────────────────────────────────────────────────────────────────────┘
五、UDP 缩写符号汇总表
| 缩写符 | 展开 | 含义 | 允许位置 |
|---|---|---|---|
? |
0, 1, x |
任意值(无关项) | 输入部分 |
b |
0, 1 |
逻辑值(不含x) | 输入部分(极少用) |
- |
保持不变 | 维持原值 | 只能用于时序UDP的输出部分 |
r |
(01) |
上升沿 | 输入部分(边沿) |
f |
(10) |
下降沿 | 输入部分(边沿) |
p |
(01), (0x), (x1) |
可能的上升沿 | 输入部分(边沿) |
n |
(10), (1x), (x0) |
可能的下降沿 | 输入部分(边沿) |
* |
(??) |
任意变化 | 输入部分(边沿) |
使用缩写符简化 D 触发器 UDP:
primitive edge_dff_simple(q, d, clock, clear);
output q;
input d, clock, clear;
reg q;
initial q = 1'b0;
table
// d clock clear : q : q+
? ? 1 : ? : 0;
? f 0 : ? : -; // 忽略 clear 下降沿
1 f 0 : ? : 1; // 下降沿锁存1
0 f 0 : ? : 0; // 下降沿锁存0
? (0?) 0 : ? : -; // 忽略上升沿
(??) ? 0 : ? : -; // 忽略 d 变化
endtable
endprimitive
f 是 (10) 的简写 ,r 是 (01) 的简写,让状态表更加简洁易读。
12.4 综合实例:T 触发器(UDP 实现)
一、T 触发器功能
T 触发器(Toggle Flip-Flop) 在时钟边沿到来时翻转输出:
-
T = 0:保持 -
T = 1:翻转(0→1 或 1→0)
二、UDP 实现
// ============================================================
// 文件名:tff_udp.v
// 功能:带异步清零的下降沿触发 T 触发器(UDP 实现)
// ============================================================
primitive tff_udp(q, clk, clear);
output q;
input clk, clear;
reg q;
initial q = 1'b0;
table
// clk clear : q : q+
// 异步清零(最高优先级)
? 1 : ? : 0;
// 忽略 clear 的下降沿
f 0 : ? : -;
// 时钟下降沿翻转
f 0 : 1 : 0; // 当前为1,翻转为0
f 0 : 0 : 1; // 当前为0,翻转为1
// 忽略时钟上升沿
(0?) 0 : ? : -;
endtable
endprimitive
三、四位脉动计数器(调用 UDP)
module ripple_counter(q, clk, clear);
output [3:0] q;
input clk, clear;
// 实例化4个T触发器
tff_udp tff0(q[0], clk, clear);
tff_udp tff1(q[1], q[0], clear);
tff_udp tff2(q[2], q[1], clear);
tff_udp tff3(q[3], q[2], clear);
endmodule
12.5 UDP 设计指南
一、何时选择 UDP vs 模块
┌─────────────────────────────────────────────────────────────────────────────┐
│ UDP vs 模块 选择指南 │
│ │
│ 是否需要超过 1 个输出端口? │
│ ├── 是 → 必须用 模块(Module) │
│ └── 否 → 继续判断 │
│ │
│ 是否需要多个输入端口(超过 10 个)? │
│ ├── 是 → 建议用 模块(UDP查找表内存呈指数增长) │
│ └── 否 → 继续判断 │
│ │
│ 功能是否复杂(包含大量算术运算)? │
│ ├── 是 → 用 模块(UDP 只能做查找表) │
│ └── 否 → 可以用 UDP │
│ │
│ 是否需要在设计中多次重复使用这个功能? │
│ ├── 是 → UDP 非常适合(调用方便,像门一样) │
│ └── 否 → 用模块或直接写逻辑 │
└─────────────────────────────────────────────────────────────────────────────┘
二、UDP 的内存考量
UDP 在综合和仿真时被实现为查找表(LUT) 。查找表的条目数 = 2^N(N 为输入端口数):
| 输入数量 | 查找表条目数 | 内存需求 |
|---|---|---|
| 2 | 4 | 很小 |
| 4 | 16 | 很小 |
| 6 | 64 | 可接受 |
| 8 | 256 | 可接受 |
| 10 | 1024 | 较大 |
| 16 | 65536 | 非常大! |
警示 :UDP 的输入端口不能太多(通常不超过 10 个),否则内存需求会指数级增长。对于超过 10 个输入的设计,使用模块实现更合适。
三、UDP 编写的最佳实践
| 实践 | 说明 |
|---|---|
| 完整列出状态表 | 尽量覆盖所有输入组合,否则未列出的组合输出为 x |
| 使用缩写符号 | ?、r、f 等让状态表更简洁 |
| 注意表项顺序 | 状态表从上到下匹配,优先级高的表项放在前面 |
| 电平敏感优先 | 在时序 UDP 中,电平敏感表项应放在边沿敏感表项之前 |
| 测试所有输入组合 | 用 Testbench 验证 UDP 是否覆盖了所有有效组合 |