【Zephyr|ESP32-S3】基础学习:用LEDC外设实现PWM呼吸灯效果 【Zephyr|ESP32-S3】基础学习用LEDC外设实现PWM呼吸灯效果哈喽我是余火一个普通的牛马打工人目前正在学如何使用Zephyr RTOS。上篇用定时器做了消抖和灯效节奏控制定时器的本质是算时间到点就回调。末尾说下一篇用定时器的亲戚——PWM 来做呼吸灯这次来填这个坑。PWM 和定时器的区别定时器是输入端算时间、到点通知PWM 是输出端硬件持续输出特定波形信号。通过调整高低电平的时间比例控制外接 LED 亮度就是呼吸灯的原理。上篇我们用三个线程分别处理按键、WS2812 灯效和定时器但 WS2812 的呼吸效果是软件模拟的——线程周期性地计算亮度、推送像素数据到 WS2812。这次引入真正的硬件 PWM 输出让外设自己维持波形CPU 只需要在切换亮度时更新一次参数就行。本篇学习目标• PWM 原理与占空比•pwm_set_dt设置 PWM 占空比控制亮度• ESP32-S3 LEDC 外设与 Zephyr PWM 驱动 API改了哪些东西在上一篇定时器工程基础上扩展新增 PWM 呼吸灯功能文件改了什么prj.conf新增CONFIG_PWMyoverlay新增 LEDC 外设节点、pinctrl 引脚映射、PWM LED 节点和别名src/main.c新增 PWM 头文件、设备树规格、呼吸灯线程和设备检查CMakeLists.txt无改动另外这次需要简单外接个硬件。ESP32-S3 的引脚本身有一点驱动能力直接在GPIO5外接一颗 LED长脚正极接 GPIO5短脚负极接 GND。PWM 与占空比PWMPulse Width Modulation脉宽调制通过快速开关信号控制输出功率。LED 亮灭切换足够快时人眼看到的是平均亮度——高电平占比越大越亮这个占比就叫占空比。举个具体例子PWM 周期 1000ns即 1kHz 频率如果高电平持续 500ns占空比就是 50%LED 表现为半亮度。高电平 1000ns 就是 100%全亮高电平 0 就是全灭。实际开发中PWM 的应用远不止 LED。电机调速通过改变占空比控制电机平均电压蜂鸣器音调通过改变 PWM 频率控制声音高低屏幕背光通过 PWM 调节亮度以节省功耗。它们本质上都是同一个 API——pwm_set_dt。ESP32-S3 的 LEDC 外设ESP32-S3 有专门的 LEDCLED PWM Controller外设可独立输出多路 PWM 信号。LEDC 底层也是定时器模块驱动的所以和上篇学的k_timer紧密关联。Zephyr 通过标准PWM 驱动 API操作 LEDC不用直接写寄存器。PWM 输出方式原理精度CPU 占用软件翻转 GPIO线程k_msleepgpio_pin_set低受线程调度影响高线程周期唤醒硬件 PWMLEDC外设自动输出波形高硬件计数器驱动零硬件自主运行启用 PWM 子系统prj.conf新增一行CONFIG_PWMy # 启用PWM子系统这一行会拉起 Zephyr 的 PWM 子系统框架包括pwm_dt_spec、pwm_set_dt等 API 的实现。不需要额外启用 LEDC 相关的 Kconfig——CONFIG_PWMy会自动根据设备树中 LEDC 节点的存在拉起 ESP32 的 LEDC 驱动。overlay 配置overlay 需要新增三块内容PWM LED 节点、LEDC 引脚映射和 LEDC 外设声明。PWM LED 节点#include zephyr/dt-bindings/pwm/pwm.h / { pwmleds { compatible pwm-leds; pwm_led_blue: pwm_led_gpio5 { label PWM LED0; pwms ledc0 0 1000 PWM_POLARITY_NORMAL; }; }; };pwms ledc0 0 1000 PWM_POLARITY_NORMAL四个字段含义字段值含义ledc0LEDC 控制器引用使用 ESP32-S3 的 LEDC 外设0通道号LEDC 通道 01000周期纳秒1000ns 1kHz 频率PWM_POLARITY_NORMAL极性高电平有效占空比越高越亮LEDC 引脚映射pinctrl { ledc0_default: ledc0_default { group1 { pinmux LEDC_CH0_GPIO5; output-enable; }; }; };LEDC 通道 0 通过 pinctrl 映射到GPIO5output-enable声明该引脚为输出模式。LEDC 外设声明ledc0 { pinctrl-0 ledc0_default; pinctrl-names default; status okay; #address-cells 1; #size-cells 0; channel00 { reg 0x0; timer 0; }; };channel00声明 LEDC 通道 0timer 0表示使用 LEDC 内部定时器 0LEDC 有独立的定时器模块每个通道绑定一个定时器。ESP32-S3 的 LEDC 支持 8 路通道每路可绑定到不同的 GPIO 引脚和不同的定时器但本篇只需要一路通道 0 驱动 GPIO5 上的一颗 LED。在aliases中补上pwm-led0 pwm_led_blue代码通过DT_ALIAS(pwm_led0)引用这个节点。PWM 设备树规格新增的头文件和设备树规格#includezephyr/drivers/pwm.h/* PWM API: pwm_dt_spec, pwm_set_dt *//* 从 pwm-led0 别名获取 LEDC 参数控制器、通道、周期 */staticconststructpwm_dt_specpwm_led0PWM_DT_SPEC_GET(DT_ALIAS(pwm_led0));PWM_DT_SPEC_GET从设备树中一次性提取控制器引用、通道号和周期封装为pwm_dt_spec结构体后续调用pwm_set_dt时直接传这个结构体即可不需要分别获取控制器、通道和周期三个参数。PWM 呼吸灯线程新增了第三个线程专门驱动 PWM 呼吸灯与 WS2812 灯效线程、按键线程独立运行/* * PWM 呼吸灯参数 * * BREATH_STEPS — 占空比渐变步数100 步从 0% 渐变到 100% * STEP_DELAY_MS — 每步间隔100 步 × 20ms 2 秒完成一个呼吸周期 */#defineBREATH_STEPS100#defineSTEP_DELAY_MS20/* * pwm_breath_thread — PWM 呼吸灯线程 * * 独立于 WS2812 灯效线程运行通过 pwm_set_dt() 控制 GPIO5 外接 LED * 的占空比实现呼吸效果。优先级 6 介于灯效线程(5)和按键线程(7)之间。 */voidpwm_breath_thread(void*p1,void*p2,void*p3){ARG_UNUSED(p1);ARG_UNUSED(p2);ARG_UNUSED(p3);uint8_tstep0;intdir1;while(1){/* pulse 周期 × 步数 / 总步数控制当前亮度 */uint32_tpulse(pwm_led0.period*step)/BREATH_STEPS;pwm_set_dt(pwm_led0,pwm_led0.period,pulse);stepdir;if(stepBREATH_STEPS){dir-1;}if(step0){dir1;}k_msleep(STEP_DELAY_MS);}}K_THREAD_DEFINE(pwm_breath_tid,512,pwm_breath_thread,NULL,NULL,NULL,6,0,0);pwm_set_dt三个参数参数含义pwm_led0pwm_dt_spec 指针包含控制器、通道、周期信息pwm_led0.period周期与设备树中的 1000ns 一致pulse脉冲宽度0 全灭等于周期 全亮PWM 与 WS2812 呼吸灯的区别上篇 WS2812 的呼吸灯是在线程里用k_msleep(20)轮询改亮度每步需要软件计算颜色值再通过 I2SDMA 推送到 WS2812。PWM 呼吸灯则是调用pwm_set_dt后硬件自动维持波形输出线程只管算占空比就行不需要周期性地重新推送数据——这就是硬件 PWM 的优势。三线程架构本篇工程运行着三个独立线程各自负责不同的外设互不干扰线程优先级功能依赖effect_thread5最高WS2812 灯效渲染常亮/呼吸信号量 互斥锁pwm_breath_thread6GPIO5 外接 LED 呼吸灯无同步原语button_thread7最低按键检测 模式切换信号量 互斥锁优先级设计逻辑灯效渲染对时序敏感呼吸效果需要稳定 20ms 步进所以优先级最高5按键处理是事件响应型优先级最低7也不会有明显延迟PWM 呼吸灯介于两者之间6。Zephyr 采用优先级抢占式调度高优先级线程可以打断低优先级线程——但如果 effect_thread 不主动k_msleep让出 CPU低优先级线程会一直得不到执行。main 函数新增检查main 函数里新增了 PWM 设备就绪检查其余初始化逻辑WS2812、GPIO 按键、定时器与上篇完全一致/* 检查 PWM 设备是否就绪 */if(!pwm_is_ready_dt(pwm_led0)){LOG_ERR(PWM device not ready);return0;}LOG_INF(PWM breathing LED on GPIO5 ready);编译烧录后WS2812 灯效和 GPIO5 外接 LED 同时运行WS2812 按定时器节奏自动换色按 BOOT 切换常亮/呼吸模式GPIO5 的 LED 独立做 2 秒周期的呼吸效果三个线程各跑各的互不干扰。LOG 打印了 WS2812 像素数、PWM 就绪信息和按键提示。串口监视器波特率 115200中可以看到如下输出*** Booting Zephyr OS build zephyr-v3.7.0 *** [00:00:00.000,000] inf main: WS2812 strip: 1 pixel(s) [00:00:00.000,000] inf main: PWM breathing LED on GPIO5 ready [00:00:00.000,000] inf main: Press BOOT button to toggle mode (steady/breathe) [00:00:00.500,000] inf main: Mode - breathe常见问题Q1外接 LED 正负极接反了会怎样LED 不亮但不会损坏。检查长脚正极接 GPIO5短脚负极接 GND。Q2LED 一直亮着没有呼吸效果可能是共阳极接法正极接 VCC、负极接 GPIO占空比越高反而越暗。改接线或将极性改为PWM_POLARITY_INVERTED。Q3编译报undefined reference to pwm_set_dt原因prj.conf没加CONFIG_PWMy解决加上配置项后重新编译Q4编译报LEDC_CH0_GPIO5undeclared原因overlay 缺少#include zephyr/dt-bindings/pwm/pwm.h该头文件提供 LEDC 引脚复用宏解决在 overlay 文件顶部加上 includePWM 是嵌入式三大输出方式之一电机调速、蜂鸣器音调、屏幕背光调节都靠它控制功率输出。学会了呼吸灯这些场景本质都是同一个 API。本篇在定时器工程基础上新增了 PWM 呼吸灯线程通过 ESP32-S3 的 LEDC 外设实现硬件 PWM 输出。同样这套pwm_set_dtAPI 可以直接套用到电机调速、蜂鸣器音调、LCD 背光等场景核心就三步设备树声明 → 代码获取 pwm_dt_spec → 线程调用 pwm_set_dt 更新占空比。希望我的笔记能对你有一点点点的帮助欢迎关注一起学习