前置知识:了解什么是嵌入式系统(第 1 章),对 CrossPoint Reader 的硬件组成有基本印象(第 2 章)。 本章目标:搭建完整的开发环境,能够编译、烧录、监控 CrossPoint Reader 固件。

3.1 PlatformIO 是什么

如果你之前接触过 Arduino,大概率用过 Arduino IDE——那个绿色的编辑器。它对初学者很友好:打开就能写代码,点一下就能上传。但当项目变大,Arduino IDE 的局限就暴露出来了:
痛点Arduino IDEPlatformIO
依赖管理手动下载 .zip 放到文件夹lib_deps 一行搞定,自动下载
多环境构建不支持一个配置文件管理 dev / release
命令行 / CI几乎没有完整的 CLI,可用于自动化构建
代码提示与补全非常有限基于 VS Code,体验完整
项目结构单文件为主标准的 src / lib / include 结构
PlatformIO 是一个开源的嵌入式开发生态系统。你可以把它理解为嵌入式世界的”包管理器 + 构建系统 + 调试器”的组合。它不是一个独立的编辑器,而是作为 VS Code 的插件或独立的命令行工具来使用。
类比:如果说 Arduino IDE 是”记事本”,那 PlatformIO + VS Code 就是”Word”。两者都能写文档,但后者在大型项目中效率高得多。

3.2 安装 PlatformIO

方式一:VS Code 插件(推荐)

  1. 安装 Visual Studio Code
  2. 打开 VS Code,按 Ctrl+Shift+X(macOS 上是 Cmd+Shift+X)打开扩展面板
  3. 搜索 PlatformIO IDE,点击安装
  4. 等待安装完成后重启 VS Code,左侧栏会出现一个小蚂蚁图标(PlatformIO 的 logo)
安装过程中 PlatformIO 会自动下载 Python 环境和核心工具链,整个过程可能需要几分钟。

方式二:CLI 安装

如果你更喜欢纯命令行工作流:
# 使用 pip 安装
pip install platformio

# 或者使用官方安装脚本
curl -fsSL -o get-platformio.py https://raw.githubusercontent.com/platformio/platformio-core-installer/master/get-platformio.py
python3 get-platformio.py

# 验证安装
pio --version
两种方式安装的核心工具是一样的。VS Code 插件只是在 CLI 的基础上加了图形界面。后文中我们统一使用 pio 命令行来演示,这样不管你用哪种方式都能跟上。

3.3 CrossPoint Reader 的 platformio.ini 解读

每个 PlatformIO 项目的根目录下都有一个 platformio.ini 文件,它是整个项目的”身份证”。打开 CrossPoint Reader 的这个文件,核心配置如下:
[env]
platform = espressif32
board = esp32-c3-devkitm-1
framework = arduino
逐行解释:
  • platform = espressif32 —— 告诉 PlatformIO:“我要用乐鑫(Espressif)的 ESP32 系列芯片。“PlatformIO 会自动下载对应的编译器(xtensa-gcc 或 riscv32-gcc)和 SDK。
  • board = esp32-c3-devkitm-1 —— 具体的开发板型号。ESP32-C3 是一颗 RISC-V 架构的芯片,PlatformIO 根据 board 名称确定 Flash 大小、时钟频率、引脚定义等参数。
  • framework = arduino —— 使用 Arduino 框架。这意味着你可以用 digitalWrite()Serial.println() 这些熟悉的 Arduino API。

开发版 vs 正式版

CrossPoint Reader 定义了两个构建环境:
[env:dev]
build_flags =
  -DDEV_BUILD=1
  -DLOG_LEVEL=4        ; 详细日志

[env:gh_release]
build_flags =
  -DDEV_BUILD=0
  -DLOG_LEVEL=1        ; 只输出错误日志
  -Os                  ; 优化代码体积
  • dev 环境:开发时使用。打开详细日志,方便排查问题。编译速度更快,但固件体积更大。
  • gh_release 环境:发布正式版本时使用。关闭调试日志,开启编译优化(-Os 代表 Optimize for Size,优化代码体积),最终固件更小、运行更快。
类比:这就像手机 App 的”Debug 版”和”Release 版”。Debug 版带着各种调试信息方便开发者排错,Release 版精简后才上架给用户使用。

3.4 Arduino 框架与 ESP-IDF 的关系

很多人以为”Arduino”是一种编程语言或芯片。实际上 Arduino 是一个框架——一套统一的 API 接口。不管底层是 AVR、ARM 还是 RISC-V 芯片,你都可以用 pinMode()digitalRead() 这些函数。 在 ESP32 的世界里,底层有一个功能更强大的官方框架叫 ESP-IDF(Espressif IoT Development Framework)。它提供了 WiFi 协议栈、蓝牙驱动、FreeRTOS 操作系统、Flash 文件系统等几乎所有底层能力。 Arduino-ESP32 框架本质上是 ESP-IDF 的一层便利封装。
┌──────────────────────────────────┐
│         你写的应用代码            │
├──────────────────────────────────┤
│       Arduino API 层             │  ← digitalWrite(), Serial.print()
├──────────────────────────────────┤
│         ESP-IDF                  │  ← WiFi, BLE, NVS, FreeRTOS ...
├──────────────────────────────────┤
│      硬件(ESP32-C3 芯片)        │
└──────────────────────────────────┘

