
| 项目 | ESP32-S3-Touch-LCD-4.3C | ESP32-S31-Touch-LCD-4.3C |
|---|---|---|
| 主控 | ESP32-S3,Xtensa LX7 双核,最高 240 MHz | ESP32-S31,RISC-V 双核,最高 320 MHz |
| 存储 | 16 MB Flash、8 MB PSRAM | N16R16:16 MB Flash、16 MB PSRAM |
| 音频 | ES8311 与 ES7210 | ES8389 |
| 外部 IO | 隔离数字输入、输出 | GPIO48~53 原生 GPIO,无隔离 |
| USB Host 座子 | 未配置该座子 | 新增 MX1.25 4PIN 座子 J3 |
| 下载通道 | 原生 USB | Type-C → CH343P → UART0 |
| 配套开发示例 | Arduino、ESP-IDF | 当前资料仅提供 ESP-IDF |
两款产品的 GPIO 分配和驱动配置不同。移植时需使用本产品的板级定义,不能只修改目标芯片名称或沿用 S3 固件。
H4 的 GPIO48~53 没有 S3 版隔离 IO 的隔离与电平转换电路。使用 3.3 V 逻辑连接,并与外设共地;不得接入原隔离 IO 使用的高电压信号或将其作为功率输出。
原理图中 RS485 方向控制与 BOOT 共用 GPIO61,运行 RS485 时不要按 BOOT。
以下针序以原理图的针脚编号为准,不代表从线缆某一面观察的左右顺序。接线前核对 PCB 丝印和连接器方向。
H4 的 SDA、SCL 具有 I2C 电平转换电路,扩展侧电压 I2C_VCC 通过硬件选择 3.3 V 或 5 V,原理图的 H3 注明默认连接 3.3 V。GPIO48~53 则为 3.3 V 原生 GPIO,不随 I2C_VCC 切换。电平选择与接线步骤见 I2C 扩展接口。
电池接口 J1 为 MX1.25 2PIN,用于连接 3.7 V 单节锂电池,连接前核对电池插头极性。


准备开发板、支持数据传输的 USB Type-C 线缆和电脑。首次连接时,断开电池、外部 DC 电源、USB Host 设备及其他扩展设备。
01_i2c_scan,确认板载总线应答,再选择显示或其他示例。Type-C 下载串口与 J3 USB Host 是不同接口。下载程序时使用 Type-C,不将 J3 当作电脑下载接口。
开发板支持 Type-C 5 V 供电、DC 7~36 V 输入和 3.7 V 单节锂电池供电,首次连接建议使用 Type-C。接入电池或外部 DC 电源前断开供电,按原理图与 PCB 丝印核对接口和极性。DC 输入连接 H4 的 1 脚 DC+、2 脚 DC−,不将 DC 输入电压接到 5 V、I2C 或 GPIO 引脚。
| 接口或器件 | 说明 |
|---|---|
| J1 电池接口 | MX1.25 2PIN,连接 3.7 V 单节锂电池;不得仅凭线色判断极性 |
| ETA6098 | 电池充电电路 |
| MP28164GD-Z | 电池升压电路 |
| BQ27220 | 电量计,直接由电池侧供电;未连接电池时无法应答 |
| CR927 电池座 | RTC 备用电源,安装后用于主电源断开时保持计时;不代替 J1 的系统供电电池 |
04_bat_bq27220 中的 650 mAh 是软件设计容量参数,不表示板载电池容量或固定适配容量。容量、电流方向及荷电状态需结合实际电池和电量计配置判断,不能将显示值当作已校准的测量结果。
带壳版本的电池仓可容纳尺寸为 60 × 30 × 6 mm(长 × 宽 × 厚)的电池。选配时同时核对电池尺寸、电气规格和插头极性;是否随附电池以所选产品配置为准。
PWR 用于指示电源状态,CHG 用于指示充电状态,USER 为 GPIO55 控制的可编程指示灯;CHG 不作为电池剩余电量的百分比指示。

