ESP32单片机开发 从入门到精通
- 单片机
- 27天前
- 136热度
- 0评论
Esp32文档:https://docs.espressif.com/projects/esp-idf/zh_CN/v4.4/esp32/
开发环境
- ESP-IDF = 原厂官方底层工具箱(地基)
- Arduino 核心库 = 套在 IDF 上面的简易封装(精装简装房)
- PlatformIO = 统一管理所有芯片开发的工具管家(装修管家)
1.ESP-IDF
ESP32 芯片是乐鑫造的,ESP-IDF 就是厂家给的完整底层工具包,纯 C 语言开发,没有任何简化、删减,芯片所有硬件能力全部开放。
下载地址:https://dl.espressif.cn/dl/esp-idf/
官方教程:https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32/get-started/index.html
2.Arduino
Arduino 是一套降低单片机门槛的简易开发体系,包含:简易编程语言 + 傻瓜式开发软件 + 配套硬件开发板,专门让不懂底层硬件、不懂复杂 C 语言的普通人快速控制电路板。
国外开发者基于 ESP-IDF 做了一层超级简化的语法封装,兼容传统 Arduino 的简单语法(setup()/loop()、digitalWrite、Serial)。
下载地址:https://docs.arduino.cc/software/ide/
3.PlatformIO
PlatformIO 是统一管理框架的「嵌入式开发管家工具」,本身不能驱动芯片,只负责:自动装工具链、编译、烧录、管理库、配置环境。
官方教程:https://platformio.org/、https://docs.platformio.org/en/latest/
ESP IDF
快速上手ESP IDF,记录使用过程中的一些知识点
Clion配置 ESP IDF开发环境:https://www.jetbrains.com.cn/help/clion/2025.2/esp-idf.html
参考文章:https://developer.espressif.com/blog/clion/
IDF命令:https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32/api-guides/tools/idf-py.html
1.环境初始化
CMD 窗口执行ESP IDF命令之前,先要加载对应的环境变量
# 加载IDF环境变量(每次新开终端必执行)
D:\Espressif\esp-idf-v5.4\export.bat
# 快速查看IDF版本
idf.py --version
# 查看IDF根目录
echo %IDF_PATH%
2.工程创建与管理
# 在当前目录创建工程 my_project
idf.py create-project my_project
# 指定路径创建工程
idf.py create-project --path ./workspace my_demo
# 在当前工程生成自定义组件sensor
idf.py create-component sensor
# 设置目标,自动清理旧配置
idf.py set-target esp32s3
# 查看当前芯片目标
idf.py get-target
# 添加第三方组件依赖(自动下载至managed_components)
idf.py add-dependency espressif/lvgl^8.3
# 更新所有依赖组件
idf.py update-dependencies
# 删除依赖
idf.py remove-dependency espressif/lvgl
3.项目配置
# 打开Kconfig菜单配置(生成sdkconfig) idf.py menuconfig # 将当前配置保存为默认模板(新工程自动继承) idf.py save-defconfig # 从sdkconfig.defaults覆盖当前配置 idf.py defconfig# 强制重新运行 CMake 配置阶段,不编译代码。 idf.py reconfigure# 重置所有配置(删除sdkconfig) idf.py erase-config # 生成compile_commands.json用于代码提示 idf.py reconfigure -D CMAKE_EXPORT_COMPILE_COMMANDS=ON
4.编译构建
# 完整编译工程(生成build/固件)
idf.py build
# 使用 app、bootloader 或 partition-table 参数运行此命令,可选择仅构建应用程序、引导加载程序或分区表。
# 写法1:只编译app(两种等价写法)
idf.py build app
# 简写(官方推荐短命令)
idf.py app
# 生成编译日志(排查报错)
idf.py build > build_log.txt
# 显示应用程序大小,包括占用的 RAM 和 flash 及各部分(如 .bss)的大小
idf.py size
# 详细内存占用分析(IRAM/DRAM/Flash)
idf.py size-files
idf.py size-components
5.清理构建缓存
# 轻度清理:删除编译中间文件,保留sdkconfig
idf.py clean
# 彻底清理:删除整个build文件夹(切换芯片/改分区表必用)
idf.py fullclean
6.串口烧录
# 仅烧录应用固件app.bin
idf.py -p COM3 flash
# 一键编译+烧录(最常用开发流程)
idf.py -p COM3 build flash
# 高速烧录
idf.py -p COM3 -b 921600 flash
# 完整烧录:bootloader+分区表+app+nvs分区
idf.py -p COM3 flash all
# 仅烧录NVS分区(密钥/参数存储)
idf.py -p COM3 flash nvs
# 擦除整片Flash(恢复出厂,清空所有数据)
idf.py -p COM3 erase_flash
# 仅擦除nvs分区
idf.py -p COM3 erase-nvs
7.串口监视器
# 列出所有可用串口(自动识别COM/ttyUSB)
idf.py list-ports
# 打开串口监控(默认115200)
idf.py -p COM3 monitor
# 高速波特率监控
idf.py -p COM3 -b 921600 monitor
# 一键编译+烧录+打开监控(开发高频命令)
idf.py -p COM3 build flash monitor
8.分区表操作
# 打印当前分区表信息
idf.py partition-table
# 生成分区表二进制文件
idf.py partition-table-flash
# 导出分区表CSV
idf.py partition-table-csv
9.固件打包
# 生成OTA升级固件 build/xxx.bin
idf.py ota
# 导出完整合并固件(bootloader+pt+app,可直接烧录工具)
idf.py merge-bin
# 生成factory出厂镜像(含所有分区完整镜像)
idf.py factory_bin
10.高频组合命令
# 编译 + 下载 + 日志
# Windows
idf.py -p COM5 -b 921600 build flash monitor
# Linux
idf.py -p /dev/ttyUSB0 -b 921600 build flash monitor
# 切换芯片完整流程
idf.py set-target esp32c3
idf.py fullclean
idf.py menuconfig
idf.py build flash monitor
# 清空数据重刷
idf.py -p COM3 erase_flash flash monitor
11.组件管理器
组件管理器(IDF Component Manager) = 内嵌在 idf.py 里的Python 工具,搭配乐鑫云端组件仓库,自动下载、版本锁定、加载第三方 / 官方扩展组件(GMF、驱动、算法库这类不在 IDF 内核里的代码包)
- 组件仓库(Registry):https://components.espressif.com,乐鑫的云端服务器仓库,存放:gmf_ai_audio、esp_bt_audio、ESP-GMF、按键库、屏幕驱动等开源组件源码。
- 组件管理器(本地工具):负责联网拉取仓库代码、版本管理、挂载到工程参与编译。
关键文件说明:
- idf_component.yml:依赖清单文件,idf.py add-dependency 命令会自动写入内容,记录需要拉取哪些组件、锁定版本。
- managed_components/:管理器下载下来的所有第三方组件存放目录,不要手动修改里面代码,管理器会校验哈希自动覆盖改动。
- dependencies.lock:版本锁定文件,多人协作、换电脑编译时,保证所有人下载一模一样版本的 GMF,不会出现版本不一致编译报错。
音频开发
ESP32 主控本身,完全不会处理“模拟声音”。
1.ES8388 Codec
人说话、喇叭发声、耳机声音,都是模拟交流电信号。ESP32 只能识别 0 和 1 的数字信号,所以必须依赖一颗音频解码芯片(Codec:ES8388)做中转。整板所有音频硬件关系:
ES8388 Codec(声卡)专门负责模拟与数字的转换,是整板音频的执行者:
- ADC 录音:麦克风模拟声音 → 转为数字给 ESP32
- 自带模拟放大电路、偏置电路、麦克风电路、耳机电路、喇叭功放电路、数模转换、寄存器配置系统。
2.I2C + I2S
所有全球音频Codec(ES8388/ES8311/WM8960/AK4490)统一架构:
- I2C = 控制域(低速、事件型、配置型)
- I2S = 数据域(高速、实时、流式)
I2C 不传输声音,只读写 Codec 内部上百个寄存器。Codec 上电默认:断电休眠、DAC关闭、ADC关闭、功放关闭、静音开启、时钟关闭
必须通过 I2C 逐条写入寄存器才能激活音频系统。I2C 可精确控制的硬件细节(工程级):
- 芯片电源域唤醒、模拟偏置电压开启
- MCLK 时钟分频、采样率配置(8K/16K/44.1K/48K)
- DAC 数字模块开关、ADC 数字模块开关
- 麦克风 PGA 增益(0~42dB)
- 喇叭功放电源开关、耳机通路开关
- 左右声道独立音量、硬件静音
- ALC自动增益、噪声门、高通滤波
- 内部模拟开关切换:SPK / HP / MIC
没有I2C初始化,I2S无论怎么发数据,永远无声音、无录音。
I2S 是硬件同步音频流式传输协议,专门解决:连续、实时、无失真传输PCM音频样本。I2S 拥有严格时钟体系,这是普通串口、SPI、I2C 完全不具备的:
- MCLK 主时钟:Codec工作基准时钟(必须精准,否则爆杂音、变速)
- BCK 位时钟:每一个bit的同步时钟
- LRCK 声道时钟:区分左声道/右声道样本
- DATA 数据线:纯PCM音频样本流
I2S 唯一工作:搬运数字音频样本,不修改任何硬件参数。I2S 数据流是持续不间断的,哪怕静音,数据流也一直在跑,只是Codec不输出模拟信号。
3.播放与录音
播放标准时序:
- 初始化I2C总线
- I2C写入寄存器:唤醒Codec、开启模拟电源、开启DAC模块
- I2C配置采样率、声道、音量、输出通道
- 初始化I2S硬件(时钟、位深、帧格式)
- 解码音频文件为PCM原始样本
- I2S持续推送PCM数据到Codec
- Codec DAC转换为模拟波形 → 输出到喇叭/耳机
录音标准时序:
- 初始化I2C
- I2C开启ADC模块、开启麦克风偏置、设置PGA增益
- 配置采样率、滤波、ALC
- 初始化I2S接收模式
- 麦克风模拟信号进入Codec → ADC转为PCM
- I2S将PCM流式传入ESP32内存
- 程序处理:降噪、保存、识别、上传
4.名词解释
- DAC(数模转换器,Digital-to-Analog Converter):ESP32 通过内部 I2S 总线把数字音频发给 ES8388 的 DAC,DAC 将数字转为模拟电信号,再通过 HPOUT 引脚送到耳机插孔发声;整个数字传输阶段就是 DAC 数字通路。
- 音量软爬坡(volume soft ramp /digital volume ramp):调节音量时,音量不会瞬间跳变,而是在几十毫秒~几百毫秒内平滑缓缓上升 / 下降。
5.总结
hfp通信 下行发给手机的数据,如果需要保持不说话没声音的状态的话,必须填充静音,不能直接不给数据。SCO/eSCO 是 同步链路 ,蓝牙空中接口的时隙是 预留固定分配 的。你的设备没有"不说话就不发"的自由——时隙已经排期了,空着不发会导致:
- 协议栈层:Bluedroid 的 audio_tx 在 SCO 连接期间 按固定间隔 (每 7.5ms 或 10ms)调用,如果你返回 0 或不填缓冲区,协议栈的行为未定义,可能发送陈旧数据、随机数据或导致 underflow 异常
- 射频层:SCO 链路不发送意味着对端(手机)收不到预期数量的数据包,手机端的 PLC(Packet Loss Concealment,丢包隐藏算法)会被触发,产生不可预测的噪声
市面上的量产产品,麦克风开关 绝大多数是硬件开关 ,或者说是 物理 + 硬件联动 ,不仅仅做逻辑静音。简单说就是直接给麦克风物理断电
ESP ADF v2.8
乐鑫官方 ESP Audio Development Framework,基于 ESP-IDF 二次封装的全栈音频开发框架,专门用于 ESP32/ESP32-S3/ESP32-C3 音频设备开发。
以下整合了初始化、管道搭建、事件循环、资源释放的完整流程:
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "audio_element.h"
#include "audio_pipeline.h"
#include "audio_event_iface.h"
#include "audio_common.h"
#include "i2s_stream.h"
#include "fatfs_stream.h"
#include "mp3_decoder.h"
#include "board.h"
static const char *TAG = "SD_MP3_PLAYER";
void app_main(void) {
// ── 1. 初始化音频板 ──
audio_board_handle_t board = audio_board_init();
audio_hal_handle_t hal = audio_board_get_audio_hal(board);
audio_hal_ctrl_codec(hal, AUDIO_HAL_CODEC_MODE_DECODE, AUDIO_HAL_PWR_ON);
audio_hal_set_volume(hal, 70);
// ── 2. 创建 Source Element:从 SD 卡读取 MP3 文件 ──
fatfs_stream_cfg_t fatfs_cfg = FATFS_STREAM_CFG_DEFAULT();
fatfs_cfg.type = AUDIO_STREAM_READER;
audio_element_handle_t reader = fatfs_stream_init(&fatfs_cfg);
audio_element_set_uri(reader, "/sdcard/test.mp3");
// ── 3. 创建 Decoder Element:MP3 解码 ──
mp3_decoder_cfg_t mp3_cfg = mp3_decoder_cfg_default();
audio_element_handle_t decoder = mp3_decoder_init(&mp3_cfg);
// ── 4. 创建 Sink Element:I2S 输出到喇叭 ──
i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT();
i2s_cfg.type = AUDIO_STREAM_WRITER;
// v2.8 的 I2S 配置通过 std_cfg (i2s_std_config_t) 三层结构
i2s_cfg.std_cfg.clk_cfg.sample_rate_hz = 44100;
i2s_cfg.std_cfg.slot_cfg.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT;
i2s_cfg.std_cfg.slot_cfg.slot_mode = I2S_SLOT_MODE_STEREO;
audio_element_handle_t writer = i2s_stream_init(&i2s_cfg);
// ── 5. 搭建管道 ──
audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG();
audio_pipeline_handle_t pipeline = audio_pipeline_init(&pipeline_cfg);
audio_pipeline_register(pipeline, reader, "reader");
audio_pipeline_register(pipeline, decoder, "decoder");
audio_pipeline_register(pipeline, writer, "writer");
audio_pipeline_link(pipeline, (const char *[]){"reader", "decoder", "writer"}, 3);
// ── 6. 事件总线 ──
audio_event_iface_cfg_t evt_cfg = AUDIO_EVENT_IFACE_DEFAULT_CFG();
audio_event_iface_handle_t evt = audio_event_iface_init(&evt_cfg);
audio_pipeline_set_listener(pipeline, evt);
// ── 7. 启动播放 ──
audio_pipeline_run(pipeline);
ESP_LOGI(TAG, "Start playing /sdcard/test.mp3");
// ── 8. 事件循环 ──
while (1) {
audio_event_iface_msg_t msg;
esp_err_t ret = audio_event_iface_listen(evt, &msg, portMAX_DELAY);
if (ret != ESP_OK) continue;
if (msg.cmd == AEL_MSG_CMD_FINISH) {
ESP_LOGI(TAG, "Play finished, stopping pipeline...");
break;
}
}
// ── 9. 释放资源 ──
// 停止 → 等待完全停止 → 终止内部任务 → 销毁管道(deinit 自动销毁注册的 element)
audio_pipeline_stop(pipeline);
audio_pipeline_wait_for_stop(pipeline);
audio_pipeline_terminate(pipeline);
audio_pipeline_deinit(pipeline);
audio_event_iface_destroy(evt);
audio_board_deinit(board);
}
1.四层分层模型
- 硬件驱动层:I2S、Codec、ADC/DAC、SD 卡、蓝牙控制器
- Audio Element 元素层(最小功能单元):输入(sdcard_reader、http_reader、bt_source、mic_recorder、处理(mp3_decoder、eq、volume、resample、noise_suppress)、输出(i2s_writer、bt_sink)
- Audio Pipeline 管道层:串联多个 Element,统一数据流调度
- 应用 & 事件层:事件总线、按键、WiFi、语音识别、业务逻辑
2.核心对象
audio_element 音频元素,每个 Element 独立完成单一功能,标准生命周期:
create() → register → link → run → stop → destroy
# 创建 MP3 解码器
audio_element_handle_t mp3_dec = mp3_decoder_init(mp3_decoder_cfg_default());
audio_pipeline 音频管道:把多个 Element 首尾连接,统一管理数据流,自动缓存同步。
// v2.8 标准管道创建链路
// 1. 创建管道
audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG();
audio_pipeline_handle_t pipeline = audio_pipeline_init(&pipeline_cfg);
// 2. 注册所有 element(v2.8 强制先 register 再 link,旧版顺序会触发断言崩溃)
audio_pipeline_register(pipeline, sd_src, "fatfs");
audio_pipeline_register(pipeline, mp3_dec, "mp3");
audio_pipeline_register(pipeline, i2s_out, "i2s");
// 3. 串联元素:SD 读取 → MP3 解码 → I2S 输出
audio_pipeline_link(pipeline, (const char *[]){"fatfs", "mp3", "i2s"}, 3);
// 4. 启动管道
audio_pipeline_run(pipeline);
3.事件总线
管道、按键、WiFi、蓝牙所有状态统一通过事件上报,非阻塞回调:
// 监听管道事件
audio_event_iface_set_listener(evt_iface, pipeline_listener);
while(1) {
audio_event_iface_msg_t msg;
audio_event_iface_listen(evt_iface, &msg, portMAX_DELAY);
switch(msg.cmd) {
case AEL_MSG_CMD_FINISH: // 歌曲播放完成
break;
case AEL_MSG_CMD_REPORT_STATUS: // 状态变化/错误
break;
} }
}
4.常用方法
- Audio Board / Audio HAL(硬件 Codec、I2C、音量、电源)
- Audio Element(元素通用创建 / 启停 / 配置)
- Audio Pipeline(管道核心操作)
- 各类读写 Element(spiffs/sd/http/bt/mic)
- Event Iface 事件总线
4.1 硬件接口
初始化 / 销毁板卡:
// 初始化音频板,自动创建 I2C、I2S、Codec 实例(ES8388 I2C 配置全在这里内部完成)
audio_board_handle_t audio_board_init(void);
// 释放板卡所有硬件资源
esp_err_t audio_board_deinit(audio_board_handle_t board);
// 获取板卡内置 audio_hal 句柄
audio_hal_handle_t audio_board_get_audio_hal(audio_board_handle_t board);
// 获取板卡 I2C 端口(需要手动读写 Codec/外设时用)
i2c_port_t audio_board_get_i2c_port(audio_board_handle_t board);
// 获取 I2S 引脚配置(极少用到)
audio_board_i2s_conf_t audio_board_get_i2s_conf(audio_board_handle_t board);
Audio HAL 声卡控制(ES8388 音量、电源、增益、静音):
/*----- 初始化 HAL -----*/
audio_hal_config_t hal_cfg = AUDIO_HAL_CONFIG_DEFAULT();
audio_hal_handle_t audio_hal_init(audio_hal_config_t *cfg, audio_board_handle_t board);
/*----- 电源与工作模式切换 -----*/
// 控制 Codec 上下电,模式:播放/录音/同时录放
esp_err_t audio_hal_ctrl_codec(audio_hal_handle_t hal,
audio_hal_codec_mode_t mode,
audio_hal_codec_power_t power);
// 模式枚举:
AUDIO_HAL_CODEC_MODE_DECODE // 播放,仅 DAC 开启
AUDIO_HAL_CODEC_MODE_ENCODE // 录音,仅 ADC 开启
AUDIO_HAL_CODEC_MODE_BOTH // 同时录音+播放
// 电源:AUDIO_HAL_PWR_ON / AUDIO_HAL_PWR_OFF
/*----- 音量、增益、静音(最常用) -----*/
// 设置喇叭 DAC 音量 0~100
esp_err_t audio_hal_set_volume(audio_hal_handle_t hal, int volume);
// 获取当前音量
int audio_hal_get_volume(audio_hal_handle_t hal);
// 喇叭静音控制
esp_err_t audio_hal_mute(audio_hal_handle_t hal, bool enable);
// 麦克风 ADC 增益 0~60
esp_err_t audio_hal_set_adc_gain(audio_hal_handle_t hal, int gain);
// 线路输入增益
esp_err_t audio_hal_set_linein_gain(audio_hal_handle_t hal, int gain);
释放 HAL:
esp_err_t audio_hal_deinit(audio_hal_handle_t hal);
4.2 Audio Element
所有 Audio Element 创建固定三步走:
- 获取默认配置结构体:xxx_cfg_t cfg = xxx_cfg_default();
- 按需修改 cfg 里的参数(缓冲区、任务栈、采样率、文件路径等)
- 调用 xxx_init(&cfg),返回 audio_element_handle_t 句柄
注意:v2.x 中 Stream 系元素(fatfs/spiffs/http/i2s)的路径不再配置在结构体中,而是通过 audio_element_set_uri(el, path) 在 init 之后设置。
基础生命周期:
// 1. 启动 element,加入管道后统一 run 触发,极少手动调用
esp_err_t audio_element_run(audio_element_handle_t el);
// 2. 停止 element 数据流
esp_err_t audio_element_stop(audio_element_handle_t el);
// 3. 等待 element 完全停止(阻塞)
esp_err_t audio_element_wait_for_stop(audio_element_handle_t el);
// 4. 销毁 element,释放内存 / 任务 / 缓冲区
esp_err_t audio_element_destroy(audio_element_handle_t el);
数据流控制:
// 文件类 source 跳转播放位置(SD/SPIFFS 文件生效)
esp_err_t audio_element_seek(audio_element_handle_t el, int offset);
// 清空 element 内部环形缓冲区
esp_err_t audio_element_clear_buffer(audio_element_handle_t el);
// 获取 element 输出缓冲区未读数据长度
int audio_element_get_output_buf_free(audio_element_handle_t el);
int audio_element_get_input_buf_data(audio_element_handle_t el);
URI 与状态:
// 设置 element 的 URI(文件路径/URL),Stream 系元素通过此 API 指定数据来源
esp_err_t audio_element_set_uri(audio_element_handle_t el, const char *uri);
// 获取 element 当前状态:NONE / INIT / RUNNING / PAUSED / STOPPED / FINISHED / ERROR
audio_element_state_t audio_element_get_state(audio_element_handle_t el);
// 设置 element 私有用户数据,回调里取出
esp_err_t audio_element_set_user_data(audio_element_handle_t el, void *data);
void *audio_element_get_user_data(audio_element_handle_t el);
// 注册自定义读写回调(自定义音效 / 自定义解码专用)
esp_err_t audio_element_set_read_cb(audio_element_handle_t el, audio_element_read_cb_t cb);
esp_err_t audio_element_set_write_cb(audio_element_handle_t el, audio_element_write_cb_t cb);
4.3 Audio Pipeline
创建、销毁管道:
// 获取默认管道配置
audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG();
// 初始化管道对象
audio_pipeline_handle_t audio_pipeline_init(audio_pipeline_cfg_t *cfg);
// 销毁整条管道,自动解除所有 element 链接、注销事件、调用 element 销毁函数并释放内存
esp_err_t audio_pipeline_deinit(audio_pipeline_handle_t pipeline);
Element 注册与链路绑定:
// 注册 element 到管道,绑定字符串名称(link 时使用)
esp_err_t audio_pipeline_register(audio_pipeline_handle_t pipeline,
audio_element_handle_t el,
const char *name);
// 解绑 element(切换音源时用)
esp_err_t audio_pipeline_unregister(audio_pipeline_handle_t pipeline, audio_element_handle_t el);
// 串联 element,定义数据流顺序
esp_err_t audio_pipeline_link(audio_pipeline_handle_t pipeline,
const char *name_list[],
int elem_num);
// 示例:{"fatfs","i2s"}, 2
全局启停整条流水线:
// 启动整条管道,所有 element 线程开始运行
esp_err_t audio_pipeline_run(audio_pipeline_handle_t pipeline);
// 暂停数据流,线程保留
esp_err_t audio_pipeline_pause(audio_pipeline_handle_t pipeline);
// 完全停止所有 element 数据流
esp_err_t audio_pipeline_stop(audio_pipeline_handle_t pipeline);
// 阻塞等待管道完全停止
esp_err_t audio_pipeline_wait_for_stop(audio_pipeline_handle_t pipeline);
// 终止管道内部任务,stop 后调用
esp_err_t audio_pipeline_terminate(audio_pipeline_handle_t pipeline);
播放进度控制:
// 整条管道跳转文件偏移(底层调用 element seek)
esp_err_t audio_pipeline_seek(audio_pipeline_handle_t pipeline, int offset);
// 获取当前播放文件总长度
int audio_pipeline_get_file_size(audio_pipeline_handle_t pipeline);
// 获取当前播放偏移
int audio_pipeline_get_file_pos(audio_pipeline_handle_t pipeline);
事件绑定:
// 将管道状态挂载到事件总线,自动推送 AEL_MSG_CMD_FINISH、AEL_MSG_CMD_REPORT_STATUS 等消息
esp_err_t audio_pipeline_set_listener(audio_pipeline_handle_t pipeline,
audio_event_iface_handle_t evt);
4.4 事件总线
创建销毁:
audio_event_iface_cfg_t evt_cfg = AUDIO_EVENT_IFACE_DEFAULT_CFG();
audio_event_iface_handle_t audio_event_iface_init(audio_event_iface_cfg_t *cfg);
esp_err_t audio_event_iface_destroy(audio_event_iface_handle_t evt);
监听与等待消息:
// 阻塞等待事件消息,tick 为超时时间 portMAX_DELAY 永久等待
esp_err_t audio_event_iface_listen(audio_event_iface_handle_t evt,
audio_event_iface_msg_t *msg,
TickType_t ticks);
// 非阻塞获取事件,无消息直接返回
esp_err_t audio_event_iface_read(audio_event_iface_handle_t evt, audio_event_iface_msg_t *msg);
常见事件 cmd 值(定义在 `audio_element.h`):
AEL_MSG_CMD_NONE = 0 // 空事件
AEL_MSG_CMD_FINISH = 2 // 流播放完成(替代旧版 AEL_STREAM_FINISHED)
AEL_MSG_CMD_STOP = 3 // 停止
AEL_MSG_CMD_PAUSE = 4 // 暂停
AEL_MSG_CMD_RESUME = 5 // 恢复
AEL_MSG_CMD_REPORT_STATUS = 8 // 状态上报(内含 AEL_STATUS_ERROR_* 子状态)
AEL_MSG_CMD_REPORT_MUSIC_INFO = 9 // 音乐信息上报
AEL_MSG_CMD_REPORT_POSITION = 11 // 播放位置上报
4.5 网络音频流播放(HTTP + MP3)
#include "http_stream.h"
#include "mp3_decoder.h"
#include "i2s_stream.h"
void play_http_stream(void) {
// ── Source:HTTP 网络流(v2.x 使用 http_stream,URL 通过 set_uri 设置)──
http_stream_cfg_t http_cfg = HTTP_STREAM_CFG_DEFAULT();
http_cfg.type = AUDIO_STREAM_READER;
audio_element_handle_t http_src = http_stream_init(&http_cfg);
audio_element_set_uri(http_src, "http://example.com/stream.mp3");
// ── Decoder:MP3 ──
mp3_decoder_cfg_t mp3_cfg = mp3_decoder_cfg_default();
audio_element_handle_t mp3_dec = mp3_decoder_init(&mp3_cfg);
// ── Sink:I2S 输出到喇叭 ──
i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT();
i2s_cfg.type = AUDIO_STREAM_WRITER;
audio_element_handle_t i2s_out = i2s_stream_init(&i2s_cfg);
// ── 管道 ──
audio_pipeline_cfg_t pipe_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG();
audio_pipeline_handle_t pipe = audio_pipeline_init(&pipe_cfg);
audio_pipeline_register(pipe, http_src, "http");
audio_pipeline_register(pipe, mp3_dec, "mp3");
audio_pipeline_register(pipe, i2s_out, "i2s");
audio_pipeline_link(pipe, (const char *[]){"http", "mp3", "i2s"}, 3);
audio_pipeline_run(pipe);
}
#include "i2s_stream.h"
#include "wav_encoder.h"
#include "fatfs_stream.h"
void record_to_sdcard(void) {
// ── Source:I2S 麦克风输入(16kHz, 16bit, 单声道)──
i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT();
i2s_cfg.type = AUDIO_STREAM_READER;
i2s_cfg.std_cfg.clk_cfg.sample_rate_hz = 16000;
i2s_cfg.std_cfg.slot_cfg.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT;
i2s_cfg.std_cfg.slot_cfg.slot_mode = I2S_SLOT_MODE_MONO;
audio_element_handle_t mic = i2s_stream_init(&i2s_cfg);
// ── Encoder:编码为 WAV 格式 ──
wav_encoder_cfg_t wav_cfg = WAV_ENCODER_CFG_DEFAULT();
audio_element_handle_t encoder = wav_encoder_init(&wav_cfg);
// ── Sink:写入 SD 卡 ──
fatfs_stream_cfg_t writer_cfg = FATFS_STREAM_CFG_DEFAULT();
writer_cfg.type = AUDIO_STREAM_WRITER;
audio_element_handle_t writer = fatfs_stream_init(&writer_cfg);
audio_element_set_uri(writer, "/sdcard/recording.wav");
// ── 管道:i2s_mic → wav_encoder → fatfs_writer ──
audio_pipeline_cfg_t pipe_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG();
audio_pipeline_handle_t pipe = audio_pipeline_init(&pipe_cfg);
audio_pipeline_register(pipe, mic, "mic");
audio_pipeline_register(pipe, encoder, "encoder");
audio_pipeline_register(pipe, writer, "writer");
audio_pipeline_link(pipe, (const char *[]){"mic", "encoder", "writer"}, 3);
audio_pipeline_run(pipe);
}
4.6 麦克风录音保存到 SD 卡
#include "i2s_stream.h"
#include "wav_encoder.h"
#include "fatfs_stream.h"
void record_to_sdcard(void) {
// ── Source:I2S 麦克风输入(16kHz, 16bit, 单声道)──
i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT();
i2s_cfg.type = AUDIO_STREAM_READER;
i2s_cfg.std_cfg.clk_cfg.sample_rate_hz = 16000;
i2s_cfg.std_cfg.slot_cfg.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT;
i2s_cfg.std_cfg.slot_cfg.slot_mode = I2S_SLOT_MODE_MONO;
audio_element_handle_t mic = i2s_stream_init(&i2s_cfg);
// ── Encoder:编码为 WAV 格式 ──
wav_encoder_cfg_t wav_cfg = WAV_ENCODER_CFG_DEFAULT();
audio_element_handle_t encoder = wav_encoder_init(&wav_cfg);
// ── Sink:写入 SD 卡 ──
fatfs_stream_cfg_t writer_cfg = FATFS_STREAM_CFG_DEFAULT();
writer_cfg.type = AUDIO_STREAM_WRITER;
audio_element_handle_t writer = fatfs_stream_init(&writer_cfg);
audio_element_set_uri(writer, "/sdcard/recording.wav");
// ── 管道:i2s_mic → wav_encoder → fatfs_writer ──
audio_pipeline_cfg_t pipe_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG();
audio_pipeline_handle_t pipe = audio_pipeline_init(&pipe_cfg);
audio_pipeline_register(pipe, mic, "mic");
audio_pipeline_register(pipe, encoder, "encoder");
audio_pipeline_register(pipe, writer, "writer");
audio_pipeline_link(pipe, (const char *[]){"mic", "encoder", "writer"}, 3);
audio_pipeline_run(pipe);
}
4.7 Stream / Source Element (v2.x 统一 API)
v2.x 将 SD、SPIFFS、HTTP、I2S 等所有流式 I/O 统一为 xxx_stream 命名规范,通过 type 字段区分读写方向,路径通过 audio_element_set_uri() 设置。
fatfs_stream(SD 卡文件读写):
// 读取 SD 卡文件
fatfs_stream_cfg_t fatfs_cfg = FATFS_STREAM_CFG_DEFAULT();
fatfs_cfg.type = AUDIO_STREAM_READER; // 读取模式
audio_element_handle_t fatfs_src = fatfs_stream_init(&fatfs_cfg);
audio_element_set_uri(fatfs_src, "/sdcard/test.mp3");
// 写入 SD 卡文件(录音保存等)
fatfs_stream_cfg_t fatfs_w_cfg = FATFS_STREAM_CFG_DEFAULT();
fatfs_w_cfg.type = AUDIO_STREAM_WRITER; // 写入模式
audio_element_handle_t fatfs_sink = fatfs_stream_init(&fatfs_w_cfg);
audio_element_set_uri(fatfs_sink, "/sdcard/recording.wav");
spiffs_stream(Flash 文件读写):
spiffs_stream_cfg_t spiffs_cfg = SPIFFS_STREAM_CFG_DEFAULT();
spiffs_cfg.type = AUDIO_STREAM_READER;
audio_element_handle_t spiffs_src = spiffs_stream_init(&spiffs_cfg);
audio_element_set_uri(spiffs_src, "/spiffs/music.pcm");
http_stream(网络音频流):
http_stream_cfg_t http_cfg = HTTP_STREAM_CFG_DEFAULT();
http_cfg.type = AUDIO_STREAM_READER;
audio_element_handle_t http_src = http_stream_init(&http_cfg);
audio_element_set_uri(http_src, "http://example.com/stream.mp3");
i2s_stream(I2S 音频输入输出,替代旧版 i2s_writer / mic_recorder):
// I2S 输出到喇叭(播放)
i2s_stream_cfg_t i2s_out_cfg = I2S_STREAM_CFG_DEFAULT();
i2s_out_cfg.type = AUDIO_STREAM_WRITER;
i2s_out_cfg.std_cfg.clk_cfg.sample_rate_hz = 44100;
i2s_out_cfg.std_cfg.slot_cfg.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT;
i2s_out_cfg.std_cfg.slot_cfg.slot_mode = I2S_SLOT_MODE_STEREO;
audio_element_handle_t i2s_out = i2s_stream_init(&i2s_out_cfg);
// I2S 麦克风输入(录音)
i2s_stream_cfg_t i2s_in_cfg = I2S_STREAM_CFG_DEFAULT();
i2s_in_cfg.type = AUDIO_STREAM_READER;
i2s_in_cfg.std_cfg.clk_cfg.sample_rate_hz = 16000;
i2s_in_cfg.std_cfg.slot_cfg.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT;
i2s_in_cfg.std_cfg.slot_cfg.slot_mode = I2S_SLOT_MODE_MONO;
audio_element_handle_t i2s_in = i2s_stream_init(&i2s_in_cfg);
bt_source 蓝牙 A2DP 手机音源:
bt_source_cfg_t bt_cfg = BT_SOURCE_CFG_DEFAULT();
bt_cfg.device_name = "ESP_BT_SPEAKER"; // 蓝牙设备名
audio_element_handle_t bt_src = bt_source_init(&bt_cfg);
bt_sink 音频发送到蓝牙耳机:
bt_sink_cfg_t bts_cfg = BT_SINK_CFG_DEFAULT();
audio_element_handle_t bt_sink = bt_sink_init(&bts_cfg);
4.8 Filter Element
所有解码器统一模板,以 MP3 举例:
mp3_decoder_cfg_t mp3_cfg = mp3_decoder_cfg_default();
mp3_cfg.task_stack = 4096;
mp3_cfg.out_rb_size = 16384;
// aac_decoder_init / wav_decoder_init / flac_decoder_init / opus_decoder_init
audio_element_handle_t mp3_dec = mp3_decoder_init(&mp3_cfg);
音效处理:
// EQ 均衡器
eq_cfg_t eq_cfg = EQ_CFG_DEFAULT();
audio_element_handle_t eq_filter = eq_init(&eq_cfg);
// 重采样(统一采样率,必备)
resample_cfg_t res_cfg = RESAMPLE_CFG_DEFAULT();
audio_element_handle_t res_filter = resample_init(&res_cfg);
// 降噪 NS
noise_suppress_cfg_t ns_cfg = NOISE_SUPPRESS_CFG_DEFAULT();
audio_element_handle_t ns_filter = noise_suppress_init(&ns_cfg);
// AGC 自动增益
agc_cfg_t agc_cfg = AGC_CFG_DEFAULT();
audio_element_handle_t agc_filter = agc_init(&agc_cfg);
C和C++
回顾一下C和C++的知识点
1.编译和链接
编译(Compile)的时候,编译器一次只看一个 cpp,完全看不见别的 cpp
#include 预处理(编译最先做)
#include "tool.h" 不是导入文件,是纯文本复制粘贴。
#include 会把 h 的文字复制粘贴进 main.cpp。编译器看完就懂:哦,有个叫 hello 的函数,可以调用。
每个 cpp 独立生成一份 .o 文件,互相不知道对方的存在。只解决:代码语法对不对,能不能写调用语句。
编译完一堆 .o 文件,现在交给链接器干活。链接器能看到全部 .o 文件,它做两件关键事:
- 遍历所有 .o,把所有函数、变量整理出来;
- 填坑:main.o 里只写了 “我要调用 hello”,没有 hello 代码;
链接器去 tool.o 里找到 hello 的完整机器码,把两者绑定。绑定完成后,整合所有代码,生成最终能双击运行的程序(exe)。
2.CMakeLists.txt
CMake = 跨平台编译配置工具,本身不会编译代码,只负责:读取你写的 CMakeLists.txt 规则 → 自动生成对应系统的编译脚本
- Windows:生成 Ninja / MSBuild 构建文件
- Linux/Mac:生成 Makefile
真正编译代码的是:gcc / clang / Ninja 编译器工具链,CMake 只是「总指挥」。
CMakeLists.txt 是写给 CMake 程序看的配置脚本文件,文本格式,固定文件名不能改大小写。
操作手册
记录开发过程中的一些知识点
1.USB驱动
不同的开发板,可能使用不同的USB接口,需要安装对应的驱动才能识别
- ESP32 WROOM 32E:https://www.wch.cn/downloads/CH341SER_EXE.html
- ESP32 A1S:https://www.silabs.com/software-and-tools/usb-to-uart-bridge-vcp-drivers?tab=downloads
2.问题总结
- ESP-IDF 的 VFS 不允许 SPIFFS 直接挂载在 / 根目录。根路径已被系统 VFS 占用,需要换一个挂载点。
