ARTICLE DETAIL

资讯详情

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

STM32F103 PWM输出实验详解:定时器原理、占空比计算与标准库代码

STM32F103 PWM输出实验详解:定时器原理、占空比计算与标准库代码 简介这是一份面向STM32入门开发者的PWM输出实验例程聚焦定时器与PWM脉宽调制技术涵盖TIM定时器选型、预分频器与自动重载值设置、通道极性及PWM模式配置等关键知识可应用于电机控制、LED调光、信号发生等实际场景。压缩包共131个文件、约2.31MB以C/H源码和Keil工程文件为主同时包含uvprojx工程、hex/axf编译输出、说明文档及常见辅助脚本目录结构清晰便于直接导入工程对照学习。通过学习工程代码读者既能理解定时器PWM输出从初始化到输出的完整流程也能结合示波器观察频率与占空比变化掌握利用中断和DMA处理PWM事件的方法配套示例电路图还可帮助了解STM32如何连接电机、LED等外部负载为后续复杂嵌入式开发打好基础。目前已有799人学习/下载适合一边阅读代码一边动手实践的STM32初学者使用。 最近在折腾STM32基本例程卡在PWM输出实验上有一阵子。网上关于STM32的PWM教程一搜一大把但要么直接贴CubeMX截图要么甩一段标准库代码就没了下文遇到问题根本无从下手。这篇博文我打算用STM32F103C8T6这块最小系统板把PWM输出实验从头到尾讲透包括定时器选型、硬件PWM原理、频率和占空比计算、标准库代码写法以及示波器和LED实测过程中踩过的坑。适合刚接触STM32、想通过例程理解定时器和脉宽调制的同学也适合已经会点灯但一直没搞懂PWM参数关系的朋友。1. 实验思路与方案设计1.1 PWM到底是什么PWMPulse Width Modulation脉冲宽度调制核心思想是把一个方波的高电平和低电平持续时间按照比例调整。想象一下你在一分钟内不停按开关灯亮30秒、灭30秒眼睛感知到的就是50%亮度。如果重复频率足够高比如1kHz以上眼睛完全感觉不到闪烁只看到明暗变化。这个“高电平时间占整个周期的百分比”就是占空比0%全灭100%全亮。PWM的本质就是通过调整占空比来控制输出的平均功率用在LED调光、电机调速、舵机角度控制、电源稳压这些场景里非常顺手。1.2 为什么用F103C8T6和定时器硬件PWM硬件PWM的好处在于定时器硬件会自动翻转引脚CPU只需要设置一个比较值完全不参与输出过程。这也正是PWM实验相对GPIO点灯的进阶之处是后续驱动舵机、直流电机、无源蜂鸣器的基础。选STM32F103C8T6是因为它便宜、教程多、最小系统板容易入手它的TIM2、TIM3、TIM4都是普通定时器每个定时器有4个通道三个定时器加起来可以同时输出12路PWM对入门实验来说绰绰有余。我不建议一上来就用TIM1因为TIM1属于高级定时器除了基本配置外还要处理刹车、死区、互补输出等问题新手很容易卡在“明明初始化了却没有波形”的坑里后面我会专门说这个问题。1.3 整个实验的目标拆解我把这个PWM实验拆成三个递进的小目标。第一是固定占空比输出观察LED亮度变化第二是通过循环修改占空比值做出呼吸灯效果第三是用示波器量波形验证频率和占空比计算是否和理论一致。没示波器也不用慌可以先用LED亮度做粗判或者用逻辑分析仪看波形。这三个目标的逻辑就是从“能输出”到“能控制”再到“能验证”每一步都建立在前一步之上出问题的时候也方便定位到底卡在哪一层。2. 硬件准备与开发环境搭建2.1 硬件清单主控板我选用的是STM32F103C8T6最小系统板板载8MHz晶振ST-Link烧录。板载LED接在PC13上但PC13并不是所有定时器通道都能直接映射所以实验里我额外用杜邦线在PA0引脚外接一个LED串一个1kΩ限流电阻用TIM2_CH1输出PWM。如果手上没有面包板和LED也可以直接把示波器探头夹在PA0上测波形。其他需要准备的东西很简单ST-Link或者串口下载器、杜邦线若干、面包板、一个直插LED和一个电阻。示波器不强制能借到最好借不到就先用LED做定性观察。2.2 Keil5开发环境与标准库的选择开发环境主流现在有两种Keil MDK 标准外设库以及STM32CubeMX HAL库。标准库的好处是逻辑更直观很多老教程、入门视频、开发板文档都是基于标准库的遇到问题容易搜到答案CubeMX的好处是图形化配置、初始化代码生成快但新手容易陷入“只会点鼠标不知道代码在干嘛”的尴尬。这篇例程我以标准库为主参考的是网上最常见的固件库模板。安装Keil5的时候要特别注意兼容C51和STM32的问题我的做法是分目录安装先装C51再装MDK分别激活对应的许可证如果顺序反了或者装在同一个目录里后面新建工程大概率会出现莫名其妙的编译错误。2.3 新建工程时容易翻车的几个细节第一次搭建固件库工程模板确实容易翻车。我习惯按固定步骤走新建工程目录、添加固件库源码、配置目标选项里的宏定义和头文件路径、编写main函数、编译下载。宏定义这里特别容易漏F103系列必须定义STM32F10X_MD同时还要勾选USE_STDPERIPH_DRIVER漏掉其中一个就会报一堆未定义错误。还有一个人问得很多的问题include头文件时要不要把.c文件所在文件夹也加进头文件路径。编译时只需要头文件路径不需要加.c文件夹固件库里的.c文件是靠工程中的源文件分组引入的两者是不同维度的东西搞清楚这一点能少走不少弯路。3. 定时器PWM输出核心原理3.1 PWM波形在定时器内部是怎么形成的PWM波形的产生本质上靠的是定时器内部三个关键寄存器计数器CNT、自动重装载寄存器ARR、捕获比较寄存器CCR。定时器启动后CNT从0开始向上计数每来一个时钟脉冲CNT加1当CNT等于ARR时溢出归零再重新计数整个过程就是不停地“数数”。而CCR寄存器里保存的是设定的比较值。在PWM模式1下当CNT小于CCR时输出引脚为有效电平一般配置为高电平当CNT计数到大于等于CCR时输出翻转为无效电平即低电平等CNT归零后重新开始下一轮。这样输出引脚就会周期性产生高低电平交替的方波高电平所占比例正好由CCR和ARR的比值决定。所以改变CCR就能改变占空比改变ARR或预分频PSC就能改变周期两者互不干扰这也是理解PWM参数关系最关键的一点。3.2 频率和占空比计算必须记住的两个减法PWM频率计算公式为PWM频率 TIMxCLK / ((PSC1) * (ARR1))。其中TIM2挂在APB1总线上当APB1预分频配置为2时TIM2的时钟是72MHz。举个例子我设置ARR999、PSC71那么频率 72,000,000 / (72 * 1000) 1000Hz也就是1kHz。占空比计算公式为占空比 CCR / (ARR1) * 100%如果CCR500则占空比就是50%。这里有两个减法陷阱PSC和ARR在公式里都要加1。很多人直接写完初始化之后发现频率不对多半就是忘了减1导致预分频和重装载的数值比预期大1倍或者差很多。F103的定时器是16位的ARR和PSC最大可以写到65535但实际使用时要根据目标频率倒推。3.3 定时器通道与引脚映射关系STM32F103各定时器的通道输出引脚不是随便接的需要查数据手册里的引脚复用表。比如TIM2的四个通道CH1对应PA0、CH2对应PA1、CH3对应PA2、CH4对应PA3TIM3_CH1对应PA6TIM4_CH1对应PB6。如果开了重映射还可以挪到其他引脚。实际做实验时有两个注意事项第一这些引脚如果同时被其他外设功能占用会产生信号冲突所以用之前最好确认一下板子的原理图第二部分开发板把PA0、PA1接到了板载按键或者其它器件上可能造成电平干扰这时候要么换定时器通道要么用重映射换到空闲引脚。热词里常出现PWMDMA输出PA1、PA3的组合就是用DMA把波形数据批量搬运到CCR寄存器实现较复杂的波形效果但那是进阶玩法基础实验先不用碰。4. 实操PWM输出实验完整代码4.1 标准库初始化函数每一行都别偷懒代码我用标准库来写先把TIM2的PWM输出初始化封装成一个函数参数是ARR周期值和PSC预分频值这样主函数传不同参数就能灵活调整频率。第一步是开时钟TIM2挂在APB1总线上所以用RCC_APB1PeriphClockCmd使能GPIO和AFIO挂在APB2总线上AFIO也要开虽然默认不重映射时未必用到但先养成习惯总没坏处。第二步配置PA0为复用推挽输出GPIO_Mode_AF_PP这里特别提醒很多人习惯性写成GPIO_Mode_Out_PP结果引脚被当成普通IO控制输出不了PWM波形。第三步配置定时器时基ARR、PSC、计数模式向上、时钟分频0。第四步配置输出比较通道为PWM模式1、输出使能、极性高。最后打开TIM_Cmd使能定时器。void TIM2_PWM_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); 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); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; 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); }4.2 主函数动态修改占空比实现呼吸灯固定占空比输出只需要调用一次TIM_SetCompare1(TIM2, 500)占空比就是50%。如果想让LED做呼吸灯效果就在主循环里不断改变比较值让它从0递增到ARR再从ARR递减回0。这里的核心是主函数里一个简单的计数循环注意占空比不能超过ARR否则CCR大于ARR的时候输出会一直是高电平看起来就像PWM失效了。使用所有定时器通道时要注意所有通道共享同一个ARR和PSC但每个通道的CCR是独立的所以可以同频输出不同占空比这是多路PWM应用的基础。int main(void) { u16 duty 0; u8 dir 1; TIM2_PWM_Init(999, 71); // 输出1kHz PWM while (1) { TIM_SetCompare1(TIM2, duty); duty dir; if (duty 1000) dir -1; if (duty 0) dir 1; delay_ms(2); } }4.3 怎么验证输出到底对不对没有示波器时可以用LED亮度来判断大致的占空比但频率太高或太低都会影响观察效果。如果PWM频率只有1HzLED会一秒亮一秒灭看得非常清楚但这时候你验证不了占空比如果频率是1kHzLED就会呈现出连续亮度占空比越大越亮。有示波器的时候直接夹在PA0和GND之间观察频率、周期和高电平脉宽。我实测C8T6最小系统板PSC71、ARR999时示波器读出频率约为1.00kHzCCR500时占空比约50.2%基本和理论计算吻合。这个误差主要来自时钟源本身的精度和示波器读数误差对大多数应用完全够用。5. 常见问题与排查技巧实录5.1 引脚没有波形输出先按顺序查这几点这是PWM实验里遇到最多的问题我整理过一个排查顺序按顺序查基本都能定位。第一定时器和GPIO时钟是否都打开了缺任何一个都不会输出。第二GPIO是不是配成了复用推挽输出GPIO_Mode_AF_PP如果配成普通推挽引脚只能输出高低电平PWM波形根本出不来。第三TIM_Cmd有没有执行定时器没使能计数器根本不跑。第四ARR和CCR是否被意外写成0比较值小于等于0时输出一直是低电平。第五引脚是否被其他外设占用比如JTAG默认占用PA13到PA15部分通道和调试口冲突时也会导致没波形。5.2 TIM1高级定时器输出PWM的特殊坑很多人用TIM1做PWM输出明明初始化步骤和TIM2一样但就是没有波形关键差在一行代码TIM_CtrlPWMOutputs(TIM1, ENABLE);。TIM1属于高级定时器主输出默认是关闭的必须在使能定时器之后手动打开主输出否则波形永远不会出现在引脚上。这个坑我从入门到现在见过不少人踩过如果你用TIM1做PWM实验搜索“stm32 tim1 pwm输出”出来的第一屏结果大概率都在说这个问题。另外TIM1通常还涉及刹车输入和死区配置如果只是做普通PWM输出不涉及电机控制可以不配置刹车但主输出必须打开。5.3 频率算出来和实际不一样问题出在哪如果初始化PSC71、ARR999理论频率应该是1kHz但实测变成500Hz先检查外部晶振是不是8MHz。有些最小系统板焊接的是12MHz晶振但程序里还按8MHz配置时钟PLL倍频结果就会出错。还有一种情况是APB1预分频没有配置对STM32F103的TIM2时钟不是简单的APB1时钟当APB1预分频为1时TIM2CLK是36MHz当APB1预分频为2时TIM2CLK反而变成72MHz这是总线时钟倍频器的作用。初学阶段建议直接用系统默认配置的72MHz主频别一上来就折腾时钟树。5.4 一个定时器能同时输出PWM和接收PWM信号吗这个问题经常有人问答案是能但要分通道。比如TIM2_CH1配置为输入捕获接收外部PWM信号TIM2_CH2配置为PWM输出二者共用一个时基也就是共享ARR和PSC但CCR是独立的一个是捕获比较寄存器一个是输出比较寄存器。实际使用时的麻烦在于输入捕获和输出PWM对时基参数的要求常常不一致输入捕获取决于外部信号频率输出PWM又由ARR和PSC决定互相妥协后两边都可能调到不理想。所以我做工程时一般用两个定时器一个负责读PWM输入另一个负责输出逻辑清晰也更好维护。6. 扩展方向与个人经验6.1 从PWM输出到舵机控制舵机是PWM最典型的应用场景标准舵机要求20ms周期也就是50Hz高电平时间在0.5ms到2.5ms之间对应0到180度。用上面公式反推PSC71、ARR19999时频率 72M / (72 * 20000) 50HzCCR设置为500到2500之间就能控制舵机角度。我自己试过把CCR从500逐步增加到2500舵机会平滑地从0度转到180度。注意舵机电源要单独供电或者用足够电流的稳压模块直接从单片机引脚取电很容易欠压导致舵机抖动。6.2 软件PWM的适用场景如果不想占用定时器资源用延时加GPIO翻转也能模拟PWM俗称软件PWM。软件PWM的好处是不占用定时器外设缺点是会一直占用CPU而且频率越高、精度越高CPU负担越大。我建议软件PWM只用在低频、低精度的场景比如LED呼吸灯、蜂鸣器播放简单音乐或者在所有硬件定时器通道都被占用时临时顶一下。真要驱动电机这类负载还是得上硬件PWM稳定性和实时性完全不是一个级别。6.3 这几次实验下来我最想分享的几个经验最后说几个实操层面的体会。调试PWM时不要把频率设得太低否则LED会明显闪烁容易误判成程序跑飞但也不要一上来就设到几百kHz引脚寄生电容和LED限流电阻会影响波形边沿示波器上看到的波形会变圆。修改占空比时如果发现波形变成恒高先检查CCR是不是比ARR还大这不算故障只是比较值写越界了。还有一个习惯我觉得很值得养成把PWM频率、占空比初始值这些参数集中用宏定义放在头文件里换板子、换引脚、换应用场景的时候只改一处代码会好维护很多。这套方法从F103C8T6出发但换到其他STM32型号思路也完全适用核心就是抓住定时器时基和比较输出这两条主线。本文还有配套的精品资源点击获取
返回列表