第四章:组合逻辑设计——从基本门到ALU的修炼之路
组合逻辑,说白了就是没有记忆的电路。输入一变,输出立马跟着变,不带半点犹豫。这就像你按开关,灯就亮;松手,灯就灭——中间没有“记住”这回事。
我在带新人时发现,很多人一上来就写always块,却连最基本的与门、或门都描述不清楚。这不行。万丈高楼平地起,咱们先把地基打牢。
4.1 基本门电路实现
Verilog描述基本门电路,其实就两种方式:连续赋值和always块。我个人习惯用连续赋值,简洁明了。
// 基本门电路示例
module basic_gates(
input a, b,
output y_and, y_or, y_not, y_nand, y_nor, 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
小提示:我在项目中见过有人把“&”写成“&&”,前者是按位与,后者是逻辑与。在组合逻辑里,这两个差别大了去了。按位与是对每一位独立操作,逻辑与只返回0或1。你想想看,如果a和b都是多bit信号,用错运算符会出大问题。
4.2 多路选择器
多路选择器,简称MUX,是组合逻辑里的“交通警察”。它决定哪条路的数据能通过。最简单的就是2选1MUX。
// 2选1多路选择器
module mux2to1(
input [3:0] data0, data1,
input sel,
output [3:0] y
);
// 三目运算符,简洁高效
assign y = sel ? data1 : data0;
endmodule
嗯,这里要注意:三目运算符虽然方便,但嵌套多了可读性会变差。我曾经接手过一个项目,里面有个8选1MUX用了7层三目运算符嵌套,看得我头皮发麻。后来我改成了case语句,代码清晰多了。
// 4选1多路选择器(case语句)
module mux4to1(
input [3:0] data0, data1, data2, data3,
input [1:0] sel,
output reg [3:0] y
);
always @(*) begin
case(sel)
2'b00: y = data0;
2'b01: y = data1;
2'b10: y = data2;
2'b11: y = data3;
default: y = 4'b0; // 防锁存器
endcase
end
endmodule
避坑指南:我曾经在写case语句时漏了default分支,结果综合出来一堆锁存器。锁存器在数字芯片里就像不定时炸弹,时序分析时能把你折腾死。记住:组合逻辑的case一定要写default,哪怕你觉得所有情况都覆盖了。
4.3 编码器与译码器
编码器把多个输入线压缩成较少的输出线,译码器则反过来。这俩就像压缩和解压缩的关系。
先看优先编码器——这是实际项目中最常用的。为什么叫“优先”?因为当多个输入同时有效时,它只认优先级最高的那个。
// 8线-3线优先编码器
module priority_encoder_8to3(
input [7:0] in,
output reg [2:0] out,
output reg valid
);
always @(*) begin
valid = 1'b1;
casez(in)
8'b1???????: out = 3'd7;
8'b01??????: out = 3'd6;
8'b001?????: out = 3'd5;
8'b0001????: out = 3'd4;
8'b00001???: out = 3'd3;
8'b000001??: out = 3'd2;
8'b0000001?: out = 3'd1;
8'b00000001: out = 3'd0;
default: begin
out = 3'd0;
valid = 1'b0; // 无有效输入
end
endcase
end
endmodule
译码器呢?3线-8线译码器是经典例子。我当年面试时,面试官让我手写3-8译码器,我刷刷刷就写出来了——因为这东西在地址译码里太常用了。
// 3线-8线译码器
module decoder_3to8(
input [2:0] in,
input enable,
output reg [7:0] out
);
always @(*) begin
out = 8'b0;
if(enable) begin
case(in)
3'b000: out[0] = 1'b1;
3'b001: out[1] = 1'b1;
3'b010: out[2] = 1'b1;
3'b011: out[3] = 1'b1;
3'b100: out[4] = 1'b1;
3'b101: out[5] = 1'b1;
3'b110: out[6] = 1'b1;
3'b111: out[7] = 1'b1;
endcase
end
end
endmodule
4.4 加法器与ALU设计
加法器是算术逻辑单元(ALU)的核心。从半加器到全加器,再到行波进位加法器,每一步都有讲究。
半加器只考虑两个输入,不考虑进位输入。全加器则多了进位输入,这才是实用的基础单元。
// 全加器
module full_adder(
input a, b, cin,
output sum, cout
);
assign {cout, sum} = a + b + cin;
endmodule
你看,Verilog里一个“+”就搞定了。但综合工具会把它映射成什么?这取决于你的综合约束。我建议初学者还是先理解门级实现,再谈行为级描述。
接下来是ALU——CPU的运算核心。一个简单的ALU至少支持加、减、与、或、异或、比较等操作。
// 简单ALU设计
module alu(
input [7:0] a, b,
input [2:0] opcode,
output reg [7:0] result,
output reg zero, carry
);
wire [8:0] tmp;
always @(*) begin
case(opcode)
3'b000: result = a + b; // 加法
3'b001: result = a - b; // 减法
3'b010: result = a & b; // 按位与
3'b011: result = a | b; // 按位或
3'b100: result = a ^ b; // 按位异或
3'b101: result = ~a; // 取反
3'b110: result = (a > b) ? 8'd1 : 8'd0; // 大于比较
3'b111: result = (a == b) ? 8'd1 : 8'd0; // 等于比较
default: result = 8'b0;
endcase
// 零标志位
zero = (result == 8'b0);
// 进位标志位(仅对加法有效)
tmp = a + b;
carry = tmp[8];
end
endmodule
核心要点:写ALU时,标志位的处理最容易出错。我在一个项目里debug了整整两天,最后发现是进位标志的计算逻辑写错了。记住:进位是加法运算的第9位,不是第8位。用9位宽的临时变量来捕获进位,这是最稳妥的做法。
知识体系总览
下面这张图是我画的组合逻辑知识体系,你可以把它当作学习路线图。
这张图把本章的知识点串起来了。你会发现,从基本门到ALU,其实是一个层层抽象的过程。基本门是砖瓦,MUX和编码器是预制件,ALU就是最终的建筑。理解了这个层次关系,你写代码时心里就有谱了。
我的建议:初学者别急着写复杂的ALU。先把基本门电路和MUX练熟,再尝试编码器、译码器,最后才是加法器和ALU。我在带团队时,要求新人必须手写一遍所有基本模块的RTL代码,再跑仿真验证。这个过程虽然枯燥,但能帮你建立扎实的“电路感”。
好了,组合逻辑的基础就讲到这里。记住:写Verilog不是写软件,你脑子里要时刻有电路图。每个assign语句背后都是一堆晶体管在干活。带着这个意识去写代码,你的设计水平会提升得很快。
公众号:蓝海资料掘金营,微信deep3321