ESP-IDF
本章节包含以下部分,请按需阅读:
ESP-IDF 入门教程
初次接触 ESP32 ESP-IDF 开发,想要快速上手?我们为您准备了一套通用的 入门教程。
- 第0节 认识 ESP32
- 第1节 搭建环境
- 第2节 运行实例
- 第3节 创建项目
- 第4节 使用组件
- 第5节 调试程序
- 第6节 FreeRTOS
- 第7节 驱动外设
- 第8节 Wi-Fi 编程
- 第9节 BLE 编程
请注意:该教程使用 ESP32-S3-Zero 作为教学示例,所有硬件代码均基于其引脚布局。在动手实践前,建议您对照手中的开发板引脚图,确认引脚配置无误。
配置 ESP-IDF 开发环境
以下内容以 Windows 系统为例,使用 VS Code + ESP-IDF 扩展 的方式进行开发。Mac/Linux 用户请参考 官方说明。
此部分图示以安装 ESP-IDF V5.5.2 为例示范,安装时请选用与您开发板示例匹配的 ESP-IDF 版本。
安装 ESP-IDF 开发环境
-
前往 ESP-IDF Installation Manager 下载 ESP-IDF 安装管理器。这是乐鑫最新推出的跨平台安装工具,下文将演示如何使用其离线安装功能。
在页面中点击 Offline Installer 标签,然后在筛选栏中选择 Windows 操作系统和你需要的 ESP-IDF 版本(图示仅为参考,请以实际为准)。

确认选择无误后,点击下载按钮。浏览器将自动同时下载两个文件:一个是 ESP-IDF 离线整合包(.zst),另一个是 ESP-IDF 安装器(.exe)。

请耐心等待两个文件下载完成。
-
下载完成后,双击运行 ESP-IDF 安装器(eim-gui-windows-x64.exe)。
启动后,可在右上角将界面语言切换为中文。

安装工具会自动检测同一目录下是否存在离线整合包。点击 从存档安装。

接下来,选择安装路径。建议使用默认路径;若需自定义,请确保路径中不包含中文或空格。确认无误后,点击 开始安装。

-
当看到如下界面时,表示 ESP-IDF 已安装成功。

-
建议同时安装驱动程序。点击 完成安装,然后点击 安装驱动程序。

安装 Visual Studio Code 与 ESP-IDF 扩展
-
下载并安装 Visual Studio Code。
-
安装时建议勾选 通过 Code 打开操作添加到 Windows 资源管理器文件上下文菜单,以便快速打开项目文件夹。
-
在 VS Code 中,点击侧边活动栏中的
扩展图标(或使用快捷键 Ctrl + Shift + X)打开 扩展 视图。
-
在搜索框中输入 ESP-IDF,找到 ESP-IDF 扩展并点击安装。