一个关键事实:setup()loop() 已经在 FreeRTOS 里了

在传统 Arduino(比如 Arduino Uno)上,setup() 执行一次,然后 loop() 无限循环,整个芯片只跑这一个任务。 但在 ESP32 上,Arduino 框架的启动代码大致是这样的:
// Arduino-ESP32 内部实现(简化)
extern "C" void app_main() {
    // 创建一个 FreeRTOS 任务来运行 Arduino 的 setup() 和 loop()
    xTaskCreatePinnedToCore(loopTask, "loopTask", 8192, NULL, 1, NULL, 1);
}

void loopTask(void *pvParameters) {
    setup();           // 你的 setup() 在这里被调用
    for (;;) {
        loop();        // 你的 loop() 在这里被反复调用
        vTaskDelay(1); // 让出 CPU 给其他任务
    }
}
这意味着:
  • 你的代码从一开始就运行在一个实时操作系统(FreeRTOS)的任务里
  • 你随时可以创建更多任务来并行执行(后面第 7 章会详细讲)
  • WiFi 协议栈等系统功能在其他任务里同时运行

可以混用 Arduino 和 ESP-IDF API

因为 Arduino 只是 ESP-IDF 的封装,你可以在同一个项目里同时使用两者的 API
#include <Arduino.h>          // Arduino API
#include <esp_wifi.h>         // ESP-IDF WiFi API
#include <nvs_flash.h>        // ESP-IDF NVS (Non-Volatile Storage) API

void setup() {
    Serial.begin(115200);     // Arduino 风格
    nvs_flash_init();         // ESP-IDF 风格,两者可以共存
}
CrossPoint Reader 大量使用了这种混合方式:用 Arduino 的 Wire(I2C)、SPI 库来驱动外设,同时用 ESP-IDF 的 esp_sleepesp_ota 等底层 API 来实现深度睡眠和 OTA 升级。

3.5 完整的构建-烧录流程

第一步:获取源码

git clone --recursive https://github.com/crosspoint-reader/crosspoint-reader.git
cd crosspoint-reader
--recursive 参数很重要,它会同时下载项目依赖的子模块(submodule)。如果忘了加,之后可以用 git submodule update --init --recursive 补上。

第二步:编译

pio run                    # 编译默认环境(dev)
pio run -e gh_release      # 编译正式版
首次编译时 PlatformIO 会自动下载工具链和依赖库,可能需要较长时间(取决于网络)。后续编译只会重新编译修改过的文件,速度快得多——这就是增量编译 编译成功后,固件文件会生成在 .pio/build/dev/firmware.bin(或 .pio/build/gh_release/firmware.bin)。

第三步:烧录

用 USB 线把 CrossPoint Reader 连接到电脑,然后:
pio run --target upload    # 编译并烧录到设备
PlatformIO 会自动检测串口、自动触发 ESP32-C3 进入下载模式、写入固件,全程不需要你手动按按钮。

第四步:查看运行日志

pio device monitor         # 打开串口监视器
这相当于打开了一根”窥视管”,可以实时看到设备通过串口输出的日志信息。按 Ctrl+C 退出。
小技巧:可以用 pio run --target upload && pio device monitor 把烧录和监控串起来,烧录完自动打开监控。

3.6 Flash 分区表详解

什么是分区表

ESP32-C3 自带的 Flash 存储(CrossPoint Reader 使用 16MB)就像一块”小硬盘”。和电脑硬盘一样,你不能把所有东西杂乱无章地塞进去——需要分区 分区表就是一张”地图”,告诉系统:“从地址 0x0000 到 0x6000 存放引导程序,从 0x10000 开始存放应用固件……”

CrossPoint Reader 的分区方案

