原文地址:https://www.bilibili.com/read/cv737296

前言

Game Boy这个词,相信一定能勾起不少80后和90后的童年回忆。从简单的打砖块到精灵宝可梦、塞尔达传说,这个平台上出现过太多经典的作品。而Game Boy本身也是从1989年开始一直生产到了2009年,总销量超过一亿台,成为了历史上第二畅销的掌上游戏机。值得指出的是,虽然Game Boy一直生产到了2009年,但是其主要的结构更接近于80年代流行的如FC(红白机)一类的8位游戏机。巨大的销量使它成为了最容易得到的8位复古游戏机之一。这一特点吸引了不少热爱8位游戏独特视听感受的玩家购买游玩Game Boy,也吸引了不少独立游戏开发者。时至今日,依然有爱好者在为Game Boy平台开发新的游戏。而Game Boy这一特点不仅仅吸引了游戏玩家。作为一台8位机, Game Boy内置的四声部PSG(可编程音频发生器)可以产生8位机独有的芯片音乐Chiptune。因此Game Boy受到了不少芯片音乐爱好者的追捧,包括也出现了如LSDJ一类的Game Boy专用的编曲软件,专门用于进行Chiptune音乐的编曲制作。8位机本身结构简单易于理解的特点也使Game Boy成为了一部分高校课堂中使用的例子。如佐治亚理工大学开设了Game Boy游戏设计课程帮助学生学习理解计算机体系架构,而卡内基梅隆大学也把设计一台Game Boy作为其高级数字系统设计课程的课程设计选题之一。这一切,发生在其停产近10年后的今天,也证明了这个经典平台强大的生命力。

要游玩Game Boy的游戏,当然是需要一台Game Boy和一张游戏卡带。如果想要玩的游戏是曾经的商业游戏,那只需要购买一张二手的卡带即可。但是如果是爱好者自制的游戏(或者程序),或者想要在Game Boy上运行自己编写的游戏,那就没有那么轻松了。通常而言,商业游戏的卡带是只可读取不可写入的,而为了写入自己的游戏,则需要一张可以写入的Game Boy烧录卡。

目前市场上可以购买到一些现成的烧录卡,然而价格都比较高昂,通常超过Game Boy本身的好几倍。然而好在,Game Boy的技术资料很容易在网上找到,只要能了解其原理,自己设计并制作一张烧录卡是可以做到的。

原理

Game Boy的卡带,就是一个用来存储数据的设备。而要自制一个卡带,需要研究清楚的就是其接口和协议。好在,如上文所述,Game Boy的整体架构是和80年代的8位机很接近的,里面也就不会有太多复杂的东西。

上网搜索一下不难找到Game Boy的卡带接口的引脚定义,如下表所示:

https://www.zephray.me/api/media/1531783908485-20180715094440.png

可以看见,主要的信号就是一组地址总线和数据总线。熟悉单片机的朋友可能已经发现了,这些信号和一般说的8080总线很接近。实际上,Game Boy使用的正是一颗8080兼容的处理器,所以也就不奇怪了。

Game Boy对于卡带的操作有两种,一种是读取,一种是写入,分别由nRD信号和nWR信号控制。当nRD信号有效(即nRD为低电平)时,Game Boy从卡带读取数据,而当nWR信号有效时,Game Boy向卡带写入数据。因为Game Boy的卡带接口并没有复用地址和数据线,就是一组独立的16位地址总线和一组独立的8位数据总线,所以连接简单的ROM并不需要额外的电路,把对应的信号连接上就可以了。Game Boy会向地址线输出地址,而卡带上的存储器根据nRD或者nWR信号向或者从数据线输出或者读取数据。

