基于STM32设计的掌上游戏机详细开发过程

一、环境与硬件介绍 开发环境:keil5
代码风格: 寄存器风格,没有采用库函数,底层代码全部寄存器方式编写,运行效率高,注释清楚。
mcu型号: stm32f103zet6
开发板: 正常的一块stm32开发板,带lcd插槽,带4颗独立按键。
游戏模拟器:  nes游戏模拟器
lcd :  alientek的3.5寸屏幕。(屏幕型号不重要,随便一款都可以的,把屏幕底层驱动代码写好,适配即可)
声音输出设备 : 采用vs1053 (spi接口,操作方便)
游戏手柄: 支持fc游戏手柄
完成这个掌上游戏机需要使用的硬件设备不复杂,如果想要体验游戏,需要的必备硬件:
1. (必要)stm32f103系列最小系统版一个
2. (必要)lcd屏一块。 2.8寸就可以了,价格便宜。
3. (非必要)fc游戏手柄一个,驱动时序很简单(后面有单独章节介绍),支持组合键,玩游戏体验感非常好。
    如果不用fc游戏手柄,使用开发板几个独立按键也行,只是手感不好。
4.  (非必要)vs1053或者其他系列声卡模块一个,游戏是有声音的,要完美的体验游戏声卡肯定是要的,不要也可以玩,只是没有声音而已。vs1053模块支持spi接口控制,时序简单,驱动代码也不复杂,资料比较多,学起来,理解起来很容易。
5. (非必要)sd卡一张。主要存储nes游戏文件,可以动态加载想要玩的游戏,切换比较方便。
如果没有sd卡,也想体验也可以,直接把游戏取模成二进制放在数组里存放到stm32的flash里即可,stm32f103zet6有512k的flash,存放一个游戏完全够用,加载速度更加快。
6. (非必要) sram外部扩展内存,如果不需要从sd里加载游戏,就不需要外部内存;如果使用sd卡加载游戏,就需要把游戏数据从sd卡里读取出来,然后放在sram外部扩展内存芯片里。因为stm32f103zet6本身只有64k内存,放不下。
游戏体验:stm32可以超频到128m,运行起来还是非常流畅,玩起来的感觉和正常的fc游戏机是一样的,没有卡顿,延迟。
游戏模拟器移植的是nes模拟器,开发过程中,代码编写了3个版本:
版本1: 精简版的掌上游戏机,最适合学习,代码牵扯很少,只有外设硬件只用到了lcd而已,最适合学习,理解代码运行原理;不支持声音输出,不支持fc游戏手柄,不支持sd卡和文件系统(也就是不支持从sd卡上选择游戏加载)。 这个版本的游戏是直接使用数组存放在代码里的,游戏的操作是通过开发板上的4个按键控制(开发板的4个按键,分别控制角色的前进、后退、暂停、跳跃),因为只有4个按键,没有支持组合按键,所以体验起来不是很舒服,控制比较困难,完美体验还是要继续加上fc游戏手柄。
版本2: 这也是精简版的掌上游戏机,在版本1的基础之上加了vs1053模块,支持声音输出,体验感要好一点,能听到游戏声音。
版本3: 这是完整版本的掌上游戏机,加入了fc游戏手柄支持,加入了vs1053声卡驱动,加入了sd卡和fatfs文件系统,可以正常从sd卡加载指定的游戏运行,体验非常好。
3个版本的源代码和nes的游戏集合,在下面的第3章有下载地址。
二、游戏运行效果(超级玛丽示例) 2.1  超级玛丽运行截图 ​
三、资料下载地址 3.1 nes游戏集合下载 
一共有293款游戏,总有一款适合你。常见的超级玛丽、魂斗罗、都有包含的。
地址:https://download.csdn.net/download/xiaolong1126626497/20722451
3.2  工程源码下载 地址: https://download.csdn.net/download/xiaolong1126626497/20973545

 一共3个版本,它们之间的区别在第一章已经介绍过。
