ESP-IDF Sigma-Delta 调制(SDM)外设驱动实战指南:PDM 脉冲输出、8 位 DAC 与 LED 调光
【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf
导读
本文围绕 ESP-IDF 中的 Sigma-Delta 调制(SDM)外设驱动展开,讲解如何利用芯片内置的二阶 sigma-delta 调制器,在任意支持输出的 GPIO 上产生独立的多通道 PDM 脉冲信号。读完本文,你将掌握 SDM 通道的创建、启用/禁用、脉冲密度(等效占空比)设置、电源管理适配、IRAM 安全与线程安全特性,并能够基于官方示例实现 LED 调光、8 位 DAC(配合低通滤波器输出模拟波形)以及 Class D 功放驱动等实战方案。文中所有 API 与配置说明均以当前仓库 driver/sdm.h 与 sdm.c 实现为准。
工作原理与典型应用场景
SDM(Sigma-Delta Modulation,Σ-Δ 调制)将模拟电压信号转换为脉冲频率或脉冲密度,本质上是脉冲密度调制(PDM):输出信号中“1”与“0”的密度比例(即脉冲密度)承载了模拟量信息,这与传统 PWM 通过调节脉冲宽度来表达占空比不同。ESP 芯片内置二阶 sigma-delta 调制器,可以为多个通道独立生成 PDM 脉冲,具体硬件通道数量因芯片型号而异(如 ESP32、ESP32-C3、ESP32-S2/S3、ESP32-P4 等均支持),请查阅对应型号的《Technical Reference Manual》中 GPIO 与 IOMUX 章节确认。
从源码结构看,SDM 外设驱动位于 components/esp_driver_sdm 组件,对外接口集中在driver/sdm.h,底层实现位于 src/sdm.c,并通过 hal/sdm_hal.h 等 HAL 层操作寄存器。
典型的 SDM 通道应用场景包括:
- LED 调光:直接驱动 LED 或 LCD 背光,人眼本身即对光强变化有低通滤波效果,无需额外滤波器;
- 简易 8 位 DAC:配合有源 RC 低通滤波器,将 PDM 脉冲还原为模拟电压,精度约 8 位;
- Class D 放大器:配合半桥或全桥电路加 LC 低通滤波器,输出音频级模拟信号。
资源分配:创建与回收 SDM 通道
在 ESP-IDF 中,SDM 通道的信息与属性通过sdm_channel_handle_t句柄管理(定义为struct sdm_channel_t *,见 driver/sdm.h)。每个通道都能输出由硬件生成的二值 sigma-delta 调制信号。驱动将所有可用通道放在一个通道池中统一管理,因此无需手动将某个固定通道绑定到 GPIO——只需指定 GPIO 编号,驱动会自动分配空闲硬件通道。
创建通道调用 sdm_new_channel,通道相关配置通过sdm_config_t结构体传入:
| 配置成员 | 说明 |
|---|---|
gpio_num | PDM 脉冲输出的 GPIO 编号,必须是合法输出 GPIO |
clk_src | SDM 模块的时钟源,所有通道必须选择相同的时钟源(否则返回ESP_ERR_INVALID_STATE) |
sample_rate_hz | SDM 模块的采样率(过采样率),决定载波脉冲频率;采样率越高,输出信号 SNR 越高,滤波后越容易还原原始信号 |
flags.invert_out | 是否翻转输出信号 |
flags.allow_pd | 是否允许系统进入睡眠时关闭该外设电源域(省电但需额外 RAM 保存寄存器上下文,且要求芯片支持 SDM 睡眠保留,否则返回ESP_ERR_NOT_SUPPORTED) |
clk_src的类型为sdm_clock_source_t(来自 hal/sdm_types.h),在绝大多数目标芯片上可直接使用SDM_CLK_SRC_DEFAULT,其底层解析为 APB 时钟。
sdm_new_channel可能因内存不足、参数非法等原因失败;当所有硬件 SDM 通道都被占满时,会返回ESP_ERR_NOT_FOUND。这一点在官方单元测试 test_sdm.cpp 中有明确验证:先反复创建通道填满硬件资源,再尝试创建即断言ESP_ERR_NOT_FOUND。
sdm_channel_handle_t chan = NULL; sdm_config_t config = { .clk_src = SDM_CLK_SRC_DEFAULT, .sample_rate_hz = 1 * 1000 * 1000, // 1 MHz 采样率 .gpio_num = 0, }; ESP_ERROR_CHECK(sdm_new_channel(&config, &chan));从实现上看,sdm_new_channel 内部完成了几件关键工作:
- 通过
sdm_register_to_group将通道注册到驱动管理的 group 池中,并依据SDM_CAPS_GET(CHANS_PER_INST)在组内查找空闲通道槽位; - 调用
esp_gpio_reserve预留 GPIO,并通过gpio_matrix_output把 sigma-delta 信号经 GPIO 矩阵连接到指定引脚(因此 SDM 不受 IOMUX 固定引脚限制); - 根据源时钟频率与期望采样率,通过
hal_utils_calc_clk_div_integer计算整数分频系数prescale并写入寄存器;若实际采样率与期望值存在精度损失,会打印precision loss警告日志,同时将chan->sample_rate_hz更新为实际频率; - 将初始脉冲密度预置为 0,并将通道状态机初始化为
SDM_FSM_INIT。
当通道不再使用时,调用 sdm_del_channel 回收,释放底层硬件通道供其他用途。删除前必须先调用sdm_channel_disable使其处于 init 状态,或确认从未被sdm_channel_enable启用过——否则返回ESP_ERR_INVALID_STATE。sdm_del_channel内部使用atomic_compare_exchange_strong检查状态机,确保只在SDM_FSM_INIT状态删除通道,从而避免与并发enable操作冲突(见 sdm.c)。
启用与禁用通道
在对 SDM 通道做进一步 IO 控制前,必须先调用 sdm_channel_enable 启用通道。该函数内部:
- 将通道状态从init切换到enable(状态机
SDM_FSM_INIT → SDM_FSM_ENABLE); - 若选择了特定时钟源(如 APB 时钟)且启用了电源管理,会获取电源管理锁,防止系统在 SDM 工作时降频或进入 Light-sleep(详见下文电源管理章节)。
相反,sdm_channel_disable 执行相反操作:把通道状态切回init,并释放电源管理锁。若通道本身未处于 enable 状态就调用 disable,会返回ESP_ERR_INVALID_STATE。
ESP_ERROR_CHECK(sdm_channel_enable(chan)); // ... 输出 PDM 脉冲 ... ESP_ERROR_CHECK(sdm_channel_disable(chan));设置脉冲密度:决定等效占空比
对于输出的 PDM 信号,脉冲密度决定了经低通滤波器还原后的模拟电压。还原电压由下式计算:
Vout = VDD_IO / 256 * duty + VDD_IO / 2其中duty即 sdm_channel_set_pulse_density 的量化脉冲密度参数,类型为 8 位有符号整数,取值范围-128 到 127。density取值不同,输出信号的占空比随之改变,例如设为 0 时,输出信号占空比约为 50%(对应还原电压约 VDD_IO/2)。
代码注释还给出了一条实践建议:虽然参数范围是 -128~127,但[-90, 90] 区间能提供更好的随机性,即输出脉冲密度更均匀、调制噪声更低。官方测试 test_sdm.cpp 对该 API 覆盖了边界值 127 与 -128 的调用验证。
// 将脉冲密度设置为 0,输出约 50% 占空比 ESP_ERROR_CHECK(sdm_channel_set_pulse_density(chan, 0)); // 设置边界值 ESP_ERROR_CHECK(sdm_channel_set_pulse_density(chan, 127)); ESP_ERROR_CHECK(sdm_channel_set_pulse_density(chan, -128));从实现看,sdm_channel_set_pulse_density 允许在通道处于init 或 enable任一状态时调用(通过状态机原子比较实现,调用后恢复原状态),并在portENTER_CRITICAL_SAFE临界区内写sdm_ll_set_pulse_density寄存器,保证多任务并发下寄存器操作的原子性。
电源管理:时钟源选择与功耗
当启用电源管理(即CONFIG_PM_ENABLE开启)时,系统在进入 Light-sleep 前会动态调整 APB 频率,这可能改变 sigma-delta 调制器的实际采样率,进而影响输出信号质量。
驱动通过获取类型为ESP_PM_APB_FREQ_MAX的电源管理锁来阻止系统改变 APB 频率:
- 凡是以
SDM_CLK_SRC_APB作为时钟源创建的 SDM 通道,驱动保证在sdm_channel_enable时获取该锁,在sdm_channel_disable时释放; - 因此 SDM 运行期间 APB 频率被锁定,采样率保持稳定。
对应实现见 sdm.c:在 group 安装时按SDM_CAPS_GET(FUNC_CLOCK_SUPPORT_APB)判断是否支持 APB 时钟源,若支持且选中 APB,则创建ESP_PM_APB_FREQ_MAX类型的锁(esp_pm_lock_create),随后在 sdm_channel_enable 中esp_pm_lock_acquire、在 sdm_channel_disable 中esp_pm_lock_release。
需要留意的是:所有 SDM 通道共享同一时钟源与同一把 PM 锁(锁挂在 group 上),因此单个通道的 enable/disable 会直接影响同组其他通道的电源管理状态,实际使用时应保持所有通道生命周期一致性。
IRAM 安全:Cache 关闭时仍可执行的函数
开启 Kconfig 选项CONFIG_SDM_CTRL_FUNC_IN_IRAM后,常用的 IO 控制函数会被放入 IRAM,从而在Cache 被禁用(如 Flash 写操作、某些中断上下文)时仍可执行。符合 IRAM 安全要求的函数为:
sdm_channel_set_pulse_density
从 Kconfig(components/esp_driver_sdm/Kconfig)看,该选项还会隐式select SDM_OBJ_CACHE_SAFE,后者保证驱动对象不会从可被关闭 Cache 的内存区域分配,从而确保 IRAM 中断路径下访问句柄的安全性。选项帮助文本同时说明开启该选项还能提升驱动性能。测试 test_sdm_iram.cpp 与 CI 配置 sdkconfig.ci.iram_safe 专门验证了这一场景。
线程安全与中断上下文支持
SDM 驱动通过临界区保证寄存器级操作的原子性,句柄中的关键成员同样受临界区保护;驱动的内部状态机使用原子指令(stdatomic.h的atomic_compare_exchange_strong/atomic_store)实现线程安全,并通过状态检查拦截非法并发操作(例如enable与delete之间的竞争、重复 enable/disable 等)。因此,SDM 驱动 API 可以在多线程环境中直接使用,无需额外加锁。
此外,以下函数还可以在中断上下文中使用:
sdm_channel_set_pulse_density
这正是官方 DAC 示例(见下文)能在 gptimer 的 ISR 回调里直接调用该函数更新正弦波数据的原因。
Kconfig 选项
SDM 驱动支持以下 Kconfig 选项(配置项定义于 components/esp_driver_sdm/Kconfig,以SOC_SDM_SUPPORTED为依赖条件):
CONFIG_SDM_CTRL_FUNC_IN_IRAM:控制 SDM 通道控制函数放置在 IRAM 还是 Flash,详见上文 IRAM 安全章节;开启后同时生效SDM_OBJ_CACHE_SAFE;CONFIG_SDM_ENABLE_DEBUG_LOG:强制启用调试日志输出。开启后驱动会忽略全局日志设置、把所有日志编译进固件,并将运行时日志级别设为 VERBOSE,会增大固件体积(实现见 sdm.c 的构造器函数,将日志级别覆盖为ESP_LOG_VERBOSE),仅建议调试时临时开启。
将 PDM 信号转换为模拟信号(可选)
如果将 sigma-delta 信号直接接到 LED 做亮度调节,通常无需添加任何滤波器——人眼本身对光强变化就是一个天然的低通滤波器。但如果要测量真实电压或观察模拟波形,就需要设计模拟低通滤波器,且官方建议优先使用有源滤波器而非无源滤波器,以获得更好的隔离效果、避免过多压降。
官方文档给出了可参考的Sallen-Key 拓扑低通滤波器电路作为设计起点:
该电路图位于仓库 docs/_static/typical_sallenkey_LP_filter.png,可用运算放大器配合 RC 网络构成二阶有源低通,将 PDM 脉冲平滑为连续模拟电压。
应用示例
仓库提供了两个与本文档直接对应的官方示例:
peripherals/sigma_delta/sdm_dac:演示如何用 sigma-delta 驱动充当8 位 DAC,配合有源/无源低通滤波器输出100 Hz 正弦波。核心思路见 sdm_dac_example_main.c:
- 以 10 MHz 过采样率创建 SDM 通道并绑定 GPIO(
EXAMPLE_SIGMA_DELTA_GPIO_NUM); - 用 1 MHz 分辨率的 gptimer 每 100 µs 触发一次 ISR,在回调内依次调用
sdm_channel_set_pulse_density写入预计算的 100 Hz 正弦波采样表(sine_wave[]),因为该 API 支持中断上下文,所以可以安全地在定时器回调里调用; - 用
idf.py -p PORT flash monitor编译烧录运行后,用示波器观察:GPIO 直连处为密度变化的方波(原始 SDM 输出),经低通滤波器后即为平滑正弦波。README 中的运行日志依次打印Sigma-delta output is attached to GPIO 0、Timer callback registered, interval 100 us等信息,参见 sdm_dac/README.md。
- 以 10 MHz 过采样率创建 SDM 通道并绑定 GPIO(
peripherals/sigma_delta/sdm_led:演示如何使用 sigma-delta 驱动控制LED 或 LCD 背光的亮度。硬件连接只需在输出 GPIO 串联 330 Ω 限流电阻再接 LED 到 GND,程序会持续改变脉冲密度,使 LED 缓慢地渐亮渐暗,参见 sdm_led/README.md。
源码结构速览
若需深入阅读 SDM 驱动实现,可按以下路径继续探索:
- 公共 API 与配置结构体:driver/sdm.h
- 驱动核心实现(通道池、状态机、PM 锁、分频计算):src/sdm.c
- 内部数据结构(group/channel、FSM 枚举):src/sdm_priv.h
- Kconfig 配置项:Kconfig
- 单元测试(资源耗尽、脉冲密度边界、IRAM 安全、睡眠保留):test_apps/sigma_delta/main
- 官方示例:见上文「应用示例」一节
注意事项小结
- 时钟源统一:同一时刻所有 SDM 通道必须使用相同的时钟源,混用会在创建时返回
ESP_ERR_INVALID_STATE; - 采样率范围:采样率实际可设置范围由源时钟频率决定(约
src_clk_hz / SDM_LL_PRESCALE_MAX到src_clk_hz),超范围会返回ESP_ERR_INVALID_ARG,存在分频精度损失时会打印警告日志; - 资源有限:硬件 SDM 通道数量有限,创建前请检查
sdm_new_channel返回值,通道占满时返回ESP_ERR_NOT_FOUND; - 删除前置条件:删除通道前必须先 disable 或确保从未 enable,否则返回
ESP_ERR_INVALID_STATE; - 脉冲密度边界:
density的有效范围为 -128~127,实践上建议工作在 [-90, 90] 区间以获得更好的调制随机性。
【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考