第二章:Verilog基础回顾——模块结构、数据类型、连续赋值与过程赋值、时序逻辑与组合逻辑
各位同学,欢迎来到第二章。说实话,Verilog 基础这部分,很多老工程师都觉得「这有什么好讲的」。但我带过不少新人,发现他们写出来的代码,要么仿真跑不动,要么综合出一堆莫名其妙的 latch。所以这一章,咱们把地基打牢。
2.1 模块结构——芯片设计的「积木块」
Verilog 里最基本的单元就是 module。你可以把它想象成一块乐高积木。每个模块都有输入、输出,内部实现具体功能。
一个标准的模块长这样:
module counter (
input wire clk,
input wire rst_n,
input wire en,
output reg [3:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'b0;
else if (en)
count <= count + 1'b1;
end
endmodule
我个人习惯把端口声明和内部逻辑分开写。这样代码结构清晰,别人一看就知道哪些是接口,哪些是内部信号。
2.2 数据类型——wire 和 reg 到底怎么选?
这个问题,我几乎每次面试新人都会问。很多人背了答案,但一写代码就乱。
说白了,记住一句话:wire 是被驱动的,reg 是被赋值的。
- wire:用于组合逻辑,比如 assign 语句的输出。它不能存储值,只能反映输入的变化。
- reg:用于过程赋值,比如 always 块里的变量。它可以在时钟沿到来时保持状态。
举个例子:
wire a, b, c;
assign c = a & b; // c 是 wire,组合逻辑
reg [7:0] data_reg;
always @(posedge clk) begin
data_reg <= data_in; // data_reg 是 reg,时序逻辑
end
2.3 连续赋值与过程赋值——assign 和 always 的区别
嗯,这里要注意。很多新手分不清什么时候用 assign,什么时候用 always。
连续赋值(assign):
- 只能用于 wire 类型
- 赋值是持续的,输入一变,输出立刻变
- 适合简单的组合逻辑,比如加法、选择器
过程赋值(always):
- 可以用于 reg 类型
- 赋值在特定事件触发时执行(比如时钟上升沿)
- 适合复杂的逻辑,尤其是时序逻辑
来看个对比:
// 连续赋值:组合逻辑
assign sum = a + b;
// 过程赋值:时序逻辑
always @(posedge clk) begin
sum_reg <= a + b;
end
你想想看,如果我想实现一个带使能的加法器,用 assign 就不好使了。因为 assign 没有「条件控制」的能力。这时候必须用 always 块。
2.4 阻塞赋值与非阻塞赋值——仿真中的「大坑」
这个话题,我敢说每个芯片工程师都踩过坑。我自己刚入行时,就因为搞混了 = 和 <=,仿真结果和预期差了十万八千里。
阻塞赋值(=):
- 顺序执行,前面的赋值会阻塞后面的
- 用于组合逻辑的 always 块
非阻塞赋值(<=):
- 并行执行,所有赋值在时钟沿同时更新
- 用于时序逻辑的 always 块
看个经典例子:
// 错误示范:阻塞赋值用于时序逻辑
always @(posedge clk) begin
a = b;
c = a; // 这里 c 拿到的是 a 更新后的值,不是上一拍的值
end
// 正确示范:非阻塞赋值用于时序逻辑
always @(posedge clk) begin
a <= b;
c <= a; // 这里 c 拿到的是 a 上一拍的值,符合预期
end
2.5 时序逻辑与组合逻辑——芯片的「记忆」与「反应」
说白了,组合逻辑就是「看到什么输出什么」,没有记忆。时序逻辑就是「记住过去的状态」,有记忆。
组合逻辑的特点:
- 输出只取决于当前输入
- 没有时钟沿触发
- 用 assign 或 always @(*) 实现
时序逻辑的特点:
- 输出取决于当前输入和过去的状态
- 需要时钟沿触发
- 用 always @(posedge clk) 实现
来看个实际例子:
// 组合逻辑:译码器
always @(*) begin
case (sel)
2'b00: out = 4'b0001;
2'b01: out = 4'b0010;
2'b10: out = 4'b0100;
2'b11: out = 4'b1000;
default: out = 4'b0000;
endcase
end
// 时序逻辑:计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 4'b0;
else
cnt <= cnt + 1'b1;
end
2.6 实战建议——如何写出「可仿真、可综合」的代码
最后,分享几个我个人的习惯:
- 模块划分要清晰:一个模块只做一件事。比如计数器模块只计数,不要在里面加译码逻辑。
- 信号命名要规范:时钟信号用 clk,复位用 rst_n(低有效加 _n),使能用 en。这样别人一看就懂。
- 避免 latch:组合逻辑的 always 块里,所有条件分支都要覆盖完整。if 要有 else,case 要有 default。
- 仿真和综合要一致:仿真通过的代码,综合不一定能过。建议写代码时就考虑可综合性。
我记得有一次,一个新人写的代码仿真完全正确,但综合时报了一堆 warning。原因是他用了 initial 块来初始化寄存器。initial 块不可综合,仿真和综合的结果对不上。嗯,这个坑大家要记住。
好了,第二章就到这里。下一章我们讲仿真环境的搭建,包括 Testbench 怎么写、波形怎么看。这些都是实战中天天用的技能。