☰
计算机组成原理课设:求5个数正数平方和的补码与数据通路实现
2026/10/16 14:21:14 网站建设 项目流程

简介:这是桂林电子科技大学信息安全专业《计算机组成原理》课程设计报告,课题为设计嵌入式CISC模型机,实现5个有符号整数的平方和验证程序。文档覆盖模型机数据通路框图、操作控制器逻辑框图、11条指令格式与微程序流程图、顶层电路图等内容,并配有汇编与机器语言源程序及功能仿真波形,完整呈现从算法到硬件的验证流程。资源包共1个doc文件,大小约1.11MB,适用于计算机组成原理课设或复习微程序控制器设计。目前已有318人学习浏览。文档详细说明了IN1、MOV、CMP、JL、JMP、TEST、JS、INC、OUT1、ADD、IMUL等指令格式与地址转移逻辑,结合定长CPU周期、联合控制方式、SF符号标志位及JS/JNS条件转移指令的运用,并附故障分析、心得体会与软件清单,可为独立完成类似模型机设计提供可借鉴的模板与排错思路。

1. 组成原理课设“求 5 个数正数平方和”:看着像数学题,卡人的全是符号位和取指

计算机组成原理课设里,这类“输入 5 个数、只把正数平方再求和”的题目出现频率很高,不少同学第一反应是“这不就是写个 for 循环”,真上手才发现,卡点全在三个地方:补码符号位怎么判、乘法结果从哪个寄存器拿、分支指令在数据通路上怎么跳。这个课设把汇编程序、指令格式、寄存器堆、ALU、时钟周期串成一条线,用来检验对 CPU 工作过程的理解特别合适。无论你是在仿真平台做汇编,还是要画数据通路图、写实验报告,这篇文章都按可复现的步骤把实现和坑位讲清楚,适合正在赶进度的人直接照做。

2. 先过算法关:正数判定、平方累加与循环控制的边界

2.1 补码符号位:判定“正数”的唯一正确姿势

在计算机里没有天然的“正负号”,只有二进制位。大多数教学 CPU 用补码表示有符号数,最高位是符号位:0 表示非负,1 表示负数。所谓“正数判定”,本质就是检查符号位为 0,同时排除 0 本身。

很多人的课设翻车,不是不会写if,而是把“正数”误判成“非负数”。以 4 位补码为例:

十进制二进制补码符号位是否正数
+300110是
+000000否
-111111否
-411001否

由表可见,只判断“符号位为 0”会把 0 也放进来。题目如果写的是“正数平方和”,就应该严格采用x > 0的语义;如果写成“非负数”,才用x >= 0。这个区分直接影响指令选择:MIPS 里可以先slti判断“是否小于 0”,再配合分支,比直接依赖bgez更可控。

先写一段伪代码把输入输出边界固定住:

count = 5 sum = 0 loop: if count == 0 goto done value = syscall_read_int() if value <= 0 goto skip square = value * value sum = sum + square skip: count = count - 1 goto loop done: syscall_print_int(sum)

这段逻辑的要点是“先判边界再运算”:进入循环先查计数器是否到 0,随后判断value <= 0而不是value < 0。用<= 0把负数排除,也自然把 0 排除,一次分支搞定。如果你用的是beq成对出现的老式教学指令集,可能需要拆成“先判 0 再判负”两步,那就要额外加一条beqz,这时候务必把真值表提前列好,否则调试时很难定位是分支条件写反还是数据写错。

2.2 五轮循环汇编实现:syscall、乘法与累加寄存器怎么安排

下面的程序用类 MIPS 指令集实现“读取 5 个整数,累加正数平方,输出结果”。它按“边读边算”的方式处理,不需要额外数组,这也是课设报告里最常建议的写法:

