news 2026/9/17 12:53:53

杭电计组实验3:多功能ALU控制码、标志位与Logisim/Verilog实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
杭电计组实验3:多功能ALU控制码、标志位与Logisim/Verilog实现

简介:杭州电子科技大学计算机组成原理与系统结构课程设计的实验三「多功能ALU设计」实验报告,面向计算机、电子信息类专业学生及Verilog HDL入门者,用于运算器建模与仿真验证。压缩包内1个doc文件,约89KB,为完整实验报告,可直接参照其排版与组织。报告基于Xilinx ISE展开,涵盖工程创建、Verilog模块编写、综合检查、Test Fixture激励与波形仿真的流程,并给出ALU核心源码与激励代码,覆盖与、或、异或、加减、比较、移位等运算及ZF、OF标志位生成逻辑。文末思考题分析该ALU无法支撑MIPS核心指令集的原因,讨论有符号与无符号「A<B」比较歧义及SIGctr控制位改进思路,并附排错记录与RTL视图说明。已有2286人学习下载,适合需要撰写同类实验报告或对照仿真代码的读者。

1. 计组实验3的多功能ALU:不是只多一个加法器

计组实验做到第三个,很多人第一次发现:8 位加法器能跑通,但把减法、与或非、移位、比较塞进同一个模块后,波形开始变得不听话。杭电计组实验3多功能ALU设计实验要解决的就是这件事——用一组控制码选中不同运算,并让进位 CF、溢出 OF、零标志 ZF、符号标志 SF 在正确的时机翻转。它上承数据通路,下接控制器,是 408 计组里运算器章节的硬件落地。适合正在做 Logisim 或 FPGA 实验的人:要能把功能表写成控制码,把真值表连成多路选择器,还要在边界数据上验证标志位。做完这一版,后面搭基本模型机时,ALU 不再是一个黑盒。

2. 多功能ALU的控制码与功能表:从需求到位宽选型

2.1 先定功能集合:8 位 ALU 的 12 条常用指令

多功能 ALU 的“多功能”不是越多越好,而是把控制器真正会用到的运算收进来。常见做法是:算术类保留加、减、加一、减一;逻辑类保留与、或、异或、非;移位类保留逻辑左移、逻辑右移、算术右移;比较类保留有符号小于、相等、不等。这样 12 条左右,刚好能用 4 位控制码覆盖。408 计组里常考的 ALU 功能也集中在这一组,尤其是减法用补码实现、比较用减法结果判断,这两条决定了后面的接线方式。

先把功能集合写成表,再谈电路。表里要同时写清楚操作码、运算表达式和受影响的标志位,否则连 Logisim 时容易漏掉某个输出。标志位不要每个运算都置位,通常只有加减法影响 CF 和 OF,逻辑运算只影响 ZF 和 SF。把影响范围写进表里,仿真时才知道该看哪个信号。

2.2 控制码编码与位宽参数:让同一套逻辑适配 8/16 位

控制码一般用 4 位op[3:0],从0000开始编号。编号顺序会影响多路选择器的连线难度:把加法放在0000、减法放在0001,可以让最低位直接参与“加/减”选择;把逻辑运算放在中间,移位放在高位,比较放在末尾,选择器的层级会清楚一些。位宽不要写死。实验箱常见 8 位,但基本模型机可能扩到 16 位,写成参数WIDTH以后,加法器、移位器和比较器都能复用。

下面是一段 Verilog 的控制码定义,放在alu.v的头部,后面用case展开。用localparam而不是宏,综合工具更容易做常量传播,也不会污染全局。

// 控制码定义:按功能分组,便于多路选择器分层 localparam OP_ADD = 4'b0000; // 加法 localparam OP_SUB = 4'b0001; // 减法 localparam OP_INC = 4'b0010; // 加一 localparam OP_DEC = 4'b0011; // 减一 localparam OP_AND = 4'b0100; // 按位与 localparam OP_OR = 4'b0101; // 按位或 localparam OP_XOR = 4'b0110; // 按位异或 localparam OP_NOT = 4'b0111; // 按位取反 localparam OP_SLL = 4'b1000; // 逻辑左移 localparam OP_SRL = 4'b1001; // 逻辑右移 localparam OP_SRA = 4'b1010; // 算术右移 localparam OP_SLT = 4'b1011; // 有符号小于 localparam OP_EQ = 4'b1100; // 相等 localparam OP_NEQ = 4'b1101; // 不等

