日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

基于STM32设计的掌上游戏机(运行NES游戏模拟器)详细开发过程

發布時間:2023/12/14 编程问答 45 豆豆
生活随笔 收集整理的這篇文章主要介紹了 基于STM32设计的掌上游戏机(运行NES游戏模拟器)详细开发过程 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一、環境與硬件介紹

開發環境: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? 超級瑪麗運行截圖

2.2? 僅僅使用獨立按鍵操作游戲效果

單手錄制,單手操作,操作起來起來不太方便。

STM32上移植NES游戲框架-運行超級瑪麗游戲

2.3?游戲自動待機運行效果(沒有操作)

基于STM32移植NES游戲框架-超級瑪麗游戲(動畫)

三、資料下載地址

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 <string.h> #include <stdio.h> #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);} }

?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&=~(0XF<<18); //清空原來的設置PLL-=2; //抵消2個單位RCC->CFGR|=PLL<<18; //設置PLL值 2~16RCC->CFGR|=1<<16; //PLLSRC ON FLASH->ACR|=0x12; //FLASH 2個延時周期RCC->CR|=0x01000000; //PLLONwhile(!(RCC->CR>>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(); //加載ROMprintf("res=%d\r\n",res); NesClockSet(16); //設置系統時鐘為128MHZ 16*8JoypadInit(); //游戲手柄初始化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-239PPU_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;//跳幀 }} }

?進來就先調用了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;i<ex;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]];//得到顏色值i++;LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到顏色值LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到顏色值 } for(i=sx;i<ex;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]];//得到顏色值i++;LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到顏色值LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到顏色值} }else{for(i=sx;i<ex;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++]]; LCD_RAM=NES_Palette[ppu->dummy_buffer[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|=1<<5; //PDRCC->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; }

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(); //加載ROMif(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<nbytes;n++){if(p[n]!=0)break; //判斷是不是剩余所有的數據都為0? }if(n==nbytes)return; //都是0,則直接不寫入VS1053了,以免引起噠噠聲. VS1053_XDCS=0; for(n=0;n<nbytes;n++){VS1053_SPI_ReadWriteByte(p[n]); }VS1053_XDCS=1; } }

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<sizeof(nes_wav_head);i++)//復制nes_wav_head內容{p[i]=nes_wav_head[i];}if(lcddev.width==480) //是480*480屏幕{sample_rate=8000; //設置8Khz,約原來速度的0.75倍}p[24]=sample_rate&0XFF; //設置采樣率p[25]=(sample_rate>>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 headwhile(VS1053_SendMusicData(p+32)); //發送wav headTimerInit(TIM6,72,1000); //1ms中斷一次free(p); //釋放內存return 1; }

?初始化完畢之后,就調用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<APU_PCMBUF_SIZE;i++)wave_buffers[i]=0;//是暫停狀態輸出的重復無效數據,直接修改為0.從而不輸出雜音. }clocks=0;nes_apu_fill_buffer(0,wave_buffers); }

最后調用了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<APU_PCMBUF_SIZE;i++){nesapusbuf[nessavebuf][i]=wavebuf[i]; }tbuf=nessavebuf;tbuf++;if(tbuf>(NES_APU_BUF_NUM-1))tbuf=0;while(tbuf==nesplaybuf)//輸出數據趕上音頻播放的位置了,等待.{ DelayMs(5);}nessavebuf=tbuf; }

到此,音頻的主要代碼就分析完畢了。 可以下載程序去體驗一下游戲,懷戀童年時光了

總結

以上是生活随笔為你收集整理的基于STM32设计的掌上游戏机(运行NES游戏模拟器)详细开发过程的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。