Verilog HDL 学习笔记(十二)| 第12章 用户自定义原语(UDP)

12.1 UDP 的基础知识

一、什么是 UDP?

UDP(User-Defined Primitive) 是 Verilog 允许用户自行定义的一种原语类型 ,其调用方式与 andor 等内置原语完全相同

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 中不能 包含 assignalwaysinitial(除了初始化)等语句,只能有状态表
规则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,即使另一个输入为 xz

复制代码
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 = 0clock = 1 时:q = d(透明)

  • clear = 0clock = 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      :  ?  :  -;    // - 表示保持不变
      endtable

    endprimitive

逐行分析:

表项 解释
? ? 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
使用缩写符号 ?rf 等让状态表更简洁
注意表项顺序 状态表从上到下匹配,优先级高的表项放在前面
电平敏感优先 在时序 UDP 中,电平敏感表项应放在边沿敏感表项之前
测试所有输入组合 用 Testbench 验证 UDP 是否覆盖了所有有效组合
相关推荐
天辛大师1 小时前
天辛大师浅谈AI时代,“学会学习“本身的内涵也需要被重新定义?
人工智能·深度学习·学习·启发式算法·量子计算
摇滚侠1 小时前
《SpringBoot 3:入门与应用实战》第 13 章 整合 MyBatis MyBatis 概述 阅读笔记 37
spring boot·笔记·mybatis
是隼人1 小时前
buuctf-pwn PWN1题解(学习过程持续更新)
c语言·学习·安全·pwn入门·ctf入门
HugoStudio_SWAN1 小时前
洛谷 P1321 单词覆盖还原——从蜡板上的密信到数字水印
c++·学习·程序人生·算法
春风解人意1 小时前
从零开始学习嵌入式P33----网络基础之HTTP
网络·学习·http
励志不掉头发的内向程序员1 小时前
【LibreCAD 2D架构】从鼠标点击到屏幕像素:LibreCAD绘图架构全链路解析之Action与命令系统
linux·开发语言·c++·qt·学习·系统架构
小道士写程序2 小时前
自然语言处理NLP - 第8章 词向量与神经网络:从手工特征到学习表示
神经网络·学习·自然语言处理
摇滚侠2 小时前
《SpringBoot 3:入门与应用实战》第 13 章 整合 MyBatis 整合 MyBatis 阅读笔记 38
spring boot·笔记·mybatis
m4Rk_2 小时前
【论文阅读】Agent 记忆机制(59):Synapse——让相关记忆沿情景—语义图被逐步激活
论文阅读·人工智能·学习·开源·github