.data prompt: .asciiz "input 5 numbers: " summsg: .asciiz "sum: " .text .globl main main: li $t0, 5 # 循环计数器,剩余待处理个数 li $s0, 0 # 累加器,保存正数平方和 la $a0, prompt li $v0, 4 syscall # 打印提示字符串 loop: beqz $t0, done # 计数器减到 0,退出循环 li $v0, 5 syscall # 读入一个整数,结果保存在 $v0 slti $t2, $v0, 1 # 如果 $v0 < 1,说明该数不是正数 bnez $t2, skip # 负数或 0 都跳到 skip,不参与平方累加 mult $v0, $v0 # 计算 value * value,64 位结果存入 HI/LO mflo $t1 # 取低位 32 位平方结果 add $s0, $s0, $t1 # 累加到求和寄存器 skip: subi $t0, $t0, 1 # 每处理一个数,计数器减 1 j loop # 回到循环入口 done: la $a0, summsg li $v0, 4 syscall # 打印输出提示 move $a0, $s0 li $v0, 1 syscall # 打印平方和 li $v0, 10 syscall # 程序退出

这段代码的寄存器分工非常明确:$t0只负责计 5 个数,$s0只负责累加。$v0是系统调用和返回值专用寄存器,所以读入的数不能长期放在$v0里,否则下一次syscall会把它覆盖。许多同学累计结果莫名消失,就是把中间值一直留在$v0或$v1里,没意识到系统调用本身就是写者。

mult指令在 MIPS 里产生 64 位乘积,高 32 位存$hi,低 32 位存$lo,所以要用mflo把$lo取出来。对 16 位教学 CPU 来说,两个 16 位补码相乘最高可能到 65536,如果只用mflo相当于只取低 16 位,大数会出现溢出错误,本文第 4 章会专门讲这个坑。

2.3 0 算不算正数?为什么课设里最好先把规则说死

这个问题看起来很小,写实验报告时却能影响一大段文字。如果题目原文是“正数”,0 不算;如果有的同学理解为“非负数”,0 平方后不改变结果,但流程图、真值表和测试用例全要改。

建议在报告开头就把规则写到“设计说明”里:

  • 输入范围:学习平台默认有符号 16 位整数,范围从 -32768 到 32767;
  • 判断条件:只有value > 0的输入参与平方和计算;
  • 边界情况:输入 5 个负数时,输出为 0;
  • 溢出处理:单次平方若超过 16 位范围,在报告中明确“以低 16 位为准”或改用 32 位位宽。

另一种做法是先把 5 个数存进内存,再统一求和:

.data buf: .word 0, 0, 0, 0, 0 .text li $t0, 5 la $t1, buf read_loop: beqz $t0, calc_start li $v0, 5 syscall sw $v0, 0($t1) # 存入 buf addi $t1, $t1, 4 # 移动到下一个字 subi $t0, $t0, 1 j read_loop calc_start: ...

先存后算的优势是调试更直观:数据在内存里,仿真波形可以逐条观察加载和累加过程;劣势是控制流更长,容易在“指针是否回移”上出问题。如果课设要求展示取数、访存和写回,这种写法反而更容易讲出东西。我一般建议学生先把直接累加版本跑通,再加内存版本,两份测试结果一致后,再决定报告里采用哪种结构。

3. 把汇编映射进 CPU:取指、译码、乘法与时钟周期的配合

3.1 从一条汇编指令到一组控制信号:R 型、I 型与分支指令

课设要求的往往不只是“程序能跑”,还要你在数据通路图上标出每一条指令经历了哪些部件。先把涉及到的指令分类列清楚:

指令类型指令示例需要什么部件
R 型add、mult、mflo、slti 可能需要寄存器堆、ALU、写回选择
I 型li、syscall 辅助指令立即数扩展、ALU、内存
分支beqz、bnez分支比较器、PC 选择器

在精简指令集里,add $s0, $s0, $t1这条指令由opcode和funct共同决定功能。取指后,控制单元根据opcode生成一组二值控制信号,例如:

  • RegWrite = 1表示 ALU 计算结果允许写回寄存器;
  • ALUSrc = 0表示 ALU 的第二个操作数来自寄存器堆而非立即数;
  • MemtoReg = 0表示写回数据来自 ALU 而不是内存。

以beqz为例,寄存器堆读出的数据经过“零检测”逻辑后,控制单元根据结果决定是否把分支目标地址送进 PC。这一步非常适合出答辩题,因为很多人只记得画连线,忘了分支条件本身需要比较,而不是“看到指令就跳转”。

我用一个极简 RTL 表示法描述add指令的数据流,方便写报告:

IR <- MEM[PC] # 取指 PC <- PC + 4 # PC 递增 ALU_A <- Reg[rs] # 读 2 个源寄存器 ALU_B <- Reg[rt] # 读 2 个源寄存器 Reg[rd] <- ALU_A + ALU_B # 写回

