给 100 块的 AI 玩具写了一套英语启蒙 App:100 个词、看图选词、开口打分

给 100 块的 AI 玩具写了一套英语启蒙 App:100 个词、看图选词、开口打分

硬件是 FoloToy AI Passport(目前约 100 元,ESP32-C3 / 8MB Flash / 240×320 屏 / 三个按键),我从零做了一个「英文学习」应用:100 个词分 8 个主题,看图选词、答对即切题、一局 10 题、结算页报分并说一句英文鼓励语。全程由 AI 代理在本地写代码、刷机、读串口日志定位崩溃。 下载地址: ai-passport.folotoy.cn/plays/2144/


一、先看成品

一局游戏的完整路径:

复制代码
首页(像素小生物 + 入口)
   ↓ 按「确定」
题目页:英文单词在上,四张图标卡在下,自动朗读单词
   ↓ 上/下 移动高亮,确定 提交
答对 → 一声升调「叮」→ 立刻切到下一题
答错 → 一声降调 → 画面停留,显示「答案 苹果」
   ↓ 第 10 题答完
结算页:得分 / 正确率 / 最佳连对 + 一句英文鼓励语
   ↓ 上/下 选择
「再来一轮」或「回首页」

几个刻意的设计:

  • 答对立刻切题,不用再按一次确定。反馈交给声音,屏幕不设 0.8 秒停留。
  • 答错才停,因为要让孩子看清正确答案。
  • 一局固定 10 题,有终点才有成就感;结算页给「得分 / 正确率 / 最佳连对」。
  • 鼓励语只在结算页说,按正确率分档:≥80% 说 "Great job!",<80% 说 "Keep going!"。
  • 新题出现后 400ms 内的「确定」不算作答,防止连按把下一题直接答掉。

二、技术栈

层 选型
芯片 ESP32-C3,8MB Flash,无 PSRAM(240×320 屏幕 + 100 张图 + 100 段语音全塞进去)
框架 ESP-IDF v5.5.3
UI LVGL 9.5
音频 ES8311 codec + I2S,16kHz/16bit 单声道 PCM 存 Flash
字体 Noto Sans SC 走 lv_font_conv 生成位图字体,覆盖 202 个中文字形

界面是深底 + 薄荷绿点缀,两套字号(16px / 28px),全部中文走自己生成的位图字体。

三、内容流水线:从 emoji 到固件

素材不是手搓的,全是脚本生成,改词表重跑一遍就行。

1. 图标:emoji → 48px 图

Apple Color Emoji 按 160px 渲染、裁剪、缩放到 48px,转成 RGB565A8(彩色 + 独立 alpha 平面)。这样贴在任意卡片底色上都不会出现方块底色。

100 张图 = 100 × 48 × 48 × 3 ≈ 676 KB,直接从 Flash 读,不占 RAM。

2. 发音:TTS → PCM

bash 复制代码
say -v Samantha -r 85 -o tts.aiff "Apple"
afconvert -f WAVE -d LEI16@16000 -c 1 tts.aiff tts.wav

再裁掉首尾静音、归一化到 70% 峰值、加 10ms 淡入淡出(避免爆音)。

85 wpm 是调出来的:原始 150 wpm 时孩子跟不上,降到 85 后每个词的时长约 0.5 秒,100 个词合计 1.76 MB。

3. 像素小生物:脚本画的,不是贴图

首页 64px、结算页 48px,两只原创小生物。在 16×16 的格子上按几何形状逐像素生成(椭圆身体 + 描边 + 顶部高光 + 底部两行压暗 + 固定位置的眼睛嘴巴),再整数倍放大(4× / 3×),所以边缘永远是硬像素。

为什么不直接用宝可梦 / 我的世界素材:那是任天堂和 Mojang 的版权资产,塞进固件再分发不合适。原创图形可以随便改颜色、改配件、改尺寸------脚本里每只生物就是一组常量。

四、踩过的五个坑(这篇的重点)

