第2章:开发环境搭建(PlatformIO + ESP-IDF)

工欲善其事,必先利其器——把阅读器固件编译、烧录到你的板子上

🔨

本章导师:鲁班

核心方法论:工欲善其事,必先利其器

「做木工先得把刨子磨快,做嵌入式先得把构建环境理顺。PlatformIO 把 ESP-IDF 那棵庞大的依赖树藏在了身后,但你必须知道它藏了什么:哪些是它替你包的,哪些需要你自己伸手进去调。这一章,我们把这把刨子磨到你闭着眼睛都能装好、用好。」

2.1 PlatformIO 与 ESP-IDF 的关系

一句话概括:ESP-IDF 是乐鑫官方的 ESP32 开发框架(提供驱动、WiFi、FreeRTOS 等 API 与一套构建系统,以 esp-idf 的方式组织整个工程),而 PlatformIO 是跨平台的嵌入式开发工具(CLI + VSCode 插件),编译、依赖、烧录这些活都归它管。这个项目的 README 说得直白:构建需要 "VSCode with the PlatformIO extension installed"。PlatformIO 负责把 ESP-IDF 背后的工具链下载、编译(把 C/C++ 源码变成能烧进芯片的二进制)、链接、烧录(把编译产物写到芯片 Flash)这些苦力活全部接管,让你只关心代码本身。

这份关系的"契约"就写在 platformio.ini 里。看 [common] 段:platform = espressif32 @ 3.3.2 锁定了乐鑫工具链平台及其版本(PlatformIO 会据此下载对应的编译器和 SDK,@ 3.3.2 是精确到小版本的版本约束);framework = espidf 选择原生 ESP-IDF API(而不是 Arduino 封装——这是两套完全不同的编程接口);board = esp32dev 提供默认的 Flash 大小、时钟等板级参数;下面还有 monitor_speed = 115200monitor_filters = esp32_exception_decoder(串口监视器把 panic 地址反编译成可读的调用栈)以及 lib_deps(声明第三方库依赖,TinyXML2 就在这里拉取)。

这三个键(platform / framework / board)是读懂整个文件的钥匙:改平台 = 换芯片家族;改框架 = 换 API 层;改板子 = 换默认参数。本项目只用一套芯片(ESP32)和一套框架(IDF),真正的变化全部发生在"板子与构建宏"这一层——这正是 2.2 多环境的由来。

; platformio.ini:平台 / 框架 / 板子三件套
[platformio]
; 默认构建环境:lilygo_t5_47 | epdiy | native | m5_paper
default_envs = lilygo_t5_47

[common]
platform = espressif32 @ 3.3.2
framework = espidf
board = esp32dev
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
lib_deps =
  https://github.com/leethomason/tinyxml2.git
build_flags =
  -Ofast
  -D__MCUXPRESSO
  -DMINIZ_NO_ZLIB_COMPATIBLE_NAMES
  -DBOARD_HAS_PSRAM
  -D LOG_ENABLED
鲁班提示

platform = espressif32 @ 3.3.2 里的版本号不是装饰——PlatformIO 会下载并缓存该版本的整套工具链。不同项目锁定不同版本时,构建结果可能不同。遇到"别人能编、我不能编"的怪问题,先核对 platform 的版本号是否一致。

2.2 多种环境:读懂四套 env

同一份源码编译出三块不同板子的固件,靠的是 platformio.ini 里 4 个 [env:*] 段 + 预处理宏。env(环境)就是 platformio.ini 里一套可独立编译的配置,对应一块板或一个构建目标。首行 default_envs = lilygo_t5_47 说明:不指定环境时,默认构建 LilyGo T5 4.7" 的固件。每个环境都 extends = esp32_common 继承公共配置,再叠加属于自己的 build_flags

