ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

STM32定时器与PWM应用编程:从原理到电机驱动实战

STM32定时器与PWM应用编程:从原理到电机驱动实战 1. 项目概述从定时器到PWM嵌入式开发的“心跳”与“脉搏”如果你玩过STM32或者任何一款单片机那你一定绕不开两个核心概念定时器和PWM。这俩兄弟一个负责精准的“计时”一个负责灵活的“调压”堪称嵌入式系统里的“心跳”与“脉搏”。我刚开始接触时也觉得它们神秘又复杂寄存器配置看得人头大。但后来在无数个项目里摸爬滚打从控制LED亮度到驱动伺服电机再到生成复杂的通信波形我才真正体会到把定时器和PWM玩明白了你的嵌入式开发功力才算真正入门。今天我就以一个老鸟的视角掰开揉碎了讲讲STM32上定时器与PWM的应用编程不扯那些虚的全是能直接“抄作业”的干货和踩过的坑。简单说定时器Timer就是单片机内部一个不断累加的计数器你可以设定它计数的速度时钟源和计数的目标值自动重装载值。当计数器达到目标值就会产生一个“时间到”的信号这就是定时中断或者更新事件。而PWMPulse Width Modulation脉冲宽度调制则是定时器一个非常经典的应用模式。它利用定时器产生一个周期固定、但高电平或低电平占空比可调的方波。通过调节占空比就能等效地调节输出信号的平均电压从而无级控制LED亮度、电机转速、舵机角度等等。为什么这俩如此重要因为嵌入式系统本质上是与“时间”和“模拟量”打交道的。你需要精准地知道“过了多久”也需要用数字信号只有0和1去控制模拟世界如亮度、速度、位置。定时器和PWM正是连接数字核心与物理世界的桥梁。无论是你看到的江科大STM32教程里闪烁的LED还是智能台灯里根据环境光自动调节的亮度亦或是两轮差速小车那精准的电机控制背后都是这套逻辑在支撑。接下来我会带你从最基础的定时器原理和配置开始一步步深入到PWM的生成、高级应用以及那些手册里不会写的调试技巧。无论你是刚用上STM32CubeMX的新手还是正在纠结HRTIM复杂配置的老手相信都能找到你需要的东西。2. 定时器核心原理与STM32的定时器家族在动手写代码之前我们必须先搞清楚定时器到底是怎么工作的以及STM32为我们提供了哪些“武器”。很多新手一上来就对着CubeMX点点点结果出了问题完全不知道从何查起根本原因就是底层的原理没吃透。2.1 定时器的基本工作原理不止是“数数”你可以把一个定时器想象成一个水桶和一个水龙头。水龙头以固定的速度时钟频率向桶里滴水桶上有一个刻度线自动重装载值ARR。每滴入一滴水桶里的水位计数器CNT就上涨一点。当水位达到刻度线时会发生两件事第一桶自动被清空计数器归零或从预设值重新开始第二它可能会触发一个警报更新中断或事件。这个“滴水的速度”和“桶的容量”就共同决定了“清空一次桶需要多久”也就是定时周期。在STM32中这个过程由几个关键寄存器控制时钟源CK_PSC水龙头的总水源。可以来自内部系统时钟APB总线也可以来自外部引脚。预分频器PSC一个水阀用于降低水流速度。如果PSC设置为N那么实际进入计数器的时钟频率 CK_PSC / (N1)。这是实现长定时周期的关键。计数器CNT那个水桶本身记录当前的水位。它可以向上计数、向下计数或向上向下双向计数。自动重装载寄存器ARR桶上的刻度线。当CNT达到ARR值向上计数或从ARR值减到0向下计数时发生更新事件。定时周期T的计算公式以向上计数为例是核心中的核心T (ARR 1) * (PSC 1) / Tclk其中Tclk是定时器时钟源的频率。比如系统时钟为72MHzPSC设为71ARR设为999那么定时周期 T (9991)*(711) / 72,000,000 0.001秒即1ms。这就是我们常说的1ms定时中断的由来。注意这里ARR和PSC都加了1是因为它们是从0开始计数的。ARR0表示计1个数就溢出PSC0表示1分频即不分频。这个“1”是新手最容易忽略导致定时不准的坑。2.2 STM32定时器分类与选型指南STM32的定时器种类繁多功能强弱差异巨大选对了事半功倍选错了要么功能无法实现要么浪费性能。它们大致可以分为以下几类定时器类型典型型号 (以F1/F4为例)主要特点与用途适用场景基本定时器TIM6, TIM7功能最简单只有最基本的定时功能计数、溢出中断/DMA。没有输入捕获和输出比较通道。为DAC提供触发时钟作简单的延时基准。通用定时器TIM2-TIM5, TIM9-TIM14功能最全面、最常用。支持定时、输入捕获测频率/脉宽、输出比较产生单脉冲、PWM、编码器接口等。PWM生成、电机控制、输入信号测量、脉冲计数等绝大多数场景。高级控制定时器TIM1, TIM8在通用定时器基础上增加了互补输出、死区插入、紧急刹车等功能。专门为电机控制和电源转换设计。三相电机FOC控制、开关电源、全桥/半桥驱动等需要复杂PWM和保护的场合。高分辨率定时器 (HRTIM)(部分系列如G4, H7)分辨率极高可达217ps拥有多个定时器单元可产生极其复杂和同步的PWM波形。数字电源、LLC谐振变换器、多相电机控制等对时序精度要求极苛刻的应用。系统滴答定时器 (SysTick)Cortex-M内核内置24位递减计数器专为操作系统提供心跳时钟。也可用于简单的延时。RTOS任务调度、简单的毫秒级延时。选型心得新手入门/普通控制无脑选通用定时器如TIM3。资源丰富教程最多。控制直流有刷电机或步进电机通用定时器足够。如果需要控制方向正反转可能需要两个PWM通道组成H桥此时注意定时器是否有足够的通道。控制无刷电机BLDC或伺服电机强烈建议使用高级控制定时器TIM1/TIM8。它的互补输出和可编程死区功能是安全驱动半桥/全桥电路常用电机驱动芯片如DRV8301、IR2104S的刚需能有效防止上下桥臂直通短路烧毁MOS管。LED调光、蜂鸣器任何一个有输出比较功能的定时器都行甚至可以用PWM硬件外设更简单的型号。需要极高PWM频率或精度如开关电源500kHz考虑具有高分辨率定时器HRTIM的型号如STM32G4系列。很多朋友在选型时纠结于“我的项目要不要用TIM1”。一个简单的判断方法是你的硬件驱动电路是否需要同时输出一对互补的、中间带有死区的PWM信号例如控制一个半桥如果是那么TIM1/TIM8几乎是你唯一的选择。否则通用定时器更简单易用。3. PWM生成全解析从理论到寄存器配置理解了定时器PWM就水到渠成了。PWM的核心思想是在一个固定的周期由ARR决定内通过改变高电平的时间由捕获/比较寄存器CCR决定来改变平均电压。3.1 PWM模式详解边沿对齐与中心对齐STM32的PWM主要工作在两三种模式下区别在于计数器计数方式和比较点PWM模式1和模式2边沿对齐模式模式1在向上计数时当CNT CCRx时通道输出有效电平可设为高或低当CNT ≥ CCRx时输出无效电平。直到计数器溢出CNTARR后归零开始下一个周期。模式2与模式1逻辑相反。特点波形是标准的边沿对齐方波。这是最常用的模式控制LED、普通电机等完全够用。它的谐波成分主要集中在开关频率及其倍数上。PWM中心对齐模式模式1或模式2下计数方式选择为“中央对齐”计数器先向上计数到ARR再向下计数到0如此往复。比较事件在向上和向下计数过程中各发生一次。这使得PWM脉冲关于周期中心对称。特点能有效减少谐波尤其是在电机驱动中可以降低电机的噪声和铁损。但控制逻辑稍复杂。在驱动无刷电机进行FOC控制时为了同步触发ADC采样在PWM波形的“谷底”或“中心点”采样电流此时电流纹波最小中心对齐模式是标准配置。这也是为什么在FOC教程中总会强调要配置定时器为中央对齐模式并启用更新事件触发ADC。占空比计算边沿对齐占空比 CCRx / (ARR 1)中心对齐占空比 CCRx / ARR 注意这里没有1因为计数器从0到ARR再到0一个完整周期计了2*ARR个数但有效比较值范围仍是0~ARR3.2 使用STM32CubeMX配置PWM的实操步骤理论说再多不如动手配一遍。我们以在STM32F103C8T6的PA8引脚TIM1_CH1上输出一个1kHz占空比50%的PWM为例。时钟树配置首先在Clock Configuration标签页确保系统时钟SYSCLK和APB2总线时钟APB2 Timer Clocks被正确设置。对于F103通常内部时钟为8MHz经过PLL倍频到72MHz。APB2的预分频器如果不为1定时器时钟可能会被倍频这里需要根据数据手册核对。一个简单的方法是在CubeMX中配置好后查看右侧Timers-TIM1的Clock Source和Counter Settings中的Timer Clock Mhz确认它是你期望的值比如72MHz。定时器模式配置在Pinout Configuration标签页找到TIM1。Clock Source选择Internal Clock。在Combined Channels中选择PWM Generation CH1。这时原理图上PA8会被自动标记为TIM1_CH1。切换到Parameter Settings选项卡Prescaler (PSC - 16 bits value)设置为71。计算目标定时器频率 PWM频率 * (ARR1)。我们先设ARR再反推PSC。假设ARR先设为999则定时器频率需为1kHz * 1000 1MHz。系统时钟72MHz所以PSC 72MHz / 1MHz - 1 71。Counter Mode选择Up边沿对齐或Center Aligned mode 1中心对齐。我们先选Up。Counter Period (AutoReload Register - 16 bits value)设置为999。这样PWM周期 (711)*(9991)/72MHz 0.001秒 1ms频率1kHz。Internal Clock Division和Auto-reload preload保持默认。切换到下方PWM Generation Channel 1子菜单Mode选择PWM mode 1。Pulse (16 bits value)设置为500。这就是CCR1的值。占空比 500 / (9991) 50%。Output compare preload务必使能Enable。这允许你在运行时通过修改CCR寄存器来平滑改变占空比而不会在本次周期内立即生效产生毛刺而是在下一个更新周期生效。CH Polarity选择High表示有效电平为高电平。你也可以根据驱动电路需求选择Low。生成代码与基本API调用配置完成后生成代码。在main.c的初始化部分CubeMX会自动生成HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1);来启动PWM输出。如果你想在运行时改变占空比只需调用__HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, new_CCR_Value); // 或者使用HAL库函数 // HAL_TIM_PWM_Stop(htim1, TIM_CHANNEL_1); // 如果需要先停止 // htim1.Instance-CCR1 new_CCR_Value; // HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1);修改CCR值后占空比会在下一个PWM周期自动更新。3.3 高级话题互补输出、死区插入与刹车功能当你需要驱动一个半桥一个高侧MOSFET和一个低侧MOSFET时绝对不能同时导通它们否则电源会直接短路瞬间烧管。这就是高级控制定时器TIM1/TIM8大显身手的地方。互补输出TIM1的一个通道如CH1可以产生一对互补的信号CH1和CH1N。通常CH1驱动高侧管CH1N驱动低侧管。它们默认是反相的。死区插入Dead Time为了防止换流瞬间的共通需要在互补信号切换时插入一段两者都为低电平或无效电平的时间即死区时间。TIM1内置了死区发生器可以硬件自动插入死区。在CubeMX中使能TIM1的Channel 1和Complementary Channel 1。在Parameter Settings-Break and Dead-Time中配置Dead Time。这个值是一个复合寄存器需要根据系统时钟和期望的死区时间来计算。例如系统时钟72MHz希望插入1us的死区可以设置为0x0048具体计算需参考参考手册的DTG位描述。死区时间必须根据你使用的MOSFET或驱动芯片的开关特性来仔细计算和调整太短了没用太长了影响效率。刹车功能Break这是一个安全特性。当外部刹车信号来自引脚或内部故障有效时定时器会强制将所有输出通道置于一个预设的安全状态通常全部关闭并产生中断。这对于电机驱动在过流、过热时紧急关断至关重要。在CubeMX中配置Break Function为Enable并选择刹车输入源和输出极性。在代码中需要编写刹车中断回调函数HAL_TIM_OC_DelayElapsedCallback或HAL_TIM_PWM_PulseFinishedCallback来处理故障。实操心得第一次做电机驱动板时我信心满满地焊好了半桥用通用定时器产生两路反相的PWM结果一上电就“放烟花”。后来才明白软件上的“反相”和硬件上的“开关延迟”无法保证绝对不共通。换上TIM1配置好死区后问题迎刃而解。所以涉及半桥/全桥等可能短路的电路死区功能不是可选项而是必选项。4. 定时器的其他关键应用模式除了PWM定时器还有其他几个强大的模式能解决很多实际问题。4.1 输入捕获模式测量频率与脉宽输入捕获用于测量外部信号的周期或高电平宽度。其原理是当检测到输入通道上的特定边沿上升沿/下降沿时定时器会瞬间将当前计数器的值CNT锁存到捕获/比较寄存器CCRx中并可以产生中断。通过记录两次捕获到的CNT值之差再乘以计数周期就能算出时间间隔。典型应用测量频率捕获两个上升沿之间的时间倒数即为频率。测量脉宽先设置为上升沿捕获记录时间T1再设置为下降沿捕获记录时间T2脉宽 T2 - T1。解码红外遥控信号如NEC协议。配置要点以测量高电平宽度为例将定时器通道配置为输入捕获模式。设置触发边沿为上升沿使能捕获中断。在上升沿中断中记录CCRx的值到变量rising_edge并将触发边沿改为下降沿。在下降沿中断中记录CCRx的值到变量falling_edge。计算脉宽pulse_width (falling_edge - rising_edge) * (计数周期)。注意处理计数器溢出的情况。将触发边沿改回上升沿准备下一次测量。注意对于高频信号定时器的计数频率要足够高预分频PSC小否则测量精度会很低。对于低频或宽脉冲信号要开启定时器的溢出中断在中断里对一个溢出计数器加1这样脉宽 (溢出次数 * (ARR1) 本次捕获差值) * 计数周期。4.2 输出比较模式生成单脉冲与驱动DAC输出比较模式是PWM的“兄弟”它更侧重于在精确的时间点改变输出电平而不是生成连续的调制波。工作原理你预先设置一个比较值CCRx。定时器计数器CNT不断变化当CNT与CCRx匹配时硬件会自动根据配置改变对应输出通道的电平置高、置低、翻转或不动作并产生中断。典型应用生成单脉冲启动定时器设置一个比较值当匹配时产生一个边沿然后停止定时器。可以用于产生精确的延时脉冲。模拟软件PWM如果硬件PWM通道不够用可以在输出比较匹配中断中手动翻转引脚电平实现多路软件PWM但会消耗CPU资源。触发其他外设比较匹配事件可以内部连接到ADC或DAC用于触发其开始转换实现定时采样。这在FOC控制中定时器触发ADC采样电流的场景是标配。4.3 编码器接口模式读取电机位置与速度这是定时器为电机控制准备的另一个“神器”。它可以将定时器配置为直接读取正交编码器的A、B两相信号并自动判断方向、计数。配置方法在CubeMX中选择定时器的Combined Channels为Encoder Mode。将编码器的A相、B相信号连接到定时器的两个输入通道如CH1和CH2。在参数设置中选择编码器模式通常为Encoder Mode TI1 and TI2并设置滤波器和极性。配置完成后定时器的计数器CNT就会随着编码器的转动而自动增减。你只需要定期比如用另一个定时器中断去读取CNT的值其变化量就代表了位置增量除以时间就是速度。这比用外部中断软件判向要可靠和高效得多。5. 实战进阶复杂场景下的定时器与PWM应用掌握了基础我们来看几个更复杂的、贴近真实项目的应用场景。这些场景往往需要组合使用定时器的多种功能。5.1 多通道同步PWM与带死区的电机驱动在驱动三相无刷直流电机BLDC时我们需要6路PWM3对互补输出来控制三个半桥。这6路PWM必须严格同步且每对互补信号之间要有死区。实现方法使用一个高级控制定时器TIM1TIM1恰好有3对互补输出通道CH1/CH1N, CH2/CH2N, CH3/CH3N可以完美生成6路同步PWM。配置中心对齐模式为了更好的波形对称性和便于ADC采样通常选择中心对齐模式。统一设置ARR和PSC所有通道共享同一个计数周期ARR和预分频PSC保证了同步性。分别设置各通道CCR通过设置不同的CCR1、CCR2、CCR3值可以产生相位互差120度的PWM波形对于方波驱动BLDC或者产生正弦波调制的SVPWM波形对于FOC驱动。启用死区在Break and Dead-Time设置中统一配置死区时间硬件会自动为所有互补通道插入死区。刹车功能务必配置好刹车输入连接电流采样或温度传感器的故障信号实现硬件级保护。代码要点启动时使用HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1);启动主通道互补通道会自动跟随启动。你也可以用HAL_TIMEx_PWMN_Start(htim1, TIM_CHANNEL_1);显式启动互补通道。改变占空比时分别修改htim1.Instance-CCR1、CCR2、CCR3即可。5.2 定时器触发ADC实现精准采样在电机控制、电源管理、音频处理等应用中经常需要以固定频率采样模拟信号如电流、电压。如果让ADC自由运行或用软件触发采样时刻的抖动会引入噪声。最佳实践是使用定时器触发ADC。配置步骤配置一个定时器如TIM2作为触发源。将其配置为PWM模式或输出比较模式但不一定需要物理引脚输出。关键是使能它的“更新事件”UEV作为触发输出。配置ADC在ADC的配置中找到External Trigger Conversion Source选择对应的定时器触发事件例如Timer 2 Update Event。配置ADC为扫描模式或连续模式如果采样多个通道使用扫描模式如果只采样一个通道单次或连续模式均可。启动定时器和ADC先启动ADCHAL_ADC_Start_IT(hadc)或HAL_ADC_Start_DMA(hadc)然后启动定时器。定时器每次更新事件都会自动触发一次ADC转换。优势采样间隔由定时器周期精确控制完全无抖动。在FOC控制中通常将ADC采样时刻设置在PWM中心对齐波形的“谷底”或开关事件中点此时电流纹波最小采样值最准确。这需要将定时器配置为中心对齐模式并使用其“周期中心”或“周期结束”事件来触发ADC。5.3 使用DMA搬运定时器捕获数据或PWM数据当需要高频、连续地处理定时器数据时如高速采集脉宽、实时更新多路PWM占空比频繁的中断会消耗大量CPU资源甚至导致丢失数据。此时DMA直接存储器访问是救星。应用一DMA输入捕获实现高频脉冲计数将定时器配置为输入捕获模式并使其在捕获事件时产生DMA请求。配置DMA通道将定时器的CCR寄存器作为源地址将一个内存数组作为目标地址。设置DMA为循环模式数据宽度为半字16位或字32位视定时器位数而定。启动DMA后每次捕获到的CNT值都会自动被搬运到数组中完全无需CPU干预。你只需要定期去检查数组里的数据即可。应用二DMAPWM实现波形发生器如果你想产生一个复杂的、非固定占空比的PWM序列例如产生特定形状的包络可以预先计算好一个CCR值数组。配置定时器为PWM模式并使能更新事件UEV的DMA请求。配置DMA将内存中的CCR数组作为源地址定时器的CCR寄存器作为目标地址传输数据宽度匹配并设置为循环模式。启动DMA和定时器。每次定时器更新时DMA会自动将下一个CCR值搬运到寄存器从而动态改变PWM占空比产生任意波形。这在实现呼吸灯、软启动、特定谐波注入等场景非常有用。6. 调试技巧与常见问题排查实录理论配置都懂了但调不通才是常态。下面分享一些我踩过的坑和解决问题的思路。6.1 PWM无输出或波形不对的排查清单检查时钟这是最最最常见的问题用示波器或逻辑分析仪看引脚没信号首先确认定时器的时钟源是否使能。在CubeMX生成的SystemClock_Config()函数里有没有调用__HAL_RCC_TIMx_CLK_ENABLE()可以在main()初始化后用寄存器读一下RCC-APBxENR看对应定时器的时钟使能位是否置1。检查引脚复用确认你使用的引脚确实被正确复用到定时器通道上。在CubeMX的引脚图上该引脚应显示为TIMx_CHx。生成代码后检查MX_GPIO_Init()函数里该引脚是否被配置为复用推挽输出GPIO_MODE_AF_PP。检查PWM是否启动你调用HAL_TIM_PWM_Start()了吗这个函数必须在定时器初始化HAL_TIM_PWM_Init()之后调用。一个常见的疏忽是只在初始化时调用了一次HAL_TIM_PWM_Start()后来在某个条件下关闭了PWMHAL_TIM_PWM_Stop()但忘记重新启动了。检查ARR和CCR的值如果ARR设置得太小比如为0或者CCR的值大于ARR可能会导致没有脉冲输出。用调试器查看htimx.Instance-ARR和htimx.Instance-CCRx寄存器的值是否符合预期。检查输出极性如果配置了低电平有效而你的测量设备默认期待高电平脉冲就会误以为没输出。尝试在CubeMX中切换CH Polarity或者用__HAL_TIM_SET_CAPTUREPOLARITY()函数在运行时修改极性。检查高级控制定时器的“主输出使能MOE”对于TIM1/TIM8即使配置好了如果BDTR寄存器的MOE位没有置1所有输出也是禁止的。HAL_TIM_PWM_Start()函数通常会设置MOE但如果你手动操作寄存器或使用了刹车功能需要确认MOE是否被意外清除。6.2 定时器中断不触发或频率不准中断未使能在CubeMX中配置了定时器中断还需要在NVIC嵌套向量中断控制器设置中使能对应的中断线并设置优先级。检查NVIC Configuration标签页。中断服务函数名错误HAL库为每个外设定义了弱定义的中断回调函数。你需要自己实现它。例如对于更新中断你需要实现void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)函数。确保函数名完全正确且被放在了不会被编译器优化的地方如主文件内。中断标志未清除在你自己编写的中断服务函数如果直接使用寄存器编程里处理完中断后必须手动清除中断标志位如TIMx-SR ~TIM_IT_UPDATE否则会连续进入中断。频率不准回头仔细核对3.1节的定时周期计算公式。确认Tclk定时器实际时钟是否正确。特别注意APB总线预分频器的影响当APB预分频器不为1时挂载在其上的定时器时钟可能会被倍频。最可靠的方法是在代码中打印或通过调试器查看__HAL_RCC_GET_TIMx_CLKSOURCE()或直接读取RCC-CFGR相关位的值来计算。6.3 互补输出与死区插入的实测验证配置了死区怎么知道它生效了必须用示波器双通道测量互补的两路信号。将探头分别接在CHx和CHxN引脚上。调整示波器时间轴放大观察信号跳变的边缘。你应该能看到在一路信号下降沿与另一路信号上升沿之间有一段两者都是低电平或无效电平的区域这就是死区时间。用示波器的测量功能可以量出这个时间是否与你配置的值相符。如果看不到死区检查BDTR寄存器的DTG位是否已正确写入检查输出极性配置是否正确确认使用的是高级控制定时器TIM1/TIM8。6.4 资源冲突与定时器选择STM32的定时器通道是有限的且与引脚复用功能绑定。在项目规划初期就要做好资源分配。使用CubeMX的Pinout视图可以直观地看到引脚复用冲突。如果一个引脚已经被其他功能如UART、SPI占用你就不能再用它做PWM输出。注意定时器的一些高级功能是独占的。例如一个定时器如果被用作编码器接口它的两个输入通道就不能同时用作PWM输出。TIM9到TIM14等通用定时器可能通道数较少只有1个或2个或者没有互补输出、DMA功能。选型时要看清数据手册。最后再分享一个调试“笨”方法但极其有效当你觉得配置都对但就是没现象时写一个最简单的测试程序。比如暂时抛开复杂的应用只让一个定时器通道输出一个固定占空比的PWM并让一个LED闪烁。如果这个基础测试通过了再逐步添加复杂功能。这能帮你快速定位问题是出在基础配置上还是出在复杂的应用逻辑上。嵌入式开发就是这样从最核心、最确定的部分一点点搭建和验证远比一头扎进复杂代码里盲目排查要高效得多。
返回列表