背景#
tiny386 是一个跑在 ESP32-P4 上的 x86 模拟器,带 VGA 帧缓冲输出,能在单片机上启动 FreeDOS,理论上也能跑 Windows 95。我们手上是 WT99P4C5-S1 开发板加一块 EK79007(1024×600,MIPI DSI)面板,目标是启动系统并点亮屏幕。
说明一下:这篇是移植过程记录,不是可复现的完整教程,我们的分支也不公开——上游后来推进了很多 commit,当时遇到的一部分问题在上游已经解决了,把过时的补丁贴出来没意义。留下的是踩坑的思路和一些通用的结论。
一、Arch 下准备 ESP-IDF#
大概三种装法:
git clone --single-branch --recursive https://github.com/espressif/esp-idf.git:能用,但要下一个多 GB 的树,慢。eim(ESP-IDF Installer Manager):最快最省心。- AUR / 系统级静态安装到
/opt/esp-idf:只适合刚好要 6.0.1 的场景。
装好之后每个 shell 都要激活一次:
source "/home/fabbo/.espressif/tools/activate_idf_v6.0.1.sh"
idf.py --version不要让 CMake 在 NTFS 分区上跑。构建时会触发 Linux 内核的 iomap BUG,直接 kernel panic。工作目录放 ext4 上。
二、板级移植#
流程大致是:
先按 README 给 SDK 打补丁。
用现成板型跑通一遍,确认链路没问题:
idf.py -DBOARD=jc4880p443 update-dependencies build能出
Successfully created a ESP32-P4 image就说明 ESP-IDF 那一侧是好的。然后新增我们自己的板级文件:
esp/main/board_wt99p4c5_s1.h、sdkconfig.wt99p4c5_s1等。USB HID 键盘鼠标支持来自社区 PR:hchunhui/tiny386#4(BigFishDev22)。很高兴能看到 BigFishDev22 的 PR 在稍晚被合并。
在
idf_component.yml里补上依赖条目。删掉
build/和旧的sdkconfig再重新配置——换了 BOARD 之后残留的 sdkconfig 会带着旧板子的选项:idf.py -DBOARD=wt99p4c5_s1 update-dependencies build idf.py flash
编译完了就有了 bios.bin、vgabios.bin镜像,再加上 FreeDOS 的 floppy.img ,和 tinyi386.ini 的配置放进 SD 卡。
值得一提的是,build.sh thirdparty 在 Arch 上编不过:脚本要编一个 Win32 的 slirp,TARGET_WINVER=0x501(Windows XP)写死在里面,现代 MinGW 的头文件不吃这个老宏。装了 mingw-w64-gcc 也不行,i686-w64-mingw32-gcc 报 Error 127 之后一路挂。好在 out/bios.bin、out/vgabios.bin 是编译出来了。
三、内存爆炸#
刷进去后开不了机,串口里是这样的:
E (1413) sdmmc_cmd: allocate_dma_buf: not enough mem, err=0x101
E (1413) diskio_sdmmc: sdmmc_read_blocks failed (0x101)
E (1713) async_fbcpy: esp_async_fbcpy_install(73): no memory for DMA2D descriptors
E (1713) lcd.dsi: esp_lcd_dpi_panel_enable_dma2d(519): install async memcpy 2d failed
ESP_ERROR_CHECK failed: esp_err_t 0x101 (ESP_ERR_NO_MEM) at 0x4002d550
file: "./main/lcd_ek79007.c" line 1280x101 就是 ESP_ERR_NO_MEM。SD 卡的 DMA buffer 分不出来,LCD 的 DMA2D 描述符也分不出来,最后 ESP_ERROR_CHECK 直接 abort。根因是这块板子上被点亮的组件太多,内部 RAM 和 DMA 可用区被吃干净了。
我们做的削减:
- 关掉 ES8311 音频编解码、I2S 驱动;
- 关掉 ESP-Hosted(无线协处理那一套);
- 试了
CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y,把 BSS 挪到 SPIRAM——是否真的有效存疑,但配合上面几条之后内存压力确实下来了。
削减之后:终于能运行系统了。FreeDOS 从 floppy.img 正常启动,速度还不错;Windows 95 能进桌面,大约 30 秒后崩溃,不过后面通过优化解决了。

