?
一、環(huán)境與硬件介紹
開(kāi)發(fā)環(huán)境:keil5
代碼風(fēng)格: 寄存器風(fēng)格,沒(méi)有采用庫(kù)函數(shù),底層代碼全部寄存器方式編寫(xiě),運(yùn)行效率高,注釋清楚。
開(kāi)發(fā)板: 正常的一塊STM32開(kāi)發(fā)板,帶LCD插槽,帶4顆獨(dú)立按鍵。
游戲模擬器: NES游戲模擬器
LCD : ALIENTEK的3.5寸屏幕。(屏幕型號(hào)不重要,隨便一款都可以的,把屏幕底層驅(qū)動(dòng)代碼寫(xiě)好,適配即可)
聲音輸出設(shè)備 : 采用VS1053 (SPI接口,操作方便)
游戲手柄: 支持FC游戲手柄
完成這個(gè)掌上游戲機(jī)需要使用的硬件設(shè)備不復(fù)雜,如果想要體驗(yàn)游戲,需要的必備硬件:
1. (必要)STM32F103系列最小系統(tǒng)版一個(gè)
2. (必要)LCD屏一塊。 2.8寸就可以了,價(jià)格便宜。
3. (非必要)FC游戲手柄一個(gè),驅(qū)動(dòng)時(shí)序很簡(jiǎn)單(后面有單獨(dú)章節(jié)介紹),支持組合鍵,玩游戲體驗(yàn)感非常好。
如果不用FC游戲手柄,使用開(kāi)發(fā)板幾個(gè)獨(dú)立按鍵也行,只是手感不好。
4. (非必要)VS1053或者其他系列聲卡模塊一個(gè),游戲是有聲音的,要完美的體驗(yàn)游戲聲卡肯定是要的,不要也可以玩,只是沒(méi)有聲音而已。VS1053模塊支持SPI接口控制,時(shí)序簡(jiǎn)單,驅(qū)動(dòng)代碼也不復(fù)雜,資料比較多,學(xué)起來(lái),理解起來(lái)很容易。
5. (非必要)SD卡一張。主要存儲(chǔ)NES游戲文件,可以動(dòng)態(tài)加載想要玩的游戲,切換比較方便。
如果沒(méi)有SD卡,也想體驗(yàn)也可以,直接把游戲取模成二進(jìn)制放在數(shù)組里存放到STM32的FLASH里即可,STM32F103ZET6有512K的FLASH,存放一個(gè)游戲完全夠用,加載速度更加快。
6. (非必要) SRAM外部擴(kuò)展內(nèi)存,如果不需要從SD里加載游戲,就不需要外部?jī)?nèi)存;如果使用SD卡加載游戲,就需要把游戲數(shù)據(jù)從SD卡里讀取出來(lái),然后放在SRAM外部擴(kuò)展內(nèi)存芯片里。因?yàn)镾TM32F103ZET6本身只有64K內(nèi)存,放不下。
游戲體驗(yàn):STM32可以超頻到128M,運(yùn)行起來(lái)還是非常流暢,玩起來(lái)的感覺(jué)和正常的FC游戲機(jī)是一樣的,沒(méi)有卡頓,延遲。
游戲模擬器移植的是NES模擬器,開(kāi)發(fā)過(guò)程中,代碼編寫(xiě)了3個(gè)版本:
版本1:精簡(jiǎn)版的掌上游戲機(jī),最適合學(xué)習(xí),代碼牽扯很少,只有外設(shè)硬件只用到了LCD而已,最適合學(xué)習(xí),理解代碼運(yùn)行原理;不支持聲音輸出,不支持FC游戲手柄,不支持SD卡和文件系統(tǒng)(也就是不支持從SD卡上選擇游戲加載)。 這個(gè)版本的游戲是直接使用數(shù)組存放在代碼里的,游戲的操作是通過(guò)開(kāi)發(fā)板上的4個(gè)按鍵控制(開(kāi)發(fā)板的4個(gè)按鍵,分別控制角色的前進(jìn)、后退、暫停、跳躍),因?yàn)橹挥?個(gè)按鍵,沒(méi)有支持組合按鍵,所以體驗(yàn)起來(lái)不是很舒服,控制比較困難,完美體驗(yàn)還是要繼續(xù)加上FC游戲手柄。
版本2:這也是精簡(jiǎn)版的掌上游戲機(jī),在版本1的基礎(chǔ)之上加了VS1053模塊,支持聲音輸出,體驗(yàn)感要好一點(diǎn),能聽(tīng)到游戲聲音。
版本3:這是完整版本的掌上游戲機(jī),加入了FC游戲手柄支持,加入了VS1053聲卡驅(qū)動(dòng),加入了SD卡和FATFS文件系統(tǒng),可以正常從SD卡加載指定的游戲運(yùn)行,體驗(yàn)非常好。
3個(gè)版本的源代碼和NES的游戲集合,在下面的第3章有下載地址。
二、游戲運(yùn)行效果(超級(jí)瑪麗示例)
2.1 超級(jí)瑪麗運(yùn)行截圖
?
三、資料下載地址
3.1 NES游戲集合下載
一共有293款游戲,總有一款適合你。常見(jiàn)的超級(jí)瑪麗、魂斗羅、都有包含的。
地址:https://download.csdn.net/download/xiaolong1126626497/20722451
3.2 工程源碼下載
地址:https://download.csdn.net/download/xiaolong1126626497/20973545
?
一共3個(gè)版本,它們之間的區(qū)別在第一章已經(jīng)介紹過(guò)。
三個(gè)都是keil工程,下載下來(lái)直接編譯、下載運(yùn)行體驗(yàn)。
四、什么是NES ?
NES就是紅白機(jī)的游戲,所謂的NES意思是歐美版的紅白機(jī),F(xiàn)C的美版,Nintendo entertainment system(任天堂娛樂(lè)系統(tǒng)),而日本的紅白機(jī)則叫family computer(FC)。
發(fā)展歷史-來(lái)至百度百科
1983年7月15日,由日本任天堂株式會(huì)社(原本是生產(chǎn)日式撲克即“花札”)的宮本茂先生領(lǐng)導(dǎo)開(kāi)發(fā)的一種第三代家用電子游戲機(jī):FC,全稱:Family Computer,也稱作:Famicom;在日本以外的地區(qū)發(fā)售時(shí)則被稱為NES,全稱:Nintendo Entertainment System;在中國(guó)大陸、臺(tái)灣和香港等地,因其外殼為紅白兩色,所以人們俗稱其為“紅白機(jī)”,正式進(jìn)入市場(chǎng)銷售,并于后來(lái)取得了巨大成功,由此揭開(kāi)了家用電子游戲機(jī)遍布世界任何角落,電子游戲全球大普及的序幕。
1985年,NES在北美地區(qū)的銷量3300萬(wàn)臺(tái),比日本地區(qū)高出近一倍, 也占據(jù)了其全球市場(chǎng)份額的一半。 NES在北美首發(fā)時(shí)的捆綁游戲《打鴨子》(Duck hunt)總共取得近3000萬(wàn)套(基本全部來(lái)自北美市場(chǎng))銷量, [6] 這在紅白機(jī)游戲中名列第二,僅次于《超級(jí)馬力歐》。
1986年,任天堂在美國(guó)收3.1億美元,這一年美國(guó)游戲產(chǎn)業(yè)的規(guī)模4.3億美元,而在一年前,深陷雅達(dá)利沖擊的美國(guó)游戲業(yè)的收入僅1億美元。 [7] 1988年發(fā)售的《超級(jí)馬力歐兄弟3》(Super Mario Bros. 3)在美國(guó)售出700萬(wàn)套,在日本銷量達(dá)400萬(wàn),銷售額5.5億美元。
1989年,任天堂的游戲機(jī)已占領(lǐng)美國(guó)90%和日本95%的市場(chǎng),任天堂成為游戲界巨無(wú)霸。
2003年7月,F(xiàn)C發(fā)售二十周年,任天堂宣布FC游戲機(jī)正式停產(chǎn)。至此,F(xiàn)C全世界已累計(jì)銷售6000萬(wàn)部以上。至今中國(guó)大陸、臺(tái)灣、香港與泰國(guó)甚至日本等地仍然在制造FC規(guī)格的兼容品。
任天堂成為了現(xiàn)代游戲產(chǎn)業(yè)的開(kāi)創(chuàng)者,在很多方面上確立了現(xiàn)代電子游戲的標(biāo)準(zhǔn)。
FC巨大成功使任天堂年純利從1985年開(kāi)始一直保持5億美元以上 ,其股票成為東京證券交易所績(jī)優(yōu)股代名詞,一度超越了3萬(wàn)日元,市值超松下等企業(yè),很多人都把任天堂成功譽(yù)為新時(shí)代商業(yè)神話。
任天堂紅白機(jī)(FC/NES)發(fā)行于1983年,在日本發(fā)行之后引起了不小的轟動(dòng),兩年之后進(jìn)軍北美市場(chǎng),更加奠定了任天堂的家用游戲機(jī)霸主地位。當(dāng)人們正需要一個(gè)高品質(zhì)的家用游戲機(jī)的時(shí)候,任天堂拿出了他們的全部家當(dāng),首發(fā)的數(shù)款游戲都贏得了玩家的贊譽(yù),超級(jí)馬力歐更成為了永遠(yuǎn)的經(jīng)典。在那個(gè)年代,擁有一臺(tái)紅白機(jī)應(yīng)該是孩子們最大的夢(mèng)想了。 根據(jù)外媒的數(shù)據(jù),在1990年30%的美國(guó)家庭都擁有NES主機(jī)。
五、工程源碼分析: 以精簡(jiǎn)版本(1)為例
工程源碼全部采用寄存器代碼風(fēng)格,基本上每行都有詳細(xì)的注釋;雖然STM32支持庫(kù)函數(shù)方式開(kāi)發(fā),效率更加快,但是寄存器方式可以更方便了解CPU底層寄存器的一些配置,對(duì)以后在學(xué)習(xí)使用其他類型的微處理器是非常有幫助的。
5.1 工程文件布局
?
5.2 主函數(shù)代碼
主函數(shù)里完成LCD屏幕初始化,按鍵初始化,LED燈初始化,串口初始化,F(xiàn)C游戲手柄初始化,默認(rèn)把LCD屏幕清屏為黑色。
LCD屏采用FSMC驅(qū)動(dòng)的,把FSMC時(shí)序速度配置到最快,達(dá)到STM32能支持的最快速度,提高LCD刷屏速度。
初始化完畢最后,調(diào)用了LoadNes函數(shù),完成游戲加載;如果加載失敗,就回到下面執(zhí)行while循環(huán),閃爍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);
//游戲文件可以通過(guò)winhex文件生成C源碼數(shù)組
extern const unsigned char nes_data1[40976];//超級(jí)瑪麗游戲的文件
extern const unsigned char nes_data2[262160];//魂斗羅游戲的文件
/*
移植說(shuō)明:
1. 加入游戲手柄
2. 優(yōu)化了游戲刷新的幀率
3. 加入開(kāi)發(fā)板本身自帶按鍵控制
*/
int main()
{
BeepInit(); //蜂鳴器初始化
LedInit(); //LED燈初始化
UsartInit(USART1,72,115200);
KeyInit(); //按鍵初始化
printf("串口工作正常!\r\n");
LcdInit(); //LCD初始化
//JoypadInit(); //游戲手柄初始化
LcdClear(0xFFFF);
/*
0000 0000:保留
0000 0001: DATAST保持時(shí)間=2個(gè)HCLK時(shí)鐘周期
0000 0010: DATAST保持時(shí)間=3個(gè)HCLK時(shí)鐘周期
……
1111 1111: DATAST保持時(shí)間=256個(gè)HCLK時(shí)鐘周期(這是復(fù)位后的默認(rèn)數(shù)值)
0、1、2、3、4、5、6、7、8、9、10、11、12、13、14
*/
LcdClear(0);
//開(kāi)始運(yùn)行游戲
LoadNes((unsigned char*)nes_data1,40976); //超級(jí)瑪麗
//LoadNes((unsigned char*)nes_data2,262160); //魂斗羅
while(1)
{
LED1=!LED1;
DelayMs(400);
}
}
5.3 加載NES游戲:LoadNes函數(shù)介紹
LoadNes函數(shù)原型:
u8 LoadNes(unsigned char* pname,u32 size)
該函數(shù)傳入NES游戲數(shù)據(jù)地址,和游戲數(shù)據(jù)大小進(jìn)來(lái)。
現(xiàn)在這個(gè)版本沒(méi)有使用SD卡和文件系統(tǒng),游戲的文件數(shù)據(jù)是直接加到代碼里編譯的。
?
這兩個(gè)數(shù)組是超級(jí)瑪麗和魂斗羅的數(shù)據(jù)。(直接使用打開(kāi)文件,使用WinHEX軟件打開(kāi),全選,右鍵編輯,選擇復(fù)制,選擇C源碼,復(fù)制成數(shù)組形式粘貼到keil里即可)
?
函數(shù)里面主要完成了NES模擬器基本的初始化。
主要完成了STM32超頻配置,配置鎖相環(huán)為16倍,超頻到128MHZ。
超頻配置代碼如下:
/*
函數(shù)功能:頻率設(shè)置
參 數(shù):PLL,倍頻數(shù)
*/
void NesClockSet(u8 PLL)
{
u8 temp=0;
RCC->CFGR&=0XFFFFFFFC; //修改時(shí)鐘頻率為內(nèi)部8M
RCC->CR&=~0x01000000; //PLLOFF
RCC->CFGR&=~(0XF<<18); //清空原來(lái)的設(shè)置
PLL-=2; //抵消2個(gè)單位
RCC->CFGR|=PLL<<18; //設(shè)置PLL值 2~16
RCC->CFGR|=1<<16; //PLLSRC ON
FLASH->ACR|=0x12; //FLASH 2個(gè)延時(shí)周期
RCC->CR|=0x01000000; //PLLON
while(!(RCC->CR>>25)); //等待PLL鎖定
RCC->CFGR|=0x02; //PLL作為系統(tǒng)時(shí)鐘
while(temp!=0x02) //等待PLL作為系統(tǒng)時(shí)鐘設(shè)置成功
{
temp=RCC->CFGR>>2;
temp&=0x03;
}
}
接下來(lái)初始化NES游戲模擬器的必要參數(shù),最后調(diào)用NesEmulateFrame函數(shù)進(jìn)入NES游戲主循環(huán)代碼,開(kāi)始運(yùn)行游戲。
LoadNes函數(shù)完整代碼如下:
/*
函數(shù)功能:開(kāi)始nes游戲
參 數(shù):pname:nes游戲路徑 u32 size 游戲大小
返 回 值:
0,正常退出
1,內(nèi)存錯(cuò)誤
2,文件錯(cuò)誤
3,不支持的map
*/
u8 LoadNes(unsigned char* pname,u32 size)
{
u8 res=0;
res=NesSramMalloc(); //申請(qǐng)內(nèi)存
romfile=(u8*)pname; //游戲源碼地址
NESrom_crc32=get_crc32(romfile+16,size-16);//獲取CRC32的值
res=LoadNesRom(); //加載ROM
printf("res=%d\r\n",res);
NesClockSet(16); //設(shè)置系統(tǒng)時(shí)鐘為128MHZ 16*8
JoypadInit(); //游戲手柄初始化
cpu6502_init(); //初始化6502,并復(fù)位
Mapper_Init(); //map初始化
PPU_reset(); //ppu復(fù)位
apu_init(); //apu初始化
NesEmulateFrame(); //進(jìn)入NES模擬器主循環(huán)
return res;
}
5.3 NES游戲主循環(huán)代碼
?
詳細(xì)代碼如下:
//nes模擬器主循環(huán)
void NesEmulateFrame(void)
{
u8 nes_frame;
NesSetWindow();//設(shè)置窗口
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);//運(yùn)行1線
NES_Mapper->HSync(NES_scanline);
start_vblank();
if(NMI_enabled())
{
cpunmi=1;
run6502(7*256);//運(yùn)行中斷
}
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幀讀取游戲手柄數(shù)據(jù)
nes_frame++;
if(nes_frame>NES_SKIP_FRAME)
{
nes_frame=0;//跳幀
}
}
}
進(jìn)來(lái)就先調(diào)用了NesSetWindow(void)函數(shù),設(shè)置窗口大小,這里面就調(diào)用了LCD的接口,如果是其他的LCD屏,使用本代碼只需要把這里適配一下即可。
u8 nes_xoff=0; //顯示在x軸方向的偏移量(實(shí)際顯示寬度=256-2*nes_xoff)
//設(shè)置游戲顯示窗口
void NesSetWindow(void)
{
u16 lcdwidth,lcdheight;
lcdwidth=256;
lcdheight=240;
nes_xoff=0;
LcdSetWindow(32,0,lcdwidth,lcdheight);
LcdWriteRAM_Prepare();//寫(xiě)入LCD RAM的準(zhǔn)備
}
接下來(lái)就進(jìn)入到NES游戲的主循環(huán)代碼,開(kāi)始循環(huán)一幀一幀的刷出圖像數(shù)據(jù),達(dá)到游戲的效果。
設(shè)置窗口大小之后,下面就是從NES游戲數(shù)據(jù)文件里取出顏色數(shù)據(jù),然后for循環(huán)一行一行刷屏即可。
上面的設(shè)置窗口大小的代碼其實(shí)并不是必要的,只是當(dāng)前使用的LCD支持坐標(biāo)自增(一般LCD都支持的),設(shè)置LCD的窗口范圍之后,連續(xù)給LCD寫(xiě)數(shù)據(jù),LCD的坐標(biāo)會(huì)自動(dòng)自增,提高刷屏效率而已。如果你的LCD屏并不支持坐標(biāo)自增或者你不會(huì)寫(xiě)代碼,也想移植,那完全不用設(shè)置窗口那個(gè)函數(shù),你只需要提供一個(gè)畫(huà)點(diǎn)函數(shù),把for循環(huán)里的刷屏代碼里行掃描改掉就行。
函數(shù)里的這個(gè)for循環(huán)就是主要刷出圖像的代碼,如果想要移植到其他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);
}
里面調(diào)用scanline_draw函數(shù)是按行掃描(也就是一行一行繪制圖像),scanline_draw函數(shù)里面也是一個(gè)for循環(huán),細(xì)化到每個(gè)像素點(diǎn),按照每個(gè)像素點(diǎn)繪制到屏幕上,代碼里的LCD_RAM就是當(dāng)前LCD屏的地址,因?yàn)楫?dāng)前LCD屏采用的是FSMC,這個(gè)LCD_RAM就是FSMC地址,向這個(gè)地址寫(xiě)數(shù)據(jù),F(xiàn)SMC就產(chǎn)生8080時(shí)序?qū)?shù)據(jù)送給LCD顯示屏,刷新顯示出來(lái)。
scanline_draw函數(shù)詳細(xì)刷屏代碼如下:
extern u8 nes_xoff; //顯示在x軸方向的偏移量(實(shí)際顯示寬度=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++)>
運(yùn)行完刷屏的for循環(huán)函數(shù),一幀游戲圖像就顯示在LCD上了。
接下來(lái)就是掃描按鍵值,完成游戲人物的控制,函數(shù)里調(diào)用了NesGetGamepadval()函數(shù),讀取按鍵值刷新按鍵狀態(tài)。
NesGetGamepadval()函數(shù)代碼如下:
/*
鍵值說(shuō)明:
開(kāi)始鍵:8
選擇建:4
方向右:128
方向左:64
方向上:16
方向下:32
功能鍵上/左:2
功能鍵下/右:1
組合鍵:方向右與
讀取游戲手柄數(shù)據(jù)和功能鍵左 :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游戲模擬器定義了兩個(gè)全局變量,分別記錄游戲手柄1和游戲手柄2的數(shù)據(jù),因?yàn)镹ES游戲是可以兩個(gè)人一起玩的。
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
只需要在這個(gè)函數(shù)給這兩個(gè)全局變量賦予正確的值,游戲人物就可以按照正常的動(dòng)作畫(huà)面出現(xiàn)。
至于你的物理按鍵采用FC游戲手柄,還是普通的其他按鍵,只要這兩個(gè)全局變量的值正確那就沒(méi)問(wèn)題。 所有手柄采用什么不重要,關(guān)鍵把代碼這里邏輯看懂,看懂了你就知道程序的運(yùn)行邏輯了。
到此,版本1的 主要代碼就分析完畢了,其他的詳細(xì)過(guò)程可以看工程源碼,把程序跑起來(lái)了,一切都懂了。
六、工程源碼分析: 以完整版本(3)為例
這個(gè)版本加入了游戲手柄,VS1053、SD、FATFS文件系統(tǒng)等功能,這里接著第五章分析,下面就主要分析新加入的代碼內(nèi)容。
?
6.1 FC游戲手柄介紹
FC游戲手柄,大致可分為兩種:一種手柄插口是 11 針的,一種是 9 針的。但 11 針的現(xiàn)在市面上很少了,現(xiàn)在幾乎都是使用 9 針 FC 組裝手柄,下面就是介紹的是 9 針 FC 手柄,該手柄還有一個(gè)特點(diǎn),就是可以直接和DR9 的串口頭對(duì)插!這樣同開(kāi)發(fā)板的連接就簡(jiǎn)單了。
FC 手柄的外觀如圖所示:
?
這種手柄一般有 10 個(gè)按鍵(實(shí)際是 8 個(gè)鍵值):上、下、左、右、 Start、 Select、 A、 B、 A連發(fā)、 B 連發(fā)。這里的 A 和 A 連發(fā)是一個(gè)鍵值,而 B 和 B 連發(fā)也是一個(gè)鍵值,只是連發(fā)按鍵當(dāng)你一直按下的時(shí)候,會(huì)不停的發(fā)送(方便快速按鍵,比如發(fā)炮彈之類的功能)。
FC 手柄的控制電路,由 1 個(gè) 8 位并入串出的移位寄存器(CD4021),外加一個(gè)時(shí)基集成電路(NE555,用于連發(fā))構(gòu)成。不過(guò)現(xiàn)在的手柄,為了節(jié)約成本,直接就在 PCB 上做綁定了,所以你拆開(kāi)手柄,一般是看不到里面有四四方方的 IC,而只有一個(gè)黑色的小點(diǎn),所有電路都集成到這個(gè)里面了,但是他們的控制和讀取方法還是一樣的。
游戲上手柄數(shù)據(jù)讀取時(shí)序
?
從上圖可看出,讀取手柄按鍵值的信息十分簡(jiǎn)單:先 Latch(鎖存鍵值),然后就得到了第一個(gè)按鍵值(A),之后在 Clock 的作用下,依次讀取其他按鍵的鍵值,總共 8 個(gè)按鍵鍵值。
常規(guī)狀態(tài)下,LATCH為低電平,CLK為高電平,DATA為高電平,這也是初始化端口時(shí)的狀態(tài)。
單片機(jī)讀取鍵值時(shí)序很簡(jiǎn)單,LATCH先發(fā)送一個(gè)高脈沖,數(shù)據(jù)將鎖存到手柄內(nèi)部的移位寄存器,然后在CLK時(shí)鐘下降沿?cái)?shù)據(jù)將從DATA低位在先連續(xù)發(fā)出。按鍵映射到數(shù)據(jù)的對(duì)應(yīng)位上,有鍵按下則對(duì)應(yīng)位為0,無(wú)鍵按下則為1.即不按任何鍵時(shí),讀取數(shù)據(jù)為0xFF。
鍵值:
[7]:右
[6]:左
[5]:下
[4]:上
[3]:Start
[2]:Select
[1]:B
[0]:A
驅(qū)動(dòng)代碼示例:
功 能:手柄初始化函數(shù)
硬件連接:
CLK :PD3 --時(shí)鐘線
PB10:DATA --數(shù)據(jù)線
PB11:LAT --鎖存接口
*/
void JoypadInit(void)
{
/*1. 開(kāi)時(shí)鐘*/
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;
}
/*
功 能:獲取手柄的按鍵值
返回值:保存了一幀按鍵的狀態(tài)
鍵值:
[7]:右
[6]:左
[5]:下
[4]:上
[3]:Start
[2]:Select
[1]:B
[0]:A
*/
u8 GetJoypadKey(void)
{
u8 key=0,i;
JOYPAD_LAT=1; //開(kāi)始鎖存
DelayUs(30);
JOYPAD_LAT=0; //鎖存當(dāng)前的按鍵狀態(tài)
for(i=0;i<8;i++)
{
key=key>>1;
if(JOYPAD_DATA==0)key|=0x80;
JOYPAD_CLK=1; //輸出一個(gè)上升沿,告訴手柄發(fā)送數(shù)據(jù)
DelayUs(30);
JOYPAD_CLK=0; //數(shù)據(jù)線保持穩(wěn)定
DelayUs(30);
}
return key;
}
6.2 加載NES游戲:nes_load函數(shù)
這里的nes_load函數(shù)和第五章的區(qū)別就是,游戲數(shù)據(jù)的來(lái)源是從SD卡讀取的。
傳入游戲名稱去SD卡上打開(kāi)指定文件,讀取數(shù)據(jù)進(jìn)來(lái)。
這里用到了外部SRAM內(nèi)存,因?yàn)樽x出的數(shù)據(jù)需要存放到數(shù)組里,STM32F103ZET6本身的內(nèi)存只有64K,肯定不夠用,這里申請(qǐng)的空間是從外部SRAM模塊里申請(qǐng)的,所以開(kāi)發(fā)板還得帶一個(gè)SRAM芯片才行,沒(méi)有自帶就去淘寶買一個(gè)SRAM模塊即可(淘寶有個(gè)叫微雪的店鋪就有賣)。
?
詳細(xì)代碼如下:
u8 nes_load(u8* pname)
{
FIL *file;
UINT br;
u8 res=0;
file=malloc(sizeof(FIL));
if(file==0)return 1; //內(nèi)存申請(qǐng)失敗.
res=f_open(file,(char*)pname,FA_READ);
if(res!=FR_OK) //打開(kāi)文件失敗
{
printf("%s 文件打開(kāi)失敗!\r\n",pname);
free(file);
return 2;
}
else
{
printf("%s 文件打開(kāi)成功!\r\n",pname);
}
res=nes_sram_malloc(file->fsize); //申請(qǐng)內(nèi)存
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,并復(fù)位
Mapper_Init(); //map初始化
PPU_reset(); //ppu復(fù)位
apu_init(); //apu初始化
nes_sound_open(0,APU_SAMPLE_RATE); //初始化播放設(shè)備
nes_emulate_frame(); //進(jìn)入NES模擬器主循環(huán)
nes_sound_close(); //關(guān)閉聲音輸出
}
}
f_close(file);
free(file);//釋放內(nèi)存
nes_sram_free(); //釋放內(nèi)存
return res;
}
這里面調(diào)用了nes_sound_open函數(shù)初始化了音頻設(shè)備(VS1053)。這個(gè)非常重要,要理解游戲聲音是如何輸出的,就認(rèn)真看這里的流程。
nes_sound_open函數(shù)里初始化了VS1053音頻設(shè)備,然后開(kāi)啟了定時(shí)器中斷,使用定時(shí)器去調(diào)用VS1053的播放接口,在定時(shí)器中斷服務(wù)器函數(shù)里完成聲音數(shù)據(jù)的輸出,這里聲音是存放在一個(gè)全局緩沖區(qū)里,后面游戲在主循環(huán)里運(yùn)行的時(shí)候會(huì)不斷的向這個(gè)緩沖區(qū)填數(shù)據(jù),定時(shí)器超時(shí)進(jìn)中斷就查詢是否有音樂(lè)可以播放,有就播放,沒(méi)有就出來(lái)。
?
VS1052聲音播放代碼示例:
//音頻播放回調(diào)函數(shù)
void nes_vs10xx_feeddata(void)
{
u8 n;
u8 nbytes;
u8 *p;
if(nesplaybuf==nessavebuf)return;//還沒(méi)有收到新的音頻數(shù)據(jù)
if(VS1053_DREQ!=0)//可以發(fā)送數(shù)據(jù)給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函數(shù)代碼如下:
//NES打開(kāi)音頻輸出
int nes_sound_open(int samples_per_sync,int sample_rate)
{
u8 *p;
u8 i;
p=malloc(100); //申請(qǐng)100字節(jié)內(nèi)存
if(p==NULL)return 1; //內(nèi)存申請(qǐng)失敗,直接退出
printf("sound open:%d\r\n",sample_rate);
for(i=0;i>8)&0XFF;
p[28]=sample_rate&0XFF; //設(shè)置字節(jié)速率(8位模式,等于采樣率)
p[29]=(sample_rate>>8)&0XFF;
nesplaybuf=0;
nessavebuf=0;
VS1053_Reset(); //硬復(fù)位
VS1053_SoftReset(); //軟復(fù)位
VS1053_SetVol(200); //設(shè)置音量等參數(shù)
//復(fù)位解碼時(shí)間
VS1053_WriteCmd(SPI_DECODE_TIME,0x0000);
VS1053_WriteCmd(SPI_DECODE_TIME,0x0000); //操作兩次
while(VS1053_SendMusicData(p)); //發(fā)送wav head
while(VS1053_SendMusicData(p+32)); //發(fā)送wav head
TimerInit(TIM6,72,1000); //1ms中斷一次
free(p); //釋放內(nèi)存
return 1;
}(nes_wav_head);i++)>
初始化完畢之后,就調(diào)用nes_emulate_frame函數(shù)進(jìn)入到游戲主循環(huán)。
6.3 游戲主循環(huán)代碼
現(xiàn)在這份代碼比第五章代碼增加了一個(gè)聲音輸出函數(shù),調(diào)用VS1053,播放游戲的聲音。
?
apu_soundoutput函數(shù)代碼如下:
//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個(gè)數(shù)據(jù),是不是都相等?
if(i==30&&wave_buffers[i])//都相等,且不等于0
{
for(i=0;i;i++)wave_buffers[i]=0;>
最后調(diào)用了nes_apu_fill_buffer 函數(shù)將數(shù)據(jù)賦值給VS1053緩沖區(qū)進(jìn)行播放。
在前面已經(jīng)分析了音頻初始化代碼,里面初始化了定時(shí)器,會(huì)不斷的查詢緩沖區(qū)是否有音樂(lè)數(shù)據(jù)需要播放,有就播放,沒(méi)有就輸出,這個(gè)函數(shù)就是向音頻緩沖區(qū)填充數(shù)據(jù)的。
nes_apu_fill_buffer 函數(shù)代碼如下:
//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)//輸出數(shù)據(jù)趕上音頻播放的位置了,等待.
{
DelayMs(5);
}
nessavebuf=tbuf;
} ;i++)>
到此,音頻的主要代碼就分析完畢了。 可以下載程序去體驗(yàn)一下游戲,懷戀童年時(shí)光了
?審核編輯:符乾江
-
STM32
+關(guān)注
關(guān)注
2257文章
10828瀏覽量
352447 -
游戲機(jī)
+關(guān)注
關(guān)注
9文章
297瀏覽量
33268
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論