-
当 ESP-IDF 扩展版本 ≥ 2.0 时,扩展会自动检测并识别上述步骤中安装的 ESP-IDF 环境,无需手动配置。
准备工程与板级定义
-
下载 ESP32-S31-Touch-LCD-4.3C 示例源码包,解压到较短路径。
-
在 VS Code 中打开
examples/esp-idf/01_i2c_scan。工程根目录应包含CMakeLists.txt、sdkconfig.defaults、main和components。 -
在该工程的 ESP-IDF 终端中执行:
idf.py --preview set-target esp32s31idf.py --preview bmgr -b esp32_s31_touch_lcd_4_3c
bmgr 读取当前工程中 components/esp_boards/esp32_s31_touch_lcd_4_3c/ 的 YAML 配置,生成 components/gen_bmgr_codes。每个示例都是独立工程,应分别执行生成命令;修改 YAML 后也需重新生成。
不要从另一个示例复制生成目录,各示例启用的设备集合可能不同。首次解析组件依赖需要联网,不能只复制 main 文件夹运行。
set-target 会重新生成目标相关配置。复用已有工程时,先备份自定义 sdkconfig、依赖锁定文件和构建产物;首次使用建议在新解压的工程中操作。
示例程序
以下功能和效果依据当前源码说明,不作为实板通过记录。所有工程位于 examples/esp-idf/,分别打开对应文件夹,执行 板级配置 和 编译与下载。
各示例按“硬件连接、代码、代码解释、运行效果”说明。折叠代码摘录自对应工程的 main/main.c,省略了头文件、部分初始化或错误处理,不能作为独立源文件编译;完整源码见 ESP32-S31-Touch-LCD-4.3C 示例源码包。
| 示例目录 | 功能 | 外接条件或结果判断 |
|---|---|---|
| 01_i2c_scan | 扫描共用 I2C 总线 | 无电池时电量计可能不应答 |
| 02_rtc_pcf85063atl | 读写 RTC、1 Hz 定时中断 | 初始时间是示例值,不自动校时 |
| 03_imu_qmi8658 | 读取温度、加速度和角速度 | 改变姿态观察数据 |
| 04_bat_bq27220 | 读取电量计 | 需连接适配电池 |
| 05_tf_card | TF 卡文件读写与目录列表 | FAT 格式 TF 卡,先备份数据 |
| 06_codec_mic_raw | 采集麦克风原始数据 | 检查双声道采样及峰值变化 |
| 07_codec_sd_wav_player | 播放 TF 卡 WAV 文件 | TF 卡、匹配扬声器和 WAV 文件 |
| 08_rs485 | 半双工 RS485 收发 | RS485 对端与匹配接线 |
| 09_canfd | CAN-FD 收发 | CAN-FD 对端与正确终端电阻 |
| 10_usb_host | 外部 USB 设备枚举 | J3 转接线和 USB 设备 |
| 11_rgb_lcd | RGB LCD 测试图案 | 板载显示屏 |
| 12_touch | 触摸轨迹和清屏 | 板载触摸屏 |
| 13_comprehensive_test | 综合测试仪表界面 | 各项目分别需要电池、TF 卡、通信对端等 |
| 14_lvgl_porting | LVGL 图形与触摸交互 | 使用工程指定的 LVGL 依赖 |
| 15_bluetooth_speaker | 经典蓝牙 A2DP 音箱 | 音频发送设备与匹配扬声器 |
| 16_usb_camera | 外接 USB 摄像头预览 | 匹配 MJPEG 模式的 UVC 摄像头 |
01_i2c_scan:I2C 扫描
本示例演示扫描板载 I2C 总线,输出应答设备的 7 位地址、总线空闲电平和超时信息。
硬件连接
- 使用 USB 数据线将开发板 Type-C 接口连接电脑。
- 使用板载 SDA GPIO0、SCL GPIO1,无需外接 I2C 模块。
代码
main/main.c 关键代码
esp_err_t touch_ret = esp_board_manager_init_device_by_name("lcd_touch");
if (touch_ret == ESP_OK) {
touch_ret = esp_board_manager_deinit_device_by_name("lcd_touch");
if (touch_ret != ESP_OK) {
ESP_LOGE(TAG, "touch release failed: %s", esp_err_to_name(touch_ret));
ret = touch_ret;
goto cleanup;
}
} else {
ESP_LOGW(TAG, "touch wake-up failed: %s", esp_err_to_name(touch_ret));
}
vTaskDelay(pdMS_TO_TICKS(10));
// ...
for (uint8_t address = 0x03; address <= 0x77; address++) {
esp_err_t probe_ret = i2c_master_probe(bus, address, SCAN_TIMEOUT_MS);
if (probe_ret == ESP_OK) {
ESP_LOGI(TAG, "found 0x%02x %s",
(unsigned)address, known_device_name(address));
found++;
} else if (probe_ret == ESP_ERR_TIMEOUT) {
timeout++;
} else if (probe_ret != ESP_ERR_NOT_FOUND) {
other_error++;
ESP_LOGW(TAG, "probe 0x%02x failed: %s",
(unsigned)address, esp_err_to_name(probe_ret));
}
}
代码解释
esp_board_periph_ref_handle():获取 Board Manager 创建的 I2C0 总线句柄。esp_board_manager_init_device_by_name("lcd_touch"):初始化触摸设备,完成 GT911 的复位、唤醒和选址,再释放设备以进行扫描。i2c_master_probe():探测0x03~0x77,分别统计应答、超时及其他错误;扫描前由i2c_bus_is_idle()检查总线电平。
运行效果
- 串口周期输出应答地址和扫描统计,地址含义见 I2C 设备表。
- GT911 可应答
0x5D或0x14;BQ27220 未连接电池时可能不应答。不要将所有配置地址都作为必须出现的扫描结果。

