Appearance
Verilog HDL 入门
关于
runnable:本站线上不提供代码运行,本机也没有 Verilog 仿真器(无 iverilog/Vivado)。所以本篇的 Verilog 代码是逐行人工审查的,而逻辑等效性用随文的 Python 行为级模型实际跑过验算。这也是"两种语言各自擅长什么"的一次现场演示。
概念
前面 15 篇,我们一直在画电路图。但当电路有几万个门、几十个状态时,画图就画不动了。
Verilog 要解决的就是这个问题:用文本描述电路,让工具去生成门级网表。
这个区别必须刻在脑子里,否则后面每一条语法都会用错:
| 软件程序(C / Python) | 硬件描述(Verilog) | |
|---|---|---|
| 执行方式 | 顺序执行,一行接一行 | 并发存在,所有语句同时生效 |
| 语句含义 | 让 CPU 做一件事 | 描述一根连线/一个门/一个触发器 |
| 变量 | 内存里的一块空间 | wire = 导线;reg = 需要赋值的信号 |
| 循环 | 真的反复执行 | 只是"复制粘贴"(展开) |
| 时间 | 程序流 | 时钟 |
"并发"这一条最能体现本质。下面两行 Verilog:
verilog
assign y1 = a & b;
assign y2 = c | d;没有任何先后关系——它们描述的是两个并排放着的门。(这和我们前 15 篇的电路图是一致的:门本来就没有"顺序"。)
再理解一句关键话:
所以 always @(posedge clk) 里的代码块不是"每次时钟沿执行一次循环",而是**"描述了一批触发器,它们的输入输出这样连着"**。这是初学者最大的认知障碍。
本层(L1 电路)在这里回答了上一层(L2 组成原理)的工程问题:L2 里那些"CPU 数据通路""控制器"的电路图,在现代工程里不是画出来的,是写出来的。这一篇就是"从电路图到 HDL"的桥。
原理
一、模块(module):电路的基本封装单位
Verilog-2001 的标准写法(推荐,端口方向+类型+位宽一次写完):
verilog
module 模块名 #(参数列表) (
input wire clk, // 输入
input wire [7:0] d, // 8 位输入
output reg [7:0] q // 8 位输出(在 always 中被赋值 → reg)
);
// ... 内部描述 ...
endmodule| 要点 | 说明 |
|---|---|
| 端口方向 | input / output / inout |
| 端口类型 | wire(连线,默认)或 reg(需要过程赋值) |
| 位宽 | [7:0] 表示 8 位,按"高位:低位"写;[0:7] 也合法但少见 |
#(参数) | 参数化设计(如把位宽做成参数),这是可复用模块的关键 |
| 一条铁律 | module 内部语句之间是并行的,与书写顺序无关 |
例化(实例化):把写好的模块当"零件"用:
verilog
// 按位置连接(不推荐,容易错)
// 按名字连接(推荐,清晰且不怕引脚顺序变)
mod10 u_counter (
.clk (clk),
.rst_n (rst_n),
.q (cnt)
);按名字连接是工程惯例。端口一多,按位置连接必然出错。
二、wire 与 reg:最容易搞混的一对
wire(线网) | reg(变量) | |
|---|---|---|
| 直观理解 | 一根导线 | 一个"需要被赋值"的信号名 |
| 怎么赋值 | assign 连续赋值 / 模块输出 / 门实例 | 只能在 always / initial 中赋值 |
| 能否存值 | 不能(值随时跟着驱动源变) | 语法上可以(但综合成什么取决于上下文!) |
| 默认值 | z(高阻) | x(未知) |
关于 reg 最重要的一句话:
- 在
always @(posedge clk)里赋值 → 综合出触发器 ✓ - 在
always @(*)里赋值 → 综合出纯组合逻辑(一根线的逻辑,没有触发器)✓
所以 reg 这个关键字只是语法规定("过程赋值必须用变量类型"),与"有无触发器"无关。 这一点是考试的高频陷阱。
那 output 什么时候要写 reg? 看它是怎么被赋值的:
verilog
module m1(output y, input a, input b);
assign y = a & b; // assign 赋值 → output 默认 wire,不用写 reg ✓
endmodule
module m2(output reg y, input a, input b);
always @(*) y = a & b; // always 中赋值 → 必须声明为 reg ✓
endmodule三、三种描述风格(同一电路,三种写法)
以一位全加器为例:
① 结构化描述(门级)—— 最接近我们画的电路图
verilog
module fa_gate (
input a, b, cin,
output s, cout
);
wire w1, w2, w3;
xor (w1, a, b); // w1 = a ^ b
xor (s, w1, cin); // s = a ^ b ^ cin
and (w2, a, b); // w2 = a & b
and (w3, w1, cin); // w3 = (a^b) & cin
or (cout, w2, w3); // cout = w2 | w3
endmodule② 数据流描述(assign)—— 直接写布尔式
verilog
module fa_data (
input a, b, cin,
output s, cout
);
assign s = a ^ b ^ cin;
assign cout = (a & b) | ((a ^ b) & cin);
endmodule③ 行为级描述 —— 描述"做什么",让工具去推电路
verilog
module fa_bhv (
input a, b, cin,
output reg s, cout
);
always @(*) begin
{cout, s} = a + b + cin; // 拼接赋值:2 位结果拆成 cout 和 s
end
endmodule三种风格的取舍:
| 风格 | 可读性 | 可控性 | 适用 |
|---|---|---|---|
| 结构化(门级) | 差(大电路看不到全貌) | 最高(精确控制) | 关键路径手工优化 |
数据流(assign) | 好 | 高 | 组合逻辑首选 |
行为级(always) | 好 | 中(靠工具推断) | 时序逻辑、算法级逻辑 |
工程惯例:组合逻辑用
assign,时序逻辑用always @(posedge clk)。这是最不容易出错的分工。
四、★ 阻塞赋值与非阻塞赋值:Verilog 最大的分水岭
为什么这个区别能决定电路对错?看一个"两级移位寄存器":
verilog
// ★ 正确写法:非阻塞 —— 两级移位寄存器(两个触发器)
always @(posedge clk) begin
a <= d; // a 拿到 d 的"旧"值
b <= a; // 这一句里的 a 是"本拍开始时的 a",不是刚更新的 a
end
// 结果:b 得到的是"上一拍的 a" → 真正的两级移位 ✓
// ✗ 错误写法:阻塞 —— 退化成一级(穿透)
always @(posedge clk) begin
a = d; // 立刻生效
b = a; // 此时 a 已经是 d 了!→ b 也直接得到 d
end
// 结果:两个触发器退化成"一个触发器 fan-out 到两处" → 少了一级 ✗用波形说明差别(输入
| 拍 | 非阻塞 | 非阻塞 | 阻塞 | 阻塞 | |
|---|---|---|---|---|---|
| 1 | 1 | 1 | 0(旧 | 1 | 1 ⚠️ |
| 2 | 0 | 0 | 1 | 0 | 0 ⚠️ |
| 3 | 1 | 1 | 0 | 1 | 1 ⚠️ |
非阻塞的
规则总结(背下来):
| 场景 | 用什么 | 理由 |
|---|---|---|
时序逻辑(always @(posedge clk)) | 非阻塞 <= | 建模"所有触发器同时更新" |
组合逻辑(always @(*)) | 阻塞 = | 建模"信号立即传播" |
同一个 always 块里不要混用 | — | 混用必然产生难以调试的仿真/综合不一致 |
一个信号只在一个 always 块里赋值 | — | 否则多驱动,综合报错 |
记忆口诀:"时序用
<=,组合用="。这条规则记住,能避开 90% 的 Verilog 坑。
五、数值表示与位宽
格式:<位宽>'<进制><数值>
| 写法 | 含义 |
|---|---|
4'b1010 | 4 位二进制 1010(= 10) |
8'hFF | 8 位十六进制 FF(= 255) |
4'd9 | 4 位十进制 9 |
8'o77 | 8 位八进制 77 |
'd10 | 位宽缺省(由上下文推断) |
1'b0 / 1'b1 | 单位常量(写 1'b1 而不是 1,避免位宽告警) |
4'b10xz | 含未知 x 与高阻 z(仿真用) |
位宽规则(考题常设):
| 规则 | 例子 |
|---|---|
| 赋值给更窄的目标 → 高位截断 | reg [3:0] q; q = 8'b1010_1100; → q = 1100 |
| 赋值给更宽的目标 → 高位补 0(无符号) | reg [7:0] q; q = 4'b1010; → q = 0000_1010 |
有符号数用 $signed / signed 声明 | 有符号右移用 >>>(补符号位) |
拼接 {} 的位宽 = 各段之和 | {cout, s} 是 2 位 |
重复 {n{...}} | {4{1'b0}} = 4'b0000 |
| 位宽不匹配会按"最大值"扩展 | 4'b1111 + 1'b1 → 扩展成 5 位算,再截断 |
常用运算符:
| 类别 | 运算符 |
|---|---|
| 位运算 | ~ & | ^ ~^(~^ 是同或) |
| 逻辑 | ! && || |
| 移位 | << >> <<< >>>(>>> 是算术右移) |
| 关系 | == != < <= > >= |
| 归约 | &a(所有位相与)、|a(相或)、^a(奇偶校验) |
| 拼接/重复 | {a,b}、{n{a}} |
归约运算符是硬件思维的好例子:
^a就是"把 a 的所有位异或起来"——一个多输入异或门。写奇偶校验位只需assign parity = ^data;。
六、★ 不完整的 if / case 会推断出锁存器
这是组合逻辑描述里最经典的坑,且和我们 13 篇的结论完全一致。
verilog
// ✗ 错误:en = 0 时 y 没有赋值 → "必须记住上次的值" → 综合出锁存器!
always @(*) begin
if (en) y = d;
end为什么:y 在 en=0 时没有被赋任何值,而组合逻辑的输出必须由输入唯一决定。既然"没写"就意味着"保持原值"——"保持"就是记忆,记忆就是锁存器(13 篇讲过:锁存器 = 电平敏感的透明存储)。
两种正确写法:
verilog
// 写法一:补全分支
always @(*) begin
if (en) y = d;
else y = 1'b0;
end
// 写法二(推荐):先给默认值
always @(*) begin
y = 1'b0; // 默认值,保证任何路径上 y 都被赋值
if (en) y = d;
endcase 同理:
verilog
// ✗ 缺少 default,且没有覆盖全部取值 → 可能推断 latch
always @(*) begin
case (sel)
2'b00: y = a;
2'b01: y = b;
// 2'b10 / 2'b11 没有赋值 → latch
endcase
end
// ✓ 补上 default
always @(*) begin
case (sel)
2'b00: y = a;
2'b01: y = b;
2'b10: y = c;
default: y = 1'b0; // 也可以 default: y = d;
endcase
end"想要锁存器怎么办"——不要用组合 always 去生成它,而是明确用带使能的时序逻辑(或直接用 14 篇讲的 LOAD 二选一):
verilog
always @(posedge clk) begin
if (en) q <= d; // 时序 always 中不写 else 是合法的:这就是"保持"
end关键区别:同样是"if 不写 else",在
always @(*)里是错误(推断 latch),在always @(posedge clk)里是正确的(这就是带使能的触发器)。 这两件事极易混淆,是考试的核心考点。
七、可综合 vs 只可仿真
| 语句 | 能综合? | 说明 |
|---|---|---|
assign、门实例、always @(*)、always @(posedge clk) | 能 | 描述真实硬件 |
parameter / localparam | 能 | 常量 |
initial | FPGA 可以用(初始化) | ASIC 一般不综合 |
#10(延时) | 不能 | 综合工具直接忽略(或报错) |
$display / $monitor / $dumpfile | 不能 | 只在仿真中有效 |
while / for(循环) | 能,但被展开 | 循环次数必须是编译期常量 |
force / release、wait | 不能 | 仿真专用 |
关于 for 循环的重要理解:
例如用 for 描述一个 8 位加法器:
verilog
// 这只生成 8 个全加器,不是"循环 8 次的硬件"
genvar i;
generate
for (i = 0; i < 8; i = i + 1) begin : FA
fa_gate u_fa (.a(a[i]), .b(b[i]), .cin(c[i]), .s(s[i]), .cout(c[i+1]));
end
endgenerate硬件上没有"循环"这回事——这是软件思维与硬件思维最根本的分界。
八、always @(*) 的敏感列表:为什么必须用 *
Verilog-95 要求手写敏感列表:
verilog
always @(a or b or cin) begin // 一旦漏写某个信号,仿真结果与综合结果不一致!
...
endVerilog-2001 引入了 @(*)(或 @*),自动包含块内读到的所有信号:
verilog
always @(*) begin // 推荐:绝不漏信号
...
end为什么手写敏感列表危险:综合工具只看逻辑功能(它不管敏感列表),而仿真器严格按敏感列表触发。漏写一个信号 → 仿真通过、上板不对。这是最经典的"仿真与综合不一致"来源。
同理,时序逻辑要写 posedge clk,不要写成电平敏感:
verilog
always @(posedge clk) ... // ✓ 边沿触发 → 触发器
always @(clk) ... // ✗ 电平敏感 → 可能推断锁存器九、testbench:不综合,只为仿真
testbench 是一个"没有端口"的模块,它的任务是给被测模块(UUT)加激励、看输出:
verilog
module tb_top;
reg clk = 0; // testbench 给 UUT 的信号 → reg
reg rst_n = 0;
reg x = 0;
wire z; // UUT 的输出 → wire
// 例化被测模块(按名字连接)
seq1101 uut (.clk(clk), .rst_n(rst_n), .x(x), .z(z));
// 产生时钟:每 5 个时间单位翻转一次 → 周期 10
always #5 clk = ~clk;
initial begin
$dumpfile("tb.vcd"); // 波形文件,给 GTKWave 看
$dumpvars;
#12 rst_n = 1; // 复位释放
#10 x = 1; #10 x = 1;
#10 x = 0; #10 x = 1; // 送入 1 1 0 1
#1 $display("t=%0t z=%b (期望 1)", $time, z);
#20 $finish;
end
endmodule| 要素 | 说明 |
|---|---|
always #5 clk = ~clk | 时钟发生器(#5 是延时,只在 testbench 里合法) |
initial begin ... end | 激励序列(顺序执行,且只执行一次) |
$display / $time | 打印(仿真专用) |
$dumpfile / $dumpvars | 导出波形给 GTKWave |
$finish | 结束仿真 |
注意
initial与always的区别:initial只执行一次,用于产生激励;always反复执行,用于产生时钟或描述硬件。testbench 里的initial是"软件思维"唯一合法的地方。
示例
例 1:全加器三种写法(对照 12 篇的电路图)
见上文第三节。三种写法综合出的电路应当完全等价——这正是 HDL 的价值:同一电路,可读性与可控性之间自由取舍。
行为级的那一句 {cout, s} = a + b + cin; 值得细看:
{cout, s} | ||||||
|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 00 | 0 | 0 |
| 0 | 0 | 1 | 1 | 01 | 0 | 1 |
| 0 | 1 | 1 | 2 | 10 | 1 | 0 |
| 1 | 1 | 1 | 3 | 11 | 1 | 1 |
与 12 篇的全加器真值表逐行一致 ✓ (和式
例 2:D 触发器(同步/异步复位 + 阻塞 vs 非阻塞)
① 异步复位——复位信号一有效就立刻清零,不依赖时钟:
verilog
module dff_arst (
input clk,
input rst_n, // 低有效
input d,
output reg q
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) q <= 1'b0;
else q <= d;
end
endmodule② 同步复位——复位只在时钟沿生效:
verilog
module dff_srst (
input clk,
input rst_n,
input d,
output reg q
);
always @(posedge clk) begin
if (!rst_n) q <= 1'b0;
else q <= d;
end
endmodule两者的硬件差别(本层视角):
| 异步复位 | 同步复位 | |
|---|---|---|
| 敏感列表 | posedge clk or negedge rst_n | 只有 posedge clk |
| 复位信号接哪 | 触发器的异步清零端(13 篇里 | D 端前面的组合逻辑 |
| 时钟不在也能复位 | 能 | 不能 |
| 抗复位毛刺 | 差(复位线上的毛刺直接生效) | 好 |
这就是 13 篇"触发器有异步清零端"那条知识在 Verilog 里的落点:
negedge rst_n写进敏感列表 = 接到端。
③ 阻塞 vs 非阻塞的现场演示(见上文第四节的波形表):同样的"两级移位",两种赋值方式得到完全不同的电路。
例 3:模 10 计数器(对照 14 篇的 74LS163 方案)
verilog
module mod10 (
input clk,
input rst_n,
output reg [3:0] q
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) q <= 4'd0;
else if (q == 4'd9) q <= 4'd0; // 检测 9 → 下一拍同步回 0
else q <= q + 4'b1;
end
endmodule与 14 篇的对应关系:
| 14 篇(MSI 器件) | 本篇(Verilog) |
|---|---|
| 4 位同步计数器芯片 74LS163 | 4 位 reg [3:0] q |
if (q == 4'd9) | |
| 同步清零 | else 分支在时钟沿生效(同步) |
| 4 个 JK/D 触发器 | always @(posedge clk) 推断出的 4 个触发器 |
这就是"行为级描述"的威力:14 篇要画卡诺图、求方程、连器件;这里只写了 3 行 if/else,工具自动综合出等价电路(且会自动用 4 个触发器 + 比较器 + MUX)。
但代价是"失去控制":工具综合出的电路延迟是多少、最高频率能到多少,你说了不算——这就是为什么 16 篇(时序分析)在 HDL 时代依然必须懂。
例 4:1101 序列检测器(对照 15 篇的五步设计)
verilog
module seq1101 (
input clk,
input rst_n,
input x,
output reg z
);
localparam S0 = 2'b00, S1 = 2'b01, S2 = 2'b10, S3 = 2'b11;
reg [1:0] state, next;
// ---- 组合逻辑:算次态(用阻塞赋值 =)----
always @(*) begin
case (state)
S0: next = x ? S1 : S0;
S1: next = x ? S2 : S0;
S2: next = x ? S2 : S3;
S3: next = x ? S1 : S0;
default: next = S0; // 保险分支,避免锁存器
endcase
end
// ---- 时序逻辑:状态寄存器(用非阻塞赋值 <=)----
always @(posedge clk or negedge rst_n) begin
if (!rst_n) state <= S0;
else state <= next;
end
// ---- Mealy 输出:含输入 x ----
always @(*) z = (state == S3) && x;
endmodule与 15 篇的对应关系(一个都不少):
| 15 篇(手算设计) | 本篇(Verilog) |
|---|---|
| 定义 4 个状态 | localparam 四行 |
| 状态表(现态 × 输入 → 次态) | case 的四行 |
| 卡诺图求 | 不需要(工具自动化简) |
| 2 个 D 触发器 | reg [1:0] state |
| Mealy 输出方程 | z = (state == S3) && x; |
两点必须注意:
next与state分成两块always,这是两段式(two-process)状态机写法——组合逻辑算next、时序逻辑存state。好处是输出不带毛刺、代码清晰。case必须有default。虽然state是 2 位、4 个取值全覆盖了,但加default是好习惯(防锁存器、防仿真时state为x时卡住)。
"两段式状态机"是工程上最推荐的写法,把 15 篇的"次态逻辑"和"状态寄存器"在代码上也分开——这正是 15 篇那张结构图(组合逻辑 → 寄存器 → 组合逻辑)的代码投影。
例 5:锁存器推断现场(★ 最经典的坑)
verilog
// ---- 模块 A:会推断出锁存器 ----
module bad_latch (
input en,
input d,
output reg y
);
always @(*) begin
if (en) y = d; // en=0 时 y 无赋值 → 综合出锁存器
end
endmodule
// ---- 模块 B:纯组合逻辑(修复后)----
module good_mux (
input en,
input d,
output reg y
);
always @(*) begin
y = 1'b0; // 默认值(关键)
if (en) y = d;
end
endmodule两者的真值表差异(这正是"锁存"与"组合"的分界):
| 模块 A(latch) | 模块 B(组合) | ||
|---|---|---|---|
| 0 | 0 | 保持原值(记忆!) | 0 |
| 0 | 1 | 保持原值(记忆!) | 0 |
| 1 | 0 | 0 | 0 |
| 1 | 1 | 1 | 1 |
用序列验证 A 的"记忆"行为(初值
| 拍 | A 的 | B 的 | |
|---|---|---|---|
| 1 | (1,1) | 1 | 1 |
| 2 | (0,0) | 1(保持!) | 0 |
| 3 | (0,1) | 1(保持!) | 0 |
| 4 | (1,0) | 0 | 0 |
看第 2、3 拍:模块 A 的输出与当前输入无关(因为它在"记")——它已经不是组合逻辑了。
为什么这很危险:锁存器是电平敏感的(13 篇),在同步设计里会带来时序分析的噩梦(工具难以分析、可能出现"毛刺穿透")。所以综合工具会报 warning,而正经代码里必须消灭它。
三个消灭方法:
| 方法 | 写法 |
|---|---|
| 补全分支 | if (en) y = d; else y = 1'b0; |
| 先给默认值(最推荐) | y = 1'b0; if (en) y = d; |
case 加 default | default: y = 1'b0; |
例 6:testbench 与仿真预期
见上文第九节的 tb_top。对 seq1101 送入 1 1 0 1 的仿真预期(与 15 篇的逐拍表一致):
| 时间 | 事件 | state | z |
|---|---|---|---|
| 0–12 | 复位中 | 0 | |
| 12 | rst_n = 1 | 0 | |
| 22 | 第 1 个 x=1 采到 | 0 | |
| 32 | 第 2 个 x=1 采到 | 0 | |
| 42 | 第 3 个 x=0 采到 | 0 | |
| 52 | 第 4 个 x=1 采到 | 1 ← 第 4 拍命中 |
Mealy 输出的注意点:
z是组合输出,它在x变成 1 的那一刻就变 1(不用等时钟沿);等到时钟沿,状态才回到、 z随之回 0。所以z是一个"宽度约一个时钟周期的脉冲",仿真时如果采样时刻不对,就会看不到它——这是 Mealy 型 testbench 最常见的"假失败"。
例 7:用 Python 做行为级对照验算
本机没有 Verilog 仿真器,所以把上面三个 Verilog 模块的行为用 Python 重写一遍,实跑核对(本篇 runnable: false,代码块在线上只作展示):
python
# ============================================================
# 本脚本 = 上面 Verilog 模块的"行为级对照"
# 目的:不依赖 Verilog 仿真器,验证逻辑本身正确
# ============================================================
# ---------- 例1:全加器(对照 fa_gate / fa_data / fa_bhv)----------
print("=== 例1 全加器:三种描述风格应完全等价 ===")
def fa_gate(a, b, cin): # 结构化(门级)
w1 = a ^ b
s = w1 ^ cin
w2 = a & b
w3 = w1 & cin
return s, (w2 | w3)
def fa_data(a, b, cin): # 数据流(assign)
return (a ^ b ^ cin), ((a & b) | ((a ^ b) & cin))
def fa_bhv(a, b, cin): # 行为级({cout,s} = a+b+cin)
t = a + b + cin # 0..3
return (t & 1), ((t >> 1) & 1) # s, cout
ref = [(0, 0, 0, 0, 0), # (a,b,cin,s,cout)
(0, 1, 1, 0, 1),
(1, 0, 1, 0, 1),
(1, 1, 1, 1, 1)]
bad = []
for a in (0, 1):
for b in (0, 1):
for c in (0, 1):
if fa_gate(a, b, c) != fa_data(a, b, c) or \
fa_gate(a, b, c) != fa_bhv(a, b, c):
bad.append((a, b, c))
for row in ref:
a, b, cin, s, co = row
got = fa_bhv(a, b, cin)
print(f" a={a} b={b} cin={cin} -> s={got[0]} cout={got[1]}"
f"{' ✓' if got == (s, co) else ' ✗'}")
print(f" 三种写法在全部 8 组输入下等价: {'PASS' if not bad else 'FAIL'}")
# ---------- 例2:阻塞 vs 非阻塞(两级移位寄存器)----------
print("\n=== 例2 阻塞 vs 非阻塞(两级移位寄存器) ===")
d_seq = [1, 0, 1, 1]
# 非阻塞:右侧读到的是"本拍开始时的旧值" → 真正的两级移位
a = b = 0
nb_log = []
for d in d_seq:
a_new, b_new = d, a # 同时更新
a, b = a_new, b_new
nb_log.append((d, a, b))
# 阻塞:a = d 立即生效,随后 b = a 看到的是新的 a → 退化成一级
a2 = b2 = 0
bl_log = []
for d in d_seq:
a2 = d # 立即生效
b2 = a2 # 此时 a2 已经是 d
bl_log.append((d, a2, b2))
print(" 拍 d 非阻塞(a,b) 阻塞(a,b)")
for i, (nb, bl) in enumerate(zip(nb_log, bl_log), 1):
print(f" {i} {nb[0]} ({nb[1]},{nb[2]}) ({bl[1]},{bl[2]})")
print(" 非阻塞: b 比 a 晚一拍 -> 两级移位 ✓")
print(" 阻塞: a 恒等于 b -> 退化成一级 ☠",
"(a==b:", all(x[1] == x[2] for x in bl_log), ")")
# ---------- 例3:模 10 计数器(对照 mod10)----------
print("\n=== 例3 模10计数器(Verilog mod10 的行为) ===")
q, seq = 0, []
for _ in range(20):
seq.append(q)
if q == 9:
q = 0 # 同步清零(else 分支在时钟沿生效)
else:
q = (q + 1) & 0xF
print(" 序列:", seq, "-> 状态数", len(set(seq)))
# ---------- 例4:1101 序列检测器(对照 seq1101)----------
print("\n=== 例4 seq1101(两段式状态机行为) ===")
S0, S1, S2, S3 = 0, 1, 2, 3
def nxt(s, x):
return {(S0, 0): S0, (S0, 1): S1,
(S1, 0): S0, (S1, 1): S2,
(S2, 0): S3, (S2, 1): S2,
(S3, 0): S0, (S3, 1): S1}[(s, x)]
def run(bits):
st, rows = S0, []
for x in bits:
z = int((st == S3) and x) # Mealy:组合输出
ns = nxt(st, x)
rows.append((x, st, ns, z))
st = ns
return rows
for row in run([1, 1, 0, 1]):
print(f" x={row[0]} 现态S{row[1]} 次态S{row[2]} z={row[3]}")
from itertools import product
bad = []
for n in range(1, 9):
for bits in product((0, 1), repeat=n):
got = [r[3] for r in run(bits)]
s = "".join(map(str, bits))
exp = [1 if s[:i + 1].endswith("1101") else 0 for i in range(n)]
if got != exp:
bad.append(bits)
print(" case 表 vs 后缀匹配参考模型(1~8 位全枚举):",
"PASS" if not bad else "FAIL")
# ---------- 例5:锁存器推断(latch vs 组合)----------
print("\n=== 例5 if 不完整 -> 锁存器 vs 组合 ===")
def module_A(y, en, d): # always @(*) if (en) y = d;
return d if en else y # en=0 时"保持" -> latch
def module_B(y, en, d): # always @(*) begin y = 0; if (en) y = d; end
y = 0
return d if en else y
seq_in = [(1, 1), (0, 0), (0, 1), (1, 0)]
ya = yb = 0
print(" 拍 (en,d) A(latch) B(组合)")
for i, (en, d) in enumerate(seq_in, 1):
ya = module_A(ya, en, d)
yb = module_B(yb, en, d)
print(f" {i} ({en},{d}) {ya} {yb}")
print(" A 在 en=0 时保持 -> 与当前输入无关(这就是'记忆')")输出要点:
- 例 1:三种写法在全部 8 组输入下等价(对照 12 篇的真值表)。
- 例 2:非阻塞的
比 晚一拍;阻塞的 恒等于 (退化成一级)——证实了第四节的结论。 - 例 3:模 10 计数器恰好 10 个状态(与 14 篇一致)。
- 例 4:
case表与"后缀匹配"参考模型在 1~8 位全枚举下完全一致——与 15 篇的方程设计结果相同。 - 例 5:模块 A 在
en=0时输出与输入无关("记忆"),模块 B 是纯组合。
考点
考点
1. 必背规则
2. 阻塞 vs 非阻塞(★ 头号考点)
= 阻塞 | <= 非阻塞 | |
|---|---|---|
| 生效时机 | 立即 | 同一时刻统一更新 |
| 后续语句看到 | 新值 | 旧值 |
| 用于 | 组合逻辑 | 时序逻辑 |
| 典型后果 | 两级移位退化成一级 | 正确的多级流水/移位 |
口诀:"时序 <=,组合 ="。
3. 锁存器推断(★ 高频)
| 写法 | 结果 |
|---|---|
always @(*) if (en) y = d; | 锁存器(en=0 时"保持") |
always @(*) begin y = 0; if (en) y = d; end | 组合逻辑 ✓ |
always @(*) if (en) y = d; else y = 0; | 组合逻辑 ✓ |
always @(*) case(...) ... endcase 缺 default | 可能锁存器 ⚠️ |
always @(posedge clk) if (en) q <= d; | 带使能的触发器 ✓(正确!) |
最后一行的对比是本题的精髓:"if 不写 else"在组合里是 bug,在时序里是 feature。
4. 高频陷阱
- 把 Verilog 当 C 写:以为语句按顺序执行。模块内语句是并行的,
always块之间也是并行的。 - 在多个
always块里给同一个reg赋值 → 多驱动,综合报错。 assign和always混着给同一个信号赋值 → 冲突。- 手写敏感列表漏信号(Verilog-95 风格)→ 仿真与综合不一致。用
@(*)。 - 用
#10给硬件模块加延时 → 不可综合。延时只能出现在 testbench 里。 - 以为
for循环会产生时序 → 它只是被"展开"成多份硬件,循环边界必须是常量。 - 位宽不匹配:
4'b1111 + 1'b1会先扩展再算,别指望"自动截断刚好对"。 - 写
1而不是1'b1做条件 → 功能通常对,但位宽告警多,且在有符号/位宽敏感场景会出问题。 output y却想用always赋值 → 必须写成output reg y(或 Verilog-2001 里output logic,SystemVerilog)。- 忘记
default:case覆盖不全时,state若因干扰进入未用编码,仿真会一路x卡死。这与 15 篇的"自启动"是同一件事。
5. 与后续章节的接口
- Verilog 是 L2 组成原理"用 HDL 描述 CPU"的工具:本站
arch课程的数据通路与硬布线控制器,都可以直接翻译成always块。 - 综合出来的电路有多快,取决于关键路径 → 下一篇
circuit/21-fpga.md与circuit/16-timing.md(时序分析)是同一件事的两面。 - L3
lang课程会讲"高级语言 → 汇编 → 机器码";而这一篇讲的是**"HDL → 门级网表"**。两条链路的中间层都是"逻辑方程",只是方向不同。
小结
- HDL 是"用文本描述硬件结构",不是"按顺序执行的程序"。模块内语句并行、
for循环被展开、#延时不可综合——这三条都是同一件事的不同侧面。 wire是导线(assign驱动),reg是"需要过程赋值的信号名";reg不等于触发器——组合always里的reg就是纯组合逻辑。- 三种描述风格:结构化(门级,可控但难读)、数据流(
assign,组合逻辑首选)、行为级(always,时序与算法级首选)。 - ★ 阻塞
=用于组合、非阻塞<=用于时序。用错的话,两级移位寄存器会退化成一个——这是 Verilog 最经典的分水岭。 - ★ 组合
always里if不写else/case不写default→ 推断出锁存器(因为"没写"意味着"保持",而"保持"就是记忆,见 13 篇)。同样的写法在时序always里完全正确。 @(*)自动敏感列表取代 Verilog-95 的手写列表,从根上消灭"仿真与综合不一致"。- testbench 不综合:
initial+#延时+$display是"软件思维"唯一合法的角落。
回到主线:写好的 Verilog 要落到真实的芯片上。它会被综合成什么?在 FPGA 里怎么烧进去?一个 200 MHz 的设计是怎么"收敛"的? 这是下一篇的主题。
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。