三个都是keil工程,下载下来直接编译、下载运行体验。
四、什么是nes ?  nes就是红白机的游戏,所谓的nes意思是欧美版的红白机,fc的美版,nintendo entertainment system(任天堂娱乐系统),而日本的红白机则叫family computer(fc)。
发展历史-来至百度百科
1983年7月15日,由日本任天堂株式会社(原本是生产日式扑克即“花札”)的宫本茂先生领导开发的一种第三代家用电子游戏机:fc,全称:family computer,也称作:famicom;在日本以外的地区发售时则被称为nes,全称:nintendo entertainment system;在中国大陆、台湾和香港等地,因其外壳为红白两色,所以人们俗称其为“红白机”,正式进入市场销售,并于后来取得了巨大成功,由此揭开了家用电子游戏机遍布世界任何角落,电子游戏全球大普及的序幕。
1985年,nes在北美地区的销量3300万台,比日本地区高出近一倍, 也占据了其全球市场份额的一半。  nes在北美首发时的捆绑游戏《打鸭子》(duck hunt)总共取得近3000万套(基本全部来自北美市场)销量, [6]  这在红白机游戏中名列第二,仅次于《超级马力欧》。 
1986年,任天堂在美国收3.1亿美元,这一年美国游戏产业的规模4.3亿美元,而在一年前,深陷雅达利冲击的美国游戏业的收入仅1亿美元。 [7]  1988年发售的《超级马力欧兄弟3》(super mario bros. 3)在美国售出700万套,在日本销量达400万,销售额5.5亿美元。
1989年,任天堂的游戏机已占领美国90%和日本95%的市场,任天堂成为游戏界巨无霸。
2003年7月,fc发售二十周年,任天堂宣布fc游戏机正式停产。至此,fc全世界已累计销售6000万部以上。至今中国大陆、台湾、香港与泰国甚至日本等地仍然在制造fc规格的兼容品。
任天堂成为了现代游戏产业的开创者,在很多方面上确立了现代电子游戏的标准。
fc巨大成功使任天堂年纯利从1985年开始一直保持5亿美元以上 ,其股票成为东京证券交易所绩优股代名词,一度超越了3万日元,市值超松下等企业,很多人都把任天堂成功誉为新时代商业神话。 
任天堂红白机(fc/nes)发行于1983年,在日本发行之后引起了不小的轰动,两年之后进军北美市场,更加奠定了任天堂的家用游戏机霸主地位。当人们正需要一个高品质的家用游戏机的时候,任天堂拿出了他们的全部家当,首发的数款游戏都赢得了玩家的赞誉,超级马力欧更成为了永远的经典。在那个年代,拥有一台红白机应该是孩子们最大的梦想了。 根据外媒的数据,在1990年30%的美国家庭都拥有nes主机。
五、工程源码分析: 以精简版本(1)为例 工程源码全部采用寄存器代码风格,基本上每行都有详细的注释;虽然stm32支持库函数方式开发,效率更加快,但是寄存器方式可以更方便了解cpu底层寄存器的一些配置,对以后在学习使用其他类型的微处理器是非常有帮助的。
5.1 工程文件布局

 5.2 主函数代码 主函数里完成lcd屏幕初始化,按键初始化,led灯初始化,串口初始化,fc游戏手柄初始化,默认把lcd屏幕清屏为黑色。
