4. 硬件描述语言速通(Verilog):模块化设计、组合逻辑与时序逻辑、阻塞与非阻塞赋值
各位同学,欢迎来到第四章。说实话,很多刚接触FPGA的朋友,一上来就被Verilog的语法细节给劝退了。其实大可不必。你想想看,Verilog说到底就是描述硬件的一种语言,跟C语言写软件完全是两码事。今天这一章,我带你快速过一遍最核心的三个概念:模块化设计、组合逻辑与时序逻辑、阻塞与非阻塞赋值。这些都是我这些年做项目天天打交道的东西,搞懂了它们,你写代码会顺手很多。
4.1 模块化设计:把大问题拆成小积木
做FPGA设计,最忌讳的就是把所有逻辑塞进一个文件里。我在做期货行情解析加速器的时候,一开始也犯过这个错——一个模块写了上千行,结果调试起来简直要命。后来我学乖了,把整个系统拆成一个个小模块,每个模块只干一件事。
模块化设计的核心思想,说白了就是“分而治之”。一个复杂的系统,比如我们的行情解析引擎,可以拆成:
- 数据接收模块:负责从网络接口拿原始数据
- 协议解析模块:把二进制协议翻译成内部格式
- 行情计算模块:算最新价、涨跌幅这些指标
- 输出接口模块:把结果送给下游的交易系统
每个模块都是一个独立的module,有自己的输入输出端口。模块之间通过信号线连接,就像搭积木一样。这样做的好处很明显:
- 可复用:写好的模块,下个项目直接拿来用
- 易调试:出问题了,定位到具体模块就行
- 好维护:改一个模块,不影响其他部分
重要:模块的端口定义要清晰。我建议每个模块的输入输出信号,命名时加上方向前缀,比如i_表示输入,o_表示输出,io_表示双向。这样看代码一目了然。
下面是一个简单的模块示例,一个加法器:
module adder (
input wire [7:0] i_a,
input wire [7:0] i_b,
output wire [8:0] o_sum
);
assign o_sum = i_a + i_b;
endmodule
你看,这个模块只做加法,输入两个8位数,输出一个9位数(防止溢出)。简单、干净、好理解。
4.2 组合逻辑与时序逻辑:硬件的两种“性格”
搞清楚了模块化,接下来要理解硬件的两种基本逻辑类型。这俩概念,我当年学的时候也绕了好一阵子。
组合逻辑,就是输出只取决于当前输入。输入一变,输出立刻跟着变,没有记忆功能。比如上面的加法器,你给i_a=3、i_b=5,输出马上就是8。组合逻辑用assign语句或者always @(*)块来实现。
时序逻辑,则是有记忆功能的。输出不仅取决于当前输入,还取决于之前的状态。时序逻辑靠时钟沿(上升沿或下降沿)来触发更新。比如一个寄存器,它在时钟上升沿才把输入锁存到输出。时序逻辑用always @(posedge clk)块来实现。
我在项目中遇到过一个问题:有人把组合逻辑和时序逻辑混在一个always块里写,结果仿真和实际行为对不上。嗯,这里要注意,组合逻辑和时序逻辑最好分开写,各司其职。
小技巧:写组合逻辑时,用always @(*),里面的变量都要用阻塞赋值(=)。写时序逻辑时,用always @(posedge clk),里面的变量都要用非阻塞赋值(<=)。这是铁律,别搞混。
来看个例子,一个带使能的计数器:
// 组合逻辑:计算下一个计数值
wire [7:0] next_count;
assign next_count = i_en ? (r_count + 1'b1) : r_count;
// 时序逻辑:在时钟上升沿更新计数值
always @(posedge i_clk or negedge i_rst_n) begin
if (!i_rst_n)
r_count <= 8'd0;
else
r_count <= next_count;
end
你看,组合逻辑算“下一个值”,时序逻辑负责“存起来”。分工明确,代码也容易读。
4.3 阻塞与非阻塞赋值:新手最容易踩的坑
这个知识点,我敢说十个新手有九个在这里翻过车。阻塞赋值用=,非阻塞赋值用<=。它们的行为完全不同,用错了仿真结果和实际电路可能天差地别。
阻塞赋值(=):执行完当前赋值语句后,再执行下一条。说白了就是“顺序执行”,跟C语言一样。它用于组合逻辑。
非阻塞赋值(<=):所有赋值语句同时计算,然后在时钟沿统一更新。它用于时序逻辑。
为什么会这样?因为硬件是并行的。你想想看,一个时钟沿到来时,所有寄存器同时采样输入,而不是一个一个来。非阻塞赋值正好模拟了这种并行行为。
我曾经在做一个高速数据采集项目时,因为在一个always @(posedge clk)块里用了阻塞赋值,导致两个寄存器的更新顺序错了,数据一直对不上。查了两天才找到原因。从那以后,我写时序逻辑只用<=,绝不用=。
警告:千万不要在同一个always块里混用阻塞和非阻塞赋值。综合工具会报错,或者生成意想不到的电路。记住:组合逻辑用=,时序逻辑用<=,泾渭分明。
来看一个对比示例,两个寄存器串联:
// 错误写法:阻塞赋值
always @(posedge clk) begin
r_a = r_b; // 先执行
r_c = r_a; // 后执行,r_c拿到的是更新后的r_a
end
// 结果:r_c和r_a同时更新,相当于只用了1个时钟周期就传了两级
// 正确写法:非阻塞赋值
always @(posedge clk) begin
r_a <= r_b; // 同时计算
r_c <= r_a; // 同时计算,r_c拿到的是更新前的r_a
end
// 结果:r_c比r_a晚一个时钟周期更新,这才是真正的两级寄存器
你看,同样的逻辑,用不同的赋值方式,行为完全不同。在时序逻辑里,非阻塞赋值才是正确的选择。
4.4 知识体系总览
为了让你更直观地理解本章的知识结构,我画了一张图。它把模块化设计、组合逻辑、时序逻辑、阻塞与非阻塞赋值之间的关系串了起来。
这张图把本章的核心脉络理清了。模块化设计是顶层思想,它指导我们如何组织代码。组合逻辑和时序逻辑是两种基本电路类型,它们决定了你用哪种赋值方式。阻塞赋值用于组合逻辑,非阻塞赋值用于时序逻辑——记住这个对应关系,你写代码就不会跑偏。
4.5 实战建议:从写一个小模块开始
理论讲完了,我建议你马上动手。别一上来就搞复杂的行情解析,先写一个简单的模块练手。比如:
- 写一个8位加法器(组合逻辑)
- 写一个带复位和使能的计数器(时序逻辑)
- 把加法器和计数器组合成一个模块,实现累加功能
每写完一个模块,就用仿真工具跑一下波形,看看行为对不对。我在带团队的时候,要求新人第一周必须手写10个小模块,每个模块不超过20行代码。这样练下来,Verilog的基本功就扎实了。
我的习惯:每个模块文件开头,我都会写一段注释,说明这个模块的功能、输入输出、以及注意事项。比如:
// 模块功能:行情数据包解析
// 输入:i_clk(时钟)、i_rst_n(复位)、i_data(原始数据包)
// 输出:o_price(解析后的价格)、o_valid(数据有效标志)
// 注意:输入数据在i_clk上升沿采样,输出延迟1个时钟周期
这样做的好处是,几个月后回头看代码,还能快速想起来当时的设计意图。
好了,这一章的内容就到这里。记住三个关键词:模块化、组合逻辑与时序逻辑、阻塞与非阻塞赋值。它们是Verilog设计的基石,也是你后续做FPGA加速项目的基础。下一章,我们会把这些知识用到实际的行情解析中,到时候你会看到它们是如何协同工作的。