3. 硬件描述语言基础(Verilog):模块结构、数据类型、连续赋值与过程赋值、组合逻辑与时序逻辑
好,咱们开始聊Verilog。说实话,很多初学者把Verilog当成C语言来写,这是个大坑。我见过不少项目,代码写得像C,综合出来一堆莫名其妙的Latch。嗯,咱们得从一开始就摆正心态——Verilog是描述硬件的,不是写软件的。
3.1 模块结构:你的第一个数字电路
Verilog的基本单元叫模块(module)。你可以把它想象成一个黑盒子,有输入、有输出,里面装着你要实现的逻辑。
一个典型的模块长这样:
module counter (
input wire clk, // 时钟
input wire rst_n, // 复位,低有效
input wire en, // 使能
output reg [3:0] count // 4位计数器输出
);
// 时序逻辑:计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'd0;
else if (en)
count <= count + 1'b1;
end
endmodule
我个人习惯把端口声明和内部信号分开写,这样结构清晰。你看,input和output后面跟了wire或reg,这涉及到数据类型,咱们马上讲。
uart_tx、spi_master。别用驼峰,也别用中文拼音,不然同事会找你麻烦。
3.2 数据类型:wire 和 reg 到底怎么选?
这是新手最容易懵的地方。说白了:
- wire:连线。它不存储值,只是把两个端口连起来。用在assign语句、模块端口连接。
- reg:寄存器。它存储值,用在always块里被赋值。
但注意!reg不一定综合成寄存器。在组合逻辑的always块里,reg只是行为描述,综合出来还是连线。我刚开始做设计时也犯过这个错,以为always块里用reg就一定是触发器,结果综合报告里一堆组合逻辑。
看个例子:
// 组合逻辑:用reg描述,但综合出来是wire
reg [3:0] sum;
always @(*) begin
sum = a + b;
end
// 时序逻辑:用reg描述,综合出来是触发器
reg [3:0] count;
always @(posedge clk) begin
count <= count + 1;
end
还有几个常用的数据类型:
- integer:32位有符号整数,常用于循环变量。别用它描述硬件,只用在testbench或generate循环里。
- parameter:常量,用来定义位宽、状态等。比如
parameter WIDTH = 8;。
reg [N-1:0],别用integer。
3.3 连续赋值 vs 过程赋值:两种思维方式
连续赋值用assign,过程赋值用always块。它们代表了两种不同的硬件描述方式。
3.4.1 连续赋值(assign)
说白了,assign就是一根导线,左边输出等于右边输入的组合逻辑。它总是激活的,只要右边变了,左边立刻更新。
// 连续赋值:描述组合逻辑
assign sum = a + b;
assign full = (count == 4'd9);
你想想看,assign不能用在always块里,也不能给reg类型赋值。它只能驱动wire。
3.4.2 过程赋值(always块)
always块里可以写两种赋值:
- 阻塞赋值(=):顺序执行,像C语言。用于组合逻辑。
- 非阻塞赋值(<=):并行执行,所有赋值同时更新。用于时序逻辑。
这是Verilog里最容易出bug的地方。我见过一个同事,在时序逻辑里用了阻塞赋值,结果仿真和综合结果完全不一样。为什么呢?
看这个例子:
// 错误写法:时序逻辑用阻塞赋值
always @(posedge clk) begin
a = b;
c = a; // 这里c得到的是b的新值,不是a的旧值
end
// 正确写法:时序逻辑用非阻塞赋值
always @(posedge clk) begin
a <= b;
c <= a; // 这里c得到的是a的旧值,符合寄存器行为
end
为什么会这样?因为非阻塞赋值模拟了寄存器的行为——所有寄存器在时钟沿同时采样输入,然后同时更新输出。阻塞赋值则是顺序执行,破坏了这种并行性。
- 组合逻辑用
always @(*)+ 阻塞赋值= - 时序逻辑用
always @(posedge clk)+ 非阻塞赋值<= - 千万别混用!
3.4 组合逻辑 vs 时序逻辑:本质区别
这两个概念是数字电路的核心。我简单总结一下:
| 特性 | 组合逻辑 | 时序逻辑 |
|---|---|---|
| 输出依赖 | 仅当前输入 | 当前输入 + 历史状态 |
| 存储能力 | 无 | 有(寄存器) |
| 时钟 | 不需要 | 需要时钟沿触发 |
| Verilog描述 | assign 或 always @(*) | always @(posedge clk) |
| 典型电路 | 加法器、多路选择器、译码器 | 计数器、移位寄存器、状态机 |
组合逻辑说白了就是一堆门电路,输入一变,输出立刻变。时序逻辑则像有个记忆,它记得之前的状态。
举个例子:
// 组合逻辑:加法器
assign result = a + b;
// 时序逻辑:累加器
always @(posedge clk) begin
if (rst_n)
accumulator <= 0;
else
accumulator <= accumulator + a;
end
你看,加法器没有记忆,每次输入a和b,输出就是它们的和。累加器则记住了之前的值,每次时钟沿来,它把当前值加上a,再存回去。
3.5 实战:一个简单的数据通路
咱们把今天学的串起来,写一个简单的数据通路:输入两个数,相加后寄存,然后输出。
module data_path (
input wire clk,
input wire rst_n,
input wire [7:0] data_a,
input wire [7:0] data_b,
output reg [8:0] result
);
// 组合逻辑:加法
wire [8:0] sum;
assign sum = data_a + data_b;
// 时序逻辑:寄存结果
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
result <= 9'd0;
else
result <= sum;
end
endmodule
这个例子很简单,但包含了今天讲的所有要素:模块结构、wire和reg、连续赋值、过程赋值、组合逻辑和时序逻辑。你想想看,assign sum是组合逻辑,always块是时序逻辑。两者通过wire连接,完美配合。
好了,这一章的内容就这些。记住,Verilog不是软件,是硬件。写代码时多想想它综合出来是什么电路,而不是它运行起来是什么结果。下一章咱们聊更深入的话题——如何写出可综合的、高效的Verilog代码。