没玩过NES游戏的童年,可能不是80后的童年。我们小时候是从玩FC开始接触游戏机的,那时真的是红极一时啊,我上初中时还省吃俭用买了一台小霸王,暑假里把电视机都给打爆了!那时任天堂单是FC机的主机的发售收入就超过全美的电视台的收入的总和,在人们的心目中扎下了任天堂的这个招牌。
前言
1983年7月15日,由日本任天堂株式会社(原本是生产日式扑克即“花札”)的宫本茂先生领导开发的一种第三代家用电子游戏机:FC,全称:Family Computer,也称作:Famicom;在欧美发售时则被称为nes,全称:Nintendo Entertainment System;在中国大陆、台湾和香港等地,因其外壳为红白两色,所以人们俗称其为“红白机”,正式进入市场销售,并于后来取得了巨大成功,由此揭开了家用电子游戏机遍布世界任何角落,电子游戏全球大普及的序幕。
什么是InfNES?
一款NES游戏模拟器。InfoNES可以很容易地被移植到各个平台,作者是Martin Freij。他是一位瑞典的程序员和游戏爱好者,于2002年开发了infoNES模拟器。infoNES是一个基于NES(任天堂娱乐系统)的模拟器,旨在让人们能够在计算机上玩经典的NES游戏。
InfoNES具备良好的可移植性,它将与环境有关的内容都清出了软件内核,并且单独集合于一个InfoNES_System.h中,我们要做的就是实现这里提到的各种函数,再把InfoNES加入到我们的工程中一起编译。
最近成功实现了USB接口的FC手柄驱动,使得在imx6ull开发板玩游戏具有可玩性,这里将这个移植过程记录下来。如果对NES模拟器的源码实现感兴趣,infoNES也是个不错的研究对象,代码结构清晰,可以让你了解到如何模拟实现k6502这款经典cpu的,加深对计算机体系结构的理解。
接下来让我们重温下经典,缅怀下童年吧!
池塘外的迷路书上,知鸟在声声叫着夏天......,伴随着优美的歌声,仿佛穿越回来了,少年。
完成以下操作,让你即刻拥有款移动游戏机,实现童年时的梦想。
很早之前我在imax283平台上移植过infoNES,那时我的github仓地址是:
https://github.com/yongzhena/infoNES
这次直接拉取下来用,只是修改下joypad手柄驱动的代码就可以完美运行啦。
移植过程
整个移植过程主要涉及三部分,显示、声音输出和usb手柄支持。前两个直接拉取上面的我的仓直接就具备了,这里着重介绍下USB手柄驱动支持。
基于fb0的LCD显示
在InfoNES_System_Linux.cpp文件中修改。显示这块儿实现两个函数,一个是lcd_fb_init,一个是lcd_fb_display_px。
static int lcd_fb_init()
{//如果使用 mmap 打开方式 必须是 读定方式fb_fd = open("/dev/fb0", O_RDWR);if(-1 == fb_fd){printf("cat't open /dev/fb0 \n");return -1;}//获取屏幕参数if(-1 == ioctl(fb_fd, FBIOGET_VSCREENINFO, &var)){close(fb_fd);printf("cat't ioctl /dev/fb0 \n");return -1;}//计算参数px_width = var.bits_per_pixel /8;line_width = var.xres * px_width;screen_width = var.yres * line_width;lcd_width = var.xres;lcd_height = var.yres;printf("fb width:%d height:%d pixel:%d \n", lcd_width, lcd_height,px_width*8);fb_mem = (unsigned char *)mmap(NULL, screen_width, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);if(fb_mem == (void *)-1){close(fb_fd);printf("cat't mmap /dev/fb0 \n");return -1;}//清屏memset(fb_mem, 0 , screen_width);return 0;
}
static int lcd_fb_display_px(WORD color, int x, int y)
{unsigned char *pen8;unsigned short *pen16;pen8 = (unsigned char *)(fb_mem + y*line_width + x*px_width);pen16 = (unsigned short *)pen8;*pen16 = color;return 0;
}
以下的实现注意zoom_x_tab,zoom_y_tab这两项。它的作用是对像素做了全屏和放大处理。 源码里的make_zoom_tab()就是干这个用。如果觉得屏幕很大,放大后颗粒感很重,能否再优化?这里是个可能的优化方向。
/*===================================================================*/
/* */
/* InfoNES_LoadFrame() : */
/* Transfer the contents of work frame on the screen */
/* */
/*===================================================================*/
unsigned short ChColor(unsigned short color)
{return (color>>3)<<4|(color&0x001f);
}void InfoNES_LoadFrame()
{int x,y;int line_width;WORD wColor,R,G,B,Gr;//修正 if(0 < fb_fd){for (y = 0; y < lcd_height; y++ ){line_width = zoom_y_tab[y] * NES_DISP_WIDTH;for (x = 0; x < lcd_width; x++ ){wColor = ChColor(WorkFrame[line_width + zoom_x_tab[x]]);lcd_fb_display_px(wColor, x, y);}}}/*16 bit per pixel*//* Exchange 16-bit to 256 gray *//*for (y = 0; y < NES_DISP_HEIGHT; y++ ){for (x = 0; x < NES_DISP_WIDTH; x++ ){//wColor = WorkFrame[y * lcd_width + x ];wColor = WorkFrame[ ( y << 8 ) + x ];R = ( ( wColor & 0x7c00 ) >>7 );G = ( ( wColor & 0x03e0 ) >>2 );B = ( ( wColor & 0x001f ) <<3 ); //Gr= ( ( 9798*R + 19235*G + 3735*B)>>15);wColor=(WORD)((B<<16)|(G<<8)|R);lcd_fb_display_px(wColor, x, y);}}*/
}
基于Alsa的声音支持
实现这个声音支持的前提是,板子上得有基于alsa框架的音频驱动且功能正常。否则以下这些实现里需要全部留空,不用实现。
/*===================================================================*/
/* */
/* InfoNES_SoundInit() : Sound Emulation Initialize */
/* */
/*===================================================================*/
void InfoNES_SoundInit( void )
{}/*===================================================================*/
/* */
/* InfoNES_SoundOpen() : Sound Open */
/* */
/*===================================================================*/
int InfoNES_SoundOpen( int samples_per_sync, int sample_rate )
{// sample_rate 采样率 44100// samples_per_sync 735// 采样率 / 8 * 声道数 = 44100 / 8 * 1 = 5512.5// 8位 声音/*声道数 1采样率 44100采样位数 8每次播放块大小(NES APU 每次生成一块)735*/unsigned int rate = sample_rate;snd_pcm_hw_params_t *hw_params;if(0 > snd_pcm_open(&playback_handle, "default", SND_PCM_STREAM_PLAYBACK, 0)) {printf("snd_pcm_open err\n");return -1;}printf("snd_pcm_open ok!\nsamples_per_sync=%d,sample_rate=%d\n",samples_per_sync,sample_rate);if(0 > snd_pcm_hw_params_malloc(&hw_params)){printf("snd_pcm_hw_params_malloc err\n");return -1;}if(0 > snd_pcm_hw_params_any(playback_handle, hw_params)){printf("snd_pcm_hw_params_any err\n");return -1;}if(0 > snd_pcm_hw_params_set_access(playback_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) {printf("snd_pcm_hw_params_any err\n");return -1;}//16bit PCM 数据if(0 > snd_pcm_hw_params_set_format(playback_handle, hw_params, SND_PCM_FORMAT_U8)){printf("snd_pcm_hw_params_set_format err\n");return -1;}if(0 > snd_pcm_hw_params_set_rate_near(playback_handle, hw_params, &rate, 0)) {printf("snd_pcm_hw_params_set_rate_near err\n");return -1;}//单声道 非立体声if(0 > snd_pcm_hw_params_set_channels(playback_handle, hw_params, 1)){printf("snd_pcm_hw_params_set_channels err\n");return -1;}if(0 > snd_pcm_hw_params(playback_handle, hw_params)) {printf("snd_pcm_hw_params err\n");return -1;}snd_pcm_hw_params_free(hw_params);if(0 > snd_pcm_prepare(playback_handle)) {printf("snd_pcm_prepare err\n");return -1;}return 1;
}/*===================================================================*/
/* */
/* InfoNES_SoundClose() : Sound Close */
/* */
/*===================================================================*/
void InfoNES_SoundClose( void )
{snd_pcm_close(playback_handle);
}/*===================================================================*/
/* */
/* InfoNES_SoundOutput() : Sound Output 5 Waves */
/* */
/*===================================================================*/
void InfoNES_SoundOutput( int samples, BYTE *wave1, BYTE *wave2, BYTE *wave3, BYTE *wave4, BYTE *wave5 )
{int i;int ret;unsigned char wav;unsigned char *pcmBuf = (unsigned char *)malloc(samples);//printf("InfoNES_SoundOutput,samples=%d\n",samples);//printf("\n");for (i=0; i <samples; i++){wav = (wave1[i] + wave2[i] + wave3[i] + wave4[i] + wave5[i]) / 5;//单声道 8位数据pcmBuf[i] = wav;//printf("%02x",wav);}//printf("\n");ret = snd_pcm_writei(playback_handle, pcmBuf, samples);if(-EPIPE == ret){snd_pcm_prepare(playback_handle);}free(pcmBuf);return ;
}
USB手柄支持
接下来这块儿是介绍的重点,实现usb手柄驱动的支持。这样才有可玩性啊。我买的这款USB的游戏手柄很便宜,也很容易买到。如果你的USB手柄不是这款,那么实现驱动支持的原理也是类似的,万变不离宗,只是键值对应关系跟我的可能不一样,实测改下即可。
关于USB游戏手柄的驱动支持,参见我的上篇博文:iMX6ULL驱动开发 | 让imx6ull开发板支持usb接口FC游戏手柄_特立独行的猫a的博客-CSDN博客
不想按上文总结的重新编译内核的话,可以把驱动单独编译成模块动态加载进去。
这里介绍下让infoNES支持usb手柄需要做哪些移植。
按键键值测试小程序
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h> #define _EV_KEY 0x01 /* button pressed/released */
#define _EV_ABS 0x03
#define _EV_MSC 0x04 int main() {printf("hello,usb hid joystick key test\n");int fd = open("/dev/input/event3", O_RDONLY);struct input_event e;while(1) {read(fd, &e, sizeof(e));switch(e.type) {case _EV_KEY:printf("type: %d, code: %d,value: %d, time: %d\n", e.type, e.code,e.value, e.time);break;case _EV_ABS:printf("type: %d, code: %d,value: %d, time: %d\n", e.type, e.code,e.value, e.time);break;case _EV_MSC:printf("type: %d, code: %d,value: %d, time: %d\n", e.type, e.code,e.value, e.time);break;default:if(e.type != 0){printf("type:%d, code: %d,value: %d, time: %d\n",e.type, e.code,e.value, e.time);}}}close(fd);return 0;
}
joypad_input.cpp文件修改
主要是USBjoypadGet()接口的实现,要跟FC手柄的键值对应上。
static int USBjoypadGet(void)
{/*** FC手柄 bit 键位对应关系 真实手柄中有一个定时器,处理 连A 连B * 0 1 2 3 4 5 6 7* A B Select Start Up Down Left Right*///因为 USB 手柄每次只能读到一位键值 所以要有静态变量保存上一次的值static unsigned char joypad = 0;struct input_event e;if(0 < read (USBjoypad_fd, &e, sizeof(e))){if(0x3 == e.type){/*上:value:0 type:0x3 code:0x1value:127 type:0x3 code:0x1*/if(0 == e.value && 0x1 == e.code){joypad |= 1<<4;printf("Up\n");}/*下:value:255 type:0x3 code:0x1value:127 type:0x3 code:0x1*/if(255 == e.value && 0x1 == e.code){joypad |= 1<<5;printf("Down\n");}//松开if(127 == e.value && 0x1 == e.code){joypad &= ~(1<<4 | 1<<5);}/*左:value:0 type:0x3 code:0x0value:127 type:0x3 code:0x0*/if(0 == e.value && 0 == e.code){joypad |= 1<<6;printf("Left\n");}/*右:value:255 type:0x3 code:0x0value:127 type:0x3 code:0x0*/if(255 == e.value && 0 == e.code){joypad |= 1<<7;printf("Right\n");}//松开if(127 == e.value && 0 == e.code){joypad &= ~(1<<6 | 1<<7);}}if(0x1 == e.type){/*选择:value:0x1 type:0x1 code:296value:0x0 type:0x1 code:296*/if(0x1 == e.value && 296 == e.code){joypad |= 1<<2;printf("Select\n");}if(0x0 == e.value && 296 == e.code){joypad &= ~(1<<2);}/*开始:value:0x1 type:0x1 code:297value:0x0 type:0x1 code:297*/if(0x1 == e.value && 297 == e.code){joypad |= 1<<3;printf("Start\n");}if(0x0 == e.value && 297 == e.code){joypad &= ~(1<<3);}/*Avalue:0x1 type:0x1 code:288value:0x0 type:0x1 code:288*/if(0x1 == e.value && 288 == e.code){joypad |= 1<<0;printf("A\n");}if(0x0 == e.value && 288 == e.code){joypad &= ~(1<<0);}/*Bvalue:0x1 type:0x1 code:289value:0x0 type:0x1 code:289*/if(0x1 == e.value && 289 == e.code){joypad |= 1<<1;printf("B\n");}if(0x0 == e.value && 289 == e.code){joypad &= ~(1<<1);}/*Xvalue:0x1 type:0x1 code:290value:0x0 type:0x1 code:290*/if(0x1 == e.value && 290 == e.code){joypad |= 1<<0;printf("X\n");}if(0x0 == e.value && 290 == e.code){joypad &= ~(1<<0);}/*Yvalue:0x1 type:0x1 code:291value:0x0 type:0x1 code:291*/if(0x1 == e.value && 291 == e.code){joypad |= 1<<1;printf("Y\n");}if(0x0 == e.value && 291 == e.code){joypad &= ~(1<<1);}}return joypad;}return -1;
}
完整实现
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <linux/input.h> #define JOYPAD_DEV "/dev/joypad"
#define USB_JS_DEV "/dev/input/event3"typedef struct JoypadInput{int (*DevInit)(void);int (*DevExit)(void);int (*GetJoypad)(void);struct JoypadInput *ptNext;pthread_t tTreadID; /* 子线程ID */
}T_JoypadInput, *PT_JoypadInput;//全局变量通过互斥体访问
static unsigned char g_InputEvent;static pthread_mutex_t g_tMutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_tConVar = PTHREAD_COND_INITIALIZER;static int joypad_fd;
static int USBjoypad_fd;
static PT_JoypadInput g_ptJoypadInputHead;static void *InputEventTreadFunction(void *pVoid)
{/* 定义函数指针 */int (*GetJoypad)(void);GetJoypad = (int (*)(void))pVoid;while (1){//因为有阻塞所以没有输入时是休眠g_InputEvent = GetJoypad();//有数据时唤醒pthread_mutex_lock(&g_tMutex);/* 唤醒主线程 */pthread_cond_signal(&g_tConVar);pthread_mutex_unlock(&g_tMutex);}
}static int RegisterJoypadInput(PT_JoypadInput ptJoypadInput)
{PT_JoypadInput tmp;if(ptJoypadInput->DevInit()){return -1;}//初始化成功创建子线程 将子项的GetInputEvent 传进来pthread_create(&ptJoypadInput->tTreadID, NULL, InputEventTreadFunction, (void*)ptJoypadInput->GetJoypad);if(! g_ptJoypadInputHead){g_ptJoypadInputHead = ptJoypadInput;}else{tmp = g_ptJoypadInputHead;while(tmp->ptNext){tmp = tmp->ptNext;}tmp->ptNext = ptJoypadInput;}ptJoypadInput->ptNext = NULL;return 0;
}static int joypadGet(void)
{static unsigned char joypad = 0;//printf("joypadGet val:\n");joypad = read(joypad_fd, 0, 0);return joypad;
}static int joypadDevInit(void)
{joypad_fd = open(JOYPAD_DEV, O_RDONLY);if(-1 == joypad_fd){printf("%s dev not found \r\n", JOYPAD_DEV);return -1;}return 0;
}static int joypadDevExit(void)
{close(joypad_fd);return 0;
}static T_JoypadInput joypadInput = {joypadDevInit,joypadDevExit,joypadGet,
};static int USBjoypadGet(void)
{/*** FC手柄 bit 键位对应关系 真实手柄中有一个定时器,处理 连A 连B * 0 1 2 3 4 5 6 7* A B Select Start Up Down Left Right*///因为 USB 手柄每次只能读到一位键值 所以要有静态变量保存上一次的值static unsigned char joypad = 0;struct input_event e;if(0 < read (USBjoypad_fd, &e, sizeof(e))){if(0x3 == e.type){/*上:value:0 type:0x3 code:0x1value:127 type:0x3 code:0x1*/if(0 == e.value && 0x1 == e.code){joypad |= 1<<4;printf("Up\n");}/*下:value:255 type:0x3 code:0x1value:127 type:0x3 code:0x1*/if(255 == e.value && 0x1 == e.code){joypad |= 1<<5;printf("Down\n");}//松开if(127 == e.value && 0x1 == e.code){joypad &= ~(1<<4 | 1<<5);}/*左:value:0 type:0x3 code:0x0value:127 type:0x3 code:0x0*/if(0 == e.value && 0 == e.code){joypad |= 1<<6;printf("Left\n");}/*右:value:255 type:0x3 code:0x0value:127 type:0x3 code:0x0*/if(255 == e.value && 0 == e.code){joypad |= 1<<7;printf("Right\n");}//松开if(127 == e.value && 0 == e.code){joypad &= ~(1<<6 | 1<<7);}}if(0x1 == e.type){/*选择:value:0x1 type:0x1 code:296value:0x0 type:0x1 code:296*/if(0x1 == e.value && 296 == e.code){joypad |= 1<<2;printf("Select\n");}if(0x0 == e.value && 296 == e.code){joypad &= ~(1<<2);}/*开始:value:0x1 type:0x1 code:297value:0x0 type:0x1 code:297*/if(0x1 == e.value && 297 == e.code){joypad |= 1<<3;printf("Start\n");}if(0x0 == e.value && 297 == e.code){joypad &= ~(1<<3);}/*Avalue:0x1 type:0x1 code:288value:0x0 type:0x1 code:288*/if(0x1 == e.value && 288 == e.code){joypad |= 1<<0;printf("A\n");}if(0x0 == e.value && 288 == e.code){joypad &= ~(1<<0);}/*Bvalue:0x1 type:0x1 code:289value:0x0 type:0x1 code:289*/if(0x1 == e.value && 289 == e.code){joypad |= 1<<1;printf("B\n");}if(0x0 == e.value && 289 == e.code){joypad &= ~(1<<1);}/*Xvalue:0x1 type:0x1 code:290value:0x0 type:0x1 code:290*/if(0x1 == e.value && 290 == e.code){joypad |= 1<<0;printf("X\n");}if(0x0 == e.value && 290 == e.code){joypad &= ~(1<<0);}/*Yvalue:0x1 type:0x1 code:291value:0x0 type:0x1 code:291*/if(0x1 == e.value && 291 == e.code){joypad |= 1<<1;printf("Y\n");}if(0x0 == e.value && 291 == e.code){joypad &= ~(1<<1);}}return joypad;}return -1;
}static int USBjoypadDevInit(void)
{USBjoypad_fd = open(USB_JS_DEV, O_RDONLY);if(-1 == USBjoypad_fd){printf("%s dev not found \r\n", USB_JS_DEV);return -1;}return 0;
}static int USBjoypadDevExit(void)
{close(USBjoypad_fd);return 0;
}static T_JoypadInput usbJoypadInput = {USBjoypadDevInit,USBjoypadDevExit,USBjoypadGet,
};int InitJoypadInput(void)
{int iErr = 0;//iErr = RegisterJoypadInput(&joypadInput);iErr = RegisterJoypadInput(&usbJoypadInput);return iErr;
}int GetJoypadInput(void)
{/* 休眠 */pthread_mutex_lock(&g_tMutex);pthread_cond_wait(&g_tConVar, &g_tMutex); /* 被唤醒后,返回数据 */pthread_mutex_unlock(&g_tMutex);return g_InputEvent;
}
编译生成
最后,交叉编译生成可执行文件,放到板子上执行即可,插上USB手柄就可以玩啦,运行不错!还很流畅。需要注意的是,为了支持声音,使用了alsa的头文件并链接了libasound库。需确保你的环境里有这个库,没有的话不支持声音输出,可以去掉这个链接。文末有NES游戏的ROM资源。
makefile脚本
#根据实际路径修改工具链路径
CHAIN_ROOT=/opt/yang/imax6ul/gcc-linaro-arm-linux-gnueabihf-4.9-2014.09_linux/bin
CROSS_COMPILE=$(CHAIN_ROOT)/arm-linux-gnueabihf-#CHAIN_ROOT= /home/yang/b503/ctools/gcc-linaro-arm-linux-gnueabihf-4.9-2014.09_linux/bin
#CROSS_COMPILE=$(CHAIN_ROOT)/arm-linux-gnueabihf-
#CROSS_COMPILE = CC := $(CROSS_COMPILE)gcc
#CC = arm-poky-linux-gnueabi-gcc
TARBALL = InfoNES08J# InfoNES
.CFILES = ./../K6502.cpp \./../InfoNES.cpp \./../InfoNES_Mapper.cpp \./../InfoNES_pAPU.cpp \./InfoNES_System_Linux.cpp joypad_input.cpp.OFILES = $(.CFILES:.cpp=.o)CCFLAGS = -o2 -fsigned-char -I../
LDFILGS = -lstdc++ -L../libs # gcc3.x.xall: InfoNESInfoNES: $(.OFILES)$(CC) $(INCLUDES) -o $@ $(.OFILES) $(LDFILGS) -lm -lpthread -lasound.cpp.o:$(CC) $(INCLUDES) -c $(CCFLAGS) $*.cpp -o $@clean:rm -f $(.OFILES) ../*~ ../*/*~ corecleanall:rm -f $(.OFILES) ../*~ ../*/*~ core InfoNESrelease: clean alltar:( cd ..; \tar cvf $(TARBALL).tar ./*; \gzip $(TARBALL).tar \)install:install ./InfoNES /usr/local/bin
其他资源
NES红白机全屏显示
NES专题——NES游戏机简介_nesfc_金小庭的博客-CSDN博客
V3S移植nes游戏模拟器(附带游戏合集)_v3s编译游戏模拟器_qq_46604211的博客-CSDN博客
任天堂红白机nes游戏简介 任天堂红白机nes游戏简介
资料:内含众多NES的游戏ROM文件及运行模拟器
链接:https://pan.baidu.com/s/1uXAxLKGmKGwZFB3Yraq8gg 提取码:qxcy
游戏合集并解压,然后改名为游戏名为英文
链接:https://pan.baidu.com/s/16hIWwYQQEX9aOBDG1dVa0A
提取码:asdf