0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

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

DS小龙哥-嵌入式技术 来源:DS小龙哥-嵌入式技术 作者:DS小龙哥-嵌入式技 2022-02-28 13:35 次阅读

一、环境与硬件介绍

开发环境: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 超级玛丽运行截图

poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

三、资料下载地址

3.1 NES游戏集合下载

一共有293款游戏,总有一款适合你。常见的超级玛丽、魂斗罗、都有包含的。

地址:https://download.csdn.net/download/xiaolong1126626497/20722451

3.2 工程源码下载

地址:https://download.csdn.net/download/xiaolong1126626497/20973545

poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

一共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 工程文件布局

poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

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.h"

extern 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);
	}
}

poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

5.3 加载NES游戏:LoadNes函数介绍

LoadNes函数原型:

u8 LoadNes(unsigned char* pname,u32 size)
poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

该函数传入NES游戏数据地址,和游戏数据大小进来。

现在这个版本没有使用SD卡和文件系统,游戏的文件数据是直接加到代码里编译的。

poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

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

poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

函数里面主要完成了NES模拟器基本的初始化。

主要完成了STM32超频配置,配置锁相环为16倍,超频到128MHZ。

超频配置代码如下:

/*
函数功能:频率设置
参    数:PLL,倍频数
*/
void NesClockSet(u8 PLL)
{
	u8 temp=0;	 
	RCC->CFGR&=0XFFFFFFFC;	//修改时钟频率为内部8M	   
	RCC->CR&=~0x01000000;  	//PLLOFF  
 	RCC->CFGR&=~(0XF<<18);	//清空原来的设置
 	PLL-=2;									//抵消2个单位
	RCC->CFGR|=PLL<<18;   	//设置PLL值 2~16
	RCC->CFGR|=1<<16;	  	  //PLLSRC ON 
	FLASH->ACR|=0x12;	  	  //FLASH 2个延时周期
 	RCC->CR|=0x01000000;  	//PLLON
	while(!(RCC->CR>>25));	//等待PLL锁定
	RCC->CFGR|=0x02;		    //PLL作为系统时钟	 
	while(temp!=0x02)    	  //等待PLL作为系统时钟设置成功
	{   
		temp=RCC->CFGR>>2;
		temp&=0x03;
	}  
} 
poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

接下来初始化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;
} 
poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

5.3 NES游戏主循环代码

poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

详细代码如下:

//nes模拟器主循环
void NesEmulateFrame(void)
{  
	u8 nes_frame;
	NesSetWindow();//设置窗口
	while(1)
	{	
		// LINES 0-239
		PPU_start_frame();
		for(NES_scanline = 0; NES_scanline< 240; NES_scanline++)
		{
			run6502(113*256);
			NES_Mapper->HSync(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_scanline<262;NES_scanline++)
		{
			run6502(113*256);	  
			NES_Mapper->HSync(NES_scanline);		  
		}	   
		end_vblank(); 
		NesGetGamepadval();	//每3帧读取游戏手柄数据
		nes_frame++;
		if(nes_frame>NES_SKIP_FRAME)
		{
			nes_frame=0;//跳帧  
		}
	}
}
poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

进来就先调用了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_scanline< 240; NES_scanline++)
{
	run6502(113*256);
	NES_Mapper->HSync(NES_scanline);
	//扫描一行		  
	if(nes_frame==0)scanline_draw(NES_scanline);
	else do_scanline_and_dont_draw(NES_scanline); 
} 
poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

里面调用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++)>
poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

运行完刷屏的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;
}
poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

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  
poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

只需要在这个函数给这两个全局变量赋予正确的值,游戏人物就可以按照正常的动作画面出现。

至于你的物理按键采用FC游戏手柄,还是普通的其他按键,只要这两个全局变量的值正确那就没问题。 所有手柄采用什么不重要,关键把代码这里逻辑看懂,看懂了你就知道程序的运行逻辑了。

到此,版本1的 主要代码就分析完毕了,其他的详细过程可以看工程源码,把程序跑起来了,一切都懂了。

六、工程源码分析: 以完整版本(3)为例

这个版本加入了游戏手柄,VS1053、SD、FATFS文件系统等功能,这里接着第五章分析,下面就主要分析新加入的代码内容。

poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

6.1 FC游戏手柄介绍

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

FC 手柄的外观如图所示:

poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

这种手柄一般有 10 个按键(实际是 8 个键值):上、下、左、右、 Start、 Select、 A、 B、 A连发、 B 连发。这里的 A 和 A 连发是一个键值,而 B 和 B 连发也是一个键值,只是连发按键当你一直按下的时候,会不停的发送(方便快速按键,比如发炮弹之类的功能)。

