4. Verilog基础(上):模块化设计、组合逻辑、时序逻辑
各位同学,欢迎来到第四讲。今天咱们聊聊Verilog里最核心的三个东西:模块化设计、组合逻辑、时序逻辑。说白了,这就是你写FPGA代码的“三块积木”。
我刚开始学Verilog的时候,总觉得这玩意儿跟C语言差不多,不就是if-else、case嘛。结果第一次写计数器,没加时钟沿,仿真跑出来一堆毛刺。嗯,从那以后我就老实了——硬件语言,你得用硬件的思维去理解它。
核心观点:Verilog不是软件,是描述硬件的语言。你写的每一行代码,最终都会变成实实在在的门电路、触发器和连线。
4.1 模块化设计:把大问题拆成小积木
模块化设计,说白了就是“分而治之”。一个复杂的系统,你不可能在一个module里写完所有逻辑。我个人的习惯是:每个模块只干一件事,干好一件事。
举个例子,你要做一个行情解析器。你可以拆成:
- 数据接收模块:负责从网口拿原始数据
- 协议解析模块:把二进制流拆成字段
- 行情缓存模块:把解析后的数据存到BRAM里
- 输出控制模块:按需把数据送给下游
每个模块独立设计、独立仿真、独立测试。最后再拼起来。这样做的好处是:
- 好调试:出问题了,定位到具体模块就行
- 好复用:下次做类似项目,直接拿过来改改
- 好维护:改一个模块,不影响其他模块
我的经验:模块的接口越简单越好。我见过有人一个模块传20多个参数,看着就头疼。接口信号控制在10个以内,可读性会好很多。
下面是一个简单的模块模板:
module counter #(
parameter WIDTH = 8
)(
input wire clk,
input wire rst_n,
input wire en,
output reg [WIDTH-1:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 0;
else if (en)
count <= count + 1;
end
endmodule
你看,这个模块只做一件事:计数。输入只有时钟、复位、使能,输出就是计数值。简单、清晰、好用。
4.2 组合逻辑:用assign和always@(*)搭电路
组合逻辑,就是输出只跟当前输入有关,跟历史无关。说白了,就是“纯函数”——输入一变,输出马上变。
Verilog里写组合逻辑有两种方式:
- assign语句:适合简单的赋值
- always@(*)块:适合复杂的逻辑
4.2.1 assign语句
assign就是连续赋值。你想想看,它就像一根导线,输入变了,输出立刻跟着变。
// 简单的加法器
assign sum = a + b;
// 多路选择器
assign out = sel ? data1 : data0;
// 位拼接
assign result = {a, b, c};
我在项目中遇到过一个问题:用assign写了一个复杂的表达式,结果综合出来面积特别大。后来发现是表达式里隐含了乘法器。嗯,这里要注意——assign虽然方便,但复杂的运算还是要拆开写,或者用流水线。
4.2.2 always@(*)块
always@(*)是组合逻辑的“瑞士军刀”。它比assign灵活,可以写if-else、case等结构。
always @(*) begin
case (sel)
2'b00: out = data0;
2'b01: out = data1;
2'b10: out = data2;
2'b11: out = data3;
default: out = 0;
endcase
end
避坑指南:我曾经在always@(*)里漏写了某个分支,结果综合出来一个锁存器(latch)。仿真没问题,上板子就出bug。后来我养成了一个习惯:case语句一定要写default,if-else一定要写全。
为什么会这样?因为组合逻辑必须“每时每刻都有输出”。如果你漏了某个条件,综合器会认为“保持原值”,于是就给你生成一个锁存器。这在大多数情况下不是你想要的。
4.3 时序逻辑:用always@(posedge clk)记住状态
时序逻辑,就是输出不仅跟当前输入有关,还跟历史状态有关。说白了,就是“有记忆”的电路。
在Verilog里,时序逻辑用always@(posedge clk)来描述。时钟上升沿触发,数据在时钟边沿更新。
// 一个简单的D触发器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 0;
else
q <= d;
end
注意这里用的是非阻塞赋值(<=),不是阻塞赋值(=)。这是新手最容易犯的错误。
黄金法则:时序逻辑用非阻塞赋值(<=),组合逻辑用阻塞赋值(=)。混着用?我劝你别试,仿真结果会让你怀疑人生。
下面是一个带使能的计数器,在金融行情解析里经常用到:
// 可配置的计数器,用于行情数据包计数
module packet_counter #(
parameter MAX_PACKET = 1000
)(
input wire clk,
input wire rst_n,
input wire packet_valid,
output reg [15:0] packet_count,
output reg overflow
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
packet_count <= 0;
overflow <= 0;
end else if (packet_valid) begin
if (packet_count == MAX_PACKET) begin
packet_count <= 0;
overflow <= 1;
end else begin
packet_count <= packet_count + 1;
overflow <= 0;
end
end
end
endmodule
你看,这个模块在时钟上升沿判断:如果收到有效包,就计数;如果达到最大值,就清零并置溢出标志。这就是典型的时序逻辑。
4.4 组合逻辑 vs 时序逻辑:一张表说清楚
| 对比项 | 组合逻辑 | 时序逻辑 |
|---|---|---|
| 输出依赖 | 只依赖当前输入 | 依赖当前输入+历史状态 |
| 触发方式 | 输入变化立即触发 | 时钟边沿触发 |
| 赋值方式 | 阻塞赋值(=) | 非阻塞赋值(<=) |
| 典型结构 | assign、always@(*) | always@(posedge clk) |
| 硬件实现 | 门电路、多路选择器 | 触发器、寄存器 |
| 常见问题 | 锁存器、竞争冒险 | 亚稳态、时序违例 |
4.5 本章知识体系
下面我用一张图来总结本章的核心内容。这张图展示了模块化设计、组合逻辑、时序逻辑三者之间的关系:
这张图想表达的是:模块化设计是顶层思想,它把系统拆成多个模块。每个模块内部,要么是组合逻辑(处理数据),要么是时序逻辑(记住状态)。两者配合,才能做出一个完整的硬件模块。
我的建议:刚开始写Verilog的时候,先画框图,再写代码。把组合逻辑和时序逻辑分开写,不要混在一个always块里。这样代码清晰,综合结果也漂亮。
好了,这一讲就到这里。记住三个关键词:模块化、组合逻辑、时序逻辑。下一讲我们会深入讨论更复杂的时序控制,比如状态机和流水线。到时候见。
公众号:蓝海资料掘金营,微信deep3321