3. Verilog基础语法回顾:模块化设计、always块、assign语句、reg与wire类型、参数化设计
各位同学,欢迎来到我们串口实战课程的第一章。在正式开始撸串口代码之前,我觉得有必要把Verilog里最核心的几个语法点再过一遍。别嫌我啰嗦,这些基础打不牢,后面写状态机、调时序的时候,你肯定会回来翻笔记的。
我个人习惯是:先理解硬件思维,再写代码。Verilog不是C语言,你写的每一行代码,最终都会变成实实在在的门电路和触发器。所以,咱们今天聊的每一个语法,我都会告诉你它对应什么样的硬件。
3.1 模块化设计:把大工程拆成小积木
模块化设计,说白了就是“分而治之”。一个复杂的串口收发器,你不可能在一个module里写完所有逻辑——那样调试起来会让人崩溃的。
我刚开始做项目时,就吃过这个亏。一个UART模块写了800行,结果仿真波形一出来,发送和接收互相干扰,找bug找了整整两天。后来我把发送、接收、波特率生成拆成三个独立模块,问题一下子就清晰了。
一个典型的Verilog模块长这样:
module uart_tx (
input wire clk, // 系统时钟
input wire rst_n, // 异步复位,低有效
input wire tx_start, // 发送启动信号
input wire [7:0] tx_data, // 待发送的8位数据
output reg tx_busy, // 发送忙标志
output reg txd // 串行输出
);
// 内部逻辑写在这里
endmodule
看到没?每个模块都有明确的输入输出接口。就像搭积木一样,你只需要关心这个模块的接口定义,内部实现可以慢慢打磨。
3.2 always块:时序逻辑与组合逻辑的“分水岭”
always块是Verilog里最常用的过程块。但很多新手容易搞混:什么时候用always @(posedge clk),什么时候用always @(*)?
我给大家一个简单的判断标准:
- 要生成寄存器(触发器) → 用
always @(posedge clk or negedge rst_n) - 只是组合逻辑(纯门电路) → 用
always @(*)
举个例子,串口接收模块里,我们需要一个计数器来记录每一位的时间:
// 时序逻辑:生成寄存器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
bit_cnt <= 4'd0;
else if (rx_enable)
bit_cnt <= bit_cnt + 1'b1;
else
bit_cnt <= 4'd0;
end
// 组合逻辑:生成下一个状态
always @(*) begin
if (bit_cnt == 4'd8)
rx_done = 1'b1;
else
rx_done = 1'b0;
end
注意看,时序逻辑里我用的是非阻塞赋值 <=,组合逻辑里用的是阻塞赋值 =。这个区别很重要,我曾经见过有人混用导致仿真结果和实际硬件不一致,查了三天才发现是赋值方式的问题。
3.3 assign语句:连续赋值的“直通车”
assign语句,说白了就是“输入一变,输出立刻变”。它描述的是组合逻辑,没有时钟参与。
比如串口发送模块里,我们需要把波特率时钟分频出来:
// 波特率时钟使能信号
assign baud_en = (baud_cnt == BAUD_DIV - 1) ? 1'b1 : 1'b0;
这个assign语句,综合出来就是一个比较器加一个选择器。你想想看,它没有时钟沿触发,只要baud_cnt变化,baud_en立刻跟着变。
我个人习惯是:简单的组合逻辑用assign,复杂的用always @(*)。比如三目运算符能搞定的,就别写always块了,代码更简洁。
3.4 reg与wire类型:搞懂“存储”与“连线”
很多初学者被reg和wire搞得晕头转向。我给大家一个最直观的理解:
| 类型 | 硬件本质 | 赋值方式 | 典型用途 |
|---|---|---|---|
| wire | 一根导线 | assign 或 模块端口连接 | 组合逻辑输出、模块间连线 |
| reg | 一个存储单元 | always 块内赋值 | 寄存器、计数器、状态机 |
但这里有个容易误解的地方:reg不一定综合成寄存器。如果你在always @(*)里给reg赋值,它综合出来还是组合逻辑,只是写法上需要用reg类型。
举个例子:
wire a, b; // 输入信号,用wire
reg c; // 组合逻辑输出,但必须声明为reg
always @(*) begin
c = a & b; // 实际综合出来就是一个与门
end
嗯,这里要注意:端口声明时,input只能是wire,output可以是wire或reg。我一般习惯把模块内部的寄存器都声明为reg,连线都声明为wire,这样一目了然。
3.5 参数化设计:让代码“活”起来
参数化设计,说白了就是用参数代替常量。这样你改一个参数,整个模块的行为就变了,不用手动改代码里的每一个数字。
比如我们的串口波特率生成模块:
module baud_gen #(
parameter CLK_FREQ = 50_000_000, // 系统时钟频率 50MHz
parameter BAUD_RATE = 115200 // 目标波特率
) (
input wire clk,
input wire rst_n,
output reg baud_clk
);
// 计算分频系数
localparam DIV_CNT = CLK_FREQ / BAUD_RATE / 16;
reg [15:0] cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 16'd0;
else if (cnt < DIV_CNT - 1)
cnt <= cnt + 1'b1;
else begin
cnt <= 16'd0;
baud_clk <= ~baud_clk;
end
end
endmodule
看到没?#(parameter CLK_FREQ = 50_000_000, BAUD_RATE = 115200) 就是参数列表。以后换一个板子,时钟频率变成100MHz,我只需要在实例化时重新传参:
baud_gen #(
.CLK_FREQ(100_000_000),
.BAUD_RATE(9600)
) u_baud_gen (
.clk(clk),
.rst_n(rst_n),
.baud_clk(baud_clk)
);
这样代码的复用性一下子就上来了。我在项目中经常用参数化设计来适配不同速率的传感器,省了不少事。
- 模块化设计:一个模块只做一件事,接口清晰
- always块:时序逻辑用posedge,组合逻辑用@(*),赋值方式别搞混
- assign语句:连续赋值,适合简单组合逻辑
- reg与wire:reg不一定综合成寄存器,看你怎么用
- 参数化设计:用parameter代替常量,提高代码复用性
好了,这一章的基础语法就回顾到这里。这些内容虽然基础,但却是后面所有实战代码的基石。你想想看,如果连reg和wire都分不清,写出来的串口收发器能稳定工作吗?
下一章,我们就要开始搭建串口发送模块了。到时候你会发现,今天讲的这些语法,每一个都会用到。
可可精品资料库