本帖最后由 qinyunti 于 2025-2-4 13:12 编辑

【电子DIY】DIY行车记录仪为游戏机之主控软件开发(11)NES移植
代码见:https://github.com/qinyunti/py32f403-nes.git



一.前言

前面我们已经准备好了文件系统,NES游戏文件的导入导出,TFT显示驱动等各种基础功能,万事俱备只欠东风,现在只剩关键的一步,移植NES游戏模拟器。

二.移植过程

代码见git仓库

主要代码如下

其中nes_main是需要移植的文件

130331qwaaw889raz9isri

2.1按键移植

根据我们手柄设计,按键信息按照4字节帧发送

即0xAA 0x55 0xXX 0xYY

其中AA55为帧头,XX是8个按键信息对应8位,对应位为1表示未按下按键,为0表示按下按键。按键信息通过蓝牙模块发送,主控这么通过蓝牙模块的串口接收,解析过程如下

Key.c中

#include "uart.h"

#include "xprintf.h"

static uint8_t s_key_buffer[4];

static int s_key_buffer_len = 0;

static int s_key = 0xFF;

uint8_t key_get(void)

{

        static uint8_t s_pre_key = 0xFF;

        uint8_t tmp;

        uint8_t len;

        do{

                len = uart_read(1,&tmp,1);

                        s_key_buffer[s_key_buffer_len] = tmp;

                        s_key_buffer_len++;

                        if(s_key_buffer_len >= 4){

                                if((s_key_buffer[0]==0xAA) && (s_key_buffer[1]==0x55) && ((s_key_buffer[2]+s_key_buffer[3])==(uint8_t)255)){

                                        s_key = s_key_buffer[2];

                                        s_key_buffer_len = 0;

                                }else{

                                  /* 错误帧,丢掉第一个字节 */

                                        s_key_buffer[0] = s_key_buffer[1];

                                        s_key_buffer[1] = s_key_buffer[2];

                                        s_key_buffer[2] = s_key_buffer[3];

                                        s_key_buffer_len = 3;

                                        s_key = 0xFF;

                                }


                        }else{

                                /* 不足帧长 */

                        }

        }while(len > 0);

        if(s_pre_key != s_key){

                s_pre_key = s_key;

                xprintf("key:%02x\r\n",s_key);

        }

        return s_key;

}

由于原始按键信息和NES对应可能不一致,所以还需要转换下

nes_main.c中

void nes_get_gamepadval(void)

{

        uint8_t key;

        uint8_t data = 0;

  /* key输入值  U D  L R    4        5      6  7     

         *            3 1  2 0    SELECT   START  B  A

         * 转为PADdata:[7:0]右7 左6 下5 上4 Start3 Select2 B1 A0         

         */

        key = key_get();

        if((key & ((uint8_t)1<<0)) == 0){

                data |= ((uint8_t)1<<7);

        }

        if((key & ((uint8_t)1<<1)) == 0){

                data |= ((uint8_t)1<<5);

        }

        if((key & ((uint8_t)1<<2)) == 0){

                data |= ((uint8_t)1<<6);

        }

        if((key & ((uint8_t)1<<3)) == 0){

                data |= ((uint8_t)1<<4);

        }        

        if((key & ((uint8_t)1<<4)) == 0){

                data |= ((uint8_t)1<<2);

        }

        if((key & ((uint8_t)1<<5)) == 0){

                data |= ((uint8_t)1<<3);

        }

        if((key & ((uint8_t)1<<6)) == 0){

                data |= ((uint8_t)1<<1);

        }

        if((key & ((uint8_t)1<<7)) == 0){

                data |= ((uint8_t)1<<0);

        }

        PADdata=data;

        PADdata1=0;                                //没有手柄2,故不采用.

}  

2.2显示移植

nes_ppu.c中

调用我们的显示接口显示一行

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;

        for(i=sx;i<ex;i++)

        {

                line_buffer[i-nes_xoff] = ppu->dummy_buffer;

        }        

        st7789_itf_fill_direct(0,ex-sx,LineNo,1,line_buffer);

}

这里需要先查找颜色画板将ppu->dummy_buffer转换为RGB颜色,

同时使用fill接口即一次填充区域替代逐点显示来提高效率。由于内存限制,这里直接使用写TFT,而不是先缓存然后帧同步。

2.3动态内存接口与存储需求

nes_main.c中

实现动态内存分配与释放接口

void* nes_malloc(uint32_t size)

{

        void* p = pvPortMalloc(size);

        if(p == (void*)0){

                xprintf("nes malloc %d err\r\n",size);

        }else{

                xprintf("nes malloc %d ok\r\n",size);

        }

        return p;

}

void nes_free(void* p)

{

        vPortFree(p);

        xprintf("nes free\r\n");

}

以下是需要动态分配的内存

首先最大的部分是游戏文件加载的缓存,这里最大加载25KB的游戏文件。

130331lj8fvqvaw11ee2s1

然后是nes运行需要的各种资源,这里删掉了音频相关apu,因为apu占用资源比较多,这里只有64k ram不够用。

最终分配如下

130331wyfb3nk4q9ivnyyv

打印如下

其中552是读文件时分配的FIL,用完释放,其他需要的时24592的游戏缓存,nes运行需要的15328,所以一共大概40字节。另外NES_RAM虽然大小是2K但是需要1k对齐,所以分配了3K。

130331j4o4551s5d5oqs5o

对的配置大小如下

130331hum5o8nyr82nxzmo

最终的程序占用ram如下,64KB用完

130331v0oqrdii7b0y9b9o

三.测试

Git仓库中有很多25KB的游戏可以导入,

130332nnhawiwbi922h69i

这里导入了3个,

130332pd0nuvd4q0szu0qr

创建一个任务运行nes

                nes_task_init("0:/jtxz.nes");

static void nes_task(void *arg)

{

    nes_main(arg);

    return;

}

void nes_task_init(char* name)

{

        TaskHandle_t xHandle;

  xprintf("nes %s\r\n",name);

  memset(nesname,0,sizeof(nesname));

  memcpy(nesname,name,strlen(name));

        xTaskCreate( nes_task, "nes", 512, nesname, 1, &xHandle );

}

void nes_main(char* name)

{

        nes_load(name);

        nes_play_romfile();

}

详见后续视频

b5daec8d79caef3f1891ba13142f3e3.jpg

四.总结

以上就完成了整个项目开发,从手柄软硬件到主控软硬件详细分享了整个过程。使用64 KB RAM也可以玩转25KB的NES游戏,git仓库中分享了很多游戏,可以加载尽情玩耍了。虽然使用直接写屏,但是显示帧率也还可以看不到刷屏的感觉。后面就开始整合到行车记录仪,然后去车上试玩了。