4. RTL设计基础(Verilog篇):模块化设计思想,组合逻辑与时序逻辑的底层区别,阻塞赋值与非阻塞赋值的坑

好,咱们今天聊聊RTL设计的基础。这部分内容,说白了就是数字IC设计的“内功心法”。很多新手上来就写代码,结果综合出来的电路跟想的完全不一样,问题就出在基础不牢。

我个人习惯,在开始写任何一行代码之前,先想清楚三件事:这个模块要干什么?输入输出是什么?用组合逻辑还是时序逻辑? 想清楚了再动手,事半功倍。

4.1 模块化设计思想:把大象装进冰箱

模块化设计,听起来高大上,其实道理很简单。你想想看,一个复杂的芯片,比如CPU,有几十亿个晶体管。如果不用模块化,谁能看得懂?

模块化就是把一个复杂的大问题,拆成一个个简单的小问题。每个小问题用一个模块(Module)来实现。模块之间通过端口(Port)连接,就像搭积木一样。

为什么要模块化?

  • 可读性强:每个模块功能单一,代码清晰,别人一看就懂。
  • 可复用性高:写好的模块,下次在其他项目里还能用。比如一个FIFO控制器,我用了好几年了。
  • 便于调试:出问题了,可以定位到具体模块,不用从头到尾翻代码。
  • 团队协作:每个人负责不同的模块,最后拼起来就行。

模块的基本结构

一个标准的Verilog模块长这样:

module counter (
    input  wire       clk,      // 时钟
    input  wire       rst_n,    // 异步复位,低有效
    input  wire       en,       // 使能信号
    output reg  [7:0] count     // 8位计数器输出
);

    // 时序逻辑:计数器核心
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 8'd0;
        else if (en)
            count <= count + 1'b1;
    end

endmodule

嗯,这里要注意,端口声明里我用了 output reg。为什么?因为 count 在 always 块里被赋值,所以必须是 reg 类型。这是新手最容易搞混的地方。

我的小技巧:写模块时,先把端口列表列出来,再想清楚每个端口是 input 还是 output,是 wire 还是 reg。这步做好了,后面写代码就顺了。

4.2 组合逻辑 vs 时序逻辑:底层区别

这两个概念,是数字电路设计的基石。搞不清楚它们,写出来的代码就是“玄学”。

组合逻辑:输出只取决于当前的输入。没有记忆功能。说白了,就是输入一变,输出立刻跟着变(不考虑门延迟)。

时序逻辑:输出不仅取决于当前输入,还取决于之前的状态。它有记忆功能,靠的是触发器(Flip-Flop)。

我刚开始做设计时,总觉得组合逻辑简单,时序逻辑复杂。后来发现,恰恰相反。组合逻辑的竞争冒险、毛刺问题,才是真正的坑。

底层区别

特性 组合逻辑 时序逻辑
基本单元 与门、或门、非门等 触发器(DFF)
输出依赖 仅当前输入 当前输入 + 历史状态
时钟 不需要 需要时钟沿触发
代码风格 assign 或 always @(*) always @(posedge clk)
典型应用 加法器、译码器、多路选择器 计数器、状态机、寄存器

代码示例对比

// 组合逻辑:一个简单的加法器
module adder (
    input  wire [3:0] a,
    input  wire [3:0] b,
    output wire [4:0] sum
);
    assign sum = a + b;
endmodule

// 时序逻辑:一个带使能的寄存器
module reg_with_en (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       en,
    input  wire [3:0] d,
    output reg  [3:0] q
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            q <= 4'd0;
        else if (en)
            q <= d;
    end
endmodule

看到了吗?组合逻辑用 assign,时序逻辑用 always @(posedge clk)。这是最直观的区别。

核心要点:写代码时,脑子里要时刻想着电路。你写的每一行Verilog,最终都会变成实实在在的门电路和触发器。不要把它当成软件来写!

4.3 阻塞赋值与非阻塞赋值的坑

这个坑,我估计每个数字IC工程师都踩过。我记得刚入行时,就因为用错了赋值方式,导致一个模块仿真结果完全不对,查了两天才找到原因。

阻塞赋值(=):顺序执行。先算完右边的值,再赋给左边。后面的语句要等前面的执行完才能执行。

非阻塞赋值(<=):并行执行。所有右边的值在同一时刻计算,然后在时钟沿统一赋值给左边。

黄金法则:

  • 组合逻辑用阻塞赋值(=)
  • 时序逻辑用非阻塞赋值(<=)

为什么?我举个例子你就明白了。

错误示例:时序逻辑用了阻塞赋值

// 错误!时序逻辑用了阻塞赋值
always @(posedge clk) begin
    a = b;
    c = a;
end

这段代码,综合出来是什么?不是两个触发器串联,而是一个触发器!因为 a = b 先执行,a 立刻变成了 b 的值,然后 c = a 执行,c 也变成了 b 的值。结果就是 a 和 c 都等于 b,只用了1个触发器。

正确示例:时序逻辑用非阻塞赋值

// 正确!时序逻辑用非阻塞赋值
always @(posedge clk) begin
    a <= b;
    c <= a;
end

这段代码,综合出来是两个触发器串联。在时钟沿,a 得到 b 的值,c 得到 a 原来的值。这才是我们想要的效果。

我曾经踩过的坑:在一个状态机里,我用了阻塞赋值来更新状态。结果仿真时状态跳来跳去,完全不对。后来改成非阻塞赋值,一切正常。从那以后,我写时序逻辑时,都会下意识地检查一下赋值符号。

组合逻辑用阻塞赋值

// 组合逻辑,用阻塞赋值
always @(*) begin
    temp = a & b;
    y    = temp | c;
end

这里用阻塞赋值是合理的。因为我们需要 temp 先计算出来,然后 y 才能用 temp 的值。如果用非阻塞赋值,y 会用到 temp 的旧值,逻辑就错了。

总结一下

逻辑类型 推荐赋值方式 原因
组合逻辑 阻塞赋值(=) 保证执行顺序,避免竞争
时序逻辑 非阻塞赋值(<=) 模拟触发器行为,避免冒险

说白了,阻塞赋值是“立刻给”,非阻塞赋值是“等会儿给”。在时序逻辑里,所有赋值都要等到时钟沿才给,所以必须用非阻塞。

嗯,这部分内容就到这里。记住这三个核心点:模块化、区分组合与时序、正确使用赋值方式。这是你写出高质量RTL代码的基础。


可可精品资料库