写报告时,每一行对应一个时钟周期。如果采用单周期 CPU,上面四步会压缩到同一个时钟沿里完成;如果采用多周期 CPU,这四步会被分配到 IF、ID、EX、WB 四个阶段,差别就体现在控制信号的时序表上。

3.2 数据通路骨架:寄存器堆、ALU 和乘法器怎么接

下面这段 Verilog 骨架展示一个 16 位平方计算单元的核心逻辑,可以直接用到逻辑仿真工具里:

module square_alu ( input wire [15:0] a, input wire [15:0] b, input wire [2:0] op, output reg [31:0] y ); always @(*) begin case (op) 3'b000: y = a + b; // 加法 3'b001: y = a - b; // 减法 3'b010: y = {{16{a[15]}}, a} * {{16{b[15]}}, b}; // 补码符号乘法 3'b011: y = (a > 0) ? a : 16'd0; // 正数筛选测试 default: y = 32'd0; endcase end endmodule

模块参数说明:a和b是 16 位有符号输入,op是 3 位操作码,y是 32 位结果。让结果位宽为 32 而不是 16,是为了展示平方后可能溢出的情况;如果课设要求最终输出 16 位,可以在后级用y[15:0]截断并增加溢出标志。

实际数据通路里,寄存器堆大概长这样:两个读地址端口负责把源操作数送到 ALU,一个写地址端口负责把 ALU 结果写回。关键就在“写使能”信号——只有控制单元确认当前指令需要写寄存器,时钟上升沿才会把结果锁存进去。如果你仿真发现输出变化了但寄存器没更新,大概率是RegWrite没有拉高,而不是 ALU 算错。

3.3 为什么平方和不能拍脑袋加进单周期:时钟周期的倒压

单片机里一条乘法指令是现成的,但在组成原理课设里,你要自己决定“加法器”和“乘法器”在哪个周期工作。平方运算如果用组合逻辑直接乘,16 位乘法器的关键路径可能比加法器长一倍以上,单周期定频时,整个 CPU 的时钟频率会被乘法器拖慢。

多周期方案把执行拆成五步:

周期阶段做什么
T1IF从指令存储器取指令,PC+4
T2ID读寄存器,生成立即数
T3EXALU 执行平方累加运算
T4WB写回寄存器堆
T5复用若指令是mult,可在 EX 中占两个周期

在报告里画出这一步的时序表,比单纯贴波形更容易拿分。尤其要说明的是:mult之后紧跟mflo,如果流水线没有停顿机制,mflo可能读到旧值,这就是所谓的数据相关。单周期 CPU 没有这个问题,但速度慢;多周期 CPU 需要额外的stall控制,这也是课设加分的常见扩展点。

4. 课设避坑排查:上板不亮、循环跑飞、负数误判的 5 个实例

4.1 现象一:仿真全绿,板上却完全不出结果

常见表现是 ModelSim 或仿真平台里波形正确,数码管和终端死活没输出,字符在串口助手看到的是乱码。

原因:仿真时钟默认 1MHz 与实验板 50MHz 不一致,按键没有消抖,输出端没有同步到系统时钟域;也可能是顶层模块根本没有把 ALU 结果接到 LED 或数码管位选信号上。

解决:先检查引脚约束文件,确认输入输出端口确实映射到物理引脚;再给按键输入加两级同步和消抖计数器。许多教学平台的 5 个数输入用的是拨码开关组,没有同步时高频率采样会误读跳变沿,表现就是“按下一次,寄存器被写了两次”。

4.2 现象二:输入了 5 个数,循环却走了 6 次

典型现象是最终结果没有错,但你已经多读了一个数,或者数组里最后一个值被重复计算。

原因:计数器更新指令放在分支之后,出现beq和延时槽指令的先后顺序问题。MIPS 的传统分支有延迟槽,分支跳转后的下一条指令已经执行了;如果你把“计数器减 1”放在分支跳转目标里,旧版仿真器可能多执行一次。

解决:把计数器递减放在循环体的开头,或者确认仿真器是否默认关闭延迟槽。更保险的做法是:

  • 先减计数器;
  • 再判断计数器是否为 0;
  • 最后再跳转。

