基于 Zephyr RTOS,使用 STM32 定时器 PWM 驱动双 LED 呼吸效果
1. 概述
本项目使用 Zephyr 的 PWM LED 驱动 (pwm-leds) 控制 STM32 Nucleo-C562RE 板上的两个 LED(PA0 / PA1),产生"呼吸灯"效果 —— 两个 LED 的亮度按三角波规律交替变化,亮度的最高点彼此错开半个周期,形成亮度在两灯之间"流动"的视觉效果。
| 目标芯片 | STM32C562 (Cortex-M33) |
| 定时器 | TIM2 (通用定时器) |
| PWM 通道 | CH1 (PA0) / CH2 (PA1) |
| 复用功能 | AF1 |
| 周期 | 1 ms(1 kHz PWM) |
| 占空比粒度 | 0 ~ 100(百分比,0~10000 内部) |
| 呼吸周期 | 2 s(吸 + 呼) |
| 步进 | 20 ms / 步(每周期 100 步) |
2. 硬件连接
┌──────────────────────────┐ PA0 (TIM2_CH1) ─► LED1 ──► GND PA1 (TIM2_CH2) ─► LED2 ──► GND └──────────────────────────┘
注意:板载 LD1(PA5,对应 led0 alias)由板级 BSP 单独管理,本驱动不触碰它。PA2 在本板上为 USART2_TX,故意未使用。
3. 启用配置 (prj.conf)
CONFIG_GPIO=y CONFIG_PWM=y CONFIG_LED=y CONFIG_LED_PWM=y # pwm-leds 驱动 CONFIG_PWM_STM32=y # STM32 的 PWM 后端
| CONFIG_PWM | 启用 Zephyr PWM 通用子系统 |
| CONFIG_PWM_STM32 | 启用 STM32 系列 PWM 驱动后端 |
| CONFIG_LED | 启用 LED 通用 API (<zephyr/drivers/led.h>) |
| CONFIG_LED_PWM | 启用基于 PWM 的 LED 驱动 pwm-leds |
4. 设备树配置 (boards/nucleo_c562re.overlay)
4.1 关键节点
#include <zephyr/dt-bindings/pwm/pwm.h>
#include <zephyr/dt-bindings/pinctrl/stm32-pinctrl.h>
/ {
aliases {
led1 = &led_2; /* 把应用可见的 led1/led2 指向 pwm_leds 子节点 */
led2 = &led_3;
};
};
&{/} {
pwm_leds {
compatible = "pwm-leds";
led_2: led_2 {
/* PA0 = TIM2_CH1, AF1 */
pwms = <&pwm2 1 PWM_MSEC(1) 0>;
label = "PA0_LED";
};
led_3: led_3 {
/* PA1 = TIM2_CH2, AF1 */
pwms = <&pwm2 2 PWM_MSEC(1) 0>;
label = "PA1_LED";
};
};
};
&pinctrl {
timers2_pwm_pa0: timers2_pwm_pa0 {
pinmux = <STM32_PINMUX('A', 0, AF1)>; /* TIM2_CH1 */
};
timers2_pwm_pa1: timers2_pwm_pa1 {
pinmux = <STM32_PINMUX('A', 1, AF1)>; /* TIM2_CH2 */
};
};
&timers2 {
status = "okay";
st,prescaler = <0>;
pwm2: pwm {
pinctrl-0 = <&timers2_pwm_pa0 &timers2_pwm_pa1>;
pinctrl-names = "default";
status = "okay";
};
};4.2 pwms 属性详解
pwms = <&pwm2 1 PWM_MSEC(1) 0>; │ │ │ │ │ │ │ └─ 初始相位(0 = 与通道 0 同步) │ │ └───────────── 周期 = 1 ms │ └──────────────── 通道号 (1 = CH1, 2 = CH2) └─────────────────────── 控制器 phandle (pwm2 = TIM2)
PWM_MSEC(1):使用 <zephyr/dt-bindings/pwm/pwm.h> 中的宏,把 1 ms 转成对应定时器 ticks。
0:初始占空比(占空比由运行时 led_set_brightness_dt() 覆盖)。
周期最终由应用通过 led_set_brightness_dt() 间接决定 — 此处只是初始化默认值。
4.3 引脚配置说明
| &pinctrl | 注入自定义 pinctrl 子节点,把 PA0/PA1 复用到 TIM2 的 AF1 |
| &timers2 | 启用 TIM2 控制器;st,prescaler = <0> 表示 1:1 分频(不预分频) |
| pinctrl-0 | 控制器默认状态使用上面定义的两组引脚配置 |
5. 应用代码解析 (src/main.c)
5.1 关键宏
#define LED_COUNT 2 #define BREATH_PERIOD_MS 2000 /* 一个完整呼吸 ≈ 2 s */ #define STEP_MS 20 /* 每步 20 ms → 一周期 100 步 */ #define LED1_NODE DT_ALIAS(led1) /* = &led_2 (PA0) */ #define LED2_NODE DT_ALIAS(led2) /* = &led_3 (PA1) */
编译期保护:若 led1/led2 alias 缺失,编译直接报错。
#if !DT_NODE_HAS_STATUS(LED1_NODE, okay) || \ !DT_NODE_HAS_STATUS(LED2_NODE, okay) #error "overlay not effective: led1/led2 aliases missing" #endif
5.2 获取 LED spec
static const struct led_dt_spec leds[] = {
LED_DT_SPEC_GET(LED1_NODE), /* PA0 = TIM2_CH1 */
LED_DT_SPEC_GET(LED2_NODE), /* PA1 = TIM2_CH2 */
};led_dt_spec 由 LED_DT_SPEC_GET() 在编译期从 devicetree 提取(控制器指针、通道、周期)。
5.3 三角波亮度
static inline uint8_t triangle_brightness(uint16_t phase, uint16_t steps)
{
if (phase < steps / 2) {
return (uint8_t)((100U * phase) / (steps / 2));
} else {
return (uint8_t)(100U - (100U * (phase - steps / 2)) / (steps / 2));
}
}输入 phase ∈ [0, steps),输出 0→100→0 的对称三角波。
| 0 | 0 |
| 25 | 50 |
| 49 | 98 |
| 50 | 100 |
| 75 | 50 |
| 99 | 0 |
5.4 主循环
uint16_t phase = 0;
while (1) {
for (size_t i = 0; i < LED_COUNT; i++) {
uint16_t p = (phase + i * phase_offset) % steps;
uint8_t b = triangle_brightness(p, steps);
led_set_brightness_dt(&leds[i], b);
}
phase = (phase + 1) % steps;
k_msleep(STEP_MS);
}phase_offset = (steps / 2) / LED_COUNT = 25:两灯之间错开 1/4 周期,视觉上呈"流动"。
每次循环 phase++ 并 k_msleep(20),整周期 100 × 20 ms = 2000 ms。
6. 关键 API 速查
| LED_DT_SPEC_GET(node) | <zephyr/drivers/led.h> | 编译期从 devicetree 构造 led_dt_spec |
| led_is_ready_dt(spec) | 同上 | 运行时检查 LED 设备是否就绪 |
| led_set_brightness_dt(spec, value) | 同上 | 设置亮度(0 = 灭,100 = 最亮;LED_COLOR_* 通用 LED 也接受 0..255) |
| k_msleep(ms) | <zephyr/kernel.h> | 非忙等延时(线程挂起) |
| printk(...) | <zephyr/kernel.h> | 内核打印 |
| DT_ALIAS(name) | <zephyr/devicetree.h> | 解析 alias 节点引用 |
| DT_NODE_HAS_STATUS(n, okay) | 同上 | 编译期判断节点是否 status = "okay" |
led_set_brightness 内部流程
应用 (0..100) │ led_set_brightness_dt() ▼ led_pwm driver │ duty = brightness / max_duty × period ▼ pwm_set_pulse_dt(&pwm_spec, pulse) │ controller = stm32_pwm ▼ stm32_pwm: 配置 TIM2_CCRx │ ▼ 硬件: 比较匹配翻转 / PWM 模式 1
7. 编译与烧录
# 在工程根目录 west build -b nucleo_c562re -p auto west flash west espressif monitor # 或任何 minicom, picocom, 串口工具
启动后串口应打印:
breathing-LED (PWM) startup: PA0 <-> PA1, period 2000ms, step 20ms
板子上的 PA0 / PA1 两个 LED 即可看到呼吸效果,亮度此起彼伏。
8. 自定义指南
8.1 改变呼吸速度
修改 main.c 顶部宏:
#define BREATH_PERIOD_MS 4000 /* 改成 4 s 一周期 */ #define STEP_MS 10 /* 改成 10 ms 一步 → 颗粒更细 */
8.2 增加 LED 数量
在 overlay 添加 led_4、led_5 ... 节点(pwms = <&pwm2 N PWM_MSEC(1) 0>;)。
在 overlay aliases 添加 led3 = &led_4;。
LED_COUNT 改为实际数量,phase_offset 自动按 half / LED_COUNT 计算。
8.3 换用其他定时器
将 overlay 中的 &pwm2 改为 &pwm3、&timers2 改为 &timers3,对应引脚换为该定时器支持的复用引脚(参考芯片 datasheet alternate function 表)。
8.4 改占空比分辨率
led_set_brightness_dt() 的第二个参数是百分比 0~100。底层 pwm_set_pulse_dt() 才接受 ticks。若需更高精度,应直接使用 pwm API 而非 led API。
8.5 关闭呼吸,关断输出
led_off_dt(&leds[0]); led_off_dt(&leds[1]);
9. 常见问题 (FAQ)
| 编译报 __device_dts_ord_DT_N_ALIAS_led_P_pwms_IDX_0_PH_ORD undeclared | overlay 没有 pwm-leds 节点或 alias 未生效 | 确认 .overlay 在 boards 目录且 west build 实际应用了它 |
| LED 不亮 | timers2 未启用 / 引脚未复用 / pinctrl-0 引用错 | 检查 &timers2 { status = "okay"; } 与 pinctrl 节点 |
| 灯全亮(亮度不变) | 循环里只设了一次占空比 | 确认主循环里 phase 在递增且 k_msleep(STEP_MS) 在执行 |
| 呼吸"卡顿" | STEP_MS 过大 | 改为 10 ms 或更小 |
| 想用 RGB 灯 | pwm-leds 不支持多通道组合 | 改用 pca963x 等专用驱动,或直接使用 pwm API 自己组合 |
10. 文件清单
| src/main.c | 主程序:循环产生三角波并设置 LED 亮度 |
| boards/nucleo_c562re.overlay | 设备树 overlay:定义 pwm_leds、pinctrl、timers2 |
| prj.conf | Kconfig 片段:开启 PWM、LED_PWM、PWM_STM32 |
| CMakeLists.txt | 链接 src/main.c 到 app target |
| README.rst | 板级 / 工程说明(与本文档互补) |
| sample.yaml | Twister 测试元数据 |
11. 实现效果

我要赚赏金
