从半加器到超前进位:用Verilog手把手搭建一个8位CPU的ALU(含testbench与仿真)

张开发
2026/4/7 6:36:21 15 分钟阅读

分享文章

从半加器到超前进位:用Verilog手把手搭建一个8位CPU的ALU(含testbench与仿真)
从半加器到超前进位用Verilog手把手搭建一个8位CPU的ALU含testbench与仿真在数字电路设计的浩瀚宇宙中ALU算术逻辑单元犹如一颗璀璨的恒星它是CPU执行算术和逻辑运算的核心引擎。对于Verilog初学者而言从零开始构建一个8位ALU不仅是一次绝佳的学习机会更是将离散的数字电路知识串联成完整体系的实践桥梁。本文将带你从最基础的半加器模块出发逐步构建行波进位和超前进位两种加法器最终集成到一个功能完备的8位ALU中。不同于简单的代码堆砌我们将重点关注如何通过testbench验证设计并通过波形图直观理解进位链的传播过程。这种设计-验证-调试的完整流程正是工业级数字电路开发的真实写照。1. 基础构建块从半加器到全加器1.1 半加器加法运算的原子单元半加器是数字加法器最基本的组成单元它实现了两个1位二进制数的加法运算。其真值表如下ABSumCarry0000011010101101对应的Verilog实现简洁而优雅module half_adder( input A, input B, output S, // Sum output C // Carry ); assign S A ^ B; assign C A B; endmodule这个模块揭示了数字电路设计的本质用逻辑门实现数学运算。异或门(XOR)计算和与门(AND)计算进位。1.2 全加器考虑进位输入的完整加法单元实际的多位加法器中每一位都需要处理来自低位的进位输入。全加器在半加器基础上增加了进位输入Ci形成了完整的加法单元module full_adder( input A, input B, input C_i, // Carry input output S, // Sum output C_o // Carry output ); wire S1, C1, C2; // First half adder stage half_adder ha1(.A(A), .B(B), .S(S1), .C(C1)); // Second half adder stage half_adder ha2(.A(S1), .B(C_i), .S(S), .C(C2)); // Carry output assign C_o C1 | C2; endmodule全加器的进位逻辑可以简化为C_o (A B) | ((A ^ B) C_i)这种两级级联的半加器结构清晰地展示了全加器的工作原理虽然在实际工程中可能会直接使用布尔表达式优化。2. 构建多位加法器行波进位 vs 超前进位2.1 行波进位加法器(RCA)简单直观的实现将多个全加器串联起来就构成了行波进位加法器(Ripple Carry Adder)。这种结构的优点是简单直观易于理解和实现module rca #( parameter WIDTH 8 )( input [WIDTH-1:0] A, input [WIDTH-1:0] B, output [WIDTH-1:0] S, input C_i, output C_o ); wire [WIDTH:0] C; genvar i; assign C[0] C_i; generate for (i0; iWIDTH; ii1) begin full_adder fa( .A(A[i]), .B(B[i]), .C_i(C[i]), .S(S[i]), .C_o(C[i1]) ); end endgenerate assign C_o C[WIDTH]; endmoduleRCA的关键问题在于进位信号必须从最低位传播到最高位导致关键路径延迟随位数线性增长。对于8位加法器最坏情况下进位信号需要穿过8个全加器。2.2 超前进位加法器(LCA)用面积换速度的优化超前进位加法器(Lookahead Carry Adder)通过并行计算进位信号显著减少了关键路径延迟。其核心思想是提前计算所有位的进位而不是等待前一位的进位结果。定义两个重要信号传播信号(P): P_i A_i ⊕ B_i生成信号(G): G_i A_i B_i进位信号可以表示为C_i G_i | (P_i C_{i-1})展开后的4位LCA进位计算module lca_4( input [3:0] A, input [3:0] B, input C_i, output [3:0] S, output C_o ); wire [3:0] G, P; wire [4:0] C; assign G A B; assign P A ^ B; assign C[0] C_i; assign C[1] G[0] | (P[0] C[0]); assign C[2] G[1] | (P[1] C[1]); assign C[3] G[2] | (P[2] C[2]); assign C[4] G[3] | (P[3] C[3]); assign S P ^ C[3:0]; assign C_o C[4]; endmodule对于8位加法器我们可以采用两级4位LCA级联的方式module lca_8( input [7:0] A, input [7:0] B, input C_i, output [7:0] S, output C_o ); wire [1:0] G, P; wire [1:0] C; // First 4-bit LCA lca_4 lca0( .A(A[3:0]), .B(B[3:0]), .C_i(C_i), .S(S[3:0]), .C_o(C[0]) ); // Second 4-bit LCA lca_4 lca1( .A(A[7:4]), .B(B[7:4]), .C_i(C[0]), .S(S[7:4]), .C_o(C[1]) ); assign C_o C[1]; endmoduleLCA的延迟主要取决于进位计算逻辑的级数对于大位宽加法器性能优势更加明显但代价是增加了电路复杂度和面积。3. 构建8位ALU集成算术与逻辑运算3.1 ALU功能定义与接口设计我们的8位ALU将支持以下基本运算操作码运算类型描述000ADD加法运算001SUB减法运算010AND按位与011OR按位或100XOR按位异或101NOT按位取反110LSHIFT逻辑左移111RSHIFT逻辑右移ALU模块接口定义module alu_8bit( input [7:0] A, input [7:0] B, input [2:0] OP, output [7:0] RESULT, output CARRY, output ZERO ); // Implementation goes here endmodule3.2 核心运算单元实现我们将使用之前设计的8位LCA作为加法器核心并集成其他逻辑运算module alu_8bit( input [7:0] A, input [7:0] B, input [2:0] OP, output reg [7:0] RESULT, output reg CARRY, output ZERO ); wire [7:0] add_result, sub_result; wire add_carry, sub_carry; // 加法运算 lca_8 adder( .A(A), .B(B), .C_i(1b0), .S(add_result), .C_o(add_carry) ); // 减法运算通过补码实现 lca_8 subtractor( .A(A), .B(~B), .C_i(1b1), .S(sub_result), .C_o(sub_carry) ); // 零标志 assign ZERO (RESULT 8b0); always (*) begin case(OP) 3b000: begin // ADD RESULT add_result; CARRY add_carry; end 3b001: begin // SUB RESULT sub_result; CARRY sub_carry; end 3b010: begin // AND RESULT A B; CARRY 1b0; end 3b011: begin // OR RESULT A | B; CARRY 1b0; end 3b100: begin // XOR RESULT A ^ B; CARRY 1b0; end 3b101: begin // NOT RESULT ~A; CARRY 1b0; end 3b110: begin // LSHIFT RESULT A 1; CARRY A[7]; end 3b111: begin // RSHIFT RESULT A 1; CARRY A[0]; end default: begin RESULT 8b0; CARRY 1b0; end endcase end endmodule这个ALU设计展示了几个关键点复用加法器模块实现减法运算通过补码转换使用多路选择器根据操作码选择运算结果为不同运算设置适当的进位标志实现零标志检测4. 验证与调试编写完备的testbench4.1 基础测试用例设计一个完备的testbench应该覆盖以下测试场景所有运算类型的边界条件测试进位标志和零标志的验证随机输入组合测试timescale 1ns/1ps module alu_tb; reg [7:0] A, B; reg [2:0] OP; wire [7:0] RESULT; wire CARRY, ZERO; alu_8bit uut( .A(A), .B(B), .OP(OP), .RESULT(RESULT), .CARRY(CARRY), .ZERO(ZERO) ); initial begin // Test ADD operation OP 3b000; A 8h12; B 8h34; #10; A 8hFF; B 8h01; // Test carry #10; // Test SUB operation OP 3b001; A 8h50; B 8h30; #10; A 8h00; B 8h01; // Test borrow #10; // Test logic operations OP 3b010; A 8hAA; B 8h55; // AND #10; OP 3b011; // OR #10; OP 3b100; // XOR #10; OP 3b101; // NOT #10; // Test shift operations OP 3b110; A 8h81; // LSHIFT #10; OP 3b111; A 8h01; // RSHIFT #10; // Random test cases repeat(10) begin OP $random % 8; A $random; B $random; #10; end $finish; end initial begin $monitor(At time %t: OP%b A%h B%h RESULT%h CARRY%b ZERO%b, $time, OP, A, B, RESULT, CARRY, ZERO); end endmodule4.2 波形分析与调试技巧使用ModelSim或Vivado Simulator观察波形时重点关注进位传播在加法运算中观察进位信号如何从低位向高位传播运算延迟比较RCA和LCA的关键路径延迟差异标志位变化验证零标志和进位标志是否按预期变化调试技巧对于意外的运算结果首先检查操作码是否正确解码如果进位标志异常检查最高位的进位逻辑使用$display在仿真中打印中间信号值// 示例在always块中添加调试输出 always (*) begin $display(OP%b, A%h, B%h, RESULT%h, OP, A, B, RESULT); // ...原有代码... end4.3 自动化验证与覆盖率分析对于更专业的验证可以使用SystemVerilog编写更复杂的测试环境class ALU_test; rand bit [7:0] a, b; rand bit [2:0] op; constraint valid_op { op inside {[0:7]}; } function void verify(input bit [7:0] result, input bit carry, zero); bit [7:0] expected; bit expected_carry, expected_zero; case(op) 0: {expected, expected_carry} a b; 1: {expected, expected_carry} a - b; 2: expected a b; 3: expected a | b; 4: expected a ^ b; 5: expected ~a; 6: {expected_carry, expected} {a, 1b0}; 7: {expected, expected_carry} {1b0, a[7:1], a[0]}; endcase expected_zero (expected 0); if(result ! expected || carry ! expected_carry || zero ! expected_zero) begin $error(Mismatch: OP%b, A%h, B%h, Got%h (C%b,Z%b), Exp%h (C%b,Z%b), op, a, b, result, carry, zero, expected, expected_carry, expected_zero); end endfunction endclass这种基于约束随机验证的方法可以大大提高测试覆盖率确保ALU在各种输入组合下都能正确工作。

更多文章