这样无论延迟槽开不开,结果都唯一。报告里也应写明你的 CPU 是否实现分支延迟槽。

4.3 现象三:负数也被累加进平方和

最坑的表现是输入-3,输出结果竟然包含9,而且你能确认代码明明写了bgez之类的分支指令。

原因:bgez语义是“大于等于零跳转”,它本身把 0 归为正数。另一个更隐蔽的原因是符号位没有做扩展:当你把FFFFFFFF的负数从内存中加载时,如果最高位被当作数据而不是符号,CPU 会把负数当成 65536 附近的大正数,平方和直接膨胀。

解决:统一使用slti $t2, $v0, 0再bnez $t2, skip,这条组合在带符号比较下绝不会污染 0;同时在数据通路图里画出Sign-extend单元。调试时先构造五个最小的测试数据:-32768, -1, 0, 1, 32767,逐项核对。

4.4 现象四:乘法输出只有一半,字数越界时结果还是 0xFFE1

表现是输入3输出9没毛病,输入200再平方就变成负数或者出现不明字符。

原因:前面说过,mult的结果是 64 位,mflo只给出低 32 位。教学 CPU 如果只有 16 位数据总线,200 * 200 = 40000已经超过 16 位有符号范围,数值被截断。

解决:参考设计项里把平方结果声明为 32 位;如果标准必须限制在 16 位,要在输入时做一个“若绝对值大于 181 则报溢出”的检查。这样可以避免答辩时被问“为什么 200 的平方不对”。我通常会在代码里加一个overflow寄存器和对应输出引脚,用于演示。

4.5 现象五:改完源码重新仿真,结果和之前一模一样

这不算 CPU 错了,而是工具使用问题。常见原因是没有重新编译顶层模块,或者测试激励文件里仍然引用旧的初始化内存。

解决:把每个版本的数据通路图和测试向量存成独立文件,文件名带时间戳。修改源码后必须重新执行“重新编译 → 重新加载初始化文件 → 重新仿真”三步。若使用 FPGA 板,还要确认工程里是否残留了旧 IP 核,特别是块内存 IP 的初始化文件路径变了,结果会一直引用旧的.coe文件。

5. 进阶验证:用 5 条测试向量把课设收敛到“答辩敢问”

测试向量不能只用“全正数”或“全负数”这种只验证一侧的用例。我常用的最小测试表如下:

向量编号输入序列期望输出覆盖点
15, 3, 1, 2, 455全为正数
2-1, -2, -3, -4, -50全为负数
3-1, 0, 1, -2, 25含 0 与负数
41, 2, 3, 4, 555与向量 1 相同结果
55, 0, 0, 0, 025仅一个正数

最后再加一条边界向量:-32768, 32767, 32767, -32768, 7,用于观察乘法溢出是否被正确提示。如果平台支持 32 位操作数,可以把位宽参数化,直接改成 32 位累加器。

`define WIDTH 32 module testbench; reg [`WIDTH-1:0] test_data[0:4]; integer i; reg [`WIDTH-1:0] sum; initial begin test_data[0] = 5; test_data[1] = -3; test_data[2] = 0; test_data[3] = 7; test_data[4] = -2; sum = 0; for (i = 0; i < 5; i = i + 1) begin if (test_data[i] > 0) sum = sum + test_data[i] * test_data[i]; end $display("sum = %d", sum); end endmodule

这段测试平台把“遍历、判断、累加”和硬件实现严格对应,便于你提前看期望值。如果跑完的结果不是74,可以从输入加载阶段开始查,而不是直接怀疑 ALU。

答辩前建议准备两个扩展点:一是把“固定 5 个数”改成“先输入 N,再输入 N 个数”,这涉及动态循环控制和额外内存分配;二是增加溢出标志位,在平方和超过位宽时输出“OVF=1”。答辩时主动讲这两个点,比等老师问再答要主动很多。

从那以后,我每次做组成原理课设都会先把测试向量写成独立文件,再写源代码和硬件描述,最后仿真比对。这个习惯帮我少踩了无数次“程序看着对、结果全乱”的坑。希望这份拆解也能让你的课设少走几个弯路,一次跑通后在报告和仿真上都能拿出真正能解释清楚的东西。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询