给朋友做第一块看板的时候,我把源码打包发过去,让他自己用 Arduino IDE 编译。结果他那边连着报了四五轮错:库版本不一样、某个 API 的参数签名不一样、板卡支持包版本不同导致的头文件找不到。每失败一次,他要把报错截图发我、我改、他再编,一天就这么过去了。后来我彻底放弃了让使用者自己编译这条路。
为什么本地编译总在折腾
Arduino IDE 的问题不在它本身,而在于每个使用者的环境都是独立的:库管理器里装了什么版本、板卡支持包更新到哪一版、有没有装过别的项目的库、编译缓存有没有污染。你在自己机器上一次编过的代码,换个环境就不一定编得过。而这类错误对使用者来说完全不可理解,他们看到的只是一堆红字。
更麻烦的是"试错"这件事本身要人守。一次编译几分钟,失败要人看一眼、改一改、再编。让使用者做这件事,等于把开发者的调试循环外包给了一个没有上下文的陌生人。
改到服务器上:pio run 一次编好
我的做法是在服务器上装 PlatformIO,源码和 platformio.ini 都放在仓库里,每次改完代码就在服务器上跑 pio run。报错信息直接进日志,我看日志改源码,编过了再把产物交给使用者。使用者要做的事只剩下"把这个文件烧进去",不需要装任何东西、不需要理解任何编译错误。
PlatformIO 的好处是依赖版本写死在配置里、库会自动拉到项目的 .pio 目录,环境是可复现的。同一份 platformio.ini 在服务器上编出来的东西,任何时候再编都能得到一致的结果。
# platformio.ini
[env:esp32c3]
platform = espressif32
board = esp32-c3-devkitm-1
framework = arduino
monitor_speed = 115200
# 服务器上编译
pio run -e esp32c3
第一次编的时候国内下载工具链很慢,经常卡在拉编译器那一步。值得先配一下国内镜像,把包索引指向镜像站,后面每次编译都省时间。这个改动看起来是小事,但它决定了你愿不愿意在服务器上频繁编译。
最容易踩的一脚:交付的不是 app 镜像
编完之后产物目录里有 firmware.bin,我一开始就直接把它发给人家,说"烧到 0x0"。结果是设备无限重启,串口日志里能看到它反复复位。查了半天才明白:ESP32 系列的 firmware.bin 是应用镜像,它必须住在 0x10000 这个位置,因为前面的地址是 bootloader 和分区表。你把应用镜像刷到 0x0,芯片启动时在 0x0 找 bootloader,找到的是应用代码,自然起不来。
正确的交付物是整机合并包:bootloader、分区表、应用镜像按各自的偏移拼成一个 bin,烧录时只烧这一个文件到 0x0。用 esptool.py merge_bin 就能做这件事,偏移量要和分区布局对应上。
# 1) 编译
pio run -e esp32c3
# 2) 合并成整机包(烧到 0x0)
esptool.py --chip esp32c3 merge_bin -o firmware-merged.bin \
--flash_mode dio --flash_size 4MB \
0x0 .pio/build/esp32c3/bootloader.bin \
0x8000 .pio/build/esp32c3/partitions.bin \
0x10000 .pio/build/esp32c3/firmware.bin
# 3) 使用者只需要这一步
# esptool.py --chip esp32c3 write_flash 0x0 firmware-merged.bin
这几个偏移(0x0、0x8000、0x10000)是这块板子默认分区布局下的位置,换板子或者换分区表时要重新确认,别照抄。合并包的好处是使用者只需要一条命令、一个文件,写错地址的概率大大降低。
现在交付的流程
我现在的流程固定成四步:改代码 → 服务器 pio run → merge_bin 出整机包 → 把包和一条烧录命令给使用者。中间任何一步出错都是我这边看日志解决,不经过使用者。
顺带说一句,PlatformIO 的编译缓存也留好,第二次编译会快很多;但如果你要"干净复现",记得先清一次构建再编,避免缓存掩盖了依赖问题。我把这两条也写在了仓库的说明里,省得以后忘了。
小结
把编译从使用者手里拿走,是这个项目里我做过最省事的一个决定。它的本质不是技术选择,而是把"需要专业知识才能诊断的失败"留给自己。使用者面对的最坏情况从"编译失败但不知道原因"变成了"烧录失败,大概率是没进下载模式",后者一句话就能解释清楚。