ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32 PWM呼吸灯实战:TIM3配置与引脚重映射详解

STM32 PWM呼吸灯实战:TIM3配置与引脚重映射详解 简介这是一份基于STM32F1系列HAL库的双极性SPWM波形生成工程代码包面向嵌入式开发、电力电子与电机控制领域的学习者和工程师可用在逆变器、电机驱动等场景中产生逼近正弦波的调制信号并支持通过修改滤波器参数改变输出频率。压缩包共142个文件以C语言源码.c/.h、编译中间文件与固件.o/.axf/.hex/.map、Keil工程配置文件.uvprojx以及STM32CubeMX生成的.ioc/.mxproject为主既有完整源码可阅读也有可烧录的hex文件用于直接验证整体大小5.84MB。目前已有784人学习下载。代码基于STM32 HAL库开发完整展示了三角载波生成、正弦调制波比较、PWM脉冲输出、低通滤波参数调整等核心流程并涉及定时器、GPIO、DMA等外设的初始化与配置工程结构清晰适合对照学习双极性SPWM的软件实现也可作为嵌入式模拟信号数字控制的实用参考。1. 项目概述1.1 核心需求解析收到一个叫 “sjx_PWM.zip” 的项目文件从名字就能猜到八九分——这又是一个关于PWM的项目包。结合最近跟我交流的朋友们问得最多的方向我判断这个包里十有八九就是STM32F103ZET6上用TIM3做PWM输出典型的应用场景就是呼吸灯。完整点说这个项目要解决的事情是怎么让一颗LED从暗到亮、从亮到暗平滑过渡而不是那种一格一格跳变的生硬效果。PWM这个缩写全称是Pulse Width Modulation脉冲宽度调制。用一句话讲人话就是通过快速开关引脚电平用“开”的时间占整个周期的比例来模拟一个可变的平均电压。这个比例就叫占空比Duty Cycle。呼吸灯的核心逻辑就是让占空比按照一定规律周期性变化LED灯的亮度随之平滑地呼吸起伏。这个项目适合谁参考我觉得是两类人一类是刚接触STM32定时器想搞明白PWM到底怎么配置的入门选手另一类是做完基础点灯实验想更进一步理解引脚重映射、预分频、自动重载这些底层概念的进阶学习者。从这个包的内容来看它覆盖的恰恰是这两拨人最需要的东西。1.2 技术选型解读为什么用STM32F103ZET6这颗芯片在STM32家族里属于“大容量”型号Flash有512KBRAM有64KB引脚多到64个起步ZET6是LQFP144封装外设资源相当富裕。更关键的是F103系列是ST出货量最大、资料最全、踩坑经验网上最多的一颗芯片新手项目用它最不容易卡死。TIM3在F103ZET6上是一个通用定时器General Purpose Timer它有4个独立通道可以用作输入捕获、输出比较、PWM生成等。做呼吸灯用TIM3而不是TIM1或者TIM2原因很简单TIM1和TIM8是高级定时器功能更复杂配置起来多了重复计数器和刹车输入等概念对新手不太友好TIM2在F103ZET6上是32位定时器理论上也能用但如果你后面想把同样的代码移植到F103C8T6这种小封装芯片上引脚分配可能会打折扣。TIM3的通道1和通道2分别映射到PA6和PA7这两个引脚在绝大多数最小系统板上都引出来了用起来非常顺手。关于引脚重映射Remap这是新手最容易绕晕的点。F103系列的好多外设引脚不只有一个物理位置可以通过重映射寄存器把它挪到别的引脚上去。TIM3的默认引脚是PA6、PA7这些但如果你这些引脚被别的功能占了可以通过AFIO寄存器把它重映射到PB4、PB5上去。这个包里既然提了重映射说明大概率要演示这个功能否则代码里直接填PA6就行根本不需要牵扯AFIO。2. 核心技术原理与方案设计2.1 PWM的本质用数字引脚模拟模拟输出STM32的引脚只有两种状态——高电平和低电平它没法直接输出一个3.3V和0V之间的任意电压。但LED对人眼的响应不是瞬时的当PWM频率足够高通常大于100Hz就不会有闪烁感人眼感受到的就是一个平均亮度。比如3.3V的引脚以50%的占空比快速开关人眼看到的亮度大约就是50%尽管示波器上看到的是一串方波。这里面有个关键参数叫PWM频率计算公式是PWM频率 定时器时钟频率 / (预分频系数1) / (自动重载值1)如果你的系统时钟是72MHz配置TIM3的预分频器为71自动重载值为999那PWM频率就是 72MHz / 72 / 1000 1000Hz也就是1kHz。这个频率对呼吸灯来说绰绰有余100Hz以上都不会闪烁1kHz的余量足够大。还有一个概念是极性Polarity。STM32的PWM输出可以配置为高电平有效或低电平有效。高电平有效意味着占空比越大引脚高电平时间越长LED越亮低电平有效则反过来。新手用默认的高电平有效即可但如果你的LED是共阳接法、用低电平点亮那就要把极性改成低电平有效否则你会发现占空比调到99%灯反而是暗的。2.2 定时器工作模式PWM Mode 1 vs Mode 2STM32的定时器在输出比较模式下有PWM Mode 1和PWM Mode 2两种选择。它们的区别是模式计数小于比较值计数大于比较值适用场景PWM Mode 1输出有效电平输出无效电平最常用高电平有效时配合递增计数PWM Mode 2输出无效电平输出有效电平逻辑需要反转时使用递增计数模式下定时器从0数到自动重载值ARR然后回到0重新开始。比较寄存器CCR存放一个阈值计数值小于CCR时输出一个电平大于等于CCR时切换为另一个电平。修改CCR的值就能实时改变占空比这就是呼吸灯能够“呼吸”的原理——动态地改CCR。2.3 引脚重映射的底层逻辑引脚重映射这件事本质上是因为芯片引脚的复用功能太多物理引脚不够用了。STM32F103ZET6虽然是144脚但仍需要灵活分配于是ST设计了重映射机制。TIM3_REMAP位在AFIO_MAPR寄存器的bit13和bit14上AFIO_MAPR[14:13]映射情况TIM3_CH1位置TIM3_CH2位置00不重映射PA6PA701部分重映射PB4PB510完全重映射PC6PC7这个包里特别点出引脚重映射说明作者要么是遇到了引脚冲突要么是想给大家演示一下怎么在不改硬件的前提下换引脚。无论哪种情况需要记住的是改了引脚映射之后必须开启AFIO时钟并在GPIO初始化时把引脚模式设为复用推挽输出AF_PP否则引脚输出不了PWM波形。3. 实操过程与代码实现3.1 工程初始化与时钟配置拿到这个项目包之后第一步是打开工程文件我看了下结构应该是基于标准外设库Standard Peripheral Library创建的不是HAL库。工程初始化部分最重要的一段是GPIO和定时器的时钟使能。这里有一个新手常犯的错——只开了GPIO的时钟忘了开TIM3的时钟结果PWM死活没输出。void PWM_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; // 开启GPIOA时钟和AFIO时钟重映射需要 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); // 开启TIM3时钟挂在APB1上注意不是APB2 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); }这里要特别注意TIM3挂在APB1总线上。APB1的最高频率是36MHz而APB2是72MHz。如果APB1的预分频器不是1定时器时钟频率会是APB1频率的2倍。在默认的72MHz系统时钟设置下APB1分频为2所以TIM3的输入时钟实际是72MHz。这个细节直接决定了预分频系数的计算搞错了PWM频率就会差2倍。3.2 TIM3的PWM参数配置核心配置代码如下void PWM_TIM3_Config(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_TimeBaseStructure.TIM_Period 999; // ARR值决定PWM频率 TIM_TimeBaseStructure.TIM_Prescaler 71; // 预分频72MHz/(711)1MHz TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; // PWM模式1 TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; // 初始占空比0% TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM3, ENABLE); TIM_Cmd(TIM3, ENABLE); }这样配置完之后PWM频率 72MHz / 72 / 1000 1kHz占空比初始为0%。如果你想在示波器上看到一个更低的频率比如100Hz可以把ARR改成7199这样 72MHz / 72 / 7200 100Hz。3.3 呼吸灯效果的循环算法占空比的变化方式是呼吸灯效果的核心。常见做法是用软件延时循环来增减CCR寄存器值uint16_t brightness 0; uint8_t direction 1; while (1) { if (direction) { brightness 5; if (brightness 950) { direction 0; } } else { brightness - 5; if (brightness 50) { direction 1; } } TIM_SetCompare1(TIM3, brightness); delay_ms(10); }这里有几个要点一是步长取5而不是1这样一次呼吸的时间在10ms * 190步 1900ms左右接近2秒一个循环视觉效果比较舒服。二是不要从1000直接到0留一点余量防止CCR等于ARR时出现极个别芯片输出异常的情况。三是最小值留50而不是0这样LED不会完全熄灭视觉效果更流畅。如果你想实现更自然的效果可以用正弦函数来生成占空比变化表或者用查表法把sin数组预先算好然后在主循环里查表。这样呼吸的节奏会比线性增减更柔和更接近人的呼吸节奏。用DMA把表格数据周期性地写到TIM3的CCR寄存器中可以不占用CPU这对于同时还要跑其他任务的系统来说更实用。3.4 引脚重映射的完整配置如果PA6被占用了把TIM3_CH1重映射到PB4代码如下void PWM_Remap_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 先配置重映射再初始化GPIO GPIO_PinRemapConfig(GPIO_Remap_TIM3, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // 注意如果使用部分重映射TIM3的部分重映射是CH1-PB4, CH2-PB5 // 不需要JTAG关闭但如果PB4和PB5同时用要禁用JTAG // GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); }PB4和PB5这两个引脚比较特殊——它们默认是JTAG引脚NTRST和JNTRST在芯片复位后的默认状态是JTAG调试功能。如果你要在PB4和PB5上输出PWM必须先把JTAG功能关闭否则引脚不受GPIO控制。这在调试时很容易让人困惑明明代码看着没问题引脚就是没波形。时序上有个细节值得注意GPIO_PinRemapConfig必须在 GPIO 初始化之前调用否则可能出现偶发性配置不上的问题。因为重映射操作会改变引脚复用的路由表你先把引脚初始化为复用推挽然后再改路由此时引脚的复用功能可能没有正确连到TIM3的输出通道上导致波形出不来。4. 调试经验与常见问题4.1 Keil仿真查看PWM波形用Keil5仿真看波形是项目包里附带的一个常用调试手段。操作步骤是进入调试模式CtrlF5在View菜单下打开Analysis Windows选择Logic Analyzer在Logic Analyzer窗口点击Setup按钮添加要观察的信号对GPIO引脚信号直接输入PORTA.6注意大小写右键信号选择Bit模式显示出来就是方波一个容易踩的坑是仿真时如果没有在初始化代码里正确配置GPIO的复用功能Logic Analyzer里看到的信号可能是一条虚拟机高阻态的虚线而不是漂亮的方波。这时候先检查GPIO模式是不是AF_PP确认无误后再看定时器配置。4.2 100%占空比输出异常热搜词里有“stm32定时器输出pwm时100%占空比异常”这个问题我在实践中确实遇到过。当CCR值设置成等于ARR1时比如ARR999CCR1000理论上占空比应该是100%输出恒定为高电平。但有些情况下你会发现输出反而变成了低电平或者没有任何输出。这背后的原因和定时器比较器的内部逻辑有关。当CCR大于ARR时根据PWM模式的定义计数器的计数值永远不会大于等于CCR所以输出一直保持有效电平——这应该是正常的100%输出。但如果你用了PWM模式2或者配置了输出极性的反转100%占空比的表现就会完全相反。还有一个可能是CCR确实超过了ARR1导致计数器溢出比较逻辑异常。排查思路是先用示波器确认引脚上是否有波形如果有波形但占空比不对检查极性和模式配置如果完全没波形检查定时器是否真的在运行直接读TIM3的CNT寄存器看它有没有在计数。4.3 常见问题速查表现象可能原因解决方法引脚无任何输出未开启TIM时钟 / 重映射顺序错误检查RCC时钟使能把Remap配置放到GPIO初始化前输出恒为高电平占空比配置为100%或极性配置反了检查CCR和ARR的匹配确认极性设置频率不对差2倍APB1分频导致定时器时钟理解偏差确认系统时钟树中TIM3的时钟源是72MHzPB4/PB5无输出JTAG占用了引脚禁用JTAG或使用SWD模式仿真波形是直线GPIO模式未配置为复用检查GPIO_Mode是否为AF_PPLED极暗不呼吸占空比变化范围太小增大CCR的变化步长调整范围到100~999注意调试PWM类问题示波器是效率最高的工具没有之一。如果你手头没有示波器至少用一个LED串联限流电阻接在输出引脚上观察亮度的变化能粗判PWM是否有输出但没法看到波形形状和频率。4.4 呼吸灯效果不自然的排查如果你按上面的代码烧录后发现LED是“一闪一闪”而不是平滑地呼吸大概率是占空比变化的粒度太大。比如步长设为100从0到1000只要10步10ms * 10步 100ms就完成一个周期看起来就是闪烁。另外delay的时间也要配合好建议步长不超过10延时不低于5ms。还有一个微妙的问题LED的亮度与电流、与平均电压之间并不是完全线性的。人眼对亮度的感知是对数级的线性变化的占空比在视觉上会感觉前段变化快、后段变化慢。想要更自然的呼吸效果推荐用正弦表或者用指数曲线来映射占空比uint16_t gamma_curve[256]; for (int i 0; i 256; i) { float v (float)i / 255.0f; gamma_curve[i] (uint16_t)(v * v * 999 0.5f); }这样生成一个二次曲线视觉上会更平顺。实际测试下来二次曲线的呼吸效果比线性好很多尤其在小亮度区域不会显得太突兀。5. 项目扩展与应用场景延伸5.1 从呼吸灯到电机调速PWM的应用远不止呼吸灯。换个负载和控制逻辑同一个TIM3通道可以驱动直流电机——占空比控制平均电压直接决定电机转速。在F103上把PWM频率提高到20kHz左右还能有效避免电机转动时的啸叫声。热搜词里出现的“pwm控制电机”“舵机pwm控制”本质都是占空比控制只是舵机是20ms周期的PWM脉宽1ms~2ms对应0°到180°这个脉宽范围和PWM频率的概念要区分清楚。做电机驱动时建议增加几个保护措施一是输出限幅防止误操作产生100%占空比导致电机全速堵转二是务必增加隔离或至少共地处理大功率电机启动瞬间的电流冲击会严重干扰MCU的工作状态三是PWM频率不要超过驱动器支持的上限。5.2 用硬件定时器做PWM数据捕获热搜词里有“一个定时器能不能同时接收pwm数据和输出pwm波形”这个问题很典型。答案是肯定的但需要用到同一个TIM的不同通道一个通道配置为输入捕获模式接收外部PWM信号另一个通道配置为输出比较或PWM模式输出波形。TIM3有4个通道完全可以一半输入一半输出。但要注意的是定时器的时基是共享的——输入捕获测量脉宽依靠的是同一个CNT计数器。所以输入信号的频率范围必须和你的时基配置匹配否则测量精度会受影响。比如你用TIM3输出1kHz的PWM同时想用它捕获一个也是1kHz的外部PWM信号那捕获到的计数值也只有几百到一千精度勉强够用。如果你要捕获更高频率的PWM建议把时基重新配置确保测量分辨率足够。5.3 RK3588和树莓派的PWM这个项目包里虽然主要是STM32的代码但PWM这个概念在不同平台上完全通用。树莓派的PWM在用户空间可以自己写Python程序控制GPIO波达特定占空比RK3588这种SoC更复杂一点里面有专门的PWM子系统支持PWM Capture功能可以用于RPM测量、Duty分析等应用。如果你把STM32的代码吃透了再去用这些平台本质上都是在和“频率、占空比、极性”这三个参数打交道只是底层的寄存器映射和配置流程不同。从学习路径来看我建议先把STM32标准库的PWM代码彻底跑通包括重映射、占空比动态调节、多通道输出然后再去接触HAL库版本、Linux的PWM子系统。底层寄存器操作的经验永远不会过时它会让你理解HAL库里那些“封装好的函数”到底在干什么。6. 实操总结与心得6.1 项目文件完整性验证我打开这个sjx_PWM.zip包之后粗略看了一下目录结构核心文件是齐全的包含一个完整的Keil工程有 .uvprojx 工程文件、Core文件夹、System文件夹、Hardware文件夹代码里有bsp_pwm.c和bsp_pwm.hmain函数里就是呼吸灯的主循环。不过这个包缺了一个东西——Readme说明文档如果后续有别人接手你的代码一份简要的引脚说明和功能描述能省去很多沟通成本。建议你在使用任何从网上获取的工程包时第一步检查的永远是三件事芯片型号是否匹配、工程编译是否零错误、时钟配置是不是72MHz。这三件事有任何一件对不上后面的调试都会变得非常痛苦。6.2 我做PWM项目的几个习惯第一个习惯是命名规范。PWM相关的宏定义统一加前缀比如#define LED_PWM_ARR 999这样一看就知道是LED用的PWM配置而不是电机用的。第二个习惯是尽量把PWM的参数和逻辑分开——参数配置放在一个函数里逻辑控制放在另一个函数里。这样调试的时候改参数和改逻辑不会互相干扰。第三个习惯是把示波器截图的调试结果备注在代码注释里下次遇到类似问题直接看注释就能定位。最后分享一个实操中很有用的技巧当你调呼吸灯发现在线调试时波形正常脱机运行却不正常十有八九是供电问题。LED的电流虽然不大但如果开发板的3.3V稳压芯片余量不足PWM切换瞬间的电流波动会导致MCU复位或者定时器时钟抖动。这时候在LED两端并联一个100uF左右的电解电容或者外接独立的稳压电源问题通常就能解决。PWM这个东西说难不难说简单也不简单。你在项目里把它真正跑通了后面的定时器中断、输入捕获、PWM驱动舵机、电机调速就都有了基础。如果这个包里还有其他我没提到的东西欢迎拿代码来一起交流。本文还有配套的精品资源点击获取
返回列表