数字逻辑电路实验 -- FGBA

  1. 实验背景
  2. 实验设计
  3. 具体实现
    1. CPU
    2. memory
    3. i/o registers
    4. VGA
    5. 键盘
    6. reprogram
  4. 测试文件
    1. cpu tests in PA/nexus-am
    2. apps in PA/nexus-am
    3. bouncing balls
    4. tank
    5. pacman
    6. Collision Course
  5. 特性
  6. 待填的坑
  7. 附录
    1. 文件结构
    2. 模块结构
    3. 模块接口
      1. cpu_armv4t
      2. memory
      3. io_register
      4. graphic
      5. keyboard
      6. reprogram
    4. 参考资料

这是我和 Massimo 的数电综合试验报告, 代码放在 https://github.com/massimodong/fgba.
实验要求在开发板上实现一个简单计算机系统, 于是我们想写 GBA 模拟器, 实验名称的 FGBA 指 FPGA + GBA.

实验背景

Game Boy Advance (简称 GBA 或 AGB) 是任天堂在 2001 年推出的便携式游戏机.
大概是童年回忆?

GBA 里面有一个 ARM7TDMI 处理器, 支持 32 位的 ARMv4 指令和 16 位的 Thumb 指令, 运行在 16.8Mhz 频率下.
内部存储空间大小是 32kb + 96kb + 256kb.
游戏卡带大小是 32Mb.
屏幕 240 × 160 像素.
除了游戏卡带, 我们的开发板似乎刚好能够支持实现这么一个童年回忆.

实验设计

  • CPU: ARMv4t 指令集
  • 储存: (GBA manual 第 18 页)
    • bios rom, 内存地址 0x00000000 开始. 储存了游戏机只读代码.
    • external ram, 内存地址 0x02000000 开始. 一个 256kb 的通用储存空间, 用作堆区.
    • internal ram, 内存地址 0x03000000 开始. 一个 32kb 的通用储存空间, 用作栈区.
    • i/o registers, 内存地址 0x04000000 开始. 这里的每个地址都对应了一个外设, 如键盘、时钟. CPU 读写这些地址可以获取设备状态或者控制设备.
    • palette ram, 内存地址 0x05000000 开始, 调色板.
    • vram, 内存地址 0x06000000 开始, 显存.
    • pak ram, 内存地址 0x08000000 开始, 储存游戏数据, 只用了片内内存所以目前大概只有 140kb.
  • 外设:
    • VGA.
    • 键盘.
    • UART, 可以通过串行接口传输数据.

具体实现

CPU

  • CPU 内部分为 ARM (32 位) 和 Thumb (16 位) 两个指令集, 除了软件中断和协处理器相关的指令之外都实现了, 大概有一百多条指令, 但可以按照 manual 分几种模式.
  • CPU运行时可以处于 User, System, Supervisor, Abort, Undefined, Interrupt, Fast interrupt 等模式. 其中 User 模式为用户模式, 权限最低, 其他为发生各种类型的中断后进入的模式. 虽然我们没有实现中断, 但是完全按照手册实现了所有的模式. 如果要实现中断的话, 只需要实现中断源 (比如键盘中断), 然后 CPU 在检测到中断后执行跳转就行了.
  • CPU 可以访问 r0 ~ r15 共 16 个通用寄存器, 其中 r15 为 program counter. 对于 r13 和 r14 寄存器, CPU处于不同模式下访问到的实际寄存器是不同的.
  • 具体架构和指令集请看 ARM 手册.
  • 我们的 CPU 采用多周期架构, 一条指令分为 IF, ID, EX 三个阶段, 除了 pusha 等需要多次内存读写的操作之外, 大部分指令都可以在三个周期内完成.
  • 在每条指令的 ID 阶段, CPU 从内存预先读取当前指令顺序的下一条指令, 使得大部分指令(顺序执行的指令)不需要 IF 阶段, 从而只需要 2 个周期.
  • 由于操作有点复杂 (写得不够优美), CPU 主频只有 25MHz.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
CPU 时序图:     0                   2                   4                   6
| IF | ID | EX | IF
v v v v

+---------+ +---------+ +---------+ +--------+
CPU | | | | | | |
| | | | | | |
+--------+ +---------+ +---------+ +---------+

+--------+ +---------+ +---------+ +---------+
| | | | | | |
MEMORY | | | | | | |
+---------+ +---------+ +---------+ +--------+

^ ^ ^
| | |
1 3 5