H4 的 3 脚为 I2C_VCC、4 脚为 GND、5 脚为 SDA、6 脚为 SCL。扩展侧 SDA、SCL 经过电平转换电路,I2C_VCC 可通过板上硬件连接选择 3.3 V 或 5 V;原理图的 H3 注明默认连接 3.3 V。
I2C_VCC、GND、SDA 和 SCL,确认极性及电平后再上电。I2C_VCC 由硬件选择,不能通过软件切换。H4 的 GPIO48~53 始终按 3.3 V 原生 GPIO 使用,不随 I2C 扩展侧电压改变。
J3 是 MX1.25 4PIN USB Host 接口。使用针序匹配的转接线,将外部 USB 设备接入开发板。
| J3 针脚 | 信号 |
|---|---|
| 1 | GND |
| 2 | USB D+ |
| 3 | USB D− |
| 4 | 5 V |
10_usb_host,查看连接、断开及 VID、PID 等枚举信息。J3 的 VBUS 接板载 5 V,没有软件控制使能;不要向该引脚反向供电。枚举成功只说明 USB 设备已识别,使用键盘、存储设备或其他设备还需要对应的 USB 类驱动。
运行 16_usb_camera 时,需通过 J3 外接支持匹配 MJPEG 模式的 UVC 摄像头。
examples/esp-idf/07_codec_sd_wav_player/test.wav 复制到 TF 卡根目录。07_codec_sd_wav_player,检查测试音与扫频声音。当前 TF 卡配置为 4 位 SDMMC,GPIO56 低电平使能卡电源;GPIO57 高电平使能扬声器功放。原理图中的扬声器功放只连接 ES8389 左声道差分输出,不能仅凭测试文件中的纯右声道片段无声判断扬声器故障。
具体针脚见 扩展连接器。连接外部设备前断电,确认信号类型、极性、电平和共地条件。
本章节包含以下部分,请按需阅读:
初次接触 ESP32 ESP-IDF 开发,想要快速上手?我们为您准备了一套通用的 入门教程。
请注意:该教程使用 ESP32-S3-Zero 作为教学示例,所有硬件代码均基于其引脚布局。在动手实践前,建议您对照手中的开发板引脚图,确认引脚配置无误。
配套示例使用 ESP-IDF v6.1,目标芯片为 esp32s31。下方通用安装图示中的版本号仅作演示。
以下内容以 Windows 系统为例,使用 VS Code + ESP-IDF 扩展 的方式进行开发。Mac/Linux 用户请参考 官方说明。
此部分图示以安装 ESP-IDF V5.5.2 为例示范,安装时请选用与您开发板示例匹配的 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。
安装时建议勾选 通过 Code 打开操作添加到 Windows 资源管理器文件上下文菜单,以便快速打开项目文件夹。
在 VS Code 中,点击侧边活动栏中的 扩展图标(或使用快捷键 Ctrl + Shift + X)打开 扩展 视图。
在搜索框中输入 ESP-IDF,找到 ESP-IDF 扩展并点击安装。

