3. Verilog基础与RTL设计(上):模块化设计思想,组合逻辑,时序逻辑,阻塞赋值与非阻塞赋值深度解析,常用电路

各位同学,欢迎来到《芯片设计项目实战宝典》的第三讲。从这一章开始,我们正式进入RTL设计的核心地带。很多新手觉得Verilog就是写代码,其实不然。我个人认为,RTL设计的本质是用代码描述硬件电路,而不是写软件程序。这个观念如果转不过来,后面会踩很多坑。

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

芯片设计动辄几百万门,没人能一口气搞定。模块化就是我们的生存法则。说白了,就是把一个复杂功能拆成多个独立、可复用的小模块,每个模块各司其职。

核心原则:

  • 高内聚:一个模块只做一件事,并且把它做好。
  • 低耦合:模块之间通过明确的接口(端口)通信,内部实现互不干扰。

我在项目中见过最头疼的代码,就是把计数器、状态机和数据通路全写在一个always块里。那代码,别说别人看不懂,过两周自己都看不懂。你想想看,调试的时候多痛苦。

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

module counter #(
    parameter WIDTH = 8
)(
    input  wire             clk,
    input  wire             rst_n,
    input  wire             en,
    output reg  [WIDTH-1:0] count
);
    // 功能实现
endmodule

注意这里的#(parameter WIDTH = 8),这是参数化设计。我习惯把经常变的参数(比如数据位宽、计数器深度)都写成parameter,这样换个场景直接例化时改参数就行,不用重写代码。

3.2 组合逻辑:assign与always@(*)

组合逻辑的输出只取决于当前输入,跟历史状态无关。它没有记忆功能。

3.2.1 用assign描述简单组合逻辑

assign是连续赋值语句,适合描述简单的逻辑门或数据通路。比如一个加法器:

module adder (
    input  wire [7:0] a,
    input  wire [7:0] b,
    output wire [8:0] sum
);
    assign sum = a + b;
endmodule

这里sum是wire类型,因为assign只能驱动wire。记住,assign左边的信号必须是wire。

3.2.2 用always@(*)描述复杂组合逻辑

当组合逻辑比较复杂,比如需要if-else或case语句时,就用always@(*)。这里的*是通配符,表示敏感列表包含所有输入信号。

module mux_4to1 (
    input  wire [1:0] sel,
    input  wire [3:0] data_in,
    output reg        data_out
);
    always @(*) begin
        case (sel)
            2'b00:   data_out = data_in[0];
            2'b01:   data_out = data_in[1];
            2'b10:   data_out = data_in[2];
            2'b11:   data_out = data_in[3];
            default: data_out = 1'b0;
        endcase
    end
endmodule

避坑指南:我曾经在always@(*)里漏写了某个输入信号,导致仿真和综合结果不一致。后来养成习惯,一律用always@(*),不用手动列敏感列表。这样最安全。

3.3 时序逻辑:always@(posedge clk)

时序逻辑有记忆功能,输出不仅取决于当前输入,还取决于之前的状态。它靠时钟沿来触发更新。

module dff (
    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

这里用了异步复位,negedge rst_n表示低电平有效。我建议所有时序逻辑都带上异步复位,这样上电后电路状态是确定的,仿真和调试都方便。

3.4 阻塞赋值与非阻塞赋值:深度解析

这是Verilog里最容易出错的地方,没有之一。很多面试官喜欢问这个,因为能看出你对硬件电路的理解深度。

特性 阻塞赋值 (=) 非阻塞赋值 (<=)
执行顺序 顺序执行,阻塞后续语句 并行执行,不阻塞后续语句
适用场景 组合逻辑 (always@(*)) 时序逻辑 (always@(posedge clk))
硬件行为 类似连线,立即更新 类似寄存器,时钟沿更新
仿真结果 立即生效 块结束后统一更新

为什么时序逻辑必须用非阻塞赋值?我举个例子你就明白了。假设我们要实现两个寄存器级联:

// 错误写法:用阻塞赋值
always @(posedge clk) begin
    a = b;
    c = a;  // 这里a已经是新值了,c得到的是b的值
end

// 正确写法:用非阻塞赋值
always @(posedge clk) begin
    a <= b;
    c <= a;  // 这里a还是旧值,c得到的是a的旧值
end

我的经验:记住一条铁律——写组合逻辑用阻塞赋值,写时序逻辑用非阻塞赋值。别问为什么,先照着做。等你写过几万行代码,自然就懂了。我曾经在同一个always块里混用两种赋值,结果综合出来的电路和仿真完全对不上,查了整整两天。

3.5 常用电路实战

理论讲完了,我们来点实际的。下面这三个电路,几乎每个芯片项目都会用到。

3.5.1 计数器

计数器是时序逻辑的入门级应用。一个带使能和同步复位的N位计数器:

module counter_nbit #(
    parameter N = 8
)(
    input  wire             clk,
    input  wire             rst_n,
    input  wire             en,
    output reg  [N-1:0]     cnt
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            cnt <= {N{1'b0}};
        else if (en)
            cnt <= cnt + 1'b1;
    end
endmodule

这里{N{1'b0}}是位拼接操作,生成N位全0。我习惯用这种方式,比直接写8'b0更通用。

3.5.2 分频器

分频器本质上就是计数器。比如实现一个2分频:

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

注意,这种分频方式会产生新的时钟域。跨时钟域处理是个大话题,我们后面会专门讲。这里先提醒一句:尽量不要在设计中产生多个时钟,除非你很清楚CDC(Clock Domain Crossing)的处理方法。

3.5.3 状态机

状态机是控制逻辑的核心。我习惯用三段式写法,清晰且不易出错。

// 三段式状态机示例
module fsm_example (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       start,
    output reg        done
);
    // 状态编码
    localparam IDLE  = 2'b00;
    localparam WORK  = 2'b01;
    localparam DONE  = 2'b10;

    reg [1:0] state, next_state;

    // 第一段:状态转移
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            state <= IDLE;
        else
            state <= next_state;
    end

    // 第二段:次态逻辑(组合逻辑)
    always @(*) begin
        case (state)
            IDLE:   next_state = start ? WORK : IDLE;
            WORK:   next_state = DONE;
            DONE:   next_state = IDLE;
            default: next_state = IDLE;
        endcase
    end

    // 第三段:输出逻辑(组合逻辑)
    always @(*) begin
        done = (state == DONE);
    end
endmodule

三段式的好处:

  • 状态转移和输出逻辑分离,代码可读性强
  • 组合逻辑和时序逻辑分开,综合工具优化效果好
  • 修改输出逻辑时不会影响状态机核心

好了,这一章的内容就到这里。模块化设计、组合逻辑、时序逻辑、阻塞与非阻塞赋值、常用电路,这些都是RTL设计的基石。下一章我们会继续深入,讲Verilog的更多高级用法。

Verilog基础与RTL设计(上)知识体系 RTL设计核心 模块化设计 组合逻辑 时序逻辑 阻塞/非阻塞赋值 常用电路 高内聚低耦合 参数化设计 assign / always@(*) always@(posedge clk) 计数器/分频器/状态机