02_rtc_pcf85063atl:实时时钟
本示例演示 PCF85063ATL 的时间读写、1 Hz 定时中断及中断状态读取。
硬件连接
- 使用 USB 数据线将开发板 Type-C 接口连接电脑。
- 使用板载 RTC 和 GPIO3 中断信号,无需外接模块。
- 如需检查主电源断开后的计时保持,先安装 CR927 备用电池;RTC 备用电池与 J1 的系统供电锂电池是不同接口。
代码
main/main.c 关键代码
void app_main(void)
{
s31_rtc_handle_t *rtc = NULL;
ESP_ERROR_CHECK(s31_rtc_acquire(&rtc));
ESP_ERROR_CHECK(s31_rtc_start_1hz_interrupt(rtc));
s31_rtc_time_t now = {0};
ESP_ERROR_CHECK(s31_rtc_get_time(rtc, &now));
if (now.voltage_low || now.year < 2024 || now.month == 0 || now.day == 0) {
const s31_rtc_time_t initial = {
.year = 2026, .month = 1, .day = 1, .weekday = 4,
.hour = 12, .minute = 0, .second = 0,
};
ESP_LOGW(TAG, "invalid RTC time; setting 2026-01-01 12:00:00");
ESP_ERROR_CHECK(s31_rtc_set_time(rtc, &initial));
}
uint32_t last_interrupt = 0;
while (true) {
ESP_ERROR_CHECK(s31_rtc_get_time(rtc, &now));
ESP_LOGI(TAG, "%04u-%02u-%02u %02u:%02u:%02u weekday=%u VL=%d",
now.year, now.month, now.day, now.hour, now.minute, now.second,
now.weekday, now.voltage_low);
s31_rtc_interrupt_status_t status = {0};
ESP_ERROR_CHECK(s31_rtc_get_interrupt_status(rtc, &status));
if (status.count != last_interrupt) {
ESP_LOGI(TAG, "INT count=%lu level=%d control2=0x%02x",
(unsigned long)status.count, status.gpio_level, status.control2);
ESP_ERROR_CHECK(s31_rtc_clear_interrupt(rtc));
last_interrupt = status.count;
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
代码解释
s31_rtc_acquire():获取 RTC 设备;s31_rtc_start_1hz_interrupt()启用 1 Hz 定时中断。s31_rtc_get_time()/s31_rtc_set_time():读取和设置时间;检测到时间无效时,设为2026-01-01 12:00:00。s31_rtc_get_interrupt_status()/s31_rtc_clear_interrupt():读取中断计数和 GPIO3 状态,并清除中断标志。
运行效果
- 串口输出日期、时间及中断状态,观察秒数递增和中断计数变化。
- 默认时间是示例值,不代表当前时间,也不会自动校时。需要准确时间时,修改初始化值或实现校时;掉电保持需具备实际的 RTC 备用电源条件。

03_imu_qmi8658:六轴传感器
本示例演示读取 QMI8658 的器件标识、数据就绪状态、温度、加速度、角速度及 GPIO54 中断状态。
硬件连接
- 使用 USB 数据线将开发板 Type-C 接口连接电脑。
- 使用板载 QMI8658,测试时缓慢改变开发板姿态。
代码
main/main.c 关键代码
void app_main(void)
{
s31_imu_handle_t *imu = NULL;
ESP_ERROR_CHECK(s31_imu_acquire(&imu));
uint8_t who = 0;
uint8_t revision = 0;
ESP_ERROR_CHECK(s31_imu_get_identity(imu, &who, &revision));
ESP_LOGI(TAG, "WHO_AM_I=0x%02x revision=0x%02x", who, revision);
uint32_t sample = 0;
while (true) {
bool ready = false;
uint8_t status_int = 0;
uint8_t status0 = 0;
esp_err_t ret = s31_imu_is_data_ready(imu, &ready, &status_int, &status0);
if (ret == ESP_OK && ready) {
s31_imu_data_t data = {0};
ESP_ERROR_CHECK(s31_imu_read(imu, &data));
ESP_LOGI(TAG,
"sample=%lu acc[g]=%+.3f,%+.3f,%+.3f gyro[dps]=%+.2f,%+.2f,%+.2f temp=%.2fC int=0x%02x status0=0x%02x gpio=%d",
(unsigned long)++sample, data.accel_x_g, data.accel_y_g,
data.accel_z_g, data.gyro_x_dps, data.gyro_y_dps,
data.gyro_z_dps, data.temperature_c, data.status_int,
data.status0, data.int_gpio_level);
} else if (ret != ESP_OK) {
ESP_LOGW(TAG, "status read failed: %s", esp_err_to_name(ret));
} else {
ESP_LOGD(TAG, "waiting: INT=0x%02x STATUS0=0x%02x", status_int, status0);
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
代码解释
s31_imu_acquire()/s31_imu_get_identity():初始化传感器并读取器件标识与修订号。s31_imu_is_data_ready():检查是否有可读取的数据。s31_imu_read():读取各轴加速度、角速度、温度及中断状态,并通过日志输出。
运行效果
- 串口持续输出采样数据,改变姿态时观察各轴读数变化。
- 该示例不包含成品姿态融合或运动精度校准。

04_bat_bq27220:电量计
本示例演示通过 BQ27220 读取电池电压、电流、温度、容量、荷电状态和状态标志。
硬件连接
- 断电后按 J1 极性连接适配电池,核对插头正负极。
- 使用 USB 数据线将开发板 Type-C 接口连接电脑。
代码
main/main.c 关键代码
void app_main(void)
{
while (true) {
s31_battery_handle_t *battery = NULL;
esp_err_t ret = s31_battery_acquire(&battery);
if (ret != ESP_OK) {
ESP_LOGE(TAG,
"BQ27220 is not responding: %s. The gauge is powered from "
"the battery; connect a battery and verify the I2C bus with "
"01_i2c_scan. Retrying...",
esp_err_to_name(ret));
vTaskDelay(pdMS_TO_TICKS(BATTERY_RETRY_DELAY_MS));
continue;
}
ESP_LOGI(TAG, "BQ27220 detected");
while (true) {
s31_battery_info_t info = {0};
ret = s31_battery_get_info(battery, &info);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "BQ27220 read failed: %s", esp_err_to_name(ret));
break;
}
ESP_LOGI(TAG,
"voltage=%umV current=%dmA avg=%dmA power=%dmW cap=%u/%umAh soc=%u%% soh=%u%% temp=%.1fC cycles=%u max_load=%dmA tte=%umin ttf=%umin status=0x%04x",
info.voltage_mv, info.current_ma, info.average_current_ma,
info.average_power_mw, info.remaining_capacity_mah,
info.full_charge_capacity_mah, info.state_of_charge_percent,
info.state_of_health_percent, info.temperature_c,
info.cycle_count, info.max_load_ma, info.time_to_empty_min,
info.time_to_full_min, info.status);
vTaskDelay(pdMS_TO_TICKS(BATTERY_RETRY_DELAY_MS));
}
ret = s31_battery_release();
if (ret != ESP_OK) {
ESP_LOGW(TAG, "BQ27220 release failed: %s", esp_err_to_name(ret));
}
vTaskDelay(pdMS_TO_TICKS(BATTERY_RETRY_DELAY_MS));
}
}
代码解释
s31_battery_acquire():获取电量计;未供电或初始化失败时,每 2 秒重试。s31_battery_get_info():周期读取电池信息;读取失败后释放设备并重试。- 设计容量
650 mAh是软件参数,需根据实际电池配置。
运行效果
- 串口输出电池电压、电流、容量和状态等信息。
- 若提示电量计不应答,先确认电池供电,再用
01_i2c_scan检查0x55;不要仅凭初始化失败判定开发板损坏。

05_tf_card:TF 卡读写
本示例演示 TF 卡供电控制、文件系统挂载、测试文件写入与读回,以及根目录列表。
示例以写入模式打开 /sdcard/s31_tf_card_demo.txt,同名文件会被覆盖。备份数据后再运行。
硬件连接
- 备份 TF 卡数据,断电后插入 FAT 格式 TF 卡。
- 使用 USB 数据线将开发板 Type-C 接口连接电脑。
代码
main/main.c 关键代码
char path[512];
int len = snprintf(path, sizeof(path), "%s/s31_tf_card_demo.txt", sd->mount_point);
if (len <= 0 || (size_t)len >= sizeof(path)) {
ESP_LOGE(TAG, "Test file path is too long");
goto cleanup;
}
FILE *file = fopen(path, "w");
if (!file) {
ESP_LOGE(TAG, "Failed to open %s for writing", path);
goto cleanup;
}
int write_ret = fputs("ESP32-S31 TF card demo OK\n", file);
int close_ret = fclose(file);
if (write_ret == EOF || close_ret != 0) {
ESP_LOGE(TAG, "Failed to write %s", path);
goto cleanup;
}
ESP_LOGI(TAG, "wrote %s", path);
char line[64] = {0};
file = fopen(path, "r");
if (!file) {
ESP_LOGE(TAG, "Failed to open %s for reading", path);
goto cleanup;
}
if (!fgets(line, sizeof(line), file)) {
ESP_LOGE(TAG, "Failed to read %s", path);
fclose(file);
goto cleanup;
}
fclose(file);
ESP_LOGI(TAG, "read back: %s", line);
代码解释
esp_board_manager_init_device_by_name("fs_sdcard"):使能 TF 卡电源并挂载文件系统;默认挂载失败不会自动格式化。fopen()/fputs()/fgets():以写入模式创建测试文件,再打开文件读回内容。list_root():每 5 秒列出根目录。当前使用 4 位 SDMMC,GPIO56 低有效;排查失败时先确认供电、文件系统和板级配置。
运行效果
- 串口输出测试文件路径和读回的
ESP32-S31 TF card demo OK,随后周期输出根目录内容。

06_codec_mic_raw:麦克风采样
本示例演示通过 ES8389 以 48 kHz、16 位、双声道采集麦克风原始音频。
硬件连接
- 使用 USB 数据线将开发板 Type-C 接口连接电脑。
- 使用板载双麦克风,无需外接音源。
代码
main/main.c 关键代码
ESP_ERROR_CHECK(esp_board_manager_init_device_by_name("audio_adc"));
dev_audio_codec_handles_t *adc = NULL;
ESP_ERROR_CHECK(esp_board_manager_get_device_handle("audio_adc", (void **)&adc));
ESP_ERROR_CHECK(adc && adc->codec_dev ? ESP_OK : ESP_ERR_INVALID_STATE);
esp_codec_dev_sample_info_t format = {
.sample_rate = SAMPLE_RATE,
.channel = CHANNELS,
.bits_per_sample = 16,
.mclk_multiple = 256,
};
ESP_ERROR_CHECK(esp_codec_dev_open(adc->codec_dev, &format) == ESP_CODEC_DEV_OK ? ESP_OK : ESP_FAIL);
ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(adc->codec_dev, 30.0f) == ESP_CODEC_DEV_OK ? ESP_OK : ESP_FAIL);
// ...
while (true) {
ESP_ERROR_CHECK(esp_codec_dev_read(adc->codec_dev, samples, BUFFER_BYTES) == ESP_CODEC_DEV_OK
? ESP_OK : ESP_FAIL);
int32_t left_peak = 0;
int32_t right_peak = 0;
for (int i = 0; i < FRAME_SAMPLES; i++) {
int32_t left = abs(samples[i * 2]);
int32_t right = abs(samples[i * 2 + 1]);
left_peak = left > left_peak ? left : left_peak;
right_peak = right > right_peak ? right : right_peak;
}
ESP_LOGI(TAG,
"frame=%" PRIu32 " peak L=%ld R=%ld first samples: [%d,%d] [%d,%d] [%d,%d] [%d,%d]",
frame++, (long)left_peak, (long)right_peak,
samples[0], samples[1], samples[2], samples[3],
samples[4], samples[5], samples[6], samples[7]);
代码解释
esp_board_manager_init_device_by_name("audio_adc"):初始化 ES8389 音频输入设备。esp_codec_dev_open()/esp_codec_dev_set_in_gain():设置采样格式和输入增益。esp_codec_dev_read():读取双声道采样数据;循环分别计算左右声道峰值并输出部分采样值。
运行效果
- 对麦克风说话,观察串口中左右声道的峰值及采样值随声音变化。
- 这是原始采样示例,不包含语音识别或回声消除。采样返回成功不能单独证明声道、音质和噪声指标正常。

07_codec_sd_wav_player:WAV 播放
本示例演示从 TF 卡读取 16 位 PCM(Pulse Code Modulation,脉冲编码调制)WAV 文件,经 ES8389 和板载功放播放。
硬件连接
- 按 TF 卡与音频准备 复制
test.wav并连接匹配的扬声器。 - 使用 USB 数据线将开发板 Type-C 接口连接电脑。
代码
main/main.c 关键代码
char path[256] = {0};
ret = find_wav(sd->mount_point, path, sizeof(path));
if (ret != ESP_OK) {
ESP_LOGE(TAG, "No playable WAV file found: %s", esp_err_to_name(ret));
goto cleanup;
}
ESP_LOGI(TAG, "Selected WAV file: %s", path);
while (true) {
ret = play_wav(dac->codec_dev, path);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Playback failed: %s", esp_err_to_name(ret));
break;
}
ESP_LOGI(TAG, "Playback complete, restarting in 1 second");
vTaskDelay(pdMS_TO_TICKS(1000));
}
代码解释
esp_board_manager_init_device_by_name():分别初始化fs_sdcard和audio_dac;ES8389 配置启用 DAC 参考,功放由 GPIO57 控制。find_wav():优先查找/sdcard/test.wav,否则选择根目录中的首个 WAV 文件。play_wav():解析并播放 16 位 PCM 单声道或双声道 WAV,单声道转换为双声道播放;播放完成后等待 1 秒再循环。该工程不是通用 MP3 播放器。
运行效果
- 串口输出选中的文件、播放状态和循环提示,结合扬声器声音检查播放情况。
- 包内
test.wav为 6 秒诊断信号,包含左声道音、右声道音和双声道扫频。功放只接 ES8389 左声道差分输出,纯右声道片段不能作为该扬声器必须发声的判断条件。 - 若有噪声或无声,按 音频排查 检查,不能将日志中的写入成功作为音频已验证结论。

08_rs485:RS485 收发
本示例演示半双工 RS485 的周期发送和接收,不包含 Modbus 协议栈。
硬件连接
- 断电后将 H4 的 TX−、TX+ 接至极性匹配的 RS485 对端,并确认参考地。
- 对端设置为 115200、8N1、无硬件流控。
- 使用 USB 数据线将开发板 Type-C 接口连接电脑;运行时不要按下与方向控制共用 GPIO61 的 BOOT。
代码
main/main.c 关键代码
periph_uart_handle_t *uart = NULL;
periph_uart_config_t *config = NULL;
ESP_ERROR_CHECK(esp_board_periph_ref_handle("uart_rs485", (void **)&uart));
ESP_ERROR_CHECK(esp_board_manager_get_periph_config("uart_rs485", (void **)&config));
ESP_ERROR_CHECK(uart ? ESP_OK : ESP_ERR_INVALID_STATE);
ESP_ERROR_CHECK(config ? ESP_OK : ESP_ERR_INVALID_STATE);
ESP_LOGI(TAG, "using uart_rs485: UART%d TX=GPIO%d RX=GPIO%d RTS/DE=GPIO%d baud=%d",
uart->uart_num, config->tx_io_num, config->rx_io_num,
config->rts_io_num, config->uart_config.baud_rate);
uint8_t rx[RX_BUFFER_SIZE];
uint32_t counter = 0;
while (true) {
char tx[96];
int length = snprintf(tx, sizeof(tx), "ESP32-S31 RS485 counter=%" PRIu32 "\r\n", counter++);
uart_write_bytes(uart->uart_num, tx, length);
uart_wait_tx_done(uart->uart_num, pdMS_TO_TICKS(100));
int rx_len = uart_read_bytes(uart->uart_num, rx, sizeof(rx) - 1, pdMS_TO_TICKS(500));
if (rx_len > 0) {
rx[rx_len] = 0;
ESP_LOGI(TAG, "RX %d byte(s): %s", rx_len, (char *)rx);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
代码解释
esp_board_periph_ref_handle("uart_rs485"):获取板级 UART 句柄,并读取 TX、RX、RTS / DE 和波特率配置。uart_write_bytes()/uart_wait_tx_done():发送递增计数文本并等待发送完成。uart_read_bytes():读取对端数据,接收成功时输出字节数和内容。
运行效果
- 对端周期收到
ESP32-S31 RS485 counter=...。 - 从对端发送数据,检查开发板串口中的接收内容,确认双向通信。

09_canfd:CAN-FD 收发
本示例演示 CAN-FD 帧发送与接收,发送 ID 为 0x123、数据长度为 16 字节。
硬件连接
- 断电后连接 CANH、CANL 和所需参考地,按总线拓扑配置终端电阻。
- 对端设置仲裁速率 500 kbit/s、数据速率 2 Mbit/s,并支持 CAN-FD 位速率切换。
- 使用 USB 数据线将开发板 Type-C 接口连接电脑。
代码
main/main.c 关键代码
uint8_t payload[16] = {0};
memcpy(payload, "S31 CAN FD", 10);
payload[12] = (uint8_t)(counter >> 24);
payload[13] = (uint8_t)(counter >> 16);
payload[14] = (uint8_t)(counter >> 8);
payload[15] = (uint8_t)counter;
twai_frame_t frame = {
.header.id = 0x123,
.header.dlc = twaifd_len2dlc(sizeof(payload)),
.header.fdf = true,
.header.brs = true,
.buffer = payload,
.buffer_len = sizeof(payload),
};
ret = twai_node_transmit(node, &frame, pdMS_TO_TICKS(100));
代码解释
s31_canfd_acquire()/s31_canfd_enable():获取并使能板级 CAN-FD 设备,注册接收回调。twai_frame_t:设置 ID、数据长度、FD 标志和 BRS 位速率切换标志,数据尾部包含计数值。twai_node_transmit():提交发送;接收回调与任务负责记录对端帧的 ID、长度、FD 和 BRS 标志。
运行效果
- 在对端检查 ID 为
0x123的 16 字节 CAN-FD 帧,并发送数据检查开发板接收日志。 - 没有有效应答对端时,不能仅由发起发送判断总线通信通过。

10_usb_host:USB 枚举
本示例演示 USB Host 设备枚举,输出连接、断开、速度、VID、PID 和设备类信息。
硬件连接
- 按 USB Host 接线 通过 MX1.25 4PIN 转接线将 USB 设备接入 J3。
- VBUS 来自板载 5 V,无 GPIO 使能;接线前核对 J3 针序。
- 使用 USB 数据线将开发板 Type-C 接口连接电脑。
代码
main/main.c 关键代码
static void client_event(const usb_host_client_event_msg_t *event, void *arg)
{
(void)arg;
if (event->event == USB_HOST_CLIENT_EVENT_NEW_DEV) {
usb_device_handle_t device = NULL;
if (usb_host_device_open(s_client, event->new_dev.address, &device) == ESP_OK) {
usb_device_info_t info = {0};
if (usb_host_device_info(device, &info) == ESP_OK) {
ESP_LOGI(TAG, "device %u speed=%s config=%u",
event->new_dev.address, usb_speed_text(info.speed),
info.bConfigurationValue);
}
const usb_device_desc_t *descriptor = NULL;
if (usb_host_get_device_descriptor(device, &descriptor) == ESP_OK) {
ESP_LOGI(TAG, "device %u VID=0x%04x PID=0x%04x class=0x%02x",
event->new_dev.address, descriptor->idVendor,
descriptor->idProduct, descriptor->bDeviceClass);
}
usb_host_device_close(s_client, device);
}
} else if (event->event == USB_HOST_CLIENT_EVENT_DEV_GONE) {
ESP_LOGI(TAG, "device disconnected");
}
}
代码解释
usb_host_install()/usb_host_client_register():安装主机驱动并注册异步客户端。usb_host_lib_handle_events()/usb_host_client_handle_events():分别处理主机库和客户端事件。client_event():处理新设备与断开事件,读取设备信息和描述符后输出日志。
运行效果
- 连接设备后查看速度、VID、PID 和设备类;断开后查看断开事件日志。
- 该工程只演示主机枚举和事件,不代表已经实现所有设备类的业务功能。

11_rgb_lcd:显示测试
本示例演示使用 PSRAM 双帧缓冲循环显示 LCD 测试图案。
硬件连接
- 使用 USB 数据线将开发板 Type-C 接口连接电脑。
- 使用板载 LCD,无需外接显示屏。
代码
main/main.c 关键代码
while (true) {
uint16_t *frame = frames[frame_index];
draw_pattern(frame, config->lcd_width, config->lcd_height, pattern);
sync_frame(frame, config->lcd_width, config->lcd_height);
ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(lcd->panel_handle,
0, 0,
config->lcd_width,
config->lcd_height,
frame));
if (!backlight_enabled) {
vTaskDelay(pdMS_TO_TICKS(LCD_FIRST_FRAME_SETTLE_MS));
ESP_ERROR_CHECK(esp_board_manager_init_device_by_name("lcd_brightness"));
backlight_enabled = true;
}
pattern = (pattern + 1) % 8;
vTaskDelay(pdMS_TO_TICKS(1000));
frame_index ^= 1U;
}
代码解释
esp_lcd_rgb_panel_get_frame_buffer():获取两个 RGB LCD 帧缓冲。draw_pattern()/sync_frame():生成测试图案并同步缓冲数据;esp_lcd_panel_draw_bitmap()提交画面。- 首帧完成后初始化
lcd_brightness使能背光,每隔约 1 秒切换图案和帧缓冲。
运行效果
- 屏幕循环显示白、红、绿、蓝、八色条、棋盘、渐变和几何图案,检查颜色、边界和图案是否完整。
- 软件参数为 800 × 480、RGB565、26 MHz 像素时钟,不将参数值等同于实测刷新率。
12_touch:触摸测试
本示例演示读取最多 5 个触点,在屏幕绘制彩色标记,并通过右上角区域清屏。
硬件连接
- 使用 USB 数据线将开发板 Type-C 接口连接电脑。
- 使用板载 GT911 触摸屏,无需外接模块。
代码
main/main.c 关键代码
ESP_ERROR_CHECK(esp_lcd_touch_read_data(touch->touch_handle));
uint16_t x[MAX_POINTS] = {0};
uint16_t y[MAX_POINTS] = {0};
uint16_t strength[MAX_POINTS] = {0};
uint8_t points = 0;
if (esp_lcd_touch_get_coordinates(touch->touch_handle, x, y, strength,
&points, MAX_POINTS)) {
bool clear_requested = false;
for (uint8_t i = 0; i < points; i++) {
if (touch_in_clear_block(x[i], y[i], lcd_config->lcd_width)) {
clear_requested = true;
break;
}
}
if (clear_requested) {
ESP_ERROR_CHECK(draw_page(lcd->panel_handle, lcd_config->lcd_width,
lcd_config->lcd_height, line));
ESP_LOGI(TAG, "clear screen");
} else {
for (uint8_t i = 0; i < points; i++) {
ESP_ERROR_CHECK(draw_mark(lcd->panel_handle, lcd_config->lcd_width,
lcd_config->lcd_height, x[i], y[i],
s_colors[i % MAX_POINTS], mark));
ESP_LOGI(TAG, "touch=%" PRIu32 " point=%u x=%u y=%u strength=%u",
count++, i, x[i], y[i], strength[i]);
}
}
代码解释
esp_board_manager_init_device_by_name():初始化 LCD、触摸和背光,获取相应设备句柄。esp_lcd_touch_read_data()/esp_lcd_touch_get_coordinates():读取触摸数据并获取坐标、强度和触点数。touch_in_clear_block():判断是否触摸清屏区域;其他区域调用draw_mark()绘制标记并记录坐标。
运行效果
- 依次触摸屏幕四角、中间和右上角清屏区域,检查标记位置、坐标方向和清屏行为。
- 触摸初始化成功不代表整个触摸区域已验证;需要分别检查边缘、连续移动和多点输入。

13_comprehensive_test:综合测试
本示例通过 LVGL 仪表界面组织显示、触摸、RTC、IMU、电量计、TF 卡、音频、RS485、CAN-FD、USB Host 和 Wi-Fi 扫描。
硬件连接
- 运行前备份 TF 卡数据,按测试项目准备电池、TF 卡、扬声器、USB 设备及通信对端。
- RS485 / CAN-FD 联合测试依赖按源码帧格式配置的外部转换器:串口帧为 69 字节,CAN-FD 数据为 64 字节;没有该设备时不能要求联合项目通过。
- 使用 USB 数据线将开发板 Type-C 接口连接电脑。
代码
main/main.c 关键代码
void app_main(void)
{
#if CONFIG_S31_TEST_USB_HOST_ONLY
run_usb_host_only_test();
#else
run_full_test();
#endif
}
代码解释
CONFIG_S31_TEST_USB_HOST_ONLY:控制运行仅 USB Host 日志模式或完整测试模式。run_usb_host_only_test()/run_full_test():分别启动对应测试流程。- 通过
idf.py --preview menuconfig中的 "ESP32-S31 comprehensive test" 选择模式,并配置测试音和 Wi-Fi 扫描。
运行效果
- 完整模式显示测试仪表界面;仅 USB Host 模式通过串口输出 USB 日志。
- 界面的
MANUAL项需要人工确认,例如听到测试音、观察麦克风电平和触摸位置;不能把成功调用驱动视为全部硬件通过。

14_lvgl_porting:LVGL 移植
本示例演示通过 esp_lvgl_adapter 接入 LCD、触摸和背光,运行 LVGL(Light and Versatile Graphics Library)图形动画、控件和性能信息界面。
硬件连接
- 使用 USB 数据线将开发板 Type-C 接口连接电脑。
- 使用板载 LCD 和触摸屏,无需外接显示设备。
代码
main/main.c 关键代码
esp_lv_adapter_config_t adapter = ESP_LV_ADAPTER_DEFAULT_CONFIG();
adapter.task_priority = 4;
adapter.task_stack_size = 8192;
adapter.tick_period_ms = 5;
ESP_ERROR_CHECK(esp_lv_adapter_init(&adapter));
esp_lv_adapter_display_config_t display_config =
ESP_LV_ADAPTER_DISPLAY_RGB_DEFAULT_CONFIG(lcd->panel_handle, lcd->io_handle,
lcd_config->lcd_width, lcd_config->lcd_height,
ESP_LV_ADAPTER_ROTATE_0);
display_config.profile.buffer_height = 80;
display_config.profile.use_psram = true;
display_config.profile.require_double_buffer = true;
display_config.profile.enable_ppa_accel = CONFIG_S31_LVGL_DEMO_ENABLE_PPA;
display_config.tear_avoid_mode = ESP_LV_ADAPTER_TEAR_AVOID_MODE_DOUBLE_DIRECT;
lv_display_t *display = esp_lv_adapter_register_display(&display_config);
ESP_ERROR_CHECK(display ? ESP_OK : ESP_FAIL);
const esp_lv_adapter_touch_config_t touch_config =
ESP_LV_ADAPTER_TOUCH_DEFAULT_CONFIG(display, touch->touch_handle);
ESP_ERROR_CHECK(esp_lv_adapter_register_touch(&touch_config) ? ESP_OK : ESP_FAIL);
ESP_ERROR_CHECK(esp_lv_adapter_start());
ESP_ERROR_CHECK(esp_lv_adapter_lock(-1));
app_ui_create();
app_ui_start();
esp_lv_adapter_unlock();
代码解释
esp_lv_adapter_init():配置适配层任务优先级、栈和时基。当前组件清单指定 LVGL9.5.0、esp_lvgl_adapter0.6.0。esp_lv_adapter_register_display()/esp_lv_adapter_register_touch():注册 RGB 显示与触摸输入,启用 PSRAM 双缓冲。esp_lv_adapter_start():启动适配层;在适配层锁保护下调用app_ui_create()和app_ui_start()创建并启动界面。
运行效果
- 屏幕显示图形、动画和控件,触摸操作时检查控件响应。
- 配置菜单中的视频画面为程序生成的 RGB565 画面,不代表外接摄像头或媒体解码性能;默认 30 FPS 是生成参数,不是实测结果。

15_bluetooth_speaker:蓝牙音箱
本示例演示经典蓝牙 A2DP(Advanced Audio Distribution Profile,音频分发协议)音箱,通过 ES8389 输出音频,并提供 LVGL 播放界面。
硬件连接
- 断电后连接匹配的扬声器,再通过 Type-C 连接电脑。
- 准备支持经典蓝牙 A2DP 的手机或其他音源。
代码
main/main.c 关键代码
void app_main(void)
{
#if CONFIG_EXAMPLE_A2DP_SINK_USE_EXTERNAL_CODEC == FALSE
ESP_ERROR_CHECK(esp_board_manager_init_device_by_name("audio_dac"));
#endif
if (music_ui_start() != ESP_OK) {
ESP_LOGW(BT_AV_TAG, "music UI disabled");
}
ESP_ERROR_CHECK(bredr_app_common_init());
bt_app_task_start_up();
/* bluetooth device name, connection mode and profile set up */
bt_app_work_dispatch(bt_av_hdl_stack_evt, BT_APP_EVT_STACK_UP, NULL, 0, NULL, NULL);
}
代码解释
esp_board_manager_init_device_by_name("audio_dac"):按工程配置初始化板载音频输出。music_ui_start():启动屏幕播放界面;bredr_app_common_init()初始化经典蓝牙公共配置。bt_app_task_start_up()/bt_app_work_dispatch():启动蓝牙任务并分发协议栈启动事件;默认使用 Bluedroid 和 SBC 解码。
运行效果
- 从音源搜索
ESP_SPEAKER并连接,播放音频,检查扬声器声音与屏幕播放界面。 - 该示例不是 BLE 音频或蓝牙串口透传,不根据上游 README 的可选编解码说明认定本工程已启用全部格式。

16_usb_camera:USB 摄像头
本示例演示通过 USB Host 采集外接 UVC(USB Video Class)摄像头的 MJPEG 数据,经 JPEG 解码后在 LCD 上预览。
硬件连接
- 通过 J3 转接线连接支持匹配 MJPEG 模式的 UVC 摄像头,接线见 USB Host 接线。
- 使用 USB 数据线将开发板 Type-C 接口连接电脑。本板没有 DVP 摄像头接口。
代码
main/main.c 关键代码
static esp_err_t open_stream(uvc_host_stream_hdl_t *stream)
{
uvc_host_stream_config_t config = s_stream_config;
for (size_t i = 0; i < sizeof(s_preferred_formats) / sizeof(s_preferred_formats[0]); i++) {
config.vs_format = s_preferred_formats[i];
log_stream_format("Opening UVC stream", &config.vs_format);
esp_err_t ret = uvc_host_stream_open(&config, pdMS_TO_TICKS(5000), stream);
if (ret == ESP_OK) {
return ESP_OK;
}
ESP_LOGW(TAG, "Open candidate %u failed: %s", (unsigned)i, esp_err_to_name(ret));
}
return ESP_ERR_NOT_FOUND;
}
代码解释
app_start():初始化 LCD、帧缓冲、USB Host 与 UVC 驱动,并创建帧处理任务。open_stream()/uvc_host_stream_open():按候选格式尝试打开视频流;源码优先请求 640 × 480、30 FPS,并尝试其他帧率配置。preview_stream():取出帧,调用draw_camera_frame()解码并绘制,归还帧缓冲,并周期输出预览 FPS。
运行效果
- 摄像头匹配成功后,屏幕显示预览画面;实际选择的格式以摄像头枚举结果和串口日志为准。
- 请求帧率不等于实测预览帧率,也不代表任意 UVC 摄像头均兼容。

板级配置维护
| 文件 | 用途 |
|---|---|
board_peripherals.yaml | I2C、I2S、UART、GPIO、背光 PWM 等外设参数 |
board_devices.yaml | LCD、触摸、存储、音频及自定义设备参数 |
main/idf_component.yml | 当前工程组件依赖与版本要求 |
sdkconfig.defaults | Flash、PSRAM、USB 和图形等默认配置 |
components/s31_board_support/ | 部分示例使用的 S31 专用适配代码 |