所以说卡带上只需要一片ROM芯片然后把线连起来就完成了?不完全是。最简单的卡带确实只需要一个ROM,但是这样缺点也很明显:容量。Game Boy卡带槽一共提供了16位地址线,也就是最多可以表示2^16=65536种地址,乘以数据线的8位(一字节),最大只能寻址64KB的内存。然而这个总线并非只是ROM独享,而是整个系统上各个部分共享的,内部内存、外设寄存器、显存等等的东西也都在这个总线上。总共分配给ROM的空间只有32KB。确实存在一些只需要32KB的游戏,比如俄罗斯方块,然而大部分的游戏需要更大的空间,比如塞尔达传说:梦见岛DX需要8MB的空间。

那么如何解决这个问题呢?这个问题其实是个在计算机系统中很常见的问题,Game Boy使用的解决方法也很常规,分页。简单来说,卡带内有一个控制器,可以负责在总共的ROM空间中,取出一部分,映射到有限的32KB地址空间当中。而这个控制器,被任天堂称作Memory Bank Controller(内存分页控制器),简称MBC。而我们要自制烧录卡,重要的一部分就是模拟一个MBC出来完成内存分页的工作。

https://www.zephray.me/api/media/1531783969627-mbc5_cart.jpg

左上角的即为MBC芯片

在开始之前,再来讲讲MBC分页具体的实现方式吧。MBC在总线上的地址是和ROM重复的,所有对ROM位置的写入操作会被MBC接收到,读取操作则会被ROM接收到(因为ROM不需要写入而MBC不需要读取,所以可以共享地址。)ROM的地址低位(A13-A0共14位,16KB)直接连接到地址总线上,而ROM的地址高位(A22-A14共9位,512页)这是连接到MBC上。主机可以向MBC写入需要选通的页数,MBC就会输出对应的地址。(其实也就是9个D触发器)MBC其它的功能还有RAM分页(工作方式类似于ROM,用于为卡带上的RAM提供分页功能)、RTC计时等。

方案选择

要实现MBC的功能,可以有两种选择,一种是使用单片机模拟,另外一种是使用CPLD实现。单片机模拟要求单片机具有足够快的速度,可以及时输入输出数据完成MBC行为的模拟。而CPLD实现则没有太多的要求,因为逻辑足够简单,通常最小规模的CPLD都可以满足要求。而且考虑到GB是使用电池供电的设备,卡带应该尽可能省电。MBC本身是不需要时钟就可以工作的,使用CPLD实现逻辑可以实现比高频单片机模拟逻辑更低的功耗。为此本设计选用的CPLD来实现。我选择的是Xilinx的XC2C32A CPLD,是Cool Runner-II产品线中逻辑规模最小的一款,只有32个宏单元,不过也足够了。

Game Boy使用5V电平,而CPLD为3.3V的IO电平,ROM和RAM也通常为3.3V的IO电平。为此,比较合理的方案是把整个电路设计成3.3V的,在接口部分加上总线驱动器以实现电平转换。

电路设计

整体的设计并没有太多特殊的地方,就和计划的一样,在接口处连接总线驱动器实现电平转换。需要注意的是,RST信号并非由Game Boy输出给卡带,而是卡带输出给Game Boy。如果不需要复位Game Boy,这个引脚应保持浮空且不应用于复位卡带上的元件。AIN和CLK不使用。

https://www.zephray.me/api/media/1531784044939-ncgb_sch1.png

RAM和ROM部分也和描述一样,数据线直接和Game Boy相连(这里是总线驱动器输入/输出)

https://www.zephray.me/api/media/1531784048130-ncgb_sch2.png

而CPLD部分则是连接上RAM和ROM的地址高位,Game Boy输出的控制信号,数据总线和一部分地址总线(任天堂的设计中MBC就是只有一部分地址线的,毕竟MBC本身也没有太多可以调整的寄存器。)CPLD除了需要3.3V IO电压之外,还需要一个1.8V核心电压,也使用一个LDO提供。CPLD通过JTAG接口烧写程序,记得留好焊盘或者测试点。

https://www.zephray.me/api/media/1531784051507-ncgb_sch3.png

那么电路设计就是这样了。最终焊接完成的电路板如图:

https://www.zephray.me/api/media/1531784140800-DSC_4515_2S.jpg

CPLD代码设计

