# P8:FPGA 实验
通过阅读本文,您肯定做不出 P8,本文也仅限于介绍很少一部分 P8 的思路以及实现
P8 作为 FPGA 实验,需要详细参考教程,本文仅供参考
P8 的主要内容是对 Verilog 搭建的 MIPS 微系统进行综合,并使其能够运行在 FPGA 上
课上课下会要求编写汇编代码实现一些功能
# 课下部分
坚持到最后感觉还是写点东西吧,这个系列最好不要半途而废,这里是 P8 的一些做法和提示,详细的等到有时间再慢慢写
首先是 P8 怎么做,P8 很范围很广,看起来很复杂,但是其实如果 P7 写的好,那么把乘除槽注释掉之后过综合不是问题,然后按照要求添加约束文件,换 IP 核之后就完成了很多工作了
然后需要修改的只有顶层模块和桥 bridge 两块,主要是按照要求添加外设
外设中有很多好添加的比如 LED、按动开关和拨动开关,尤其复杂的是串口通信和数码管
数码管建议对着我的代码看看,然后再去仔细看看教程,搞清楚数码管是四个一组,每一次刷新两个 8 的位置,利用视觉暂留原理显示图像
串口通信我也没太搞明白,总之按我的设计文档加一个中断就完了,但是据助教说这样有问题,需要考虑一下 read_over 这个信号,因为 P8 选做,我也没细看,留个坑等以后看懂了再填
后面附的有设计文档
# 课上部分
课上的话三选一,下面就是三道题目,解法就不放了,毕竟这个汇编各自有各自的写法,但是一定注意一下 beq 后面一定要手写一个 nop,否则就会出锅,个人觉得 UART 最难没选,计时器肯定最简单,然后第三题就没看了
另:今年的 P8 可以带自己的笔记本,不知道以后还是不是这样
# 可变速计时器
从拨码开关 (switch) 读入数据: 在计数器进行计数时触发读入操作。拨码开关组 A 中读入一个无符号 32 bit 数字,设该无符号 32 bit 数字为,实现以下功能
控制计时器计数时间间隔: 即为计时器计数的时间间隔秒数,并有以下限制:
- 若,计数器不工作,数码管显示为自定义初始值(该值有且仅有一种,形如 "00000000");
- 若,计数器每隔大约 n 秒进行一次计数的操作,并在数码管上显示;
- 若,视,计数器每隔大约 2 秒进行一次计数的操作。时间间隔可以不精确,但需要使记数速度的差异可以被分辨。
循环计数:在计时器工作时,计数器从 0 计数至 9,之后又回到 0 重新从 0 至 9 计数。计时器任意一个工作时刻所记的数需要在数码管上显示。
举例:拨码开关 1 设置为 00000000000000000000000000000010
,则每隔大约 2 秒数码管上的数值加 1。特别地,当要更新数码管数值时,数码管显示数值为 9,则更新数码管显示数值为 0。
# 十六进制数串口输出
在不改变外部设备代码设计的情况下,编写 mips
汇编程序,实现以下功能。
从拨码开关 ( switch
) 读入数据:从拨码开关组 B 中读入一个无符号 32bit 数字,设该无符号 32bit 数字为。
用户定义开关 ( user key
) 触发操作:将 显示在数码管上(8 个十六进制数字),并通过 UART 将数码管上显示的数字以 ASCII 字符形式输出。
举例:拨码开关 B 设置为 10101010101010101010101010101010
,触发用户按钮 1 后,数码管显示 AAAAAAAA
,UART 在勾选 "ascii mode" 的情况下输出 AAAAAAAA
# 可交互存储器
在不改变外部设备代码设计的情况下,编写 mips 汇编程序,实现以下功能。
从拨码开关 ( switch
) 读入数据:从拨码开关组 A 和 B 中读入两个无符号 32bit 数字,设 A 读入数字为,设 B 读入数字为。
用户定义开关 ( user key
) 触发操作:触发某一个开关,将 显示在数码管上(8 个十六进制数字),并将 写入 所对应的地址中;触发另一个开关,将 地址中储存的值显示在数码管上(8 个十六进制数字)
注意:只需支持 在 DM
范围的访问操作,超出 DM
范围则不需进行访存操作(即不用改变数码管的显示,或者使数码管显示自定义内容);按字存储,地址不对齐则不需进行访存操作。
举例:
- 拨码开关 A 设置为
00000000_00000000_00000000_00000000
,拨码开关 B 设置为00000000_00000000_00000000_00000001
,触发用户按钮 1 后,数码管显示00000001
; - 再将拨码开关 A 设置为
00000000_00000000_00000000_00000004
,触发用户按钮 2 后,数码管显示00000000
; - 再将拨码开关 A 设置为
00000000_00000000_00000000_00000000
,触发用户按钮 1 后,数码管显示00000001
。
# 设计文档
# 总体设计概述
要求实现的指令集为 MIPS-C5
,即 LB、LBU、LH、LHU、LW、SB、SH、SW、ADD、ADDU、 SUB、SUBU、SLL、SRL、SRA、SLLV、 SRLV、SRAV、AND、OR、XOR、NOR、ADDI、ADDIU、ANDI、ORI、 XORI、LUI、SLT、SLTI、SLTIU、SLTU、BEQ、BNE、BLEZ、BGTZ、 BLTZ、BGEZ、J、JAL、JALR、JR、MFHI、MFLO、MTHI、MTLO、MFC0、MTC0、ERET,在 P7 的基础上减少了乘除槽指令
总体文件目录树如下:
CPU 部分的目录树如下:
# CPU 设计
# CPU 数据通路设计
(P5 的图,也没有乘除槽,大致相同吧~)
# 控制逻辑设计
- 控制逻辑为分布式译码控制逻辑
- 第一部分为译码部分,根据输入的 Instr 信号译码,得到 rs,rt 等信息
第二部分为分类部分,根据输入的 opcode 和 funct 决定这条指令是什么,属于哪一类(P6 的分类方法)
第三部分为控制信号生成部分,根据第二部分决定的哪一类指令决定生成什么样的控制信号
控制模块位于每一个流水线层级的顶层,如上面图片所示
在阻塞控制中由于需要对指令分类,顺便复用了控制模块
# Others
可以看出与 P7 相比,P8 增加了较多的外设,解决的主要问题是系统 I/O 设计,但是同时也需要对 CPU 进行修改以符合可综合的要求
首先是去除乘除槽,这一部分直接把相关内容注释掉即可
然后是 IM 和 DM 的改造,按照教程添加 IP 核即可,对于读写时序问题,我的方法是采用新建一个时钟 IP 核,分出 CPU 时钟频率的二倍频去驱动 IM 和 DM,按照教程的说法,这样存储器的行为即与原来一致
然后改变顶层模块端口以符合约束文件的要求,这样即可满足通过综合的要求,即完成了大部分改造工作,对于原有的流水线数据通路,并不需要做任何改造工作
# 整体系统设计
我的整体系统设计与教程保持一致,即 CPU 只与 Bridge 交互,由 Bridge 负责向哪个外设写出数据,从哪个外设读取数据,把 DM 也视作一个外设
对于 LED、数码管、按键等外设,我在 mips.v 中实例化外设,将他们跟桥连接在一起
关于 Bridge 模块的输入输出端口定义如下:
其中 tmp_m_***
为 CPU 发出的读写存储器的请求, m_***
为实际访问的地址和读取的数据,Bridge 会根据 CPU 发出的请求返回合适的数据,通过 tmp_m_data_rdata
返回读取的数据,或者往合适的外设里根据 tmp_m_data_byteen
写入 tmp_m_data_wdata
的值
具体做法是根据请求的地址,选择合适的外设
然后选择合适的读出的值或者给出恰当的写使能
像 DipSwitch,Key 和 LED 这一类较为简单的外设,只有简单的读或写的功能,需要编写的是与 FPGA 外部端口交互的控制逻辑,我直接用简单的赋值完成读写操作
下面以较为复杂的 DigitalTube 为例,说明外设如何连接
首先是译码的函数,每四位表示一个十六进制数字
根据教程,数码管四个为一组,每一次只刷新一个数码管,利用视觉暂留原理,看到的就是变化的数码管数字
在控制逻辑中,我们内置一个状态机,每次经过 32'd500000
个时钟周期后,更新一次状态,选择下一个数字位刷新
刷新时就根据内置的两个寄存器 reg0 和 reg1 的值控制屏幕显示的十六进制数字
还有 UART 模块,课程组已经给出了大部分代码,根据教程提示,我们需要添加中断请求信号,接受状态中的 receive status 信号就是表示是否接收完毕的信号,如果接收完毕会发出中断请求,因此直接把 receive status 当做 IntReq 即可
另外需要在 head_uart.v 中改一下时钟周期,使其与我的设计 55MHz 一致
复位后默认波特率位 9600,不需要特地设置
关于发送,写使能判断发送
# 习题代码设计
# 简易计算器
检测两组拨动开关的值,读出作为运算数
检测按键开关哪一个是开启状态,在我的设计中分别表示加、减、与、或、异或、逻辑左移、逻辑右移和算术右移
然后计算出结果,直接写入数码管外设中即可
代码如下
# *** I/O Address Table ***
# Data Memory 0x0000_0000 - 0x0000_2fff
# Timer0 0x0000_7f00 - 0x0000_7f0b
# UART 0x0000_7f20 - 0x0000_7f3b
# Digital Tube 0x0000_7f40 - 0x0000_7f47
# Dip Switch 0x0000_7f50 - 0x0000_7f57
# Button Key 0x0000_7f58 - 0x0000_7f5b
# LED 0x0000_7f60 - 0x0000_7f63
dead_loop:
lw $s0, 0x7f50($0)
lw $s1, 0x7f54($0)
lb $t0, 0x7f58($0)
beq $t0, 1, ADD
nop
beq $t0, 2, SUB
nop
beq $t0, 4, AND
nop
beq $t0, 8, OR
nop
beq $t0, 16, XOR
nop
beq $t0, 32, SHIFT_LEFT_LOGIC
nop
beq $t0, 64, SHIFT_RIGHT_LOGIC
nop
beq $t0, 128, SHIFT_RIGHT_ALGORITHM
nop
ADD:
addu $s2, $s0, $s1
sw $s2, 0x7f40($0)
j End
SUB:
subu $s2, $s0, $s1
sw $s2, 0x7f40($0)
j End
AND:
and $s2, $s0, $s1
sw $s2, 0x7f40($0)
j End
OR:
or $s2, $s0, $s1
sw $s2, 0x7f40($0)
j End
XOR:
xor $s2, $s0, $s1
sw $s2, 0x7f40($0)
j End
SHIFT_LEFT_LOGIC:
sllv $s2, $s0, $s1
sw $s2, 0x7f40($0)
j End
SHIFT_RIGHT_LOGIC:
srlv $s2, $s0, $s1
sw $s2, 0x7f40($0)
j End
SHIFT_RIGHT_ALGORITHM:
srav $s2, $s0, $s1
sw $s2, 0x7f40($0)
j End
End:
j dead_loop
# 计时器
读取拨码开关的值,存入 $t1
寄存器
外设计时器采用 1 模式,循环计数,设成循环 55M 个周期,因为我的频率是 55MHz,每次停止都触发中断,在中断中停止计数、更新新的计数值、更新数码管显示,然后循环重新设置计时器开始计数
代码如下
# *** I/O Address Table ***
# Data Memory 0x0000_0000 - 0x0000_2fff
# Timer0 0x0000_7f00 - 0x0000_7f0b
# UART 0x0000_7f20 - 0x0000_7f3b
# Digital Tube 0x0000_7f40 - 0x0000_7f47
# Dip Switch 0x0000_7f50 - 0x0000_7f57
# Button Key 0x0000_7f58 - 0x0000_7f5b
# LED 0x0000_7f60 - 0x0000_7f63
.text
ori $2, $0, 0xfc01
mtc0 $2, $12
lw $t1, 0x7f50($0) # Read Time Length From Dip Switch Group 0-3
sw $t1, 0x7f40($0)
li $t2, 55000000
sw $t2, 0x7f04($0) # Set Timer
Start:
li $t2, 0xb
sw $t2, 0x7f00($0) # Start Count
Wait:
bgtz $t1, Wait
nop
li $t2, 0x0
sw $t2, 0x7f00($0) # End Count
dead_loop:
j dead_loop
nop
.ktext 0x4180
subi $t1, $t1, 1 # Count - 1
sw $t1, 0x7f40($0)
eret
# UART 回显
主要是处理 UART 串口通讯
首先是打开中断,然后死循环等待接收信息响应中断
在中断处理程序中,读取接收的数据写入内存,然后 EPC 设为 EPC+4 跳出循环
然后再把数据写入 0x7f20,重新发送会外部,再次回到等待接收信息死循环状态
代码如下
# *** I/O Address Table ***
# Data Memory 0x0000_0000 - 0x0000_2fff
# Timer0 0x0000_7f00 - 0x0000_7f0b
# UART 0x0000_7f20 - 0x0000_7f3b
# Digital Tube 0x0000_7f40 - 0x0000_7f47
# Dip Switch 0x0000_7f50 - 0x0000_7f57
# Button Key 0x0000_7f58 - 0x0000_7f5b
# LED 0x0000_7f60 - 0x0000_7f63
.text
# Turn On the Interrupt
ori $2, $0, 0x1001
mtc0 $2, $12
# Wait receiving data
Wait:
j Wait
sw $t2, 0x7f40($0) # Display the character in the Digital Tube
sw $t2, 0x7f20($0) # Re-Write to UART, Send out
j Wait # Back to send data and waiting
.ktext 0x4180
# When receive completely, IntReq process in 0x4180
lw $t2, 0x7f20($0) # Read Data From UART
sw $t2, 0($0) # Save to the Memory
mfc0 $k0, $14 # EPC + 4, Jump out of the loop
addiu $k0, $k0, 4
mtc0 $k0, $14
eret