4. 组合逻辑电路设计:多路选择器、编码器、译码器、加法器、乘法器与ALU
各位同学,欢迎来到第四章。这一章我们聊聊组合逻辑。说白了,组合逻辑就是那种“输入一变,输出立马跟着变”的电路,没有时钟,没有寄存器,纯粹靠门电路搭出来的逻辑。
我个人觉得,组合逻辑是数字电路的基础,也是FPGA设计的“基本功”。你想想看,一个复杂的CPU里,除了寄存器,剩下的全是组合逻辑。RISC-V内核里的ALU、译码器、地址计算,哪个离得开这些基础模块?
4.1 多路选择器(MUX)
多路选择器,说白了就是一个“数据开关”。你给它几个输入,它根据选择信号,只让其中一个通过。
我在项目中遇到过一个问题:用if-else写MUX,综合出来竟然是一大堆LUT,延迟很大。后来改成case语句,立马清爽了。为什么?因为case语句综合工具能更好地推断出MUX结构。
看一个简单的2:1 MUX的Verilog代码:
module mux_2to1 (
input wire a, b,
input wire sel,
output wire y
);
assign y = sel ? b : a;
endmodule
嗯,这里要注意:条件运算符? : 综合出来就是一个MUX。如果你写if-else,记得要写完整,不然会综合出锁存器(latch)。
4.2 编码器与译码器
编码器和译码器,这俩是“反着来”的。编码器把多个输入线压缩成较少的输出线;译码器则反过来,把较少的输入线扩展成多个输出线。
你想想看,RISC-V的指令译码器,本质上就是一个巨大的译码器。它把32位的指令编码,翻译成各种控制信号。
先看一个8线-3线优先编码器:
module priority_encoder_8to3 (
input wire [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'b0000_1???: out = 3'd3;
8'b0000_01??: out = 3'd2;
8'b0000_001?: out = 3'd1;
8'b0000_0001: out = 3'd0;
default: begin
out = 3'd0;
valid = 1'b0;
end
endcase
end
endmodule
这里用了casez,?表示“不关心”。优先级最高的输入是in[7],最低的是in[0]。valid信号告诉你输入是否有效。
再看3线-8线译码器:
module decoder_3to8 (
input wire [2:0] in,
input wire en,
output reg [7:0] out
);
always @(*) begin
out = 8'd0;
if (en) begin
case (in)
3'd0: out = 8'b0000_0001;
3'd1: out = 8'b0000_0010;
3'd2: out = 8'b0000_0100;
3'd3: out = 8'b0000_1000;
3'd4: out = 8'b0001_0000;
3'd5: out = 8'b0010_0000;
3'd6: out = 8'b0100_0000;
3'd7: out = 8'b1000_0000;
endcase
end
end
endmodule
4.3 加法器
加法器是ALU的核心。这里我们聊两种:行波进位加法器和超前进位加法器。
4.3.1 行波进位加法器(Ripple Carry Adder)
行波进位,名字很形象。进位就像水波一样,从低位往高位“一波一波”地传。每个全加器都要等低位的进位算出来,才能算自己的结果。
优点是结构简单,缺点是慢。32位的行波进位加法器,延迟大约是32个全加器的延迟之和。
module ripple_carry_adder #(parameter WIDTH = 4) (
input wire [WIDTH-1:0] a, b,
input wire cin,
output wire [WIDTH-1:0] sum,
output wire cout
);
wire [WIDTH:0] carry;
assign carry[0] = cin;
assign cout = carry[WIDTH];
genvar i;
generate
for (i = 0; i < WIDTH; i = i + 1) begin : fa_gen
full_adder fa (
.a (a[i]),
.b (b[i]),
.cin (carry[i]),
.sum (sum[i]),
.cout(carry[i+1])
);
end
endgenerate
endmodule
4.3.2 超前进位加法器(Carry Lookahead Adder)
超前进位加法器,说白了就是“不等了”。它通过额外的逻辑,提前算出每一位的进位,不用等低位传上来。
核心思想是引入两个信号:
- 生成信号(Generate):g = a & b,只要a和b都是1,这一位肯定产生进位。
- 传播信号(Propagate):p = a ^ b,只要a和b有一个是1,进位就能传过去。
然后进位可以写成:c[i+1] = g[i] | (p[i] & c[i])
展开后,每一位的进位只依赖于输入和cin,不依赖于中间进位。这就是“超前进位”的由来。
module carry_lookahead_adder #(parameter WIDTH = 4) (
input wire [WIDTH-1:0] a, b,
input wire cin,
output wire [WIDTH-1:0] sum,
output wire cout
);
wire [WIDTH-1:0] g, p, c;
assign g = a & b;
assign p = a ^ b;
// 超前进位逻辑
assign c[0] = cin;
assign c[1] = g[0] | (p[0] & c[0]);
assign c[2] = g[1] | (p[1] & g[0]) | (p[1] & p[0] & c[0]);
assign c[3] = g[2] | (p[2] & g[1]) | (p[2] & p[1] & g[0]) | (p[2] & p[1] & p[0] & c[0]);
assign cout = g[3] | (p[3] & g[2]) | (p[3] & p[2] & g[1]) | (p[3] & p[2] & p[1] & g[0]) | (p[3] & p[2] & p[1] & p[0] & c[0]);
assign sum = p ^ c;
endmodule
4.4 乘法器(组合逻辑实现)
组合逻辑乘法器,说白了就是“用加法器搭出来的乘法”。原理和小学学的竖式乘法一模一样。
举个例子,4位乘法:a[3:0] * b[3:0]。把b的每一位分别和a相乘,得到4个部分积,然后加起来。
module comb_multiplier #(parameter WIDTH = 4) (
input wire [WIDTH-1:0] a, b,
output wire [2*WIDTH-1:0] product
);
wire [WIDTH-1:0] partial[WIDTH-1:0];
wire [2*WIDTH-1:0] sum[WIDTH-1:0];
genvar i, j;
generate
// 生成部分积
for (i = 0; i < WIDTH; i = i + 1) begin : pp_gen
for (j = 0; j < WIDTH; j = j + 1) begin : pp_bit
assign partial[i][j] = a[j] & b[i];
end
end
// 累加部分积
assign sum[0] = partial[0];
for (i = 1; i < WIDTH; i = i + 1) begin : add_stage
assign sum[i] = sum[i-1] + (partial[i] << i);
end
assign product = sum[WIDTH-1];
endgenerate
endmodule
4.5 算术逻辑单元(ALU)设计
ALU是CPU的“计算核心”。RISC-V的ALU需要支持加减法、逻辑运算、移位、比较等操作。
我设计ALU时,习惯用一个操作码(opcode)来选择功能。每个功能对应一个组合逻辑模块,最后用MUX选出结果。
module alu #(parameter WIDTH = 32) (
input wire [WIDTH-1:0] a, b,
input wire [3:0] alu_op, // 操作码
output reg [WIDTH-1:0] result,
output reg zero, // 结果为0标志
output reg carry, // 进位标志
output reg negative // 负数标志
);
wire [WIDTH-1:0] add_result, sub_result, and_result, or_result, xor_result;
wire [WIDTH-1:0] slt_result; // set less than
// 加法
assign add_result = a + b;
// 减法
assign sub_result = a - b;
// 逻辑运算
assign and_result = a & b;
assign or_result = a | b;
assign xor_result = a ^ b;
// 小于比较 (slt)
assign slt_result = (a < b) ? 32'd1 : 32'd0;
always @(*) begin
case (alu_op)
4'b0000: result = add_result;
4'b0001: result = sub_result;
4'b0010: result = and_result;
4'b0011: result = or_result;
4'b0100: result = xor_result;
4'b0101: result = slt_result;
default: result = {WIDTH{1'b0}};
endcase
zero = (result == {WIDTH{1'b0}});
negative = result[WIDTH-1];
carry = (alu_op == 4'b0000) ? (a[WIDTH-1] & b[WIDTH-1]) |
(a[WIDTH-1] ^ b[WIDTH-1]) & result[WIDTH-1] : 1'b0;
end
endmodule
好了,这一章的内容就到这里。组合逻辑是数字电路的基础,也是FPGA设计的“基本功”。多写、多练、多仿真,慢慢就能找到感觉了。
公众号:蓝海资料掘金营,微信deep3321