通常而言,CPLD内部逻辑的设计方法有两种,一种是直接画原理图,用逻辑门来表示需要实现的逻辑;另外一种是使用硬件描述语言(HDL)通过代码来描述需要实现的逻辑。我这里使用Verilog语言来实现MBC的逻辑。

首先是ROM分页的实现,把对应地址的写入存入寄存器,并且把寄存器内容输出到ROM地址线高位即可。需要注意的是可分页区域只有32KB空间中的高16KB,低16KB为固定的第0页,为此检测到主机在读取低16位时应禁用分页输出。

reg [8:0] rom_bank = 9'b000000001; //高16K默认映射第1页

wire [15:0] gb_addr = { GB_A[15:12], 12'b0 };
wire rom_addr_en = (gb_addr >= 16'h0000)&(gb_addr <= 16'h7FFF); //当前读取地址在ROM范围内
wire rom_addr_lo = (gb_addr >= 16'h0000)&(gb_addr <= 16'h3FFF); //当前读取地址在ROM低16K范围内

wire rom_bank_lo_clk = (!GB_WR) & (gb_addr == 16'h2000); //主机写入ROM分页高位
wire rom_bank_hi_clk = (!GB_WR) & (gb_addr == 16'h3000); //主机写入ROM分页低位

always@(negedge rom_bank_lo_clk)
begin
  rom_bank[7:0] <= GB_D[7:0]; 
end

always@(negedge rom_bank_hi_clk)
begin
  rom_bank[8] <= GB_D[0];
end

assign ROM_A[22:14] = rom_addr_lo ? 9'b0 : rom_bank[8:0]; //输出ROM地址高位
assign ROM_CS = ((rom_addr_en) & (GB_RST == 1)) ? 0 : 1; //输出ROM片选

RAM分页也是相同的原理。与ROM不同的是,RAM还有一个专门的开关位,可以用于禁用RAM的读写。

wire ram_addr_en = (gb_addr >= 16'hA000)&(gb_addr <= 16'hBFFF); //当前读写地址在RAM范围内

wire ram_bank_clk = (!GB_WR) & ((gb_addr == 16'h4000) | (gb_addr == 16'h5000)); //主机写入RAM分页
wire ram_en_clk = (!GB_WR) & ((gb_addr == 16'h0000) | (gb_addr == 16'h1000)); //主机写入RAM使能

always@(negedge ram_bank_clk)
begin
  ram_bank[3:0] <= GB_D[3:0];
end

always@(negedge ram_en_clk)
begin
  ram_en <= (GB_D[3:0] == 4'hA) ? 1 : 0; 
end

assign RAM_A[16:13] = ram_bank[3:0]; //输出RAM地址高位
assign RAM_CS = ((ram_addr_en) & (ram_en) & (GB_RST == 1)) ? 0 : 1; //输出RAM片选

这样MBC的功能就实现了。不过因为之前提到了增加了一个总线驱动器用于实现电平转换,而总线驱动器本身需要一个额外的信号来控制数据总线的传输方向(从Game Boy到卡带或者从卡带到Game Boy)。这个信号也不难处理:

assign DDIR = (((!ROM_CS) | (!RAM_CS)) & (!GB_RD)) ? 1 : 0; //数据总线驱动方向控制

至此,软件部分就完成了。综合后一共使用了27个宏单元,在设备容量范围之内。

调试&效果

可以将卡带插入到烧录器上,使用烧录器产生必要的信号进行调试:

https://www.zephray.me/api/media/1531784313294-test.jpg

确认了可以正确读写之后就可以烧录游戏测试了:

https://www.zephray.me/api/media/1531784318424-cart_flasher.png

https://www.zephray.me/api/media/1531784321101-gba.jpg

这样一个Game Boy烧录卡就完成了。

本文相关原理图、PCB源文件以及CPLD代码可以在GitHub上下载到:https://github.com/zephray/NekoCart-GB

注:Game Boy、精灵宝可梦、塞尔达传说是任天堂公司的注册商标

全文完,ZephRay 2018