参数说明:OP_ADDOP_DEC归入算术组,减法用a + (~b) + 1a - b实现;OP_SLLOP_SRA归入移位组,算术右移要保留符号位;OP_SLTOP_NEQ归入比较组,比较结果只占 1 位,其余位补零。控制码一旦定下,功能表就不能再随意改,否则控制器那边也要跟着改。我一般会把这张表贴在实验报告第一页,连线时逐行打勾。

2.3 参数表与真值表:从表格推导多路选择器

把功能集合、控制码和标志位放在同一张表里,是后面画电路和写case的依据。下面这张表按 8 位、WIDTH=8给出。Y列写结果表达式,CF/OF只对加减法有意义,ZF/SF对所有运算都有效。加减法的CF表示无符号进位或借位,OF表示有符号溢出,这两个标志在实验里最容易混。

op[3:0]助记符运算Y 输出CFOFZF/SF
0000ADDa + bsum[7:0]sum[8]同号相加得异号有效
0001SUBa - bdiff[7:0]diff[8]异号相减得异号有效
0010INCa + 1sum[7:0]sum[8]同 ADD有效
0011DECa - 1diff[7:0]diff[8]同 SUB有效
0100ANDa & b按位与00有效
0101ORa | b按位或00有效
0110XORa ^ b按位异或00有效
0111NOT~a按位取反00有效
1000SLLa << 1左移一位移出位0有效
1001SRLa >> 1逻辑右移移出位0有效
1010SRA$signed(a) >>> 1算术右移00有效
1011SLT$signed(a) < $signed(b)1 或 000有效
1100EQa == b1 或 000有效
1101NEQa != b1 或 000有效

表格里sumdiff要扩一位。加法用{1'b0, a} + {1'b0, b},减法用{1'b0, a} - {1'b0, b},这样最高位就是进位或借位。无符号比较可以直接看diff[8]a < b时借位为 1;有符号比较不能只看借位,要比较符号和差值。逻辑运算和移位运算把CF/OF固定为 0,不是偷懒,而是让控制器只在高位控制码下查询这两个标志,减少无效跳转。

从真值表到电路,通常用两级多路选择器:第一级选算术结果(加、减、加一、减一),第二级选逻辑、移位和比较结果。控制码的高两位可以拿来分组,低两位用来选组内具体项。这样比直接用一个 14 选 1 的选择器更容易查线,也好在 Logisim 里分屏显示。移位器可以先用一个桶形移位器,或者按实验要求只做一位移位,后者连线更少,但功能表要写清楚只支持移一位。

3. 杭电计组实验3的 Logisim 搭法:数据通路与标志位接线

3.1 子电路引脚划分与元件库选型

Logisim 里先建一个子电路,命名ALU8。输入引脚放A[7:0]B[7:0]op[3:0],输出引脚放Y[7:0]ZFCFOFSF。如果实验要求带使能端,再加en,但多数版本不加,直接靠控制码保持输出。元件库主要用Wiring里的分线器、Arithmetic里的加法器和比较器、Memory里的多路选择器,以及Gates里的与或非异或门。不要一开始就用Arithmetic里的 ALU 元件,那个封装太厚,实验要的是自己搭出数据通路。

分线器要先按位宽设置。AB各用一个 8 位分线器拆成高低位,后面取符号位和移位时要用。多路选择器选Multiplexer,数据位宽设为 8,选择端位数按分组数算。如果做两级选择,第一级选 4 个算术结果,用 2 位选择端;第二级选 4 组结果,再用 2 位选择端。控制码op[3:2]接第二级,op[1:0]接第一级,接线时别把高低位接反。

