1. 项目缘起与整体设计思路
1.1 一个“等不及”催生的复刻计划
事情的起因特别简单:某开源硬件社区里有个叫 microduck 的小玩意儿火了,众筹页面上的发货日期一拖再拖,评论区从“期待”变成“催更”再变成“算了我自己来”。我属于第三类人。翻了一圈资料发现,这东西的核心逻辑并不复杂——一块小屏幕、一个主控、一套能根据 AI 输出内容实时切换表情的固件,再加上一个能把它立起来的底座。于是就有了这个 HachimoDock 的复刻计划。
HachimoDock 这个名字拆开看很直白:Hachimo 是那只跟着 AI 变脸的小家伙,Dock 是它的底座。合起来就是一个带屏幕的桌面小摆件,屏幕上的表情会随着你接入的 AI 服务返回的内容发生变化。你问它一句话,它思考的时候是眯眼,回答的时候是张嘴,被夸的时候会笑,被怼的时候会翻白眼。说白了,它把“AI 在跟你交互”这件事从纯文字变成了一个有情绪反馈的物理存在。
这个项目适合谁?如果你手里有闲置的开发板、会一点焊接、能看懂 Arduino 或 MicroPython 的基础代码,那完全可以跟着做。如果你是完全零基础的小白,也别急着关页面,我会把每个环节拆到“拧螺丝”的粒度,你照着抄作业就行。整个项目从备料到点亮大约需要两个周末,成本控制在两百元以内,比等众筹发货靠谱多了。
1.2 为什么选择复刻而不是等成品
这里得说清楚一个逻辑:复刻开源硬件和买成品是两条完全不同的路径。买成品你得到的是一个黑盒,能用但不知道里面发生了什么;复刻你得到的是一个白盒,每一行代码、每一根线你都知道它为什么在那儿。microduck 这类产品的核心价值不在于硬件本身有多精密,而在于“AI 输出到表情映射”这套逻辑的调校。你只有自己动手做一遍,才能理解为什么某个表情要延迟 200 毫秒再切换,为什么屏幕刷新率不能设太高,为什么供电不稳会导致表情卡成 PPT。
另一个现实原因是成本。众筹价加上国际运费和可能的关税,到手差不多要四百到五百元。而自己复刻,主控用常见的 ESP32 系列,屏幕用 1.28 寸或 1.54 寸的圆屏,加上底座结构件和几根线,物料成本能压到一百五到两百元之间。省下来的钱够你再买两块屏幕做备用。更重要的是,复刻过程中你会被迫学会怎么选屏幕驱动、怎么配网络、怎么调 AI 接口的返回格式,这些技能迁移到其他项目上一样管用。
1.3 整体架构的取舍与考量
整个系统的架构可以分成三层:感知层、决策层、表现层。感知层负责接收你的输入,可以是一个物理按键、一个触摸屏,或者干脆通过串口从电脑发指令;决策层是主控芯片,它要把你的输入打包成请求发给 AI 服务,然后解析返回的文本;表现层就是那块小屏幕,根据解析结果播放对应的表情动画。
这里有个关键取舍:AI 请求放在主控上直接发,还是通过电脑中转?直接发的好处是设备独立,插上电就能用;坏处是主控要处理网络连接、API 鉴权、JSON 解析,对内存和算力有要求。中转的好处是主控只负责显示,逻辑都在电脑上跑,简单稳定;坏处是设备离不开电脑。我最终选了直接发,因为 HachimoDock 的定位是桌面摆件,独立运行才符合它的气质。为了降低难度,我用了 ESP32-S3 这颗芯片,它自带 Wi-Fi、内存够大、社区支持好,踩坑了也容易搜到答案。
屏幕选型上,我试过三种:1.28 寸圆形 IPS 屏、1.54 寸方形屏、0.96 寸 OLED。最后留在方案里的是 1.28 寸圆屏,原因是圆屏更符合“脸”的视觉隐喻,而且这块屏的驱动芯片 GC9A01 资料齐全,刷新率够用,颜色也比 OLED 鲜艳。方形屏虽然便宜,但做表情时四个角会显得很愣。OLED 省电但尺寸太小,远看就是一粒米,没有存在感。
2. 核心细节解析与实操要点
2.1 主控与屏幕的匹配逻辑
选主控不是越贵越好,而是要看它和屏幕的匹配度。GC9A01 这块圆屏走的是 SPI 接口,需要主控有足够的 GPIO 和稳定的 SPI 时钟。ESP32-S3 的 SPI 时钟可以跑到 80MHz,刷一张 240x240 的图只需要几毫秒,动画过渡很顺。如果你用 ESP8266,SPI 时钟上限低一些,刷屏时会有肉眼可见的撕裂感。这不是说 ESP8266 不能用,而是你要接受动画没那么丝滑。
另一个匹配点是内存。一张 240x240 的 16 位色图片,未压缩时占 115200 字节,差不多 112KB。ESP32-S3 有 512KB 的 SRAM,放几张表情图绰绰有余。ESP8266 只有 80KB 左右,你得把图片存到外部 Flash 里再分块读取,代码复杂度直接翻倍。所以我的建议很明确:做表情类项目,主控内存至少 256KB 起步,别在这上面省钱。
接线方面,GC9A01 需要 7 根线:VCC、GND、SCL、SDA、RES、DC、CS。如果你买的模块还带背光控制,那就再多一根 BLK。我习惯把 BLK 直接接 3.3V,省一个 GPIO,代价是不能调亮度。如果你想让屏幕在夜里暗一点,那就把 BLK 接到一个支持 PWM 的引脚上,代码里用analogWrite或ledcWrite调占空比。实测下来,占空比设到 30% 左右,白天能看清,晚上不刺眼。
2.2 表情映射的设计原则
这是整个项目里最“软”的部分,也是最见功力的地方。AI 返回的是一段文字,你怎么把它变成表情?我的做法是关键词匹配加情绪打分。先定义一个情绪表,比如“开心”对应关键词“哈哈、太好了、棒、喜欢”,“生气”对应“讨厌、烦、不行、错误”,“思考”对应“嗯、让我想想、可能、也许”。每次收到 AI 回复,先扫一遍关键词,命中哪个情绪就加一分,最后取分数最高的那个。
但这里有个坑:AI 的回复往往很长,一句话里可能同时出现“太好了”和“但是”。如果你只取最高分,可能会把整体偏负面的回复判成开心。我的解决办法是给关键词加权重,并且看它出现的位置。句首的关键词权重高,句尾的权重低;转折词后面的关键词权重翻倍。这套逻辑不复杂,但调参需要耐心。我大概花了一个晚上,对着五十条测试回复反复调整权重,最后准确率能到八成左右。
还有一个细节是表情切换的时机。你不能等整段回复都解析完再切表情,那样会有明显的延迟感。我的做法是流式解析:AI 每返回一个片段,就立刻做一次快速匹配,如果命中高权重关键词就马上切表情,同时启动一个定时器,如果 1.5 秒内没有新关键词命中,就回落到默认的“待机”表情。这样交互起来会感觉小家伙反应很快,而不是傻等。
2.3 供电与底座的隐藏问题
供电是很多新手会忽略的环节。ESP32-S3 加屏幕,峰值电流能到 300mA 左右。如果你用电脑的 USB 口供电,一般没问题;但如果你用那种便宜的 USB 充电头,电压不稳会导致屏幕闪烁甚至主控重启。我实测过一个标称 5V/1A 的充电头,带载后电压掉到 4.6V,屏幕就开始随机闪白。换了一个 5V/2A 的之后,问题消失。所以电源这一块,余量要给足,别卡着最低要求配。
底座的设计也有讲究。HachimoDock 的“Dock”部分不只是个架子,它还要藏线、配重、散热。我用 3D 打印做了一个斜角底座,屏幕朝上倾斜 15 度,这样坐在桌前看过去正好是平视。底座内部留了一个小格子放配重块,我用的是几枚一元硬币用胶带缠在一起,重量刚好让设备不会被线拽倒。散热方面,ESP32-S3 发热不大,但如果你把屏幕亮度拉满又长时间跑动画,背面还是会温温的。底座背面开了几排小孔,空气能对流就行,不需要风扇。
注意:3D 打印底座时,屏幕开孔的尺寸要比屏幕实际尺寸大 0.5mm 左右,给装配公差留余量。我第一版按精确尺寸打,结果屏幕塞不进去,用锉刀磨了半天。
3. 实操过程与核心环节实现
3.1 物料清单与工具准备
先把要买的东西列清楚,免得做到一半发现少零件。下面这张表是我实际用到的物料,价格按常见渠道估算,仅供参考。
| 物料名称 | 规格说明 | 数量 | 参考单价 |
|---|---|---|---|
| 主控板 | ESP32-S3 开发板,带 USB-C | 1 | 35 元 |
| 圆屏模块 | 1.28 寸 IPS,GC9A01 驱动,240x240 | 1 | 45 元 |
| 杜邦线 | 母对母,10cm | 7 根 | 2 元 |
| 按键模块 | 6x6mm 轻触按键 | 2 | 1 元 |
| 底座结构件 | 3D 打印,PLA 材质 | 1 | 15 元 |
| 配重块 | 硬币或金属片 | 若干 | 忽略 |
| USB 电源 | 5V/2A 充电头加线 | 1 | 20 元 |
| 合计 | 约 118 元 |
工具方面,你需要一把电烙铁(如果买的是排针未焊接的模块)、一把斜口钳、一把镊子、一台电脑。如果你买的是已经焊好排针的模块,那连烙铁都可以省了。我建议新手直接买焊好的,多花几块钱省很多事。
3.2 硬件连接与初步点亮
接线这一步,我建议先用杜邦线在面包板上搭一遍,确认能点亮再考虑焊接或做成品。GC9A01 和 ESP32-S3 的对应关系如下:
| 屏幕引脚 | ESP32-S3 引脚 | 说明 |
|---|---|---|
| VCC | 3.3V | 供电 |
| GND | GND | 共地 |
| SCL | GPIO 12 | SPI 时钟 |
| SDA | GPIO 11 | SPI 数据 |
| RES | GPIO 10 | 复位 |
| DC | GPIO 9 | 数据/命令选择 |
| CS | GPIO 8 | 片选 |
| BLK | 3.3V | 背光常亮 |
接好之后,先别急着写表情逻辑,跑一个最简单的刷屏测试。用 Arduino IDE 装好 TFT_eSPI 库,在 User_Setup.h 里把驱动改成 GC9A01,引脚按上表填好。然后跑一段填充红绿蓝的代码,如果屏幕能正常变色,说明硬件通路没问题。这一步看着简单,但能帮你排除掉八成以上的接线错误。
#include <TFT_eSPI.h> TFT_eSPI tft = TFT_eSPI(); void setup() { tft.init(); tft.setRotation(0); tft.fillScreen(TFT_RED); delay(1000); tft.fillScreen(TFT_GREEN); delay(1000); tft.fillScreen(TFT_BLUE); } void loop() { }提示:如果屏幕白屏或花屏,先检查 CS 和 DC 有没有接反,再检查 SPI 频率是不是设太高。把频率降到 20MHz 试试,如果正常了再往上加。
3.3 AI 接口的接入与解析
硬件通了之后,下一步是让主控能跟 AI 服务对话。我用的是 HTTP 请求的方式,主控把用户输入打包成 JSON,通过 Wi-Fi 发出去,然后解析返回的 JSON 取文本字段。这里的关键是 JSON 解析库的选择。ArduinoJson 是首选,它内存占用可控,解析速度快。但你要注意版本,6.x 和 7.x 的 API 有差异,网上很多示例是 6.x 的,你装 7.x 会编译报错。
请求的构造大概长这样:
DynamicJsonDocument doc(1024); doc["model"] = "your-model-name"; doc["input"] = userInput; String payload; serializeJson(doc, payload); http.begin(apiUrl); http.addHeader("Content-Type", "application/json"); http.addHeader("Authorization", "Bearer " + apiKey); int code = http.POST(payload); String response = http.getString();拿到 response 之后,用 ArduinoJson 解析出文本内容,然后送进情绪匹配函数。这里有个内存管理的坑:DynamicJsonDocument 的大小要按实际返回内容调整。如果你设了 1024 字节,但返回的 JSON 有 2KB,解析就会失败。我的做法是先用一个较大的缓冲区(比如 4096),解析成功后立刻把需要的文本复制出来,然后释放 JSON 对象。ESP32-S3 内存够,但养成好习惯没坏处。
3.4 表情动画的实现与优化
表情动画我用了两种方式结合:静态图切换和简单几何动画。静态图就是预先做好几张 240x240 的图片,存成数组,切换时直接刷屏。几何动画是用绘图函数画圆、画弧、画点,比如“思考”表情就是三个点轮流闪烁,“开心”是嘴巴弧度变大。静态图表现力强但占内存,几何动画省内存但效果朴素。我最后是混合用:待机和开心用静态图,思考和生气用几何动画。
刷屏优化有个小技巧:不要每次全屏刷新,只刷新变化区域。比如眼睛部分只占屏幕上半部分,那你就只重绘上半部分。TFT_eSPI 提供了setAddrWindow和pushColors,可以指定区域更新。实测下来,局部刷新能把帧率从 15fps 提到 30fps 以上,动画明显更跟手。
// 局部刷新示例:只更新眼睛区域 tft.setAddrWindow(60, 40, 120, 60); tft.pushColors(eyeBuffer, 120 * 60, true);另一个优化点是图片压缩。240x240 的 16 位图,一张就 112KB,存五张就 560KB,ESP32-S3 的 Flash 一般有 8MB,存是存得下,但读取速度会拖慢动画。我的做法是把图片转成 RLE 压缩格式,读取时解压再刷屏。压缩率大概能到 50% 左右,代价是解压要消耗一点 CPU 时间。对于静态表情切换来说,这点开销完全可以接受。
4. 常见问题与排查技巧实录
4.1 屏幕相关故障速查
屏幕不亮、花屏、闪屏是最高频的问题。我整理了一张速查表,按现象对原因:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全黑屏 | 供电没接好 | 万用表量 VCC 和 GND 之间电压 |
| 白屏无内容 | 驱动型号选错 | 检查 User_Setup.h 里的驱动宏 |
| 花屏条纹 | SPI 频率过高 | 降到 10MHz 试,逐步往上加 |
| 随机闪白 | 电源带载能力不足 | 换 5V/2A 以上电源 |
| 颜色反了 | 颜色顺序设置错误 | 改 TFT_RGB_ORDER 宏 |
| 显示偏移 | 屏幕偏移参数不对 | 调整 setRotation 和偏移量 |
其中“随机闪白”最容易被误判成代码问题。我一开始以为是 SPI 时序不对,调了半天参数没用,后来拿示波器看电源纹波才发现是供电问题。所以遇到随机性故障,先怀疑硬件,再怀疑软件。
4.2 网络连接与接口调用的坑
Wi-Fi 连接这块,ESP32-S3 支持 2.4G 和 5G,但有些老路由器对 5G 的兼容性不好,连上了但丢包严重。我的建议是优先连 2.4G 频段,稳定性明显更好。另外,如果你在代码里用了WiFi.begin(ssid, password),记得加超时判断,别让它死等。我见过有人因为路由器没开,主控卡在连接循环里,屏幕一直停在待机表情,还以为程序跑飞了。
接口调用方面,最常见的问题是鉴权失败和超时。鉴权失败一般是 API Key 填错或者请求头格式不对,检查Authorization字段有没有多余空格。超时的话,把http.setTimeout设到 10000 毫秒以上,因为有些 AI 服务响应比较慢,默认的 5 秒不够用。如果还是超时,考虑把请求改成流式,边收边解析,这样首字节到达时间会短很多。
注意:不要把 API Key 硬编码在代码里然后上传到公开仓库。我习惯把它放在一个单独的 config.h 文件里,然后把这个文件加到 .gitignore 中。虽然麻烦一点,但安全。
4.3 表情切换不跟手的调优经验
表情切换延迟是体验的杀手。你问了一句话,等了半秒屏幕才变脸,那种“智能感”就没了。我踩过的坑包括:解析整段 JSON 后才切表情、图片解码耗时太长、刷屏和网络请求在同一个任务里互相阻塞。解决办法有三个:一是流式解析,收到片段就匹配;二是把图片预解码到内存,切换时直接推屏;三是用 FreeRTOS 把网络任务和显示任务分到不同核心上,ESP32-S3 是双核的,一个核跑网络,一个核跑显示,互不干扰。
实测下来,这三招用完,从 AI 返回首字节到屏幕变脸,延迟能压到 200 毫秒以内。人的感知阈值大概在 100 到 150 毫秒,200 毫秒虽然能感觉到一点点,但已经不影响“跟手”的感觉了。如果你追求极致,可以把常用表情的图片数据放在 PSRAM 里,读取速度比 Flash 快一个数量级。
4.4 结构装配与日常使用的细节
装配的时候,屏幕和底座之间最好垫一层薄海绵或泡棉胶,一是防尘,二是防止屏幕受压产生水波纹。我第一版没垫,用了两周屏幕边缘就出现了一条暗线,拆开发现是底座边缘压到了排线。后来加了 1mm 厚的泡棉垫,问题再没出现过。
日常使用中,如果你打算让它一直亮着,建议设置一个自动息屏时间。我设的是 5 分钟无交互就熄屏,按一下按键唤醒。这样屏幕寿命会长很多,也不至于夜里晃眼。另外,固件更新最好留一个 OTA 接口,不然每次改代码都要拆底座插 USB,麻烦得很。ESP32-S3 支持 Arduino OTA,几行代码就能加上,后续调试省心不少。
最后分享一个我个人的小习惯:我会在底座底部贴四个硅胶脚垫,一来防滑,二来把设备垫高一点,让底部空气流通。这东西几块钱一包,但体验提升很明显。整个项目做下来,最花时间的不是写代码,而是调表情映射的权重和等 3D 打印。如果你也想复刻一个,建议先把硬件点亮,再慢慢磨软件,别想着一次到位。