FC 手柄的控制电路,由 1 个 8 位并入串出的移位寄存器(CD4021),外加一个时基集成电路(NE555,用于连发)构成。不过现在的手柄,为了节约成本,直接就在 PCB 上做绑定了,所以你拆开手柄,一般是看不到里面有四四方方的 IC,而只有一个黑色的小点,所有电路都集成到这个里面了,但是他们的控制和读取方法还是一样的。

游戏上手柄数据读取时序

poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

从上图可看出,读取手柄按键值的信息十分简单:先 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|=1<<5; //PD
	  RCC->APB2ENR|=1<<3; //PB
	  
	  /*2. 配置模式*/
	  GPIOD->CRL&=0xFFFF0FFF;
	  GPIOD->CRL|=0x00003000;
	  
	  GPIOB->CRH&=0xFFFF00FF;
	  GPIOB->CRH|=0x00003800;
	  
	  /*3. 上拉*/
	  GPIOD->ODR|=1<<3;
}

/*
功  能:获取手柄的按键值
返回值:保存了一帧按键的状态
键值:
[7]:右
[6]:左
[5]:下
[4]:上
[3]:Start
[2]:Select
[1]:B
[0]:A
*/
u8 GetJoypadKey(void)
{
	  u8 key=0,i;
	  JOYPAD_LAT=1; //开始锁存
	  DelayUs(30);
	  JOYPAD_LAT=0; //锁存当前的按键状态
	  for(i=0;i<8;i++)
	  {
			 key=key>>1;
		   if(JOYPAD_DATA==0)key|=0x80;
			 JOYPAD_CLK=1;  //输出一个上升沿,告诉手柄发送数据
			 DelayUs(30);
			 JOYPAD_CLK=0;  //数据线保持稳定
       DelayUs(30);			
		}
		return key;
}	
poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

6.2 加载NES游戏:nes_load函数

这里的nes_load函数和第五章的区别就是,游戏数据的来源是从SD卡读取的。

传入游戏名称去SD卡上打开指定文件,读取数据进来。

这里用到了外部SRAM内存,因为读出的数据需要存放到数组里,STM32F103ZET6本身的内存只有64K,肯定不够用,这里申请的空间是从外部SRAM模块里申请的,所以开发板还得带一个SRAM芯片才行,没有自带就去淘宝买一个SRAM模块即可(淘宝有个叫微雪的店铺就有卖)。

poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

详细代码如下:

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;
} 
poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

这里面调用了nes_sound_open函数初始化了音频设备(VS1053)。这个非常重要,要理解游戏声音是如何输出的,就认真看这里的流程。

nes_sound_open函数里初始化了VS1053音频设备,然后开启了定时器中断,使用定时器去调用VS1053的播放接口,在定时器中断服务器函数里完成声音数据的输出,这里声音是存放在一个全局缓冲区里,后面游戏在主循环里运行的时候会不断的向这个缓冲区填数据,定时器超时进中断就查询是否有音乐可以播放,有就播放,没有就出来。

poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

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++)>
poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

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++)>
poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

初始化完毕之后,就调用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<30;i++)if(wave_buffers[i]!=wave_buffers[i+1])break;//判断前30个数据,是不是都相等?
	if(i==30&&wave_buffers[i])//都相等,且不等于0
	{
		for(i=0;i;i++)wave_buffers[i]=0;>
poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

最后调用了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++)>
poYBAGDYdXCAWkKMAAAAK8RNs4s030.png

到此,音频的主要代码就分析完毕了。 可以下载程序去体验一下游戏,怀恋童年时光了

​审核编辑:符乾江

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • STM32
    +关注

    关注

    2270

    文章

    10896

    浏览量

    355752
  • 游戏机
    +关注

    关注

    9

    文章

    299

    浏览量

    33434