元件库位置用途关键参数
分线器Wiring拆分 A/B 的位位宽 8,扇出按需
加法器Arithmetic加、减、加一、减一位宽 8,带进位输入
比较器Arithmetic有符号/无符号比较位宽 8,输出 1 位
多路选择器Memory选择运算结果数据位 8,选择端 2 位
移位器Arithmetic左移、右移位宽 8,方向控制
与或非异或门Gates逻辑运算位宽 8,按位连接

3.2 加减法共用加法器的连线步骤

减法不要单独再放一个减法器,用加法器加补码更贴近 408 计组里的运算器结构。步骤是:把B的每一位经过一个异或门,异或门的另一输入接sub控制信号;sub=1B取反,同时加法器的进位输入Cinsub,完成加一。这样sub=0时算A+Bsub=1时算A+~B+1,也就是A-B。加法器输出扩到 9 位,第 8 位就是进位或借位。

溢出判断不要直接接加法器的进位输出。无符号进位CF看第 8 位,有符号溢出OF要看符号位:加法时,A[7]B[7]相同且Y[7]与它们不同;减法时,A[7]B[7]不同且Y[7]A[7]不同。用异或门和与门搭出来,或者用两个异或门加一个与门。接线时把sub也纳入B[7]的判断,否则减法溢出会误判。

加一和减一可以复用同一加法器:B固定为00000001sub=0就是加一,sub=1就是减一。也可以单独拉一个 1 位常量,减少选择器输入。实验报告里最好画清楚这个复用关系,答辩时老师常问“减法怎么实现的”。我一般会把sub信号同时接到B的异或门和Cin,在图上用同一根线标出来。

3.3 移位、比较与标志位生成

移位器用Arithmetic里的Shifter,设置方向为左或右,移位位数设为 1。逻辑左移补 0,逻辑右移补 0,算术右移补符号位。Logisim 的 Shifter 如果只有方向控制,算术右移要用A[7]复制 8 位后与右移结果做选择:A[7]=0时逻辑右移和算术右移一样,A[7]=1时高两位补 1。比较器直接输出 1 位,小于、相等、不等三个结果分别接多路选择器。比较器的有符号模式要确认设置,否则SLT会变成无符号比较。

标志位生成放在最后一级:ZF = (Y == 0),用 8 输入或非门实现;SF = Y[7],直接取最高位;CFOF从加减法结果里取,逻辑和移位运算时用控制码屏蔽。屏蔽可以用与门:op属于算术组时CF有效,否则强制 0。控制码算术组是00000011,用两位高位和两位低位组合出一个is_arith信号,接与门。

# 生成 Logisim ROM 可加载的测试向量:每行 3 字节 A,B,op,另存期望 Y # 用法:python gen_alu_vectors.py > alu_vectors.hex for a in [0x00, 0x01, 0x7F, 0x80, 0xFF, 0x55, 0xAA]: for b in [0x00, 0x01, 0x7F, 0x80, 0xFF, 0x55, 0xAA]: for op in range(0x10): y = (a + b) & 0xFF if op == 0x0 else (a - b) & 0xFF if op == 0x1 else a & b print(f"{a:02X}{b:02X}{op:X}{y:02X}")

这段脚本只列了一部分运算,用来看关键边界:0x7F + 0x01会溢出,0x80是符号位为 1 的最小负数,0xFF是全 1。把输出保存成十六进制文件,在 Logisim 里用ROM加载,再逐行对照Y和标志位。参数说明:ab是测试输入,op是 4 位控制码,y是低 8 位结果。跑之前先确认脚本里的运算和功能表一致,否则会把错误期望带进仿真。

3.4 用 Python 生成测试向量灌入 Logisim ROM

Logisim 的ROM地址输入接计数器,数据输出分成ABopY_expected四段。计数器时钟用Clock元件,频率设低一点,比如 1 Hz,方便看单步。每来一个时钟,ROM 输出一组向量,同时接到ALU8的输入和比较器的一端,比较器另一端接Y_expected,输出match。如果match一直为 1,说明当前测试向量通过;如果变 0,记下地址,回到脚本查那一行。

