SystemVerilog FPGA 入门 – 第 3 节 – 设计: 4 位算术逻辑单元(ALU)

设计: 4 位算术逻辑单元(ALU)

本节目标:完成一个支持 4 种运算的 4 位 ALU

接口规格

信号 位宽 作用
a 4 位 第一个操作数
b 4 位 第二个操作数
op 2 位 选择运算
y 4 位 运算结果

运算编码

op 运算 示例
00 加法 3 + 2 = 5
01 减法 5 – 3 = 2
10 按位与 AND 1010 & 1100 = 1000
11 按位或 OR 1010 | 1100 = 1110

认识 4 位 ALU

4 位 ALU(Arithmetic Logic Unit,算术逻辑单元)是 CPU 中负责执行算术运算和逻辑运算的核心部件之一。

之前实现过 4 位 2:1 MUX,它根据 sel 从 a、b 中选一个输出。ALU 可以把这个思想扩展到多个运算结果:每种运算都产生一个候选结果,操作码决定最终输出哪一个。

目前只完成一个 4 位 ALU 的设计,支持加法、减法、按位与、按位或四种运算。但是需要知道,完整 ALU 的设计还会涉及更多的运算、进位、溢出等问题,这些在后续会继续完善。

使用 SystemVerilog 设计简易 4 位 ALU

新建一个 src/alu.sv 文件。

先自己写出模块的接口声明,参考上一节的 mux2,但需要注意:

  • a、b 是 4 位输入。
  • op 是 2 位输入。
  • y 是 4 位输出。
  • 这是组合逻辑,不需要 clk。

暂时不要写运算逻辑。

module alu(
    input logic [3:0] a,
    input logic [3:0] b,
    input logic [1:0] op,
    output logic [3:0] y
);


endmodule

定义好了接口后,使用 always_comb 和 case。

之前的 MUX 用 assign 描述连续赋值。现在我们使用 always_comb,它表示这段逻辑是组合逻辑:输入变化时,仿真器会重新计算输出,不需要时钟。

在 alu.sv 的模块内部、endmodule 之前,先添加这个框架:

always_comb begin
    case (op)
        2'b00: ; // 加法
        2'b01: ; // 减法
        2'b10: ; // 按位与
        2'b11: ; // 按位或
        default: ;
    endcase
end

这里的 ; 是临时占位,下一步需要替换成赋值语句。

现在先写加法逻辑。不过赋值有 y = a + b; 和 y <= a + b; 两种写法,前者是阻塞赋值,后者是非阻塞赋值。这里在 always_comb 中推荐使用阻塞赋值。

简单区分:

  • =:阻塞赋值,语句执行时立即更新变量,适合组合逻辑。
  • <=:非阻塞赋值,通常用于 always_ff 描述的时序逻辑。

然后补齐减法逻辑、按位与逻辑、按位或逻辑。得到完整代码如下:

module alu (
    input  logic [3:0] a,
    input  logic [3:0] b,
    input  logic [1:0] op,
    output logic [3:0] y
);

    always_comb begin
        case (op)
            2'b00: y = a + b;
            2'b01: y = a - b;
            2'b10: y = a & b;
            2'b11: y = a | b;

        endcase
    end
endmodule

然后开始创建测试文件 test/tb_alu.sv,编写测试用例。现在给出模板如下,补充 TODO 的部分。