0: 准备好下一条指令的地址.
1: 从内存的读出指令.
2-4: 译码, 组合逻辑.
3: prefetch, 读顺序的下一条指令.
4-6: 执行, 可能需要不止一个周期.
5: 读写内存.
6: 写回寄存器.

memory

  • memory 是存储控制模块, 与 CPU, VGA, i/o registers 和 重编程模块相连.
  • 当 rpg 信号为低时, 忽略重编程模块, 处理 CPU 的读/写请求, 当 rpg 信号为高时, 忽略 CPU, 处理重编程的写请求.
  • 根据访问地址决定读写哪个存储器, 并执行不同的操作:
    • 通用存储器: 通用存储器有 internal_ram, external_ram, rom 和 pak_ram. 它们的数据宽度是 32 位, 所以对于数据宽度为 32 位的读/写操作, 一个周期内可以完成. 对于 16 或 8 位的读操作, 同样可以在一个周期内完成, 只需要读出相应块后选择一部分输出即可. 对于 16 或 8 位的写操作, 需要两个周期, 第一个周期读出那一块原来的数据, 第二个周期把更新好的数据写入存储器. rom 不支持写操作.
    • i/o registers: 对于访问 i/o registers 的操作, 我们把操作再次分发给 i/o registers 模块, 然后把它返回的结果输出就可以了. i/o registers 模块保证可以在一个周期内完成所有宽度的读/写操作. (毕竟它只是一小堆寄存器)
    • 显存和调色板: 显存和调色板是真双端口的 16 位存储器, 另外一个端口供VGA使用. 调色板只支持 16 位读/写操作, 一个周期内完成. 显存还支持 32 位的写操作(不支持 32 位读), 需要两个周期分别修改两个内存块.
  • VGA 通过显存和调色板的第二个读写端口和它们相连. 但是只用到了读端口.

i/o registers

  • i/o registers 以地址映射的方式把各种外设暴露给 CPU.
  • 目前只实现了时钟, VGA, 键盘, 串口等相关的寄存器, 支持 CPU 在一个时钟周期内进行 8/16/32 位的读写操作.
  • CPU 可以通过读/写相应寄存器来获取设备状态或操作设备. 比如为了防止错帧, CPU 可以读 0x04000006 地址来获取当前显示器扫描线所在行数, 从而决定是否更新屏幕.
  • 因为各种寄存器与设备直接相连, 所以这个模块输入/输出引脚会比较多且杂.
  • 串口是 GBA 手册上没有的设备, 我为了实现 putc 而添加的.

VGA

  • GBA 的屏幕是 240×160 的, 屏幕外面的位置显示了灰色.
  • GBA 有 6 种显示模式. 前三种是 character mode, 显示 8×8 的 object; 后三种是 background mode, 显示整个屏幕的背景. 由于 character mode 实在是太复杂了, 需要数据结构来维护这些 object, 还需要支持旋转, 翻转等操作, 目前没有实现. 只实现了 background mode 里常用的 mode 3 & 4. 其中 mode 3 只有 1 个 frame, 将 vram 中的连续 16 位解释成颜色, 直接显示到屏幕上; 而 mode 4 有 2 个 frame, 将 vram 中的连续 8 位解释成调色板的位置, 然后去调色板中找到相应的颜色, 再显示到屏幕上.
  • 由于访问内存有延迟, 所以在每个时刻我们访问的是若干个(取决于显示模式)像素点之后的像素点颜色.

键盘

GBA 的按键只有 10 个, 下面是按键和键盘的对应关系.

GBA 的按键 键盘上的按键
A button J
B button K
Select N / BACKSPACE
Start M / ENTER
Right D
Left A
Up W
Down S
Right shoulder I
Left shoulder U

reprogram

  • 支持通过串行接口 (uart) 向 FPGA 板发送程序, 实现动态切换程序的功能.
  • FPGA 也可以通过串口输出内容, 从而实现 putc 函数的功能. 但是实现这一功能的不是这个模块.
  • 由于不会使用 FPGA 开发板的自带串口, 所以我们把 GPIO 接线柱作为 rx 和 tx. 其中 rx 为 gpio[8], tx 为 gpio[9].

测试文件

请见 tests 目录.
大多都是 c/c++ 文件, 用 devkitpro 编译成 arm 指令的二进制文件 ( *.gba).
如果想测试的话, 请打开 SW[0] 并按下 key[0](reset), 然后在电脑上用 USB 转 TTL 接口, 将二进制文件发送到 FPGA, 并关掉 SW[0], 就可以开始工作了.