这一步常见坑是分线器位宽和 ROM 数据位宽不一致。ABY_expected各 8 位,op4 位,一共 28 位,ROM 数据位宽要设 28,地址位宽按测试向量条数算。地址线不要接错到op段,否则会看到结果时对时错。也可以在 Logisim 里手动改A/B/op,先跑几条:A=7F, B=01, op=0000,期望Y=80, CF=0, OF=1, ZF=0, SF=1。这条过了,再做A=80, B=01, op=0001,期望Y=7F, CF=1, OF=1, ZF=0, SF=0。手动过两条边界,比盲跑一堆向量更容易定位接线错误。

4. 用 Verilog 写一版可综合的多功能 ALU:代码与仿真

4.1 参数化 ALU 的 Verilog 实现

Verilog 版本适合往 FPGA 实验板上搬,也方便用iverilog做自动化仿真。模块端口保持和 Logisim 子电路一致,位宽用parameter WIDTH。所有输出用reg,在always @(*)里用case赋值,综合工具会展开成多路选择器。加减法先扩一位,统一算好sumdiff,避免在case里重复写位宽转换。

module alu #(parameter WIDTH = 8)( input wire [WIDTH-1:0] a, input wire [WIDTH-1:0] b, input wire [3:0] op, output reg [WIDTH-1:0] y, output reg zf, output reg cf, output reg of, output reg sf ); // 扩展一位,最高位作为进位/借位 wire [WIDTH:0] sum = {1'b0, a} + {1'b0, b}; wire [WIDTH:0] diff = {1'b0, a} - {1'b0, b}; wire [WIDTH:0] inc = {1'b0, a} + 1'b1; wire [WIDTH:0] dec = {1'b0, a} - 1'b1; always @(*) begin y = {WIDTH{1'b0}}; cf = 1'b0; of = 1'b0; case (op) 4'b0000: begin y = sum[WIDTH-1:0]; cf = sum[WIDTH]; of = (a[WIDTH-1] == b[WIDTH-1]) && (y[WIDTH-1] != a[WIDTH-1]); end 4'b0001: begin y = diff[WIDTH-1:0]; cf = diff[WIDTH]; // 借位 of = (a[WIDTH-1] != b[WIDTH-1]) && (y[WIDTH-1] != a[WIDTH-1]); end 4'b0010: begin y = inc[WIDTH-1:0]; cf = inc[WIDTH]; of = (a[WIDTH-1] == 1'b0) && (y[WIDTH-1] == 1'b1); end 4'b0011: begin y = dec[WIDTH-1:0]; cf = dec[WIDTH]; of = (a[WIDTH-1] == 1'b1) && (y[WIDTH-1] == 1'b0); end 4'b0100: y = a & b; 4'b0101: y = a | b; 4'b0110: y = a ^ b; 4'b0111: y = ~a; 4'b1000: y = {a[WIDTH-2:0], 1'b0}; // 逻辑左移 4'b1001: y = {1'b0, a[WIDTH-1:1]}; // 逻辑右移 4'b1010: y = {a[WIDTH-1], a[WIDTH-1:1]}; // 算术右移 4'b1011: y = ($signed(a) < $signed(b)) ? {{(WIDTH-1){1'b0}}, 1'b1} : {WIDTH{1'b0}}; 4'b1100: y = (a == b) ? {{(WIDTH-1){1'b0}}, 1'b1} : {WIDTH{1'b0}}; 4'b1101: y = (a != b) ? {{(WIDTH-1){1'b0}}, 1'b1} : {WIDTH{1'b0}}; default: y = {WIDTH{1'b0}}; endcase zf = (y == {WIDTH{1'b0}}); sf = y[WIDTH-1]; end endmodule

