第1章:RTL设计基础

各位同学,欢迎来到《卷积加速器Vivado工程实战》的第一章。

说实话,很多初学者一上来就急着写代码,结果后面debug到怀疑人生。我当年也是这样,踩了不少坑。所以这一章,咱们先把地基打牢。

RTL设计,说白了就是「用代码描述硬件」。你写的每一行Verilog,最终都会变成真实的逻辑门和触发器。这一点,请时刻记在心里。

1.1 Verilog HDL语法回顾

Verilog语法其实不难,但有几个关键点,我每次带新人都会反复强调。

1.1.1 模块的基本结构

每个Verilog文件就是一个模块。模块有端口,端口有方向。

module conv_core (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [7:0]  data_in,
    output reg  [15:0] data_out
);
    // 内部逻辑写在这里
endmodule

注意看,input和output声明了信号方向。wire和reg是数据类型。我个人习惯:组合逻辑输出用wire配合assign,时序逻辑输出用reg配合always块。

1.1.2 阻塞赋值与非阻塞赋值

这是新手最容易翻车的地方。我见过太多人把=和<=混用,结果仿真和综合结果对不上。

赋值方式符号典型用途说明
阻塞赋值=组合逻辑立即执行,顺序敏感
非阻塞赋值<=时序逻辑并行执行,块结束时统一更新

记住一个口诀:组合用等号,时序用小于等于。我曾经在项目里因为一个赋值符号写错,导致整个流水线数据错位,查了两天才找到问题。嗯,那种感觉,你懂的。

1.1.3 参数化设计

别把数字写死在代码里。用parameter或者localparam,后面改起来方便得多。

module mac_unit #(
    parameter DATA_WIDTH = 8,
    parameter ACC_WIDTH  = 16
) (
    input  wire [DATA_WIDTH-1:0] a,
    input  wire [DATA_WIDTH-1:0] b,
    output reg  [ACC_WIDTH-1:0]  result
);
    always @(*) begin
        result = a * b;
    end
endmodule

你看,这样设计,以后想改成16位输入,改一个参数就行,不用满世界找代码改。

1.2 模块化设计思想

模块化,说白了就是「分而治之」。一个复杂的卷积加速器,你不可能在一个文件里写完所有逻辑。那样做,你自己都看不懂,更别说别人了。

1.2.1 为什么要模块化?

  • 可读性强:每个模块只做一件事,逻辑清晰
  • 可复用:写好的模块,下个项目直接拿来用
  • 可调试:出问题了,定位到具体模块,不用从头查
  • 团队协作:每人负责一个模块,互不干扰

1.2.2 我的模块划分原则

我一般按照功能来切。比如卷积加速器,我会分成:

  • 数据加载模块:负责从外部存储器读数据
  • 计算阵列模块:真正的乘加运算
  • 累加与量化模块:结果累加,数据截位
  • 控制模块:状态机,调度整个流程

每个模块的接口要尽量简单。信号太多,耦合就重,后面改起来很痛苦。

核心思想:高内聚,低耦合。每个模块内部逻辑尽量完整,模块之间只通过明确的接口通信。

1.3 同步与异步逻辑

这个问题,面试的时候经常被问到。其实理解起来不难,但实际工程中,处理不好会出大问题。

1.3.1 同步逻辑

所有触发器都在同一个时钟边沿触发。这是最常用的设计方式。

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        data_out <= 8'd0;
    else
        data_out <= data_in;
end

同步逻辑的好处是时序容易分析,STA(静态时序分析)工具能帮你检查有没有违规。

1.3.2 异步逻辑

信号变化不受时钟控制,直接作用于触发器。比如异步复位:

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        counter <= 8'd0;
    else
        counter <= counter + 1'b1;
end

注意这里的negedge rst_n,复位信号不依赖时钟。这样做的好处是复位响应快,但坏处也很明显——容易产生亚稳态。

警告:跨时钟域的信号传递,不能直接连!必须用同步器(两级或三级触发器打拍)。我曾经接手过一个项目,就是因为异步信号没做同步处理,导致系统偶尔死机,查了整整一周。

1.3.3 我的建议

能用同步逻辑,就别用异步。实在要用异步复位,记得做「异步复位,同步释放」处理。这个后面讲到复位设计时,我会详细展开。

1.4 状态机设计

状态机是控制逻辑的核心。卷积加速器里的数据调度、计算控制,全靠它。

1.4.1 三段式状态机

我强烈推荐三段式写法。结构清晰,综合结果也好。

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

// 第二段:下一状态逻辑
always @(*) begin
    case (state)
        IDLE:    if (start)      next_state = LOAD;
                 else            next_state = IDLE;
        LOAD:    if (load_done)  next_state = COMPUTE;
                 else            next_state = LOAD;
        COMPUTE: if (comp_done)  next_state = STORE;
                 else            next_state = COMPUTE;
        STORE:   if (store_done) next_state = IDLE;
                 else            next_state = STORE;
        default: next_state = IDLE;
    endcase
end

// 第三段:输出逻辑
always @(*) begin
    case (state)
        IDLE:    begin load_en = 0; comp_en = 0; store_en = 0; end
        LOAD:    begin load_en = 1; comp_en = 0; store_en = 0; end
        COMPUTE: begin load_en = 0; comp_en = 1; store_en = 0; end
        STORE:   begin load_en = 0; comp_en = 0; store_en = 1; end
        default: begin load_en = 0; comp_en = 0; store_en = 0; end
    endcase
end

你看,三段各司其职:第一段管寄存器,第二段管跳转,第三段管输出。出了问题,一眼就能定位。

1.4.2 状态机编码

编码方式优点缺点适用场景
二进制编码节省寄存器组合逻辑复杂状态数多时
独热码速度快,组合逻辑简单浪费寄存器状态数少(<16)
格雷码相邻状态切换只变1位编码复杂跨时钟域传递

我个人习惯:状态少于8个用独热码,多于16个用二进制。中间看情况。

小技巧:写状态机时,记得加一个default分支。万一因为某些原因进入了非法状态,还能跳回来。这叫「容错设计」,我在产品级代码里一定会加。

本章知识体系

下面这张图,是我自己总结的RTL设计核心脉络。你可以把它当作本章的思维导图。

RTL设计基础 Verilog语法回顾 • 模块结构 • 阻塞/非阻塞赋值 • 参数化设计 模块化设计 • 高内聚低耦合 • 功能划分原则 • 接口简洁化 同步与异步逻辑 • 同步设计优势 • 异步复位风险 • 跨时钟域处理 状态机设计 • 三段式写法 • 编码方式选择 • 容错default分支 打好基础,后面写卷积加速器才能游刃有余

好了,第一章的内容就到这里。记住,RTL设计不是写软件,你是在「画电路」。每一行代码,都要想清楚它会综合成什么。

下一章,我们会开始搭建卷积加速器的整体架构。到时候,这些基础知识都会用上。

可可精品资料库