module tb_alu;
    // TODO: 声明 a、b、op、y

    // TODO: 实例化 alu,连接 a、b、op、y

    initial begin
        // Test 1: 3 + 2 = 5
        a  = 4'd3;
        b  = 4'd2;
        op = 2'b00;
        #1;

        if (y !== 4'd5)
            $fatal(1, "ADD failed");

        $display("ADD passed");

        $finish;
    end
endmodule

第一个 TODO 声明四个信号,按照上节方法声明即可:

logic [3:0] a;
logic [3:0] b;
logic [1:0] op;
logic [3:0] y;

第二个 TODO 实例化 alu,连接四个信号:

alu dut (
    .a (a),
    .b (b),
    .op(op),
    .y (y)
);

注意,4'd3 是 4 位的十进制数 3;!== 是严格不等比较;如果结果中出现 X 或 Z,也会判定为不相等。

现在开始运行测试,检查输出是否正确。

just test-sv alu tb_alu

加法测试成功了:

ADD passed
- tests/tb_alu.sv:25: Verilog $finish
- S i m u l a t i o n   R e p o r t: Verilator 5.032 2025-01-01
- Verilator: $finish at 1ps; walltime 0.001 s; speed 861.778 ps/s
- Verilator: cpu 0.001 s on 1 threads; alloced 57 MB

加法通过后,我们再依次添加:

运算 输入 a 输入 b op 预期 y
减法 4'd5 4'd3 01 4'd2
按位与 1010 1100 10 1000
按位或 1010 1100 11 1110

最后大致得到这样的代码:

module tb_alu;
    logic [3:0] a;
    logic [3:0] b;
    logic [1:0] op;
    logic [3:0] y;

    alu dut (
        .a (a),
        .b (b),
        .op(op),
        .y (y)
    );

    initial begin
        // Test 1: 3 + 2 = 5
        a  = 4'd3;
        b  = 4'd2;
        op = 2'b00;
        #1;

        if (y !== 4'd5) $fatal(1, "ADD failed");

        // Test 2: 5 - 3 = 2
        a  = 4'd5;
        b  = 4'd3;
        op = 2'b01;
        #1;

        if (y !== 4'd2) $fatal(1, "SUB failed");


        // Test 3: 0b1010 & 0b1100 = 0b1000
        a  = 4'b1010;
        b  = 4'b1100;
        op = 2'b10;
        #1;

        if (y !== 4'b1000) $fatal(1, "AND failed");


        // Test 4: 0b1010 | 0b1100 = 0b1110
        op = 2'b11;
        #1;

        if (y !== 4'b1110) $fatal(1, "OR failed");

        $display("Passed");

        $finish;
    end
endmodule

测试通过。

下一步,测试 4 位减法的溢出表现

现在的输出 y 只有 4 位,因此:

2-5=-3

负数无法直接用 4 位无符号数表示。在 4 位二进制补码中,结果会按模 16 回绕:

2-5\equiv 13\pmod{16}

因此预期输出是 4'b1101。

现在,在 tb_alu.sv 中新增一个减法测试,设置 a = 4'd2、b = 4'd5,并检查 y 是否等于 4'b1101。

// Test 5: 2 - 5 = -3 === 13 (mod 16)
a = 4'd2;
b = 4'd5;
op = 2'b01;
#1;

if (y !== 4'b1101) $fatal(1, "SUB MOD failed");

标志位

这个 ALU 目前没有标志位输出。标志位通常用于表示运算结果的状态,例如:

  • 零标志(Zero Flag):当结果为零时置位。
  • 负标志(Negative Flag):当结果为负数时置位(结果最高位为 1)。

先实现以上两个标志位,规定:

运算结果 y zero negative
0000 1 0
0011 0 0
1101 0 1
1111 0 1

zero 和 negative

这里先采用简单定义:negative 直接取 y[3]。它表示结果的最高位,不保证所有情况下都代表数学意义上的负数,尤其是无符号运算或发生溢出时。

现在开始完成以下任务:

  1. 在 alu.sv 中添加 zero 和 negative 两个 output logic。
  2. 想一想,应该把它们写在 always_comb 的 case 内部,还是在 case 结束后统一计算?
  3. 在测试台中增加检查:当 2 - 5 得到 1101 时,zero 应为 0,negative 应为 1。
应该把它们写在 always_comb 的 case 内部,还是在 case 结束后统一计算?





这样一来 alu.sv 应该变成以下代码:

module alu (
    input  logic [3:0] a,
    input  logic [3:0] b,
    input  logic [1:0] op,
    output logic [3:0] y,
    output logic zero,
    output logic negative
);

    always_comb begin
        case (op)
            2'b00: y = a + b;
            2'b01: y = a - b;
            2'b10: y = a & b;
            2'b11: y = a | b;
        endcase

        if (y === 4'd0) zero = 1'b1;
        else zero = 1'b0;
        negative = y[3];
    end
endmodule

测试里面也要增加 2 - 5 对 zero 和 negative 的检查:

if (y !== 4'b1101 || zero !== 1'b0 || negative !== 1'b1) $fatal(1, "SUB MOD failed");

测试应当通过。

然而,现在的 ALU 的 negative 标志位只是简单地取了 y[3],并没有考虑溢出情况。对于减法运算,溢出会导致结果的符号位不正确。先添加两个测试,一个测试 zero 标志位是否生效,另一个测试 negative 标志位是否过滤正常判断溢出(现在应当未正常判断)。

输入 运算 预期 y zero negative
3、3 减法 0000 1 0
7、1 加法 1000 0 1

添加测试应当如下:

// Test 6: 3 - 3 = 0
a  = 4'd3;
b  = 4'd3;
op = 2'b01;
#1;

if (zero !== 1'b1) $fatal(1, "zero test failed");

// Test 7: 7 + 1 = 8 = 0b1000, negative !== 0
a  = 4'd7;
b  = 4'd1;
op = 2'b00;
#1;

if (negative === 1) $fatal(1, "negative test failed");

应当有以下测试错误:

[7] %Fatal: tb_alu.sv:73: Assertion failed in tb_alu: negative test failed
%Error: tests/tb_alu.sv:73: Verilog $stop
Aborting...
Aborted
error: Recipe `test-sv` failed on line 10 with exit code 134

现在的 negative 标志位只是简单地取了 y[3],并没有考虑溢出情况。但在常见 CPU 的 ALU 中,通常会把 negative 定义为结果最高位,即 N = y[3],再用独立的 overflow 标志判断有符号溢出。这样职责更清晰。

所以,这里的测试错误是因为我们没有实现 overflow 标志位。现在开始实现 overflow 和 carry 标志位。

把第七个测试目标结果改为 0,继续接下来的操作。

当前 ALU 的 y 只有 4 位。比如:

1111+0001=1\,0000

完整结果需要 5 位,但如果只赋给 4 位的 y,最高位的进位就会被丢掉。

因此,需要先想一想:怎样把两个 4 位数相加时产生的第 5 位保留下来?

提示:可以用一个 5 位的中间变量保存加法结果,再分别取低 4 位和最高位。先不改 overflow,把 carry 的加法行为单独验证好再继续。

carry

先在 alu.sv 中做这三项修改:

  1. 新增 output logic carry。
  2. 在模块内部声明一个 5 位中间变量,例如 logic [4:0] ext;。
  3. 在 always_comb 开始处给 carry 一个默认值,避免推导出锁存器;然后在加法分支中,用中间变量保存完整结果。

注意,参与加法的两个操作数也要扩展到 5 位,否则需要仔细确认表达式的位宽规则。

完成后,先增加这个测试:

a b 预期 y 预期 carry
0011 0010 0101 0
1111 0001 0000 1
1111 1111 1110 1

先只实现加法的 carry,暂时不要实现减法的 carry 和 overflow。

加法 carry 的实现

module alu (
    input logic [3:0] a,
    input logic [3:0] b,
    input logic [1:0] op,
    output logic [3:0] y,
    output logic zero,
    output logic negative,
    output logic carry
);

    logic [4:0] ext;
    always_comb begin
        ext = 5'b00000;
        carry = 1'b0;
        case (op)
            2'b00: begin
                ext = {1'b0, a} + {1'b0, b};
                y = ext[3:0];
                carry = ext[4];
            end
            2'b01: y = a - b;
            2'b10: y = a & b;
            2'b11: y = a | b;
        endcase

        if (y === 4'd0) zero = 1'b1;
        else zero = 1'b0;
        negative = y[3];
    end
endmodule

这里使用 {1'b0, a} 将 a 扩展为 5 位,最高位补 0。然后把加法结果赋给 ext,再分别取低 4 位给 y,最高位给 carry。这样能保证最高位确实被保留。

...
// Test 8: Carry
a = 4'b0011;
b = 4'b0010;
op = 2'b00;
#1;
if (carry !== 1'b0) $fatal(1, "CARRY TEST 1 should be carry = 0");

a = 4'b1111;
b = 4'b0001;
op = 2'b00;
#1;
if (carry !== 1'b1) $fatal(1, "CARRY TEST 2 should be carry = 1");

a = 4'b1111;
b = 4'b1111;
op = 2'b00;
#1;
if (carry !== 1'b1) $fatal(1, "CARRY TEST 3 should be carry = 1");
...

运行 just test-sv alu tb_alu,确认全部通过。

减法 carry 的实现

接下来实现减法的 carry。

先思考一个问题:在 4 位无符号减法中,a = 3、b = 2 时没有借位;而 a = 2、b = 3 时发生借位。按照我们约定的 carry = 1 表示没有借位,这两种情况下 carry 应该分别是多少?

这两种情况下 carry 应该分别是多少?







按照我们采用的约定:

a b 运算 是否借位 carry
3 2 $3-2=1$ 否 1
2 3 $2-3=-1$ 是 0
5 5 $5-5=0$ 否 1
0 1 $0-1=-1$ 是 0

下一步,实现减法的 carry

现在已经有了 5 位中间变量 ext。可以利用二进制补码,把减法转换为加法:

a-b=a+\overline{b}+1

这里的 \overline{b} 是对 b 的每一位取反。

你的任务:
在 op == 2'b01 的分支中:

  1. 用 5 位扩展后的 a 和 b 计算 a + ~b + 1。
  2. 将 ext[3:0] 赋给 y。
  3. 将 ext[4] 赋给 carry。

注意:要对扩展到 5 位的 b 取反,再加 1,确保运算位宽正确。

完成后,至少测试以下三种情况:
– 3 - 2:y = 0001,carry = 1
– 2 - 3:y = 1111,carry = 0
– 5 - 5:y = 0000,carry = 1

此时 op == 2'b01 的分支应当如下:

2'b01: begin
    ext = {1'b0, a} + {1'b0, ~b} + 5'd1;
    y = ext[3:0];
    carry = ext[4];
end

注意:这里的 ~b 是对 b 的每一位取反,不要写成 ~{1'b0, b}!

添加测试:

a  = 4'd3;
b  = 4'd2;
op = 2'b01;
#1;
if (carry !== 1'b1) $fatal(1, "CARRY TEST 4 should be carry = 1");

a = 4'd2;
b = 4'd3;
#1;
if (carry !== 1'b0) $fatal(1, "CARRY TEST 5 should be carry = 0");

a = 4'd5;
b = 4'd5;
#1;
if (carry !== 1'b1) $fatal(1, "CARRY TEST 6 should be carry = 1");

测试应当通过。

overflow

现在进入最后一个标志位:overflow(有符号溢出)。

溢出条件:

4 位二进制补码的表示范围为 -8 到 +7。

运算 4 位结果 overflow
$3+2=5$ 0101 0
$7+1=8$ 1000 1
$(-3)+(-2)=-5$ 1011 0
$3-2=1$ 0001 0
$(-8)-1=-9$ 0111 1

注意:carry 和 overflow 是不同的概念。比如 7+1 没有无符号进位,但发生了有符号溢出。

实现加法 overflow

对于有符号加法,溢出只会发生在以下情况:

  • 两个操作数符号相同;
  • 结果符号与操作数符号不同。

可以把它转化为逻辑表达式。

提示:操作数的符号位分别是 a[3]、b[3],结果符号位是 y[3]。

先写出这个条件,并在 op == 2'b00 的分支中赋值给 overflow。

至少增加以下两个测试:

  1. a = 4'b0111,b = 4'b0001,预期 y = 4'b1000、overflow = 1。
  2. a = 4'b0011,b = 4'b0010,预期 y = 4'b0101、overflow = 0。

先只实现加法的 overflow。通过后,我们再用类似的思路处理减法。

分支 op == 2'b00 的代码应当如下:

2'b00: begin
    // Carry
    ext = {1'b0, a} + {1'b0, b};
    y = ext[3:0];
    carry = ext[4];
    // Overflow
    if (a[3] === b[3] && y[3] !== a[3]) overflow = 1'b1;
end

添加新的测试:

// Test 9: Overflow
a  = 4'b0111;
b  = 4'b0001;
op = 2'b00;
#1;
if (overflow !== 1) $fatal(1, "OVERFLOW TEST 1 should be overflow = 1");

a = 4'b0011;
b = 4'b0010;
#1;
if (overflow !== 0) $fatal(1, "OVERFLOW TEST 2 should be overflow = 0");

测试应当通过。

实现减法 overflow

有符号减法发生溢出的条件与加法不同:

  • 被减数 a 和减数 b 的符号不同;
  • 结果 y 的符号与被减数 a 的符号不同。

也就是说,判断逻辑需要关注 a[3]、b[3] 和 y[3]。

在 2'b01 的减法分支中实现这个条件,并增加以下测试:

a b 运算 预期 y overflow
0111 0001 $7-1$ 0110 0
1000 0001 $-8-1$ 0111 1
0111 1111 $7-(-1)$ 1000 1

注意:最后一项中,1111 按 4 位补码解释为 -1。

分支 op == 2'b01 的代码应当如下:

2'b01: begin
    // Carry
    ext = {1'b0, a} + {1'b0, ~b} + 5'd1;
    y = ext[3:0];
    carry = ext[4];
    // Overflow
    if (a[3] !== b[3] && y[3] !== a[3]) overflow = 1'b1;
end

添加测试:

a  = 4'b0111;
b  = 4'b0001;
op = 2'b01;
#1;
if (overflow !== 0) $fatal(1, "OVERFLOW TEST 3 should be overflow = 0");

a = 4'b1000;
b = 4'b0001;
#1;
if (overflow !== 1) $fatal(1, "OVERFLOW TEST 4 should be overflow = 1");

a = 4'b0111;
b = 4'b1111;
#1;
if (overflow !== 1) $fatal(1, "OVERFLOW TEST 5 should be overflow = 1");

目前已经完全实现了功能,不过,overflow 需要改进一下判断方法。例如,现在加法中的溢出判断是:

if (a[3] === b[3] && y[3] !== a[3]) overflow = 1'b1;

这里使用了 ===,表示严格比较,避免出现 X 或 Z 的情况。但是对于 ALU 的正常运算逻辑,可以使用普通的 ==、!=,或者直接写成布尔表达式。

现在,把加法条件判断改成更易读的形式:

overflow = (a[3] == b[3]) && (y[3] != a[3]);

减法同理。

故现在的 alu.sv 完整模块应当如下:

module alu (
    input logic [3:0] a,
    input logic [3:0] b,
    input logic [1:0] op,
    output logic [3:0] y,
    output logic zero,
    output logic negative,
    output logic carry,
    output logic overflow
);

    logic [4:0] ext;
    always_comb begin
        ext = 5'b00000;
        carry = 1'b0;
        overflow = 1'b0;
        case (op)
            2'b00: begin
                // Carry
                ext = {1'b0, a} + {1'b0, b};
                y = ext[3:0];
                carry = ext[4];
                // Overflow
                overflow = (a[3] == b[3]) && (y[3] != a[3]);
            end
            2'b01: begin
                // Carry
                ext = {1'b0, a} + {1'b0, ~b} + 5'd1;
                y = ext[3:0];
                carry = ext[4];
                // Overflow
                overflow = (a[3] != b[3]) && (y[3] != a[3]);
            end
            2'b10: y = a & b;
            2'b11: y = a | b;
        endcase

        if (y === 4'd0) zero = 1'b1;
        else zero = 1'b0;
        negative = y[3];
    end
endmodule

至此,4 位 ALU 的设计已经完成,支持加法、减法、按位与、按位或四种运算,并且实现了 zero、negative、carry 和 overflow 四个标志位。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