# 将韦东山NES游戏机移植到ESP32S3 **Repository Path**: zijinkunjiang/WeiDongShanNESForESP32 ## Basic Information - **Project Name**: 将韦东山NES游戏机移植到ESP32S3 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2025-03-26 - **Last Updated**: 2025-03-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 手把手将Nofredo NES移植到ESP32S3 ## 日志 20250328 简单的移植了nofrendo,移植失败(发现这玩意只支持从SPIFlash加载游戏ROM),因此果断放弃! 20250329 ** 第五点移植过程仅对源码进行分析,感兴趣的可以看一下,已经遗弃。** 20250329 **开始韦东山的代码移植....** //----------------------------------------------------------------------------------------------// ## 介绍 本项目将对Nofredo,白问网NES游戏机进行移植(ESP32 -> ESP32S3),对移植过程中遇到的问题进行记录,并对移植后的代码进行二次开源。 源代码自Espressif/Nofredo NES,本项目对此代码进行移植并且记录移植全过程 移植前,感谢韦东山老师开源的参考案例和代码:代码源地址 [开源地址](https://gitee.com/weidongshan/esp32_100ask_project) 这里我选择从esp32-nesemu-master开始移植,这是乐鑫官方的一个nes模拟器,移植的过程中参考学习了韦老师的代码,在此向韦老师开源表示感谢 韦老师的代码很值得我们学习!!深表感谢:) ## 使用说明 项目将记录nofredo的移植过程,最后会给出移植成功后的程序工程 ## 移植过程记录 #### 一、移植目标 本项目将会把nofredo移植成一个方便的组件,大家可以根据这个移植框架去对接大家的开发环境,并且方便大家在后续增加部分功能。 #### 二、Git源码 这里我们也选择韦东山的NES进行参考,其实他们本质上都是基于nofredo移植的,韦东山老师的源代码适配了多种手柄,也适配了音频部分,值得我们学习和参考。 源代码地址 [白问网NES游戏机](https://gitee.com/weidongshan/esp32_100ask_project) ![输入图片说明](Image/Others/%E5%85%8B%E9%9A%86%E4%B8%8B%E8%BD%BD.png) #### 三、模拟器源代码结构研究: 首先解压源代码,直接放到VSCode上,让我们来看看源代码里面都有啥 ```MarkDown components //模拟器代码区域,包含了所有的文件 |-nofrendo //这里应该是nofredo的主部分 | //模拟器的核心文件都放在这里 | |-cpu //6502CPU的模拟器程序 | |-libsnss //这段代码是一个用于处理SNSS文件格式的C语言库。 | |-mappers //这里应该是NES的mapper:mappers 是用于管理游戏卡带中程序 ROM(PROM)和字符 ROM(CROM)的内存映射的组件 | |-nes //应该是NES模拟器的主部分 | |-sndhrdw //unknow | |-AUTHORS | |-bitmap.c | |-bitmap.h //创建、绘制、销毁BMP图片的程序 | |-config.c //配置文件?目前不知道 | |-event.c | |-event.h //定义了一系列事件,目前也不知道做什么用 | |-gui_elem.c | |-gui_elem.h //定义了一些文字取模的符号以及光标的符号 | |-gui.c | |-gui.h //gui部分的东西,包括在屏幕上显示内容 | |-intro.c | |-intro.h //里面也有取模信息,估计是开机LOGO,目前并不知 | |-log.c | |-log.h //估计跟LOGO有关?? | |-memguard.c | |-memguard.h //估计跟memu有关?? | |-nofconfig.h //nofredo配置文件 | |-nofrendo.c | |-nofrendo.h //模拟器主程序 | |-noftypes.h // | |-osd.h // | |-pcx.c | |-pcx.h //跟屏幕截图有关 | |-version.h //版本定义 | |-vid_drv.c | |-vid_drv.h //视频显示驱动 | | |//--以下为我们需要对接的位置-- |-nofrendo-esp32 //nfredo,我们需要对接的地方 | |-osd.c //对接部分文件名字符串的操作 | |-psxcontroller.c //貌似是PSX控制器(可能是手柄)的驱动程序 | |-SPICLD.c //这个我就不多说了 SPILCD驱动,默认驱动ILI9341的显示屏 | |-video_aduio.c //有关声音方面的代码(也需要重点关注) ``` 了解大概的代码架构后,我们就可以准备开始移植了 #### 四、移植前的准备 ###### 1.工程准备 这里我准备了一个简单的工程,包括显示屏,SD卡等用得上的硬件,并且对程序进行编译,编译成功 ![基础工程编译成功](Image/BasicTarget/%E5%9F%BA%E7%A1%80%E5%B7%A5%E7%A8%8B%E7%BC%96%E8%AF%91%E6%88%90%E5%8A%9F.png) ![输入图片说明](Image/BasicTarget/Success.png) 基础工程简介: 基础工程包括LCD驱动程序、SD卡驱动程序以及FATFS程序,工程架构如下: 以下是图片中工程的文件树: ```MarkDown ├── 00_LCD_DISPLAY_BASIC │ ├── components //硬件驱动库(nofredo会放置在这个文件夹下) │ │ └── BSP │ │ ├── LCD //LCD驱动组件,驱动ST7789,你也可以换成你自己的驱动程序 │ │ │ ├── lcd.c │ │ │ ├── lcd.h │ │ │ └── lcdfont.h │ │ └── SDIO //SDIO驱动库,包含驱动SDIO的驱动程序 │ │ ├── exfuns.c │ │ ├── exfuns.h │ │ ├── spi_sdcard.c │ │ └── spi_sdcard.h │ │ └── SPI //SPI驱动库,注册SPI │ ├── CMakeLists.txt //CMake 包含了对driver/gpio的依赖和对fatfs的依赖 │ └── main //主程序 │ ├── APP │ ├── CMakeLists.txt │ └── main.c //主程序函数 测试SD和显示屏 │ ├── CMakeLists.txt │ ├── partitions-16MiB.csv //分区表 │ ├── README.md │ └── sdkconfig ``` ###### 2.工程基础框架 复制一份基础工程文件,复制一份nofredo的程序: 复制基础工程,改名00_NorFedo ![输入图片说明](Image/BasicTarget/%E5%87%86%E5%A4%87%E5%9F%BA%E7%A1%80%E5%B7%A5%E7%A8%8B.png) 先解压esp32-nesemu-master(norfedo),得到以下文件: ![输入图片说明](Image/BasicTarget/Norfedo_Main.png) 文件整理: 首先将nofredo中components文件夹下的文件放置到我们的工程文件下 ![输入图片说明](Image/BasicTarget/Manage1.png) 正确后.... ![输入图片说明](Image/BasicTarget/Manage2.png) 其次还有main文件,这个main文件我们先保留,后续参考 其他的文件是ESP32的配置文件,包括分区表的配置文件,这里将这些文件先保留(不放到工程中)后续参考 检查无误后,通过VSCode打开工程 注意: 我们发现复制过来的文件中还有一个些多余的文件,这些多余的文件我们先不管(因为我是一边写文章一边记录移植的,这里我也不知道能不能直接删) ###### 2.为新增的工程文件编写CMake文件 #### 五、驱动层适配 前面我们分析了驱动层,那么现在就来对驱动层进行适配 ![输入图片说明](Image/DriverCompair/Driver.png) ##### 1. spi_lcd 这里是SPILCD的驱动,我们不使用ILI9341的屏幕,这里我们希望可以使用用户的任意一块屏幕(但是分辨率应该是320X240),比如ST7789 ###### 1.1 通过条件编译管理驱动库的思想 这里我们使用一个宏定义的方式,如果宏定义未激活,表示使用默认的驱动库,如果宏定义激活,使用我们自己的驱动库 ```C #define USE_USER_LCD_DRIVER 1 //使用用户的驱动库 : 1 #if USE_USER_LCD_DRIVER == 0 //使用原来的驱动库 #else //使用用户的驱动库 #endif ``` ###### 1.2通过条件编译管理原先的驱动库实现 打开spi_lcd.c 处理原先的头文件引用 ```c #include #include #include "sdkconfig.h" #if USE_USER_LCD_DRIVER == 0 #include "rom/ets_sys.h" #include "rom/gpio.h" #include "soc/gpio_reg.h" #include "soc/gpio_sig_map.h" #include "soc/gpio_struct.h" #include "soc/io_mux_reg.h" #include "soc/spi_reg.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/periph_ctrl.h" #include "spi_lcd.h" #else //引用用户的LCD驱动头文件 #endif ``` 我们在这里加入我们的头文件————lcd.h ```c #include #include #include "sdkconfig.h" #if USE_USER_LCD_DRIVER == 0 #include "rom/ets_sys.h" #include "rom/gpio.h" #include "soc/gpio_reg.h" #include "soc/gpio_sig_map.h" #include "soc/gpio_struct.h" #include "soc/io_mux_reg.h" #include "soc/spi_reg.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/periph_ctrl.h" #include "spi_lcd.h" #else //引用用户的LCD驱动头文件 #include "lcd.h" //引入LCD头文件 #endif ``` 用同样的条件编译处理原先的ILI9341的驱动库: (代码比较长,我这里就不列举了) 后面添加我们的驱动程序: ```c #if USE_USER_LCD_DRIVER == 0 //官方的驱动库,这里我就不举例了.... #else //使用用户的驱动库 //这里适配你自己的驱动程序...... #endif ``` ###### 1.3LCD驱动适配 首先要做的第一件事!!!检查你是否引入了你自己的驱动的头文件,将头文件放置到SPI_LCD.c文件的头文件引用区域!!! ![输入图片说明](Image/DriverCompair/HeadAdd.png) 下面进行驱动适配,打开spi_lcd.h,查看有哪些函数: ```c void ili9341_write_frame(const uint16_t x, const uint16_t y, const uint16_t width, const uint16_t height, const uint8_t *data[]); void ili9341_init(); ``` 可以看到,这里适配两个函数,其中一个初始化LCD,另一个向LCD写数据 ######1.4 LCD初始化适配 先来看初始化程序: ```c void ili9341_init() { spi_master_init(); //注册LCDSPI ili_gpio_init(); //初始化LCDGPIO ILI9341_INITIAL (); //发送LCD初始化序列 } ``` 这里我们可以看到,程序分别进行了SPI注册,GPIO注册,发送初始化序列,这些东西我们的一个LCD_Init就搞定了 (你的LCD初始化程序不一定把这些功能一次到位的完成注册,那么你可以按照原驱动程序的思路进行注册) ```c /** * @brief LCD初始化 * @param 无 * @retval 无 */ void lcd_init(void) { int cmd = 0; esp_err_t ret = 0; lcd_self.dir = 0; lcd_self.wr = LCD_NUM_WR; /* 配置WR引脚 */ lcd_self.cs = LCD_NUM_CS; /* 配置CS引脚 */ gpio_config_t gpio_init_struct; /* SPI驱动接口配置 */ spi_device_interface_config_t devcfg = { .clock_speed_hz = 60 * 1000 * 1000, /* SPI时钟 */ .mode = 0, /* SPI模式0 */ .spics_io_num = lcd_self.cs, /* SPI设备引脚 */ .queue_size = 7, /* 事务队列尺寸 7个 */ }; /* 添加SPI总线设备 */ ret = spi_bus_add_device(SPI2_HOST, &devcfg, &MY_LCD_Handle); /* 配置SPI总线设备 */ ESP_ERROR_CHECK(ret); //配置普通GPIO引脚 //配置WR引脚 gpio_init_struct.intr_type = GPIO_INTR_DISABLE; /* 失能引脚中断 */ gpio_init_struct.mode = GPIO_MODE_OUTPUT; /* 配置输出模式 */ gpio_init_struct.pin_bit_mask = 1ull << lcd_self.wr; /* 配置引脚位掩码 */ gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; /* 失能下拉 */ gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; /* 使能下拉 */ gpio_config(&gpio_init_struct); /* 引脚配置 */ //配置复位引脚 gpio_init_struct.intr_type = GPIO_INTR_DISABLE; /* 失能引脚中断 */ gpio_init_struct.mode = GPIO_MODE_OUTPUT; /* 配置输出模式 */ gpio_init_struct.pin_bit_mask = 1ull << LCD_NUM_RST; /* 配置引脚位掩码 */ gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; /* 失能下拉 */ gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; /* 使能下拉 */ gpio_config(&gpio_init_struct); // lcd_hard_reset(); /* LCD硬件复位 */ /* 初始化代码 */ #if SPI_LCD_TYPE /* 对2.4寸LCD寄存器进行设置 */ lcd_init_cmd_t ili_init_cmds[] = { {0x11, {0}, 0x80}, {0x36, {0x00}, 1}, {0x3A, {0x65}, 1}, {0X21, {0}, 0x80}, {0x29, {0}, 0x80}, {0, {0}, 0xff}, }; #else /* 不为0则视为使用1.3寸SPILCD屏,那么屏幕将不会反显 */ lcd_init_cmd_t ili_init_cmds[] = { {0x11, {0}, 0x80}, {0x36, {0x00}, 1}, {0x3A, {0x65}, 1}, {0xB2, {0x0C, 0x0C, 0x00, 0x33,0x33}, 5}, {0xB7, {0x75}, 1}, {0xBB, {0x1C}, 1}, {0xC0, {0x2c}, 1}, {0xC2, {0x01}, 1}, {0xC3, {0x0F}, 1}, {0xC4, {0x20}, 1}, {0xC6, {0X01}, 1}, {0xD0, {0xA4,0xA1}, 2}, {0xE0, {0xD0, 0x04, 0x0D, 0x11, 0x13, 0x2B, 0x3F, 0x54, 0x4C, 0x18, 0x0D, 0x0B, 0x1F, 0x23}, 14}, {0xE1, {0xD0, 0x04, 0x0C, 0x11, 0x13, 0x2C, 0x3F, 0x44, 0x51, 0x2F, 0x1F, 0x1F, 0x20, 0x23}, 14}, {0X21, {0}, 0x80}, {0x29, {0}, 0x80}, {0, {0}, 0xff}, }; #endif /* 循环发送设置所有寄存器 */ while (ili_init_cmds[cmd].databytes != 0xff) { lcd_write_cmd(ili_init_cmds[cmd].cmd); lcd_write_data(ili_init_cmds[cmd].data, ili_init_cmds[cmd].databytes & 0x1F); if (ili_init_cmds[cmd].databytes & 0x80) { vTaskDelay(120); } cmd++; } lcd_display_dir(1); /* 设置屏幕方向 */ LCD_PWR(1); lcd_clear(WHITE); /* 清屏 */ } ``` 所以哈,直接在lil9341_Init中添加我们的LCD初始化程序,如下 ``` void ili9341_init() { lcd_init(); //你自己的LCD初始化 } ``` ###### 1.4LCD刷屏程序适配 先来研究一下原先的刷屏程序是如何工作的: ```c void ili9341_write_frame(const uint16_t xs, const uint16_t ys, const uint16_t width, const uint16_t height, const uint8_t * data[]) { int x, y; int i; uint16_t x1, y1; uint32_t xv, yv, dc; uint32_t temp[16]; dc = (1 << PIN_NUM_DC); // 设置DC引脚的掩码,用于区分命令和数据 for (y = 0; y < height; y++) { // 遍历每一行 // 设置起始行和结束行 x1 = xs + (width - 1); y1 = ys + y + (height - 1); xv = U16x2toU32(xs, x1); // 将起始列和结束列合并为32位数据 yv = U16x2toU32((ys + y), y1); // 将起始行和结束行合并为32位数据 // 等待SPI空闲 while (READ_PERI_REG(SPI_CMD_REG(SPI_NUM)) & SPI_USR); // 设置DC为低电平,表示接下来发送的是命令 GPIO.out_w1tc = dc; // 设置SPI数据长度为7位 SET_PERI_REG_BITS(SPI_MOSI_DLEN_REG(SPI_NUM), SPI_USR_MOSI_DBITLEN, 7, SPI_USR_MOSI_DBITLEN_S); // 写入列地址设置命令0x2A WRITE_PERI_REG((SPI_W0_REG(SPI_NUM)), 0x2A); // 触发SPI传输 SET_PERI_REG_MASK(SPI_CMD_REG(SPI_NUM), SPI_USR); // 等待SPI传输完成 while (READ_PERI_REG(SPI_CMD_REG(SPI_NUM)) & SPI_USR); // 设置DC为高电平,表示接下来发送的是数据 GPIO.out_w1ts = dc; // 设置SPI数据长度为31位 SET_PERI_REG_BITS(SPI_MOSI_DLEN_REG(SPI_NUM), SPI_USR_MOSI_DBITLEN, 31, SPI_USR_MOSI_DBITLEN_S); // 写入列地址数据 WRITE_PERI_REG((SPI_W0_REG(SPI_NUM)), xv); // 触发SPI传输 SET_PERI_REG_MASK(SPI_CMD_REG(SPI_NUM), SPI_USR); // 等待SPI传输完成 while (READ_PERI_REG(SPI_CMD_REG(SPI_NUM)) & SPI_USR); // 设置DC为低电平,表示接下来发送的是命令 GPIO.out_w1tc = dc; // 设置SPI数据长度为7位 SET_PERI_REG_BITS(SPI_MOSI_DLEN_REG(SPI_NUM), SPI_USR_MOSI_DBITLEN, 7, SPI_USR_MOSI_DBITLEN_S); // 写入行地址设置命令0x2B WRITE_PERI_REG((SPI_W0_REG(SPI_NUM)), 0x2B); // 触发SPI传输 SET_PERI_REG_MASK(SPI_CMD_REG(SPI_NUM), SPI_USR); // 等待SPI传输完成 while (READ_PERI_REG(SPI_CMD_REG(SPI_NUM)) & SPI_USR); // 设置DC为高电平,表示接下来发送的是数据 GPIO.out_w1ts = dc; // 设置SPI数据长度为31位 SET_PERI_REG_BITS(SPI_MOSI_DLEN_REG(SPI_NUM), SPI_USR_MOSI_DBITLEN, 31, SPI_USR_MOSI_DBITLEN_S); // 写入行地址数据 WRITE_PERI_REG((SPI_W0_REG(SPI_NUM)), yv); // 触发SPI传输 SET_PERI_REG_MASK(SPI_CMD_REG(SPI_NUM), SPI_USR); // 等待SPI传输完成 while (READ_PERI_REG(SPI_CMD_REG(SPI_NUM)) & SPI_USR); // 设置DC为低电平,表示接下来发送的是命令 GPIO.out_w1tc = dc; // 设置SPI数据长度为7位 SET_PERI_REG_BITS(SPI_MOSI_DLEN_REG(SPI_NUM), SPI_USR_MOSI_DBITLEN, 7, SPI_USR_MOSI_DBITLEN_S); // 写入内存写入命令0x2C WRITE_PERI_REG((SPI_W0_REG(SPI_NUM)), 0x2C); // 触发SPI传输 SET_PERI_REG_MASK(SPI_CMD_REG(SPI_NUM), SPI_USR); // 等待SPI传输完成 while (READ_PERI_REG(SPI_CMD_REG(SPI_NUM)) & SPI_USR); x = 0; // 设置DC为高电平,表示接下来发送的是数据 GPIO.out_w1ts = dc; // 设置SPI数据长度为511位 SET_PERI_REG_BITS(SPI_MOSI_DLEN_REG(SPI_NUM), SPI_USR_MOSI_DBITLEN, 511, SPI_USR_MOSI_DBITLEN_S); while (x < width) { // 遍历每一列 for (i = 0; i < 16; i++) { // 每次处理16个像素 if (data == NULL) { // 如果没有数据,填充黑色 temp[i] = 0; x += 2; continue; } // 从调色板中获取颜色值 x1 = myPalette[(unsigned char)(data[y][x])]; x++; y1 = myPalette[(unsigned char)(data[y][x])]; x++; // 将两个颜色值合并为32位数据 temp[i] = U16x2toU32(x1, y1); } // 等待SPI空闲 while (READ_PERI_REG(SPI_CMD_REG(SPI_NUM)) & SPI_USR); // 写入像素数据 for (i = 0; i < 16; i++) { WRITE_PERI_REG((SPI_W0_REG(SPI_NUM) + (i << 2)), temp[i]); } // 触发SPI传输 SET_PERI_REG_MASK(SPI_CMD_REG(SPI_NUM), SPI_USR); } } // 等待SPI传输完成 while (READ_PERI_REG(SPI_CMD_REG(SPI_NUM)) & SPI_USR); } ``` 程序的执行流程是遍历刷屏区域的行,设置窗口,将每一行的部分内容复制到缓冲中,再依次刷到屏幕上 我们要适配的程序会直接设置整个大窗口,然后通过WriteGRAM的函数向屏幕发送数据,直到窗口填充完毕 先来看设置窗口的函数吧,这个函数属实来说有点长,我这里就不列举了,简单的讲一下程序的执行原理(我遇见的SPILCD基本上都是这个逻辑) ![输入图片说明](Image/DriverCompair/LCD_WriteADDR.png) 那么这里我们直接使用我们自己的屏幕设置窗口的程序进行适配,以下是源代码(请参考你自己的LCD源代码): ```C /** * @brief 设置窗口大小 * @param xstar:左上角x轴 * @param ystar:左上角y轴 * @param xend:右下角x轴 * @param yend:右下角y轴 * @retval 无 */ void lcd_set_window(uint16_t xstar, uint16_t ystar,uint16_t xend,uint16_t yend) { uint8_t databuf[4] = {0,0,0,0}; databuf[0] = xstar >> 8; databuf[1] = 0xFF & xstar; databuf[2] = xend >> 8; databuf[3] = 0xFF & xend; lcd_write_cmd(lcd_self.setxcmd); lcd_write_data(databuf,4); databuf[0] = ystar >> 8; databuf[1] = 0xFF & ystar; databuf[2] = yend >> 8; databuf[3] = 0xFF & yend; lcd_write_cmd(lcd_self.setycmd); lcd_write_data(databuf,4); lcd_write_cmd(lcd_self.wramcmd); /* 开始写入GRAM */ } ``` 1.计算结束坐标 正常来说,设置窗口的时候都需要在结束坐标的基础上-1,我们设置坐标的程序里面没有自动-1,因此我们在适配这个程序的时候需要进行-1,如下 ```c uint16_t x1, y1; //结束的X Y坐标 x1 = xs+(width-1); y1 = ys+(height-1); //计算结束坐标(已经进行-1) lcd_set_window(xs,ys,x1,yq); //设置大窗口 ``` 接着就是刷入内容到显示屏,以下是几种常见的刷屏方式: 1.依次设置点坐标,再刷入点显示的内容 这是最慢的刷屏方式,因为我们的显示屏每刷入一个像素都需要对像素点寻址(重新发一次坐标信息) 2.设置大窗口,依次调用WriteRAM刷入数据 这个方式还不够快,因为每次发送数据都需要调用一次SPI启动程序进行,包括对GPIO的操作(这些反复调用都需要算作时间的成本) 3.使用缓冲刷屏 将要刷入的内容保存在一个小缓冲区域里面,循环刷入内容,这个方式是我目前使用下来的最快的方式 此程序中我们先计算要刷入的内容大小,将要刷入的指针先保存到缓冲中,再从缓冲一次性刷入屏幕中,这个做法可以减少刷屏程序调用的次数,加快刷屏速度 1.刷屏缓冲定义 刷屏缓冲定义在我自己的屏幕驱动程序中,其他的驱动程序依据你的方式定义就行 缓冲导出再lcd.h中 ```c #define LCD_TOTAL_BUF_SIZE (320 * 240 * 2) #define LCD_BUF_SIZE 15360 /* 导出相关变量 */ extern lcd_obj_t lcd_self; extern uint8_t lcd_buf[LCD_BUF_SIZE]; ``` 真正的定义在lcd.c中 ```c uint8_t lcd_buf[LCD_BUF_SIZE]; ``` 2.刷屏程序基础变量定义 定义这些基础变量,如下 ```c uint32_t i = 0; //按顺序扫描文件指针的指针 uint32_t total_size = 0 , size = 0 , size_remain = 0; //刷入数据总大小 本次刷入数据大小 胜屿数据量 uint32_t image_mask = 0; //图像指针块位置 ``` 接着计算需要刷入的窗口大小,计算公式:长x宽x2(一个像素十六位,两个字节) 计算后判断缓冲空间能否刷完,如果能刷完则一次刷入,否则分批次刷入数据。 计算大小结束后,清除图像指针的位置,至此缓冲区准备完毕,即将开始填充数据 ``` //绘图区域没有超过上屏的最大画图区域,可以直接刷入屏幕,因此直接刷入 total_size = width * height * 2; //计算图片大小 image_mask = 0; //清除图像指针位置 //判断是不是要分批次刷入 if(total_size > LCD_BUF_SIZE) { size_remain = total_size - LCD_BUF_SIZE; //剩余要写入的大小 size = LCD_BUF_SIZE; //本次要写入的大小,不得超过最大区域 } else { size_remain = 0; //标记没有剩余的内存需要写 size = total_size; //要刷入的内容可以直接写入缓冲 } lcd_set_window(xs,ys,x1,y1); //设置大窗口 image_mask = 0; //清除指针位置 ``` 数据填充: 因为FC游戏机输入的色彩数据类型是八位数据,需要通过查找颜色表的方式进行颜色查找,查表后得到一个RGB565的颜色 ```c rgb565_color = myPalette[(unsigned char)(data[image_mask])];//通过查表法获得一个RGB565颜色 ``` 得到颜色后,将一个十六位颜色拆分成高低八位,保存到显存中 之后程序循环判断是否写完,如果写完就退出 若未能写完,则分次数写入机器,一次以最大缓冲量写入,直到写完最后的数据为止。 ```c while(1) { //一次移动两个八位,相当于移动一个像素 for (i = 0; i < size; i = i+2) { //首先取出一次颜色(8Bit -> RGB565) rgb565_color = myPalette[(unsigned char)(data[image_mask])];//通过查表法获得一个RGB565颜色 //复制颜色的时候,加上指针位置 //这里后续可以对这个颜色表进行拆分,减少计算量 lcd_buf[i] = (uint8_t)(rgb565_color >> 8); lcd_buf[i + 1] = (uint8_t)(rgb565_color & 0x0F); image_mask = image_mask + 1;//图像地址自加 } //image_mask = image_mask + size; //记录下现在的位置 //image_mask一直处于自加状态,这里我们就不用管他了 lcd_write_data(lcd_buf, size,LCD1_SELECT); if (size_remain == 0) //如果要写入的部分都写完了,退出 break; if (size_remain > LCD_BUF_SIZE) //还有要写入的东西 { //超过LCD BUF SIZE的情况下,每一次都可以写入,因此我们只需要减去一定量即可 size_remain = size_remain - LCD_BUF_SIZE; size = LCD_BUF_SIZE;//继续写入相应的内容 } else { size = size_remain; size_remain = 0; } } ``` 完整程序如下: ```c void ili9341_write_frame(const uint16_t xs, const uint16_t ys, const uint16_t width, const uint16_t height, const uint8_t * data[]) { //首先适配你自己的LCD的设置窗口函数 uint16_t x1, y1; //结束的X Y坐标 //跟刷屏缓冲有关 uint32_t i = 0; //按顺序扫描文件指针的指针 uint32_t total_size = 0 , size = 0 , size_remain = 0; //刷入数据总大小 本次刷入数据大小 胜屿数据量 uint32_t image_mask = 0; //图像指针位置 //总图像指针 //颜色缓冲 /** * 因为输入的颜色是8位颜色,因此需要通过查颜色表的方式获取现在像素的颜色 */ uint16_t rgb565_color = 0; //RGB565颜色 x1 = xs+(width-1); y1 = ys+(height-1); //计算结束坐标(已经进行-1) //绘图区域没有超过上屏的最大画图区域,可以直接刷入屏幕,因此直接刷入 total_size = width * height * 2; //计算图片大小 image_mask = 0; //清除图像指针位置 //判断是不是要分批次刷入 if(total_size > LCD_BUF_SIZE) { size_remain = total_size - LCD_BUF_SIZE; //剩余要写入的大小 size = LCD_BUF_SIZE; //本次要写入的大小,不得超过最大区域 } else { size_remain = 0; //标记没有剩余的内存需要写 size = total_size; //要刷入的内容可以直接写入缓冲 } lcd_set_window(xs,ys,x1,y1); //设置大窗口 image_mask = 0; //清除指针位置 while(1) { //一次移动两个八位,相当于移动一个像素 for (i = 0; i < size; i = i+2) { //首先取出一次颜色(8Bit -> RGB565) rgb565_color = myPalette[(unsigned char)(data[image_mask])];//通过查表法获得一个RGB565颜色 //复制颜色的时候,加上指针位置 //这里后续可以对这个颜色表进行拆分,减少计算量 lcd_buf[i] = (uint8_t)(rgb565_color >> 8); lcd_buf[i + 1] = (uint8_t)(rgb565_color & 0x0F); image_mask = image_mask + 1;//图像地址自加 } //image_mask = image_mask + size; //记录下现在的位置 //image_mask一直处于自加状态,这里我们就不用管他了 lcd_write_data(lcd_buf, size,LCD1_SELECT); if (size_remain == 0) //如果要写入的部分都写完了,退出 break; if (size_remain > LCD_BUF_SIZE) //还有要写入的东西 { //超过LCD BUF SIZE的情况下,每一次都可以写入,因此我们只需要减去一定量即可 size_remain = size_remain - LCD_BUF_SIZE; size = LCD_BUF_SIZE;//继续写入相应的内容 } else { size = size_remain; size_remain = 0; } } //------------------------------------------------------------ } ``` **LCD的代码就先适配到这里,因为工程暂未完成,后续还会跟进Debug,这里就先不赘述了** ##### 2.音视频及按键代码适配 打开Video_Audio,即将开始适配这部分的程序 ![输入图片说明](Image/DriverCompair/Video_Audio.png) ###### 2.1代码结构解析 做移植适配工作之前,应该先来解析一下代码的结构,清楚我们要做对哪些位置进行适配 **1.定时器创建** 此函数的功能用于创建一个定时器,定时调用函数,我们先不去管他(除非编译出现错误) ```c int osd_installtimer(int frequency, void *func, int funcsize, void *counter, int countersize) ``` **2.Audio部分** 先来看第一部分,这里声明了一个音频回调函数,这些我们都不需要动 ```c /* ** Audio */ static void (*audio_callback)(void *buffer, int length) = NULL; #if CONFIG_SOUND_ENA QueueHandle_t queue; static uint16_t *audio_frame; #endif ``` 这里是处理I2S发送音频的函数,唯一我们可能要动的就是I2S写数据的这行代码(可能要改成适配ESP32的代码) ```c static void do_audio_frame() { #if CONFIG_SOUND_ENA int left=DEFAULT_SAMPLERATE/NES_REFRESH_RATE; while(left) { int n=DEFAULT_FRAGSIZE; if (n>left) n=left; audio_callback(audio_frame, n); //get more data //16 bit mono -> 32-bit (16 bit r+l) for (int i=n-1; i>=0; i--) { audio_frame[i*2+1]=audio_frame[i]; audio_frame[i*2]=audio_frame[i]; } i2s_write_bytes(0, audio_frame, 4*n, portMAX_DELAY); left-=n; } #endif } ``` 接下来是设置声音和音频结束的回调函数,这里依据情况来吧,比如说音频结束后对于NS4168应该把使能引脚拉低(避免烧了扬声器) 我们暂时不去动 ```c void osd_setsound(void (*playfunc)(void *buffer, int length)) { //Indicates we should call playfunc() to get more data. audio_callback = playfunc; } static void osd_stopsound(void) { audio_callback = NULL; } ``` 接下来的地方比较重要,这部分设计音频初始化,对于I2S,我们可能要换掉原先的初始化方式,或者添加我们自己的音频芯片的一些初始化程序进去。 ```c static int osd_init_sound(void) { #if CONFIG_SOUND_ENA audio_frame=malloc(4*DEFAULT_FRAGSIZE); i2s_config_t cfg={ .mode=I2S_MODE_DAC_BUILT_IN|I2S_MODE_TX|I2S_MODE_MASTER, .sample_rate=DEFAULT_SAMPLERATE, .bits_per_sample=I2S_BITS_PER_SAMPLE_16BIT, .channel_format=I2S_CHANNEL_FMT_RIGHT_LEFT, .communication_format=I2S_COMM_FORMAT_I2S_MSB, .intr_alloc_flags=0, .dma_buf_count=4, .dma_buf_len=512 }; i2s_driver_install(0, &cfg, 4, &queue); i2s_set_pin(0, NULL); i2s_set_dac_mode(I2S_DAC_CHANNEL_LEFT_EN); //I2S enables *both* DAC channels; we only need DAC1. //ToDo: still needed now I2S supports set_dac_mode? CLEAR_PERI_REG_MASK(RTC_IO_PAD_DAC1_REG, RTC_IO_PDAC1_DAC_XPD_FORCE_M); CLEAR_PERI_REG_MASK(RTC_IO_PAD_DAC1_REG, RTC_IO_PDAC1_XPD_DAC_M); #endif audio_callback = NULL; return 0; } ``` 最后还有一个函数,这个函数用于处理并且获取当前音频播放的信息(采样率和速度),这个我们肯定不用动啦,但是需要注意的是,对于某些音频芯片使用之前需要进行设置采样率信息的,可以调用以下这个程序,获取当前的音频信息状态: ``` void osd_getsoundinfo(sndinfo_t *info) { info->sample_rate = DEFAULT_SAMPLERATE; info->bps = 16; } ``` **视频相关代码:** 接下来是视频部分,这部分包括显示内容的宏定义,调色板定义,调色板设置程序,清屏程序,写入数据程序,这里我们默认就好,暂时不考虑去动 ```c /* ** Video */ static int init(int width, int height); static void shutdown(void); static int set_mode(int width, int height); static void set_palette(rgb_t *pal); static void clear(uint8 color); static bitmap_t *lock_write(void); static void free_write(int num_dirties, rect_t *dirty_rects); static void custom_blit(bitmap_t *bmp, int num_dirties, rect_t *dirty_rects); static char fb[1]; //dummy QueueHandle_t vidQueue; viddriver_t sdlDriver = { "Simple DirectMedia Layer", /* name */ init, /* init */ shutdown, /* shutdown */ set_mode, /* set_mode */ et_palette, /* set_palette */ clear, /* clear */ lock_write, /* lock_write */ free_write, /* free_write */ custom_blit, /* custom_blit */ false /* invalidate flag */ }; bitmap_t *myBitmap; void osd_getvideoinfo(vidinfo_t *info) { info->default_width = DEFAULT_WIDTH; info->default_height = DEFAULT_HEIGHT; info->driver = &sdlDriver; } /* flip between full screen and windowed */ void osd_togglefullscreen(int code) { } /* initialise video */ static int init(int width, int height) { return 0; } static void shutdown(void) { } /* set a video mode */ static int set_mode(int width, int height) { return 0; } uint16 myPalette[256]; /* copy nes palette over to hardware */ static void set_palette(rgb_t *pal) { uint16 c; int i; for (i = 0; i < 256; i++) { c=(pal[i].b>>3)+((pal[i].g>>2)<<5)+((pal[i].r>>3)<<11); //myPalette[i]=(c>>8)|((c&0xff)<<8); myPalette[i]=c; } } /* clear all frames to a particular color */ static void clear(uint8 color) { // SDL_FillRect(mySurface, 0, color); } /* acquire the directbuffer for writing */ static bitmap_t *lock_write(void) { // SDL_LockSurface(mySurface); myBitmap = bmp_createhw((uint8*)fb, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_WIDTH*2); return myBitmap; } /* release the resource */ static void free_write(int num_dirties, rect_t *dirty_rects) { bmp_destroy(&myBitmap); } static void custom_blit(bitmap_t *bmp, int num_dirties, rect_t *dirty_rects) { xQueueSend(vidQueue, &bmp, 0); do_audio_frame(); } //This runs on core 1. static void videoTask(void *arg) { int x, y; bitmap_t *bmp=NULL; x = (320-DEFAULT_WIDTH)/2; y = ((240-DEFAULT_HEIGHT)/2); while(1) { // xQueueReceive(vidQueue, &bmp, portMAX_DELAY);//skip one frame to drop to 30 xQueueReceive(vidQueue, &bmp, portMAX_DELAY); ili9341_write_frame(x, y, DEFAULT_WIDTH, DEFAULT_HEIGHT, (const uint8_t **)bmp->line); } } ``` **4.手柄相关代码** 接下来有一个很重要的函数,这个函数涉及到手柄输入程序。 **手柄初始化:** 这部分主要就是初始化手柄或者按键,放入你自己的初始化程序即可,你使用的可能是同程序里面的PSX手柄,或者某种USB手柄/键盘,或者按键(我的方式就是使用按键)或者FC手柄,触摸屏……无关紧要,只需要在这里放置你的手柄初始化程序即可 ```c static void osd_initinput() { psxcontrollerInit(); } ``` **手柄按键扫描函数:** 扫描手柄按下的按键,这里使用的方式是轮询扫描,如果扫描到按键则返回相应的键值,如果扫描不到则返回没有按键 ```c void osd_getinput(void) { const int ev[16]={ event_joypad1_select,0,0,event_joypad1_start,event_joypad1_up,event_joypad1_right,event_joypad1_down,event_joypad1_left, 0,0,0,0,event_soft_reset,event_joypad1_a,event_joypad1_b,event_hard_reset }; static int oldb=0xffff; int b=psxReadInput(); int chg=b^oldb; int x; oldb=b; event_t evh; // printf("Input: %x\n", b); for (x=0; x<16; x++) { if (chg&1) { evh=event_get(ev[x]); if (evh) evh((b&1)?INP_STATE_BREAK:INP_STATE_MAKE); } chg>>=1; b>>=1; } } ``` 按键释放函数??不明白,先不去管他 看了后面的的程序,明白了,这行代码用于释放按键(或者说让按键进入休眠模式) ```c static void osd_freeinput(void) { } ``` 获取鼠标坐标函数,这里我们用不上,触屏和鼠标可能用的上,估计是用来支持光枪游戏的 ```c void osd_getmouse(int *x, int *y, int *button) { } ``` **6.硬件层关闭函数** 硬件层关闭函数,执行后硬件层释放(包括释放音频和按键),根据需求添加我们的代码吧,这里暂时不用去管 ``` void osd_shutdown() { osd_stopsound(); osd_freeinput(); } ``` **7.打印日志函数** 打印产生的日志,没啥好说的,调用printf,这里我们也没必要去管它 ```c static int logprint(const char *string) { return printf("%s", string); } ``` **8.硬件层启动(初始化)** 硬件层启动,启动顺序:产生日志->声音初始化(若声音初始化失败返回1)->显示初始化->显示屏初始化->显示屏清屏->定时器初始化->按键初始化 ```c /* ** Startup */ int osd_init() { log_chain_logfunc(logprint); if (osd_init_sound()) return -1; ili9341_init(); ili9341_write_frame(0,0,320,240,NULL); vidQueue=xQueueCreate(1, sizeof(bitmap_t *)); xTaskCreatePinnedToCore(&videoTask, "videoTask", 2048, NULL, 5, NULL, 1); osd_initinput(); return 0; } ``` 最后来捋一下思路,看看那些地方需要我们修改 ![输入图片说明](Image/DriverCompair/NeedToChange.png) ###### 2.2定时器代码适配 这里调用的是xTimerCreate,调用的是FreeRTOS的Timer代码,经过查询,这段代码具备互通性,因此我们不需要进行修改,这里直接使用源代码就行 ```c //Seemingly, this will be called only once. Should call func with a freq of frequency, int osd_installtimer(int frequency, void *func, int funcsize, void *counter, int countersize) { printf("Timer install, freq=%d\n", frequency); timer=xTimerCreate("nes",configTICK_RATE_HZ/frequency, pdTRUE, NULL, func); xTimerStart(timer, 0); return 0; } ``` ###### 2.3 Audio部分适配 首先说明,如果不需要使用音频功能,修改宏定义后就不会进行条件编译: ```c #if CONFIG_SOUND_ENA ``` 首先检查音频信息,音频格式定义C文件开头的位置: ```c //这里是音频采样率的信息块和默认数据量的信息块,此数据量需要x4 #define DEFAULT_SAMPLERATE 22100 #define DEFAULT_FRAGSIZE 128 ``` 这部分我们自然是不需要去修改,就这样放着就好 ```C #define DEFAULT_SAMPLERATE 22100 #define DEFAULT_FRAGSIZE 128 ``` 接下来是写音频数据的程序 ```c static void do_audio_frame() { #if CONFIG_SOUND_ENA int left=DEFAULT_SAMPLERATE/NES_REFRESH_RATE; while(left) { int n=DEFAULT_FRAGSIZE; if (n>left) n=left; audio_callback(audio_frame, n); //get more data //16 bit mono -> 32-bit (16 bit r+l) for (int i=n-1; i>=0; i--) { audio_frame[i*2+1]=audio_frame[i]; audio_frame[i*2]=audio_frame[i]; } i2s_write_bytes(0, audio_frame, 4*n, portMAX_DELAY); left-=n; } #endif } ``` 这里有一个简单的算法将单声道转换成双声道数据,转换后送入I2S进行播放 ```c for (int i=n-1; i>=0; i--) { audio_frame[i*2+1]=audio_frame[i]; audio_frame[i*2]=audio_frame[i]; } ``` 检查了以下,这里有一些API并不互通,那么就需要我们这里我需要引入我们的I2S写数据程序 将I2S驱动文件夹拷贝到BSP内,里面包含了i2s.c,i2s.h 程序的作用:初始化指定I2S,对I2S启动,停止,发送音频和接受音频的API进行封装 由于篇幅限制,这里就不对这个API进行解析了,我们用到哪些内容就讲解哪些内容。 记得在video_audio中引入对I2S的支持,在Components》BSP>CMakeLists中加入对I2S的include ![输入图片说明](Image/DriverCompair/I2SRequest.png) 首先是I2S_Write,用于替换原有的i2s_write_bytes,这个函数的原型是: 函数的作用是调用ESP32提供的API,进行传输数据,并且对I2S设备进行了封装,这样外部程序调用时,就可以避免与I2S硬件的直接接触,避免了硬件句柄在多API之间调用可能出现的问题 ```c /** * @brief I2S传输数据 * @param buffer: 数据存储区的首地址 * @param frame_size: 数据大小 * @retval 无 */ size_t i2s_tx_write(uint8_t *buffer, uint32_t frame_size) { size_t bytes_written; i2s_write(I2S_NUM, buffer, frame_size, &bytes_written, 100); return bytes_written; } ``` 函数有两个参数,buffer是要写入的数据的指针,frame_size是数据流的大小 注释原来的代码,更改为我们的I2S代码 ```c //i2s_write_bytes(0, audio_frame, 4*n, portMAX_DELAY); i2s_tx_write(audio_frame,4*n); ``` 修改后的函数: ```c static void do_audio_frame() { #if CONFIG_SOUND_ENA int left=DEFAULT_SAMPLERATE/NES_REFRESH_RATE; while(left) { int n=DEFAULT_FRAGSIZE; if (n>left) n=left; audio_callback(audio_frame, n); //get more data //16 bit mono -> 32-bit (16 bit r+l) for (int i=n-1; i>=0; i--) { audio_frame[i*2+1]=audio_frame[i]; audio_frame[i*2]=audio_frame[i]; } //这里注释掉原来的代码,更改为我们的I2S代码 //i2s_write_bytes(0, audio_frame, 4*n, portMAX_DELAY); i2s_tx_write(audio_frame,4*n); left-=n; } #endif ``` 接下来处理I2S初始化函数,在CONFIG_SOUND_ENA的情况下,程序会完成音频缓冲内存申请和I2S的初始化。 首先是音频缓冲audio_frame的内存申请,这里申请了4*DEFAULT_FRAGSIZE(4*128)也就是512字节的音频,我们不需要去动他 ``` audio_frame=malloc(4*DEFAULT_FRAGSIZE); ``` 接下来来分析源代码的I2S配置: ```c i2s_config_t cfg={ .mode=I2S_MODE_DAC_BUILT_IN|I2S_MODE_TX|I2S_MODE_MASTER, //设置I2S模式 .sample_rate=DEFAULT_SAMPLERATE, //设置采样率 .bits_per_sample=I2S_BITS_PER_SAMPLE_16BIT, //设置位宽16比特 .channel_format=I2S_CHANNEL_FMT_RIGHT_LEFT, //通道格式(左右声道) .communication_format=I2S_COMM_FORMAT_I2S_MSB, //I2S数据格式 .intr_alloc_flags=0, .dma_buf_count=4, //和传输DMA通道有关 .dma_buf_len=512 }; i2s_driver_install(0, &cfg, 4, &queue); //下面这行代码可能和DAC输出有关 i2s_set_pin(0, NULL); i2s_set_dac_mode(I2S_DAC_CHANNEL_LEFT_EN); ``` 这里我们使用我们自己的I2S驱动程序(也可以使用原来的I2S驱动程序): ```c /** * @brief 初始化I2S * @param 无 * @retval ESP_OK:初始化成功;其他:失败 */ esp_err_t i2s_init(void) { esp_err_t ret_val = ESP_OK; //设置I2S设备引脚,这些引脚的定义请参考i2s.h i2s_pin_config_t pin_config = { .bck_io_num = I2S_BCK_IO, .ws_io_num = I2S_WS_IO, .data_out_num = I2S_DO_IO, .data_in_num = I2S_DI_IO, .mck_io_num = IS2_MCLK_IO, }; //配置I2S i2s_config_t i2s_config = I2S_CONFIG_DEFAULT(); i2s_config.sample_rate = SAMPLE_RATE; i2s_config.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT; i2s_config.use_apll = true; ret_val |= i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL); ret_val |= i2s_set_pin(I2S_NUM, &pin_config); ret_val |= i2s_zero_dma_buffer(I2S_NUM); return ret_val; } ``` 这里的I2S_CONFIG_DEFAULT()位于I2S.c,我们看一下: ``` /* I2S默认配置 */ #define I2S_CONFIG_DEFAULT() { \ .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_RX), \ //设置I2S模式,输入输出模式 .sample_rate = SAMPLE_RATE, \ //设置I2S采样率,采样率的宏定义参考i2s.h .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, \ //比特率,默认16比特 .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, \ //音频通道格式:左右声道 .communication_format = I2S_COMM_FORMAT_STAND_I2S, \ .intr_alloc_flags = 0, \ //dma buf设置,这里需要修改 .dma_buf_count = 8, \ .dma_buf_len = 256, \ .use_apll = false \ //不使用apll } ``` 进入i2s.h,这里我们创建三个define ```c #define I2S_SAMPLE_RATE (22100) /* 采样率 */ #define I2S_DMA_BUF_COUNT (4) /* DMA BUF COUNT */ #define I2S_DMA_BUF_LEN (512) /* DMA BUF LEN */ ``` 备注:我手上的DMA程序:DMA_BUF_COUNT是8,DMA_BUF_LEN是256,不确定与4 512是否兼容,这里先修改为兼容源程序的配置,后续在测试。 将Samplerate,buf count,buf len的宏定义放到配置中,完成后: ```c /* I2S默认配置 */ #define I2S_CONFIG_DEFAULT() { \ .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_RX), \ .sample_rate = I2S_SAMPLE_RATE, \ //注意这里使用了来自i2s.h中对默认采样率的宏定义 .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, \ .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, \ .communication_format = I2S_COMM_FORMAT_STAND_I2S, \ .intr_alloc_flags = 0, \ .dma_buf_count = I2S_DMA_BUF_COUNT, \ //注意这里使用了来自I2S.h中的共定义,定义了BUF Count和BUF Len .dma_buf_len = I2S_DMA_BUF_LEN, \ .use_apll = false \ } /** * @brief 初始化I2S * @param 无 * @retval ESP_OK:初始化成功;其他:失败 */ esp_err_t i2s_init(void) { esp_err_t ret_val = ESP_OK; i2s_pin_config_t pin_config = { .bck_io_num = I2S_BCK_IO, .ws_io_num = I2S_WS_IO, .data_out_num = I2S_DO_IO, .data_in_num = I2S_DI_IO, .mck_io_num = IS2_MCLK_IO, }; i2s_config_t i2s_config = I2S_CONFIG_DEFAULT(); i2s_config.sample_rate = I2S_SAMPLE_RATE; //注意这里使用了来自i2s.h中对默认采样率的宏定义 i2s_config.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT; i2s_config.use_apll = true; ret_val |= i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL); ret_val |= i2s_set_pin(I2S_NUM, &pin_config); ret_val |= i2s_zero_dma_buffer(I2S_NUM); return ret_val; } ``` 很好,其他位置都不需要动,将我们的I2S初始化程序引入到video_audio中: ```c static int osd_init_sound(void) { #if CONFIG_SOUND_ENA /* //源程序自带的I2S注册函数,这里我们不使用这个注册函数,使用我们自己的I2S注册函数 audio_frame=malloc(4*DEFAULT_FRAGSIZE); i2s_config_t cfg={ .mode=I2S_MODE_DAC_BUILT_IN|I2S_MODE_TX|I2S_MODE_MASTER, .sample_rate=DEFAULT_SAMPLERATE, .bits_per_sample=I2S_BITS_PER_SAMPLE_16BIT, .channel_format=I2S_CHANNEL_FMT_RIGHT_LEFT, .communication_format=I2S_COMM_FORMAT_I2S_MSB, .intr_alloc_flags=0, .dma_buf_count=4, .dma_buf_len=512 }; i2s_driver_install(0, &cfg, 4, &queue); i2s_set_pin(0, NULL); i2s_set_dac_mode(I2S_DAC_CHANNEL_LEFT_EN); //I2S enables *both* DAC channels; we only need DAC1. //ToDo: still needed now I2S supports set_dac_mode? CLEAR_PERI_REG_MASK(RTC_IO_PAD_DAC1_REG, RTC_IO_PDAC1_DAC_XPD_FORCE_M); CLEAR_PERI_REG_MASK(RTC_IO_PAD_DAC1_REG, RTC_IO_PDAC1_XPD_DAC_M); */ //调用我们的注册函数 i2s_init(); #endif audio_callback = NULL; return 0; } ``` ###### 2.4 Video部分适配 这部分暂时不需要我们去适配,跳过。。。。。 ###### 2.5 游戏控制器适配 游戏控制器是很重要的外设,它可以是按键,可以是外接手柄,也可以是鼠标。 1.按键初始化程序 这部分放置初始化按键的程序,放入你自己的初始化程序即可,没啥好说的... ```c 这里输入代码 ``` 2.按键扫描函数(获取按键事件函数) 首先映入眼帘的是关于按键事件的枚举 ```c const int ev[16] = { event_joypad1_select, 0, 0, event_joypad1_start, event_joypad1_up, event_joypad1_right, event_joypad1_down, event_joypad1_left, 0, 0, 0, 0, event_soft_reset, event_joypad1_a, event_joypad1_b, event_hard_reset }; // 定义了一个事件数组,包含 16 个事件函数指针或事件标识符 // 每个位置对应一个按键的事件处理函数或事件标识符 // 例如:event_joypad1_select 表示 SELECT 按键的事件,event_joypad1_a 表示 A 按键的事件 // 数组中有些位置为 0,表示这些按键没有对应的事件处理函数 ``` 打开位于nofredo - > event.h中,我们就可以找到这些枚举 ``` /* joypad 1 */ event_joypad1_a, event_joypad1_b, event_joypad1_start, event_joypad1_select, event_joypad1_up, event_joypad1_down, event_joypad1_left, event_joypad1_right, /* joypad 2 */ event_joypad2_a, event_joypad2_b, event_joypad2_start, event_joypad2_select, event_joypad2_up, event_joypad2_down, event_joypad2_left, event_joypad2_right, ``` 这里我选择不去动 接下来就是按键扫描的部分,如下: ```c static int oldb = 0xffff; // 定义一个静态变量 oldb,用于保存上一次按键状态 // 初始化为 0xffff(所有位为 1),表示初始状态没有按键被按下 int b = psxReadInput(); // 调用 psxReadInput() 函数读取当前的按键状态 // 返回值是一个整数,其中每一位代表一个按键的状态: // 位为 0 表示按键被按下,位为 1 表示按键未被按下 int chg = b ^ oldb; // 计算当前按键状态 b 和上一次按键状态 oldb 的异或值 // 异或操作的结果中,只有按键状态发生变化的位才会为 1 // 例如:如果某个按键从按下变为释放,或者从释放变为按下,对应的位会为 1 int x; oldb = b; // 将当前按键状态 b 保存到 oldb 中,用于下一次比较 event_t evh; // 定义一个事件处理函数指针 evh,用于调用按键事件处理函数 // printf("Input: %x\n", b); // 调试输出,打印当前按键状态 b 的值(注释掉了) for (x = 0; x < 16; x++) { // 遍历 16 个按键事件 if (chg & 1) { // 检查当前位是否发生了变化(chg 的最低位是否为 1) evh = event_get(ev[x]); // 获取当前按键的事件处理函数 // 如果 ev[x] 是有效的事件标识符,event_get() 返回对应的事件处理函数指针 // 如果 ev[x] 是 0(无事件),event_get() 返回 NULL if (evh) { // 如果获取到有效的事件处理函数 evh((b & 1) ? INP_STATE_BREAK : INP_STATE_MAKE); // 调用事件处理函数,传递按键状态: // 如果当前位为 1(按键未被按下),传递 INP_STATE_BREAK(按键释放) // 如果当前位为 0(按键被按下),传递 INP_STATE_MAKE(按键按下) } } // 右移 chg 和 b,处理下一个位 chg >>= 1; b >>= 1; } ``` 这里涉及到关于按键读取的函数,十六位表示十六个按键操作,如下: | bit0 | bit1 | bit2 | bit3 | bit4 | bit5 | bit6 | bit7 | |----------------------|-------|-------|---------------------|-------------------|---------------------|--------------------|--------------------| | event_joypad1_select | 0 | 0 | event_joypad1_start | ,event_joypad1_up | event_joypad1_right | event_joypad1_down | event_joypad1_left | | bit8 | bit9 | bit10 | bit11 | bit12 | bit13 | bit14 | bit15 | |-------|-------|-------|-------|-------------------|-----------------|-----------------|------------------| | 0 | 0 | 0 | 0 | ,event_soft_reset | event_joypad1_a | event_joypad1_b | event_hard_reset | 在这里我们只需要按照这个顺序写入按键位即可,这就意味着我们只需要修改读取按键的代码行,这里换成我们自己的函数: ```c int b=psxReadInput(); ``` 现阶段我们先测试能否正常移植+编译,这部分我们直接返回0xFFFF ```c //int b=psxReadInput(); //放入你自己的按键操作函数!! //在这里放下你的按键操作函数,如果没有,则请直接返回0xFFFF int b = 0xffff; //x先返回0xFFFF ``` 还有一个释放按键输入的函数,和一个鼠标函数,这里我们没用上,因此暂时不进行适配。 ```c static void osd_freeinput(void) { } void osd_getmouse(int *x, int *y, int *button) { } ``` ###### 2.5 硬件系统层 1.系统释放函数 这部分用于释放系统,保留 ```c /* this is at the bottom, to eliminate warnings */ void osd_shutdown() { osd_stopsound(); osd_freeinput(); } ``` 2.日志打印函数,保留 ```c static int logprint(const char *string) { return printf("%s", string); } ``` 3.系统启动函数,保留 ```c int osd_init() { log_chain_logfunc(logprint); if (osd_init_sound()) return -1; ili9341_init(); ili9341_write_frame(0,0,320,240,NULL); vidQueue=xQueueCreate(1, sizeof(bitmap_t *)); xTaskCreatePinnedToCore(&videoTask, "videoTask", 2048, NULL, 5, NULL, 1); osd_initinput(); return 0; } ``` 这里我们分析一下系统启动函数 这段代码应该是用于设置日志打印程序的 ```c log_chain_logfunc(logprint); ``` 初始化声音,这里要注意了,如果声音初始化失败,是会返回-1的 ```c if (osd_init_sound()) return -1; ``` 初始化屏幕并且清屏,这里清屏的指针是NULL,是否会造成溢出?? ```c ili9341_init(); ili9341_write_frame(0,0,320,240,NULL); ``` 创建视频任务: ``` vidQueue=xQueueCreate(1, sizeof(bitmap_t *)); xTaskCreatePinnedToCore(&videoTask, "videoTask", 2048, NULL, 5, NULL, 1); ``` 初始化输入设备: ``` osd_initinput(); ``` 最后返回0,成功 #### 六、第一次编译 ##### 6.1 工程整理 因为工程编译需要编写CMake文件,这里我新创建一个新的文件夹来放置两个NES主程序文件夹(nofrendo和nofrendo-esp32) ![输入图片说明](Image/FirstBuild/NesFolder.png) 接着就是编辑NES目录下的NES文件: ```CMake set(src_dirs NOFRENDO NOFRENDO-ESP32) set(include_dirs NOFRENDO NOFRENDO-ESP32) set(requires driver fatfs esp_timer) idf_component_register(SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs} REQUIRES ${requires}) component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format) ``` 给nofrendo文件夹创建CMake文件,保证编译正常: 首先是根目录: ![输入图片说明](Image/FirstBuild/nofredoCMake.png) CMake文件内的内容: ```c cmake_minimum_required(VERSION 3.16) project(nofrendo LANGUAGES C) # 设置 CMake 编译器和工具链 set(CMAKE_TOOLCHAIN_FILE "$ENV{IDF_PATH}/tools/cmake/toolchain-esp32s3.cmake" CACHE FILEPATH "Toolchain file") # 设置项目源文件和头文件路径 set(SOURCES bitmap.c config.c event.c gui_elem.c gui.c intro.c log.c memguard.c nofrendo.c pcx.c vid_drv.c ) # 添加子目录 add_subdirectory(cpu) add_subdirectory(libsnes) add_subdirectory(mappers) add_subdirectory(nes) add_subdirectory(sndhrdw) # 创建可执行文件 add_executable(nofrendo ${SOURCES}) # 包含头文件路径 target_include_directories(nofrendo PRIVATE ${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/cpu ${CMAKE_SOURCE_DIR}/libsnes ${CMAKE_SOURCE_DIR}/mappers ${CMAKE_SOURCE_DIR}/nes ${CMAKE_SOURCE_DIR}/sndhrdw ) # 链接 ESP-IDF 库 target_link_libraries(nofrendo PRIVATE c freertos driver esp_common esp_hw_support esp_rom esp_system log newlib soc xtensa ) ``` 然后给文件夹分别创建一个CMake文件,这个CMake文件将会引用文件夹内的源文件,这里以cpu文件夹为例,其他的文件夹雷同: ```CMake file(GLOB SOURCES "*.c") add_library(cpu STATIC ${SOURCES}) ``` 其他文件夹修改“CPU”为文件夹名字即可 接着给nofredo创建CMake文件: ```c cmake_minimum_required(VERSION 3.16) set(COMPONENT_NAME "nofrendo-esp32") # 设置源文件 set(SOURCES osd.c spi_lcd.c video_audio.c ) # 设置头文件路径 set(INCLUDE_DIRS . ) # 注册组件 idf_component_register( SOURCES ${SOURCES} INCLUDE_DIRS ${INCLUDE_DIRS} ) # 处理 Kconfig.projbuild idf_component_get_property(idf_path IDF_PATH) set(KCONFIG_PROJBUILD ${CMAKE_CURRENT_SOURCE_DIR}/Kconfig.projbuild) ``` 这样就算是完成了。 ##### 6.2 main.c 这里参考原来的main.c函数,在我们的main.c程序中添加nofrendo的启动函数调用: 首先就是在原来的基础上添加一些列的头文件include啦... ```c #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "nvs_flash.h" #include "spi.h" #include "lcd.h" #include "spi_sdcard.h" #include "math.h" #include "exfuns.h" #include "esp_wifi.h" #include "esp_system.h" #include "esp_event.h" #include "esp_event_loop.h" #include "nvs_flash.h" #include "driver/gpio.h" #include "nofrendo.h" #include "esp_partition.h" ``` 接着复制nofrendo的main.c代码过来: 前面是两个任务句柄申明的程序,主要是用于查找ROM ``` char *osd_getromdata() { char* romdata; const esp_partition_t* part; spi_flash_mmap_handle_t hrom; esp_err_t err; nvs_flash_init(); part=esp_partition_find_first(0x40, 1, NULL); if (part==0) printf("Couldn't find rom part!\n"); err=esp_partition_mmap(part, 0, 3*1024*1024, SPI_FLASH_MMAP_DATA, (const void**)&romdata, &hrom); if (err!=ESP_OK) printf("Couldn't map rom part!\n"); printf("Initialized. ROM@%p\n", romdata); return (char*)romdata; } esp_err_t event_handler(void *ctx, system_event_t *event) { return ESP_OK; } ``` 接着就是启动程序了: 值得一提的是:nofrendo并不是从SD卡查找文件的,而是从分区表查找文件的!! ``` /** * 创建nofrendo启动程序 * */ void nofrendo_NES_start(void) { printf("NoFrendo start!\n"); nofrendo_main(0, NULL); printf("NoFrendo died? WtF?\n"); asm("break.n 1"); return 0; } ``` 所以这里后续还要移植韦东山老师的代码。。。。。 (所以前面白忙活了吗.....) 最后就是喜闻乐见的编译环节了 出现了这些报错.... 算了吧重来吧...................................................... #### 七、再次开始韦东山NES移植 这部分的配置和准备流程,参考了CSDN:胖哥王老师的移植流程,这里深表感谢!!! //源文链接————单片机开发---ESP32S3移植NES模拟器(一):[单片机开发---ESP32S3移植NES模拟器(一)](https://blog.csdn.net/baidu_19348579/article/details/128860870) 在此对王老师深表感谢! 感谢韦老师提供的完整版代码,但是我真的不得不吐槽一下,韦东山的代码真的是太臃肿了,因此在保证移植的前提下,这里将会对韦东山老师的代码进行大量删减... /////////////////////////////////////////////////////////////////////////////////// ##### 7.1创建工程 创建基础工程,工程名为02_100ask_nes(源工程为00_lcd_display_Basic) ![输入图片说明](Image/FirstBuild/100ask_basic.png) ##### 7.2韦东山老师源代码获取,分析 ![输入图片说明](Image/FirstBuild/WDSproject.png) ##### 7.3 代码准备 将lib文件放置到我们的components/NES下面 ![输入图片说明](Image/DriverCompair/100AskNES_Copy.png) 接下来简单分析工程结构,看看都有什么! ![输入图片说明](Image/DriverCompair/Target.png) 对于我们来说,我们首要适配的层就是esp32文件夹下的适配层 将ESP32从menu提取出来,放置到NES根目录里面,改名esp32s3 将scr中的文件提取出来,放置在nofrendo文件夹中,其余文件不要动 完成后: ![输入图片说明](Image/Source/TargetNew.png) ##### 7.4编写CMake文件 这部分添加一个CMake文件,确保所有的文件夹都能被编译到,避免出现某些错误 编写CMake文件是很重要的,编写了CMake文件才能保证需要编译的文件都被正确的引入到程序中 ![输入图片说明](Image/Source/CMake.png) 文件中原先包好的MakeFile文件做保留 ##### 7.5 移植目标确定 ![输入图片说明](Image/Source/Ceng.png) #### 八、nes_main 我们希望将NES模拟器移植成一个组件,因此我们再次将NES启动代码进行包装,因此我们创建nes_main.c和nes_main.h: ##### 8.1 nes_main.h 在nes_main中,添加这些头文件的引用: ```c #ifndef _NES_MAIN_H_ #define _NES_MAIN_H_ #include "freertos/FreeRTOS.h" #include "esp_system.h" #include "esp_event.h" #include "esp_event_loop.h" #include "esp_partition.h" #include "esp_err.h" // #include "driver/spi_master.h" #include "esp_log.h" #include "esp_vfs_fat.h" // #include "driver/sdmmc_host.h" #include "driver/sdspi_host.h" #include "sdmmc_cmd.h" #include "esp_spiffs.h" #include "nvs_flash.h" #include "driver/gpio.h" #include "psxcontroller.h" #include "nofrendo.h" #include "menu.h" #endif ``` 添加函数定义,这里我们只定义一个可以在外部调用的程序,用于启动nes模拟器: ```c void nes_start_main(void); ``` ##### 8.2 nes_main主程序解析 此外就是主程序中,跟SD卡读取和文件打开的程序了,这些程序我们先复制过来: ```c #include "nes_main.h" //声明头文件 #define READ_BUFFER_SIZE 64 #define ASSERT_ESP_OK(returnCode, message) \ if (returnCode != ESP_OK) \ { \ printf("%s. (%s)\n", message, esp_err_to_name(returnCode)); \ return returnCode; \ } char *selectedRomFilename; // ROM file name char *nes_100ask_osd_getromdata() { char *romdata; spi_flash_mmap_handle_t handle; esp_err_t err; // Locate our target ROM partition where the file will be loaded esp_partition_t *part = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, 0xFF, "rom"); if (part == 0) printf("Couldn't find rom partition!\n"); // Open the file printf("Reading rom from %s\n", selectedRomFilename); FILE *rom = fopen(selectedRomFilename, "r"); long fileSize = -1; if (!rom) { printf("Could not read %s\n", selectedRomFilename); exit(1); } // First figure out how large the file is fseek(rom, 0L, SEEK_END); fileSize = ftell(rom); rewind(rom); if (fileSize > part->size) { printf("Rom is too large! Limit is %dk; Rom file size is %dkb\n", (int)(part->size / 1024), (int)(fileSize / 1024)); exit(1); } // Copy the file contents into EEPROM memory char buffer[READ_BUFFER_SIZE]; int offset = 0; while (fread(buffer, 1, READ_BUFFER_SIZE, rom) > 0) { if ((offset % 4096) == 0) esp_partition_erase_range(part, offset, 4096); esp_partition_write(part, offset, buffer, READ_BUFFER_SIZE); offset += READ_BUFFER_SIZE; } fclose(rom); printf("Loaded %d bytes into ROM memory\n", offset); // Memory-map the ROM partition, which results in 'data' pointer changing to memory-mapped location err = esp_partition_mmap(part, 0, fileSize, SPI_FLASH_MMAP_DATA, (const void **)&romdata, &handle); if (err != ESP_OK) printf("Couldn't map rom partition!\n"); printf("Initialized. ROM@%p\n", romdata); return (char *)romdata; } void esp_wake_deep_sleep() { esp_restart(); } esp_err_t nes_100ask_event_handler(void *ctx, system_event_t *event) { return ESP_OK; } esp_err_t nes_100ask_register_sd_card() { esp_err_t ret; sdmmc_card_t *card; sdmmc_host_t host = SDSPI_HOST_DEFAULT(); host.command_timeout_ms=200; //host.flags = SDMMC_HOST_FLAG_4BIT; host.max_freq_khz = SDMMC_FREQ_PROBING; //host.max_freq_khz = 240; sdspi_slot_config_t slot_config = SDSPI_SLOT_CONFIG_DEFAULT(); slot_config.gpio_miso = CONFIG_SD_MISO; slot_config.gpio_mosi = CONFIG_SD_MOSI; slot_config.gpio_sck = CONFIG_SD_SCK; slot_config.gpio_cs = CONFIG_SD_CS; slot_config.dma_channel = 2; esp_vfs_fat_sdmmc_mount_config_t mount_config = { .format_if_mount_failed = false, .max_files = 5, //.allocation_unit_size = 16 * 1024 }; ret = esp_vfs_fat_sdmmc_mount("/sdcard", &host, &slot_config, &mount_config, &card); ASSERT_ESP_OK(ret, "Failed to mount SD card"); // Card has been initialized, print its properties sdmmc_card_print_info(stdout, card); return ESP_OK; } /** * @brief NES启动程序 * @param 无 */ void nes_start_main(void) { printf("100ASK NESEMU START!\n"); ASSERT_ESP_OK(nes_100ask_register_sd_card(), "Unable to register SD Card"); // 初始化存储设备(SD卡) nes_100ask_controller_init(); // 初始化输入设备 selectedRomFilename = nes_100ask_run_menu(); nofrendo_main(0, NULL); // 进入nes模拟器循环 printf("NoFrendo died? WtF?\n"); asm("break.n 1"); return 0; } ``` ##### 8.3 nes_main适配——读取游戏函数 **这里有一些东西需要我们进行修改** 首先来看`char *nes_100ask_osd_getromdata()` 函数,这个函数用于打开游戏Rom文件,这里有一些产生日志的地方,后面我们可以与之对接,这里暂时不进行对接 其次是`void esp_wake_deep_sleep()`,这个函数操作ESP32S3进入复位程序,这里我们也不用去管 再来是白问网NES游戏机的事件回调函数`esp_err_t nes_100ask_event_handler(void *ctx, system_event_t *event)`,这里我们也不用管(它默认返回ESP_OK) 接下来就是SD卡初始化的程序了`esp_err_t nes_100ask_register_sd_card()`,这个程序除了配置SD卡的基本参数,还会对SD卡进行初始化,这里我们使用我们自己的SD卡初始化程序,注意!!使用前必须初始化SPI. ##### 8.4 SD卡适配nes_main 接下来就是SD卡初始化的程序了`esp_err_t nes_100ask_register_sd_card()`,这个程序除了配置SD卡的基本参数,还会对SD卡进行初始化,这里我们使用我们自己的SD卡初始化程序,注意!!使用前必须初始化SPI. ```c esp_err_t nes_100ask_event_handler(void *ctx, system_event_t *event) { return ESP_OK; } esp_err_t nes_100ask_register_sd_card() { esp_err_t ret; /* esp_err_t ret; sdmmc_card_t *card; sdmmc_host_t host = SDSPI_HOST_DEFAULT(); host.command_timeout_ms=200; //host.flags = SDMMC_HOST_FLAG_4BIT; host.max_freq_khz = SDMMC_FREQ_PROBING; //host.max_freq_khz = 240; sdspi_slot_config_t slot_config = SDSPI_SLOT_CONFIG_DEFAULT(); slot_config.gpio_miso = CONFIG_SD_MISO; slot_config.gpio_mosi = CONFIG_SD_MOSI; slot_config.gpio_sck = CONFIG_SD_SCK; slot_config.gpio_cs = CONFIG_SD_CS; slot_config.dma_channel = 2; esp_vfs_fat_sdmmc_mount_config_t mount_config = { .format_if_mount_failed = false, .max_files = 5, //.allocation_unit_size = 16 * 1024 }; ret = esp_vfs_fat_sdmmc_mount("/sdcard", &host, &slot_config, &mount_config, &card); ASSERT_ESP_OK(ret, "Failed to mount SD card"); // Card has been initialized, print its properties sdmmc_card_print_info(stdout, card); */ //百问网SD卡初始化程序,这里使用我们的SPISDCard进行对接 //注意,使用之前,一定要调用spi2_Init while (sd_spi_init()) /* 检测SD卡 */ { /*如果检测不到,就一直检测SD卡*/ lcd_show_string(30, 110, 200, 16, 16, "SD Card Error!", RED); vTaskDelay(500); lcd_show_string(30, 130, 200, 16, 16, "Please Check! ", RED); vTaskDelay(500); } ret = exfuns_init(); //初始化Exfuns,为SD卡文件系统申请内存 return ESP_OK; } ``` ##### 8.5 nes_main启动流程 接着来看一下启动流程: ```c /** * @brief NES启动程序 * @param 无 */ void nes_start_main(void) { printf("100ASK NESEMU START!\n"); ASSERT_ESP_OK(nes_100ask_register_sd_card(), "Unable to register SD Card"); // 初始化存储设备(SD卡) nes_100ask_controller_init(); // 初始化输入设备 selectedRomFilename = nes_100ask_run_menu(); nofrendo_main(0, NULL); // 进入nes模拟器循环 printf("NoFrendo died? WtF?\n"); asm("break.n 1"); return 0; } ``` 首先打印的是启动信息,表示游戏机启动`printf("100ASK NESEMU START!\n");`c 接下来初始化SD卡,注意,只有SD卡初始化成功才会继续`ASSERT_ESP_OK(nes_100ask_register_sd_card(), "Unable to register SD Card"); // 初始化存储设备(SD卡)`c 接下来初始化输入设备:`nes_100ask_controller_init();// 初始化输入设备` 再来进入韦老师的游戏选择界面: 从界面中退出来会带着游戏ROM的地址,程序会自动复制这个ROM并且组合南北运行 最后就是进入nofrendo了:`nofrendo_main(0, NULL);// 进入nes模拟器循环` 如果运行nofrendo出现了错误,还会抱怨一句Nofrendo Died? What The Fuck!`printf("NoFrendo died? WtF?\n");` #### 九、韦东山NES--驱动层适配 ##### 9.1 LCD驱动适配 ###### 9.1.1 通过条件编译,选择编译我们的LCD驱动代码 我们直接打开esp32s3\spi_lcd.c,这里是整个SPILCD的驱动程序位置: 这里我们的目标与前面移植显示屏的驱动时一致,我们使用我们自己的驱动程序!那么我们下面再逐块分析代码,并且对显示代码进行适配: 臃肿的代码就是这里...... 首先,还是使用#define + 条件编译的方式对代码进行条件编译,不选择编译韦老师的屏幕驱动代码,使用我们自己的驱动代码: ... 算了,宏定义可能出现某些bug,这里选择直接使用注释的方式注释掉臃肿的驱动代码,以后使用的时候需要用户提供LCD的接口,这样也好,方便移植到各个平台. ###### 9.1.2 通过条件编译(删掉)直接注释注释掉臃肿的代码 这里我就不放源代码出来了,记录过程就行 **1.注释掉引脚定义的代码** 因为使用我们自己的LCD驱动程序,毫不留情的直接注释掉 ![输入图片说明](Image/Rubbish/LCDIO.png) 也不知道为什么,这些跟IO有关的代码又在menu中重新定义了一次,真他妈臃肿 **2.注释SPI等待代码** 这里还有一个用于选择ST7789/ili9341的宏定义,这不关我们事,我们使用我们自己的LCD驱动库 ![输入图片说明](Image/Rubbish/LCDBLK.png) **3.注释背光设置代码** **这里注释掉背光设置代码部分,保留函数** ![输入图片说明](Image/Rubbish/BLK.png) **4.注释LCD-SPI写数据,LCD写指令,LCD写数据的代码,注释掉LCD初始化序列发送代码.** 也就是注释掉下面这几个函数,篇幅过长我就不列出来了... ```c static void spi_write_byte(const uint8_t data); static void LCD_WriteCommand(const uint8_t cmd); static void LCD_WriteData(const uint8_t data); static void ILI9341_INITIAL(); ``` **5.注释掉GPIO初始化代码,注释掉SPI初始化代码** 注释掉这两个函数 ```c static void ili_gpio_init(); static void spi_master_init(); ``` 除了上面所述的,其他的都不进行注释,注意,注释并非删除,因为某些API可能还跟其他地方产生牵连,因此需要等待编译通过后才会考虑删除 ###### 9.1.3 剩余部分代码分析 **1.16 -> 32数据合并函数** 这个将两个uint16_t合称为一个uint32_t的数据的函数,这个函数是通过宏定义完成的,这里暂时保留: ```c #define U16x2toU32(m, l) ((((uint32_t)(l >> 8 | (l & 0xFF) << 8)) << 16) | (m >> 8 | (m & 0xFF) << 8)) ``` **2.文字和图标取模** 这些数据在其他地方的UI中有被调用,保留这些取模,这里还有一些按键符号的宏定义,选择保留. **3.UI菜单位置检查函数** ```c static uint16_t renderInGameMenu(int x, int y, uint16_t x1, uint16_t y1, bool xStr, bool yStr) ``` 检查菜单是否位于显示区域边界,如果位于显示区域边界,则直接返回特定的值 **4.显示缓冲区域** 这里定义一个显示的缓冲区域,保留: ```c //申请的显示缓存 uint16_t rowCrc[NES_SCREEN_HEIGHT]; uint16_t scaleX[320]; uint16_t scaleY[240]; ``` 5.图像CRC校验 图像CRC校验函数,校验当前图像是否需要刷入显示屏(减少不必要的刷屏数据量) ```c //用于CRC计算 //这个代码可以说是点睛之笔 #define ignore_border 4 static int calcCrc(const uint8_t *row) { int crc = 0; int tmp; for (int i = ignore_border; i < 256 - ignore_border; i++) { tmp = ((crc >> 8) ^ row[i]) & 0xff; tmp ^= tmp >> 4; crc = (crc << 8) ^ (tmp << 12) ^ (tmp << 5) ^ tmp; crc &= 0xffff; } return crc; } ``` **6.刷屏程序** 刷屏主程序用于刷入图像显示,这里就是一会我们要修改的程序 ```c void ili9341_write_frame(const uint16_t xs, const uint16_t ys, const uint16_t width, const uint16_t height, const uint8_t *data[], bool xStr, bool yStr) { ``` 上方还有一个标记变量,标记上一次有没有刷入显示: ``` static int lastShowMenu = 0; ``` **7.图像压缩算法(把指定大小的图片压缩成合适的大小)** ``` //快速图像压缩算法 void precalculateLookupTables() { for (int i = 0; i < 320; i++) { scaleX[i] = i * 0.8; } for (int i = 0; i < 240; i++) { scaleY[i] = i * 0.94; } } ``` **8.LCD初始化** 用于初始化一块LCD显示屏 ```c //LCD初始化 void ili9341_init() { // lineEnd = textEnd = 0; // spi_master_init(); // ili_gpio_init(); // ILI9341_INITIAL(); // #if PIN_NUM_BCKL >= 0 // LCD_BKG_ON(); // initBCKL(); // #endif memset(rowCrc, 0x1234, sizeof rowCrc); lcd_init();//初始化LCD precalculateLookupTables(); } ``` ###### 9.1.4 适配我们的驱动库 1.在spi_lcd.c中加入我们的LCD库的支持 加入`#include "lcd.h"` **2.修改设置显示窗口函数** 注释掉原先的设置窗口的函数,改为使用我们LCD驱动库中的函数,关于LCD设置窗口的解析可以前往前面移植过程的分析 注意计算结束坐标的时候需要-1: ```c x1 = xs + (width - 1); y1 = ys + y + (height - 1); ``` 添加我们的设置窗口程序: ```c lcd_set_window(xs,ys+y,x1,y1); ``` **3.修改刷屏程序** 源代码先将处理的像素保存在Temp中,处理一定像素后再刷入显示区域 不使用源代码中的十六位缓冲,使用我们自己的LCD缓冲(注释掉Temp) 将处理的数据转化成四个八位,保存到我们的LCD_buf 一次性刷入64字节,刷完显示的内容: ```c // Render 32 pixels, grouped as pairs of 16-bit pixels stored in 32-bit values for (i = 0; i < 16; i++) { xx = xStr ? scaleX[x] : x; if (xx >= 32 && !xStr) xx -= 32; //获取到第一个像素 evenPixel = myPalette[(unsigned char)(data[yy][xx])]; x++; xx = xStr ? scaleX[x] : x; if (xx >= 32 && !xStr) xx -= 32; //获取到第二个像素 oddPixel = myPalette[(unsigned char)(data[yy][xx])]; x++; //如果这个像素超出显示范围 if (!xStr && (x <= 32 || x >= 288)) evenPixel = oddPixel = backgroundColor; if (!yStr && y >= 224) evenPixel = oddPixel = backgroundColor; //如果要显示菜单,覆盖掉像素 if (getShowMenu()) { evenPixel = oddPixel = renderInGameMenu(x, y, evenPixel, oddPixel, xStr, yStr); } //temp[i] = U16x2toU32(evenPixel, oddPixel); //分四次将像素刷入到我们的buffer lcd_buf[lcd_buff_data_mask] = (uint8_t)(evenPixel>>8); lcd_buff_data_mask = lcd_buff_data_mask+1; lcd_buf[lcd_buff_data_mask] = (uint8_t)(evenPixel & 0x00FF); lcd_buff_data_mask = lcd_buff_data_mask+1; lcd_buf[lcd_buff_data_mask] = (uint8_t)(oddPixel>>8); lcd_buff_data_mask = lcd_buff_data_mask+1; lcd_buf[lcd_buff_data_mask] = (uint8_t)(oddPixel & 0x00FF); lcd_buff_data_mask = lcd_buff_data_mask+1; } // //先复制到我们的显示缓冲中 // for (i = 0; i < 16; i++) // { // lcd_buf[LCD_BUF_SIZE] = temp[i] // WRITE_PERI_REG((SPI_W0_REG(SPI_NUM) + (i << 2)), temp[i]); // } //一次到位,刷入屏幕! lcd_write_data(lcd_buf,4*16); //一次四字节,一次处理16个像素 lcd_buff_data_mask = 0; //清除buff指针位置,避免出错! ``` 4.LCD初始化 这里就很简单了,注释掉原来的初始化代码,引入我们的初始化代码,OK ```c //LCD初始化 void ili9341_init() { // lineEnd = textEnd = 0; // spi_master_init(); // ili_gpio_init(); // ILI9341_INITIAL(); // #if PIN_NUM_BCKL >= 0 // LCD_BKG_ON(); // initBCKL(); // #endif memset(rowCrc, 0x1234, sizeof rowCrc); lcd_init();//初始化LCD precalculateLookupTables(); } ``` ##### 9.2 Video_Audio部分处理 这里处理video_audio部分的代码,因为大部分的内容我在前面的移植日志里面已经写清楚了,这里就不过多赘述了,没有讲过的部分我会进行补充 1.处理I2S音频发送函数 ##### 9.3psxcontroller游戏机控制器处理 因为游戏机控制器部分的程序与NES层有对接,这里选择不直接删除(避免后续出现引用上的问题) ###### 9.3.1去除按键宏定义 这部分关于按键宏定义的部分注释掉! ![输入图片说明](Image/DriverCompair/KeyDefine.png) 这里有个使用nop(单片机执行空代码)以实现微秒级别延迟的程序,这部分程序具有一定的学习意义: ```c #define DELAY() asm("nop; nop; nop; nop;nop; nop; nop; nop;nop; nop; nop; nop;nop; nop; nop; nop;") ``` #### 十、第一次编译 ##### 10.1 该死的CMake 来解决CMake问题,首先是BSP下面的CMake: 正确的写法: ```CMake FILE(GLOB_RECURSE app_sources ./*.* ./menu/*.* ./esp32s3/*.* ./nofrendo/*.* ./nofrendo/cpu/*.* ./nofrendo/libsnss/*.* ./nofrendo/mappers/*.* ./nofrendo/nes/*.* ./nofrendo/sndhrdw/*.*) set(requires driver fatfs esp_timer esp_event spiffs) idf_component_register( SRCS ${app_sources} INCLUDE_DIRS "." "./menu/" "./menu/src/" "./esp32s3/" "./nofrendo/" "./nofrendo/cpu/" "./nofrendo/libsnss/" "./nofrendo/mappers/" "./nofrendo/nes/" "./nofrendo/sndhrdw/" EMBED_FILES "menu/data/100ask_logo.jpg" REQUIRES ${requires} ``` 这里要注意的是requires下添加的对特定库的依赖,注意这些依赖!!!! | driver | 基础的驱动库 | |-----------|---------------------------------------------| | fatfs | fatfs的支持 | | esp_timer | esp_timer的支持 | | esp_event | esp_event.c/esp_event.h,如果不requirest那么可能会报错 | | spiffs | esp_spiffs需求 | | | | 然后就开始编译,开门红..... ![输入图片说明](Image/FirstBuild/Red.png) ##### 10.2解决malloc宏定义与函数冲突问题 备注:此部分参考王老师的解决方案,在此深表感谢! 源文链接————[单片机开发---ESP32S3移植NES模拟器(一)](https://blog.csdn.net/baidu_19348579/article/details/128860870) 这个问题是这样的: ![输入图片说明](Image/FirstBuild/malloc%20ERROR.png) 有问题的代码出现在对malloc的重定义上与函数冲突,解决的方法也很简单, 只需要替换成新的定义代码即可 替换后... ```c #ifdef NOFRENDO_DEBUG #define malloc(s) _my_malloc((s), __FILE__, __LINE__) #define free(d) _my_free((void **) &(d), __FILE__, __LINE__) #define strdup(s) _my_strdup((s), __FILE__, __LINE__) extern void *_my_malloc(int size, char *file, int line); extern void _my_free(void **data, char *file, int line); extern char *_my_strdup(const char *string, char *file, int line); #else /* !NORFRENDO_DEBUG */ /* Non-debugging versions of calls */ #define sj_malloc(s) _my_malloc((s)) #define sj_free(d) _my_free((void **) &(d)) #define strdup(s) _my_strdup((s)) extern void *_my_malloc(int size); extern void _my_free(void **data); extern char *_my_strdup(const char *string); #endif /* !NOFRENDO_DEBUG */ ``` 注意替换的位置 ![输入图片说明](Image/FirstBuild/%E6%9B%BF%E6%8D%A2%E5%90%8E%E7%9A%84%E4%BD%8D%E7%BD%AE.png)