
Flipper Zero 跑起 Tamagotchi P1 模拟器ROM、构建与 4 位 CPU 原理解析【免费下载链接】FlipperPlayground (and dump) of stuff I make or modify for the Flipper Zero项目地址: https://gitcode.com/GitHub_Trending/fl/Flipper32×16 的黑白像素块里一只虚拟宠物正在等你喂食——这不是游戏厅的拓麻歌子而是运行在 Flipper Zero 上的 Tama P1 模拟器外部 fap 插件加载 ROM 即可复活这只 1996 年的电子宠物还支持存档、蜂鸣器和 64 kHz 精确时钟。一个外壳加一颗芯片它在模拟什么Tama P1 模拟器的目标硬件很特别拓麻歌子初代内置的是一颗 Epson E0C6S464 位 MCU主频 32,768 Hz屏幕 32×16 像素外加 8 个状态图标。而它的 ROM 至今没有正规渠道是社区从芯片裸片照片上光学识读出来的——你从网上找到的那份 ROM本质上是一张显微照片的产物。这个应用由两层组成。外面一层是 Flipper 应用外壳负责从 SD 卡读 ROM、接住五向按键、驱动显示屏和蜂鸣器。里面一层是 TamaLIB一个从零写起、硬件无关的模拟内核逐指令复刻那颗 4 位 CPU。类比一下外壳负责翻译硬件内核负责复刻芯片两者之间只隔着一张函数指针表。这种分层直接决定了它的部署形态——应用元数据里apptype是EXTERNAL意味着它编译成.fap插件放在 SD 卡里加载入口函数tama_p1_app依赖gui与storage两个系统服务应用栈只有 1 KB真正的模拟循环跑在另一个独立线程里。从零跑起来ROM 到屏幕的时间线第 1 步准备 ROM。在 microSD 卡根目录新建tama_p1文件夹把网上找到的 Tamagotchi P1 ROM有的来源里那个文件叫a改名为rom.bin放进去。源码里这个路径是写死的EXT_PATH(tama_p1/rom.bin)。第 2 步放置源码。把整个 tama_p1 目录 拷进你本地的固件源码树放到applications/plugins/tama_p1位置——这是 Flipper 外部插件的标准落点。第 3 步编译图标。应用引用了compiled/assets_icons.h里的 8 个状态图标符号I_icon_0到I_icon_7这个头文件需要先由固件自带的资源脚本从 PNG 编译出来scripts/assets.py icons applications/tama_p1/icons applications/tama_p1/compiled这条命令在固件根目录下执行把icons/里的 8 张 PNG 转成 C 头文件输出到compiled/仓库里已带一份编译产物源码改动图标时才需要重跑。第 4 步一条命令编译、推送、启动./fbt launch_app APPSRCapplications/plugins/tama_p1fbt是固件自带的构建工具launch_app目标会完成编译、通过串口安装到设备并立即运行。注意原 README 写的是反斜杠路径在 Linux 或 macOS 的终端里必须换成斜杠。第 5 步启动后看屏幕。一切顺利你会看到 32×16 的像素区居中放大两倍的画面上下各排着 4 个状态图标。如果 ROM 路径不对屏幕只会冷冷地打出 No ROM若模拟 CPU 中途 halt则显示 Halted。⚡ 编译时如果 TamaLIB 的cpu.c、hw.c因未使用参数告警而失败严格告警配置下会被当错误按 README 的提示在site_cons/cc.scons的CCFLAGS里追加-Wno-unused-parameter。调试彩蛋。想看清按键与存档的行为可以一边启动应用一边挂上串口日志python serial_logger.py这是 FlipperScripts 里的监听脚本配合./fbt launch_app ...分号连用即可。应用内部所有关键动作——ROM 加载、save.bin 读写、每次按键——都会以sequence/key/type三要素打进日志Tag 分别是TamaP1和TamaLIB。上手体验你按下左键时模拟器里发生了什么基础操作只有三个键左键是拓麻歌子的 A 键OK 键是 B 键右键是 C 键——对应游戏里的确认、菜单、返回。长按返回键保存并退出应用这是最标准的收尾方式。真正的玩法藏在另外两个方向键里。按上键应用会同时向 TamaLIB 上报 A 和 C 两个按键的状态这正是拓麻歌子上静音的经典组合键短按下键则直接触发一次tama_p1_save_state()不退出就先把状态写盘——相当于手动存档。短按返回键同样触发保存。这些行为 README 一句没提全部写在 tama_p1.c 的输入分支里。你以为没有存档其实有。README 正文写着There is currently no saving同一文件的 Implemented 一节却又列着 Saving/Loading emulator state (stored in/ext/tama_p1/save.bin)。以源码为准存档功能完整实现退出时写/ext/tama_p1/save.bin启动时自动读回你的宠物会从上次的状态继续活着。前那句无存档属于过时描述。幕后原理4 位机如何在 STM32WB 上活过来先给结论TamaLIB 在 Flipper 的 STM32WB 上逐周期模拟一颗 32,768 Hz 的 4 位 MCU而整个应用的难点不在算得快在于让模拟时钟、真机时钟、屏幕刷新三者严丝合缝。64 kHz 时钟如何驱动 4 位模拟被模拟的 CPU 主频 32,768 Hz但模拟精度需要约 30 微秒1/32768的分辨率所以整个模拟时间基被放大 2 倍用 64 kHz 的计数器来驱动——这就是tamalib_init第三个参数传 64000 的原因。时间源直接借用 STM32 的硬件定时器 TIM2预分频设为 999计数频率恰好落到 64 kHz 附近get_timestamp()返回LL_TIM_GetCounter(TIM2)即可模拟与真实时间天然同步。节奏控制的关键在sleep_until当模拟时间落后于目标时间戳时它会释放互斥锁、furi_delay_tick(1)让出一个 CPU 时隙再重新拿锁继续步进。源码里那行注释说得很直白——这里是唯一能判断跑快了还是跑慢了的地方否则模拟线程会越积越慢。4096 个 4 位单元与 LOW_FOOTPRINT 压缩被模拟 CPU 的地址空间是 4096 个 4 位单元划分如下区域地址范围大小RAM0x000–0x27F640 个 4 位单元MEM_RAM_SIZE 0x280显示区 1LCD0xE00–0xE4F80 个 4 位单元显示区 2LCD0xE80–0xECF80 个 4 位单元I/O 寄存器0xF00–0xF7F128 个 4 位单元完整映射要占 4096 字节但 Flipper 的 RAM 宝贵。cpu.h里默认打开LOW_FOOTPRINT宏只把有效区域打包进约 464 字节的缓冲区928 个半字节两两塞进一个u8_t代价是每次读写都要靠SET_RAM_MEMORY/GET_RAM_MEMORY这类宏做位移和掩码运算。用一点 CPU 换空间是嵌入式模拟器的标准权衡。寄存器方面cpu.h 的state_t把 PC 声明为 13 位、通用寄存器 X/Y 各 12 位、累加器 A 和 B 寄存器各 4 位、NP 页寄存器 5 位——存档文件里那些 0x1F、 0xF的掩码全是从这里来的。HAL 桥接两百行代码接通时钟、屏幕与蜂鸣器TamaLIB 与平台之间只有一个hal_t结构体约 14 个函数指针内存分配、halt 回调、日志、时钟、屏幕、声音、事件处理器。hal.c 逐个落地最值得看的有三块。时钟如前所述TIM2 计数即时间戳。屏幕走先写缓冲、后统一渲染set_lcd_matrix(x, y, val)把每个像素写入g_ctx-framebuffer[y]的对应位——一行 32 像素正好用一个uint32_t表示set_lcd_icon则往g_ctx-icons这个字节里置位。真正的画屏由 GUI 主循环完成所以update_screen()是空实现。声音上set_frequency把 TamaLIB 报来的频率除以 10 换算成真实值play_frequency通过furi_hal_speaker_acquire/start/stop驱动 Flipper 内置蜂鸣器音量固定 0.5。还有一个容易被忽略的细节hal_types.h 里timestamp_t被特意注释为必须是无符号类型——时间戳以微秒计时时32 位无符号数约 1 小时 11 分就会自然回绕有符号类型会让现在是否早于目标时间的比较彻底错乱。sleep_until里那个delay (8*sizeof(uint32_t)-1)的判断就是在检查无符号减法结果是否为负。渲染与线程同步30 FPS 如何追上模拟时间应用启动后是三个角色在协作模拟线程、GUI 主循环、一个 30 FPS 的周期定时器。定时器每隔furi_kernel_get_tick_frequency() / 30个 tick 往FuriMessageQueue深度 8塞一个 Tick 事件GUI 循环收到后调用view_port_update()最终进入绘制回调。绘制回调第一行就是furi_mutex_acquire(mutex, 25)——25 个 tick 的超时与模拟线程共用这把递归互斥锁g_state_mutex抢不到锁就跳过本帧宁可掉帧不可读到撕裂的帧缓冲。拿到锁后32×16 的像素矩阵按TAMA_SCREEN_SCALE_FACTOR 2逐位放大成 64×32 的色块居中绘制再在矩阵上下两侧各画 4 个 14×14 的状态图标间距按矩阵宽度自动均分。模拟线程则简单得多在 tama_p1.c 的 worker 里同步参考时间戳、使能 TIM2 计数器、先tama_p1_load_state()恢复存档然后死循环调用tamalib_step()。它没有调用tamalib_mainloop()——把循环拆成单步Flipper 的 GUI 事件循环才能完全掌控节奏按键和退出信号也能在同一把锁下干净地介入。save.bin 的 128 字节 I/O 区存档写在/ext/tama_p1/save.bin由tama_p1_save_state生成、tama_p1_load_state消费布局是一个纯小端二进制流字段大小说明Magic4 B固定TLST校验文件合法性Version1 B当前为 2PC2 B程序计数器高字节掩码0x1F13 位X / Y各 2 B通用寄存器高字节掩码0xF12 位A / B / NP / SP / Flags各 1 B累加器、B 寄存器4 位、页寄存器5 位、栈指针、标志4 位tick_counter4 B时钟周期计数clk / prog 定时器时间戳各 4 B两个定时器的时间戳定时器使能 / data / rld各 1 B可编程定时器状态call_depth4 B调用栈深度中断槽3×16 B6 个中断槽INT_SLOT_NUM的 factor/mask/triggeredRAM640 B每个半字节存 1 字节I/O128 B0xF00–0xF7F的 128 个 I/O 半字节加载端先验 magic 和版本任一不符就打 FATAL 日志并放弃恢复。把寄存器、RAM、I/O 全部灌回后调用tamalib_refresh_hw()让显示与蜂鸣器状态和恢复出的内存重新同步。换句话说宠物死亡、饥饿计时、图标亮灭全部在这个文件里原样存活。目前还做不到什么多存档槽位只有一个save.bin想换宠物只能覆盖游戏内重置不退出应用就无法软复位只能靠存档绕测试模式拓麻歌子固件自带的自检入口尚未接通加速与音量fast_forward_done已被硬置为true旁边挂着TODO: implement fast forwarding的注释蜂鸣器音量锁死在 0.5不可调。想给宠物开挂或者多养几只就从这份 To-do 入手。延伸阅读回到开场那只 32×16 像素的宠物它的生命周期——从 ROM 半字节重排到 save.bin 的 128 字节 I/O 区——现在都握在你手里。想继续往下挖两个方向都在这个仓库里翻翻 Applications 目录下其他可玩的.fap游戏插件或者逐行读 tamalib/ 的cpu.c看一颗 4 位 CPU 的指令集是怎么被几十页 C 代码完整复刻出来的。【免费下载链接】FlipperPlayground (and dump) of stuff I make or modify for the Flipper Zero项目地址: https://gitcode.com/GitHub_Trending/fl/Flipper创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考