第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设计不是写软件,你是在「画电路」。每一行代码,都要想清楚它会综合成什么。
下一章,我们会开始搭建卷积加速器的整体架构。到时候,这些基础知识都会用上。