是什么:Verilog(IEEE 1364 标准)是一种硬件描述语言(HDL):它看起来像 C,但描述的不是"按步骤执行的指令",而是一堆同时存在的连线、寄存器和组合逻辑。你写的每一行代码,最终由综合器(如 Yosys / Design Compiler / Vivado)翻译成门级网表,再落到 FPGA 或流片成 ASIC 芯片。
为什么 NPU 工程师必须会:NPU 的本体就是 RTL——几百个乘加单元(PE)组成的脉动阵列、数据搬运通路、指令译码与调度状态机,全部用 Verilog/SystemVerilog 描述。看不懂 Verilog,就看不懂 NPU 的微架构论文和开源实现(如 picorv32、NN 硬化加速器);想做 AI 芯片相关工作(设计/验证/性能建模),这是入场券。
怎么做——建立两条思维铁律:
always 块和 assign 是同时工作的,没有"先执行第 1 行再执行第 2 行"——只有"这根线连到哪、哪个时钟沿采样什么"。最直观的对比:同样的"两个数相加并打一拍",C 语言和 Verilog 的写法差异——
// C 语言:CPU 在某个时刻"执行"这一行,算完就完了
int y = a + b;
// Verilog:描述一个"永远存在的电路"——一个加法器 + 一个 D 触发器
module adder_pipe #(parameter W = 16)(
input clk,
input [W-1:0] a, b, // 两位宽为 W 的输入总线
output reg [W-1:0] y // reg:在 always 中被赋值
);
always @(posedge clk) // 每个时钟上升沿
y <= a + b; // 采样加法器结果,存入寄存器(非阻塞赋值)
endmodule
综合结果 = 1 个 W 位加法器 + 1 个 W 位寄存器。它不是"运行一次",而是每个时钟周期都在算。
是什么:Verilog 里真正常用的数据类型只有两大类四小类——
| 类型 | 是什么 | 谁来驱动 | 综合成什么 |
|---|---|---|---|
wire(net 型) | 物理连线,自身不存值 | assign 连续赋值、模块输出端口 | 一根线 / 组合逻辑输出 |
reg(variable 型) | always 块中的"变量" | 过程赋值(= 或 <=) | 取决于上下文:时序块→触发器;组合块→纯组合逻辑,不是寄存器! |
parameter / localparam | 编译期常量 | — | 不占硬件,综合时按常量展开;parameter 可被例化覆盖,localparam 只能模块内部用 |
integer / genvar / real | 辅助类型 | — | integer 用于循环/genvar 用于 generate;real 不可综合 |
为什么 reg ≠ 寄存器:这是全 Verilog 最容易误解的一点。reg 只是"可以被过程赋值的变量"这个语法角色;它变成触发器还是组合逻辑,完全看 always 的敏感列表。wire 只能被连续赋值驱动,reg 只能被过程赋值驱动——output 端口既可以是 wire(默认)也可以声明成 output reg。
怎么做——位宽规则(面试笔试必考):
[7:0] a 是 8 位,MSB 在左;没声明范围的 wire x 是 1 位。signed / signed 字面量如 -2)做符号扩展。module width_demo();
reg [3:0] a, b;
reg [4:0] sum5;
reg [7:0] wide;
reg signed [7:0] s8;
// ★ 经典位宽 bug:进位被截断
assign sum5 = a + b; // OK:目标 5 位,表达式按 5 位算,进位保留在 bit4
wire [3:0] bad = a + b; // BUG:a=9,b=8 时真值 17,截断后 bad=4'b0001,进位静默丢失
wire [4:0] fix = {1'b0, a} + b; // 手动扩位:把 a 扩到 5 位再相加,进位不丢
assign wide = a; // 零扩展:高 4 位补 0
assign wide = s8; // 符号扩展:s8 为负时高 4 位补 1(因为 s8 是 signed)
// 有符号比较陷阱:只要有一个操作数无符号,整个表达式按无符号比
wire c1 = (s8 < 4'd9); // s8 = -1 时,4'd9 无符号 → 整体无符号比较,-1(8'hFF)被当成 255!
wire c2 = (s8 < $signed(4'd9)); // 两侧都 signed → 有符号比较,结果才正确
endmodule
{1'b0,a}+b 或让目标位宽为 N+1;③「parameter 和 localparam 区别?」——parameter 可从例化处 #(.W(8)) 覆盖,localparam 由 parameter 推导、对外只读,防止下游乱改派生常量。wire x 默认 1 位,接到 8 位总线上只连 1 根线且常常无 warning。是什么:module...endmodule 定义一个电路单元,是 Verilog 的基本复用与层次单位。端口三兄弟:input(只进)、output(只出)、inout(双向,必须声明为 wire/net 型,综合成三态门——片内逻辑禁用,只用于 I2C 这类真实双向总线)。
为什么重要:数字设计 = 层次化分解。一个 NPU 不是一口气写出来的:ALU → PE 单元 → PE 阵列 → 乘加核 → NPU Top,每层都是一个 module,靠端口连接。模块接口(端口名+位宽+方向)就是你和团队/未来的自己签的"合同",接口定了,内部随便重构。
怎么做——两种端口连接方式:
// 子模块:可参数化的加法器
module adder #(parameter W = 8)( // #(...) 参数列表
input [W-1:0] a, b,
input cin,
output [W-1:0] sum,
output cout
);
assign {cout, sum} = a + b + cin; // 位拼接:{cout,sum} 组成 W+1 位,进位自然落到 cout
endmodule
// 顶层:两种例化方式
module top();
reg [7:0] x, y;
wire [7:0] s;
wire c;
// ① 位置关联(不推荐):按端口声明顺序一一对应
// 隐患:端口顺序一变,连接全错,且 8 位接 1 位位宽错配难以发现
adder u1 (x, y, 1'b0, s, c);
// ② 命名关联(工程标准):.端口名(信号名),顺序无关、一眼可查
adder #(.W(8)) u2 ( // #(.W(8)) 参数化:覆盖默认位宽
.a (x),
.b (y),
.cin (1'b0),
.sum (s),
.cout (c)
);
endmodule
层次化设计三条军规:① 上层模块只做"连接与调度",不做具体运算;② 每个子模块有独立 testbench 验证后再集成(自底向上验证);③ 端口命名全队统一(如 clk/rst_n/i_/o_ 前缀),命名关联例化让 code review 能逐端口对上。
output reg,否则编译报错;③给 input 悬空/接 z,综合出意外逻辑——例化时所有端口都要显式连接,不用就接 'd0 或注释说明。是什么:可综合 RTL 的"运动形态"只有三种,对应组合电路和时序电路两大物种:
| 写法 | 描述什么 | 被驱动对象 | 赋值符号 |
|---|---|---|---|
assign f = ...; | 连续赋值:纯组合连线,右侧任何信号变化立即(0 延迟)传播 | wire | = |
always @* | 组合逻辑(多路器、译码器、ALU) | reg(变量) | = |
always @(posedge clk) | 时序逻辑:时钟沿采样,输出保持到下一个沿 | reg(变量) | <= |
什么时候用哪个——判断口诀:这个值需要"记住"(打一拍)吗?需要 → 时序 always;不需要、只是组合运算/选择 → 能一行写完用 assign,带分支用 always @*。同一个输出绝不能既被 assign 又被 always 驱动(多驱动,见 6.2)。
// 同一个"三选一"逻辑的三种等价写法
// ① assign + 条件运算符:适合单层选择
assign dout = sel_a ? a : sel_b ? b : c;
// ② always @* + if-else:组合分支,reg 但综合出纯组合逻辑
always @* begin
if (sel_a) dout2 = a;
else if (sel_b) dout2 = b;
else dout2 = c; // ★ 每个 if 都必须收尾,否则 latch(见 6.1)
end
// ③ always @* + case:多分支并行选择(无优先级)
always @* begin
case ({sel_b, sel_a}) // 拼接成选择码
2'b00: dout3 = c;
2'b01: dout3 = a;
2'b10: dout3 = b;
default: dout3 = c; // ★ case 必须有 default
endcase
end
// 时序:时钟沿采样并保持
always @(posedge clk or negedge rst_n) begin // 异步低有效复位(见 6.3)
if (!rst_n) dout_q <= 3'd0;
else dout_q <= dout; // 把组合结果打一拍 → 寄存器
end
敏感列表怎么写:组合块一律用 always @*(自动包含所有读取的信号)——手写 always @(a or b) 漏一个信号就会仿真-综合不一致(仿真里不触发,综合却按完整组合处理)。时序块只写时钟沿与(可选的)异步复位沿,绝不把数据信号放进时序敏感列表。
posedge clk or rst_n(漏了 negedge / 写成 posedge)会让复位极性错;③一个信号两处驱动,综合直接报 multiple driver 错误。是什么:两种过程赋值的调度时机不同——
=:语句立即生效,下一行立刻看到新值,像 C 一样顺序执行。用于组合 always。<=:先算好右边(RHS)暂存,在当前时间步结束时统一更新左边(IEEE 调度中的 NBA 区域)。用于时序 always。精确模拟了"所有触发器在同一个时钟沿同时采样旧值"。为什么"组合用 = 、时序用 <=":组合电路是"输入一变、输出立即变"——阻塞语义正好;寄存器是"时钟沿集体采样旧值"——非阻塞语义正好。这不是风格偏好,而是调度语义决定仿真结果是否与真实电路一致。
对比表:
| 对比项 | 阻塞赋值 = | 非阻塞赋值 <= |
|---|---|---|
| 生效时机 | 语句执行完立即更新,阻塞后续语句 | 当前时间步末尾统一更新 |
| 后续语句看到的值 | 刚写入的新值 | 更新前的旧值 |
| 模拟的硬件行为 | 组合逻辑的立即传播 | 触发器时钟沿集体采样 |
| 正确使用场景 | always @* 组合逻辑 / testbench 激励顺序 | always @(posedge clk) 时序逻辑 |
| 用错的典型症状 | 时序块用 = → 移位寄存器"吞拍",仿真一级变一级穿透 | 组合块用 <= → 输出晚一拍、块内自依赖死循环或错误保持 |
| 工程规范 | 同一 always 块内禁止混用(IEEE 1364 声明混用结果不确定;综合器会直接报错或给出不可预期结构) | |
经典 bug 示例——时序块误用阻塞赋值:
// 目标:三级移位寄存器 d → q1 → q2 → q3
// ★ 错误写法:阻塞赋值,时钟沿到来时语句顺序执行
always @(posedge clk) begin
q1 = d; // q1 立即变成 d
q2 = q1; // 读到的是"新 q1"= d,而不是时钟沿前的旧 q1!
q3 = q2; // 同理 q3 也 = d
end
// 仿真结果:q1=q2=q3 全部等于 d,三级流水变成"直通",综合器虽可能强行
// 推断出三级触发器,但仿真模型已和硬件行为不一致 —— 仿真不可信了。
// ★ 正确写法:非阻塞赋值,三条语句同时用"时钟沿前的旧值"更新
always @(posedge clk) begin
q1 <= d;
q2 <= q1; // 旧 q1
q3 <= q2; // 旧 q2
end
// 仿真结果:d 延迟三拍出现在 q3 —— 与三级触发器电路完全一致。
是什么:软件里的 if/case/for 在 Verilog 里描述的是多选器树、译码器和复制粘贴的硬件,不是运行时分支。能否综合、综合成什么,规则如下:
default。4'b1??? 匹配"最高位为 1"一类模式;casex 连 x 也当通配,仿真中会掩盖真实 x 态,工程禁用,要用 casez 并只让"字面量"带 ?。// for 循环可综合示例:8 位输入中 1 的个数(popcount)
// 综合结果:一个加法树(7 个加法器),不是"循环执行的程序"
module popcount8 (
input [7:0] din,
output reg [3:0] cnt
);
integer i; // 循环变量用 integer
always @* begin
cnt = 4'd0; // ★ 先默认赋值,防 latch
for (i = 0; i < 8; i = i + 1)
cnt = cnt + din[i]; // 展开为 cnt = d0+d1+...+d7 的加法树
end
endmodule
// casez 模式匹配:指令译码(顺带为 NPU 译码模块铺垫)
always @* begin
imm = 32'd0; // 默认值
casez (instr[31:20]) // 只在"字面量"一侧用 ? 做通配
12'b0000001?????: imm = instr[31:20]; // 匹配 LOAD 类
12'b0000010???1?: imm = 32'd4; // 部分位固定才匹配
default : imm = 32'd0;
endcase
end
if-else 与 case 的选择标准:条件有天然的优先顺序(如中断抢占、越界保护优先)→ if-else;条件互斥(状态译码、选择码)→ case,综合器能并行化,时序更好。SystemVerilog 还有 unique case / priority case 显式声明意图,综合器与 lint 工具能帮你检查漏分支。
是什么:两者都是"把一段常用逻辑封装起来反复调用",但定位完全不同:
| 对比项 | function 函数 | task 任务 |
|---|---|---|
| 仿真耗时 | 必须为 0 时间(内部不能有 #/@/wait) | 可包含时延、事件等待(可"跑一段时间") |
| 输入输出 | 至少 1 个输入,恰好 1 个返回值(函数名即返回变量) | 可有 0~多个 input/output/inout(适合多输出) |
| 能否调用 task | 不能 | 能调用 function 与其他 task |
| 可综合性 | 可综合(综合为纯组合逻辑)——前提:不包含不可综合结构 | 含时序控制则不可综合,只用于 testbench;纯组合 task 部分综合器可能支持,但工程上不当 RTL 用 |
| 典型用途 | RTL 里复用纯组合表达式:饱和裁剪、补码转换、奇偶校验、C 计算 | testbench 里封装激励序列、总线读写、打拍等待 |
automatic 是什么:Verilog-2001 引入。function automatic / task automatic 让局部变量动态分配——函数可被递归调用/多处并发调用而不互相踩变量。静态(默认)函数的局部变量只有一份,递归会出错;testbench 里的 task 也建议加 automatic,防止多处调用共用局部变量引发竞争。
// RTL 中的 function:饱和裁剪(神经网络量化后常用的 clamp)
// 综合结果:一组比较器 + 多选器,纯组合
function automatic signed [15:0] sat16(input signed [16:0] acc);
if (acc > $signed(32767)) sat16 = 16'sd32767; // 正饱和
else if (acc < -$signed(32768)) sat16 = -16'sd32768; // 负饱和
else sat16 = acc[15:0];
endfunction
// 在时序逻辑中调用:累加并限幅(一个 MAC 单元的输出级)
always @(posedge clk)
accum_q <= sat16(accum_q + mult_result); // 像调用函数一样复用
// testbench 中的 task:封装一次总线写(可含时延,不可综合)
task automatic axi_write(input [31:0] addr, input [31:0] data);
begin
@(posedge clk); awvalid = 1; awaddr = addr;
@(posedge clk); awvalid = 0; wvalid = 1; wdata = data;
@(posedge clk); wvalid = 0;
$display("[%0t] write %h = %h", $time, addr, data);
end
endtask
什么时候用:RTL 中出现同样的组合表达式写第二遍时,就该抽成 function(或用参数化子模块,当逻辑大到需要寄存器边界时)。testbench 中凡是"要重复敲三遍以上的激励序列",抽成 task。
是什么:generate 让"例化多少个、例化哪个模块"成为编译期可编程的决策,配合 #(.W(8)) 参数化例化,是硬件复用的终极手段。三种形式:generate-if(按参数选结构)、generate-for(阵列复制)、generate-case(多选一结构)。
为什么是 NPU 的关键:NPU 的核心就是"同一个 PE(处理单元)复制 N 份排成阵列"。手写 64 个例化不可能,必须用 generate-for;位宽(数据 W 位、累加 AW 位)必须用 parameter 贯穿全设计——改一个参数就能出 8 位版/16 位版。
// 一个 NPU 的最小 PE 单元:乘法 + 累加(可参数化位宽)
module pe_unit #(parameter W = 8, parameter AW = 32)(
input clk, rst_n,
input signed [W-1:0] ifmap, // 激活值
input signed [W-1:0] weight, // 权重
input signed [AW-1:0] psum_in, // 上游传来的部分和(脉动/级联)
output signed [AW-1:0] psum_out // 累加后的部分和
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) psum_out <= 'd0;
else psum_out <= psum_in + ifmap * weight; // MAC
end
endmodule
// 顶层:用 generate-for 例化 4 个 PE,级联成一条 MAC 链
module mac_chain #(parameter N = 4, parameter W = 8, parameter AW = 32)(
input clk, rst_n,
input signed [W-1:0] act, // 广播给所有 PE
input signed [N*W-1:0] weights, // N 个权重打包成一条总线
output signed [AW-1:0] result
);
wire signed [AW-1:0] psum [0:N]; // N+1 级部分和
assign psum[0] = 'd0;
genvar i;
generate
for (i = 0; i < N; i = i + 1) begin : g_pe // ★ 必须命名 label
pe_unit #(.W(W), .AW(AW)) u_pe (
.clk (clk),
.rst_n (rst_n),
.ifmap (act),
.weight (weights[i*W +: W]), // 切出第 i 个权重
.psum_in (psum[i]),
.psum_out (psum[i+1])
);
end
endgenerate
assign result = psum[N];
endmodule
// 把 N 改成 64,一条 64 级 MAC 链即刻生成 —— 二维排布 + 层间互连,
// 就是一个 NPU 脉动阵列的雏形(见本板块后续页面)。
generate-if 用法:按参数裁剪结构,如 if (PIPELINE) begin: g_pipe ... end else begin: g_nop ... end——被裁掉的分支完全不参与综合,常用于"调试逻辑/流水寄存器可选"。注意 generate 块内例化必须带命名 label(begin : 名字),层次路径形如 u_top.g_pe[3].u_pe.xxx,波形调试全靠它定位。
#(.W(8)) 内联参数;③开放题「怎么把一条 MAC 链改成 4×4 阵列?」——两层嵌套 generate-for + 行/列索引互连,考察层次化建模能力。i 做位选择没问题,但循环变量不能被过程赋值修改(它不是运行时变量);②忘写 begin : label,部分工具报错、调试时实例名不可预测;③参数没贯穿——顶层 #(.W(8)) 改了,子模块内部却写死 8'd0,改参数等于没改;④generate for 里所有实例输出同名信号冲突(要用索引化 wire 数组)。是什么:有限状态机(FSM)= 状态寄存器 + 次态逻辑 + 输出逻辑,是所有数字控制通路(握手、仲裁、指令译码、DDR 控制器、NPU 分发调度)的骨架。按输出怎么产生分两派:
| 对比项 | Moore 型 | Mealy 型 |
|---|---|---|
| 输出依赖 | 只依赖当前状态:out = g(state) | 依赖状态 + 当前输入:out = g(state, in) |
| 输出时序 | 与时钟对齐、稳定(状态变它才变) | 输入一变输出立即变,可能带组合毛刺 |
| 响应速度 | 慢一拍(先转移状态再反映输出) | 快一拍(当前周期就能反映输入) |
| 状态数 | 通常更多 | 通常更少 |
| 典型场景 | 对外接口控制、需要输出干净稳定的场合 | 高速数据通路、需要当拍响应的握手 |
怎么做——设计流程五步法:① 画状态转移图(先圈出"需要记住的信息",每种记忆 = 一个状态)→ ② 列状态转移表 → ③ 状态编码(状态少用 one-hot 利时序,状态多用二进制省寄存器;FPGA 常用 one-hot,工具可自动重编码)→ ④ 选写法(下节)→ ⑤ 写 RTL + testbench 逐状态验证。
是什么:同一个状态机按"代码怎么组织"有一段式、两段式、三段式三种写法——功能等价,工程价值天差地别:
| 对比项 | 一段式(单 always) | 两段式(时序+组合) | 三段式(时序+组合+输出)★ |
|---|---|---|---|
| 代码结构 | 状态转移+输出全塞一个时序 always,case 套 case | always① 时序:state <= next_state;always② 组合:次态+输出一起算 | always① 时序:state 寄存;always② 组合:次态译码;always③ 输出(常为时序寄存) |
| 可读性/可维护性 | 差:状态多时嵌套爆炸,转移条件藏在输出里 | 中:转移集中,但输出是组合、直接外翻 | 好:三大职责各占一块,review/改条件/加状态互不干扰 |
| 输出质量 | 寄存输出但逻辑纠缠 | 组合输出:可能有毛刺,驱动下游长路径 | 可寄存输出:滤毛刺、下游 Tsu 余量大 |
| 代价 | — | — | 输出寄存晚一拍;面积几乎不变 |
| 工程态度 | 教材演示用,严禁进代码库 | 小模块可用,团队规范多不推荐 | 团队强制标准 |
为什么工程强制三段式(三条硬理由):① 代码同构——所有状态机长得一模一样,任何人接手 3 分钟看懂,review 只需盯第二段的转移条件;② 时序可控——输出经寄存器打出,组合路径被切成两段,高频设计(≥500MHz)基本必须;③ 验证友好——状态、次态、输出三层信号在波形里一目了然,覆盖率按状态/翻转统计都有标准做法(见 04 页 CDC 与验证部分)。
怎么做——先看结构,再背模板。三段式状态机的硬件框架只有三块,数据流自左向右,state 反馈回次态逻辑:
标准模板(背下来,所有团队代码都是它的变体):
module fsm_template (
input wire clk,
input wire rst_n, // 低有效复位(策略见 6.3)
input wire din,
output reg dout // 第三段寄存输出 → output reg
);
// 状态编码:localparam 只读;小状态机用 one-hot 亦常见
localparam [1:0] S_IDLE = 2'd0,
S_RUN = 2'd1,
S_DONE = 2'd2,
S_ERR = 2'd3; // ★ 永远留一个"安全态/错误态"
reg [1:0] state, next_state; // 现态 / 次态
// ── 第一段:状态寄存器(时序,只会"打一拍",永不写逻辑) ──
always @(posedge clk or negedge rst_n) begin
if (!rst_n) state <= S_IDLE;
else state <= next_state;
end
// ── 第二段:次态组合(所有转移条件集中在这一个 case 里) ──
always @* begin
next_state = S_IDLE; // ★ 先给默认值(回安全态):防 latch + 防非法态锁死
case (state)
S_IDLE: if (din) next_state = S_RUN;
S_RUN : if (!din) next_state = S_DONE;
else next_state = S_ERR;
S_DONE: next_state = S_IDLE;
default: next_state = S_IDLE; // 非法态自动恢复
endcase
end
// ── 第三段:输出逻辑(推荐寄存输出,滤毛刺、改善下游时序) ──
always @(posedge clk or negedge rst_n) begin
if (!rst_n) dout <= 1'b0;
else if (state == S_DONE) dout <= 1'b1; // Moore:只看 state
else dout <= 1'b0;
end
endmodule
逐段讲解:
state 赋值的地方,内容永远不变(复位回 IDLE、否则打一拍)。它把组合的次态"冻结"成现态,是状态机的"心脏"。用非阻塞 <=。always @* + 阻塞 =;入口先写 next_state = 安全态 默认值:一举两得(组合不完整赋值 → 防 latch;非法编码 → 自动回安全态)。所有转移条件集中在唯一一个 case 里,加状态只改这一段。case(state);Mealy 型条件里可含 din。推荐用时序块寄存输出(dout <= ...):代价是晚一拍,收益是输出无毛刺、下游时序余量大;对响应当拍有要求的场合可改用 always @* 组合输出版本,但要评估毛刺与关键路径。是什么:序列检测器是状态机的"Hello World",也是数字前端面试出镜率最高的手撕题:输入端每个时钟周期串行来 1 bit,检测到目标序列「1011」时输出 dout 拉高一个周期,并要求支持重叠检测(如输入 1011011 要命中两次,第二次复用上一次的尾巴)。
为什么状态 = "已匹配的最长前缀":判断"下一个 bit 该算命中还是继续等",只需要知道当前已经匹配到目标序列的哪个前缀。「1011」的全部前缀:∅、"1"、"10"、"101"、"1011"——恰好 5 种,每种一个状态。这就是状态机设计的核心思维:状态是用来"记住历史"的最少信息。
怎么做——状态转移三条规则:① 新 bit 到来时,若前缀延长成功 → 前进到下一状态;② 失配 → 回退到"旧后缀仍是新前缀"的那个状态(不是粗暴回 S0!这是重叠检测的关键);③ 到达「1011」→ 检测成功,同样按规则②回退。完整转移图:
怎么做:把 4.1 的状态图逐边翻译成三段式——第一段打一拍、第二段搬转移条件、第三段寄存输出。注意每一段只干自己的事:
module seq_det_1011 (
input wire clk,
input wire rst_n, // 低有效复位(策略见 6.3)
input wire din, // 串行输入比特,每个 clk 上升沿采样
output reg dout // 检测到 1011 → 拉高一个周期
);
// 状态编码:3 bit 覆盖 5 个状态(2^3=8 ≥ 5),留足非法态余量
localparam [2:0] S0 = 3'd0, // 未匹配任何前缀
S1 = 3'd1, // 已匹配 "1"
S2 = 3'd2, // 已匹配 "10"
S3 = 3'd3, // 已匹配 "101"
S4 = 3'd4; // 已匹配 "1011" → 命中
reg [2:0] state, next_state; // 现态 / 次态
// ── 第一段:状态寄存器(时序,永远只打一拍) ──
always @(posedge clk or negedge rst_n) begin
if (!rst_n) state <= S0;
else state <= next_state;
end
// ── 第二段:次态组合(重叠检测的精髓全在"回退到哪") ──
always @* begin
next_state = S0; // 默认回安全态:防 latch + 防非法态锁死
case (state)
S0: next_state = din ? S1 : S0;
S1: next_state = din ? S1 : S2; // 收 1:"11" 的后缀 "1" 仍是前缀,保持
S2: next_state = din ? S3 : S0; // 收 0:"100" 无有效后缀,清零
S3: next_state = din ? S4 : S2; // 收 0:"1010" 的后缀 "10" 仍是前缀!
S4: next_state = din ? S1 : S2; // 命中后继续检测,复用旧后缀
default: next_state = S0; // 非法编码(3'd5~7)自动恢复
endcase
end
// ── 第三段:输出逻辑(Moore:只看 state;寄存输出,高一个周期) ──
always @(posedge clk or negedge rst_n) begin
if (!rst_n) dout <= 1'b0;
else dout <= (state == S4); // state 进入 S4 的下一拍,dout 打出去
end
endmodule
代码自查清单(交卷前过一遍):①三段边界清晰,state 只在第一段赋值?②第二段有默认赋值 + default?③输出寄存且复位值正确?④状态编码 localparam、位宽足够?⑤所有赋值符号:组合 = 、时序 <= ?全对,这份 RTL 就可以进代码库。
dout_r <= (shift_reg == 8'b1011_0000) 类思路),后者在序列变长时代码量不涨,工程更常用。怎么做——testbench 四件套:时钟、复位、激励 task、结果观测。激励用 task 封装(见 2.6),观测用 $display 逐沿打印 + 查看波形(GTKWave / Vivado / ModelSim)。
`timescale 1ns/1ps
module tb_seq_det_1011();
reg clk, rst_n, din;
wire dout;
// ① 例化 DUT(命名关联)
seq_det_1011 u_dut (.clk(clk), .rst_n(rst_n), .din(din), .dout(dout));
// ② 100 MHz 时钟:周期 10 ns
initial clk = 1'b0;
always #5 clk = ~clk;
// ③ 激励 task:高位先发,逐位注入
task automatic send_bits(input [7:0] data, input integer n);
integer k;
begin
for (k = n - 1; k >= 0; k = k - 1) begin
din = data[k]; // 阻塞赋值设置激励
@(posedge clk); // 等到上升沿:该 bit 被采样
end
end
endtask
// ④ 观测:每个时钟沿打印状态(dut.state 为层次引用)
always @(posedge clk)
$display("t=%0t ns | din=%b | state=%0d | dout=%b",
$time, din, u_dut.state, dout);
initial begin
$dumpfile("tb_seq_det_1011.vcd"); // 生成波形,GTKWave 打开
$dumpvars(0, tb_seq_det_1011);
// 复位:释放时刻(13ns)避开时钟沿,模拟真实异步复位
rst_n = 0; din = 0;
#13 rst_n = 1;
repeat (2) @(posedge clk);
// ── 测试 1:主用例 1 0 1 1 0 1 1 0 → 预期两次命中(重叠) ──
send_bits(8'b1011_0110, 8);
repeat (2) @(posedge clk);
// ── 测试 2:全 0 / 全 1 负用例 → 预期一次也不命中 ──
send_bits(8'b0000_0000, 8);
send_bits(8'b1111_1111, 8);
repeat (2) @(posedge clk);
$display("==== 仿真结束 ====");
$finish;
end
endmodule
// 运行(参考 08-02 页的 iverilog 工具链):
// iverilog -o sim tb_seq_det_1011.v seq_det_1011.v && vvp sim
// gtkwave tb_seq_det_1011.vcd
仿真预期波形(以主用例 8 bit 为例,行内数字均为"该时钟沿采样/更新后的值"):
| 采样沿 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|
| din | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 0 | — |
| state(沿后) | S1 | S2 | S3 | S4 | S2 | S3 | S4 | S2 | S0 |
| dout(沿后) | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 |
读波形要点:①沿 4 时 state 已进 S4,但寄存输出让 dout 在沿 5 才拉高——正好持续一个完整周期,供下游安全采样;②沿 5 收 0 回退到 S2(后缀 "10" 复用),沿 7 再次进 S4、沿 8 第二个脉冲——重叠检测成功;③负用例全 0/全 1 全程 dout=0,复位后 state 从 S0 出发。
@(posedge clk) 之后紧挨着改 din,和时钟沿"零建立时间",高频仿真会出现采样歧义——习惯上让激励变化与沿错开(如 @(negedge clk) 处更新);②复位释放放在整时钟沿上(#10),异步复位与时钟沿竞争,不同仿真器结果不同——错开几 ns;③忘了 $dumpvars 或层次路径写错,打开波形一片空白。是什么:Verilog 语法全集 ≠ 综合器认得的子集。综合器(综合视角)只把"能唯一对应成电路结构"的语法翻译成门和触发器;凡是涉及"仿真时间流逝"或"验证专用"的语句,一律被忽略或报错。判断口诀:这句话描述的是"电路长什么样"还是"仿真怎么跑"?前者可综合,后者不可。
| 语法结构 | 本来用途 | 可综合? | 综合器实际处理 |
|---|---|---|---|
initial 块 | 仿真 0 时刻初始化激励 | ASIC:否 / FPGA:部分支持 | ASIC 综合报错;Xilinx/Altera 对 FPGA 可把 reg 初值写进配置,但不可移植,RTL 初值仍推荐靠复位 |
#delay(如 #5 a=1;) | 仿真延时 | 否 | 综合时静默丢弃——仿真里"看起来有节奏",综合后一点节奏都没有,仿真-综合不一致的重灾区 |
fork...join | 并行仿真进程 | 否 | 报错/不支持 |
wait / @事件 | 等待事件 | 否 | 报错(testbench 专用) |
force / release | 强制信号(调试) | 否 | 仅仿真 |
$display/$monitor/$finish 等系统任务 | 打印/结束仿真 | 否 | 忽略或报错 |
real / 浮点运算 | 实数计算 | 基本否 | 数字前端用定点化,浮点交给专门的 FPU IP |
| assign / always / if / case / for(常量边界)/ function / generate | RTL 描述 | 是 | 翻译为组合逻辑、触发器、存储器等 |
综合器视角再看两件事:① 代码 = 约束:你写的每个 case 分支、"不可能发生"的假设,综合器都可能照单全收做成硬件——所以别指望"运行时条件不满足就不会执行"这种软件思维;② 延迟不参与综合:想控制速度靠流水线、约束(SDC)与工艺库,不是 #3。
// 典型的"仿真正常、综合翻车"代码
always @(posedge clk) begin
en = 1; // ← 时序块用阻塞赋值(2.4 的坑)
#3 data = din; // ← #3 被综合丢弃:仿真里 data 晚 3ns 变化,
// 综合后 data 与 din 之间没有任何延时结构
end
// 正确姿势:把"晚一拍"写成真正的寄存器,data <= din;
是什么/为什么:latch(锁存器)是电平敏感的存储单元——使能有效期间输出"跟随"输入,失效时"锁住"。它不是你想要的触发器:无法做静态时序分析、对毛刺敏感、ASIC 库中品质差。它几乎总是无意生成的:在组合 always @* 块中,只要存在"某些输入组合下输出没被赋值"的路径,综合器就只能用 latch "记住上次值"。
怎么做——三大生成场景与修复:
// 场景① if 缺 else
always @* begin
if (sel) y = a; // sel=0 时 y 没人管 → latch!
end
// 修复:补 else y = 1'b0; 或块首默认赋值 y = 1'b0;
// 场景② case 缺分支或缺 default
always @* begin
case (op)
2'b00: y = a + b;
2'b01: y = a - b; // 10/11 没写 → latch!
endcase
end
// 修复:补 default: y = 1'b0;(或先 y = 默认值)
// 场景③ 输出赋给了自己(反馈)且部分路径漏赋值
always @* begin
if (en) y = y & din; // en=0 时保持 → 这是刻意的 latch/或综合报错
end
// 若真要"保持"语义:说明你需要的是时序逻辑,改用 always @(posedge clk)
工程防线:①组合块入口先做默认赋值;②if 必配 else、case 必配 default;③打开综合/lint 工具的 latch 检查告警(Quartus/Vivado 会直接列出推断出的 latch,一个都不该有)。
是什么:一根 wire 同一时刻只能被一个驱动源驱动。两个 assign、两个 always、或"assign + 模块输出"同时驱动同一信号,就是多驱动冲突:
assign busy = flag_a | flag_b;
always @(posedge clk) busy <= done; // ★ busy 被第二处驱动 → 综合直接报错
// multiple (or conflicting) drivers
// 仿真里更隐蔽:多驱动 net 会发生"线或/线与"式的 x 值竞争,
// 波形上表现为信号莫名变 x、时对时错 —— bug 藏得极深。
怎么做:①一个信号一个驱动源;多个来源需要"汇合"时,显式写出合并逻辑(|、mux、总线仲裁器);②真要多驱动只有两种合法场景——三态总线(inout + 1'bz)与 testbench 的 force,片内逻辑禁用三态(FPGA 内部三态会被转换为 mux,ASIC 库也不提供内部三态);③拼接写法容易误驱动:output reg 已在 always 驱动,又在别处 assign,编译器有的报错、有的只 warning——把 warning 当 error 处理。
是什么:复位让电路进入确定的初始状态。两大流派:
| 对比项 | 同步复位(复位只出现在 always 的数据条件里) | 异步复位(敏感列表含复位沿,如 negedge rst_n) |
|---|---|---|
| 写法特征 | always @(posedge clk) if(!rst_n) q<=0; else ... | always @(posedge clk or negedge rst_n) if(!rst_n) q<=0; else ... |
| 时钟未起时能复位? | 不能(需要时钟采样复位) | 能(复位立即生效,上电即可用) |
| 抗毛刺 | 好(复位被时钟过滤) | 差(复位线上的毛刺直接打入寄存器) |
| 数据路径开销 | 复位信号进数据选择逻辑,增加 D 端组合 | 专用复位端,数据路径干净 |
| 典型选择 | ASIC 高频设计/复位可同步化的场合 | FPGA 与多数工程默认(器件有专用异步复位网络) |
怎么做——"异步复位、同步释放"工程标准:异步复位有两大隐患:①恢复/移除违例(release 时刻离时钟沿太近,部分寄存器"看到"复位、部分没看到,状态机解体);②复位毛刺敏感。标准解法是用两拍寄存器同步复位释放:
// 异步复位同步释放(Async reset, sync release)——每个时钟域一套
module reset_sync #(parameter W = 1)( // 简化:单bit演示
input clk, async_rst_n,
output reg rst_sync_n
);
reg rst_meta;
always @(posedge clk or negedge async_rst_n) begin
if (!async_rst_n) begin
{rst_sync_n, rst_meta} <= 2'b00; // 复位立即有效(异步)
end else begin
{rst_sync_n, rst_meta} <= {rst_meta, 1'b1}; // 释放打两拍(同步)
end
end
endmodule
// 复位树:每个时钟域一套 reset_sync;大设计还要考虑复位顺序(先核内后接口)。
posedge clk or rst_n(漏 negedge)→ 复位极性错,综合器推断出置位/清零逻辑错乱;②组合逻辑输出的复位(如 rst_n = en & rst_raw)带毛刺直接当异步复位用;③跨时钟域的复位没按域同步( rigor 见下一页 CDC)。是什么:例化时子模块端口位宽与实际连接信号位宽不一致,Verilog 不报 error:宽度不匹配会做隐式的零扩展/截断,只给一条容易淹没在编译日志里的 warning。
module counter #(parameter W = 8)(input clk, output reg [W-1:0] cnt);
always @(posedge clk) cnt <= cnt + 1'b1;
endmodule
// 顶层不小心接了 4 位信号:
wire [3:0] small_cnt;
counter #(.W(8)) u_cnt (.clk(clk), .cnt(small_cnt));
// 真实电路:计数器 8 位,但只引出低 4 位 → 上游读到的是"被截断的计数"
// 高位继续在芯片里翻转,白白耗功耗;仿真对比 golden 模型时"数值对不上"。
// 更阴险的是反方向:给 8 位端口接 1 位信号
counter #(.W(8)) u2 (.clk(clk), .cnt({7'b0, tick})); // 显式拼接,意图清晰
怎么做:①端口位宽从参数推导(#(.W(8)) 与信号声明 [W-1:0] 用同一参数),禁止手写两处数字;②lint 工具(Spyglass/Verilator -Wall)开 WIDTH 类告警并零容忍;③实在需要收窄/扩展,用显式的位选择或拼接写出意图,让 review 一眼看到。
是什么:RTL 仿真全部通过,综合/上板后行为不同甚至彻底失效。它不是单一 bug,而是一类"代码在两个世界语义不同"的问题。前文已埋了四颗雷,这里汇总成排查清单:
| 根因 | 仿真表现 | 上板/综合表现 | 预防 |
|---|---|---|---|
| 时序块用阻塞赋值(2.4) | 语句顺序执行,"碰巧"对 | 触发器级数/行为与仿真模型不符 | 组合 = / 时序 <= 铁律 |
| #delay 参与"功能"(第 5 节) | 有延时节奏 | 延时被静默丢弃,节奏消失 | 延拍一律用寄存器打拍实现 |
| 手写敏感列表漏信号(2.3) | 信号不变不触发,波形"卡住"或滞后 | 综合按完整组合逻辑生成,行为不同 | 组合块一律 always @* |
| 依赖 x 态/initial 初值 | x 传播或初值为 0/1 | 上电随机值,行为不确定 | 所有状态有真实复位;仿真加 x 检查 |
| 异步复位释放违例(6.3) | 复位释放与沿的关系理想化 | 个别寄存器晚一拍复位,状态机解体 | 异步复位同步释放 + STA 检查 |
怎么做——三条工程习惯:①RTL 只用"最小可综合子集"+ 固定的赋值/敏感列表规范,让两个世界"没有发挥空间";②仿真采用 gate-level 回归(综合后网表仿真)或至少跑综合后形式等价检查(LEC),抓结构性偏差;③把 lint(Spyglass/Verilator)与综合 warning 清零纳入提交门槛——绝大多数不一致在 warning 阶段就有信号。