Flipper Zero Tama P1 模拟器实战
【免费下载链接】FlipperPlayground (and dump) of stuff I make or modify for the Flipper Zero项目地址: https://gitcode.com/GitHub_Trending/fl/Flipper
一只住进 Flipper Zero 的虚拟宠物
把 Tamagotchi P1 的 ROM 拷进 SD 卡,启动 Tama P1 模拟器,Flipper Zero 的屏幕上就会出现一只黑白像素的拓麻歌子:它会按蜂鸣器叫、用 8 个状态图标提示饥饿与游戏,退出前存档,下次启动它还会从上次的状态继续长大。
这个项目位于 Applications/Official/source-OLDER/xMasterX/tama_p1 目录,是典型的"外部插件 + 跨平台模拟内核"结构:应用外壳负责 ROM 加载、事件循环和渲染,真正干活的是 TamaLIB——一个从零实现的、硬件无关的 4 位机模拟库。读完整篇文章,你既能把它跑起来,也看得懂一台 32,768 Hz 的 4 位 CPU 是怎么被"装进" Flipper 的。
最短路径:放 ROM、编译、启动
放置 ROM 文件
ROM 路径在 tama.h 里写死为/ext/tama_p1/rom.bin。做法只有两步:
- 在 microSD 卡根目录新建
tama_p1文件夹; - 把从网络找到的 P1 ROM 改名为
rom.bin放进去。
文件缺失时应用会显示 "No ROM",模拟 CPU 异常停机则显示 "Halted",后面排查时会用到这两个提示。
一键编译并启动
把tama_p1文件夹复制进 Flipper 固件源码树的applications/plugins/tama_p1,然后一条命令完成编译、安装并启动:
./fbt launch_app APPSRC=applications/plugins/tama_p1应用元信息在 application.fam 中:EXTERNAL 类型(编译成 .fap 插件)、入口函数tama_p1_app、依赖 gui 与 storage 两个系统服务、1 KB 堆栈,发布到 Games_Extra 分类。
三键操作与隐藏功能
| Flipper 按键 | 对应动作 |
|---|---|
| 左键 | 宠物 A 键 |
| OK 键 | 宠物 B 键 |
| 右键 | 宠物 C 键 |
| 上键 | 静音(模拟同时按 A+C) |
| 下键(短按) | 立即存档 |
| 返回(短按) | 立即存档 |
| 返回(长按) | 存档并退出 |
上键静音、下键存档是文档没细说但源码里已实现的功能,全部映射逻辑集中在 tama_p1.c 的输入回调中。
它是怎么做到的
4 位内核:TamaLIB 与 ROM 字节序重排
要解决的问题:Tamagotchi P1 的"大脑"是一颗 Epson E0C6S46 4 位 MCU,而网上流传的 ROM 来自芯片裸片照片的光学识读。tamalib/ 把取指、标志位、定时器、中断全部软件化,用一张内存表复刻整颗芯片的地址空间:
| 区域 | 地址 | 大小(半字节) |
|---|---|---|
| RAM | 0x000–0x27F | 640 |
| 显示区 1 | 0xE00–0xE4F | 80 |
| 显示区 2 | 0xE80–0xECF | 80 |
| I/O 寄存器 | 0xF00–0xF7F | 128 |
取舍在内存占用上:Flipper 的 STM32WB 内存宝贵,TamaLIB 默认开启LOW_FOOTPRINT宏,把无效地址段裁掉、每两个半字节打包进一个字节,整张表只占 464 字节,读写靠位运算宏完成。
ROM 还有个字节序问题:文件里的 4 位字和 TamaLIB 预期的位置正好相反,加载后必须重排:
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以 64000 作为时间基准启动内核,节拍从此对齐到 64 kHz。
HAL 适配:时钟、屏幕与蜂鸣器
TamaLIB 从不直接碰硬件,只依赖一个hal_t函数表;Flipper 侧的实现全在 hal.c,不到两百行。关键取舍有三处:
时钟:get_timestamp()直接读 STM32 定时器 TIM2 的计数器,预分频设为 999,计数频率恰好 64 kHz,与内核基准一致:
LL_TIM_InitTypeDef tim_init = { .Prescaler = 999, .CounterMode = LL_TIM_COUNTERMODE_UP, .Autoreload = 0xFFFFFFFF, };屏幕:模拟器的 32×16 像素不直接上屏,而是写进uint32_t framebuffer[16]——每行一个 32 位整数,1 位对应 1 个像素;8 个状态图标写进一个icons位图。真正的绘制交给 GUI 线程,所以 HAL 里的update_screen()是空函数。
蜂鸣器:TamaLIB 上报的频率要先除以 10 才是真实频率,play_frequency再调furi_hal_speaker_start驱动内置喇叭。
双线程时间模型与存档流水线
模拟和绘制各走一个线程,中间用一把递归互斥锁加 8 深度消息队列隔开。模拟线程在 worker 里循环调用tamalib_step();GUI 线程由 30 FPS 的周期定时器驱动重绘:
furi_timer_start(timer, furi_kernel_get_tick_frequency() / 30);节奏控制靠sleep_until():模拟时间还没到目标时间戳时,它释放互斥锁、让出 1 个 tick 的 CPU 再拿回锁继续步进。这样模拟线程既不会空转耗资源,也不会把 64 kHz 节拍拖慢。重绘回调则在同一把锁下读帧缓冲,按 2 倍缩放居中画出 64×32 的画面,再按位图把 14×14 的图标排在矩阵上下两侧。
存档走同一条流水线。save.bin(/ext/tama_p1/save.bin,约 850 字节)的布局:4 字节魔数"TLST"加版本号,随后是 PC 与 X/Y/A/B 等寄存器、两个定时器时间戳和可编程定时器参数、16 个中断槽,最后是整块 RAM 与 I/O。加载时先校验魔数与版本,失败只打日志不恢复;成功后调tamalib_refresh_hw()让显示与蜂鸣器状态重新同步。长按返回退出、下键短按、返回短按都会触发一次完整写档,宠物不会因为你合上设备就"死"了。
踩坑与排查
- ⚠️文档说"无存档"?README 前文写着进度会重置,但同一文档的 Implemented 一节和源码都表明存档已实现。以源码为准,前文属于过时描述。
- ⚠️fbt 命令的反斜杠:README 里
APPSRC=applications\plugins\tama_p1是 Windows 写法,Linux 或 macOS 的 Shell 要换成正斜杠。 - ⚠️编译报未使用参数:TamaLIB 的
cpu.c、hw.c会触发-Wunused-parameter,严格告警配置下可能直接编译失败,需要在固件site_cons/cc.scons的CCFLAGS里加-Wno-unused-parameter抑制。 - ⚠️"No ROM" 与 "Halted":前者是
rom.bin路径或文件名不对;后者是模拟 CPU 停机,多半是 ROM 内容不对,换一份再试。 - 图标编译:应用引用
compiled/assets_icons.h。仓库里已带好编译产物,若替换了icons/下的 PNG,需要用固件根目录的scripts/assets.py重新生成这个头文件。 - 串口定位:源码用
FURI_LOG_D/E打了 ROM 加载、存档读写和按键序列日志(Tag 为TamaP1与TamaLIB)。配合 FlipperScripts 里的serial_logger.py监听串口,即可确认按键映射与状态机行为。
边界与延伸
当前版本的能力边界(来自 README 的 To-do 与源码注释):
- 单一存档槽位,没有多存档;
- 无法在游戏内重置宠物,只能删掉
save.bin重来; - 未实现测试模式与音量调节;
- 加速未实现——源码里
fast_forward_done被直接置为 true。
想继续深入,建议从 tamalib 的cpu.c取指与标志位处理读起,再对照hw.c里的定时器与中断模型;之后可以翻看 Applications 目录下的其他游戏类插件,体会同一套"HAL + 模拟内核"模式如何复用到别的芯片上。
【免费下载链接】FlipperPlayground (and dump) of stuff I make or modify for the Flipper Zero项目地址: https://gitcode.com/GitHub_Trending/fl/Flipper
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考