二、Verilog基础与组合逻辑:模块化设计、assign语句、always块、阻塞与非阻塞赋值、位宽与数据类型
各位同学,咱们今天聊点实在的。Verilog 这东西,说白了就是硬件描述语言,不是软件编程。我见过太多新手把 Verilog 当 C 语言写,结果综合出来的电路完全不是那么回事。嗯,咱们今天就把这些基础但关键的点,一个一个掰扯清楚。
2.1 模块化设计:把大问题拆成小积木
我个人习惯,拿到一个通信协议解析任务,第一件事不是写代码,而是画框图。把整个系统拆成几个功能模块,比如:帧同步模块、CRC校验模块、数据提取模块。每个模块独立设计、独立仿真,最后再拼起来。
模块化设计的好处,说白了就是好维护、好复用。你想想看,如果所有代码都写在一个大模块里,出错了找 bug 得找到猴年马月去。
一个典型的模块声明长这样:
module frame_sync (
input wire clk, // 系统时钟
input wire rst_n, // 异步复位,低有效
input wire data_in, // 串行数据输入
output reg sync_done // 同步完成标志
);
// 模块内部逻辑
endmodule
这里要注意,端口方向要写清楚。input、output、inout,别搞混了。我在项目中遇到过有人把 input 写成 output,结果仿真怎么都不对,查了两天才发现是端口方向反了。
2.2 assign语句:组合逻辑的直通车
assign 语句,说白了就是连续赋值。它描述的是组合逻辑,没有时钟,没有寄存器。只要右边表达式变了,左边立刻跟着变。
举个例子,我们要实现一个 8 位数据的奇偶校验:
module parity_check (
input wire [7:0] data,
output wire even_parity,
output wire odd_parity
);
assign even_parity = ^data; // 异或归约,结果为1表示奇数个1
assign odd_parity = ~even_parity;
endmodule
你看,两行 assign 就搞定了。简洁、高效、一目了然。
2.3 always块:时序逻辑与组合逻辑的舞台
always 块是 Verilog 的核心。它既可以描述时序逻辑(带时钟),也可以描述组合逻辑(不带时钟)。
描述时序逻辑时,敏感列表里要有时钟沿:
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
counter <= 8'd0;
else if (enable)
counter <= counter + 1'b1;
end
描述组合逻辑时,敏感列表里要包含所有输入信号:
always @(*) begin
if (sel == 1'b0)
mux_out = a;
else
mux_out = b;
end
这里有个坑,我必须要说。组合逻辑的 always 块里,如果敏感列表不完整,综合出来的电路会多出锁存器。我曾经因为这个原因,导致一个简单的地址译码器多出了十几个锁存器,面积大了不少。
always @(*) 可以自动包含所有敏感信号,推荐使用。
2.4 阻塞赋值与非阻塞赋值:天壤之别
这个问题,我每次讲课都要强调。阻塞赋值 = 和非阻塞赋值 <=,用错了,电路行为就完全变了。
简单来说:
- 阻塞赋值(=):顺序执行,前面的赋值会立即影响后面的语句。用于组合逻辑。
- 非阻塞赋值(<=):并行执行,所有赋值在 always 块结束时同时更新。用于时序逻辑。
看个对比的例子:
// 阻塞赋值 - 组合逻辑
always @(*) begin
temp = a & b;
c = temp | d;
end
// 非阻塞赋值 - 时序逻辑
always @(posedge clk) begin
temp <= a & b;
c <= temp | d; // 这里用的是上一个时钟周期的 temp
end
在时序逻辑里用阻塞赋值,会导致仿真结果和实际电路不一致。我刚开始做 FPGA 时犯过这个错,仿真波形看着完美,上板子就是不对。后来发现是赋值方式用错了。
2.5 位宽与数据类型:细节决定成败
位宽这东西,看似简单,但出问题往往就在这。通信协议里,数据包的长度、CRC 的宽度、状态机的编码,都跟位宽有关。
先看数据类型:
| 类型 | 说明 | 默认值 |
|---|---|---|
| wire | 线网类型,用于 assign 赋值、模块端口连接 | 高阻态 z |
| reg | 寄存器类型,用于 always 块中赋值 | 不定态 x |
| integer | 32位有符号整数,常用于仿真 | 0 |
位宽声明时,要特别注意:
wire [7:0] data_bus; // 8位宽,data_bus[7]是最高位
wire [0:7] reverse_bus; // 也是8位宽,但 reverse_bus[0]是最高位
// 位宽不匹配的赋值
reg [3:0] small;
reg [7:0] large;
assign large = small; // 高位自动补0,没问题
assign small = large; // 高位被截断,可能丢失数据
我在做以太网 MAC 层解析时,遇到过一个问题。CRC 校验结果是 32 位,但我定义了一个 31 位的寄存器来存,结果校验永远不对。查了半天才发现是位宽少了一位。
2.6 本章知识体系
下面这张图,是我自己总结的本章知识结构。你可以把它当成一个检查清单,看看自己掌握了哪些。
嗯,这张图把今天讲的内容串起来了。你写代码的时候,可以对照着看看,有没有哪个环节没注意到。
最后说一句,Verilog 基础这东西,光看不行,得动手。找个简单的通信协议,比如 UART,自己从头写一遍,把模块化、assign、always、阻塞非阻塞、位宽这些都用上。写完了仿真,仿真过了上板子。这样走一遍,比看十遍书都管用。
公众号:蓝海资料掘金营,微信deep3321