第 19 章 Verilog/SystemVerilog 的硬件表达
1. 本章要解决的问题
前 18 章一直用电路图、逻辑方程、状态表和寄存器传输描述硬件。真正交给数字设计工具时,还需要一种精确、可检查、可转换为门级结构的文本表达。Verilog 和 SystemVerilog 就承担这个任务。
最容易犯的错误,是把 HDL
当作普通程序设计语言:看到两行代码,便认为第一行先执行、第二行后执行;看到
for
循环,便认为硬件要花多个周期逐次运行;看到变量,便认为它只是软件内存中的一个数。
硬件描述语言(hardware description language, HDL)中的代码首先描述结构、连接和随时间发生的硬件行为。工具可能用它做仿真,也可能把其中可综合的部分转换为门、触发器、MUX 和存储器。本章回答十个问题:
- HDL 代码与实际硬件是什么关系?
- 模块、端口和实例分别表示什么?
- 一位信号、位向量和常量怎样书写?
- 连续赋值与组合过程分别适合描述什么?
- 为什么组合过程漏掉赋值会推断锁存器?
- 怎样描述触发器、寄存器、使能和复位?
- 阻塞赋值
=与非阻塞赋值<=为什么不能随意互换? - 参数、局部参数和数组怎样表达可复用硬件?
- 怎样把存储器和 FSM 写成清楚的 RTL?
- 怎样在综合前发现位宽、多驱动、组合环和接口语义错误?
本章使用 SystemVerilog 的基础可综合子集,文件扩展名写作
.sv。经典 Verilog 的核心思想相同,但 SystemVerilog 的
logic、always_comb 和 always_ff
能更清楚地表达设计意图,也便于工具检查。
2. 与第 17、18 章的联系
第 17 章已经建立寄存器传输级(RTL)的硬件思考方式。例如:
\[ R^{+}=ld_R?D:R. \]
它表示有效时钟边沿到来时,ld_R=1 便把 D
写入寄存器 R,否则保持。对应的 SystemVerilog 可以写成:
always_ff @(posedge clk) begin
if (ld_r)
r <= d;
end第 18 章的 D × W bit 存储器则可以写成一个具有
D 个元素、每个元素宽 W bit
的数组。地址选择数组元素,写过程规定哪个边沿更新它。
本章不会让代码替代电路理解。每读到一段 RTL,都要继续追问:
- 哪些信号只是连线?
- 哪些表达式形成组合逻辑?
- 哪些对象在时钟边沿保存状态?
- 每个状态元件的 D 输入来自哪里?
- 是否存在没有定义清楚的保持、冲突或位宽行为?
3. 前置知识快速检查
- 2 选 1 MUX 的输出怎样由选择信号决定?
- 组合逻辑的输出能否依赖未声明的历史状态?
- 两个 D 触发器在同一时钟边沿更新时,右侧表达式读取旧值还是新值?
- 同步复位与异步复位分别在什么事件下改变状态?
16 × 8 bit存储器的深度、字宽和地址位数分别是多少?
答案是:选择一路输入;不能;读取边沿前的旧值;同步复位在有效时钟边沿生效,异步复位可由复位有效事件直接生效;深度 16、字宽 8 bit、地址 4 bit。若第 1、2 题不熟,回看第 8、10 章;若第 3、4 题不熟,回看第 12 章;若第 5 题不熟,回看第 18 章。
4. 学习目标
完成本章后,你应能够:
- 把模块、端口、实例和连线对应到硬件层次结构;
- 正确声明一位信号、位向量、参数和基本数组;
- 使用
assign或always_comb描述组合逻辑; - 检查组合过程是否对输出完整赋值;
- 使用
always_ff描述寄存器、使能以及同步或异步复位; - 从非阻塞赋值读出同一边沿的并行状态更新;
- 区分组合过程中的阻塞赋值和时序过程中的非阻塞赋值;
- 解释参数化代码展开后对应多少硬件;
- 描述简单异步读、同步写存储器;
- 写出小型两过程 FSM;
- 识别位宽截断、隐式有符号转换、多驱动、锁存器和组合环风险;
- 画出一段基础 RTL 可能综合出的寄存器、MUX 和组合逻辑。
5. 第一原则:RTL 描述的是并行硬件
观察图 19-1 时,先看左侧三条语句,再看右侧三块硬件。三条连续赋值没有共享一位“处理器”轮流执行;它们各自形成逻辑,并在输入变化后并行传播。
图 19-1 三条连续赋值同时存在于芯片上,代码的上下顺序不表示跨周期执行顺序
例如:
assign n = ~a;
assign p = b & c;
assign y = sel ? n : p;这段代码对应:
- 一个反相器产生
n; - 一个与门产生
p; - 一个 MUX 在
n与p之间选择y。
输入改变后,各条组合路径按自身传播延迟响应。代码写在前面,不会自动获得更早的时钟周期。
立即检查
如果把前两条 assign
的书写顺序交换,硬件功能会改变吗?
不会。它们是并行的连续驱动关系,交换文本顺序不改变连接。
6. 模块是带端口的硬件边界
模块(module)把一块硬件的外部接口和内部实现包在一起。下面是 1 bit 的 2 选 1 MUX:
module mux2_1bit (
input logic a,
input logic b,
input logic sel,
output logic y
);
assign y = sel ? b : a;
endmodule读这段代码时按以下顺序:
- 模块名是
mux2_1bit; a、b、sel是进入模块的输入端口;y是离开模块的输出端口;- 条件运算符规定
sel=1选择b,sel=0选择a; - 整个模块没有时钟和状态元件,因此是组合逻辑。
图 19-2 把模块定义、模块实例与实体硬件分开。定义像一张电路蓝图;每写一个实例,设计层次中就出现一份对应硬件。
图 19-2 模块定义规定接口和内部关系,两个实例对应两份同时工作的硬件
在上层模块中实例化两次:
logic y0, y1;
mux2_1bit u_mux0 (
.a (d0),
.b (d1),
.sel (s0),
.y (y0)
);
mux2_1bit u_mux1 (
.a (d2),
.b (d3),
.sel (s1),
.y (y1)
);.a(d0) 表示实例端口 a 连接上层信号
d0。命名端口连接比只按位置排列更容易检查,也不怕端口声明顺序改变。
例题 1:把模块实例还原为硬件
某上层模块实例化三个 mux2_1bit,前两个分别选择
(a,b) 和
(c,d),第三个在前两个输出之间选择。这个结构包含多少个
MUX?
每个实例对应一个 MUX,因此共有三个 MUX。若第三个选择信号作为最高层选择位,前两个共享另一个选择位,这三个 MUX 可以组成 4 选 1 MUX。代码复用的是模块定义,实例化后并不会让三次选择共用同一个实体。
变式:如果在 for 生成结构中实例化 8
次,展开后就是 8 份硬件。生成循环在
elaboration(设计展开)阶段决定结构数量,不表示运行时花 8 个周期。
7. logic
表示四状态逻辑对象,不自动表示寄存器
SystemVerilog 常用 logic 声明信号:
logic valid;
logic [7:0] data;
logic [15:0] sum;logic 可以在仿真中取四种状态:
0:逻辑 0;1:逻辑 1;x:未知,表示当前无法确定是 0 还是 1;z:高阻,表示当前驱动器没有主动驱动该网络。
logic [7:0] data 是一个 8 bit
打包数组(packed array),位编号从 7 到 0:
data[7]是最高位;data[0]是最低位;data[7:4]是高 4 bit;data[3:0]是低 4 bit。
声明为 logic
并不决定它综合为导线还是寄存器。决定因素是驱动方式:
- 由连续赋值或完整组合过程产生,通常对应组合逻辑输出;
- 在边沿触发的
always_ff中赋值,对应触发器或寄存器; - 在组合过程中未覆盖所有路径而需要保留旧值,会推断锁存器。
wire 与
logic 的基础选择
经典 Verilog 用 wire 表示网络连接,用 reg
表示过程赋值对象。reg
这个名字容易让初学者误以为一定产生寄存器。SystemVerilog 通常用
logic 统一表达单驱动设计信号,并让
always_comb、always_ff 表明过程意图。
基础 RTL 可以采用这条规则:模块内部和端口优先使用
logic;一个信号只由一个明确来源驱动。真正需要多驱动网络或三态总线时,再使用相应网络类型并单独定义解析规则。
8. 位宽是硬件数量,不能当作排版细节
下面两个声明对应不同硬件:
logic [7:0] a; // 8 bit
logic [8:0] b; // 9 bit加法尤其要关注结果位宽。两个 8 bit 无符号数相加,完整结果可能需要 9 bit:
logic [7:0] a, b;
logic [8:0] sum_full;
assign sum_full = {1'b0, a} + {1'b0, b};{1'b0, a} 是拼接,把一个 0 放在 a
的高位,明确把操作数扩展到 9 bit。若只写到 8 bit
输出,最高进位会被截断。这可能正是模 \(2^8\)
运算的规格,也可能是错误;必须由接口契约决定。
常量的尺寸
常用写法为 位数'进制数值:
8'b1010_0110
8'hA6
8'd166
1'b0
4'bx下划线只用于分组阅读,不改变数值。'0
表示按左侧目标宽度填满 0,适合复位:
q <= '0;有符号运算还受 signed
属性影响。基础设计中,除非规格确实要求补码有符号运算,优先明确使用无符号向量;需要有符号数时写出
logic signed [W-1:0],并检查两侧宽度和符号扩展。
立即检查
logic [3:0] y; assign y = 4'b1111 + 4'b0001;
保存的结果是什么?
4 bit 输出只保留低 4 bit,得到
0000。若还需要进位,应把结果和操作数显式扩展为 5 bit。
9. 连续赋值:直接描述组合连接
assign 适合表达一个清楚的组合方程:
assign y_and = a & b;
assign y_xor = a ^ b;
assign y_mux = sel ? d1 : d0;
assign equal = (x == y);右侧任一输入变化,仿真器都会重新计算左侧。综合时,这些关系转换为门、比较器、MUX 或连线。
运算符要分清:
| 类别 | 常用运算符 | 含义 |
|---|---|---|
| 按位 | ~ & \| ^ |
对向量每一位分别运算 |
| 逻辑 | ! && \|\| |
把操作数当作真假条件 |
| 关系 | == != < <= > >= |
比较并得到一位条件 |
| 移位 | << >> |
左移或逻辑右移 |
| 算术 | + - * |
算术运算,结果宽度需检查 |
| 选择 | cond ? a : b |
二选一,通常对应 MUX |
| 拼接 | {a,b} |
把多个位段拼成更宽向量 |
这里的 <=
有两种上下文:在表达式中是“小于等于”比较;在过程赋值左侧之后是非阻塞赋值符号。读代码时要结合位置判断。
10.
always_comb:描述有分支的组合逻辑
复杂组合逻辑用 always_comb 更清楚:
always_comb begin
if (sel)
y = b;
else
y = a;
endalways_comb 告诉工具:
- 这是组合逻辑意图;
- 过程读取的信号变化时应重新求值;
- 过程内被写对象不应再由其他过程驱动;
- 工具应检查是否存在组合描述问题。
在组合过程中通常使用阻塞赋值
=。它使同一过程内后续语句立即看到前面刚算出的临时结果,适合按计算依赖顺序写组合算法。
例题 2:4 bit 小型 ALU
规格如下:op=00 加法,01
减法,10 按位 AND,11 按位 XOR。结果只保留 4
bit。
module alu4 (
input logic [3:0] a,
input logic [3:0] b,
input logic [1:0] op,
output logic [3:0] y,
output logic zero
);
always_comb begin
case (op)
2'b00: y = a + b;
2'b01: y = a - b;
2'b10: y = a & b;
default:y = a ^ b;
endcase
end
assign zero = (y == 4'b0000);
endmodule硬件上,四种运算结果进入选择网络,op
决定输出。zero 是对最终 y
的组合比较。default 覆盖
op=11,所以四种二进制操作码都有定义。
若还要输出完整加法进位,当前 4 bit 的 y 不够,应增加
carry,并在加法分支使用 5 bit
临时结果。不要指望赋值目标在结果已经截断后恢复丢失的进位。
变式:若 op 增至 3 bit,而
case 仍只覆盖四种编码,就必须规定其他编码的输出,例如令
y='0 并产生 illegal_op。
11. 组合逻辑必须对输出完整赋值
考虑:
always_comb begin
if (en)
y = d;
end当 en=0 时,代码没有规定 y 的当前值。为了让
y
保留上次结果,硬件必须加入状态保持,这会推断锁存器。问题不在语法,而在规格缺了一条组合路径。
图 19-3 对比完整 MUX 与意外锁存器。先看
en=0:左边明确选择常量 0;右边只能反馈旧
y。
图 19-3 组合过程必须为每条可达路径规定输出;“什么都不写”意味着需要保存旧值
若规格是 en=0 输出 0,应写成:
always_comb begin
if (en)
y = d;
else
y = '0;
end也可以先给默认值,再覆盖特殊条件:
always_comb begin
y = '0;
if (en)
y = d;
end例题 3:找出锁存器来源
always_comb begin
case (sel)
2'b00: y = a;
2'b01: y = b;
2'b10: y = c;
endcase
endsel=11 时没有给 y 赋值,因此 y
必须保持旧值,形成锁存器。修正方法是增加:
default: y = d;或者在 case 前写 y=d;
作为默认值。选择哪种默认行为由规格决定,不能为了消除警告随意填一个值。
变式:若设计确实需要电平敏感锁存器,应明确使用
always_latch
表达意图。基础同步设计通常优先使用边沿触发寄存器。
12. 组合过程中的阻塞赋值
下面的组合过程先计算中间量,再计算输出:
always_comb begin
temp = a & b;
y = temp | c;
end阻塞赋值 = 让第二行立即读到本次刚算出的
temp。对应逻辑方程是:
\[ y=(a\cdot b)+c. \]
在同一组合过程中反过来写:
always_comb begin
y = temp | c;
temp = a & b;
end虽然有些工具会根据组合敏感关系重复求值并最终稳定,但这种写法破坏了清楚的数据依赖顺序,也容易产生仿真与理解问题。组合过程按“先算依赖,再算使用者”的顺序书写。
13.
always_ff:描述边沿触发状态更新
一个带同步复位和使能的 8 bit 寄存器:
module reg8_en (
input logic clk,
input logic rst,
input logic en,
input logic [7:0] d,
output logic [7:0] q
);
always_ff @(posedge clk) begin
if (rst)
q <= '0;
else if (en)
q <= d;
end
endmodule它表达三个优先级:
- 只在
clk上升沿检查同步控制; rst=1时清零,优先于en;rst=0且en=1时装载d;- 两者均不成立时没有赋值,边沿触发寄存器自然保持。
组合过程漏赋值会推断锁存器;时序过程中某个边沿分支不赋值则表示寄存器保持。两者的区别来自过程触发方式和所表达的状态元件。
14. 非阻塞赋值表达“同一边沿一起更新”
时序过程通常使用非阻塞赋值 <=:
always_ff @(posedge clk) begin
q0 <= d;
q1 <= q0;
q2 <= q1;
end在一个上升沿:
- 右侧的
d、q0、q1都从边沿前状态取样; - 三个新值在该边沿的更新阶段一起提交;
- 因此数据每个周期只前进一级。
观察图 19-4:边沿到来时先拍下所有右侧旧值,再更新所有 Q。文本上下顺序不会把数据在一个边沿穿过三级寄存器。
图 19-4 非阻塞赋值先读取边沿前状态,再并行提交新状态,符合真实寄存器同时采样
例题 4:两个寄存器交换数据
边沿前 a=8'h3C、b=8'hA5:
always_ff @(posedge clk) begin
a <= b;
b <= a;
end边沿后
a=8'hA5、b=8'h3C。两条右侧都读取旧值,所以完成交换。
若在同一时序过程中错误地使用阻塞赋值:
a = b;
b = a;第二行会读到第一行刚写入的 a,仿真结果变为两者都等于旧
b,无法表达两个真实触发器的并行交换。
基础编码规则:组合过程用
=;边沿触发状态更新用
<=。这条规则既表达硬件时间语义,也减少竞争与仿真顺序依赖。
15. 同步复位与异步复位的代码差别
同步高有效复位:
always_ff @(posedge clk) begin
if (rst)
q <= '0;
else
q <= d;
end只有上升沿到来时才检查 rst。
异步高有效复位:
always_ff @(posedge clk or posedge rst) begin
if (rst)
q <= '0;
else
q <= d;
endrst 从 0 变 1
本身就是过程事件,因此无需等待时钟即可清零;解除复位后的第一次正常采样仍由时钟边沿完成。
复位极性必须与端口命名一致。低有效异步复位通常写作
rst_n:
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= '0;
else
q <= d;
end第 15 章讨论过“异步置位、同步释放”的系统策略。RTL 模板只能表达局部触发器行为;复位树、释放同步器和工艺库支持还要在系统与实现阶段统一规划。
16. 时序过程中的优先级会形成 MUX 链
always_ff @(posedge clk) begin
if (rst)
q <= '0;
else if (load)
q <= d;
else if (inc)
q <= q + 1'b1;
end这不是三件互不相关的事。它形成明确优先级:复位最高,装载其次,加一最低。寄存器 D 端前需要组合选择逻辑。
若规格要求 load 与 inc 同时为 1
时装载,那么上述顺序正确。若要求装载后再加一,应直接写出
q <= d + 1'b1
的分支。代码顺序体现的是硬件选择优先级,必须来自规格。
立即检查
若
rst=0、load=1、inc=1,上面代码选择什么?
选择 d,因为 else if
只执行第一个成立分支。
17. 参数化模 N 计数器
例题 5:用参数决定计数范围与状态位宽
参数化让同一模块定义适配不同规模。下面的计数器从 0 数到
N-1 后回到 0:
module modn_counter #(
parameter int unsigned N = 10,
localparam int unsigned W = (N <= 1) ? 1 : $clog2(N)
) (
input logic clk,
input logic rst,
input logic en,
output logic [W-1:0] count,
output logic terminal
);
assign terminal = en && (count == N-1);
always_ff @(posedge clk) begin
if (rst)
count <= '0;
else if (en) begin
if (count == N-1)
count <= '0;
else
count <= count + 1'b1;
end
end
endmoduleparameter
是实例化时可改的设计常量。localparam
是模块内部派生常量,不允许实例从外部随意覆盖。$clog2(N)
返回覆盖 N 个编码所需的向上取整对数;N<=1
的分支保证位宽至少为 1。
若 N=10,则 W=4,计数器物理上是 4
个状态位加比较与加一逻辑。位模式 10~15
没有正常计数含义。若需要从任意非法状态自动恢复,可增加
count >= N-1
的策略;基础版本假定复位后从合法状态运行。
terminal
被定义为“本周期允许计数且当前即将回绕”,它是组合状态信号。若接口要求寄存一个完整周期的完成脉冲,则应再用触发器保存。
变式:N=16 时 W=4,所有 4
bit 编码均合法;比较器仍可保留,综合工具可能根据常量关系优化逻辑。
18. 参数化不是运行时改变电路宽度
实例化:
modn_counter #(.N(60)) u_sec (...);
modn_counter #(.N(1000)) u_ms (...);设计展开时,工具分别计算两个实例的位宽并生成两套硬件。芯片运行后,N
不是普通输入端口,不能在每个周期改变计数范围。
这一区分很重要:
- 参数决定制造前/综合前的结构配置;
- 输入信号决定运行时的当前行为。
如果计数终点需要软件运行时配置,就应把终点做成寄存器输入,而不是参数。
19. 打包数组与非打包数组
下面是第 18 章存储组织在 SystemVerilog 中的常见写法:
logic [7:0] mem [0:15];图 19-5 从中括号位置读结构:变量名左边的 [7:0]
是每个元素内部的 8 bit 打包向量;变量名右边的 [0:15] 表示
16 个独立元素。
图 19-5 logic [7:0] mem [0:15] 表示 16
个可索引数据字,每个字由 8 个连续位组成
访问方式:
mem[3] // 第 3 个 8 bit 数据字
mem[3][0] // 第 3 个数据字的最低位
mem[3][7:4] // 第 3 个数据字的高半字节交换两个维度的位置会改变含义。声明数组时先用自然语言读一遍:“这是 16 个元素,每个元素 8 bit”。
20. 存储器的 RTL 表达
例题 6:异步读、同步写的小型存储器
module simple_mem #(
parameter int unsigned D = 16,
parameter int unsigned W = 8,
localparam int unsigned AW = (D <= 1) ? 1 : $clog2(D)
) (
input logic clk,
input logic we,
input logic [AW-1:0] raddr,
input logic [AW-1:0] waddr,
input logic [W-1:0] wdata,
output logic [W-1:0] rdata
);
logic [W-1:0] mem [0:D-1];
assign rdata = mem[raddr];
always_ff @(posedge clk) begin
if (we)
mem[waddr] <= wdata;
end
endmodule这段代码表达:
raddr变化后,rdata经组合路径更新,所以是异步读;- 只有时钟上升沿且
we=1时,mem[waddr]更新,所以是同步写; - 数据阵列没有复位,仿真初始内容通常为未知,实际实现的上电内容取决于目标工艺和初始化方式;
- 当读写同一地址时,RTL 仿真与目标存储资源可能具有特定冲突行为,接口规格应明确旧值、新值或旁路策略。
若接口要求写周期立即从读口看到新数据,可以显式加入旁路:
assign rdata = (we && (waddr == raddr)) ? wdata : mem[raddr];这会增加地址比较器和读数据 MUX。它不是一条“免费”的软件判断。
观察图
19-6,把每段代码对应到端口与硬件:数组对应存储主体,always_ff
对应写端口的边沿更新,assign
对应读选择网络,旁路条件对应比较器和 MUX。
图 19-6 异步读形成地址到数据的组合路径,同步写在时钟边沿更新选中数据字,旁路需要额外比较与选择硬件
变式:同步读。把读数据也放入
always_ff:
always_ff @(posedge clk) begin
if (re)
rdata <= mem[raddr];
if (we)
mem[waddr] <= wdata;
end此时 rdata
是寄存输出,地址在边沿被接受。读写同址行为仍需结合 RTL
语义、综合推断结果和目标存储宏规格检查。
21. 两过程 FSM:状态寄存器与下一状态逻辑分开
下面是一个最小的 IDLE → RUN → DONE → IDLE 控制器:
module controller (
input logic clk,
input logic rst,
input logic start,
input logic terminal,
output logic count_en,
output logic done
);
typedef enum logic [1:0] {
IDLE,
RUN,
DONE
} state_t;
state_t state, next_state;
always_ff @(posedge clk) begin
if (rst)
state <= IDLE;
else
state <= next_state;
end
always_comb begin
next_state = state;
count_en = 1'b0;
done = 1'b0;
case (state)
IDLE: begin
if (start)
next_state = RUN;
end
RUN: begin
count_en = 1'b1;
if (terminal)
next_state = DONE;
end
DONE: begin
done = 1'b1;
next_state = IDLE;
end
default: begin
next_state = IDLE;
end
endcase
end
endmodule这段代码直接对应第 16、17 章的结构:
state在always_ff中更新,是状态寄存器;next_state、count_en、done在always_comb中计算,是组合逻辑;- 组合过程开头先给默认值,保证所有路径都有定义;
default让未列出的状态编码回到IDLE;done只由当前DONE状态决定,是 Moore 输出。
枚举(enumeration)用状态名替代裸二进制常量,提高可读性。具体状态编码可由工具选择或按声明处理;需要固定编码时应明确给每个枚举项赋值。
22. for
循环通常描述复制的硬件
一个 8 bit 按位异或:
always_comb begin
for (int i = 0; i < 8; i++) begin
y[i] = a[i] ^ b[i];
end
end循环边界在设计展开或综合时可确定,因此对应 8
个并行异或门,不是一个异或门运行 8 个周期。写成
assign y=a^b; 更简洁,硬件含义相同。
若确实要用一个运算器分 8 个周期处理,就需要计数器、状态机、寄存器和每周期一次的状态更新。循环语法本身不创造时间。
生成循环(generate for)常用于复制模块实例或条件结构;过程
for
常用于规则化组合赋值或固定次数运算。二者都要求综合工具能在构建硬件时确定规模。
23. 一个信号只保留一个清楚的驱动来源
错误示例:
always_comb begin
y = a & b;
end
assign y = c | d;y
同时由过程和连续赋值驱动。对普通片上逻辑,这不是“后一句覆盖前一句”,而是两个硬件源争夺同一信号。工具通常报多驱动错误。
正确做法是先明确选择规则,再由一个表达式驱动:
always_comb begin
if (sel)
y = a & b;
else
y = c | d;
end多个时序过程写同一个寄存器同样有问题。复位、装载、计数和保持应在同一个
always_ff 中按规格排出优先级。
24. 组合环:没有状态边界的反馈
assign a = ~b;
assign b = ~a;这形成组合反馈环。理想零延迟逻辑无法给出稳定的单一求值顺序,实际电路可能振荡、停在不可预测状态或受到物理延迟支配。
第 11 章的锁存器也有反馈,但它依靠精心设计的交叉耦合结构保存状态,并具有明确控制与库模型。普通 RTL 中意外形成的组合环通常是连接错误。同步反馈应经过寄存器:
always_ff @(posedge clk)
q <= ~q;这描述每个时钟边沿翻转一次的触发器状态,不是组合环。
25. 可综合代码与仅用于仿真的代码
综合(synthesis)要把代码转换为有限硬件,因此并非所有仿真语句都能直接实现。
常见可综合基础结构包括:
- 模块、端口和实例;
- 连续赋值;
- 完整的
always_comb; always_ff描述的寄存器;- 边界静态可确定的
for; - 参数、局部参数、常量函数和基本数组;
if、case与算术/逻辑运算。
常见测试平台语句包括:
#10这样的显式仿真延迟;$display、$monitor等打印任务;- 用于产生激励的无限循环;
- 依赖文件读取、随机化或仿真控制的过程。
某些 FPGA 工具支持用 initial 初始化寄存器或存储器,某些
ASIC
流程则不会把它当作通用上电复位。是否可综合以及映射成什么硬件取决于目标技术和工具。基础可移植
RTL 应用明确的接口、复位和初始化方案表达所需行为。
第 20 章会把设计模块与测试平台分开,并解释事件驱动仿真、激励、自检查和断言。
26. x 不是普通的“任意值”
仿真中的 x 表示未知。它可能来自:
- 寄存器或存储器尚未初始化;
- 多个驱动源冲突;
- 某个组合分支没有定义;
- 上游计算包含未知输入;
- 仿真模型主动报告非法状态或时序问题。
看到 x
时应沿信号来源查找原因。不要在普通功能逻辑中用宽松匹配把未知位静默当成 0
或 1。特别是 casex 会把 x、z
当作通配符,可能掩盖真实设计错误,基础 RTL 中避免使用。
若规格存在真正的“不关心项”,应在组合化简、明确的 casez
模式或综合约束中谨慎表达,并同时检查仿真行为。设计输出在正常合法输入下应有确定值。
27. 常见误区
误区 1:代码从上到下跨周期运行
模块实例、连续赋值和不同过程并行存在。只有时钟边沿和明确状态决定跨周期推进。
误区 2:logic
就是寄存器
logic
是数据类型。组合驱动得到组合逻辑,边沿过程赋值得到触发器,不完整组合赋值可能得到锁存器。
误区 3:所有 if
都是软件分支
组合 if 通常形成 MUX;时序 if
通常形成寄存器 D
输入前的选择和使能逻辑。所有候选路径都实际存在于硬件中。
误区 4:不写 else
等于输出 0
组合过程不写 else
表示没有规定该路径,常导致锁存器;时序过程中不写对应分支表示寄存器保持。
误区 5:非阻塞赋值只是另一种写法
<=
表达边沿取样与并行提交。它使同一时序过程中的右侧读取旧状态,正好匹配真实触发器。
误区 6:位宽不够时工具会自动保留所有信息
目标位宽不足会截断高位。符号、常量尺寸和中间表达式宽度还会影响扩展方式,关键运算要显式设计位宽。
误区 7:for
循环会自动节省硬件
固定次数循环通常展开为并行硬件。资源共享必须加入寄存器、计数和控制状态,明确用周期换面积。
误区 8:能仿真就是可综合、可实现
仿真只说明模型按语言事件规则运行。综合还要求代码能映射为目标硬件;实现还受时序、面积、功耗、库和物理结构约束。
28. 一套实用的 RTL 阅读方法
面对一个陌生模块,按下列顺序阅读:
- 端口契约:输入、输出、位宽、时钟、复位和握手信号是什么?
- 状态清单:哪些对象在
always_ff中更新?它们保存什么历史? - 组合路径:输出和各寄存器 D 输入由哪些表达式、MUX、运算器产生?
- 边沿语义:每个寄存器在什么边沿、什么条件下更新?
- 优先级:复位、装载、使能、计数同时成立时谁优先?
- 完整性:组合输出是否覆盖每条路径?
case是否处理非法编码? - 位宽:加法、移位、拼接、切片和常量是否保留所需信息?
- 驱动:每个信号是否只有一个清楚来源?
- 接口特例:同址读写、禁用输出、上电未知值如何规定?
- 硬件草图:能否画出寄存器、MUX、比较器、运算器和存储器?
若无法画出硬件草图,通常说明代码的时间语义还没有真正读懂。
29. 从规格到 RTL 的推荐顺序
写代码前先完成硬件设计:
- 用一句话写清模块任务;
- 列端口表和每个端口的周期语义;
- 画数据通路与状态元件;
- 写寄存器传输或状态表;
- 明确复位、使能、优先级和非法输入;
- 再选择
assign、always_comb、always_ff和数组; - 手工逐拍检查至少一个正常流程和一个边界流程;
- 第 20 章再用测试平台自动验证。
这能避免“先写出能编译的代码,再猜它代表什么硬件”。RTL 是设计结果的精确表达,不是用语法碰运气。
30. 工程中的实际意义
RTL 是逻辑设计、验证、综合和物理实现之间的共同接口:
- 设计者用 RTL 表达功能结构与周期行为;
- 仿真器按事件语义检查功能;
- 综合工具把 RTL 映射为门级网表;
- 静态时序分析检查寄存器之间的组合路径;
- 物理实现工具把网表映射到标准单元和互连。
一行 RTL 可能带来大量硬件。例如 y=a*b
在宽位向量上可能形成乘法器;大数组的复位循环可能阻止存储宏推断;多层条件选择可能形成很深的
MUX 链。代码短不等于面积小或速度快。
因此,基础 RTL 质量包含三层一致:
- 功能一致:代码实现规格要求;
- 周期一致:输入接受、状态更新和输出有效的时刻正确;
- 结构合理:推断出的组合逻辑、寄存器和存储器符合设计意图。
31. 本章知识链
硬件规格
↓
模块与端口:定义边界
↓
logic / 位向量 / 数组:定义数据形状
↓
assign / always_comb:组合关系
↓ ↘ 完整赋值,避免锁存器
always_ff:边沿状态更新
↓ ↘ 非阻塞赋值,读取旧状态
参数、枚举与层次实例:组织可复用结构
↓
存储器与 FSM RTL
↓
画出寄存器、MUX、逻辑和阵列
↓
第 20 章:用仿真和测试平台验证
32. 本章小结
- Verilog/SystemVerilog 描述并行硬件及其时间行为,不是普通顺序程序。
- 模块定义硬件边界,实例对应一份实际硬件结构。
logic是四状态数据类型,是否产生寄存器取决于驱动方式。- 位向量宽度决定硬件位数;截断、扩展和有符号属性必须与规格一致。
assign和always_comb描述组合逻辑,组合输出必须在所有路径有定义。- 不完整组合赋值会要求保存旧值,从而推断锁存器。
always_ff描述边沿触发状态;时序过程使用非阻塞赋值。- 非阻塞赋值使同一边沿的右侧读取旧状态,再一起提交新状态。
- 同步/异步复位的事件列表不同,分支顺序定义复位、装载和使能优先级。
- 参数在设计展开时配置结构,不是运行时输入。
logic [W-1:0] mem [0:D-1]表示D个Wbit 数据字。- 数组、比较、旁路和循环都会映射为具体硬件资源。
- 每个普通设计信号应有一个清楚的驱动来源,组合反馈应被主动识别。
- 能仿真、能综合和能满足实现约束是三个不同检查层次。
33. 练习
基础题
- HDL 中两条独立的
assign为什么不是按文本顺序逐条执行? - 模块定义与模块实例有什么区别?实例化四次通常意味着多少份硬件?
logic [11:0] data有多少位?最高位和最低位编号是什么?8'h3F、8'd63和8'b0011_1111是否表示相同位模式?assign y = sel ? b : a;通常对应什么组合模块?always_comb中为什么通常使用阻塞赋值=?always_ff @(posedge clk)表示在什么事件更新状态?- 组合过程中漏写
else与时序过程中没有赋值分别通常表示什么? - 非阻塞赋值为什么适合多个寄存器在同一边沿更新?
分析与改错题
logic [7:0] y; assign y = 8'hFF + 8'h01;的y是多少?怎样保留进位?找出并修正下面代码的组合逻辑问题:
always_comb begin if (a) y = b; end一个 4 选 1 MUX 的
case(sel)只覆盖00、01、10,会发生什么?写出一种完整修正。边沿前
q0=0、q1=1、d=1,执行q0 <= d; q1 <= q0;后两个寄存器分别是什么?解释下面代码为什么不是交换:
a = b; b = a;。给出正确时序写法。某寄存器过程依次判断
rst、load、inc。三者同时为 1 时执行什么?怎样改成inc最高优先级?logic [15:0] mem [0:31];的深度、字宽和总容量分别是多少?mem[7][3]表示什么?$clog2(100)是多少?logic [W-1:0] addr能编码多少种位模式?其中多少个可作为0~99地址?找出下面代码的问题并改为单驱动:
assign y = a & b; always_comb y = c | d;for (int i=0; i<16; i++) y[i]=a[i]&b[i];通常综合为一个与门运行 16 次,还是 16 个并行与门?若要复用一个与门,需要增加哪些状态结构?
综合题
- 写一个 8 bit、带同步高有效复位和使能的寄存器模块,并说明
rst与en同时为 1 时的结果。 - 用
always_comb写一个 8 bit 饱和加法器:和超过 255 时输出8'hFF,否则输出正常和。 - 写一个参数化
Wbit 2 选 1 MUX,默认W=8,并实例化一个 32 bit 版本。 - 写一个 16 深度、8 bit
字宽、异步读、同步写的存储器;规定写周期同址读取时通过旁路看到新
wdata。 - 把第 16 章的三状态
IDLE/RUN/DONE控制器写成两过程 FSM。要求同步复位,RUN中terminal=1转到DONE,DONE输出一个周期done=1后回到IDLE。 - 阅读例题 5 的模 N 计数器。对
N=6,列出W、合法状态、未使用编码,并逐拍写出复位解除后连续使能 8 个边沿的count。 - 为交通信号项目制定 RTL
纸面方案:列出控制器、持续时间寄存器文件、计数器和输出译码模块的端口;说明哪些用
always_comb、哪些用always_ff;规定复位、配置写入、状态转换与持续时间为 0 时的行为;画出模块连接,并列出至少十项第 20 章要自动检查的场景。
34. 练习答案
展开第 19 章练习答案
题 1
每条连续赋值描述一个持续存在的驱动关系,对应各自的组合逻辑或连线。输入变化会使相关表达式重新求值,各块硬件同时存在;文本先后顺序不产生时钟周期。
题 2
模块定义规定接口和内部实现,是可复用的硬件蓝图。模块实例是在上层层次中放入一份具体硬件并连接端口。实例化四次通常得到四份并行硬件。
题 3
[11:0] 含从 11 到 0 共 12 位。最高位为
data[11],最低位为 data[0]。
题 4
三者都表示 8 bit 位模式 0011_1111,数值为十进制
63。进制标记改变书写方式,不改变最终位模式。
题 5
它对应 2 选 1 MUX。sel=0 选择
a,sel=1 选择 b。
题 6
组合过程常按数据依赖顺序计算临时量和输出。阻塞赋值使后续语句立即读到本次过程刚算出的值,适合表达单次组合求值。
题 7
它表示在 clk 的 0→1
上升沿采样右侧和控制条件,并更新所描述的边沿触发状态。
题 8
组合过程漏掉一条路径时,输出若要保留旧值就需要锁存器。时序过程本来只在边沿更新;某个边沿条件下不赋值表示触发器保持原值。
题 9
非阻塞赋值在边沿先读取所有右侧旧状态,再提交所有左侧新状态,匹配多个真实触发器同一边沿并行采样。
题 10
8 bit 输出只保存低位,所以 y=8'h00。保留进位可写:
logic [8:0] y_full;
assign y_full = {1'b0, 8'hFF} + {1'b0, 8'h01};结果为 9'h100。
题 11
a=0 时 y
未赋值,会要求保留旧值并推断锁存器。若规格要求此时输出 0:
always_comb begin
if (a)
y = b;
else
y = '0;
end也可先写 y='0;,再在 a=1 时覆盖。
题 12
sel=11
时没有定义输出,会推断锁存器或触发工具检查。完整写法之一:
always_comb begin
case (sel)
2'b00: y = d0;
2'b01: y = d1;
2'b10: y = d2;
default:y = d3;
endcase
end题 13
两条右侧读取边沿前状态,所以边沿后
q0=1、q1=0。
题 14
阻塞赋值第一行先把 a 改为旧
b,第二行再读取已经改变的 a,因此两者都变成旧
b。正确时序写法为:
always_ff @(posedge clk) begin
a <= b;
b <= a;
end题 15
if (rst) ... else if (load) ... else if (inc)
中三者同时为 1 时执行复位。若要求 inc
最高优先级,可写:
always_ff @(posedge clk) begin
if (inc)
q <= q + 1'b1;
else if (rst)
q <= '0;
else if (load)
q <= d;
end这种优先级是否合理必须由规格确认;工程中复位常被置于更高优先级。
题 16
深度 32,字宽 16 bit,总容量为
\[ 32\times16=512\ \text{bit}=64\ \text{byte}. \]
mem[7][3] 是第 7 个数据字的第 3 位。
题 17
因为 \(2^6=64<100\le128=2^7\),所以
$clog2(100)=7。7 bit 地址共有 128 种位模式,其中 100
个对应地址 0~99,28 个未使用。
题 18
y 同时由连续赋值和组合过程驱动,属于多驱动。若
sel 选择两种函数,应合并为单一驱动:
always_comb begin
if (sel)
y = a & b;
else
y = c | d;
end题 19
静态 16 次循环通常展开成 16 个并行与门。若要复用一个与门,需要保存输入或当前位、保存部分结果、用计数器选择位,并用 FSM 控制 16 个周期的读取、运算和写回。
题 20
module reg8 (
input logic clk,
input logic rst,
input logic en,
input logic [7:0] d,
output logic [7:0] q
);
always_ff @(posedge clk) begin
if (rst)
q <= '0;
else if (en)
q <= d;
end
endmodulerst 与 en 同时为 1
时清零,因为复位分支优先。
题 21
logic [8:0] sum;
always_comb begin
sum = {1'b0, a} + {1'b0, b};
if (sum[8])
y = 8'hFF;
else
y = sum[7:0];
end两个 8 bit 无符号数的完整和最多为 510,9 bit 足够。最高位为 1 表示结果超过 255。
题 22
module mux2 #(
parameter int unsigned W = 8
) (
input logic [W-1:0] a,
input logic [W-1:0] b,
input logic sel,
output logic [W-1:0] y
);
assign y = sel ? b : a;
endmodule
logic [31:0] a32, b32, y32;
logic sel32;
mux2 #(.W(32)) u_mux32 (
.a(a32), .b(b32), .sel(sel32), .y(y32)
);展开后是一组 32 bit 宽的选择逻辑。
题 23
module mem16x8 (
input logic clk,
input logic we,
input logic [3:0] raddr,
input logic [3:0] waddr,
input logic [7:0] wdata,
output logic [7:0] rdata
);
logic [7:0] mem [0:15];
assign rdata = (we && (waddr == raddr))
? wdata
: mem[raddr];
always_ff @(posedge clk) begin
if (we)
mem[waddr] <= wdata;
end
endmodule比较器检测同址,MUX
在旁路写数据和阵列读数据之间选择。该规格规定写请求有效期间同址读口显示新
wdata。
题 24
typedef enum logic [1:0] {IDLE, RUN, DONE} state_t;
state_t state, next_state;
always_ff @(posedge clk) begin
if (rst)
state <= IDLE;
else
state <= next_state;
end
always_comb begin
next_state = state;
count_en = 1'b0;
done = 1'b0;
case (state)
IDLE: if (start) next_state = RUN;
RUN: begin
count_en = 1'b1;
if (terminal) next_state = DONE;
end
DONE: begin
done = 1'b1;
next_state = IDLE;
end
default: next_state = IDLE;
endcase
endstate
是唯一状态寄存器;其余为当前状态和输入形成的组合逻辑。
题 25
N=6 时 W=$clog2(6)=3。合法状态为 0~5,即
000~101;110、111
未使用。假设复位边沿把计数器置 0,随后解除复位并保持
en=1,连续 8 个上升沿后的序列为:
边沿前:0
边沿 1:1
边沿 2:2
边沿 3:3
边沿 4:4
边沿 5:5
边沿 6:0
边沿 7:1
边沿 8:2
题 26
一种清楚的模块划分如下:
| 模块 | 主要端口 | RTL 结构 |
|---|---|---|
traffic_ctrl |
clk,rst,start,terminal,state |
always_ff 保存状态,always_comb
计算下一状态 |
duration_regs |
clk,rst,cfg_we,cfg_addr,cfg_wdata,state,duration |
always_ff 写配置;组合读或同步读需明确 |
phase_counter |
clk,rst,clear,en,limit,count,terminal |
always_ff 保存计数,always_comb/assign
比较终点 |
light_decode |
state,main_r/y/g,side_r/y/g |
完整 always_comb 输出译码 |
同步复位把控制状态置
IDLE、计数器清零,并把四项持续时间寄存器装入规定默认值。配置只在
cfg_we=1
的上升沿写入。进入新交通相位时计数清零,随后每个有效工作拍加一;达到当前
duration 后转到下一相位。
持续时间为 0 必须有明确策略。一种安全规格是把 0 解释为 1
个周期,比较时使用
effective_duration=(duration==0)?1:duration;另一种规格是拒绝写入
0 并报告配置错误。二者任选其一并在接口中固定。
连接关系为:配置寄存器根据控制状态输出当前
duration;计数器比较后产生 terminal
返回控制器;控制器输出
state、计数器清零/使能;灯光译码只读取
state。
第 20 章至少检查:复位默认灯态;每个相位持续拍数;四种状态顺序;终点边界;计数清零;暂停或使能关闭;运行时配置写入;读取配置;持续时间 0;最大持续时间;复位与配置写同拍优先级;非法状态恢复;输出互锁,确保冲突方向不同时为绿灯。
35. 自测清单
若第 1、4、7 项不稳定,请遮住代码说明,分别重画图
19-1、19-3、19-4;若第 8~10 项不稳定,请重做题 10、16、17;若第 11
项不稳定,请把例题 6 和两过程 FSM 中每个
assign、always_comb、always_ff
逐一标为组合路径或状态更新。
36. 下一章衔接
RTL 写清了“希望硬件怎样工作”,还需要系统地证明它在正常、边界和错误输入下确实这样工作。第 20 章将建立事件驱动仿真的时间模型,编写测试平台产生时钟、复位和输入激励,使用任务、检查器与基础断言自动判断结果,并区分设计错误、测试错误和规格缺口。