
简介这份资源面向STM32初学者与嵌入式开发者围绕PWM脉冲宽度调制与按键输入控制LED亮度这一典型应用展开。包内共153个文件以32个c源文件、34个h头文件为核心涵盖定时器、RCC、GPIO等外设驱动代码另有32个s汇编启动文件、14个o目标文件、13个crf编译中间文件以及uvprojx工程文件、hex烧录文件、map映射文件等压缩包约2.18MB构成一套可直接编译调试的完整工程。资源重点演示定时器PWM模式配置、预分频与自动重载值设定、通道比较值调节占空比以及按键中断响应并动态更新亮度等关键环节读者可据此理解占空比与LED平均电流的关系掌握从时钟使能到中断处理的完整流程。目前已有13839人学习下载适合作为智能照明、自动化设备等场景的入门实践参考。1. PWM调光不是玄学从按键到LED亮度的完整控制链很多人第一次在STM32上做PWM调光代码烧进去发现LED要么全亮要么全灭按键按下去亮度纹丝不动于是开始怀疑人生。其实问题不在PWM本身而在于你没有把「按键扫描→占空比更新→定时器重装载」这条链路串起来。这个标题讲的就是用STM32的定时器输出PWM波通过按键动态修改比较值从而改变LED的平均导通时间实现亮度调节。它适合已经能点亮LED、能跑通GPIO输入输出的嵌入式初学者也适合想从「点灯」进阶到「调光」的工程师。核心就三件事定时器配置对、按键消抖稳、占空比算得准。2. 定时器选型与PWM模式为什么TIM2比TIM1更适合新手调光2.1 通用定时器和高级定时器的实际差异STM32的定时器分好几类高级定时器如TIM1、TIM8通用定时器如TIM2到TIM5基本定时器如TIM6、TIM7。做LED调光通用定时器完全够用而且配置比高级定时器少一层刹车和死区设置的坑。高级定时器要输出PWM必须额外调用TIM_CtrlPWMOutputs(TIM1, ENABLE)打开主输出否则通道配置全对也没有波形出来。我一般建议新手从TIM2开始它的通道1到通道4对应PA0到PA3接线直观查手册也方便。选定时器还要看总线频率。以常见的STM32F103为例TIM2挂在APB1总线上如果系统时钟72MHzAPB1预分频系数为2则TIM2的时钟为72MHz。这个频率决定了你后面算预分频和自动重装载值的基准。不同型号的时钟树有差异动手前先确认自己芯片的参考手册里对应定时器的时钟来源。2.2 PWM模式1和模式2的区别与选择PWM模式1和模式2的区别就一句话模式1下计数器小于比较值时输出有效电平模式2下计数器小于比较值时输出无效电平。听起来绕实际用的时候记住如果你想让比较值越大LED越亮就用PWM模式1且有效电平设为高。这样占空比 CCR / (ARR1)CCR越大高电平时间越长LED越亮。配置代码如下以标准外设库为例// TIM2 PWM初始化通道1对应PA0 void PWM_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // PA0复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 定时器基础配置 TIM_TimeBaseStructure.TIM_Period arr; // 自动重装载值 TIM_TimeBaseStructure.TIM_Prescaler psc; // 预分频系数 TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); // PWM模式1配置 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; // 初始占空比为0 TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM2, ENABLE); TIM_Cmd(TIM2, ENABLE); }这段代码里arr和psc决定了PWM频率。公式是PWM频率 定时器时钟 / ((arr1) × (psc1))。假设定时器时钟72MHz想要1kHz的PWM频率可以设psc71arr999。这样每个PWM周期是1ms人眼完全感觉不到闪烁。TIM_Pulse就是CCR值初始设为0LED不亮。后面按键每按一次就修改这个值。TIM_OCPreload_Enable和TIM_ARRPreload_Enable这两个预装载使能很重要。它们保证你在修改CCR或ARR时新值在下一个更新事件才生效避免当前周期出现毛刺。如果不使能调光时偶尔会看到LED闪一下这就是血泪经验。2.3 频率和分辨率的权衡PWM频率不能太低低于200Hz人眼可能察觉到闪烁尤其是用手机摄像头拍的时候。但频率也不能太高因为ARR值会变小分辨率下降。比如72MHz时钟要输出100kHz的PWMARR最大只能到719占空比只能分720级调光细腻度就差了。一般LED调光用1kHz到5kHz比较合适ARR设成999或4999分辨率足够也不会闪。3. 按键扫描与占空比更新把抖动和连按都管住3.1 按键硬件连接与上拉电阻的选择按键一端接GPIO另一端接地GPIO配置为上拉输入。这样按键未按下时读到高电平按下读到低电平。STM32内部有可配置的上拉电阻一般30k到50k欧姆对于普通轻触按键够用了。如果板子上按键离MCU较远或者环境干扰大建议外部再加一个10k欧姆上拉内部上拉作为备份。按键对应的GPIO初始化// 按键初始化PA1为上拉输入 void KEY_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // 上拉输入 GPIO_Init(GPIOA, GPIO_InitStructure); }GPIO_Mode_IPU就是上拉输入模式。如果按键接的是VCC而不是GND那就用下拉输入GPIO_Mode_IPD逻辑反过来。接线方式决定配置别搞混。3.2 软件消抖的两种写法与选择按键抖动一般持续5ms到20ms不消抖的话按一次可能被识别成好几次。最简单的消抖是延时法检测到低电平后延时10ms再读一次还是低电平就确认按下。缺点是阻塞CPU但在这个小项目里无所谓。// 带消抖的按键扫描返回键值 u8 KEY_Scan(u8 mode) { static u8 key_up 1; // 按键松开标志 if (mode) key_up 1; // 连按模式 if (key_up (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) 0)) { delay_ms(10); // 消抖延时 if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) 0) { key_up 0; return 1; // 按键按下 } } else if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) 1) { key_up 1; } return 0; }mode参数控制是否支持连按。传0时按住不放只算一次传1时按住会连续触发。调光场景一般用不支持连按的模式每按一次亮度加一档按到最大再回到最小循环调节。3.3 占空比步进策略线性调光和视觉调光的差别占空比从0到100%线性增加LED亮度并不是线性变化的。人眼对亮度的感知近似对数关系低占空比区间变化明显高占空比区间变化迟钝。如果按固定步进比如每次加10%你会发现低亮度时跳变很大高亮度时几乎看不出变化。常见做法是分两段0到50%区间步进小一点比如5%50%到100%区间步进大一点比如10%。或者直接用查表法预先算好一组人眼感觉均匀的CCR值按键时切换索引。我一般用查表法省事且效果稳定。// 人眼视觉均匀的CCR值表共11档 const u16 ccr_table[11] {0, 50, 120, 220, 350, 500, 680, 880, 1100, 1350, 1600}; u8 level 0; // 当前亮度档位 // 按键处理函数 void KEY_Process(void) { if (KEY_Scan(0) 1) { level; if (level 10) level 0; // 循环 TIM_SetCompare1(TIM2, ccr_table[level]); } }这个表假设ARR是1600占空比从0%到100%分11档。数值不是等差的低档位间隔小高档位间隔大补偿人眼的非线性。你可以根据自己的LED和限流电阻实测调整没有标准答案。4. 避坑与排查调光不生效的五个典型原因4.1 现象LED常亮按键无反应原因通常是GPIO模式配错。PA0如果配成了普通推挽输出而不是复用推挽定时器波形根本出不来LED可能因为其他代码被拉高而常亮。解决方法是检查GPIO_Mode_AF_PP是否写对以及对应引脚是否真的支持该定时器通道。查数据手册的引脚复用表别凭记忆。4.2 现象LED微亮但调不了或者亮度跳变先看CCR值有没有超过ARR。如果CCR大于ARR输出会一直有效或一直无效取决于模式。用TIM_SetCompare1设值前确保CCR不超过ARR。另外检查预装载使能是否打开没打开的话修改CCR会立即生效可能在周期中间产生窄脉冲看起来就是跳变。4.3 现象按键按一次亮度变好几档这是消抖没做好或者按键扫描周期太短。如果主循环里没有其他延时按键扫描可能每微秒执行一次抖动期间被多次判定为按下。解决办法是确保消抖延时足够或者在主循环里加一个1ms到5ms的节拍按键扫描按节拍执行。4.4 现象调光时LED闪烁或听到啸叫PWM频率设太低了。如果频率低于200Hz人眼能察觉闪烁如果低于20kHz但高于听觉范围下限可能听到电感或陶瓷电容的啸叫。把PWM频率提到1kHz以上一般就没了。但注意频率提高后ARR变小分辨率下降需要重新平衡。4.5 现象换一块板子就不工作了不同STM32型号的定时器时钟源不同。比如F0系列和F1系列的时钟树差异很大F4系列APB1和APB2的定时器时钟倍频规则也不一样。移植代码时先确认目标芯片的定时器时钟频率重新算psc和arr。不要直接复制粘贴否则频率可能差好几倍。5. 进阶技巧用DMA和查表做呼吸灯与多通道同步5.1 用DMA自动更新CCR实现呼吸灯呼吸灯的本质是CCR值按正弦或三角波规律连续变化。如果靠主循环改CCR占空比更新时刻和PWM周期不同步会有细微抖动。更好的做法是用DMA把预先算好的CCR数组搬运到TIM2-CCR1寄存器每个PWM周期自动更新一次完全不占CPU。配置思路是开启TIM2的更新DMA请求DMA源地址指向CCR数组目标地址指向TIM2-CCR1传输长度设为数组长度循环模式。这样定时器每个更新事件触发一次DMA传输CCR值自动切换呼吸效果丝滑。// DMA配置片段以标准库为例 DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel5); // TIM2_UP对应DMA1通道5 DMA_InitStructure.DMA_PeripheralBaseAddr (u32)TIM2-CCR1; DMA_InitStructure.DMA_MemoryBaseAddr (u32)ccr_table; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize 11; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_Init(DMA1_Channel5, DMA_InitStructure); TIM_DMACmd(TIM2, TIM_DMA_Update, ENABLE); DMA_Cmd(DMA1_Channel5, ENABLE);这段代码里DMA_Mode_Circular让DMA搬完一轮后自动回到开头形成循环。DMA_MemoryInc_Enable让源地址每次递增依次读取数组元素。目标地址不变始终写CCR1。这样CCR值按数组顺序循环LED就呈现呼吸效果。数组内容可以按正弦规律生成呼吸更自然。5.2 多通道同步调光的注意事项如果要同时控制RGB三色LED用TIM2的通道1、2、3分别输出PWM。三个通道共用同一个ARR所以PWM频率一致但CCR各自独立。修改亮度时分别调三个CCR即可。注意三个通道的预装载使能都要打开否则修改时可能不同步颜色会闪。另外如果三个LED的限流电阻不同同样的CCR值亮度不一样。需要分别做校准表或者用软件做gamma校正。我一般先让三个通道输出相同占空比用手机摄像头看颜色是否偏再微调CCR偏移量。5.3 一个验证调光线性度的小方法拿一个光敏电阻或手机亮度传感器固定距离对着LED记录不同CCR值下的传感器读数。如果读数随CCR线性变化说明调光线性度好如果低CCR区间读数跳变大说明需要调整查表值。这个方法不需要专业设备手机的光传感器App就能凑合用。我试过几次比肉眼判断靠谱得多。调光这件事说到底就是不断逼近人眼感觉舒服的曲线。代码写对只是第一步参数调到位才是功夫。我自己的习惯是先在纸上画一条目标亮度曲线再反推CCR表烧进去看效果不行就改表。反复几次总能找到合适的。希望帮到你。本文还有配套的精品资源点击获取