一、Verilog RTL设计基础:从模块化到实战

各位同学好,我是老李。在芯片设计这行摸爬滚打了十几年,今天咱们聊聊Verilog RTL设计的基础。说实话,这部分内容看似简单,但很多新手甚至工作两三年的工程师都会在这里栽跟头。我见过太多因为赋值没搞明白导致芯片功能错误的案例了。

咱们这一章要讲四个核心点:模块化设计、组合逻辑与时序逻辑、阻塞与非阻塞赋值、常用RTL模板。嗯,这四个点就像盖房子的地基,打不牢后面全白搭。

Verilog RTL设计基础 知识体系 RTL设计核心 模块化设计 接口清晰 · 可复用 组合 vs 时序逻辑 电平 vs 时钟边沿 阻塞与非阻塞赋值 = 与 <= 的区别 常用RTL模板 计数器 · 状态机 · 移位 顶层 · 子模块 · 接口 always · assign · 敏感列表 仿真行为 · 综合行为 掌握这四点,RTL设计不再难

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

模块化设计,说白了就是分而治之。一个复杂的芯片动辄几千万门,你不可能在一个文件里写完所有逻辑。我个人的习惯是:每个模块只做一件事,做好一件事。

举个例子,一个简单的计数器模块长这样:

module counter #(
    parameter WIDTH = 8
)(
    input  wire             clk,
    input  wire             rst_n,
    input  wire             en,
    output reg  [WIDTH-1:0] count
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 0;
        else if (en)
            count <= count + 1;
    end
endmodule
我的经验:模块接口要尽量简单。我见过有人把模块端口写成四五十个,调试起来简直噩梦。一般控制在20个以内比较合理。

模块化设计有几个原则:

  • 高内聚低耦合——模块内部功能紧密,模块之间依赖少
  • 接口标准化——用统一的握手信号(valid/ready)
  • 参数化设计——用parameter让模块更灵活

1.2 组合逻辑与时序逻辑:两个好兄弟

组合逻辑的输出只取决于当前输入,没有记忆功能。时序逻辑则依赖时钟边沿,能记住历史状态。你想想看,如果芯片里全是组合逻辑,那数据就没办法在时间上对齐了。

组合逻辑用 assign 或者 always @(*) 实现:

// 组合逻辑:加法器
assign sum = a + b;

// 组合逻辑:多路选择器
always @(*) begin
    case (sel)
        2'b00: out = a;
        2'b01: out = b;
        2'b10: out = c;
        default: out = 0;
    endcase
end

时序逻辑用 always @(posedge clk) 实现:

// 时序逻辑:D触发器
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 0;
    else
        q <= d;
end
关键区别:组合逻辑用阻塞赋值(=),时序逻辑用非阻塞赋值(<=)。这个后面会细讲。

我在项目中遇到过一个问题:有人把组合逻辑的敏感列表写漏了信号,结果仿真和综合结果不一致。比如:

// 错误写法:敏感列表不完整
always @(a or b) begin  // 漏了c
    out = a & b & c;
end
避坑指南:组合逻辑的敏感列表一定要写全,或者直接用 always @(*) 让工具自动推导。我曾经因为这个bug查了整整两天。

1.3 阻塞与非阻塞赋值:新手最容易踩的坑

阻塞赋值 = 是立即执行的,非阻塞赋值 <= 是在always块结束时统一更新。这个区别太重要了,我刚开始学的时候也搞混过。

看个对比例子:

阻塞赋值(组合逻辑) 非阻塞赋值(时序逻辑)
always @(*) begin
    a = b;
    c = a;  // c = b
end
always @(posedge clk) begin
    a <= b;
    c <= a;  // c = 旧a值
end
a赋值后立即生效,c拿到的是新a 所有赋值同时更新,c拿到的是旧a

为什么会这样?因为非阻塞赋值在硬件上对应的是D触发器,数据是在时钟边沿采样的。而阻塞赋值对应的是连线,信号变化立即传递。

我的建议:写时序逻辑永远用非阻塞赋值,写组合逻辑永远用阻塞赋值。别混着用,否则仿真和综合结果可能不一样。

1.4 常用RTL模板:拿来就能用

工作中有些代码模式是反复出现的。我整理了几个最常用的模板,你直接套用就行。

模板1:同步复位D触发器

always @(posedge clk) begin
    if (!rst_n)
        q <= 0;
    else
        q <= d;
end

模板2:异步复位D触发器

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 0;
    else
        q <= d;
end

模板3:N位计数器

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        cnt <= 0;
    else if (en)
        cnt <= cnt + 1;
end

模板4:有限状态机(三段式)

// 状态编码
localparam IDLE = 2'b00,
           S1   = 2'b01,
           S2   = 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) ? S1 : IDLE;
        S1:   next_state = (done)  ? S2 : S1;
        S2:   next_state = IDLE;
        default: next_state = IDLE;
    endcase
end

// 第三段:输出逻辑(组合或时序)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        out <= 0;
    else
        case (state)
            S1: out <= 1;
            S2: out <= 2;
            default: out <= 0;
        endcase
end
重要提醒:三段式状态机是我最推荐的写法。它把状态转移、次态逻辑、输出逻辑分开了,代码清晰,综合结果也好。我见过太多人把状态机写成一坨,调试起来想哭。

嗯,这一章的内容就到这里。记住这四个核心点,你的RTL设计就有了坚实的基础。下一章咱们聊聊仿真验证,到时候我会分享一些实战中遇到的坑。


可可精品资料库