坑 1:LVGL 的悬空回调,直接把设备打崩

跑到第 3 题时设备突然重启:

vbnet 复制代码
Guru Meditation Error: Core 0 panic'ed (Load access fault).
MEPC : 0x42016574   MTVAL : 0x000168d6

反查地址:

makefile 复制代码
0x42016574: lv_obj_get_display          (lv_obj_tree.c:311)
0x42033cea: update_layout_completed_cb (lv_label.c:1081)
0x4202603e: lv_timer_handler

原因:lv_label_set_text() 会往 display 上挂一个「布局完成后刷新文字」的延迟回调 ,回调的 user_data 就是那个 label 的指针。而 LVGL 删除对象时,只清理对象自己的事件列表,不会清理挂在 display 上、以该对象为 user_data 的回调。

于是「刚设置过文字就删屏」就变成:布局一完成,回调拿着已释放的指针去访问 → MTVAL = 0x168d6(一块被复用的垃圾地址)。

修法:页面只建一次,之后只做 lv_screen_load(),运行期绝不删控件。三块页面在启动时各建一次,几十个控件常驻,代价是十几 KB 堆,换来的是这类生命周期 bug 彻底消失。

坑 2:lv_timer 的 repeat_count 归零后不再执行回调

想做「答对停 800ms 再跳题」,于是建了一个一次性定时器:

c 复制代码
lv_timer_t *t = lv_timer_create(cb, 800, NULL);
lv_timer_set_repeat_count(t, 1);

第一次触发后,repeat_count 被减到 0 并自动 pause。之后再 lv_timer_reset() + lv_timer_resume() 都不会执行回调 ------因为 lv_timer_exec() 里是这样的:

c 复制代码
if (timer->timer_cb && original_repeat_count != 0) { timer->timer_cb(timer); }

每次重新武装都必须重设 repeat_count:

c 复制代码
lv_timer_set_repeat_count(t, 1);
lv_timer_reset(t);
lv_timer_resume(t);

后来干脆把定时器删了:答对直接切题,反馈交给声音,状态机更简单。

坑 3:音量调了两次都「没变化」,因为刻度算错了

答对/答错的提示音太吵,我把音量从 75% 调到 64%,用户反馈「还是太大」。

查驱动才发现:esp_codec_dev_set_out_vol() 用的是应用层音量曲线 ,0% = -50dB,100% = 0dB,线性。我拿 ES8311 芯片内部的 -95.5dB ~ +32dB 去换算------那是已经映射过一层之后的寄存器量程,不是百分比刻度。

正确换算:

ini 复制代码
db = percent * 0.5 - 50
  75% = -12.5dB      64% = -18dB      50% = -25dB

所以 75% → 64% 只降了 5.5 dB(幅度约 53%),难怪听不出来。这次直接降到 -25 dB ,并且把波形从方波换成正弦------方波的高次谐波才是「刺耳」的主因,光调音量治不了。

坑 4:编译通过 ≠ 屏幕上有字

LVGL 缺字形时会画一个占位方框,构建日志不会有任何警告。

所以加了一道门禁检查:解析字体文件里的 cmaps 得到覆盖码点,再扫描源码里的中文字符串字面量,缺一个字就让构建失败。

这个检查器本身也踩过坑:最初用正则 "([^"]*)" 直接扫全文,结果注释里的英文引号 被当成字符串边界,把后面注释里的中文也算进了字表,报出一堆假缺字。修法是先剥掉 // 和 /* */ 注释(字符串和字符字面量里的引号仍正确处理),再取字面量。

坑 5:100 个词不能让「Lion」配「car / apple / duck」

词表扩到 100 个之后,如果四个选项全库随机抽,干扰项会荒谬得没有区分度。所以给每个词加了主题字段(农场动物 / 野生动物 / 海洋 / 水果 / 食物 / 交通 / 自然 / 家居),出题时干扰项优先从同一主题里抽,同主题不足 4 个词再退回全库:

