ESP-IDF 开发基础

reTerminal Sticky 是一款适合长期展示天气、日程、留言和轻量数据的电子纸信息终端。通过 ESP-IDF,你可以使用它的电子纸屏幕、触摸屏、实体按键和板载外设,开发自己的常驻信息页面。

本篇将带你运行综合开发 Demo(Sticky_dashboard_demo),熟悉如何基于 ESP-IDF 驱动 Sticky 的屏幕、板载外设和交互功能。示例工程提供了一套完整的硬件调用和页面开发参考,你可以在此基础上创建自己的页面、接入板载外设,并进一步开发自己的 Sticky 项目。

Note

刷写示例工程会替换 Sticky 当前运行的固件。请确认设备中没有需要保留的内容,再继续操作。

分享你的项目

如果你已经基于 reTerminal Sticky 完成了自己的项目,欢迎将它贡献到 Sticky Playground,与更多用户分享你的创意。感谢每一位参与共建的开发者。

前往 Playground Registry

请按照仓库 README 中的贡献指南准备项目并提交 Pull Request。项目通过审核并发布后,其他用户便可以在 Sticky Playground 中安装和体验。

开发环境

开始前,请准备:

  • reTerminal Sticky
  • 支持数据传输的 USB-C 数据线
  • Windows、Linux 或 macOS 电脑
  • ESP-IDF v5.4

Sticky_dashboard_demo 已在 ESP-IDF v5.4 上完成开发和实机验证。尚未安装开发环境时,请先按照乐鑫的 ESP32-S3 ESP-IDF v5.4 入门指南 完成安装。

安装完成后,在 ESP-IDF 终端中运行:

idf.py --version

输出中应包含:

ESP-IDF v5.4
Tip

只能充电的 USB-C 线可以为 Sticky 供电,但电脑无法通过它识别串口。如果设备能够充电却没有出现串口,请先更换支持数据传输的线缆。

Sticky 的主要开发资源如下:

硬件在示例中的用途
3.97 英寸、800 × 480 四阶灰度电子纸绘制和长期保留页面内容
电容式触摸屏识别左右滑动
UP、DOWN 和 AI 按键切页、刷新和进入深度睡眠
SHT40、RTC、电池计量和 IMU提供环境、时间、电量和姿态数据
MicroSD 和 PDM 麦克风读取文本和采集音频
蜂鸣器提供操作反馈

更完整的规格和硬件关系,请先阅读硬件概览

运行 Demo

本系列提供两份示例工程:

工程名称适用场景
Sticky_peripheral_demo外设最小控制 Demo提供独立、精简的板载外设控制示例,适合开发者直接参考和复用
Sticky_dashboard_demo综合开发 Demo整合页面、外设、交互和低功耗功能,用于跟随本系列 Wiki 学习完整开发流程

如果你已经熟悉 ESP-IDF,希望直接查看某个板载外设的控制方式,可以使用 Sticky_peripheral_demo。其中的示例相互独立,便于直接参考或移植到自己的项目中。

下载外设最小控制 Demo

如果你希望跟随本系列 Wiki 从页面显示、外设接入一路学习到局部刷新和低功耗,请使用 Sticky_dashboard_demo。后续三篇教程中的代码、工程结构和页面效果均以该 Demo 为准。

下载综合开发 Demo

下载并解压 Sticky_dashboard_demo,在包含顶层 CMakeLists.txt 的目录中打开 ESP-IDF v5.4 终端。

第一次编译时,设置目标芯片:

idf.py set-target esp32s3

编译工程:

idf.py build

看到 Project build complete 后,用 USB-C 数据线连接 Sticky。将 PORT 替换为设备串口,然后烧录并打开串口监视器:

idf.py -p PORT flash monitor

例如,Windows 串口为 COM5 时:

idf.py -p COM5 flash monitor

Linux 通常使用 /dev/ttyUSB0/dev/ttyACM0,macOS 通常使用 /dev/cu.usbserial-*/dev/cu.usbmodem-*。按 Ctrl+] 可以退出串口监视器。

启动后,串口会出现:

I (...) sticky_dashboard: Starting reTerminal Sticky Dashboard Demo

屏幕完成第一次刷新后显示 Home 页面。使用 UP/DOWN 按键或左右滑动,可以依次查看:

Home -> Sensor -> Battery -> Note -> IMU -> Microphone -> Home

Home 用于展示四阶灰度,不显示 RTC 时间;其他五个页面使用纯黑白画面,并在状态栏显示 RTC 时间。

电子纸全屏刷新会经过完整的像素更新过程。刷新完成后,页面将稳定显示新的内容。

工程结构

Sticky_dashboard_demo 将硬件访问、应用逻辑和页面绘制分开管理:

Sticky_dashboard_demo/
|-- components/                 外部组件和底层硬件组件
`-- main/
    |-- main.cpp                初始化与主事件循环
    |-- pin_config.h            引脚和设备地址
    |-- board/                  供电、GPIO 和共享总线
    |-- devices/                板载外设访问
    |-- app/                    状态、数据、事件和页面导航
    |-- pages/                  页面绘制
    `-- ui/                     Canvas、字体和公共状态栏

