4. RTL设计基础(Verilog篇):模块化设计思想,组合逻辑与时序逻辑的底层区别,阻塞赋值与非阻塞赋值的坑
好,咱们今天聊聊RTL设计的基础。这部分内容,说白了就是数字IC设计的“内功心法”。很多新手上来就写代码,结果综合出来的电路跟想的完全不一样,问题就出在基础不牢。
我个人习惯,在开始写任何一行代码之前,先想清楚三件事:这个模块要干什么?输入输出是什么?用组合逻辑还是时序逻辑? 想清楚了再动手,事半功倍。
4.1 模块化设计思想:把大象装进冰箱
模块化设计,听起来高大上,其实道理很简单。你想想看,一个复杂的芯片,比如CPU,有几十亿个晶体管。如果不用模块化,谁能看得懂?
模块化就是把一个复杂的大问题,拆成一个个简单的小问题。每个小问题用一个模块(Module)来实现。模块之间通过端口(Port)连接,就像搭积木一样。
为什么要模块化?
- 可读性强:每个模块功能单一,代码清晰,别人一看就懂。
- 可复用性高:写好的模块,下次在其他项目里还能用。比如一个FIFO控制器,我用了好几年了。
- 便于调试:出问题了,可以定位到具体模块,不用从头到尾翻代码。
- 团队协作:每个人负责不同的模块,最后拼起来就行。
模块的基本结构
一个标准的Verilog模块长这样:
module counter (
input wire clk, // 时钟
input wire rst_n, // 异步复位,低有效
input wire en, // 使能信号
output reg [7:0] count // 8位计数器输出
);
// 时序逻辑:计数器核心
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 8'd0;
else if (en)
count <= count + 1'b1;
end
endmodule
嗯,这里要注意,端口声明里我用了 output reg。为什么?因为 count 在 always 块里被赋值,所以必须是 reg 类型。这是新手最容易搞混的地方。
4.2 组合逻辑 vs 时序逻辑:底层区别
这两个概念,是数字电路设计的基石。搞不清楚它们,写出来的代码就是“玄学”。
组合逻辑:输出只取决于当前的输入。没有记忆功能。说白了,就是输入一变,输出立刻跟着变(不考虑门延迟)。
时序逻辑:输出不仅取决于当前输入,还取决于之前的状态。它有记忆功能,靠的是触发器(Flip-Flop)。
我刚开始做设计时,总觉得组合逻辑简单,时序逻辑复杂。后来发现,恰恰相反。组合逻辑的竞争冒险、毛刺问题,才是真正的坑。
底层区别
| 特性 | 组合逻辑 | 时序逻辑 |
|---|---|---|
| 基本单元 | 与门、或门、非门等 | 触发器(DFF) |
| 输出依赖 | 仅当前输入 | 当前输入 + 历史状态 |
| 时钟 | 不需要 | 需要时钟沿触发 |
| 代码风格 | assign 或 always @(*) |
always @(posedge clk) |
| 典型应用 | 加法器、译码器、多路选择器 | 计数器、状态机、寄存器 |
代码示例对比
// 组合逻辑:一个简单的加法器
module adder (
input wire [3:0] a,
input wire [3:0] b,
output wire [4:0] sum
);
assign sum = a + b;
endmodule
// 时序逻辑:一个带使能的寄存器
module reg_with_en (
input wire clk,
input wire rst_n,
input wire en,
input wire [3:0] d,
output reg [3:0] q
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 4'd0;
else if (en)
q <= d;
end
endmodule
看到了吗?组合逻辑用 assign,时序逻辑用 always @(posedge clk)。这是最直观的区别。
4.3 阻塞赋值与非阻塞赋值的坑
这个坑,我估计每个数字IC工程师都踩过。我记得刚入行时,就因为用错了赋值方式,导致一个模块仿真结果完全不对,查了两天才找到原因。
阻塞赋值(=):顺序执行。先算完右边的值,再赋给左边。后面的语句要等前面的执行完才能执行。
非阻塞赋值(<=):并行执行。所有右边的值在同一时刻计算,然后在时钟沿统一赋值给左边。
黄金法则:
- 组合逻辑用阻塞赋值(=)
- 时序逻辑用非阻塞赋值(<=)
为什么?我举个例子你就明白了。
错误示例:时序逻辑用了阻塞赋值
// 错误!时序逻辑用了阻塞赋值
always @(posedge clk) begin
a = b;
c = a;
end
这段代码,综合出来是什么?不是两个触发器串联,而是一个触发器!因为 a = b 先执行,a 立刻变成了 b 的值,然后 c = a 执行,c 也变成了 b 的值。结果就是 a 和 c 都等于 b,只用了1个触发器。
正确示例:时序逻辑用非阻塞赋值
// 正确!时序逻辑用非阻塞赋值
always @(posedge clk) begin
a <= b;
c <= a;
end
这段代码,综合出来是两个触发器串联。在时钟沿,a 得到 b 的值,c 得到 a 原来的值。这才是我们想要的效果。
组合逻辑用阻塞赋值
// 组合逻辑,用阻塞赋值
always @(*) begin
temp = a & b;
y = temp | c;
end
这里用阻塞赋值是合理的。因为我们需要 temp 先计算出来,然后 y 才能用 temp 的值。如果用非阻塞赋值,y 会用到 temp 的旧值,逻辑就错了。
总结一下
| 逻辑类型 | 推荐赋值方式 | 原因 |
|---|---|---|
| 组合逻辑 | 阻塞赋值(=) | 保证执行顺序,避免竞争 |
| 时序逻辑 | 非阻塞赋值(<=) | 模拟触发器行为,避免冒险 |
说白了,阻塞赋值是“立刻给”,非阻塞赋值是“等会儿给”。在时序逻辑里,所有赋值都要等到时钟沿才给,所以必须用非阻塞。
嗯,这部分内容就到这里。记住这三个核心点:模块化、区分组合与时序、正确使用赋值方式。这是你写出高质量RTL代码的基础。
可可精品资料库