这些 -D 开头的行就是 build flags(编译宏):以 -D 传给编译器的定义,在编译期生效,用来开关功能或告诉代码自己在为哪块板编译。以 [env:lilygo_t5_47] 为例逐条拆:-DBOARD_TYPE_LILIGO_T5_47Board::factory() 在编译期实例化 Lilygo_t5_47-DCONFIG_EPD_DISPLAY_TYPE_ED047TC2-DCONFIG_EPD_BOARD_REVISION_LILYGO_T5_47 来自 EPDiy 库,声明屏幕型号与板卡修订;SD 卡用 4 个 GPIO_NUM_* 宏定义 SPI 引脚(MISO14 / MOSI13 / CLK15 / CS12);-DBATTERY_ADC_CHANNEL=ADC1_CHANNEL_0 告诉电池模块用哪个 ADC 通道读电压分压(GPIO_NUM_36);-D USE_SPIFFS 启用 SPIFFS 文件系统(ESP32 上的一个小型文件系统,烧在 Flash 里,掉电不丢);-D USE_L58_TOUCH 加上 CONFIG_TOUCH_SDA/SDL/INT 打开触摸屏。

再看另两个板子环境,这套宏的"作用域"就清楚了。[env:epdiy] 面向 EPDiy V6 开发板:屏幕换成 CONFIG_EPD_DISPLAY_TYPE_ED060XC3(6")、CONFIG_EPD_BOARD_REVISION_V6,还多了一个 CONFIG_EPD_DRIVER_V6_VCOM=1580(VCOM 电压),SD 引脚完全不同(MISO36 / MOSI0 / CLK13 / CS14),并且 lib_ignore = touch / FT6X36 把触摸库从编译中剔除。[env:m5_paper] 则复用了 ED047TC2 的屏幕定义,只改 SD 引脚(MISO13 / MOSI12 / CLK14 / CS4)与 ADC 通道(ADC1_CHANNEL_7)。规律一目了然:屏幕型号决定渲染代码,引脚宏决定外设,lib_ignore 决定链接哪些库——三层全部在编译期用宏切分。

; platformio.ini:[env:lilygo_t5_47](default_envs)
[env:lilygo_t5_47]
extends = esp32_common
build_flags =
  ${common.build_flags}
  ; Based on the EPDIY with some minor changes
  -DBOARD_TYPE_LILIGO_T5_47
  ; Setup display format and model via build flags
  -DCONFIG_EPD_DISPLAY_TYPE_ED047TC2
  -DCONFIG_EPD_BOARD_REVISION_LILYGO_T5_47
  ; setup the pins for the SDCard
  -DSD_CARD_PIN_NUM_MISO=GPIO_NUM_14
  -DSD_CARD_PIN_NUM_MOSI=GPIO_NUM_13
  -DSD_CARD_PIN_NUM_CLK=GPIO_NUM_15
  -DSD_CARD_PIN_NUM_CS=GPIO_NUM_12
  ; the adc channel that is connected to the battery voltage divider
  -DBATTERY_ADC_CHANNEL=ADC1_CHANNEL_0
  ; use SPIFFS - experimental feature
  -D USE_SPIFFS
  ; Touch configuration
  -D USE_L58_TOUCH
  -D CONFIG_TOUCH_SDA=15
  -D CONFIG_TOUCH_SDL=14
  -D CONFIG_TOUCH_INT=13
; platformio.ini:[env:native]——在本机跑单元测试
[env:native]
platform = native
test_build_project_src = false
targets = test
build_flags =
  -std=c++11
  -D__MCUXPRESSO
lib_deps =
  https://github.com/leethomason/tinyxml2.git
lib_ignore =
  touch
  epdiy
  sd_card
  spiffs
  FT6X36
  m5Paper
debug_test = *
环境目标板屏幕关键宏备注
[env:lilygo_t5_47]LilyGo T5 4.7"ED047TC2BOARD_TYPE_LILIGO_T5_47USE_SPIFFSUSE_L58_TOUCHdefault_envs;SD 引脚 MISO14/MOSI13/CLK15/CS12
[env:epdiy]EPDiy V6 开发板ED060XC3BOARD_TYPE_EPDIYBOARD_REVISION_V6VCOM=1580lib_ignore touch、FT6X36
[env:m5_paper]M5 PaperED047TC2BOARD_TYPE_M5_PAPERADC1_CHANNEL_7复用 T5 屏幕定义;SD 引脚 MISO13/MOSI12/CLK14/CS4
[env:native]本机(macOS/Linux)-std=c++11只跑 test/ 单元测试;lib_ignore 全部硬件库
注意

README 里的示例写的是 -DCONFIG_EPD_DISPLAY_TYPE_ED047TC1,但 platformio.ini 里实际是 ED047TC2。这也是文档与代码漂移的又一例——记住:永远以 platformio.inisdkconfig.*(ESP-IDF 的配置项文件,Kconfig 风格,记录着各类选项)为准。

鲁班提示

想亲眼看某套环境展开后的完整配置?在项目根执行 pio project config 会打印所有 [env:*] 段继承展开后的结果;加 -vpio run -e m5_paper -v 则会打印完整编译命令,所有 -D...=... 一览无余。

2.3 构建与烧录

环境选定之后,命令就非常简单了。pio run 构建默认环境(也就是 lilygo_t5_47);pio run -e epdiy 指定环境;pio run -t upload 编译并烧录固件;pio run -t uploadfs 只上传文件系统;日志则用 pio device monitor 查看。值得注意的是 [common] 里配的 monitor_filters = esp32_exception_decoder——它会把 ESP32 panic 时的地址反编译成可读的调用栈,是排查段错误最趁手的工具。

为什么还需要 uploadfs?因为固件只负责"读书",书本身存在文件系统里data/ 下那两本古登堡 EPUB(pg43-images.epubpg14838-images.epub)会被打进 SPIFFS 镜像,uploadfs 把它们烧进 partitions.csv 划分出的 spiffs 分区。看分区表:app00x120000(约 1.125 MB),spiffs0x2D0000(约 2.8 MB)——所以"能装几本书"取决于分区表,而不是你有多大的 SD 卡。

第三条路是 [env:native]:它不碰任何硬件,targets = test 让 PlatformIO 直接在本机编译并运行 test/ 下的单元测试(rubbish_html_parsertinyxml_html_parserepub_load 等),相关硬件库全部 lib_ignore。这意味着你可以不插任何板子,先在电脑上验证解析逻辑是否正确。另外 README 也提醒过:SPIFFS 在"保存深睡眠恢复状态"上有已知问题,能上 SD 卡就尽量上 SD 卡。

# 构建默认环境(lilygo_t5_47)
pio run

# 构建指定环境
pio run -e m5_paper

# 编译并烧录固件
pio run -t upload

# 上传 SPIFFS 文件系统(把 data/ 里的两本书烧进分区)
pio run -t uploadfs

# 打开串口监视器(配合 esp32_exception_decoder)
pio device monitor

# 在本机跑单元测试([env:native])
pio test -e native
鲁班提示

第一次烧录,先只执行 pio run -t upload 刷固件、用 pio device monitor 看启动日志(串口默认 115200)。确认屏幕上出现书目列表后,再 pio run -t uploadfs 上传两本书。一步到位反而不好排查问题。

2.4 依赖与子模块

除了 lib_deps 里由 PlatformIO 自动拉取的 TinyXML2,这个项目还依赖三个 git 子模块(git submodule 就是把别的仓库嵌进本项目的一种引用方式),记录在根目录的 .gitmodules 里:lib/epdiymartinberlin/epdiy-rotation,屏幕驱动)、lib/touchmartinberlin/FT6X36-IDF,触摸驱动)、lib/png/PNGdecatomic14/PNGdec,PNG 解码)。子模块不会随普通 git clone 自动检出内容,所以 README 特意要求 git clone --recursive