页面显示硬件数据时,数据按下面的方向传递:

硬件
  ↓
devices        读取设备
  ↓
app_data       更新应用数据
  ↓
AppState       保存页面状态
  ↓
pages          绘制页面
  ↓
Canvas         生成完整画面
  ↓
display        刷新电子纸

各目录的职责如下:

目录负责内容
board/初始化板级供电和多个设备共用的总线
devices/初始化、读取或控制具体硬件
app/保存状态、更新数据、处理事件和选择页面
pages/根据 AppState 绘制页面
ui/提供 Canvas、字体和公共状态栏

这种结构可以避免硬件通信、数据处理和页面绘制相互耦合。开发新功能时,通常只需要接入数据来源、扩展 AppState 并创建页面,不必重新实现现有的显示、输入和刷新框架。

页面绘制与屏幕刷新

页面 Renderer(页面绘制函数)只负责把内容写入 Canvas。绘制完成后,再由 App 层统一刷新电子纸:

render_current_page(canvas, state);
ESP_RETURN_ON_ERROR(
    refresh_current_page(state), "app", "refresh current page");

refresh_current_page() 会根据 state.current_page 选择刷新方式:Home 使用四阶灰度全刷,其他页面使用黑白全刷。因此,页面文件只负责绘制 Canvas;Display 初始化和刷新由现有的 Device 与 App 流程统一完成。

Canvas 使用 800 × 480 的横屏逻辑坐标,原点位于左上角。屏幕旋转已经由 sticky_display 处理,页面中不需要再次转换坐标。

数据流与页面显示

Sensor 页面展示了一条完整的数据链路:SHT40 读取温湿度,应用保存数据,页面再把结果显示出来。

SHT40
  ↓
sticky_sht40_read()
  ↓
update_environment()
  ↓
AppState.environment
  ↓
sensor_page_render()
  ↓
Canvas

读取传感器

main/devices/sticky_sht40.* 封装 SHT40 的硬件访问。初始化时,它使用 Board 层已经创建的传感器 I2C 总线:

ESP_ERROR_CHECK(sticky_sht40_init(board_sensor_i2c_bus()));

RTC、电池计量和 IMU 也使用这条共享总线,因此不需要为每个设备重复创建 I2C 总线。

更新应用状态

main/app/app_state.h 中的 EnvironmentState 保存页面需要的温湿度和读取状态:

struct EnvironmentState {
    float temperature_c = 0.0F;
    float humidity_percent = 0.0F;
    bool valid = false;
    esp_err_t error = ESP_ERR_INVALID_STATE;
};

main/app/app_data.cpp 调用 Device API,并把读取结果写入 AppState。核心关系如下:

Sht40Reading reading = {};
const esp_err_t result = sticky_sht40_read(reading);

state.environment.error = result;
state.environment.valid = result == ESP_OK;

读取成功后,函数继续保存温度和湿度;读取失败时,valid 保持为 false,并通过错误码和串口日志帮助定位问题。

绘制传感器数据

main/pages/sensor_page.cpp 只读取状态。数据有效时显示温度;读取失败时在相同位置显示 N/A

canvas.draw_text(76, 235, "Temperature:", 3, GrayLevel::Black);
if (state.environment.valid) {
    std::snprintf(value, sizeof(value), "%.1f C",
                  static_cast<double>(state.environment.temperature_c));
    canvas.draw_text(500, 235, value, 3, GrayLevel::Black);
} else {
    canvas.draw_text(500, 235, "N/A", 3, GrayLevel::Black);
}

进入 Sensor 页面时,App 层按顺序更新数据、绘制页面并刷新屏幕。页面本身不直接读取 SHT40。

这条链路适用于大多数数据页面:先确定数据来自哪里,再通过 AppState 交给页面显示。第二篇 Wiki 将继续使用 Battery、MicroSD、IMU 和 Microphone 展示不同类型的外设数据。

添加 Hello 页面

下面在现有页面循环末尾加入一个 Hello 页面。它不访问硬件,只用于完成一次完整的页面接入。

完成后,页面顺序变为:

Home -> Sensor -> Battery -> Note -> IMU -> Microphone -> Hello -> Home

步骤 1:创建页面文件

新建 main/pages/hello_page.h

#pragma once

class Canvas;
struct AppState;

void hello_page_render(Canvas &canvas, const AppState &state);

新建 main/pages/hello_page.cpp

#include "hello_page.h"

#include "app_state.h"
#include "canvas.h"
#include "page.h"

void hello_page_render(Canvas &canvas, const AppState &state)
{
    page_draw_layout(canvas, state, "Hello Sticky", "My First Page");
    canvas.draw_text(76, 250, "ESP-IDF Ready", 3, GrayLevel::Black);
    canvas.draw_line(76, 310, 718, 310, GrayLevel::Black);
}

