4、Verilog基础回顾(上):模块化设计、always块与assign语句、组合逻辑与时序逻辑、阻塞与非阻塞赋值

各位同学,欢迎来到《小基站FPGA逻辑开发与加速实战》的第四讲。

今天咱们聊聊Verilog的基础。我知道很多同学觉得Verilog语法简单,不就是几个关键字嘛。但说实话,我见过太多项目出问题,根源就是基础没打牢。尤其是做小基站这种通信系统,时序要求严苛,一个赋值语句写错,整个链路就崩了。

好,咱们直接进入正题。

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

模块化设计,说白了就是“分而治之”。你想想看,一个完整的5G小基站基带处理链,动辄几百万门电路,如果全写在一个文件里,那调试起来简直是噩梦。

我个人习惯,每个功能单元独立成一个模块。比如一个OFDM符号处理,我会拆成FFT模块、信道估计模块、均衡模块等等。每个模块只干一件事,接口清晰,复用性也强。

模块化设计的核心原则:

  • 高内聚低耦合:模块内部逻辑紧密,模块之间依赖尽量少。
  • 接口标准化:用统一的握手信号(valid/ready)或者AXI-Stream接口。
  • 层次化实例化:顶层模块只做连线,不写逻辑。

举个例子,一个简单的D触发器模块:

module d_flip_flop (
    input  wire clk,
    input  wire rst_n,
    input  wire d,
    output reg  q
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            q <= 1'b0;
        else
            q <= d;
    end
endmodule

嗯,这里要注意,模块名和文件名最好保持一致。我在项目中遇到过有人把模块名写错,结果综合时报了一堆“module not found”的错误,排查了半天才发现是大小写问题。

4.2 always块与assign语句:两种描述方式

Verilog里描述电路,主要就两种方式:assignalways。它们各有各的脾气。

4.2.1 assign语句——连续赋值

assign用来描述组合逻辑。它就像一根导线,输入变了,输出立刻跟着变。没有时钟,没有延迟(综合后当然有门延迟,但仿真时是立即赋值)。

assign y = a & b;  // 与门
assign sum = a ^ b; // 异或

说白了,assign右边的表达式一旦变化,左边立刻更新。它不能用在always块里给reg类型赋值(除非用force,但那不是常规用法)。

4.2.2 always块——过程赋值

always块就灵活多了。它可以描述组合逻辑,也可以描述时序逻辑。关键看敏感列表怎么写。

  • 组合逻辑:敏感列表用@(*)或者把所有输入信号列出来。
  • 时序逻辑:敏感列表用时钟沿posedge clknegedge clk

我曾经见过一个新手,把组合逻辑的敏感列表写成了always @(posedge clk),结果综合出来一个巨大的寄存器堆,功能完全不对。嗯,这就是没搞清楚always块的用法。

我的建议:

描述组合逻辑时,尽量用assign或者always @(*)。描述时序逻辑时,用always @(posedge clk or negedge rst_n)。别混着用,容易出问题。

4.3 组合逻辑与时序逻辑:一个“立刻”一个“等一拍”

这两个概念是FPGA设计的基石。我简单总结一下:

特性 组合逻辑 时序逻辑
输出依赖 仅当前输入 当前输入 + 历史状态
时钟 不需要 需要时钟沿触发
存储能力 有(寄存器/锁存器)
典型电路 加法器、多路选择器 计数器、状态机
Verilog描述 assign 或 always @(*) always @(posedge clk)

你想想看,为什么小基站里的同步模块必须用时序逻辑?因为我们需要在精确的时钟节拍下处理数据,组合逻辑的毛刺和延迟会直接破坏时序。

我记得有一次调试一个PRACH检测模块,发现检测结果总是随机出错。后来用示波器一看,组合逻辑输出端有毛刺,刚好被下一级寄存器采到了。从那以后,我对跨时钟域的组合逻辑输出都会加一级寄存器打拍。

避坑指南:

我曾经在项目中用组合逻辑做了个复杂的译码器,输出直接驱动了外部管脚。结果因为组合逻辑路径太长,时序收敛不了,整个芯片工作频率上不去。后来改成流水线寄存器,问题才解决。

所以,如果你的组合逻辑路径超过10级门,建议插入寄存器做流水线。

4.4 阻塞与非阻塞赋值:一个“立即”一个“排队”

这是Verilog里最容易踩坑的地方,没有之一。我面试FPGA工程师时,必问这个问题。

4.4.1 阻塞赋值 =

阻塞赋值,顾名思义,它“阻塞”了后面的语句。执行完当前赋值,才执行下一条。它描述的是组合逻辑的行为。

always @(*) begin
    a = b & c;
    d = a | e;  // 这里用的是a更新后的值
end

说白了,就是顺序执行。上面的代码中,d用的是a的新值。

4.4.2 非阻塞赋值 <=

非阻塞赋值,它不阻塞后面的语句。所有赋值操作在同一个时间步内“同时”计算,然后“同时”更新。它描述的是时序逻辑的行为。

always @(posedge clk) begin
    a <= b & c;
    d <= a | e;  // 这里用的是a更新前的旧值
end

为什么会这样?因为非阻塞赋值在时钟沿到来时,先计算所有右边的表达式,然后再统一赋值给左边。所以d拿到的是a的旧值。

黄金法则(请牢记):

  • 描述组合逻辑:用阻塞赋值 =
  • 描述时序逻辑:用非阻塞赋值 <=
  • 同一个always块里,不要混用两种赋值方式

我曾经接手过一个项目,前任工程师在同一个always块里混用了=<=,结果仿真和综合结果不一致。仿真时功能正常,综合后电路乱跳。排查了整整两天,最后发现是赋值方式用错了。从那以后,我写代码前都会先问自己一句:“这个always块是组合还是时序?”

4.5 小结

今天的内容就到这里。咱们回顾一下:

  • 模块化设计:把大系统拆成小模块,接口清晰,便于复用。
  • assign vs always:assign描述组合逻辑,always更灵活,但要注意敏感列表。
  • 组合 vs 时序:组合逻辑“立刻”响应,时序逻辑“等一拍”。
  • 阻塞 vs 非阻塞:组合用=,时序用<=,千万别混。

这些基础概念,是小基站FPGA开发的“内功心法”。内功不扎实,招式再花哨也没用。下一讲咱们继续深入,聊聊Verilog的进阶用法,包括状态机设计、同步与异步复位,还有如何写出可综合的代码。

好,下课!