怎么判断自己踩没踩这个坑?ls lib/epdiy——如果它是空目录,说明子模块根本没下来。漏掉 --recursive 的后果很直接:PlatformIO 编译时找不到 epdiy 的头文件,include 第一步就报错。补救也简单:在仓库根执行 git submodule update --init --recursive,内容会立刻补全。

子模块与 lib_deps 其实是两种依赖哲学lib_deps 由构建工具去网上拉取,版本跟着远端走(这里就是 master 分支),灵活但不够可复现;git 子模块则把版本钉在你的仓库提交里,对"换一台电脑还能编出一样的东西"更友好。同理,仓库里随提交的三份 sdkconfig.* 把 ESP-IDF 的完整配置也一起锁进了版本库——这样你不需要手动 idf.py menuconfig,也能得到与作者一致的构建。

; .gitmodules:三个 git 子模块
[submodule "lib/png/PNGdec"]
	path = lib/png/PNGdec
	url = https://github.com/atomic14/PNGdec.git
[submodule "lib/touch"]
	path = lib/touch
	url = https://github.com/martinberlin/FT6X36-IDF.git
[submodule "lib/epdiy"]
	path = lib/epdiy
	url = https://github.com/martinberlin/epdiy-rotation.git
依赖类型用途
tinyxml2lib_deps(GitHub 自动拉取)XML / XHTML 解析
lib/epdiygit 子模块电子墨水屏驱动(epdiy-rotation fork)
lib/touchgit 子模块FT6X36 触摸驱动(LilyGo 用)
lib/png/PNGdecgit 子模块PNG 解码
miniz / tjpgd3 / sd_card / spiffs仓库自带 lib/zip 解压 / JPEG 解码 / 存储
注意

