把拓麻歌子 P1 塞进 Flipper Zero:从 ROM 到按键的完整上手指南
【免费下载链接】FlipperPlayground (and dump) of stuff I make or modify for the Flipper Zero项目地址: https://gitcode.com/GitHub_Trending/fl/Flipper
一块 32×16 像素的黑白屏里,跑着整台 4 位掌机——更夸张的是,这台掌机的 ROM 最初是从芯片裸片的高清照片上一个比特一个比特光学识读出来的。现在,它正跑在你的 Flipper Zero 上:Tama P1 Emulator(基于 TamaLIB 的拓麻歌子初代模拟器)就是这样一个 EXTERNAL 插件,源码位于仓库的 Applications/Official/source-OLDER/xMasterX/tama_p1。读完这篇,你能把 ROM 放对位置、把 .fap 构建部署上机,并看懂它模拟 4 位 CPU 的每个关键决策。
先看它跑起来:屏幕上会发生什么
这一节先立预期:装好之后,你看到和摸到的是什么。
正常运行的画面:32×16 的像素矩阵被 2 倍放大成 64×32 的色块居中绘制,矩阵上下各排 4 枚 14×14 的状态图标,共 8 枚,按内部位图逐位显隐。
两个兜底提示:ROM 文件没放对,屏幕只会显示 "No ROM";模拟 CPU 运行中触发 halt,则显示 "Halted"。
按键手感:三个基础键对应拓麻歌子的 A/B/C,另外藏着两个 README 没细说的功能——
- 左键 → Tamagotchi A 键
- OK 键 → B 键
- 右键 → C 键
- 长按返回键 → 保存并退出应用
- 上键 → 静音(同时向模拟内核上报左、右两个按键的按下/释放,等效于原机的 A+C 组合)
- 短按 Down 或短按返回键 → 立刻把当前状态写进存档
静音和"随手存"都藏在源码的输入分支里:上键同时调用两次tamalib_set_button,而 Down/Back 的短按直接触发存档函数。
开跑之前的准备:ROM 只有一个合法名字
这一节只回答一个问题:上机前 SD 卡上要放什么、放哪。
- 在 microSD 卡上建一个
tama_p1目录,即/ext/tama_p1。 - 上网搜索 Tamagotchi P1 的 ROM。原 README 的说法是:搜到一个叫
a的文件,它本身就是 ROM,重命名为rom.bin即可。 - 文件最终落在
/ext/tama_p1/rom.bin。这个路径由 tama.h 里的TAMA_ROM_PATH(EXT_PATH("tama_p1/rom.bin"))写死,改名或挪窝都会直接落到 "No ROM"。 - 加载后还有一步不可见的字节序重排,别指望 ROM 是"原样可读"的:P1 的 CPU 是 4 位机,程序按半字节(nibble)组织。加载代码每两个字节交换一次,并把换出来的高字节只保留低 4 位(
b & 0xF),把字节流重排成模拟内核认识的半字节序列。
从源码到能玩:两条命令的最短路径
这一节解决"怎么把插件构建成 .fap 并直接跑起来"。按真实执行顺序来:
- 把整个
tama_p1文件夹放进固件源码树的applications/plugins/tama_p1。 - 依次执行下面的命令:
scripts/assets.py icons applications/tama_p1/icons applications/tama_p1/compiled ./fbt launch_app APPSRC=applications/plugins/tama_p1第一条把icons/下 8 个状态图标 PNG 编译成 C 头文件,产物是 compiled/assets_icons.h,里面声明I_icon_0到I_icon_7供绘制回调引用;第二条一步完成编译、部署到设备并立即启动,APPSRC指向插件目录。注意原 README 写的是反斜杠路径(applications\plugins\tama_p1),在 Linux Shell 里必须换成斜杠,即上式写法。
- 如果编译因"未使用参数"告警而失败,把
-Wno-unused-parameter追加进 site_cons/cc.scons 的CCFLAGS——TamaLIB 的cpu.c、hw.c会触发这类告警。
应用身份(写 application.fam):appid 为TAMA_P1,入口函数tama_p1_app,依赖gui与storage两个系统服务,线程栈 1 KB,上架分类Games_Extra,图标tamaIcon.png。
收尾排错:出问题就挂串口日志。FlipperScripts 里有个serial_logger.py,单独跑python serial_logger.py可以持续监听;也可以和启动命令用分号连成一条,应用一起动日志就跟上。源码里有两个日志 Tag 值得盯:TamaP1(ROM 加载、存档读写、以及每次按键的 sequence/key/type 三要素)和TamaLIB(模拟内核内部)。
它是怎么模拟出一台 4 位掌机的
模拟内核:E0C6S46 的 4096 个半字节
TamaLIB 是从零写的硬件无关模拟库,目标是从 MCU 一直跑上桌面。它模拟的对象是Epson E0C6S46 4 位 MCU,主频 32,768 Hz,屏幕 32×16 黑白加 8 个状态图标——也就是拓麻歌子 P1 的全部硬件假设。
地址空间总大小MEMORY_SIZE 4096(4096 个 4 位单元),四段划分:
- RAM:
0x000–0x27F,640 个半字节 - 显示区 1:
0xE00–0xE4F,80 个半字节 - 显示区 2:
0xE80–0xECF,80 个半字节 - I/O:
0xF00–0xF7F,128 个半字节
默认开启LOW_FOOTPRINT宏后,这 928 个半字节被两两打包进约 464 字节的连续缓冲,用SET_RAM_MEMORY/GET_RAM_MEMORY一类宏按位读写——对 STM32WB 这种 RAM 寸土寸金的环境,省下的是一整块 4 KB。
标准用法是"注册 HAL →tamalib_init(program, bps, freq)→ 主循环 → 释放"四步,freq参数决定时间戳的单位刻度。本项目取了更灵活的路:不起tamalib_mainloop(),而是开一个独立线程循环调tamalib_step()(线程栈 1 KB,名叫 "TamaLIB"),freq传 64000,把模拟节奏完全交还给 Flipper 的 GUI 事件循环。
HAL 对接:TIM2 计时、蜂鸣器与帧缓冲
TamaLIB 通过hal_t结构体把平台依赖全部外抛:内存分配、halt 回调、日志、时钟、屏幕、声音。hal.c 不到两百行,把每一项落到 Flipper 硬件上:
- 时间:拿硬件定时器 TIM2 当模拟时间源。初始化时
Prescaler = 999,计数频率约 64 kHz,get_timestamp()直接读LL_TIM_GetCounter(TIM2)。sleep_until()是节奏核心——发现模拟时间落后于目标戳时,先释放互斥锁、furi_delay_tick(1)让出 CPU,再抢回锁继续步进。 - 屏幕:
set_lcd_matrix(x, y, val)把模拟器逐像素的写操作转存进framebuffer[16]——一行一个uint32_t,一个 bit 一个像素;set_lcd_icon()则在icons这个字节上置/清位。真正的上屏由 GUI 主循环负责,所以update_screen()是空实现。 - 声音:
set_frequency把上报值除以 10(freq / 10.0)换算成实际频率,play_frequency通过furi_hal_speaker_acquire/start/stop驱动内置蜂鸣器。 - halt:模拟 CPU 停机时置位
halted,界面随即切到 "Halted"。
hal_types.h 里有个容易忽略的约束:timestamp_t必须是无符号类型——以微秒计时时,32 位无符号数约 1 小时 11 分钟就会自然回绕,有符号类型会让时间比较直接出错。
渲染节奏与 save.bin 的二进制布局
刷新由一个 30 FPS 周期定时器驱动(周期 = 内核 tick 频率 ÷ 30),每次 Tick 事件触发view_port_update()。绘制回调先以 25 tick 超时抢锁与模拟线程同步,然后按 2 倍因子把 32×16 矩阵放大居中,再依据g_ctx->icons的 8 个位,在矩阵上下两侧均分位置逐枚画 14×14 图标。模拟线程与 GUI 线程靠一把递归互斥锁和 8 深度的FuriMessageQueue(传EventTypeInput/EventTypeTick两类事件)协同。
存档有一处文档矛盾,结论是:它真的能存。README 前文写着"目前无存档,退出即重置",但同一份 README 的 "Implemented" 一节又列着 "Saving/Loading emulator state (stored in/ext/tama_p1/save.bin)",而源码里加载与写档函数完整存在:长按返回退出时写盘,启动时自动恢复。判定为前文是过时描述,存档功能以源码行为为准。
写盘与读盘共用同一份二进制布局,读档时先校验文件头再逐字段恢复,最后调tamalib_refresh_hw()让显示和蜂鸣器重新同步:
| 字段 | 说明 |
|---|---|
"TLST" | 4 字节魔数,合法性校验,不匹配则记 FATAL 并放弃恢复 |
2 | 1 字节版本号 |
| PC | 2 字节,程序计数器,高字节掩码 0x1F(共 12 位) |
| X / Y | 各 2 字节,通用寄存器,高字节掩码 0xF |
| A / B / NP / SP / Flags | 各 1 字节:累加器、B 寄存器(取低 4 位)、页选择(低 5 位)、栈指针、标志 |
| 三个时间戳类计数器 | tick_counter、时钟定时器戳、可编程定时器戳,各 4 字节 |
| 定时器控制 | 使能位、数据、重装载值,各 1 字节 |
| call_depth | 4 字节,调用栈深度 |
| 16 × 3 字节 | 16 个中断槽的 factor/mask/triggered |
| RAM | 640 字节,每个半字节独占 1 字节 |
| I/O | 128 字节,对应0xF00–0xF7F |
边界与取舍:五件没做完的事
这些是 README To-do 清单上明牌挂着的缺口,每条附一句"为什么现状说得过去":
- Slots(多存档槽):目前只有一个
save.bin。合理——单槽让魔数+版本号的校验路径保持最简,扩槽位时格式机制可直接复用。 - In-game reset(游戏内重置):不退出应用就无法软复位。合理——复位入口不在 4 位机固件暴露的常规操作里,属于锦上添花而非玩法刚需。
- Test mode(测试模式):固件自带自检模式的入口尚未打通。合理——自检对"养宠物"这条主线无直接影响。
- Volume adjustment(音量调节):蜂鸣器音量不可调。合理——扬声器 HAL 只关心频率使能,音量属于平台层参数。
- Fast-forward(加速):源码里
fast_forward_done被直接置真,旁边挂着TODO: implement fast forwarding。合理——加速涉及模拟时钟缩放,做坏了会破坏存档里的时间戳一致性,留到有把握再动。
把rom.bin放进 SD 卡、跑一次那条launch_app命令,这只 4 位虚拟宠物就从上次退出的那一秒继续活着。
【免费下载链接】FlipperPlayground (and dump) of stuff I make or modify for the Flipper Zero项目地址: https://gitcode.com/GitHub_Trending/fl/Flipper
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考