资讯动态

ESP IoT Solution 的 pwm_audio 组件:利用 LEDC 外设实现无 Codec 的 PWM 音频输出

发布时间:2026/9/19 1:08:46 来源:尧图企业网站定制
ESP IoT Solution 的 pwm_audio 组件利用 LEDC 外设实现无 Codec 的 PWM 音频输出【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solutionpwm_audio 是 ESP IoT Solution 仓库components/audio/pwm_audio中提供的一个音频输出组件它直接复用 ESP32 系列芯片内部 LEDCPWM外设来产生模拟音频信号完全不需要外接音频 Codec 芯片。本文围绕该组件的功能特性、配置结构、完整 API、底层驱动原理与测试用例展开讲解并结合仓库内的 wav_player 示例说明从初始化到播放 WAV 的完整实战流程读完即可在自己的板卡上用任意可输出 GPIO 实现单声道/立体声的 PWM 音频播放。组件概览为什么用 PWM 也能放音频PWM 音频的核心思想是以远高于音频频率的 PWM 载波对音频采样值进行脉宽调制再经由扬声器/耳机的物理低通特性把占空比变化还原成连续的模拟电压波形。ESP32 系列内置的 LEDC 外设本身就是一个多通道、可调分辨率和频率的 PWM 发生器因此 pwm_audio 组件把它改造成声卡使用不需要外部 Codec 芯片省去 I2S 音频解码芯片的物料与电路只用一个 GPIO 加一个扬声器或加一级简单 RC 低通即可出声任意具备输出能力的 GPIO 均可作为音频输出脚引脚分配非常灵活支持 8-bit ~ 16-bit 的 PWM 分辨率源码实际校验范围为 8~10 bit见 pwm_audio.c支持立体声双通道输出支持 8 ~ 48 KHz 采样率源码中SAMPLE_RATE_MIN/SAMPLE_RATE_MAX分别为 8000 与 48000见 pwm_audio.c。从组件元数据看idf_component.yml 声明该组件当前版本为 1.1.2要求idf 4.3并依赖espressif/cmake_utilities而 CHANGELOG.md 记录了它的演进2022 年首发即支持 IDF release/4.4v1.1.0 起支持 IDF release/5.0改用 gptimerv1.1.1 修复了 stop/deinit 时未关闭 gptimer 的问题v1.1.2 补充了 test-apps 测试工程。配置结构体 pwm_audio_config_t在使用前需要先填充pwm_audio_config_t结构体并调用pwm_audio_init()。结构体完整定义位于 include/pwm_audio.h字段含义取值/说明tg_num/timer_num仅 IDF 5.0定时器组与定时器编号TIMER_GROUP_0~TIMER_GROUP_1、TIMER_0~TIMER_1IDF ≥ 5.0 时内部改用 gptimer不再需要这两个字段gpio_num_left左声道 LEDC 输出引脚任意可输出 GPIO传-1表示不启用该声道gpio_num_right右声道 LEDC 输出引脚同上-1表示不启用右声道ledc_channel_left左声道对应的 LEDC 通道LEDC_CHANNEL_0~LEDC_CHANNEL_7芯片不同上限不同ledc_channel_right右声道对应的 LEDC 通道同上需与左声道区分ledc_timer_sel通道使用的 LEDC 定时器源LEDC_TIMER_0~LEDC_TIMER_3duty_resolutionLEDC PWM 位宽LEDC_TIMER_8_BIT/LEDC_TIMER_9_BIT/LEDC_TIMER_10_BIT即 8~10 bitringbuf_len内部环形缓冲区大小字节源码要求不小于BUFFER_MIN_SIZE 2即至少 1024 字节见 pwm_audio.c示例中常用1024 * 8注意左右两个 GPIO 至少必须启用一个否则pwm_audio_init会返回ESP_ERR_INVALID_ARG同时duty_resolution被严格限制在 8~10 bit 范围内。核心 API 全景组件对外 API 全部声明于 include/pwm_audio.h按功能可分为生命周期、播放、参数与音量四类生命周期init / start / stop / deinitesp_err_t pwm_audio_init(const pwm_audio_config_t *cfg)初始化 LEDC 通道、定时器并创建环形缓冲区。成功返回ESP_OK参数错误返回ESP_ERR_INVALID_ARG重复初始化返回ESP_ERR_INVALID_STATE内存不足返回ESP_ERR_NO_MEM。初始化完成后组件自动进入PWM_AUDIO_STATUS_IDLE状态并默认设置参数为 16 kHz / 8 bit / 双声道、音量 0dB见 pwm_audio.c。esp_err_t pwm_audio_start()启动定时器中断把状态切换为PWM_AUDIO_STATUS_BUSY。未初始化或已处于播放状态会返回ESP_ERR_INVALID_STATE。esp_err_t pwm_audio_stop()停止并关闭定时器。关键行为它只停定时器不关 PWM 输出——这样是为了避免播放结束瞬间产生切换噪声同时会清空环形缓冲区rb_flush防止残余数据播放出杂音。若想真正关闭 PWM 输出需要调用pwm_audio_deinit。esp_err_t pwm_audio_deinit()删除定时器、调用ledc_stop停止各 LEDC 通道、把输出 GPIO 复位为输入模式、释放环形缓冲区与内部句柄。数据写入pwm_audio_writeesp_err_t pwm_audio_write(uint8_t *inbuf, size_t len, size_t *bytes_written, TickType_t ticks_to_wait);把待播放的 PCM 数据写入内部环形缓冲区。bytes_written返回实际写入字节数超时后会小于请求长度ticks_to_wait为等待缓冲区空间的最长阻塞时间可传portMAX_DELAY表示无限等待。写入过程会先按 4 字节对齐裁剪可写长度bytes_can_write 0xfffffffc计算 PWM 分辨率与采样位宽之差shift bits_per_sample - duty_resolution按bits_per_sample8/16/32分别做有符号转无符号偏移8-bit 偏移0x7f、16-bit 偏移0x7fff、32-bit 偏移0x7fffffff、按音量系数缩放、右移shift后逐字节写入环形缓冲区见 pwm_audio.c。这也是该组件自动适配位宽的实现基础例如 16-bit 音频配 8-bit PWM 分辨率时只取高字节写入8-bit 音频配 10-bit PWM 时则扩展为 16-bit 数值再取高 10 bit。参数设置set_param / set_sample_rate / get_paramesp_err_t pwm_audio_set_param(int rate, ledc_timer_bit_t bits, int ch)同时设置采样率、位宽与声道数并据此重算定时器中断报警值即每采样周期触发一次中断。约束采样率 8000~48000位宽仅支持 8、16、32声道数仅支持 1单声道或 2立体声。esp_err_t pwm_audio_set_sample_rate(int rate)仅修改采样率。esp_err_t pwm_audio_get_param(int *rate, int *bits, int *ch)查询当前参数不关心的字段可传NULL。重要限制pwm_audio_start()之后即状态为BUSY时不允许再调用set_param/set_sample_rate修改参数必须先pwm_audio_stop()。音量控制set_volume / get_volumeesp_err_t pwm_audio_set_volume(int8_t volume);音量取值范围为-16 ~ 16语义为0原始输出小于 0衰减-16为静音大于 0放大16为两倍输出VOLUME_0DB 16见 pwm_audio.c。实现上内部把音量保存为volume 16写入数据时按sample * volume / 16缩放见 pwm_audio.c 与写入分支。头文件注释特别提醒音量过小时会产生严重失真因此不建议把音量设得过低。状态查询pwm_audio_get_statuspwm_audio_status_t枚举包含PWM_AUDIO_STATUS_UN_INIT未初始化、PWM_AUDIO_STATUS_IDLE空闲、PWM_AUDIO_STATUS_BUSY播放中三种状态可用pwm_audio_get_status()查询当前状态用于流程控制。底层驱动原理定时器 ISR 寄存器直操作pwm_audio 的播放节奏完全由硬件定时器中断驱动其内部流程值得深入理解实现见 pwm_audio.c定时器基准内部使用TIMER_BASE_CLK默认 80 MHz经 16 分频后得到 5 MHz 的计数时钟定时器报警值被设置为5 MHz / framerate即每1/framerate秒触发一次中断gptimer用于 IDF ≥ 5.0传统 timer group 用于 IDF 5.0见 pwm_audio.c。ISR 中取数据并更新占空比每次中断从环形缓冲区读取一个采样点再调用ledc_set_left_duty_fast()/ledc_set_right_duty_fast()把占空比写入 LEDC 寄存器。这两个函数直接操作 LEDC 寄存器地址g_ledc_left_duty_val等全局指针在 init 时一次性取好地址以省去寻址开销写入 duty 并触发更新位从而获得极低的 ISR 开销。分辨率自适应当duty_resolution 8时每个采样点占 2 字节低字节高字节拼成 16 位值当分辨率等于 8 位时每个采样点只占 1 字节见 pwm_audio.c。声道策略若左右 GPIO 都配置了但音频数据只有单声道则把同一份数据复制给右声道见 pwm_audio.c反之若只配了左 GPIO 而音频是立体声数据则丢弃右声道数据只播放左声道见 pwm_audio.c。流控当环形缓冲区空闲空间超过BUFFER_MIN_SIZE256 字节时ISR 通过xSemaphoreGiveFromISR释放信号量唤醒阻塞在pwm_audio_write中的任务形成生产者-消费者模型避免写入任务无限忙等见 pwm_audio.c。此外LEDC 定时器频率在 init 时被固定为APB_CLK_FREQ / (1 duty_resolution)并取 1000 的整倍数见 pwm_audio.c确保 PWM 载波频率稳定且远高于音频频率配合扬声器的低通特性即可输出可听的声音。测试用例验证test-apps组件自带的测试工程位于 components/audio/pwm_audio/test_apps其中 main/pwm_audio_test.c 提供了两组 Unity 测试用例pwm audio sine wave test在 GPIO26左/GPIO25右上以 8-bit 分辨率、48 kHz 采样率输出两个相位相差 90° 的正弦波生成频率约 200 Hz循环写满 240 个采样点的双声道缓冲。示波器抓取结果就是本文章首部展示的sine_wave_test.png波形图——两条几乎重合的规则正弦波验证了 PWM 还原出的模拟信号质量与双声道相位一致性。pwm audio play music test遍历LEDC_TIMER_8_BIT/9_BIT/10_BIT× 单/双声道共 6 种硬件配置播放预置的 32 kHz、8/16 bit 单双声道波形数据见wave_1ch_8bits.c、wave_1ch_16bits.c、wave_2ch_8bits.c、wave_2ch_16bits.c并在播放过程中切换音量0 / -10 / 15最后统一做内存泄漏检查setUp/tearDown对比MALLOC_CAP_8BIT与MALLOC_CAP_32BIT堆余量见 pwm_audio_test.c。从测试代码还可以看到标准的配置模板左右通道 GPIO、LEDC_CHANNEL_0/LEDC_CHANNEL_1、LEDC_TIMER_0、ringbuf_len 1024 * 8这些都可以直接照搬到自己的应用里。实战示例基于 wav_player 播放 WAV 文件组件 README 推荐了仓库内的 examples/audio/wav_player 示例它完整演示了解析 WAV 文件头 → 设置参数 → 启动播放 → 分块写入 PCM 数据的典型用法。硬件连接参考 examples/audio/wav_player/README.md 的接线表不同 SoC 的默认引脚如下SoC左声道 GPIO右声道 GPIOESP32GPIO26GPIO25ESP32-S2GPIO1GPIO2ESP32-S3GPIO1GPIO2ESP32-C3GPIO3GPIO2扬声器/耳机可按如下电路直接挂到 GPIO 上47Ω 电阻限流音量偏小属正常现象需要更大音量和更好音质时可在 GPIO 与喇叭之间加一级功放或简单 RC 低通滤波47R GPIOX ----/\/\/\---- | | /| - | Speaker | | | or - | earphone | \| | -------------- GND初始化与播放流程示例的主逻辑位于 main/app_main.c。初始化代码使用了 10-bit PWM 分辨率pwm_audio_config_t pac { .duty_resolution LEDC_TIMER_10_BIT, .gpio_num_left GPIO_AUDIO_OUTPUT_L, .ledc_channel_left LEDC_CHANNEL_0, .gpio_num_right GPIO_AUDIO_OUTPUT_R, .ledc_channel_right LEDC_CHANNEL_1, .ledc_timer_sel LEDC_TIMER_0, .ringbuf_len 1024 * 8, #if ESP_IDF_VERSION ESP_IDF_VERSION_VAL(5, 0, 0) .tg_num TIMER_GROUP_0, .timer_num TIMER_0, #endif }; pwm_audio_init(pac);播放 WAV 时先解析 RIFF 文件头拿到采样率、声道数与位宽然后依次调用pwm_audio_set_param(wav_head.SampleRate, wav_head.BitsPerSample, wav_head.NumChannels); pwm_audio_start(); /* 循环分块读取示例中每次 4096 字节并写入 */ pwm_audio_write(buffer, len, cnt, 1000 / portTICK_PERIOD_MS); pwm_audio_stop();其中pwm_audio_set_param的位宽参数直接取自 WAV 头的BitsPerSample16-bit 文件即为LEDC_TIMER_16_BIT的枚举值语义——源码内部只按 8/16/32 三个数值判断声道数取自NumChannels1单声道、2立体声可见组件与标准 PCM WAV 格式天然对齐。编译、烧录与运行在examples/audio/wav_player目录下执行idf.py -p PORT flash monitor退出串口监视器按Ctrl-]。运行前可选idf.py menuconfig进行配置在 WAV Player Configuration 菜单选择音频文件来源默认从 flash 的 SPIFFS 分区读取sample.wav也可切换为从 SD 卡读取并自动遍历所有.wav文件同时建议在 Serial Flasher Options 中把 flash size 设为 4 MB并选择自定义分区表partitions.csv示例已提供也可直接使用仓库中的 sdkconfig.defaults。示例运行时输出类似I (335) wav player: [APP] Startup.. I (351) pwm_audio: timer: 0:0 | left io: 25 | right io: 26 | resolution: 10BIT I (640) wav player: frame_rate32000, ch1, width16 I (30426) wav player: File reading complete, total: 1920000 bytes使用注意事项小结初始化限制duty_resolution仅支持 8~10 bitringbuf_len最小 1024 字节左右声道至少启用一个。播放中不能改参数set_param/set_sample_rate必须在stop后调用。音量范围 -16 ~ 16过大/过小都会被拒绝且过小音量会引入明显失真。停止不等于关断pwm_audio_stop保留 PWM 输出以降低切换噪声彻底关闭请用pwm_audio_deinit。音质天花板PWM 直出方案音质受限于位宽与载波适合提示音、语音播报等场景高保真需求应改用 I2S Codec 方案。平台适配IDF 5.0 使用传统 timer group需配置tg_num/timer_numIDF ≥ 5.0 自动切换为 gptimerAPI 完全一致兼容性由组件内部处理。【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价