4.2 PWM呼吸灯实战


文档摘要

4.2 PWM 呼吸灯实战 上一节让定时器产生了精确节拍,这一节解锁它的第二条出口:比较输出。计数器每涨一次都与比较值 CCR 比一下,引脚电平随比较结果自动翻转——不需要 CPU 参与,方波就从引脚持续流出。把方波的占空比缓慢变化,LED 亮度随之渐变,这就是呼吸灯。它体量小,却是"数字系统输出模拟效果"的经典样本,电机调速、屏幕背光、舵机控制全是同一套原理的放大。 PWM 三要素:周期、占空比、分辨率 PWM(脉宽调制)用恒定周期的方波承载"模拟量"信息,信息藏在脉冲宽度里。三个参数定义一路 PWM: 参数 | 寄存器来源 | 含义 | 例 周期 | ARR | 一个方波循环的时长 | ARR=99,滴答 0.

4.2 PWM 呼吸灯实战

上一节让定时器产生了精确节拍,这一节解锁它的第二条出口:比较输出。计数器每涨一次都与比较值 CCR 比一下,引脚电平随比较结果自动翻转——不需要 CPU 参与,方波就从引脚持续流出。把方波的占空比缓慢变化,LED 亮度随之渐变,这就是呼吸灯。它体量小,却是"数字系统输出模拟效果"的经典样本,电机调速、屏幕背光、舵机控制全是同一套原理的放大。

PWM 三要素:周期、占空比、分辨率

PWM(脉宽调制)用恒定周期的方波承载"模拟量"信息,信息藏在脉冲宽度里。三个参数定义一路 PWM:

参数 寄存器来源 含义
周期 ARR 一个方波循环的时长 ARR=99,滴答 0.1 ms → 周期 10 ms
占空比 CCR 高电平在周期里的占比 CCR=30 → 高 3 ms,占空比 30%
分辨率 由 ARR 决定 占空比可分的档数 ARR=99 → 100 档,约 7 位

硬件比较规则(PWM 模式 1,向上计数):CNT < CCR 时输出有效电平(高),CNT ≥ CCR 输出无效电平(低)。所以 CCR 从 0 涨到 ARR+1,高电平从全无到全满,亮度自然从灭到最亮。分辨率与频率天生是一对冤家:ARR 越大分辨率越细,但周期越长(频率越低)。LED 呼吸灯对频率的底线是避开人眼可辨的闪烁——高于 100 Hz 起步,200 Hz 以上更稳;手机背光这类高档需求会冲到几万赫兹以消除相机拍摄的频闪纹。

配置 TIM3 通道输出 1 kHz PWM

目标:PA6(TIM3 通道 1 的复用引脚)输出 1 kHz、初始占空比 30% 的 PWM。先算参数:定时器时钟 72 MHz,要 1 kHz 周期且尽量高的分辨率 → PSC=71(滴答 1 MHz),ARR=999(1000 滴答 = 1 ms,分辨率 1000 档):

#include "stm32f10x.h" void pwm_tim3ch1_pa6_init(void) { /* 1. 时钟:GPIOA 与 TIM3(APB1)都要开 */ RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; /* 2. PA6 配成复用推挽输出(CNF=10, MODE=11): PWM 波形由 TIM3 硬件驱动,引脚的控制权交给外设 */ GPIOA->CRL &= ~(0xFUL << 24); GPIOA->CRL |= (0xBUL << 24); /* 0b1011 = 复用推挽 + 50 MHz */ /* 3. 时基:与 4.1 同一套数学 */ TIM3->PSC = 71; /* 72 MHz / 72 = 1 MHz 滴答 */ TIM3->ARR = 999; /* 1000 滴答 = 1 kHz 波形,分辨率 1000 档 */ /* 4. 比较通道:PWM 模式 1,使能通道输出 */ TIM3->CCMR1 &= ~TIM_CCMR1_OC1M; /* 清模式位段 */ TIM3->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; /* 110 = PWM 模式 1 */ TIM3->CCMR1 |= TIM_CCMR1_OC1PE; /* 使能 CCR1 预装载:改占空比在周期边界生效,无毛刺 */ TIM3->CCER |= TIM_CCER_CC1E; /* 通道 1 输出到引脚 */ /* 5. 占空比初值 30%:CCR=300(0 到 999 计满 1000 档)*/ TIM3->CCR1 = 300; TIM3->CR1 |= TIM_CR1_CEN; /* 启动 */ } int main(void) { pwm_tim3ch1_pa6_init(); while (1) { } /* 波形已在硬件上持续输出,CPU 可以去干别的了 */ }