四、屏幕驱动#
是用的现成的 lcd_ek79007 库,其实遇到的都是些细节问题。
旋转问题#
在 lcd_ek79007.c 里写 MADCTL 寄存器(0x36,置 MV 位 0x20)做硬件级旋转。
镜像问题#
画面位置对了,但左右颠倒,文字完全没法读。先试标准接口:
E (1585) lcd_panel: esp_lcd_panel_swap_xy(59): invalid panel handle
ESP_ERROR_CHECK failed: esp_err_t 0x102 (ESP_ERR_INVALID_ARG) at 0x400160b4esp_lcd_panel_swap_xy() 发现 EK79007 这套 DPI 面板驱动不支持。翻数据手册才发现,这颗面板的扫描方向是由两组 GPIO 硬件引脚决定的:
UPDN:垂直扫描方向(从上到下 / 从下到上);SHLR:水平扫描方向(从左到右 / 从右到左)。
问题在 lcd_ek79007.c 的 bsp_configure_panel_direction_pins():改成 UPDN = 1、SHLR = 1(都按正向扫描),画面就正常了。
看来MIPI DSI 面板的扫描方向不一定走软件接口,很多是引脚拉死的。
五、优化#
此后部分的优化 fabbo 就没实际参与了,都是队友的功劳。
分析启动全流程,识别出四个瓶颈:
| 瓶颈项 | 详情 |
|---|---|
| CPU 频率 | 默认 360MHz,ESP32-P4 支持最高 400MHz |
| PSRAM 自检 | 上电后遍历全部 10MB PSRAM,耗时 2-3 秒 |
| 编译优化 | -Os(体积优先),模拟器核心计算密集 |
| 日志输出 | INFO 级别,启动过程大量串口日志 |
通过修改 sdkconfig.defaults 中的 Kconfig 选项:
| 优化项 | 优化前 | 优化后 | 效果 |
|---|---|---|---|
| CPU 频率 | 360MHz | 400MHz | 全局性能 +11% |
| PSRAM 自检 | 启用 | 禁用 | 省去 10MB 内存遍历耗时 |
| 编译优化 | -Os (体积优先) | -O3 (速度优先) | 模拟器核心加速 |
| 日志级别 | INFO | WARN | 减少串口 I/O 开销 |
整体启动速度提升约 20-30%。进一步分析发现:
Kconfig 层面:ESP32-P4 硬件支持 RISC-V ZCMP 压缩指令扩展(
SOC_CPU_HAS_ZC_EXTENSIONS=y),但 Kconfig 中未启用CONFIG_COMPILER_ENABLE_RISCV_ZCMP。FreeRTOS 内核函数可从 Flash 迁移到 IRAM(CONFIG_FREERTOS_IN_IRAM),减少任务切换时的 cache miss。VGA 刷新:
FULL_UPDATE宏禁用了增量文本模式刷新,每次全屏重绘所有字符。DOS/文本模式下的应用程序(BIOS 自检、命令行)大部分时间只改变屏幕的一小部分,全量刷新浪费大量 CPU。I2S 任务延迟:
vTaskDelay(8ms)将 VGA 刷新率限制在 125Hz,而 VGA 自身场消隐周期为 5ms(RETRACE_INTERVAL_US = 5000),存在 3ms 不必要的延迟。TLB 大小:ESP32 构建使用
tlb_size=256,仅为桌面版的 50%。TLB miss 触发昂贵的 PSRAM 页表遍历,TLB 减半意味着 miss 率翻倍。任务栈:4096 字节栈偏小,限制编译器内联优化。
IRAM_ATTR_CPU_EXEC1:
cpu_exec1()(模拟器最热函数)被注释掉,运行在 Flash 上。尝试启用后链接器报sram_low溢出 40240 字节——函数体过大无法放入 IRAM,且与FREERTOS_IN_IRAM冲突。 ESP32-P4 的 IRAM(sram_low)容量有限,在已启用FREERTOS_IN_IRAM、GDMA_CTRL_FUNC_IN_IRAM、ESP_SYSTEM_IN_IRAM等系统组件的 IRAM 优化后,没有足够空间容纳 6000 行的cpu_exec1()。一种折中方案是只将cpu_exec1中最热的基本块(如 ALU 指令、内存读写)提取到独立函数并标 IRAM_ATTR,但这需要大规模重构。当前最现实的优化是确保该函数在 Flash 中被 i-cache 充分覆盖。
| 优化项 | 改动文件 | 效果 |
|---|---|---|
| RISC-V ZCMP | sdkconfig.defaults | 代码压缩,减少 i-cache 占用 |
| FreeRTOS 入 IRAM | sdkconfig.defaults | 内核函数零等待执行 |
| 增量 VGA 刷新 | board_wt99p4c5_s1.h(注释 FULL_UPDATE) | 文本模式下减少无效重绘 |
| vTaskDelay 8→5ms | lcd_ek79007.c | 刷新率 125→200Hz |
| tlb_size 256→512 | i386.c | TLB miss 率减半 |
| 栈 4096→8192 | esp_main.c | 解锁编译器内联 |
| IRAM_ATTR_CPU_EXEC1 | board_wt99p4c5_s1.h(回退) | IRAM 溢出 40KB,不可行 |



