组合逻辑建模:基本门电路、多路选择器、加法器、译码器的RTL实现
各位同学,今天我们来聊聊组合逻辑建模。说白了,就是怎么用Verilog把那些纯逻辑电路给描述出来。组合逻辑有个特点——输出只取决于当前输入,跟历史状态没关系。你给它什么输入,它立马给你什么输出,不带半点延迟(当然,实际物理门是有延迟的,但那是后端的事)。
我刚开始学RTL设计时,总觉得组合逻辑太简单,不就是几个与或非门嘛。后来做项目才发现,组合逻辑写不好,轻则面积爆炸,重则时序收敛不了。嗯,这里面的门道还挺多的。
一、基本门电路的RTL实现
先看最基本的。与门、或门、非门、与非门、或非门、异或门,这些是数字电路的砖瓦。Verilog里怎么描述?直接上代码:
// 基本门电路示例
module basic_gates (
input wire a, b,
output wire y_and,
output wire y_or,
output wire y_not,
output wire y_nand,
output wire y_nor,
output wire y_xor
);
// 连续赋值语句实现
assign y_and = a & b; // 与门
assign y_or = a | b; // 或门
assign y_not = ~a; // 非门
assign y_nand = ~(a & b); // 与非门
assign y_nor = ~(a | b); // 或非门
assign y_xor = a ^ b; // 异或门
endmodule
这里用的是assign连续赋值语句。我个人习惯,组合逻辑能用assign搞定的,绝不用always块。为什么?因为assign更直观,综合工具也更容易优化。
always @(*) 写组合逻辑,记得把所有输入信号都放进敏感列表。虽然现代综合工具会自动推断,但养成好习惯总没错。
二、多路选择器(MUX)的实现
多路选择器,说白了就是个数据开关。根据选择信号,从多个输入中挑一个送到输出。2选1、4选1、8选1,原理都一样。
我在项目中遇到过一个问题:用case语句写MUX时,漏掉了某些分支,结果综合出来一堆锁存器。嗯,这是个经典坑。
// 4选1多路选择器
module mux4to1 (
input wire [1:0] sel,
input wire [3:0] data_in,
output reg y
);
always @(*) begin
case (sel)
2'b00: y = data_in[0];
2'b01: y = data_in[1];
2'b10: y = data_in[2];
2'b11: y = data_in[3];
default: y = 1'b0; // 一定要加default!
endcase
end
endmodule
看到那个default了吗?我曾经因为漏掉它,仿真跑得好好的,综合出来却多了一堆锁存器。后来查了两天才发现,原来是case语句没写全。你想想看,综合工具看到分支没覆盖完,它不知道输出该是什么,就给你插个锁存器保持住。这可不是我们想要的。
case 语句时,务必加上 default 分支。或者用 casez / casex 处理无关项,但要注意仿真和综合的一致性。
还有一种更简洁的写法——用三目运算符:
// 2选1 MUX,一行搞定
assign y = sel ? a : b;
这种写法在数据路径中特别常见。比如你要在两组数据之间做选择,用三目运算符既简洁又高效。
三、加法器的实现
加法器是算术运算的基础。从最简单的半加器,到全加器,再到行波进位加法器、超前进位加法器,复杂度逐步上升。
先看半加器:
// 半加器:两个1位二进制数相加
module half_adder (
input wire a, b,
output wire sum, carry
);
assign sum = a ^ b; // 和
assign carry = a & b; // 进位
endmodule
全加器多了个进位输入:
// 全加器:三个1位二进制数相加
module full_adder (
input wire a, b, cin,
output wire sum, cout
);
assign sum = a ^ b ^ cin;
assign cout = (a & b) | (a & cin) | (b & cin);
endmodule
实际项目中,我们很少手写这种位级的加法器。直接用+运算符,综合工具会自动优化。但理解底层原理很重要——你想想看,如果不知道进位链是怎么传播的,你怎么去优化关键路径?
// 8位加法器,直接用+运算符
module adder8 (
input wire [7:0] a, b,
input wire cin,
output wire [7:0] sum,
output wire cout
);
assign {cout, sum} = a + b + cin;
endmodule
这里用拼接运算符{cout, sum}同时得到进位和结果,非常方便。综合工具会把它映射成库里的加法器单元,可能是行波进位,也可能是超前进位,取决于你的时序约束。
+ 运算符时,综合工具会自动选择最优的加法器结构。但如果你对性能有极致要求,比如要跑2GHz,那就得手动设计超前进位加法器了。
四、译码器的实现
译码器,就是把输入的二进制编码翻译成独热码。3-8译码器是最经典的例子:3位输入,8位输出,每次只有一位输出为1。
// 3-8译码器
module decoder3to8 (
input wire [2:0] in,
output reg [7:0] out
);
always @(*) begin
case (in)
3'b000: out = 8'b0000_0001;
3'b001: out = 8'b0000_0010;
3'b010: out = 8'b0000_0100;
3'b011: out = 8'b0000_1000;
3'b100: out = 8'b0001_0000;
3'b101: out = 8'b0010_0000;
3'b110: out = 8'b0100_0000;
3'b111: out = 8'b1000_0000;
default: out = 8'b0000_0000;
endcase
end
endmodule
还有一种更简洁的写法,用移位运算符:
assign out = 1'b1 << in;
一行搞定!但要注意,这种写法只适用于输出为独热码的情况。如果译码器输出有其他编码方式,还是老老实实写case语句。
我记得有一次做指令译码器,32位指令要译出几十种操作码。一开始用case语句写了上百行,后来改用casez处理无关位,代码量直接砍半。嗯,这里面的技巧就是利用casez的无关项匹配,把相似的指令合并处理。
五、知识体系总览
下面这张图,把本章的知识结构梳理了一下。你可以看到,组合逻辑建模从基本门电路出发,逐步扩展到MUX、加法器、译码器。每个模块都有多种实现方式,选择哪种取决于你的设计目标——面积优先还是速度优先。
六、总结与建议
组合逻辑建模,说白了就是三件事:
- 选对描述方式:简单的逻辑用
assign,复杂的用always @(*)配合case或if-else。 - 避免锁存器:组合逻辑的
always块里,所有分支都要赋值,所有输入都要在敏感列表里。 - 理解综合行为:你写的RTL代码,最终会映射成什么电路?心里要有数。
我个人习惯,写组合逻辑时先画个真值表或卡诺图,把逻辑关系理清楚再动手。虽然现在综合工具很智能,但人脑的优化空间还是有的。尤其是关键路径上的组合逻辑,稍微调整一下表达式,可能时序就过了。
好了,这一章的内容就到这里。记住,组合逻辑是数字电路的基础,基础不牢,地动山摇。多写、多练、多思考,慢慢就有感觉了。
assign 和 always 两种方式,分别实现一个8位比较器。比较一下两种写法的综合结果有什么不同。
可可精品资料库