逻辑说明:sumdiffincdec都扩到WIDTH+1位,case里只取低WIDTH位赋给y,最高位给cf。加减法的of用符号位判断,加一和减一单独判断符号翻转。逻辑和移位运算把cf/of保持为 0,比较结果只放最低位,其余位补 0。zfsf在所有分支之后统一计算,避免每个分支重复写。参数WIDTH改成 16 时,移位表达式仍然成立,比较器的位宽也跟着变。

信号方向位宽说明
a输入WIDTH操作数 A
b输入WIDTH操作数 B
op输入4控制码
y输出WIDTH运算结果
zf输出1结果为零
cf输出1无符号进位/借位
of输出1有符号溢出
sf输出1结果符号位

注意:$signed(a) < $signed(b)依赖综合工具支持有符号比较,如果工具报错,可以改写成(a[WIDTH-1] ^ b[WIDTH-1]) ? a[WIDTH-1] : (a < b)。算术右移在WIDTH=1时会有位选越界,实验里位宽至少 8,不用考虑。

4.2 自检 testbench:覆盖进位、溢出与零标志

测试平台不要只给几个随机数,要把边界固定下来。下面这段 testbench 用任务封装,每调一次打印ABopY和四个标志,方便和功能表对照。重点覆盖0x7F+0x010x80+0x010x00-0x010xFF+0x010x80>>>10x7F<0x80这几组。

