在 Flipper Zero 上跑通拓麻歌子 P1 模拟器:从 ROM 部署到 4 位机模拟原理
在 Flipper Zero 上跑通拓麻歌子 P1 模拟器从 ROM 部署到 4 位机模拟原理【免费下载链接】FlipperPlayground (and dump) of stuff I make or modify for the Flipper Zero项目地址: https://gitcode.com/GitHub_Trending/fl/Flipper想在 Flipper Zero 的 128×64 屏幕上养一只初代拓麻歌子这个仓库里的 Tama P1 模拟器插件可以直接满足你它模拟 P1 初代的 4 位 CPU 与蜂鸣器三键即可游玩支持把宠物状态写进save.bin存档下次开机接着养。跟着本文走一遍你不仅能在自己的设备或 SD 卡上把它跑起来还会清楚它如何用一块硬件定时器驱动 64 kHz 的模拟时钟、又如何把 4096 个半字节的虚拟内存压进不到 500 字节的真实 RAM。它是什么一个外壳 模拟内核的插件应用Tama P1 是一个EXTERNAL类型的外部应用编译产物是.fap从 SD 卡加载源码位于 Applications/Official/source-OLDER/xMasterX/tama_p1/。它的架构可以一句话概括平台无关的模拟内核 TamaLIB 负责跑 CPUFlipper 侧的外壳代码只干三件事——接按键、喂时钟、画屏幕。文件职责tama_p1.c应用入口tama_p1_appROM 加载、事件循环、渲染回调、存档读写tama.h路径、缩放因子、状态结构等常量定义hal.c / hal_types.hTamaLIB 硬件抽象层HAL在 STM32WB 上的实现tamalib/模拟内核cpu.c指令集、hw.cLCD/蜂鸣器时序、tamalib.c调度icons/ compiled/8 个状态图标源文件及编译产物assets_icons.happlication.fam 里还有几个值得留意的身份参数入口tama_p1_app、依赖gui与storage两个系统服务、应用栈仅1 * 1024字节、发布到Games_Extra分类。从零跑起来放 ROM、编图标、一条命令上设备前置条件一张 microSD 卡、一份新版 Flipper 固件源码树用于构建。仓库若需获取git clone https://gitcode.com/GitHub_Trending/fl/Flipper放置 ROM在 SD 卡根目录建tama_p1文件夹把 P1 的 ROM 改名为rom.bin放进去。这个路径在 tama.h 中写死#define TAMA_ROM_PATH EXT_PATH(tama_p1/rom.bin)启动时先storage_common_stat检查文件是否存在缺 ROM 时屏幕只打印 No ROM。编译图标头文件应用引用的是compiled/assets_icons.h里的I_icon_0~I_icon_7需要先跑一次资源脚本把icons/下的 PNG 转成 C 头文件输出到compiled/目录scripts/assets.py icons applications/tama_p1/icons applications/tama_p1/compiled编译时若 TamaLIB 的cpu.c/hw.c报未使用参数告警严格告警配置下会被当错误在site_scons/cc.scons的CCFLAGS中追加-Wno-unused-parameter即可。编译并推送到设备在固件源码树根目录执行fbt它会完成编译、安装并立即在设备上启动应用Linux 下注意用正斜杠./fbt launch_app APPSRCapplications/plugins/tama_p1前提是把整个tama_p1目录放到固件树的applications/plugins/tama_p1位置——README 明确要求这一步。怎么玩按键映射、两个隐藏功能与存档版本差异基础操作只有三键源码 tama_p1.c 的输入分发里还藏着两个 README 没细说的功能按键行为说明左键拓麻歌子 A 键BTN_LEFTOKB 键BTN_MIDDLE右键C 键BTN_RIGHT上键静音同时向 TamaLIB 上报 AC 按下/释放下键短按立即存档调用tama_p1_save_state()返回短按立即存档同上返回长按存档并退出停掉 30 fps 定时器走退出流程README 与源码打架时以源码为准README.md 的 How to play 一节约 14 行写着 There is currently no saving但同一 README 的 Implemented 一节列着 Saving/Loading emulator state (stored in/ext/tama_p1/save.bin)。两处矛盾而源码给出的答案是存档早已实现——退出时返回长按写入TAMA_SAVE_PATH启动时tama_p1_worker里第一步就是tama_p1_load_state()。前文那句无存档属于过时描述。save.bin 的内部结构存档把整台机器冻结成一个定长二进制块字段大小说明Magic4 B固定TLST校验文件合法性Version1 B当前为 2PC / X / Y222 B程序计数器与各寄存器高字节按位宽掩码0x1F/0xFA / B / NP / SP / Flags各 1 B累加器、B 寄存器、页选择、栈指针、标志位tick / 定时器时间戳3×4 B 3 B时钟周期计数、两个定时器及使能位call_depth4 B调用栈深度interrupts16×3 B6 个中断槽 × (factor/mask/triggered)按INT_SLOT_NUM展开RAM640 B0x000~0x27F每半字节存 1 字节I/O128 B0xF00~0xF7F的 128 个半字节加载时先校验 magic 与版本号不匹配则打印FATAL日志并放弃恢复恢复完 RAM 与 I/O 后调用tamalib_refresh_hw()让内核重新同步显示和蜂鸣器状态。串口日志辅助定位应用内用FURI_LOG_D/FURI_LOG_E记录了 ROM 加载、存档读写TagTamaP1和 TamaLIB 层日志TagTamaLIB。配合 FlipperScripts 里的串口日志脚本即可边跑边看python .\serial_logger.py每次按键都会以sequence / key / type三要素打印验证按键映射和状态机行为时最有用。为什么能跑起来硬件时钟、半字节内存与 30 fps 渲染用一块硬件定时器当模拟时间源P1 初代基于 Epson E0C6S46 4 位 MCU晶振 32,768 HzLCD 为 32×16 黑白像素加 8 个状态图标网上流传的 ROM 据 TamaLIB 的说明是从芯片裸片照片光学识读出来的。要保真模拟模拟时钟的粒度必须够细。该应用的做法是把 STM32WB 的 TIM2 定时器当时间源LL_TIM_InitTypeDef tim_init { .Prescaler 999, .CounterMode LL_TIM_COUNTERMODE_UP, .Autoreload 0xFFFFFFFF, }; LL_TIM_Init(TIM2, tim_init);预分频 999 让计数器以 64 kHz 走数get_timestamp()直接返回LL_TIM_GetCounter(TIM2)。TamaLIB 初始化时传入的64000就是这个频率——它决定了时间戳的最小粒度。sleep_until()是节奏控制的关键模拟时间落后于目标时释放互斥锁、furi_delay_tick(1)让出 CPU再拿锁继续步进否则模拟线程会越跑越欠账。4096 个半字节如何塞进小内存被模拟 CPU 的地址空间共MEMORY_SIZE 40964096×4 bit区域地址半字节数RAM0x000~0x27F640显示区 10xE00~0xE4F80显示区 20xE80~0xECF80I/O0xF00~0xF7F128cpu.h 默认开启LOW_FOOTPRINT宏无效地址不占用缓冲四个有效区各两半字节打包进一个u8_t总缓冲只有 464 字节读写都靠SET_RAM_MEMORY/GET_RAM_MEMORY等位操作宏完成。STM32WB 的 RAM 紧张这一步压缩是它能在设备端常驻的前提。ROM 读进来后还要做一次半字节重排原因在于 P1 是 4 位机、程序按半字节组织而 SD 卡读到的是字节流// Reorder endianess of ROM for(size_t i 0; i fi.size; i 2) { uint8_t b ctx-rom[i]; ctx-rom[i] ctx-rom[i 1]; ctx-rom[i 1] b 0xF; }重排完成后调用tamalib_init((u12_t*)ctx-rom, NULL, 64000)把 ROM 交给内核。HAL约 146 行代码接通屏幕与蜂鸣器TamaLIB 通过hal_t结构体hal.h声明了全部平台依赖内存、halt、日志、时钟、屏幕、声音。hal.c 逐一对应实现其中三处值得细看static void tama_p1_hal_set_lcd_matrix(u8_t x, u8_t y, bool_t val) { if(val) g_ctx-framebuffer[y] | 1 x; else g_ctx-framebuffer[y] ~(1 x); }模拟器对每个像素的写入只是改帧缓冲里的一位——每行 32 个像素正好装进一个uint32_t共 16 行真正的绘制由 GUI 线程完成所以update_screen()是空实现。声音侧set_frequency把上报频率除以 10 换算成实际值play_frequency通过furi_hal_speaker_acquire/start/stop驱动内置蜂鸣器固定 0.5 音量因此音量调节至今是 To-do。线程模型模拟线程、GUI 线程与一把递归锁整体数据流是一条清晰的因果链独立线程栈 1024 字节持递归互斥锁g_state_mutex循环调用tamalib_step()手动步进 CPU按键事件经FuriMessageQueue深度 8送入 GUI 主循环分发到tamalib_set_button()30 fps 的周期定时器furi_kernel_get_tick_frequency() / 30触发重绘回调里先以 25 tick 超时抢锁再把帧缓冲按TAMA_SCREEN_SCALE_FACTOR 2放大成 64×32 的像素块画在屏幕正中上下各 4 个 14×14 的状态图标按g_ctx-icons的 8 个位逐一决定显隐。两个线程读写同一份帧缓冲全靠这把锁串行化模拟线程退出前 GUI 线程会furi_thread_flags_setfuri_thread_join干净收尾。TIM2、蜂鸣器、显示屏都是 STM32WB 的片上外设——上图可以帮你定位这颗主控理解模拟时间源实际落在哪块硬件上。边界与坑还没做到的事和容易踩的地方多存档槽Slots目前只有一个save.bin想多养几只宠物只能手动备份文件游戏内复位无法不退出应用就软复位宠物测试模式、音量调节都在 README 的 To-do 清单里蜂鸣器音量固定 0.5快进源码中fast_forward_done已被置为true上方留着// TODO: implement fast forwarding注释——加速功能尚未实现时间戳回绕hal_types.h 特意把timestamp_t定为无符号uint32_t并注释了原因以微秒计时时 32 位无符号数约 1 小时 11 分自然回绕换成有符号类型会让时间比较出错文档可信度遇到 README 与源码不一致如前文的无存档描述以源码为准。想继续深入tamalib/cpu.c 是指令集实现的最好入口hw.c里则是 LCD 与蜂鸣器时序的还原也可以翻 Applications/ 下其他插件看看同一套外壳 内核模式还有哪些玩法。【免费下载链接】FlipperPlayground (and dump) of stuff I make or modify for the Flipper Zero项目地址: https://gitcode.com/GitHub_Trending/fl/Flipper创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考