五步里最值得咀嚼的是第 2 步和第 4 步。引脚配成"复用"意味着 GPIO 寄存器不再管电平,OC1 信号直通引脚——这与 3.1 节"寄存器决定电平"形成对照:GPIO 的终极使命是当好外设的出口。OC1PE 预装载位则是无毛刺的关键:没有它,运行中改 CCR 立即生效,可能恰好砍在周期中间,输出一个残缺脉冲;有了它,新值在下一个周期边界整体切换,LED 视觉上毫无波澜。

图 4-2:占空比渐变与 LED 亮度的对应

图 4-2:占空比渐变与 LED 亮度的对应

呼吸灯:渐变占空比的两种实现

硬件只管按 CCR 输出波形,"呼吸"靠软件定期改 CCR。最直接的实现是线性增减,配一个 10 ms 节拍(4.1 的 TIM3 中断正好提供):

/* 基于 4.1 节的 g_ticks_10ms 节拍,线性呼吸 */ void breathe_linear(void) { static uint16_t duty = 0; static int8_t dir = 1; /* 1 渐亮,-1 渐暗 */ if (g_ticks_10ms % 5 == 0) { /* 每 50 ms 调一档:一个呼吸周期约 10 秒 */ duty += (uint16_t)(dir * 5); if (duty >= 999) { duty = 999; dir = -1; } if (duty == 0) { dir = 1; } TIM3->CCR1 = duty; /* 改一个寄存器,波形即变 */ } }

跑起来会发现一个微妙现象:低占空比段(0 到 10%)亮得飞快,高占空比段变化却看不太出来。这不是错觉,是 LED 亮度与占空比近似线性、而人眼感知近似对数的落差。补救办法是用伽马曲线安排渐变表——多在低端排档位,高端粗着走:

/* 伽马校正渐变表:duty[i] = (i/32)^2.2 x 999,低端细分、高端粗分 */ static const uint16_t gamma_table[33] = { 0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 66, 79, 92, 107, 123, 140, 159, 178, 199, 222, 245, 271, 297, 326, 355, 387, 420, 455, 492, 530, 571, 613 }; void breathe_gamma(void) { static uint8_t idx = 0; static int8_t dir = 1; if (g_ticks_10ms % 8 == 0) { /* 每 80 ms 走一档 */ idx = (uint8_t)(idx + dir); if (idx >= 32 || idx == 0) dir = -dir; TIM3->CCR1 = gamma_table[idx]; } }

查表法是嵌入式的万能榫头:把"算不过来"的数学在 PC 上预先算好存进 Flash(2.2 节的 const 数组正派上用场),运行时只做索引。工程里凡是曲线、字体、校准数据,几乎都这么处理。

从呼吸灯到舵机与电机:同一条数学

把参数换个用法,PWM 立刻变成另外两个世界的钥匙。舵机吃的是 50 Hz(周期 20 ms)信号,脉宽 0.5 到 2.5 ms 对应角度 0 到 180 度——换算成寄存器:PSC 设成滴答 10 us,ARR=1999,CCR 在 50 到 250 之间滑动就是角度旋钮。直流电机调速更直接:占空比即平均电压,20% 占空比约等于两成全速,配 H 桥还能换向。参数全变,原理一字未改:周期定节奏,CCR 定有效值

ESP32 上的对应设施叫 LEDC(LED Controller),硬件通道同样以"频率 + 分辨率位"声明,一行 ledcWrite(channel, duty) 等价于改 CCR;写电机驱动时换 MCPWM 外设,还带死区与刹车。平台的外设名目繁多,你现在已经拥有把它们一眼看穿的透镜:找时钟、找周期寄存器、找比较寄存器。

下一章我们让芯片开口说话——串口。你会看到波特率发生器依然是一个定时器,而"打印调试"这个嵌入式最重要的习惯,从那一章起才算真正武装到位。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U