Flash 存储(16MB)
┌────────────────────────────────────────────────────┐
│  nvs        │  24 KB  │  Non-Volatile Storage      │  ← 存放设置、WiFi 密码等
├─────────────┼─────────┼────────────────────────────┤
│  otadata    │   8 KB  │  OTA 数据                  │  ← 记录"当前从哪个分区启动"
├─────────────┼─────────┼────────────────────────────┤
│  app0       │ 6.25 MB │  应用分区 A                │  ← 固件运行区(主)
├─────────────┼─────────┼────────────────────────────┤
│  app1       │ 6.25 MB │  应用分区 B                │  ← 固件运行区(备)
├─────────────┼─────────┼────────────────────────────┤
│  coredump   │  64 KB  │  崩溃转储                  │  ← 设备崩溃时保存现场
└────────────────────────────────────────────────────┘
逐个解释:
  • nvs(Non-Volatile Storage,非易失性存储):24KB 的小空间,用来存放键值对数据——比如 WiFi 密码、用户设置、阅读进度等。“非易失性”意味着断电不丢失。
  • otadata:8KB 的极小空间,但作用关键——它记录着”当前应该从 app0 还是 app1 启动”。
  • app0 和 app1:两个相同大小的应用分区,这是实现 OTA(Over-The-Air,空中升级) 的核心。
  • coredump:当程序崩溃时,系统会把 CPU 寄存器、调用栈等信息写到这里,方便事后分析原因。

双 OTA 分区:为什么需要两个应用分区

想象你正在给一栋大楼刷漆。如果你只有一栋楼,刷漆期间住户就没地方住。但如果有两栋楼(A 和 B),你可以:
  1. 住户先搬到 A 楼
  2. 在 B 楼刷漆
  3. 刷完后让住户搬到 B 楼
  4. 如果 B 楼的漆有问题,住户还可以搬回 A 楼
ESP32 的双 OTA 分区就是这个思路:
正常运行:    从 app0 启动

收到新固件:  将新固件写入 app1(app0 不受影响)

写入完成:    修改 otadata,下次从 app1 启动

重启验证:    新固件正常 → 确认成功
              新固件崩溃 → 自动回滚到 app0
这个机制保证了升级过程中即使断电或新固件有 bug,设备也不会变砖。这对于一个没有屏幕调试界面的嵌入式设备来说至关重要。

3.7 网页烧录工具(ESPTool Web Flash)

不是所有用户都会安装 PlatformIO。CrossPoint Reader 提供了一个基于浏览器的烧录方式—— ESPTool Web Flash 它利用了 Web Serial API,让浏览器可以直接通过 USB 与设备通信。用户只需要:
  1. 用 Chrome 或 Edge 浏览器打开烧录页面
  2. 用 USB 线连接设备
  3. 选择固件文件
  4. 点击”烧录”
整个过程不需要安装任何软件。这对于普通用户升级固件来说非常友好。
注意:Web Serial API 目前只有 Chromium 内核的浏览器(Chrome、Edge)支持,Firefox 和 Safari 暂不支持。

3.8 调试方式:串口打印

在桌面开发中,你可以设置断点、单步调试、查看变量。但在嵌入式开发中,最常用(也最可靠)的调试方式其实是最朴素的——串口打印
void setup() {
    Serial.begin(115200);    // 初始化串口,波特率 115200
    Serial.println("Device starting...");
}

void loop() {
    int batteryLevel = readBattery();
    Serial.printf("Battery: %d%%\n", batteryLevel);  // 格式化输出
    delay(1000);
}
Serial.begin(115200) 中的 115200 是波特率(baud rate),即每秒传输的位数。这个数字必须和串口监视器的设置一致,否则你看到的就是乱码。115200 是最常用的波特率。 串口打印看似简单,但有几个实用技巧:
  • 分级日志:用不同前缀区分信息级别,如 [INFO][WARN][ERROR],方便过滤
  • 条件编译:用 #ifdef DEV_BUILD 包裹调试日志,正式版自动移除,不浪费 Flash 空间
  • 时间戳:加上 millis() 时间戳,方便追踪时序问题
#ifdef DEV_BUILD
  #define LOG_D(tag, fmt, ...) Serial.printf("[%lu][D][%s] " fmt "\n", millis(), tag, ##__VA_ARGS__)
#else
  #define LOG_D(tag, fmt, ...)   // 正式版:这个宏展开为空,不产生任何代码
#endif
CrossPoint Reader 正是用这种方式实现了开发版(满屏日志)和正式版(静默运行)的切换。

本章要点

  1. PlatformIO 是嵌入式开发的现代化工具链,相比 Arduino IDE 在依赖管理、多环境构建、命令行支持方面有明显优势。
  2. platformio.ini 是项目配置的核心,定义了芯片平台、开发板型号、使用的框架,以及不同构建环境(dev / release)的差异。
  3. Arduino 框架是 ESP-IDF 的上层封装。在 ESP32 上,setup()loop() 已经运行在 FreeRTOS 的一个任务中。你可以在同一个项目里混用 Arduino API 和 ESP-IDF API。
  4. 完整的开发流程是:git clonepio run(编译)→ pio run --target upload(烧录)→ pio device monitor(监控)。
  5. Flash 分区表 把存储空间划分为 nvs、otadata、app0、app1、coredump 五个区域,双 OTA 分区保证了升级的安全性。
  6. 网页烧录工具让不懂开发的用户也能通过浏览器升级固件。
  7. 串口打印是嵌入式开发中最常用的调试手段。通过条件编译可以在开发版和正式版之间切换日志输出。