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=3i_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 知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张图。它把模块化设计、组合逻辑、时序逻辑、阻塞与非阻塞赋值之间的关系串了起来。

Verilog 核心知识体系 模块化设计 组合逻辑 时序逻辑 输出只取决于当前输入 用 assign 或 always @(*) 阻塞赋值(=) 输出取决于输入和历史状态 用 always @(posedge clk) 非阻塞赋值(<=)

这张图把本章的核心脉络理清了。模块化设计是顶层思想,它指导我们如何组织代码。组合逻辑和时序逻辑是两种基本电路类型,它们决定了你用哪种赋值方式。阻塞赋值用于组合逻辑,非阻塞赋值用于时序逻辑——记住这个对应关系,你写代码就不会跑偏。

4.5 实战建议:从写一个小模块开始

理论讲完了,我建议你马上动手。别一上来就搞复杂的行情解析,先写一个简单的模块练手。比如:

  1. 写一个8位加法器(组合逻辑)
  2. 写一个带复位和使能的计数器(时序逻辑)
  3. 把加法器和计数器组合成一个模块,实现累加功能

每写完一个模块,就用仿真工具跑一下波形,看看行为对不对。我在带团队的时候,要求新人第一周必须手写10个小模块,每个模块不超过20行代码。这样练下来,Verilog的基本功就扎实了。

我的习惯:每个模块文件开头,我都会写一段注释,说明这个模块的功能、输入输出、以及注意事项。比如:

// 模块功能:行情数据包解析
// 输入:i_clk(时钟)、i_rst_n(复位)、i_data(原始数据包)
// 输出:o_price(解析后的价格)、o_valid(数据有效标志)
// 注意:输入数据在i_clk上升沿采样,输出延迟1个时钟周期

这样做的好处是,几个月后回头看代码,还能快速想起来当时的设计意图。

好了,这一章的内容就到这里。记住三个关键词:模块化、组合逻辑与时序逻辑、阻塞与非阻塞赋值。它们是Verilog设计的基石,也是你后续做FPGA加速项目的基础。下一章,我们会把这些知识用到实际的行情解析中,到时候你会看到它们是如何协同工作的。


专注资料整理