收藏 人收藏

    评论

    相关推荐

    HAL库在STM32开发中的重要性

    HAL库(Hardware Abstraction Layer Library,硬件抽象层库)在STM32开发中扮演着至关重要的角色。以下是HAL库在STM32开发中的重要性分析: 一
    的头像 发表于 12-02 13:35 283次阅读

    索尼PS5 Pro游戏机震撼发布

    游戏界的万众瞩目下,索尼于近日举行的PS5技术展示会上,由首席架构师Mark Cerny亲自揭晓了万众期待的PS5 Pro游戏机。这款全新力作以699美元的定价,定于11月7日盛大发售,无疑为游戏爱好者们带来了前所未有的震撼与
    的头像 发表于 09-11 16:59 697次阅读

    微软开发者成功在任天堂 NES 游戏机上运行.NET

    游戏机被誉为“红白”,搭载 1.78 MHz 的 8 位 CPU,内存仅有 2KB,游戏卡带最大容量可达 512 Kilobytes。然而,这样的设备如今运行安卓/iOS app 明显吃力,因为现今的主流应用均大于 55-2
    的头像 发表于 05-30 14:36 453次阅读

    索尼或将于近期公布PS5 Pro游戏机及《宇宙机器人》新作发布日期

    5月份,Jeff Grubb曾透露索尼计划近期发布PS5 Pro游戏主机,而近期揭示的众多信息中亦可见到这个发布会的痕迹,甚至传言《宇宙机器人》将作为新的作品亮相,进一步暗示索尼可能在此次发布会上推出PS5 Pro游戏机
    的头像 发表于 05-28 15:56 591次阅读

    YXC可编程振荡器,频点22.578MHz,工作电压3.3V,应用于游戏机

    游戏机是一种专门用于游戏运行的电子设备,它通过外界载体(如光盘、卡带等)来运行游戏。与电脑和手机等多功能设备不同,游戏机专注于游戏体验,具有
    的头像 发表于 05-23 17:43 292次阅读
    YXC可编程振荡器,频点22.578MHz,工作电压3.3V,应用于<b class='flag-5'>游戏机</b>

    任天堂Switch初代游戏机模拟运行Windows 11 ARM效果展示

    5 月 14 日,某开发者PatRyk在X平台发布帖子,展示了将任天堂Switch初代游戏机在Linux环境中通过QEMU模拟Windows 11 ARM系统的效果。
    的头像 发表于 05-14 10:16 837次阅读

    Funkey游戏机新作,基于全志T113的全新版本

    目名称: T113-S3-FunKeys过往项目:V3s掌项目组说: 双核FunKey掌的发布不仅是我们团队的努力成果,更是开源社区的共同成就。我们鼓励更多的开发者加入到我们的项目中来,共同探索
    发表于 05-11 11:04

    OrangePi Neo:好玩不贵,最具性价比的游戏来了!

    3月24日,香橙派在深圳发布了备受期待的OrangePiNeo游戏。据称,这款游戏售价4099元起,将为用户带来强大的性能和丰富的游戏
    的头像 发表于 04-11 17:28 614次阅读
    OrangePi Neo:好玩不贵,最具性价比的<b class='flag-5'>游戏</b>掌<b class='flag-5'>机</b>来了!

    索尼PSV及PSC游戏机售后服务终止

    据悉,PlayStation Vita这一款由索尼计算机娱乐有限公司创新研发的掌上电脑,通称为PSV,其名字VITA源于拉丁文中代表生命的意思。
    的头像 发表于 03-28 10:43 763次阅读

    任天堂计划2025年3月发布Switch 2游戏机

    根据推测,这款新游戏机或拥有类似原有型号Switch的手持特性,且配备尺寸较大的屏幕。尽管大部分业内人士预期新机即刻上市,但任天堂决定先进行销售规划及为开发者预留足够的时间来制作受欢迎的游戏,从而达到简化销售
    的头像 发表于 02-27 16:45 3020次阅读

    Stages—研发过程可视化建模和管理平台

    Stages是美国UL Solutions旗下UL Method Park GmbH的产品,用于帮助企业定义、管理、发布、控制、优化其研发过程,同时使其研发过程符合CMMI、ASPICE
    的头像 发表于 02-05 14:36 391次阅读
    Stages—研<b class='flag-5'>发过程</b>可视化建模和管理平台

    氮化镓芯片研发过程

    氮化镓芯片(GaN芯片)是一种新型的半导体材料,在目前的电子设备中逐渐得到应用。它以其优异的性能和特点备受研究人员的关注和追捧。在现代科技的进步中,氮化镓芯片的研发过程至关重要。下面将详细介绍氮化镓
    的头像 发表于 01-10 10:11 1041次阅读

    家用游戏机的控制器接口类型

    家用游戏机的控制器接口类型是游戏机和手柄之间进行连接和通信的关键部分。它决定了用户如何与游戏机进行交互,并直接影响到游戏体验的质量和多样性。本文将
    的头像 发表于 01-04 11:23 1430次阅读

    应用在游戏机触摸屏中的触摸感应芯片

    触屏游戏机通常由屏幕、主板、处理器、内存、电源、按键、触控器器等组成。其中,触控器器是实现屏幕触控功能的重要组成部分。
    的头像 发表于 01-03 09:32 874次阅读
    应用在<b class='flag-5'>游戏机</b>触摸屏中的触摸感应芯片

    ASIC芯片开发过程

    电子发烧友网站提供《ASIC芯片开发过程.ppt》资料免费下载
    发表于 12-25 10:04 1次下载