page_draw_layout() 会绘制与其他黑白页面一致的边框、状态栏和标题。Hello 页面只需要补充自己的内容。

Demo 只将 Home 定义为四阶灰度页面,因此新增的 Hello 页面会自动使用黑白全刷。新增常规页面时应使用 GrayLevel::BlackGrayLevel::White;如果页面确实需要四阶灰度,还需要在 page_uses_gray4() 中加入对应的 PageId

步骤 2:增加页面 ID

main/app/app_state.hPageId 末尾加入 Hello

enum class PageId {
    Home,
    Sensor,
    Battery,
    Note,
    Imu,
    Microphone,
    Hello,
};

Hello 页面没有新的数据,因此不需要修改 AppState。如果页面需要显示外设数据,应参考上一节增加对应的状态结构和数据更新函数。

步骤 3:注册页面

main/app/app.cpp 中包含页面头文件:

#include "hello_page.h"

page_name() 中加入页面名称:

case PageId::Hello:
    return "Hello";

render_current_page() 中关联 Renderer:

case PageId::Hello:
    renderer = hello_page_render;
    break;

步骤 4:加入页面导航

select_next_page() 中,让 Microphone 进入 Hello,再从 Hello 返回 Home:

case PageId::Microphone:
    state.current_page = PageId::Hello;
    break;
case PageId::Hello:
    state.current_page = PageId::Home;
    break;

select_previous_page() 中补上反向关系:

case PageId::Home:
    state.current_page = PageId::Hello;
    break;
case PageId::Hello:
    state.current_page = PageId::Microphone;
    break;

步骤 5:加入构建系统

main/CMakeLists.txt 的页面源文件列表中加入:

"pages/hello_page.cpp"

保存修改后重新编译和烧录:

idf.py build
idf.py -p PORT flash monitor

进入 Hello 页面时,屏幕应显示 My First PageESP-IDF Ready

继续向下一页切换会返回 Home,向上一页切换也应保持完整切页过程。

到这里,你已经完成了一个页面的完整接入:

创建页面文件
  ↓
增加 PageId
  ↓
注册 Renderer
  ↓
加入页面导航
  ↓
更新 CMakeLists.txt
  ↓
编译并在设备上验证

应用扩展流程

创建新的信息页面时,可以按照下面的顺序进行:

  1. 确定页面要显示的数据,以及工程中是否已有对应的 Device API。
  2. 没有可用接口时,在 devices/ 中添加硬件访问;已有接口时直接复用。
  3. AppState 中保存页面需要的数据、有效状态和错误码。
  4. app_data 中读取数据并更新状态。
  5. 创建 Page Renderer,并将页面加入 PageId、页面路由和导航顺序。
  6. 将新增的 .cpp 加入 main/CMakeLists.txt,编译后在设备上验证显示和异常状态。

扩展应用时,各模块保持以下分工:

  • 页面只读取 AppState 并绘制 Canvas,不直接访问硬件。
  • Device 模块只负责硬件访问,不绘制 UI。
  • 共享的总线和供电资源由 Board 层管理。
  • 页面绘制完成后,由 App 层统一刷新电子纸。

常见问题

  1. 新增页面后出现 undefined reference

    页面 .cpp 没有加入 main/CMakeLists.txt,或函数声明与实现不一致。确认源文件已加入 SRCS,并检查函数名称和参数后重新构建。

  2. 新增页面可以编译,但无法切换进入

    仅添加 PageId 或 Renderer 还不完整。确认 render_current_page() 已注册页面,并在正向和反向导航中加入对应的 PageId

  3. 页面内容的方向与实机不一致

    页面应始终使用左上角为原点的 800 × 480 逻辑坐标。物理屏幕所需的 180° 旋转由 Display 层统一处理,不要在 Page Renderer 中再次旋转坐标。

  4. 页面文字显示为 ?

    当前内置点阵字库支持可打印 ASCII。中文、 等未收录字符会显示为 ?,使用前需要补充对应字形,或改用字库已支持的字符。

  5. 工程换到另一台电脑后构建失败

    build/ 中包含本机生成的路径和配置。切换开发环境后,先确认 ESP-IDF v5.4 和 ESP32-S3 目标正确;仍有错误时,只删除自动生成的 build/ 目录并重新构建。

下一步

本篇完成了两条基础开发路径:

页面接入:页面文件 -> PageId -> Renderer -> 页面导航 -> CMake

数据显示:Device -> app_data -> AppState -> Page -> Canvas -> Display

下一篇将在同一个 Demo 中继续介绍:

  • Battery、MicroSD、IMU 和 Microphone 的数据读取与页面显示
  • Button 和 Touch 如何通过 AppEvent 触发切页和刷新
  • 不同外设如何复用 Board 层的共享资源

继续阅读 ESP-IDF 页面与外设

Community 社区支持

Need more help? 还需要帮助?

Join our community, ask questions, or reach out to Seeed Studio technical support. 加入社区提问交流,或直接联系 Seeed Studio 技术支持。