lcd屏采用fsmc驱动的,把fsmc时序速度配置到最快,达到stm32能支持的最快速度,提高lcd刷屏速度。
初始化完毕最后,调用了loadnes函数,完成游戏加载;如果加载失败,就回到下面执行while循环,闪烁led灯。
代码如下:
#include stm32f10x.h#include led.h#include lcd.h#include delay.h#include key.h#include usart.h#include #include #include joypad.hextern u8 loadnes(u8* pname,u32);//游戏文件可以通过winhex文件生成c源码数组extern const unsigned char nes_data1[40976];//超级玛丽游戏的文件extern const unsigned char nes_data2[262160];//魂斗罗游戏的文件/*移植说明:1. 加入游戏手柄2. 优化了游戏刷新的帧率3. 加入开发板本身自带按键控制*/int main(){ beepinit(); //蜂鸣器初始化 ledinit(); //led灯初始化 usartinit(usart1,72,115200); keyinit(); //按键初始化 printf(串口工作正常!\r\n); lcdinit(); //lcd初始化 //joypadinit(); //游戏手柄初始化 lcdclear(0xffff); /*0000 0000:保留0000 0001: datast保持时间=2个hclk时钟周期0000 0010: datast保持时间=3个hclk时钟周期……1111 1111: datast保持时间=256个hclk时钟周期(这是复位后的默认数值)0、1、2、3、4、5、6、7、8、9、10、11、12、13、14*/ lcdclear(0); //开始运行游戏 loadnes((unsigned char*)nes_data1,40976); //超级玛丽 //loadnes((unsigned char*)nes_data2,262160); //魂斗罗 while(1) { led1=!led1; delayms(400); }}  5.3 加载nes游戏:loadnes函数介绍 loadnes函数原型:
u8 loadnes(unsigned char* pname,u32 size) 该函数传入nes游戏数据地址,和游戏数据大小进来。
现在这个版本没有使用sd卡和文件系统,游戏的文件数据是直接加到代码里编译的。

 这两个数组是超级玛丽和魂斗罗的数据。(直接使用打开文件,使用winhex软件打开,全选,右键编辑,选择复制,选择c源码,复制成数组形式粘贴到keil里即可)

 函数里面主要完成了nes模拟器基本的初始化。
