没玩过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:0x1
value:127 type:0x3 code:0x1
*/
if(0 == e.value && 0x1 == e.code)
{
joypad |= 1<<4;
printf("Up\n");
}
/*下:
value:255 type:0x3 code:0x1
value: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:0x0
value:127 type:0x3 code:0x0
*/
if(0 == e.value && 0 == e.code)
{
joypad |= 1<<6;
printf("Left\n");
}
/*右:
value:255 type:0x3 code:0x0
value: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:296
value: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:297
value: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);
}
/*A
value:0x1 type:0x1 code:288
value: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);
}
/*B
value:0x1 type:0x1 code:289
value: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);
}
/*X
value:0x1 type:0x1 code:290
value: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);
}
/*Y
value:0x1 type:0x1 code:291
value: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:0x1
value:127 type:0x3 code:0x1
*/
if(0 == e.value && 0x1 == e.code)
{
joypad |= 1<<4;
printf("Up\n");
}
/*下:
value:255 type:0x3 code:0x1
value: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:0x0
value:127 type:0x3 code:0x0
*/
if(0 == e.value && 0 == e.code)
{
joypad |= 1<<6;
printf("Left\n");
}
/*右:
value:255 type:0x3 code:0x0
value: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:296
value: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:297
value: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);
}
/*A
value:0x1 type:0x1 code:288
value: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);
}
/*B
value:0x1 type:0x1 code:289
value: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);
}
/*X
value:0x1 type:0x1 code:290
value: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);
}
/*Y
value:0x1 type:0x1 code:291
value: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.x
all: InfoNES
InfoNES: $(.OFILES)
$(CC) $(INCLUDES) -o $@ $(.OFILES) $(LDFILGS) -lm -lpthread -lasound
.cpp.o:
$(CC) $(INCLUDES) -c $(CCFLAGS) $*.cpp -o $@
clean:
rm -f $(.OFILES) ../*~ ../*/*~ core
cleanall:
rm -f $(.OFILES) ../*~ ../*/*~ core InfoNES
release: clean all
tar:
( 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