组合与时序逻辑(Combinational and Sequential Logic)
三种日常 RTL 写法
| 目的 | SystemVerilog | 兼容旧 Verilog | 赋值习惯 |
|---|---|---|---|
| 简单组合表达式 | assign y = ...; |
相同 | 连续赋值 |
| 多分支组合逻辑 | always_comb |
always @* |
阻塞 = |
| 边沿触发寄存器 | always_ff @(posedge clk) |
always @(posedge clk) |
非阻塞 <= |
连续赋值(continuous assignment)在输入变化时重新计算;过程赋值(procedural assignment)在过程被触发后执行。always_comb 会自动建立组合敏感关系并在仿真开始执行一次,工具还能检查写入约束;always_ff 表达时序意图。但关键字本身不能代替完整的组合赋值或正确的时钟/复位设计。工具支持见 Vivado Always Procedures。
组合逻辑:每条路径都赋值
module Mux4 (
input logic [7:0] a, b, c, d,
input logic [1:0] sel,
output logic [7:0] y
);
always_comb begin
y = '0;
case (sel)
2'd0: y = a;
2'd1: y = b;
2'd2: y = c;
2'd3: y = d;
default: y = '0;
endcase
end
endmodule
- 默认值放在过程开头,分支覆盖需要改变的值;每个输出、每个中间变量都要覆盖。
if / else if / else从上到下具有优先级;case也选择首先匹配的分支。- 普通
case是四态精确匹配;case (1'b1)把分支表达式当条件,课程 Controller 用它组织译码优先级。 casez可将 Z/?用作通配符,casex还把 X 当通配符,RTL 译码应谨慎使用。- 不要手写遗漏信号的
always @(a or b);后续表达式加了 c 却没更新敏感列表,会使仿真与综合不一致。
锁存器与组合环路
这是电平敏感存储的意图,会产生锁存器(latch)或工具诊断。用 else y = b; 或事先默认赋值可得到 mux。组合块中 q = q + 1; 也不能用来构造计数器,它引入反馈;应使用时钟寄存器。
遗漏赋值导致锁存,未被寄存器隔开的循环依赖导致组合环路(combinational loop),两者不同。 组合地址计算本身不会导致锁存器,只有未完整赋值、错误依赖等才会。框架 VGA_Debugger 的分支中应特别检查 vram_waddr 等输出是否在 default 路径也赋值。
时序逻辑:复位、使能、保持
module RegisterEn #(
parameter int unsigned W = 32
) (
input logic clk, rst_n, en,
input logic [W-1:0] d,
output logic [W-1:0] q
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= '0;
else if (en)
q <= d;
end
endmodule
边沿触发块中省略最后的 else 表示 保持寄存器原值,对应使能(clock enable),不会因为这一省略产生锁存器。条件顺序同时定义优先级:上例复位优先于使能。
| 复位形式 | 敏感事件与条件 | 何时生效 |
|---|---|---|
| 同步低有效 | @(posedge clk),if (!rst_n) |
仅有效时钟边沿 |
| 异步低有效 | @(posedge clk or negedge rst_n) |
时钟边沿,或复位拉低时 |
| 异步高有效 | @(posedge clk or posedge rst) |
时钟边沿,或复位拉高时 |
rst_n 的名字不能证明它是异步复位,必须读敏感事件。课程 FSM 用异步低有效复位,Cnt 用同步低有效复位,乘法器示例又有高有效复位,连接时要逐项核对。异步复位释放需满足时序要求,跨域复位常使用各域“异步置位、同步释放”(asynchronous assertion, synchronous deassertion)方案。
不要给同一寄存器同时配置两个普通时钟,或在两个过程分别写入。通过 if (en) 控制更新,优于用 clk & en 临时构造门控时钟。
阻塞与非阻塞:右边取什么值
阻塞赋值(blocking assignment)= 执行时立即更新目标,后续语句看到新值。非阻塞赋值(nonblocking assignment, NBA)<= 执行时计算右侧,把更新安排到该时间步的 NBA 阶段;后续语句通常仍读到旧寄存器值。
这是两级寄存器链。若改成 a = in_data; b = a;,同一过程内 b 会读到刚更新的 a;若不同过程也读 a,还可能因调度顺序发生竞态(race)。
| 时钟沿前 | 当次 RHS 求值 | NBA 更新后 |
|---|---|---|
| a=3,b=7,in_data=9 | a 的新值=9;b 的新值=3 | a=9,b=3 |
同一过程不是“所有语句都无序”
同一过程对同一变量的多次 NBA,按执行顺序,最后一次有效赋值决定结果;跨过程多驱动不应依靠调度顺序设计。
组合累加应先用阻塞赋值计算临时结果,再在时序块中一次性 sum <= next_sum;。移位数组则适合 NBA:data[i] <= data[i+1] 让每个元素都取邻居的旧值,不会依次覆盖源数据。
不要在同一组合块中先读取某个临时量,后才给它赋值:
寄存器边界与物理时序
组合逻辑必须在下一个采样边沿前稳定。建立时间(setup time)是边沿前数据需稳定的时间;保持时间(hold time)是边沿后需稳定的时间;余量(slack)是“要求到达时间 − 实际到达时间”,slack 不是 setup time。
忽略时钟偏斜时,一条寄存器到寄存器路径需满足:
RTL 仿真中的 #5 时钟不证明硬件能在 200 MHz 工作;综合后还需静态时序分析(Static Timing Analysis, STA)。运算链过长可以分级插入寄存器,但这样会增加延迟(latency),并要求控制信号和数据一起延迟。