`timescale 1ns/1ps module alu_tb; reg [7:0] a, b; reg [3:0] op; wire [7:0] y; wire zf, cf, of, sf; alu #(.WIDTH(8)) dut(.a(a), .b(b), .op(op), .y(y), .zf(zf), .cf(cf), .of(of), .sf(sf)); task check; input [7:0] ta, tb; input [3:0] top; begin a = ta; b = tb; op = top; #10; $display("A=%02X B=%02X op=%X Y=%02X ZF=%b CF=%b OF=%b SF=%b", a, b, op, y, zf, cf, of, sf); end endtask initial begin check(8'h7F, 8'h01, 4'b0000); // 127+1,有符号溢出 check(8'h80, 8'h01, 4'b0000); // -128+1,不溢出 check(8'h00, 8'h01, 4'b0001); // 0-1,借位 check(8'hFF, 8'h01, 4'b0000); // 255+1,无符号进位 check(8'h80, 8'h00, 4'b1010); // 算术右移,保留符号 check(8'h7F, 8'h80, 4'b1011); // 127 < -128,有符号比较 check(8'h55, 8'hAA, 4'b0110); // 异或 check(8'h00, 8'h00, 4'b1100); // 相等 $finish; end endmodule

任务check的三个参数是tatbtop,分别对应被测 ALU 的输入。#10是等待组合逻辑稳定,$display的格式里%02X输出两位十六进制,%b输出二进制标志。每一行后面注释了期望现象,仿真时逐行核对。如果OF0x80+0x01变成 1,说明溢出判断写成了“结果最高位为 1”,那是错的;0x80是负数,加正数结果0x81仍是负数,不溢出。

4.3 用 iverilog 跑仿真并看波形

命令行仿真用iverilogvvp,如果装了波形工具再加gtkwave。下面三条命令分别编译、运行、看波形。-o指定输出文件,-Wall打开警告,能提前发现位宽截断和未连接信号。$dumpfile$dumpvars要加在 testbench 的initial里,否则没有波形文件。

# 编译 ALU 和 testbench,打开警告 iverilog -Wall -o alu_sim alu.v alu_tb.v # 运行仿真,终端打印 $display 结果 vvp alu_sim # 如果 testbench 里写了 $dumpfile("alu.vcd"),用 gtkwave 打开 gtkwave alu.vcd &

参数说明:-Wall会提示WIDTH相关的隐式位宽转换;-o alu_sim生成的可执行文件由vvp解释;gtkwave后面的&是放到后台,不占用终端。如果vvperror: Unable to open input file,先确认alu.valu_tb.v在同一目录,模块名和文件名一致。波形里重点看ycfof,把时间轴缩放到每次check调用的位置,和终端打印对照。

Verilog 仿真和 Logisim 手动连线会得出同一组结果,前提是控制码表一致。常见不一致是 Logisim 里移位器的方向位接反,或者 Verilog 里用了>>>但没加$signed。排错时先把ABop固定成一条边界向量,两边同时跑,看YCF/OF哪一位先分叉。分叉点通常就是接线或表达式写错的位置。

5. ALU 边界用例与排错:进位、溢出、零标志最容易翻车的地方

5.1 边界测试表与自动化穷举

边界数据比随机数据更能暴露 ALU 的问题。下面这张表列出 8 位 ALU 最该先跑的用例,每一行都能对应到功能表里的标志位规则。手动跑完这些,再做穷举或随机测试,效率更高。

用例ABop期望 Y期望 CF期望 OF期望 ZF期望 SF
最大正数加一7F01ADD800101
最小负数加一8001ADD810001
全 1 加一FF01ADD001010
零减一0001SUBFF1001
负数减正数8001SUB7F1100
算术右移负数8000SRAC00001
有符号比较7F80SLT010000
相等比较5555EQ010000

穷举测试可以写一个 Python 脚本,针对 8 位输入生成所有AB和主要控制码,把期望值写成 Verilog$readmemh能读的格式。也可以直接在 testbench 里用双重for循环,把ab从 0 到 255 遍历。穷举 8 位加法的全部组合是 65536 组,仿真时间可以接受;如果位宽是 16 位,就改成随机采样加边界组合,否则组合数会涨到 40 亿以上。

5.2 常见错误与定位方法

第一个高频错误是把无符号进位和有符号溢出混为一谈。FF+01CF=1OF=0,因为按有符号数-1+1=0没有溢出;7F+01CF=0OF=1,因为127+1超出 8 位有符号范围。定位方法是在波形里同时看a[7]b[7]y[7],加法溢出要求a[7]b[7]相同且y[7]与它们不同。

第二个错误是减法借位判断反了。a - ba < bdiff[WIDTH]为 1,表示借位,CF应该置 1;如果接成~diff[WIDTH]00-01就会得到CF=0。减法溢出要求a[7]b[7]不同且y[7]a[7]不同,别直接套加法的条件。

第三个错误是ZF生成时机不对。ZF应该在多路选择器输出之后计算,而不是在某个运算分支里提前算。把所有分支写完后统一zf = (y == 0),就不会出现“逻辑运算零标志正常、移位运算零标志丢失”的情况。SF也一样,统一取y的最高位。

第四个错误是控制码冲突。两个不同的case分支用了同一个op值,综合工具可能只保留一个,仿真和 Logisim 表现不一致。检查方法很简单:把功能表里的op列排序,看有没有重复;写完case后在default分支给y赋 0,避免锁存器。最后用iverilog -Wall重新编译一遍,警告数量降到 0 再上板。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 12:52:40

OData核心实战:Metadata、$select与$filter构建高效查询接口

第一次看到 OData 的请求 URL 时&#xff0c;我是有点懵的——一条查询路径&#xff0c;后面跟着$filter、$select、$orderby&#xff0c;乍一看像把 SQL 直接塞进了网址里。这其实不是错觉。我记得最早给一个内部管理系统对接 SAP 数据&#xff0c;后端为了满足前端各种列表页…

作者头像 李华
网站建设 2026/9/17 12:51:28

MariaDB ZIP包安装:data目录下mysql文件夹无法自动创建的排查与解决

先问自己一句&#xff1a;你看到的 data 目录&#xff0c;是安装程序建的&#xff0c;还是你自己动手建的&#xff1f;用 ZIP 包在 Windows 上装 MariaDB 10&#xff0c;卡在 data 目录下的 mysql 文件夹无法自动创建&#xff0c;这是我见过的高频问题里最容易被误解的一个。很…

作者头像 李华
网站建设 2026/9/17 12:45:58

STM32C5串口调试实战:从引脚映射到printf重定向

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 12:45:57

AlpacaEval 反超 GPT-4 的 Xwin-LM,用 TaoToken Key 对照跑分怎么验证?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华