当 ESP-IDF 扩展版本 ≥ 2.0 时,扩展会自动检测并识别上述步骤中安装的 ESP-IDF 环境,无需手动配置。
以下以 01_i2c_scan 为例,使用 ESP-IDF v6.1 终端执行命令。
下载 ESP32-S31-Touch-LCD-4.3C 示例程序,解压到较短且不含中文或空格的路径。
在 VS Code 中打开其中一个示例的工程目录,例如 examples/esp-idf/01_i2c_scan。按 Ctrl + Shift + P 打开命令面板,运行 "ESP-IDF: Open ESP-IDF Terminal"。
在终端中安装板级代码生成工具,同一个 ESP-IDF 环境只需安装一次:
python -m pip install esp-bmgr-assist
设置当前终端的目标芯片。每次新建 ESP-IDF 终端后都需重新设置:
$env:IDF_TARGET = "esp32s31"
生成当前示例的板级代码并编译,首次执行需联网下载所需组件:
idf.py bmgr -b esp32_s31_touch_lcd_4_3c
idf.py build
用 Type-C 数据线连接开发板,开启电源开关。在设备管理器中确认 CH343P 串口号,将下列 COMx 替换为实际串口号,下载程序并打开串口监视器:
idf.py -p COMx flash monitor
串口应周期输出 I2C 应答地址与扫描统计,结果判断见 I2C 扫描。
按 Ctrl + ] 退出串口监视器。运行其他示例时,打开对应工程目录和 ESP-IDF 终端,重复目标芯片设置、板级代码生成、编译和下载步骤。
使用 VS Code 的 ESP-IDF 扩展按钮编译前,先打开当前示例的工程目录,运行 "ESP-IDF: Open ESP-IDF Terminal",或点击 打开 ESP-IDF 终端,再按顺序完成以下准备:
安装板级代码生成工具。同一个 ESP-IDF 环境只需安装一次,已安装可跳过:
python -m pip install esp-bmgr-assist
设置当前终端的目标芯片。每次新建 ESP-IDF 终端后都需重新设置:
$env:IDF_TARGET = "esp32s31"
生成当前示例的板级代码:
idf.py bmgr -b esp32_s31_touch_lcd_4_3c
配置当前工程:
idf.py reconfigure
以上命令执行成功后,可使用扩展按钮编译、烧录和监视。切换到其他示例工程时,需在对应工程的 ESP-IDF 终端中重新设置目标芯片、生成板级代码并配置工程。
所有工程位于 examples/esp-idf/,分别打开对应文件夹,按 板级配置与编译下载步骤 操作。
折叠代码用于讲解关键流程,省略了头文件、部分初始化或错误处理。编译时使用完整工程,完整源码见 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 摄像头 |
本示例演示扫描板载 I2C 总线,输出应答设备的 7 位地址、总线空闲电平和超时信息。
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() 检查总线电平。0x5D 或 0x14;BQ27220 未连接电池时可能不应答。不要将所有配置地址都作为必须出现的扫描结果。
本示例演示 PCF85063ATL 的时间读写、1 Hz 定时中断及中断状态读取。
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 状态,并清除中断标志。
本示例演示读取 QMI8658 的器件标识、数据就绪状态、温度、加速度、角速度及 GPIO54 中断状态。
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():读取各轴加速度、角速度、温度及中断状态,并通过日志输出。
本示例演示通过 BQ27220 读取电池电压、电流、温度、容量、荷电状态和状态标志。
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;不要仅凭初始化失败判定开发板损坏。
本示例演示 TF 卡供电控制、文件系统挂载、测试文件写入与读回,以及根目录列表。
示例以写入模式打开 /sdcard/s31_tf_card_demo.txt,同名文件会被覆盖。备份数据后再运行。
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,随后周期输出根目录内容。
本示例演示通过 ES8389 以 48 kHz、16 位、双声道采集麦克风原始音频。
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():读取双声道采样数据;循环分别计算左右声道峰值并输出部分采样值。
本示例演示从 TF 卡读取 16 位 PCM(Pulse Code Modulation,脉冲编码调制)WAV 文件,经 ES8389 和板载功放播放。
test.wav 并连接匹配的扬声器。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 左声道差分输出,不能仅凭纯右声道片段无声判断扬声器故障。
本示例演示半双工 RS485 的周期发送和接收,不包含 Modbus 协议栈。
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=...。
本示例演示 CAN-FD 帧发送与接收,发送 ID 为 0x123、数据长度为 16 字节。
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 标志。0x123 的 16 字节 CAN-FD 帧,并发送数据检查开发板接收日志。
本示例演示 USB Host 设备枚举,输出连接、断开、速度、VID、PID 和设备类信息。
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():处理新设备与断开事件,读取设备信息和描述符后输出日志。
本示例演示使用 PSRAM 双帧缓冲循环显示 LCD 测试图案。
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 秒切换图案和帧缓冲。
本示例演示读取最多 5 个触点,在屏幕绘制彩色标记,并通过右上角区域清屏。
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() 绘制标记并记录坐标。
本示例通过 LVGL 仪表界面组织显示、触摸、RTC、IMU、电量计、TF 卡、音频、RS485、CAN-FD、USB Host 和 Wi-Fi 扫描。
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 扫描。MANUAL 项需逐项检查测试音、麦克风电平和触摸位置;驱动初始化成功后仍需完成这些检查。
本示例演示通过 esp_lvgl_adapter 接入 LCD、触摸和背光,运行 LVGL(Light and Versatile Graphics Library)图形动画、控件和性能信息界面。
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():配置适配层任务优先级、栈和时基。示例使用 LVGL 9.5.0 和 esp_lvgl_adapter 0.6.0。esp_lv_adapter_register_display() / esp_lv_adapter_register_touch():注册 RGB 显示与触摸输入,启用 PSRAM 双缓冲。esp_lv_adapter_start():启动适配层;在适配层锁保护下调用 app_ui_create() 和 app_ui_start() 创建并启动界面。
本示例演示经典蓝牙 A2DP(Advanced Audio Distribution Profile,音频分发协议)音箱,通过 ES8389 输出音频,并提供 LVGL 播放界面。
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 并连接,播放音频,检查扬声器声音与屏幕播放界面。
本示例演示通过 USB Host 采集外接 UVC(USB Video Class)摄像头的 MJPEG 数据,经 JPEG 解码后在 LCD 上预览。
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。
ESP32-S31-Touch-LCD-4.3C 提供 16 MiB 出厂合并固件,用于恢复出厂演示。固件文件位于配套仓库的 firmware/ 目录:
| 项目 | 内容 |
|---|---|
| 固件文件 | ESP32-S31-Touch-LCD-4.3C_FactoryOnly_20261030.bin |
| 烧录地址 | 0x00 |
| 下载接口 | Type-C,经 CH343P 连接 UART0;不使用 J3 USB Host |
该出厂固件尚未提供对应源码工程。
写入该合并镜像会覆盖应用程序、分区表、设置及数据。操作前备份需要保留的内容。
烧录工具需明确支持 esp32s31,不要选择 esp32s3。如需手动进入下载模式,按以下顺序操作:
烧录完成后,按 RESET 运行固件。
以下步骤适用于烧录本页提供的固件。
下载并解压乐鑫官方烧录工具 Flash Download Tool(下载地址)
运行 Flash Download Tool,根据开发板的主控芯片与下载接口选择对应选项。下图以 ESP32-S3 与 USB 为例,实际选项以产品硬件设计为准

参数设置
0x00),同时勾选该行最左侧的复选框
等待烧录完成(烧录过程需要一定时间,请耐心等待)
按下复位键,观察运行现象

周一-周五(9:30-6:30)周六(9:30-5:30)
手机:13434470212
邮箱:services04@spotpear.cn
QQ:202004841
