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

2023-08-04  

一、环境与硬件介绍

开发环境: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("串口工作正常!rn");

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)

该函数传入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=%drn",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_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); 


里面调用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]];//得到颜色值

文章来源于:电子工程世界    原文链接
本站所有转载文章系出于传递更多信息之目的,且明确注明来源,不希望被转载的媒体或个人可与我们联系,我们将立即进行删除处理。