ARTICLE DETAIL

资讯详情

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

STM32F1 PWM输出详解:从标准库到CubeMX的定时器实战

STM32F1 PWM输出详解:从标准库到CubeMX的定时器实战 简介STM32F1系列PWM输出实验完整工程面向初学STM32定时器与PWM应用的开发者帮助理解PWM产生原理、占空比调节及外设协作方式。资源共32个文件以C源文件与头文件为主包含10个C源码和14个头文件同时包含Keil工程配置文件、启动汇编文件、hex固件与清理脚本压缩包仅105KB结构简洁便于直接打开学习。工程内不仅实现PWM输出还集成了定时器、蜂鸣器、LED、按键、外部中断、看门狗及串口等模块驱动并配有readme说明可对照代码理解各模块初始化与中断流程。目前已有1001人学习下载适合对照硬件实验板逐模块调试或作为课程设计的参考工程。 在STM32F1的开发序列里PWM输出实验几乎是绕不开的一课。我最早用这块板子做PWM是为了给直流电机调速结果配置完烧进去电机纹丝不动示波器上也什么都看不到查了半天才发现是GPIO复用时钟没开。这篇内容就围绕stm32f1的PWM输出从原理到实操、从标准库到CubeMX整体梳理一遍。适合刚学完定时器中断、想往PWM方向迈进的同学也适合做项目时临时需要调光、调速、舵机控制的工程实践参考。1. 实验前先想清楚几个问题1.1 你拿PWM到底要干什么PWM能干的活太多了。调个LED亮度叫呼吸灯控制直流电机转速叫调速给舵机发脉宽叫舵机控制和ADC联动又变成了触发采样。实验9这个标题虽然看起来简单但不同用途对应的参数选择完全不同。调光一般1kHz甚至更低就够用电机调速可能需要10kHz以上来降低线圈噪声舵机控制则固定是50Hz周期。如果你没想清楚应用场景就直接抄配置后面大概率要返工。1.2 选哪个定时器和通道STM32F103C8T6这类小容量芯片上定时器资源是有限的。TIM1是高级定时器带互补输出和刹车功能适合电机控制里的H桥驱动TIM2、TIM3、TIM4是通用定时器每个有4个通道日常调光调速完全够用。实验9一般推荐用TIM3或者TIM2因为时钟挂在APB1总线上最终也能拿到72MHz做分频周期计算很方便。如果追求引脚好引PA6TIM3_CH1和PA7TIM3_CH2是最常用的两个点它们不容易和调试串口冲突。定时器通道数主要特点常用引脚TIM14 互补高级定时器支持刹车、死区PA8, PA9, PA10, PA11TIM24通用定时器32位计数器PA0, PA1, PA2, PA3TIM34通用定时器16位计数器PA6, PA7, PB0, PB1TIM44通用定时器16位计数器PB6, PB7, PB8, PB9同一组引脚如果复用了其他外设比如USART1的TX/RX就不能同时做PWM输出。引脚复用表要提前查这是新手最容易踩的第一个坑。1.3 标准库还是CubeMX这个问题不用争吵。标准库的好处是配置代码透明你能看到每个寄存器的具体操作非常适合理解PWM的原理比如CCR和ARR的关系、预装载和影子寄存器的作用代码里都体现得清清楚楚。CubeMX的优势是快图形化点几下就能生成一个可用的工程适合项目里PWM只是辅助功能、不想在初始化上耽误时间的情况。我的建议是第一次学PWM先用标准库把原理搞懂之后再切CubeMX做量产项目。两个方式下面都会展开。2. PWM原理与定时器映射2.1 PWM和PPM到底有什么区别网上搜PWM经常捎带搜出PPM很多人分不清。简单讲PWM是脉宽调制用高电平持续时间和整个周期的比例来表达信息比如占空比50%的1kHz方波。PPM是脉冲位置调制它用脉冲出现的时间位置来表达信息常见于遥控接收机的信号传输。STM32定时器输出的是PWM不是PPM。如果你做遥控解码接收机出来的是PPM或者SBUS那要用输入捕获去解而不是直接输出PWM。把这两者区分清楚能避免方案选型时走弯路。2.2 PWM生成的底层逻辑用STM32的定时器做PWM本质上是让计数器从0数到ARR在计数值小于CCR时输出高电平等于或超过CCR时输出低电平。计数器溢出后自动回0重新开始于是引脚上就出现一个周期为(ARR1)/定时器时钟、占空比为CCR/(ARR1)的方波。整个过程完全由硬件完成CPU不需要干预所以PWM输出几乎不消耗主循环时间。这也是为什么在实时性要求高的系统里能用硬件PWM就尽量别用软件PWM——软件PWM要进中断翻转引脚CPU占用高输出抖动也大。2.3 频率和占空比怎么算定时器的时钟来源要先分清。TIM1挂在APB2上最高72MHzTIM2到TIM4挂在APB1上虽然APB1总线时钟是36MHz但定时器时钟倍频后仍是72MHz。计算PWM频率的公式是PWM频率 定时器时钟 / ((PSC 1) * (ARR 1))占空比 CCR / (ARR 1)举个例子我常用的那组参数PSC71ARR999CCR500。定时器时钟72MHz那么频率就是72MHz/(72*1000)1kHz占空比是500/100050%。改动PSC可以粗调频率范围改动ARR可以细调频率改动CCR只影响占空比。理解这个关系后任何参数要求你都能快速反推出配置值。3. 标准库方式实现PWM输出3.1 GPIO和定时器初始化以TIM3_CH1在PA6输出为例完整初始化分三步。第一步开时钟打开GPIOA和TIM3的时钟。注意TIM3在APB1上用的是RCC_APB1PeriphClockCmd和GPIO的RCC_APB2PeriphClockCmd不是同一个函数漏掉任何一个都会导致输出无反应。第二步配GPIOPA6要设为复用推挽输出GPIO_Mode_AF_PP而不是普通推挽输出。很多人把GPIO配成GPIO_Mode_Out_PP结果引脚上永远是高或低电平根本看不到波形。第三步才是定时器初始化。时间基准配置包括预分频、计数模式、自动重装载值和时钟分频然后配置通道1的输出比较模式为PWM1使能输出设置Pulse即CCR的初值。完整代码是这样void PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); 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); TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 999; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 500; 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); }3.2 启动PWM和主循环标准库初始化完成之后别忘了使能定时器也就是上面代码里的TIM_Cmd。很多教程对这一步一笔带过实际工程里漏掉TIM_Cmd就会出现GPIO配置全对、代码编译通过、但引脚上没有波形的情况。使能之后如果只想固定输出一个占空比主循环里什么都不用做。但多数场景需要动态调占空比这时用到TIM_SetCompare1。比如做呼吸灯主循环里用一个变量从0加到999每加一次调用一次TIM_SetCompare1再加软件延时让变化肉眼可见LED就能从暗变亮再变暗。PWM1模式下CCR增大高电平时间变长LED变亮方向别搞反。3.3 预装载和影子寄存器我单独把预装载拿出来说是因为这个点面试爱问调试时也容易踩。STM32的ARR和CCR寄存器其实都是影子寄存器程序写入的新值不会立即生效要等更新事件才会被装载到真正参与比较的寄存器。TIM_OC1PreloadConfig和TIM_ARRPreloadConfig就是用来使能这种预装载行为的。使能后占空比和频率的更新发生在同一个时间点不会出现一个周期里用了新旧混杂值的情况。如果你发现PWM在动态调节时波形抖动、周期忽长忽短多半是预装载没开启或者开了但软件更新频率不对。4. CubeMX方式配置PWM4.1 图形化配置步骤现在用CubeMX的人越来越多我说一下完整配置路径。新建工程选STM32F103C8T6后先在System Core里的RCC把HSE设为Crystal/Ceramic Resonator这是外部8MHz晶振。然后在Clock Configuration里把系统时钟配到72MHz确认APB1定时器时钟也是72MHz。接着在Pinout Configuration里找到TIM3把Channel1设为PWM Generation CH1在Parameter Settings里设置Prescaler71Counter Period999Pulse500。计数模式默认是Up自动重装载预装载建议打开。设置完引脚会自动分配到PA6芯片图里能看到PA6变绿。4.2 生成工程后的关键改动CubeMX生成的代码主函数里必须手动加一行启动函数HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1)否则没有波形。动态改占空比用__HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 800)把800改成实际需要的CCR即可。HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 800);这里有个隐藏细节HAL库初始化时不管你设置的是什么Pulse只要没调用StartPWM引脚就保持普通状态。我遇到过不少从标准库转HAL库的人配置里明明写了Pulse500就是忘了Start结果示波器上什么都没有。所以拿到生成代码的第一步先补上Start再用示波器验证。5. PWM应用的延伸玩法5.1 呼吸灯玩起来呼吸灯是PWM输出最直观的实验。把呼吸周期设为2秒亮度从0到100%再到0按1kHz的PWM输出CCR从0平滑升到999再降回0。这里有个视觉上的坑人眼对亮度的感知不是线性的直接用for循环线性递增CCR你会看到LED在暗区变化很快、亮区变化不明显。要做得舒服一般要按指数或者对数曲线映射CCR。这个优化是后话实验9阶段先跑通线性版就行。5.2 舵机控制与电机调速舵机控制和普通PWM的主要区别在频率和脉宽范围。标准舵机要求20ms周期也就是50Hz脉宽0.5ms到2.5ms对应0到180度。用前面公式反推如果PSC71ARR要设19999CCR在500到2500之间就对应0到180度。角度和CCR可以简单按线性算每度大约11.1个计数值。电机调速则是另一套逻辑直流有刷电机通常用10kHz到20kHz的PWM频率太低会有明显电噪声。电机属于感性负载PWM输出端要加续流二极管或使用半桥驱动芯片不能拿单片机引脚直接驱动大电流电机。做双电机或者带方向控制的系统一般要用TIM1的高级功能配合互补PWM和刹车输入实现H桥控制。5.3 PWM触发ADC采样PWM触发ADC是电机控制里非常经典的用法。思路是让PWM的更新事件触发ADC启动采样这样采样点可以固定在PWM周期内的某个相位避免在电流纹波大的时刻采到不稳定的值。实现方式是利用定时器的TRGO事件在CubeMX里把TIM1的Trigger Event Selection设为Update EventADC触发源选择TIM1 Trigger Out。每个PWM周期自动启动一次ADC转换不需要软件干预。这种做法的好处是采样时序和PWM严格同步做电流环或FOC控制时会频繁用到。5.4 PWM配合DMA输出任意波形PWM和DMA组合起来能玩出很多花样。比如用DMA把存储在数组里的占空比序列逐个搬运到CCR寄存器每次更新事件搬运一个值就能在引脚上合成任意周期波形也就是低成本DAC的替代方案。另一个常见场景是多路PWM需要同步更新用DMA同时更新多路CCR可以避免逐路设置导致的时间偏差。PA1、PA3这类引脚如果同时要做多路PWM输出配合DMA操作会比在中断里逐路赋值可靠得多。5.5 软件PWM和高级定时器软件PWM不是用定时器外设而是用定时器中断或者SysTick中断在中断里翻转GPIO电平。它有两个问题CPU占用高输出抖动大因为中断响应时间不确定。但在低成本方案里比如定时器不够用、或者PWM频率要求很低软件PWM确实能凑合。如果要用建议频率不要超过200Hz中断里只做GPIO翻转不做复杂运算。至于HRTIM、CCU6这些名词是部分高端MCU的高分辨率PWM模块或英飞凌平台上的PWM单元包括AUTOSAR MCU底层开发里提到的PWM抽象本质都是计数器比较输出但寄存器配置路径完全不同换了平台不能照搬。还有一个常被问到的问题一个定时器能不能既接收PWM数据又输出PWM波形答案是可以通用定时器不同通道可以分别配置为输入捕获和输出比较但前提是它们共享同一个时基输入信号周期和输出PWM周期必须匹配或者你对时基精度要求不高。6. 常见问题与排查技巧实录6.1 板子上看不到波形最经典的问题。我按顺序列一个检查清单GPIO复用时钟有没有开定时器时钟有没有开GPIO是不是复用推挽输出定时器有没有调用TIM_Cmd或者HAL_TIM_PWM_Start引脚有没有被其他外设占用示波器探头是不是接在正确的通道上。还有一个容易忽略的如果用TIM1必须调用TIM_CtrlPWMOutputs(TIM1, ENABLE)来使能主输出这是高级定时器特有的通用定时器不需要。6.2 频率和占空比和计算值不一致这种问题多半出在时钟源理解上。TIM2到TIM4虽然挂APB1但APB1总线分频不为1时定时器时钟是APB1的两倍。如果你的系统时钟不是72MHz而是改成了48MHz或别的值计算时要按实际时钟来。排查方法很简单把PSC设成0直接观察计数溢出频率用逻辑分析仪数一下就能反推出实际定时器时钟。还有一种情况是更新事件和比较事件逻辑搞混CCR和ARR被其他代码覆盖了这种情况要全局搜索谁在写TIM_Period或者CCR。6.3 多路PWM抢引脚、互相干扰多路PWM同时输出先确认引脚复用没有冲突。比如PA3既是TIM2_CH4又是USART2的RX如果你同时开了串口和PWM引脚就会被抢占。其次是定时器资源是否够用一个16位定时器最多4路PWM如果项目要8路PWM可以用两个定时器或者外接PCA9685这种PWM扩展芯片后者更适合驱动多路舵机。还有一个工程经验同一块板子上PWM输出线尽量短地线要稳特别是驱动电机时电机启停瞬间的电流尖峰会干扰PWM波形电源端要加稳压电容。6.4 PWM故障保护和刹车功能最后提一下高级定时器的故障保护。做电机驱动时如果MCU检测到过流、过压必须立刻切断PWM输出这个动作不能依赖软件判断再关PWM中断响应再快也有延迟。TIM1和TIM8的刹车输入就是干这个的硬件上把故障信号接到BKIN引脚配置成高电平或低电平触发刹车一旦触发PWM输出立即进入安全状态。CubeMX里对应的是Break and Dead-time配置。这是做电机驱动产品时必须考虑的功能实验9用不上但后面的项目一定会遇到。7. 写在最后说实话PWM这个外设我用了很多年每次换芯片平台还都得重新查一遍定时器时钟和引脚映射。但底层的计数比较、预装载、更新事件这套逻辑只要是标准定时器就都类似。我个人习惯是先把一个通道用标准库跑通用示波器确认波形频率和占空比完全对得上再开始往上叠功能。实验9做到这里已经够把呼吸灯、舵机、电机调速玩起来了后面想深入可以试试PWM触发ADC再配合DMA做电流采样那些就是另外一个层次的话题了。本文还有配套的精品资源点击获取
返回列表