c 复制代码
// 先把候选拆成"同主题"和"其它主题"两摞, 同主题优先
while (written < need && same_count > 0) { ... }
while (written < need && other_count > 0) { ... }

这样一局题目集中在同一个主题上,接近 RAZ 分级读物「一本一个主题」的做法。降级路径也有单元测试覆盖(主题只有 2 个词时仍要凑出 4 个不重复选项)。

五、质量保障:一条命令跑完

bash 复制代码
./tools/validate.sh

依次是:仓库规范检查 → 中文字形覆盖 → 音频素材校验 → host 单元测试 → 隔离构建 → 合并镜像。

能纯函数化的逻辑全部做成了不依赖 LVGL / ESP-IDF 的状态机 ,在 Mac 上直接 cc 编出来跑:

  • app_quiz.c:出题、选项、计分、连对、一局边界、同主题干扰项、小主题兜底
  • sound_pcm.c:音调合成(升/降调的过零数、首尾静音、峰值上限)

音频校验也进了门禁:每个词必须有发音、时长 0.15--1.6 秒、峰值不削顶、RMS 够响、首尾必须是静音(否则会爆音)。

六、数据

项 数值
词库 100 词 / 8 主题(13×6 + 11×2)
图标 100 张 48×48 RGB565A8 ≈ 676 KB
单词发音 100 段 16kHz PCM ≈ 1.76 MB
鼓励语 7 句 ≈ 169 KB
中文字形 202 个(16px / 28px 两套位图字体)
固件 app 3.53 MB / 分区 8.23 MB(约 43%)
启动 880 ms 进入首页

8.23 MB 的应用分区用了不到一半,后面加词、加主题、加音效都还有余量。

七、下一步

  • 用 NVS 存「最好成绩」和「最近玩的主题」,结算页显示历史最佳
  • 主题选择页:让孩子自己挑今天玩水果还是动物
  • 答对后播放该词的中文释义(现在只显示文字)
  • 待机时的省电策略

八、写在最后

这套东西最特别的地方不是功能,而是开发方式:需求是口述的(「答对后直接进入下一题」「10 题就结束」「鼓励语放在结算页」),改动由 AI 代理直接落到代码里,然后自己刷机、自己读串口日志定位崩溃、自己跑门禁。

上面五个坑里,有三个是只有真机跑起来才会暴露 的:悬空回调导致的 panic、repeat_count 归零、音量曲线算错。AI 能读日志、反查地址、改代码、再刷机验证,这一轮闭环在十几分钟内就完成了------比人肉试错快得多。

硬件:FoloToy AI Passport · 固件:ESP-IDF 5.5.3 + LVGL 9.5 · 素材:脚本生成,iOS/macOS emoji 与系统 TTS,仅供个人设备使用

相关推荐
黄啊码25 分钟前
从 0 到 1 做 FDE:如何获客、成交、交付?百万级 AI 项目实战复盘
人工智能
玩美数据35 分钟前
海外市场数据服务常用术语与名词解释权威指南
大数据·人工智能
Lansonli1 小时前
Claude Code 写 Shell 脚本翻车实录:一份避坑指南
人工智能
IT_陈寒1 小时前
Vue的这个响应式陷阱,差点让我加班到天亮
前端·人工智能·后端
东离与糖宝2 小时前
Spring AI 大模型容易混淆概念一次性讲透
人工智能
复杂网络与机器学习3 小时前
全双工 160 毫秒一格:李沐让模型不再「一问一答」
人工智能
武子康3 小时前
Qwen-Drive 的 91.4 分,究竟选中了什么?
大数据·人工智能·agent
王中阳Go4 小时前
你把重试加到 2 次那天,200 条里成功还是 181 条,你回看过吗?
人工智能·后端·架构
北杨4 小时前
三键之内的意图:用 AIUI 在 Rokid 眼镜上做一个能玩的 21 点
人工智能