ESP32单片机开发 从入门到精通

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.常用方法

所有接口分五大类:
  1. Audio Board / Audio HAL(硬件 Codec、I2C、音量、电源)
  2. Audio Element(元素通用创建 / 启停 / 配置)
  3. Audio Pipeline(管道核心操作)
  4. 各类读写 Element(spiffs/sd/http/bt/mic)
  5. 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接口,需要安装对应的驱动才能识别

2.问题总结

  • ESP-IDF 的 VFS 不允许 SPIFFS 直接挂载在 / 根目录。根路径已被系统 VFS 占用,需要换一个挂载点。