主要完成了stm32超频配置,配置锁相环为16倍,超频到128mhz。
超频配置代码如下:
/*函数功能:频率设置参 数:pll,倍频数*/void nesclockset(u8 pll){ u8 temp=0; rcc->cfgr&=0xfffffffc; //修改时钟频率为内部8m rcc->cr&=~0x01000000; //plloff rcc->cfgr&=~(0xfcr>>25)); //等待pll锁定 rcc->cfgr|=0x02; //pll作为系统时钟 while(temp!=0x02) //等待pll作为系统时钟设置成功 { temp=rcc->cfgr>>2; temp&=0x03; } } 接下来初始化nes游戏模拟器的必要参数,最后调用nesemulateframe函数进入nes游戏主循环代码,开始运行游戏。
loadnes函数完整代码如下:
/*函数功能:开始nes游戏参 数:pname:nes游戏路径 u32 size 游戏大小返 回 值: 0,正常退出 1,内存错误 2,文件错误 3,不支持的map*/u8 loadnes(unsigned char* pname,u32 size){ u8 res=0; res=nessrammalloc(); //申请内存 romfile=(u8*)pname; //游戏源码地址 nesrom_crc32=get_crc32(romfile+16,size-16);//获取crc32的值 res=loadnesrom(); //加载rom printf(res=%d\r\n,res); nesclockset(16); //设置系统时钟为128mhz 16*8 joypadinit(); //游戏手柄初始化 cpu6502_init(); //初始化6502,并复位 mapper_init(); //map初始化 ppu_reset(); //ppu复位 apu_init(); //apu初始化 nesemulateframe(); //进入nes模拟器主循环 return res;}  
5.3 nes游戏主循环代码 ​
详细代码如下:
//nes模拟器主循环void nesemulateframe(void){ u8 nes_frame; nessetwindow();//设置窗口 while(1) { // lines 0-239 ppu_start_frame(); for(nes_scanline = 0; nes_scanlinehsync(nes_scanline); //扫描一行 if(nes_frame==0)scanline_draw(nes_scanline); else do_scanline_and_dont_draw(nes_scanline); } nes_scanline=240; run6502(113*256);//运行1线 nes_mapper->hsync(nes_scanline); start_vblank(); if(nmi_enabled()) { cpunmi=1; run6502(7*256);//运行中断 } nes_mapper->vsync(); // lines 242-261 for(nes_scanline=241;nes_scanlinehsync(nes_scanline); } end_vblank(); nesgetgamepadval(); //每3帧读取游戏手柄数据 nes_frame++; if(nes_frame>nes_skip_frame) { nes_frame=0;//跳帧 } }}  
 进来就先调用了nessetwindow(void)函数,设置窗口大小,这里面就调用了lcd的接口,如果是其他的lcd屏,使用本代码只需要把这里适配一下即可。
u8 nes_xoff=0; //显示在x轴方向的偏移量(实际显示宽度=256-2*nes_xoff)//设置游戏显示窗口void nessetwindow(void){ u16 lcdwidth,lcdheight; lcdwidth=256; lcdheight=240; nes_xoff=0; lcdsetwindow(32,0,lcdwidth,lcdheight); lcdwriteram_prepare();//写入lcd ram的准备 } 接下来就进入到nes游戏的主循环代码,开始循环一帧一帧的刷出图像数据,达到游戏的效果。
设置窗口大小之后,下面就是从nes游戏数据文件里取出颜色数据,然后for循环一行一行刷屏即可。
上面的设置窗口大小的代码其实并不是必要的,只是当前使用的lcd支持坐标自增(一般lcd都支持的),设置lcd的窗口范围之后,连续给lcd写数据,lcd的坐标会自动自增,提高刷屏效率而已。如果你的lcd屏并不支持坐标自增或者你不会写代码,也想移植,那完全不用设置窗口那个函数,你只需要提供一个画点函数,把for循环里的刷屏代码里行扫描改掉就行。
函数里的这个for循环就是主要刷出图像的代码,如果想要移植到其他lcd屏,主要就改这里,示例代码如下:
for(nes_scanline = 0; nes_scanlinehsync(nes_scanline); //扫描一行 if(nes_frame==0)scanline_draw(nes_scanline); else do_scanline_and_dont_draw(nes_scanline); } 里面调用scanline_draw函数是按行扫描(也就是一行一行绘制图像),scanline_draw函数里面也是一个for循环,细化到每个像素点,按照每个像素点绘制到屏幕上,代码里的lcd_ram就是当前lcd屏的地址,因为当前lcd屏采用的是fsmc,这个lcd_ram就是fsmc地址,向这个地址写数据,fsmc就产生8080时序将数据送给lcd显示屏,刷新显示出来。
scanline_draw函数详细刷屏代码如下:
extern u8 nes_xoff; //显示在x轴方向的偏移量(实际显示宽度=256-2*nes_xoff)void scanline_draw(int lineno){ uint16 i; u16 sx,ex; do_scanline_and_draw(ppu->dummy_buffer); sx=nes_xoff+8; ex=256+8-nes_xoff; if(lcddev.width==480) { for(i=sx;idummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 } for(i=sx;idummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 i++; lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 lcd_ram=nes_palette[ppu->dummy_buffer[i]];//得到颜色值 } }else { for(i=sx;idummy_buffer[i++]]; lcd_ram=nes_palette[ppu->dummy_buffer[i++]]; lcd_ram=nes_palette[ppu->dummy_buffer[i++]]; lcd_ram=nes_palette[ppu->dummy_buffer[i++]]; lcd_ram=nes_palette[ppu->dummy_buffer[i++]]; lcd_ram=nes_palette[ppu->dummy_buffer[i++]]; lcd_ram=nes_palette[ppu->dummy_buffer[i++]]; lcd_ram=nes_palette[ppu->dummy_buffer[i++]]; lcd_ram=nes_palette[ppu->dummy_buffer[i++]]; lcd_ram=nes_palette[ppu->dummy_buffer[i++]]; lcd_ram=nes_palette[ppu->dummy_buffer[i++]]; lcd_ram=nes_palette[ppu->dummy_buffer[i++]]; lcd_ram=nes_palette[ppu->dummy_buffer[i++]]; lcd_ram=nes_palette[ppu->dummy_buffer[i++]]; lcd_ram=nes_palette[ppu->dummy_buffer[i++]]; lcd_ram=nes_palette[ppu->dummy_buffer[i]]; } }};i++)>;i++)>;i++)> 运行完刷屏的for循环函数,一帧游戏图像就显示在lcd上了。
接下来就是扫描按键值,完成游戏人物的控制,函数里调用了nesgetgamepadval()函数,读取按键值刷新按键状态。
nesgetgamepadval()函数代码如下:
/*键值说明: 开始键:8选择建:4方向右:128方向左:64方向上:16方向下:32功能键上/左:2功能键下/右:1组合键:方向右与读取游戏手柄数据和功能键左 :130*/void nesgetgamepadval(void){ u8 key;// paddata0=getjoypadkey(); //读取手柄1的值 //printf(%d\r\n,paddata0); key=getkeyvalue(0); if(key==1)paddata0=8; else if(key==2)paddata0=128; else if(key==3)paddata0=64; else if(key==4)paddata0=1; else paddata0=0;} nes游戏模拟器定义了两个全局变量,分别记录游戏手柄1和游戏手柄2的数据,因为nes游戏是可以两个人一起玩的。
u8 paddata0; //手柄1键值 [7:0]右7 左6 下5 上4 start3 select2 b1 a0 u8 paddata1; //手柄2键值 [7:0]右7 左6 下5 上4 start3 select2 b1 a0 只需要在这个函数给这两个全局变量赋予正确的值,游戏人物就可以按照正常的动作画面出现。
至于你的物理按键采用fc游戏手柄,还是普通的其他按键,只要这两个全局变量的值正确那就没问题。  所有手柄采用什么不重要,关键把代码这里逻辑看懂,看懂了你就知道程序的运行逻辑了。
到此,版本1的 主要代码就分析完毕了,其他的详细过程可以看工程源码,把程序跑起来了,一切都懂了。
六、工程源码分析: 以完整版本(3)为例 这个版本加入了游戏手柄,vs1053、sd、fatfs文件系统等功能,这里接着第五章分析,下面就主要分析新加入的代码内容。

6.1 fc游戏手柄介绍  fc游戏手柄,大致可分为两种:一种手柄插口是 11 针的,一种是 9 针的。但 11 针的现在市面上很少了,现在几乎都是使用 9 针 fc 组装手柄,下面就是介绍的是 9 针 fc 手柄,该手柄还有一个特点,就是可以直接和dr9 的串口头对插!这样同开发板的连接就简单了。
fc 手柄的外观如图所示:

这种手柄一般有 10 个按键(实际是 8 个键值):上、下、左、右、 start、 select、 a、 b、 a连发、 b 连发。这里的 a 和 a 连发是一个键值,而 b 和 b 连发也是一个键值,只是连发按键当你一直按下的时候,会不停的发送(方便快速按键,比如发炮弹之类的功能)。
fc 手柄的控制电路,由 1 个 8 位并入串出的移位寄存器(cd4021),外加一个时基集成电路(ne555,用于连发)构成。不过现在的手柄,为了节约成本,直接就在 pcb 上做绑定了,所以你拆开手柄,一般是看不到里面有四四方方的 ic,而只有一个黑色的小点,所有电路都集成到这个里面了,但是他们的控制和读取方法还是一样的。
游戏上手柄数据读取时序

从上图可看出,读取手柄按键值的信息十分简单:先 latch(锁存键值),然后就得到了第一个按键值(a),之后在 clock 的作用下,依次读取其他按键的键值,总共 8 个按键键值。
常规状态下,latch为低电平,clk为高电平,data为高电平,这也是初始化端口时的状态。 
单片机读取键值时序很简单,latch先发送一个高脉冲,数据将锁存到手柄内部的移位寄存器,然后在clk时钟下降沿数据将从data低位在先连续发出。按键映射到数据的对应位上,有键按下则对应位为0,无键按下则为1.即不按任何键时,读取数据为0xff。
键值:
[7]:右
[6]:左
[5]:下
[4]:上
[3]:start
[2]:select
[1]:b
[0]:a
驱动代码示例:
功 能:手柄初始化函数硬件连接: clk :pd3 --时钟线 pb10:data --数据线 pb11:lat --锁存接口*/void joypadinit(void){ /*1. 开时钟*/ rcc->apb2enr|=1crh&=0xffff00ff; gpiob->crh|=0x00003800; /*3. 上拉*/ gpiod->odr|=1<1; if(joypad_data==0)key|=0x80; joypad_clk=1; //输出一个上升沿,告诉手柄发送数据 delayus(30); joypad_clk=0; //数据线保持稳定 delayus(30); } return key;} 6.2 加载nes游戏:nes_load函数  这里的nes_load函数和第五章的区别就是,游戏数据的来源是从sd卡读取的。
 传入游戏名称去sd卡上打开指定文件,读取数据进来。
这里用到了外部sram内存,因为读出的数据需要存放到数组里,stm32f103zet6本身的内存只有64k,肯定不够用,这里申请的空间是从外部sram模块里申请的,所以开发板还得带一个sram芯片才行,没有自带就去淘宝买一个sram模块即可(淘宝有个叫微雪的店铺就有卖)。

详细代码如下:
u8 nes_load(u8* pname){ fil *file; uint br; u8 res=0; file=malloc(sizeof(fil)); if(file==0)return 1; //内存申请失败. res=f_open(file,(char*)pname,fa_read); if(res!=fr_ok) //打开文件失败 { printf(%s 文件打开失败!\r\n,pname); free(file); return 2; } else { printf(%s 文件打开成功!\r\n,pname); } res=nes_sram_malloc(file->fsize); //申请内存 if(res==0) { f_read(file,romfile,file->fsize,&br); //读取nes文件 nesrom_crc32=get_crc32(romfile+16, file->fsize-16);//获取crc32的值 res=nes_load_rom(); //加载rom if(res==0) { nesclockset(16); //usartinit(usart1,128,115200); joypadinit(); cpu6502_init(); //初始化6502,并复位 mapper_init(); //map初始化 ppu_reset(); //ppu复位 apu_init(); //apu初始化 nes_sound_open(0,apu_sample_rate); //初始化播放设备 nes_emulate_frame(); //进入nes模拟器主循环 nes_sound_close(); //关闭声音输出 } } f_close(file); free(file);//释放内存 nes_sram_free(); //释放内存 return res;} 这里面调用了nes_sound_open函数初始化了音频设备(vs1053)。这个非常重要,要理解游戏声音是如何输出的,就认真看这里的流程。
nes_sound_open函数里初始化了vs1053音频设备,然后开启了定时器中断,使用定时器去调用vs1053的播放接口,在定时器中断服务器函数里完成声音数据的输出,这里声音是存放在一个全局缓冲区里,后面游戏在主循环里运行的时候会不断的向这个缓冲区填数据,定时器超时进中断就查询是否有音乐可以播放,有就播放,没有就出来。 

vs1052声音播放代码示例:
//音频播放回调函数void nes_vs10xx_feeddata(void){ u8 n; u8 nbytes; u8 *p; if(nesplaybuf==nessavebuf)return;//还没有收到新的音频数据 if(vs1053_dreq!=0)//可以发送数据给vs10xx { p=nesapusbuf[nesplaybuf]+nesbufpos; nesbufpos+=32; if(nesbufpos>apu_pcmbuf_size) { nesplaybuf++; if(nesplaybuf>(nes_apu_buf_num-1))nesplaybuf=0; nbytes=apu_pcmbuf_size+32-nesbufpos; nesbufpos=0; }else nbytes=32; for(n=0;n;n++)>  
nes_sound_open函数代码如下:
//nes打开音频输出int nes_sound_open(int samples_per_sync,int sample_rate) { u8 *p; u8 i; p=malloc(100); //申请100字节内存 if(p==null)return 1; //内存申请失败,直接退出 printf(sound open:%d\r\n,sample_rate); for(i=0;i>8)&0xff; p[28]=sample_rate&0xff; //设置字节速率(8位模式,等于采样率) p[29]=(sample_rate>>8)&0xff; nesplaybuf=0; nessavebuf=0; vs1053_reset(); //硬复位 vs1053_softreset(); //软复位 vs1053_setvol(200); //设置音量等参数 //复位解码时间 vs1053_writecmd(spi_decode_time,0x0000); vs1053_writecmd(spi_decode_time,0x0000); //操作两次 while(vs1053_sendmusicdata(p)); //发送wav head while(vs1053_sendmusicdata(p+32)); //发送wav head timerinit(tim6,72,1000); //1ms中断一次 free(p); //释放内存 return 1;}(nes_wav_head);i++)>  
 初始化完毕之后,就调用nes_emulate_frame函数进入到游戏主循环。
6.3 游戏主循环代码 现在这份代码比第五章代码增加了一个声音输出函数,调用vs1053,播放游戏的声音。

 apu_soundoutput函数代码如下:
//apu声音输出void apu_soundoutput(void) { u16 i; apu_process(wave_buffers,apu_pcmbuf_size); for(i=0;i 最后调用了nes_apu_fill_buffer 函数将数据赋值给vs1053缓冲区进行播放。
在前面已经分析了音频初始化代码,里面初始化了定时器,会不断的查询缓冲区是否有音乐数据需要播放,有就播放,没有就输出,这个函数就是向音频缓冲区填充数据的。
nes_apu_fill_buffer 函数代码如下:
//nes音频输出到vs1053缓存void nes_apu_fill_buffer(int samples,u8* wavebuf){ u16 i; u8 tbuf; for(i=0;i(nes_apu_buf_num-1))tbuf=0; while(tbuf==nesplaybuf)//输出数据赶上音频播放的位置了,等待. { delayms(5); } nessavebuf=tbuf; } ;i++)> 到此,音频的主要代码就分析完毕了。 可以下载程序去体验一下游戏,怀恋童年时光了


微模块数据中心的优势
NVIDIA DRIVETMPX自动驾驶汽车的端到端学习
iPhone8将把4.7英寸的小屏改成5.5英寸,惊不惊喜?
Fitbit放弃索取侵权赔偿,因Jawbone濒临破产
AOPU手机开山之作AOPU 6656将正式亮相
基于STM32设计的掌上游戏机详细开发过程
光敏电阻的工作原理
医疗平板设计需要因地制宜,满足用户的特定需求
2019年手机摄像头最流行哪些要素
Github2018年最热语言\软件包\项目top 10盘点
工程振弦采集仪监测技术的发展现状与展望
RT-Thread专业版实现对于AMD 赛灵思自适应平台的全面支持
什么叫微电网孤岛运行 描述孤岛效应机理及危害
连接器采购必看 60000+现货型号+9.5折让利!(附热销型号表)
如何测试最大输出电流?纳米软件带你了解
硅谷风险投资人看好VR 成为主流技术势在必得
多地“拉闸限电”,隐藏着三大原因
盘点联建光电等企业在Mini LED领域的成果
瑞萨电子强化全球采购体系,重点强化大陆业务
STM32F429的USB工程CAN通讯不正常经验分享