下面是 demo, 包括一些测试的展示.

cpu tests in PA/nexus-am

PA(Programming Assignment) 是南京大学计算机系统基础课的实验项目.
我们改了一下 nemu 的 cpu tests, 如果 Hit Good Trap 会在屏幕显示绿色, 并通过串口发送 "Hit Good Trap".
经过测试, 通过了所有的 tests.
其实是用这些 tests 来 debug. 实名感谢.
来自 https://github.com/NJU-ProjectN/nexus-am/tree/ics2018/tests/cputest.

apps in PA/nexus-am

  • dhrystone
  • coremark

  • microbench

microbench 的 bf fail 了, 但是模拟器上也是这个结果, 所以不管了.
其他的点 ignore 了因为堆区空间不足.
跑分不算很低吧, 自己写的 NEMU 也没高多少.
来自 https://github.com/NJU-ProjectN/nexus-am/tree/ics2018/apps.

bouncing balls

  • bcb 和 bcb2 是紫色的轨迹, 碰撞边框后反弹, 使用 mode3 现实.
    • bcb 通过 cpu 循环计时.
    • bcb2 通过访问 io registers (地址 0x04000100 部分), 使用硬件时钟计时.
  • bcb3 是紫色的方块, 碰撞边框后反弹, 使用 mode4 现实.

tank

一个小游戏, 左右移动坦克来避开子弹.
tank2 是魔改了代码, 子弹有概率会朝坦克方向移动, 且概率随分数增加而增加. 非常鬼畜.
来自 http://www.loirak.com/gameboy/tank.php, 并添加显示分数的功能.

pacman

来自 https://github.com/zjhzyyk/gba-pacman.

Collision Course

模拟太空射击的游戏, 非常好玩.
按 AD 左右旋转, W 前进, J 发射, M 开始 / 暂停.
没有找到代码只有编译好的可执行文件, 但是最终还是在板子上完美运行了.
来自 http://www.gbadev.org/demos.php?showinfo=705.

特性

  • ARM 比 MIPS 复杂多了, 而且没有指导讲义, 完全靠自己.
  • 25MHz 加 prefetch, 一般 2 个周期 1 条指令, 反正比真机快很多, 甚至 PA 里的测试跑分也不低.
  • 通过串口传送数据, 可以动态切换程序. 每次重新编译太慢了.
  • 可以自己写 c/c++ 语言程序编译后放上去跑. 还能 Hit Good Trap.
  • 能跑大部分不含 object 的 140kb 以内的游戏, 并且跑起来和模拟器没有区别, 非常流畅. 然后写完就玩了好久游戏.

待填的坑

  • 提高主频, 还有流水线.
  • 中断, 目前没什么用, 所以没有实现.
  • sdram, 可以跑更大的游戏.
  • 显示, 有了 character mode 就可以跑基本所有游戏了.
  • 音效, 好像不那么重要就先不做了.
  • DMA, 似乎没有需要, 因为我们板子比真实机器快多了, for 循环够用了.

附录

文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
├─quartus_proj # 工程文件, 不展开了
├─scripts # 一些 c 语言脚本
│ ├─bin2mif # 二进制转 mif, 用法: ./bin2mif [数据宽度] [文件名]
│ ├─bin2txt # 二进制转 txt, 用法: ./bin2txt [数据宽度] [文件名]
│ ├─bios # 生成bios用的, 现在应该没用了
│ └─serial # 通过串口发送程序, 用法: ./serial /dev/ttyUSB[数字] [程序名]
└─tests # 测试文件目录, 大部分上面介绍过了
├─bcb
├─bcb2
├─bcb3
├─coremark
├─dhrystone
├─microbench
├─mul
├─nemu-cputests
├─rgb # 在屏幕中间现实 rgb 三个像素点
└─uart2pc # 实现了 putc, 通过串口向电脑发送 `Hello, World`

模块结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fgba # 顶层模块
├── cpu_armv4t # CPU
│ ├── admode1_shifter
│ ├── admode2_shifter
│ ├── ex_shifter
│ ├── alu
│ ├── cond_check
│ ├── mult_add
│ └── smult_add
├── memory
│ ├── bios_rom
│ ├── external_ram
│ ├── internal_ram
│ ├── pak_ram
│ ├── palette_ram
│ └── vram
├── io_register
│ └── uart_send
├── graphic
│ └── vgac # 来自《Computer Principles and Design in Verilog HDL》
├── keyboard
│ ├── ps2_keyboard # 来自讲义
│ └── dispose_keyboard
├── reprogram # 通过串口接收程序, 并且写入内存
│ └── uart_recv
└── uart16 # 生成用于串口通信的时钟

