STM32 PWM驱动LED呼吸灯:定时器配置与占空比调制实战

发布时间:2026/10/14 11:17:44
STM32 PWM驱动LED呼吸灯:定时器配置与占空比调制实战
简介这份资源面向嵌入式开发初学者与STM32入门实践者围绕PWM脉宽调制驱动LED呼吸灯这一经典实验展开帮助读者理解定时器输出比较、占空比调节与GPIO复用功能的核心原理。压缩包共221个文件约6.35MB包含31个c源文件与33个h头文件如stm32f10x_tim.c、stm32f10x_rcc.c等标准外设库代码、45个o目标文件、44个crf编译中间文件以及uvprojx工程文件、hex烧录文件、map映射表与asm汇编清单覆盖从源码到可执行固件的完整工程链路。资源已有577人学习下载适合对照Keil工程结构梳理PB0引脚PWM输出配置、TIM2预分频与自动重载值设定、比较值渐变实现亮度平滑过渡等关键环节也可用于排查时钟使能、通道映射与占空比更新中的常见问题是掌握STM32定时器PWM应用的实用参考。1. 从“灯怎么呼吸”说起STM32 PWM 驱动 LED 的完整落地路径很多人第一次接触 STM32 的定时器都是从“让一盏 LED 慢慢亮、慢慢灭”开始的。这个需求听起来简单但真正动手时你会发现卡住新手的往往不是 C 语言而是三个具体问题PWM 频率设多少才不闪、占空比怎么按正弦规律变化、以及为什么我的灯一亮就常亮或者干脆不亮。这篇笔记拆的就是一份围绕“STM32 PWM 驱动 LED 呼吸灯”的实战资源它把定时器配置、PWM 输出、占空比调制这条链路完整串了起来。适合刚上手 STM32 的嵌入式开发者也适合想回头把定时器 PWM 模式彻底搞明白的熟手。下面按“这是什么 → 怎么配 → 坑在哪 → 怎么调得更好”的顺序走一遍。2. 定时器与 PWM 的底层逻辑为什么呼吸灯非得用定时器2.1 PWM 到底在做什么频率决定闪不闪占空比决定亮不亮PWM 全称脉冲宽度调制本质是一个方波。它有两个关键参数频率和占空比。频率决定这个方波一秒钟重复多少次占空比决定高电平占整个周期的比例。LED 接在 PWM 输出脚上时如果频率足够高人眼因为视觉暂留不会察觉到闪烁只会感知到“平均亮度”。平均亮度约等于占空比乘以最大亮度。所以呼吸灯的核心操作就是固定一个足够高的频率然后让占空比从 0% 慢慢升到 100%再从 100% 降回 0%循环往复。这里有个容易混淆的点频率不是越高越好。频率太高比如几十 kHz虽然不会闪但 LED 驱动电路的响应可能跟不上而且定时器预分频和自动重装载值的组合会变得很别扭。常见做法是把 PWM 频率设在 1 kHz 到 5 kHz 之间这个区间人眼完全看不到闪烁同时定时器参数也好算。2.2 STM32 定时器输出 PWM 的硬件路径STM32 的通用定时器比如 TIM2、TIM3、TIM4和高级定时器TIM1、TIM8都能输出 PWM。以通用定时器为例信号路径是这样的定时器计数器 CNT 在时钟驱动下不断累加从 0 数到自动重装载寄存器 ARR 的值然后归零重新开始。与此同时比较寄存器 CCR 里存着一个阈值。当 CNT 小于 CCR 时输出通道处于有效电平当 CNT 大于等于 CCR 时输出通道翻转为无效电平。这样CCR 的值就直接决定了占空比。占空比的计算公式是占空比 CCR / (ARR 1)。注意这里是 ARR1因为计数器从 0 数到 ARR一共经历了 ARR1 个计数周期。PWM 频率的计算公式是PWM 频率 定时器时钟 / ((预分频器 PSC 1) × (ARR 1))。这两个公式是后面所有参数配置的基础建议先记牢。2.3 呼吸灯为什么常用正弦波调制而不是线性变化如果让占空比从 0 到 100 线性增加你会发现灯的亮度变化看起来并不“自然”——前半段感觉亮得很快后半段又好像没什么变化。这是因为人眼对亮度的感知近似对数关系不是线性的。所以更自然的呼吸效果通常用正弦波或者指数曲线来调制占空比。常见做法是预先算好一个正弦表或者用查表加插值的方式让 CCR 的值按正弦规律变化。这一点在资源里通常会有对应的数组或计算公式拿到后可以直接改表长和幅值来调整呼吸速度与深度。3. 从零配置一路到灯亮定时器参数、通道映射与代码落地3.1 时钟树与定时器时钟先搞清楚你的定时器跑多快在写任何定时器代码之前必须先确认定时器挂在哪条总线上、时钟频率是多少。以常见的 STM32F103 为例TIM2、TIM3、TIM4 挂在 APB1 总线上。如果系统时钟是 72 MHzAPB1 预分频系数不为 1 时定时器时钟会是 APB1 时钟的两倍也就是 72 MHz。这个细节如果搞错算出来的 PWM 频率会差一倍灯可能闪得厉害或者呼吸节奏完全不对。确认时钟的步骤不复杂查参考手册的时钟树图找到对应定时器的时钟源再结合 SystemInit 里的时钟配置算出实际频率。我一般会在代码里先把定时器时钟频率定义成一个宏后面算 PSC 和 ARR 时直接引用避免硬编码。3.2 定时器初始化PSC、ARR、CCR 三个参数怎么定假设定时器时钟是 72 MHz目标 PWM 频率是 2 kHz希望占空比分辨率尽量高。先定 ARR。ARR 越大占空比可调的档位越多但频率会降低。取 ARR 999那么计数周期是 1000 个时钟。PWM 频率 72 MHz / ((PSC1) × 1000)。要得到 2 kHz则 (PSC1) 72 MHz / (2 kHz × 1000) 36所以 PSC 35。这样占空比分辨率是 1/1000足够做平滑呼吸。下面是定时器 PWM 初始化的核心代码以 HAL 库风格为例// 定时器时钟 72MHz目标 PWM 频率 2kHz分辨率 1000 档 #define TIM_CLK_HZ 72000000U #define PWM_FREQ_HZ 2000U #define PWM_RESOLUTION 1000U TIM_HandleTypeDef htim3; TIM_OC_InitTypeDef sConfigOC {0}; void PWM_Init(void) { // 计算预分频值PSC1 定时器时钟 / (PWM频率 × 分辨率) uint32_t psc (TIM_CLK_HZ / (PWM_FREQ_HZ * PWM_RESOLUTION)) - 1; htim3.Instance TIM3; htim3.Init.Prescaler psc; // 预分频器 htim3.Init.CounterMode TIM_COUNTERMODE_UP; // 向上计数 htim3.Init.Period PWM_RESOLUTION - 1; // ARR自动重装载值 htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_PWM_Init(htim3); sConfigOC.OCMode TIM_OCMODE_PWM1; // PWM 模式 1 sConfigOC.Pulse 0; // 初始占空比 0 sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; // 有效电平为高 sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); // 启动 PWM 输出 }这段代码里Prescaler和Period就是前面算出来的 PSC 和 ARR。Pulse对应 CCR初始设为 0灯是灭的。OCMode选 PWM1 模式意思是 CNT 小于 CCR 时输出有效电平。OCPolarity设为 HIGH有效电平就是高电平。如果你的 LED 是低电平点亮这里可以改成 LOW或者硬件上做反相。3.3 GPIO 复用与通道映射别把引脚配成普通输出PWM 输出脚必须配置成复用推挽输出而不是普通推挽输出。以 TIM3 通道 1 为例对应引脚通常是 PA6。配置时要使能 GPIOA 时钟和 TIM3 时钟然后把 PA6 设为GPIO_MODE_AF_PP复用映射到 TIM3。如果这里配错定时器内部再怎么跑引脚上也不会有波形。HAL 库里这一步通常在HAL_TIM_PWM_MspInit回调里完成void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { __HAL_RCC_TIM3_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef gpio {0}; gpio.Pin GPIO_PIN_6; gpio.Mode GPIO_MODE_AF_PP; // 复用推挽 gpio.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, gpio); } }注意Mode必须是GPIO_MODE_AF_PP不是GPIO_MODE_OUTPUT_PP。这个坑很常见现象是定时器配置看起来都对但示波器测引脚没有任何波形。3.4 占空比动态更新用正弦表驱动 CCR 实现呼吸呼吸效果的关键是让 CCR 随时间按正弦规律变化。常见做法是生成一个正弦表表里每个值映射到 0 到 ARR 之间。下面是一个简单的实现#define BREATH_TABLE_SIZE 128 // 预计算正弦表值域 0 ~ PWM_RESOLUTION-1 static uint16_t breath_table[BREATH_TABLE_SIZE]; void BreathTable_Init(void) { for (int i 0; i BREATH_TABLE_SIZE; i) { // sin 范围 -1~1映射到 0~1再缩放到分辨率 float s sinf(2.0f * 3.1415926f * i / BREATH_TABLE_SIZE); breath_table[i] (uint16_t)((s 1.0f) * 0.5f * (PWM_RESOLUTION - 1)); } } // 在定时器更新中断或主循环里调用index 递增 void Breath_Update(uint32_t index) { uint16_t ccr breath_table[index % BREATH_TABLE_SIZE]; __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, ccr); }breath_table里存的是 0 到 999 之间的值对应占空比 0% 到 100%。Breath_Update每次把新的 CCR 写进去灯就会平滑地亮灭。更新频率决定了呼吸速度如果每 10 ms 更新一次128 个点走完一轮大约 1.28 秒呼吸节奏比较舒服。改BREATH_TABLE_SIZE可以调快慢改更新周期也可以。4. 避坑与排查呼吸灯不亮、常亮、闪烁的五个真实原因4.1 灯常亮不呼吸CCR 没更新或更新在错误的通道上现象是灯一直亮着亮度不变。原因通常是__HAL_TIM_SET_COMPARE写错了通道或者根本没调用更新函数。还有一种可能是 CCR 被设成了大于 ARR 的值PWM1 模式下输出会一直保持有效电平。解决方法是先确认通道号与初始化一致再检查 CCR 是否在 0 到 ARR 之间最后用调试器观察 CCR 寄存器是否在变化。4.2 灯完全灭GPIO 没配成复用模式或定时器没启动现象是引脚测不到波形灯一点反应没有。最常见的原因是 GPIO 配成了普通输出或者HAL_TIM_PWM_Start没调用。也有可能是时钟没使能定时器根本没跑。排查顺序是先量引脚波形没有波形就查 GPIO 模式有波形但灯不亮查硬件接线和极性。4.3 灯闪烁明显PWM 频率太低或更新周期太长现象是能看到灯在闪而不是平滑呼吸。原因是 PWM 频率低于 100 Hz 左右人眼就能察觉。或者呼吸表的更新周期太长比如每 100 ms 才更新一次亮度变化就会一跳一跳。解决办法是把 PWM 频率提到 1 kHz 以上同时把更新周期控制在 10 ms 到 20 ms 之间。4.4 呼吸节奏不对正弦表映射范围算错现象是灯亮灭不对称或者最亮时不够亮、最暗时不够暗。原因是正弦表映射时没有正确缩放到 0 到 ARR 之间。比如用了(s1)*0.5*ARR但忘了减 1或者用了整数除法导致精度丢失。建议在初始化后把表里最大值和最小值打印出来确认覆盖了 0 和 ARR。4.5 换了个定时器就不行通道映射和时钟总线不同现象是在 TIM3 上跑得好好的换到 TIM1 或 TIM4 就没输出。原因是不同定时器的通道对应引脚不同挂载的总线也可能不同时钟频率不一样。解决方法是查数据手册的复用功能映射表重新确认引脚和时钟并重新计算 PSC 和 ARR。5. 进阶调优用 DMA 和中断把 CPU 解放出来5.1 用定时器更新中断驱动呼吸表前面是在主循环里调用Breath_Update如果主循环还有其他任务呼吸节奏可能会被拖慢。更稳的做法是用定时器更新中断在中断里递增索引并更新 CCR。这样呼吸节奏由硬件定时器保证不受主循环影响。配置时把更新中断使能在HAL_TIM_PeriodElapsedCallback里调用更新函数即可。注意中断频率不要太高否则 CPU 开销会上去。一般 100 Hz 到 200 Hz 的更新率就足够平滑。5.2 用 DMA 把正弦表直接搬到 CCR 寄存器如果不想让 CPU 参与每次 CCR 更新可以用 DMA。思路是把正弦表放在内存里配置 DMA 通道从内存搬到定时器的 CCR 寄存器由定时器更新事件触发 DMA 传输。这样 CPU 只需要初始化一次之后呼吸完全由硬件自动完成。配置时注意 DMA 的源地址递增、目标地址固定传输长度等于表长并且要开启循环模式。这个方案适合对 CPU 占用敏感的场景比如同时要跑其他实时任务。5.3 呼吸曲线调优正弦、三角、指数的选择正弦曲线最接近自然呼吸但如果你想要更“急促”的效果可以用三角波或者指数曲线。三角波实现最简单占空比线性升降但视觉上不如正弦自然。指数曲线则更接近人眼感知亮起快、暗下慢。实际调优时我一般会先把表长和更新周期定下来再微调曲线形状。一个实用技巧是把表里的值先归一化到 0 到 1再乘以 ARR这样换分辨率时不用重算表。5.4 验证方法示波器看波形眼睛看效果调完之后怎么确认参数对了最直接的是用示波器测引脚波形看频率是不是目标值占空比是不是在 0% 到 100% 之间来回变化。没有示波器的话可以用定时器的输入捕获功能测频率或者用调试器实时观察 CCR 寄存器的值。眼睛看效果时注意在暗环境下观察亮灭过渡是否平滑、有没有跳变。如果发现某个亮度点跳变明显多半是表长不够或者更新周期不均匀。从那以后我每次调 PWM 呼吸灯都会先把定时器时钟频率和 ARR、PSC 的算式写在纸上核对一遍再上电看波形最后才调呼吸曲线。这个顺序能省掉很多来回改参数的功夫。希望帮到你。本文还有配套的精品资源点击获取