3、Verilog基础回顾:模块化设计、组合逻辑与时序逻辑、阻塞与非阻塞赋值

好,咱们进入第三讲。说实话,每次带新人做FPGA项目,我都要花不少时间跟他们掰扯Verilog的基础。不是他们不懂语法,而是写出来的代码风格、逻辑习惯,一看就是软件思维。嗯,这一章咱们就把几个最核心、也最容易踩坑的点,彻底捋一遍。

3.1 模块化设计:把大问题拆成小积木

模块化设计,说白了就是“分而治之”。你想想看,一个撮合系统可能有几十万门电路,你不可能在一个文件里写完所有逻辑。我个人的习惯是,先画顶层框图,把功能拆成独立的模块,比如:订单解析模块、价格比较模块、撮合引擎模块、结果输出模块。

每个模块就是一个 module,有明确的输入输出接口。这样做的好处太多了:

  • 可读性强:别人看你的代码,一眼就知道每个模块负责什么。
  • 可复用性高:比如价格比较模块,下次做别的项目也能直接用。
  • 调试方便:哪个模块出问题了,单独仿真它就行,不用跑整个系统。

举个例子,一个简单的加法器模块:

module adder (
    input  wire [7:0] a,
    input  wire [7:0] b,
    output reg  [7:0] sum,
    output reg        carry
);
    always @(*) begin
        {carry, sum} = a + b;
    end
endmodule

你看,接口清晰,逻辑简单。我在项目中遇到过有人把整个撮合逻辑写在一个always块里,结果仿真跑不动,综合也报错。嗯,这就是没做好模块化的后果。

我的小技巧:每个模块的端口尽量控制在10个以内。如果超过20个,说明这个模块该拆了。

3.2 组合逻辑与时序逻辑:一个管现在,一个管未来

这两个概念,是FPGA设计的基石。我经常跟新人说:组合逻辑是“即时响应”,时序逻辑是“等时钟来了再动”

组合逻辑

组合逻辑的输出只取决于当前的输入。没有时钟,没有记忆。比如一个与门、一个加法器。你用 assign 或者 always @(*) 来描述它。

// 组合逻辑:输出随输入变化
assign result = a & b;

这里要注意,组合逻辑容易产生毛刺。我曾经在撮合系统的价格比较模块里,因为组合逻辑路径太长,导致比较结果出现短暂错误,差点把订单撮合错了。后来加了寄存器打拍,才解决。

时序逻辑

时序逻辑的输出不仅取决于当前输入,还取决于之前的状态。它靠时钟沿来触发,用 always @(posedge clk) 来描述。

// 时序逻辑:每个时钟上升沿更新
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end

你想想看,撮合系统里,订单的到达时间、撮合结果的输出,都需要严格的时序控制。没有时序逻辑,整个系统就是一团乱麻。

核心区别:组合逻辑是“线”,时序逻辑是“寄存器”。线是透明的,寄存器是带锁的。

3.3 阻塞与非阻塞赋值:一个天大的坑

这个知识点,我敢说90%的新人都栽过跟头。我自己刚入行时也吃过亏,有一次写一个状态机,用错了赋值方式,结果仿真和实际行为完全不一样,查了两天才找到原因。

阻塞赋值 =

阻塞赋值是“立即生效”的。在同一个always块里,后面的语句会等前面的执行完再执行。它通常用于组合逻辑。

// 阻塞赋值:顺序执行
always @(*) begin
    a = b;
    c = a;  // c 得到的是 b 的值
end

非阻塞赋值 <=

非阻塞赋值是“并行生效”的。在同一个always块里,所有赋值语句同时计算,然后在时钟沿统一更新。它通常用于时序逻辑。

// 非阻塞赋值:并行执行
always @(posedge clk) begin
    a <= b;
    c <= a;  // c 得到的是 a 的旧值,不是 b
end

为什么会这样?因为非阻塞赋值在时钟沿到来时,先读取所有右边的值,然后再统一赋给左边。所以 c <= a 读到的 a 是上一个时钟周期的值。

铁律
- 写组合逻辑(always @(*))用阻塞赋值 =
- 写时序逻辑(always @(posedge clk))用非阻塞赋值 <=
混用的话,综合结果可能跟你想象的天差地别。

我曾经在撮合引擎的流水线设计里,不小心在时序逻辑里用了阻塞赋值,结果数据一路“穿透”过去,根本没被寄存器锁住。嗯,从那以后,我每次写完代码都会专门检查一遍赋值方式。

3.4 知识体系总览

下面这张图,是我自己总结的Verilog基础核心逻辑。你看一眼,就能把这三个知识点串起来。

Verilog基础核心知识体系 模块化设计 组合逻辑 时序逻辑 assign / always @(*) 阻塞赋值 = always @(posedge clk) 非阻塞赋值 <= 接口清晰 可复用 易调试 核心原则:组合用阻塞,时序用非阻塞,模块化拆分

3.5 避坑指南

最后,我把自己这些年踩过的坑,总结成几条铁律,你记下来:

  • 不要在一个always块里混用阻塞和非阻塞赋值——综合工具会报warning,但行为可能完全不对。
  • 组合逻辑的敏感列表要写全——用 always @(*) 最安全,别手写敏感信号列表。
  • 时序逻辑的复位要统一——要么全部异步复位,要么全部同步复位,别混着来。
  • 模块的端口命名要有意义——别用 a1b2 这种,三个月后你自己都看不懂。
我的习惯:写代码前,先在纸上画好模块框图,标清楚每个模块的输入输出。这样写出来的代码,基本不会有大问题。

好了,这一章的内容就这些。记住,Verilog不是C语言,它是描述硬件的。你写的每一行代码,最终都会变成实实在在的电路。所以,从模块化开始,把组合逻辑和时序逻辑分清楚,赋值方式用对,你的FPGA设计之路就稳了。


专注资料整理