模块接口

cpu_armv4t

引脚 类型 宽度 说明
clk input 1 CPU主频
rstn input 1 reset 信号
mem_addr inout 32 读写内存的地址, 事实上只用了 output
mem_data inout 32 读内存时, 作为数据输入端; 写内存时, 作为写入数据
mem_width output 2 内存读写宽度, 0 表示 8 bit, 1 表示 16 bit, 2 表示 32 bit
mem_read output 1 读使能, 高电平有效
mem_write output 1 写使能, 高电平有效
mem_ok input 1 低电平时, 表示内存操作未完成, CPU 还需要继续等待一个周期
mult_wait_time input 5 对于乘法运算, CPU 要等待的周期数, 仅作调试用

memory

引脚 类型 宽度 说明
clk input 1 CPU主频取反, 这样上升沿就在一个 CPU 周期的正中间了
clk_25mhz input 1 vga 时钟
mem_addr inout 32 CPU 内存访问地址
mem_data inout 32 CPU 内存访问数据
mem_width input 2 CPU 内存访问宽度
mem_read input 1 CPU 内存读使能
mem_write input 1 CPU 内存写使能
ok output 1 低电平表示读/写操作未完成, CPU需要继续等待
vgac_addr input 16 VGA 访问的显存地址
vgac_data output 16 VGA 访问的显存数据
vgac_palette_addr input 8 VGA 访问的调色板地址
vgac_palette_data output 16 VGA 访问的调色板数据
rpg input 1 串口重编程使能信号, 高电平时忽略 CPU 的读/写, 转而接收重编程模块的写信号
rpg_addr input 23 串口重编程地址
rpg_data input 32 串口重编程数据
rpg_write input 1 串口重编程写使能信号
io_addr output 24 i/o register 地址
io_data_in output 32 i/o register 写入数据
io_data_out input 32 i/o register 读出数据
io_read output 1 i/o register 读使能
io_write output 1 i/o register 写使能
io_width output 2 i/o register 数据宽度

io_register

引脚 类型 宽度 说明
clk_mem input 1 内存时钟
clk_uart16 input 1 串口时钟
addr input 24 地址
data_in input 32 输入数据
data_out output 32 输出数据
read input 1 读使能
write input 1 写使能
width input 2 读写宽度
vgac_v_addr input 8 当前显示器扫描线所在行数
key_data input 10 键盘按键情况
dispcnt output 16 显示控制寄存器, 请看手册[^2]第五章
mult_wait_time output 5 对于乘法运算, CPU 要等待的周期数, 仅作调试用

graphic

引脚 类型 宽度 说明
clk input 1 25mhz 的时钟
vram_addr output 16 显存访问地址
palette_addr output 8 调色板访问地址
v_addr output 8 当前显示器扫描线所在行数, 注意这里模拟了 GBA 的屏幕, 所以当行数超过227时, 输出的值是227
vram_data input 16 显存返回数据
palette_data input 16 调色板放回数据
dispcnt input 16 显示控制寄存器, 请看 GBA 手册第五章

keyboard

引脚 类型 宽度 说明
clk input 1 一个足够快的时钟 (50mhz)
ps2_clk input 1 ps2_clk
ps2_data input 1 ps2_data
en output 10 键盘按键情况, 低电平表示按下

reprogram

引脚 类型 宽度 说明
clk_50mhz input 1 CPU 的主频, 名字为 clk_50mhz 是因为项目开始时 CPU 主频定为 50mhz, 但是事实上现在是 25mhz
clk_uart16 input 1 串口通信频率 115200 bit/s 的 16 倍
rstn input 1 reset
rx input 1 串口输入线
addr output 23 内存访问地址
data output 32 写入内存的数据
write output 1 写使能
xorc output 8 读到的所有数据的异或和, 接在 LED 上, 用于验证数据是否正确

参考资料

  1. ARM Architecture Reference Manual
  2. AGB Programming Manual Version 1.1
  3. CowBite Virtual Hardware Specifications
  4. Gameboy Advance Development
  5. Visualboyadvance M
  6. Gameboy Advance Programming for Beginners
  7. NJU-ProjectN/nexus-am