一、Verilog RTL设计基础:从模块化到实战
各位同学好,我是老李。在芯片设计这行摸爬滚打了十几年,今天咱们聊聊Verilog RTL设计的基础。说实话,这部分内容看似简单,但很多新手甚至工作两三年的工程师都会在这里栽跟头。我见过太多因为赋值没搞明白导致芯片功能错误的案例了。
咱们这一章要讲四个核心点:模块化设计、组合逻辑与时序逻辑、阻塞与非阻塞赋值、常用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块结束时统一更新。这个区别太重要了,我刚开始学的时候也搞混过。
看个对比例子:
| 阻塞赋值(组合逻辑) | 非阻塞赋值(时序逻辑) |
|---|---|
|
|
| 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设计就有了坚实的基础。下一章咱们聊聊仿真验证,到时候我会分享一些实战中遇到的坑。
可可精品资料库