克隆仓库后请先执行 ls lib/epdiy lib/touch 确认子模块已检出。发现是空目录,立刻 git submodule update --init --recursive。这是本仓库编译失败最常见的原因,没有之一。

章末练习

练习 1:读配置 入门

打开 platformio.ini,回答:(a) 不指定环境时默认构建哪个 env?(b) monitor_speed 是多少?(c) 把 monitor_speed 改成 921600 会发生什么?

提示

[platformio][common] 两段;想想监视器波特率与固件 UART 波特率的关系。

参考答案

(a) default_envs = lilygo_t5_47;(b) 115200;(c) 串口监视器与固件的 UART 波特率不匹配,日志会变成乱码/花屏——除非你同时把固件里串口的波特率改成 921600。

练习 2:拆 build flags 进阶

逐条解释 [env:epdiy] 里下面几行的作用:CONFIG_EPD_DISPLAY_TYPE_ED060XC3CONFIG_EPD_DRIVER_V6_VCOM=1580lib_ignore = touch / FT6X36。再结合 src/boards/Board.cppfactory(),说明 BOARD_TYPE_EPDIY 是如何参与"选板"的。

提示

前两个宏来自 EPDiy 库本身;lib_ignore 是 PlatformIO 的特性;factory()#ifdef 匹配。

参考答案

两个宏告诉 EPDiy 用哪款 6" 屏幕(ED060XC3)以及 V6 板卡的 VCOM 电压(1580 mV);lib_ignore 让 touch、FT6X36 两个库不参与编译——EPDiy 板用 GPIO 按键导航,不需要触摸库。而 BOARD_TYPE_EPDIYBoard.cppfactory() 里被 #ifdef BOARD_TYPE_EPDIY 匹配,编译期决定 new Epdiy()

练习 3:移植新板子 挑战

参照 README 的 "Porting to other boards",给一块"未知品牌、但同样是 EPDiy 并行墨水屏"的板子做移植。写出最小改动清单,并说明哪些步骤是硬性必须、哪些可以靠宏省掉。

提示

两块工作:新建板类 + 新增 env;但如果它兼容 EPDiy,板类这一步可以用宏替代。

参考答案

(1) 在 src/boards/ 新建板类,实现 Board.h 的纯虚方法(power_upprepare_to_sleepget_rendererget_button_controls),并把它加进 factory();(2) 在 platformio.ini 新增 [env:my_board],写入屏幕型号、SD 引脚、ADC 通道等宏;(3) 若这块屏兼容 EPDiy,第 1 步可省——只需 -DBOARD_TYPE_EPDIY + CONFIG_EPD_DISPLAY_TYPE_* 等宏,工厂直接复用 Epdiy 板。硬性必须的是"板类或 EPDiy 宏二选一"加上"新增 env"。