12012023 罗嘉俊 贡献比: 33.3% 负责任务: 子模块 (ALU) 以及顶层模块(CPU_Top) , syscall部分
12012924 张骥霄 贡献比: 33.3% 负责任务: 子模块(data memory and ifetch),Uart接口,syscall,七段数码管
12012919 廖铭骞 贡献比: 33.3% 负责任务: 子模块(decoder and controller), 测试场景1, 2 的测试汇编, Report
实现了Minisys指令集,Minisys 指令集是MIPS32指令集的子集
除此之外,我们还额外扩展了syscall 指令
本小组的 CPU 属于哈佛结构,即指令内存与数据内存分开存储。
外设IO寻址空间:
- led 灯:0b10xx_xxxx_xxxx_xxxx_xxxx_xxxx_xxxx_xxxx
- 七段数码管:11xx_xxxx_xxxx_xxxx_xxxx_xxxx_xxxx_xxxx
- Instruction: [15:2],第15位保持0,代表instruction
- Mem: 0x1001_0000-0x1004_0000
本小组的 CPU 属于单周期的CPU,不支持 pipeline,CPU 频率为23MHz,CPI 为1
本小组的 CPU 拥有一个复位按钮,读取输入信息的回车开关,以及时钟信号接口以及 uart 接口
Decoder
-
模块功能:通过输入的指令信号以及来自 controller 对应的信号以及 复位,时钟信号,重置或者读出对应寄存器的数据 (扩展后的32位立即数,以及两个寄存器里面的操作数)
-
端口规格以及功能
input[31:0] Instruction; // 取指单元来的指令 input[31:0] mem_data; // 从DATA RAM or I/O port取出的数据 input[31:0] ALU_result; // 从执行单元来的运算的结果 input Jal; // 来自控制单元,说明是JAL指令 input RegWrite; // 来自控制单元 input MemtoReg; // 来自控制单元 input RegDst; input clock,reset; // 时钟和复位 input[31:0] opcplus4; // 来自取指单元,JAL中用 output[31:0] Sign_extend; // 扩展后的32位立即数 output[31:0] read_data_1; // 输出的第一操作数 output[31:0] read_data_2; // 输出的第二操作数
Controller
- 模块功能:通过输入的指令对应字段,输出与指令类型符合的控制信号(RegDst, RegWrite, ALUSrc, PCSrc, MemRead, MemWrite, MemtoReg)
- 端口规格以及功能
input[5:0] Opcode; // 来自IFetch模块的指令高6bit,instruction[31..26]
input[5:0] Function_opcode; // 来自IFetch模块的指令低6bit,用于区分r-类型中的指令,instructions[5..0]
output Jr; // 为1表明当前指令是jr,为0表示当前指令不是jr
output RegDST; // 为1表明目的寄存器是rd,否则目的寄存器是rt
output ALUSrc; // 为1表明第二个操作数(ALU中的Binput)是立即数(beq,bne除外),为0时表示第二个操作数来自寄存器
output MemtoReg; // 为1表明需要从存储器或I/O读数据到寄存器
output RegWrite; // 为1表明该指令需要写寄存器
output MemWrite; // 为1表明该指令需要写存储器
output Branch; // 为1表明是beq指令,为0时表示不是beq指令
output nBranch; // 为1表明是Bne指令,为0时表示不是bne指令
output Jmp; // 为1表明是J指令,为0时表示不是J指令
output Jal; // 为1表明是Jal指令,为0时表示不是Jal指令
output I_format; // 为1表明该指令是除beq,bne,LW,SW之外的其他I-类型指令
output Sftmd; // 为1表明是移位指令,为0表明不是移位指令
output[1:0] ALUOp; // 是R-类型或I_format=1时位1(高bit位)为1, beq、bne指令则位0(低bit位)为1
Dmemory
- 模块功能:通过传入的信号,输入的内存地址以及需要写入的数据控制内存数据的写入以及读出,并且同步映射到 LED 灯进行显示
- 端口规格以及功能
input clock, memWrite; //memWrite 来自controller,为1'b1时表示要对data-memory做写操作
input [31:0] address; //address 以字节为单位
input [31:0] writeData; //writeData :向data-memory中写入的数据
input [23:0] switch_in; //switch_in :开关输入
output reg[31:0] readData; //writeData :从data-memory中读出的数据
output reg[23:0] led_out; // 控制 LED 灯相关信号
Ifetch
- 模块功能: 通过输入的地址数据以及确定读取指令内存信息的信号来从指令内存获取相应的指令
- 端口规格以及功能
input[31:0] Addr_result; // 来自ALU,为ALU计算出的跳转地址
input[31:0] Read_data_1; // 来自Decoder,jr指令用的地址
input Branch; // 来自控制单元
input nBranch; // 来自控制单元
input Jmp; // 来自控制单元
input Jal; // 来自控制单元
input Jr; // 来自控制单元
input Zero; //来自ALU,Zero为1表示两个值相等,反之表示不相等
input clock,reset; //时钟与复位,复位信号用于给PC赋初始值,复位信号高电平有效
output[31:0] Instruction; // 根据PC的值从存放指令的prgrom中取出的指令
output[31:0] branch_base_addr; // 对于有条件跳转类的指令而言,该值为(pc+4)送往ALU
output reg[31:0] link_addr; // JAL指令专用的PC+4
ALU
- 模块功能:利用输入的数据信息以及对应的运算信号来进行数据之间的运算,运算完毕后设置对应的信号以及结果的数据信息
- 端口规格以及功能
input[31:0] Read_data_1; // 从译码单元的Read_data_1中来
input[31:0] Read_data_2; // 从译码单元的Read_data_2中来
input[31:0] Sign_extend; // 从译码单元来的扩展后的立即数
input[5:0] Function_opcode; // 取指单元来的r-类型指令功能码,r-form instructions[5:0]
input[5:0] Exe_opcode; // 取指单元来的操作码
input[1:0] ALUOp; // 来自控制单元的运算指令控制编码
input[4:0] Shamt; // 来自取指单元的instruction[10:6],指定移位次数
input Sftmd; // 来自控制单元的,表明是移位指令
input ALUSrc; // 来自控制单元,表明第二个操作数是立即数(beq,bne除外)
input I_format; // 来自控制单元,表明是除beq, bne, LW, SW之外的I-类型指令
input Jr; // 来自控制单元,表明是JR指令
output Zero; // 为1表明计算值为0
output reg[31:0] ALU_Result; // 计算的数据结果
output[31:0] Addr_Result; // 计算的地址结果
input[31:0] PC_plus_4; // 来自取指单元的PC+4
CPU top
module cputop(
input rst_in, // 重置信号
input enter, // 回车键控制
input wire[23:0] sw, // 拨码开关输入
input wire clk_in, // 时钟信号输入
output wire[23:0] led, // led 灯输出
//uart pinpoints
input start_pg,
input rx,
output tx,
output[7:0] segment_en, // 七段数码管使能输出
output[7:0] segment_out // 七段数码管显示
);
Debounce防抖模块
module debounce(
input wire clk,// 时钟信号
input wire key_in, // 信号
output reg key_out
);
七段数码管
module seg7(
input rst_n,
input clk,
input [31:0] data,
input uart_start,
input uart_done,
output reg [7:0] segment_en,
output reg[7:0] segment_out
);
Uart
module Uart(
input upg_clk_o,
input upg_rst,
input rx, // 接受来自UART的数据
input upg_clk_w,
input upg_wen_w,
input upg_adr_w,
input upg_dat_w,
output upg_done_w,
output tx//接受完毕后向电脑传输一个信号,告知用户传输完毕。
);
测试方法:仿真 + 上板测试
测试类型:集成
场景1.用例编号 | 用例描述 | 测试结果 |
---|---|---|
3‘b000 | 输入测试数a,输入完毕后在led灯上显示a,同时用1个led灯显示a是否为二进制回文的判定(根据a 的实际位宽来做判断,比如4’b1001是回文,该led灯亮,否则该led灯不亮) | 通过 |
3'b001 | 输入测试数a,输入完毕后在输出设备上显示a;输入测试数b;输入完毕后在输出设备上显示b | 通过 |
3'b010 | 计算 a & b,将结果显示在输出设备 | 通过 |
3'b011 | 计算 a | b,将结果显示在输出设备 | 通过 |
3'b100 | 计算 a ^ b,将结果显示在输出设备 | 通过 |
3'b101 | 计算 a 逻辑左移 b位,将结果显示在输出设备 | 通过 |
3'b110 | 计算 a 逻辑右移 b位,将结果显示在输出设备(测试时会考虑先做左移后再右移) | 通过 |
3'b111 | 计算 a 算数右移 b位,将结果显示在输出设备(测试时会考虑先做左移后再右移) | 通过 |
数据集编号 | 数据集0 | 数据集1 | 数据集2 | 数据集3 |
---|---|---|---|---|
数据集的起始地址 | base + 0 | base + 1*space | base + 2*space | base + 3*space |
数据说明 | 输入数据原样保存 | 将输入数据视为无符号数,按从小到大排序后顺序存放的数据 | 将输入数据视为按照下标中的3’b010对应行的方式做识别,转换成补码后进行存放,存放次序与数据集0中的输入数据一致 | 基于“数据集2”中的数据,视其为有符号数的 补码,按从小到大排序后顺序存放的数据 |
测试用例编号 | 测试描述 | 测试结果 |
---|---|---|
3’b000 | 输入测试数据的个数(小于或等于10),以2进制的方式输入多个测试数据,将测试数据原样存入上表 ”数据集0“对应的空间 | 通过 |
3'b001 | 将测试数据视为无符号数(bit7与bit6~bit0一样,都被视为数值位的一部分),将数据按照从小到大的方式进行排序,排序后的数据集合作 为一个整体存入上表 ”数据集1“ 对应的空间中(数值最小的存放在低地址,依次类推,数值最大的存放在高地址) | 通过 |
3;b010 | 将测试数据(此时”数据集0“中的每一个数据,其bit6~bit0被视为该数值的绝对值,bit7对应该数值的符号位)转换为有符号数的补码形式, 将做完转换后的数据集存入上表 ”数据集2“ 对应的空间中(存放次序与数据集0中的数据一致 | 通过 |
3’b011 | 基于“数据集2”中的数据,视其为有符号数的补码,按从小到大排序后顺序存放的数据,排序后的数据集合作为一个整体存入上表 ”数据集 3“ 对应的空间中(数值最小的存放在低地址,依次类推,数值最大的存放在高地址) | 通过 |
3'b100 | 输入数据集编号(1或3),用该数据集的最大值减去其中的最小值,将结果做输出 | 通过 |
3'b101 | 输入数据集编号(1或3),用该数据集的最大值减去其中的最小值,将结果做输出 | 通过 |
3’b110 | 输入数据集编号(1或2或3)和该数据集中元素的下标(从0开始编址),将该数组元素转为IEEE745单精度浮点形式, 输出IEEE745单精度形式中的符号位(1位)和指数位(8位) | 通过 |
3’b111 | 输入元素的下标(从0开始编址),交替显示两组数据(每隔5秒交替一次) 1)1‘b0,数据集0中该下标对应数据的二进制形式(8bit位宽) 2)1’b1, 该数据的IEEE745单精度浮点形式编码(含符号位(1位)和指数位(8位)) | 通过 |
CPU 可以正常按照测试用例进行运算以及显示,可以完成正常 CPU 具有的功能。
在本 CPU 当中,我们实现了 syscall ,且我们的 syscall 可以比较全面地支持实际 syscall 使用中的大部分功能,具体功能如下所示:
- 显示数字
- 读入数字
- 系统休眠一秒
- 系统休眠指定任意秒数
- 单独控制七段数码管的显示
- 程序退出
并且全部的 syscall 功能是通过软件而不是硬件实现,符合现实操作系统中syscall的实现方式。
对于 syscall,我们小组的实现方式是通过软件进行,即实现了 syscall 之后,我们可以在实际的测试asm文件当中将 $v0 寄存器的值进行赋值来指定使用的 syscall 指令类型,随后直接 syscall 即可使得系统使用该功能。
在测试文件中调用syscall 的方式如下列代码所示
ori $v0, $zero, 1 # 读取数字
syscall
ori $v0, $zero, 2 # 显示数字
syscall
首先对我们小组支持的 syscall 种类进行相应的代码展示以及解释
-
读取数字
需要解释的是第一次循环时在不断读取回车的状态等待回车键按下,第二次循环时在等待回车键松开,而第三部分是在读取 switch 的状态并且存放到 $v0 寄存器,之后 $v0 就放置了按下回车键的时候 switch 存储的内容,实现了对数据的读取。回车键以及 swirch 的状态都存储在一个固定的内存地址。
sys1:
ori $26, $zero, 1
bne $26, $v0, sys2
lui $26, 0xf000
loopenter:
lw $27,0($26)
beq $27,$zero,loopenter
loopenter2:
lw $27,0($26)
bne $27,$zero,loopenter2
#switch
lui $26, 0x8000
lw $v0,0($26)
j exit
- 显示数字
sys2:
ori $26, $zero, 2
bne $26, $v0, sys3
lui $26, 0x8000
sw $a0, 0($26)
j exit
通过一系列测试,测定出来系统休眠秒数与执行 asm 语句条数存在线性关系,于是思考可以通过让 asm 语句执行进入一些循环使得系统 "休眠"。
- 系统休眠一秒
sys4:
ori $26, $zero, 4
bne $26, $v0, sys5
lui $27, 0x0000
ori $27, $27, 0x8000
loopsys4:
addi $26, $26, 1
bne $26, $27, loopsys4
j exit
- 系统休眠指定任意指定时间
sys5:
ori $26, $zero, 5
bne $26, $v0, sys6
or $26,$zero,$zero
or $27,$zero,$a0
loopsys5:
addi $26, $26, 1
bne $26, $27, loopsys5
j exit
- 单独控制七段数码管的显示
sys3:
ori $26, $zero, 3
bne $26, $v0, sys4
lui $26, 0xf000
sw $a0, 0($26)
j exit
- 程序退出
sys6:
ori $26, $zero, 6
bne $26, $v0, exit
loopsys6:
j loopsys6
在扩展了 syscall 指令集之后,调用系统指令可以更加便捷更加直观地在 asm 测试文件中执行,提高了测试文件的编写效率。
在 CPU 当中,我们小组实现了回车键的功能,在用户通过拨码开关输入了数字之后,可以通过回车键进行输入的确认,减少了误触等情况,优化了用户的优化体验。
在 CPU 当中,我们小组利用七段数码管,实现了以下显示:
- 显示输入原始数字
- 显示CPU运算结果
- 显示CPU启动欢迎字符
- 进入 Uart 模式显示 “INPUT”
- 烧写完成显示 “DONE”
- 支持自定义字符扩展
这些七段数码管的显示已经在视频当中都进行了展示。
为了增加 CPU 开机的提示,在 CPU 开机时,会交替在七段数码管显示 “HELLO” 字样与在 LED 灯进行流水灯展示。而这个功能的实现依赖于第一点提到的 syscall 使系统休眠任意秒数的功能。这一部分也已经在视频当中有所展示。
我们的 CPU 总体支持 5 种 IO 设备,这些设备的相关显示已经在视频当中有了充分的展示,我们小组支持的IO设备种类如下所示:
- 拨码开关
- Uart
- 七段数码管
- LED 灯
- 按键
在我们小组实现的 CPU 当中,进行了CPU子模块结构的优化,将 MemOrIO 模块优化综合进入 dmemory 子模块,不需要像原有的 MemOrIO 需要修改其他子模块(controller,decoder...),可以更加优雅地控制IO设备以及内存的映射关系。
在进行 memorIO 的替换,将其合入 dmemory 子模块部分之后,dmemory 模块内部实现介绍如下:
下面这部分代码是通过判断两位高位地址是否是 2'b10
或者是2b11
来指示所用信号是来自拨码开关还是来自回车键或者是数据内存。
always @(*)//read
begin
if(address[31:30] == 2'b10)
readData = read_from_switch;
else if(address[31:30] == 2'b11)
readData = enter;
else
readData=read_from_ram;
end
之后在时钟下降沿的时候判断高两位地址的数值,来替换 memorIO 的功能进行 led 灯以及七段数码管的控制。
always @(negedge clock) // 此处是根据高两位地址的数值来进行 led 灯的对应显示
begin
if(rst) // 如果复位信号为 1,则对 led 灯信号进行置零
led_out<=0;
else if(address[31:30] ==2'b10 && memWrite)// 如果高两位地址信号为 10 且 写入内存信号为1,则led 灯进行对应的显示
led_out<=writeData[23:0];
else
led_out<=led_out;
end
always @(negedge clock)// 此处是根据高两位地址的数值来进行七段数码管相应信息的对应显示
begin
if(rst)// 如果复位信号为 1,则对七段数码管信号进行置零
seg_out=0;
else if(address[31:30] ==2'b11 && memWrite)// 如果高两位地址信号为 10 且 写入内存信号为1,则七段数码管进行对应的显示
seg_out=writeData;
else if(address[31:30] ==2'b10 && memWrite)
seg_out={8'b0000_0000, writeData[23:0]};
else
seg_out = seg_out;
end
通过在 dmemory 里面增加相应的信号控制,可以将 memorIO 子模块进行优化,使得不需要在加入 memorIO 的同时改变其他子模块的对应接口与功能,更加方便地控制了IO设备与内存之间的映射关系。
我们小组还对 Uart 的使用进行了优化,使得高位内存不会被 Uart 覆盖,而将其保留给 syscall。并且高位内存还有较多可供开发,使得 CPU 的可扩展性更优。
对于 syscall 指令集的扩展,以及在 asm 当中调用 syscall 的方式已经在前面的部分进行了阐释,下面我们将对 uart 的优化进行详细阐释。
我们小组对 uart 做的优化是让 uart 只烧写内存的低段,使得 syscall 程序烧写之后便不会被修改。
我们的 syscall 存储在内存的末端,当程序调用 syscall 的时候,就会将程序跳转到对应的区域进行执行。如果 uart 不进行优化,每次烧写新的程序之后, syscall 程序就需要重新烧写,这样会使得效率降低。而通过优化 uart,使得第一次烧写 syscall 的时候将该程序写入地址的高段,在之后通过 uart 烧写程序时,syscall 便会因为在地址的高段而被保留下来,不需要重新进行烧写。并且这样还可以使得高位内存还有较多空间用于开发其他功能。
经过这样的优化,我们的程序可以随时使用 syscall ,并且还可以随时烧写新的程序,syscall 程序并不会受到新程序的干扰,也不需要反复烧写 syscall 程序。