ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32定时器进阶:PWM输出、输入捕获与编码器模式详解

STM32定时器进阶:PWM输出、输入捕获与编码器模式详解

1. 为什么定时器是STM32里最值得深挖的外设

玩STM32的朋友大概都有这么个体会:GPIO点灯、串口打印这些外设,翻翻手册半小时就能跑通,但一到定时器这块,寄存器多、模式杂、手册厚,很多人就卡住了。我见过不少项目,PWM调个占空比还行,一旦要测外部信号频率或者接个旋钮做调节,就开始到处抄代码,抄完也不知道为什么这么配。其实定时器这东西,你把它当成一个"会数数的瑞士军刀"就好理解了——本质上它就是一个按固定节奏递增或递减的计数器,围绕这个计数器衍生出了输出比较、输入捕获、编码器接口等一堆玩法。

这篇内容我想把STM32定时器的三个进阶用法掰开揉碎讲清楚:PWM输出、输入捕获和编码器模式。这三个东西覆盖了绝大多数实际项目里的定时器需求,从电机调速、LED调光,到测转速、读旋钮、做正交解码,基本都绕不开它们。适合已经能跑通基础定时中断、想进一步把定时器用透的嵌入式开发者,也适合正在做电机控制、传感器采集类项目、需要快速落地的朋友。我会按"为什么这么设计—怎么配—配的时候注意什么"的顺序来讲,尽量把每个参数背后的逻辑说透,而不是甩一堆代码让你自己悟。

先说个前提:不同型号的STM32定时器资源差异挺大,高级定时器(比如TIM1、TIM8)功能最全,通用定时器(TIM2到TIM5这类)够用且灵活,基本定时器(TIM6、TIM7)就只能数数。下面讲的内容默认以通用定时器为主,涉及高级定时器特有的地方我会单独点出来。你手上具体是哪颗芯片,先翻一下参考手册的定时器章节开头那张资源表,确认你要用的定时器支持哪些功能,这一步别省,我踩过用基本定时器去配PWM结果死活不出波形的坑。

2. PWM输出:从占空比到死区,把波形捏在手里

2.1 PWM到底是怎么"算"出来的

PWM的本质特别朴素:一个计数器在0到ARR(自动重装载值)之间循环数数,同时有个比较寄存器CCR,计数器值小于CCR时输出一种电平,大于等于CCR时输出另一种电平。于是输出的方波频率由计数节奏决定,占空比由CCR和ARR的比例决定。公式很直接:

  • 频率= 定时器时钟 / ((PSC+1) × (ARR+1))
  • 占空比= CCR / (ARR+1)

这里PSC是预分频器。举个实际例子,假设定时器挂在72MHz总线上,我想要一个1kHz、占空比可调的PWM去驱动LED做呼吸灯。先定ARR:如果PSC取71,那计数时钟就是72MHz/72=1MHz,要1kHz就是每1000个计数一个周期,ARR取999。此时CCR从0到999变化,占空比就从0%到100%线性变化,分辨率是千分之一,做呼吸灯绰绰有余。

提示:ARR和PSC都是"实际值减一"写进寄存器的,因为硬件是从0开始数的。新手最容易在这里差一,算出来的频率总跟预期对不上,先检查这个。

为什么这么设计而不是直接给频率?因为这种"分频+重装载"的结构给了你极大的灵活性——同一个定时器,改PSC能大范围调频率,改ARR能调周期精度,改CCR能实时调占空比且不影响频率。这种解耦是PWM能同时服务电机调速和LED调光的原因。

2.2 输出模式的选择:PWM1还是PWM2

配置PWM时你会遇到PWM模式1和模式2的选择。区别就一句话:模式1下计数器小于CCR时输出有效电平,模式2反过来。听起来无所谓,但配合"有效电平极性"设置,实际输出会差一个反相。我的经验是:驱动LED、驱动MOS管这类,先想清楚你的硬件是高电平有效还是低电平有效,然后模式1配高极性基本能覆盖大部分场景。如果发现波形反了,别急着改硬件,先检查这两个设置。

还有个常被忽略的点是预装载使能(OCxPE)。CCR寄存器如果不开预装载,你改占空比时新值立刻生效,可能在周期中间突然跳变,对电机这种负载会产生冲击。开了预装载后,新值要等到下一个更新事件才生效,波形过渡平滑。做电机调速我强烈建议开预装载,做LED调光开不开都行,但养成开的习惯没坏处。

2.3 高级定时器的死区:驱动半桥的必修课

如果你要驱动半桥或全桥(比如直流无刷电机、逆变电路),就必须用高级定时器,因为只有它们带死区插入功能。死区是什么?就是上下两个互补的开关管在切换时,故意留一小段两者都关断的时间,防止同时导通造成短路炸管。这个时间通常几十到几百纳秒,具体看你的开关管关断速度。

死区时间通过BDTR寄存器的DTG位配置,它有一套编码规则,不是直接写纳秒数。以常见的配置为例,DTG的低几位决定死区时钟的分频,高位决定计数。我一般这么算:先确定定时器时钟周期,比如72MHz对应约13.9ns,想要500ns死区,大约需要36个时钟周期,然后对照手册的DTG编码表反推该写什么值。这块手册讲得细,但表有点绕,建议写个小脚本或者用CubeMX生成后反查寄存器值,比手算靠谱。

注意:死区配置错了轻则波形异常,重则烧管。第一次调半桥驱动,务必先用示波器双通道看上下管的栅极波形,确认死区真实存在且大小合适,再上功率电。

2.4 一份可直接抄的PWM配置流程

以通用定时器输出一路PWM为例,我平时是这么走的:

  1. 使能定时器时钟和对应GPIO时钟,GPIO复用为定时器输出功能。
  2. 配置时基:PSC、ARR按上面公式算好填入,计数模式选向上。
  3. 配置输出比较:选PWM模式1,使能预装载,设置初始CCR。
  4. 使能对应通道输出(CCxE),高级定时器还要使能主输出(MOE)。
  5. 启动定时器计数器。
  6. 运行中通过改CCR调占空比。

代码层面用标准库或HAL都行,核心就是这几个寄存器。我习惯在初始化后先给个50%占空比,用示波器确认波形频率和占空比都对,再接入实际负载。这个"先空载验证"的习惯帮我省过很多事——有次直接接电机发现不转,排查半天结果是初始化顺序里通道输出没使能,空载一看波形根本没有,问题立刻定位。

3. 输入捕获:把外部信号的脾气摸清楚

3.1 输入捕获解决的是什么问题

PWM是"往外发",输入捕获是"往里收"。它的原理是:外部信号接到定时器的捕获通道上,当信号发生边沿跳变(上升沿或下降沿)时,硬件瞬间把当前计数器的值锁存到捕获寄存器CCR里,同时可以触发中断。你拿到这个锁存值,就能反推信号的时间信息。

最典型的两个应用:测频率和测脉宽。测频率的思路是在两个相邻上升沿之间记录计数器差值,这个差值就是一个信号周期对应的计数值,乘以计数时钟周期就是实际周期,取倒数就是频率。测脉宽则是捕获上升沿和下降沿之间的差值。这里有个关键细节:如果信号周期比定时器的一个计数周期(ARR溢出周期)还长,计数器会溢出,单次捕获的差值就不准了。解决办法要么把PSC调大让计数范围覆盖信号周期,要么在溢出中断里累加溢出次数,用"溢出次数×ARR+当前捕获值"来算总时间。后者更通用,我测低频信号(比如几Hz的转速脉冲)时基本都用溢出累加。

3.2 捕获边沿与滤波:别让噪声骗了你

输入捕获配置里有两个容易被忽视但很要命的参数:捕获边沿和输入滤波。边沿好理解,测周期就配上升沿,测脉宽就上升沿下降沿交替。滤波(ICF位)则是用来对付信号毛刺的——机械按键、长线传输的编码器信号、电机换相时的干扰,都可能带来高频抖动,不滤波的话一次跳变被识别成好几次,捕获值全乱。

滤波的原理是要求信号在连续N个采样周期内保持稳定才认作有效边沿,N越大抗噪越强但响应越慢。我的经验值:干净的方波信号滤波设最小甚至关掉;机械触点或长线信号设中等强度;电机环境下的编码器信号适当加大。这个没有标准答案,得拿示波器看实际信号质量再定。有次测一个霍尔传感器输出,不滤波时频率读数乱跳,加了滤波立刻稳了,就是信号边沿有振铃导致的。

3.3 一个完整的测频实操

假设要测一路0到10kHz的方波信号频率,定时器时钟72MHz。我的配置思路:

  • PSC取71,计数时钟1MHz,即每个计数1微秒。
  • ARR取最大65535,这样单次计数范围约65ms,对应最低能测约15Hz,覆盖10kHz没问题。
  • 捕获通道配上升沿触发,开捕获中断。
  • 中断里读CCR,和上次捕获值做差得到周期计数,换算频率。

如果信号可能低于15Hz,就得开更新中断累加溢出。代码逻辑是:每次捕获中断里,用"溢出次数×65536 + 当前CCR - 上次CCR"算周期,然后清零溢出计数。这里有个坑——捕获和溢出可能同时发生,读的顺序不对会差一个周期。稳妥做法是在捕获中断里先读溢出标志再读CCR,或者干脆在捕获时短暂关中断保护一下。我一般用前者,实测够用。

提示:测频精度取决于计数时钟。1MHz计数时钟下,测10kHz信号一个周期是100个计数,量化误差约1%,够用。如果要更高精度,可以提高计数时钟或者用"测多个周期取平均"的办法,后者更划算。

3.4 PWM输入模式:一次捕获同时拿频率和占空比

STM32的定时器还有个很实用的PWM输入模式,它用两个捕获通道配合,一个测周期一个测高电平时间,一次就能同时得到频率和占空比。原理是同一个信号同时进两个通道,一个配上升沿一个配下降沿,硬件自动把周期值和高电平值分别锁存。这个模式测舵机信号、测PWM传感器输出特别方便,省得你自己在中断里来回切边沿。配置时注意从模式要选复位模式,让每次上升沿把计数器清零,这样两个捕获值直接就是周期和脉宽,不用做差。我第一次用这个模式时没配从模式,结果两个值对不上,查了半天手册才发现漏了这一步。

4. 编码器模式:让旋钮和电机自己报位置

4.1 正交编码器为什么需要专门的硬件

旋转编码器输出两路方波A和B,相位差90度。正转时A领先B,反转时B领先A,通过判断两路信号的相位关系就能知道转向,通过数边沿就能知道转了多少。如果用普通GPIO加中断去软件解码,信号一快中断就爆了,CPU全耗在这上面。STM32定时器的编码器模式就是把这个解码过程交给硬件:A、B两路接到定时器的两个捕获输入上,硬件自动根据相位关系做加减计数,你只管定期读计数器的值就行。

这个设计的精妙之处在于,计数器值的变化本身就编码了"转了多少、往哪转"——正转加、反转减,读一次差值就知道这段时间的净位移。而且硬件解码能支持很高的信号频率,软件方案根本比不了。做电机测速、做带旋钮的人机界面,这是标准解法。

4.2 计数倍频:1倍、2倍还是4倍

编码器模式有个计数倍频的选择,可以在A的边沿计数、A和B的边沿都计数、或者A和B的双边沿都计数,对应1倍、2倍、4倍频。倍频越高分辨率越高,但也要看信号质量。4倍频下每个周期能数4个数,分辨率最高,但如果信号有抖动,误计数也会放大。我的建议:信号干净(比如光电编码器)用4倍频,信号一般(比如机械编码器)用2倍频甚至1倍频求稳。

举个算例:一个1000线的编码器,4倍频后电机转一圈计数4000。如果定时器每10ms读一次,两次读数差400,那就是10ms转了0.1圈,转速就是10转/秒即600转/分。这个换算关系做电机测速时天天用,记住"差值/每圈计数/采样时间"这个套路就行。

4.3 编码器模式的配置要点与坑

配置上主要几步:两个GPIO配成复用输入并上拉(编码器输出常是开漏,必须上拉),定时器选编码器模式,设置计数范围(ARR),使能定时器。这里有几个实操心得:

第一,ARR别设太小。编码器计数器是循环的,正转到头会从ARR跳回0,反转从0跳到ARR。如果你读值的间隔内转过的计数超过ARR范围,就会算错方向。一般ARR设最大65535,除非你的采样间隔特别长、转速特别高。

第二,初始值建议设中间值。比如ARR=65535时初值设32767,这样正反转都有足够余量,避免刚上电就在边界附近来回跳。

第三,读值要做有符号处理。两次读数的差如果超过ARR一半,说明发生了回绕,要按有符号数解释。比如差值算出65000,实际应该是65000-65536=-536,即反转了536个计数。这个处理不做,反转时读数会突然变得巨大。

注意:机械编码器(带咔哒声那种)的触点抖动很严重,硬件上建议加RC滤波,软件上可以配合定时器输入滤波。我做过一个面板旋钮,不加滤波时慢慢转都会误判方向,加了滤波和适当降低倍频后才稳。

5. 三个模式的横向对比与选型思路

把这三个模式放一起看,其实它们对应了定时器的三种角色:PWM输出是"信号发生器",输入捕获是"信号分析仪",编码器模式是"运动解码器"。选型时先问自己:我是要产生信号、测量信号,还是解读位置信息?

模式核心寄存器典型场景关键配置常见坑
PWM输出CCR、ARR、PSC电机调速、LED调光、舵机模式选择、预装载、死区差一错误、极性反、死区算错
输入捕获CCR、PSC、滤波测频、测脉宽、PWM输入边沿、滤波、溢出处理溢出未累加、噪声误触发
编码器模式ARR、输入滤波旋钮、电机测速定位倍频、上拉、回绕处理边界回绕、抖动误计数

实际项目里这三个经常组合出现。比如一个电机控制项目,可能同时用PWM输出驱动电机、用编码器模式测转速、用输入捕获测某个传感器信号。这时候要注意定时器资源的分配——高级定时器优先给需要死区的PWM,通用定时器分给捕获和编码器,别让一个定时器同时干互相冲突的活。我见过有人想用一个定时器的同一个通道既做PWM输出又做捕获,这物理上就不行,通道是复用的,得分开。

还有个资源层面的经验:定时器数量有限的芯片上,能用硬件就别用软件模拟。比如编码器解码,软件模拟在低速时看着能用,一旦转速上去就丢步,最后还是得回到硬件方案。前期规划时把定时器资源列个表,哪个干什么是定死的,后期改需求才不会手忙脚乱。

6. 调试实录:那些手册上不会写的排查经验

6.1 PWM没波形?按这个顺序查

PWM调不出来是最常见的求助。我的排查顺序是:先看GPIO有没有配成复用功能(配成普通输出是没波形的),再看定时器时钟使能没有,然后看通道输出使能位(CCxE)和高级定时器的主输出使能(MOE),最后用示波器看。这四步能解决八成问题。剩下两成里,一半是ARR或PSC算错导致频率离谱(比如算出几Hz以为是没输出),另一半是引脚被别的功能占用了。有次调了一小时,最后发现是调试时把那个引脚配成了SWD的复用脚,冲突了。

6.2 捕获值总在跳?先怀疑信号

输入捕获读数不稳,很多人第一反应是代码问题,其实先该怀疑信号质量。拿示波器看原始波形,有没有振铃、毛刺、边沿太缓。信号本身脏,代码怎么写都白搭。确认信号干净后再查滤波配置和溢出处理。我遇到过一次捕获值周期性跳变,最后发现是信号源和板子共地不良,加了根地线就好了——这种问题纯看代码永远找不到。

6.3 编码器反转读数异常?查回绕

编码器正转正常反转乱,九成是回绕处理没做对。前面说过,差值要按有符号数解释。还有个隐蔽情况:如果ARR设得不是最大值,而你又没考虑回绕,正转到ARR附近再稍微转一点就跳回0,差值算出来是个大正数,方向就判反了。解决办法要么ARR设满,要么差值判断时用"超过一半就减一个周期"的逻辑。这个逻辑我封装成一个小函数,所有读编码器的地方都调它,省得每处都写一遍还容易漏。

6.4 中断里读寄存器的顺序问题

捕获和更新(溢出)中断同时挂起时,读CCR和清标志的顺序会影响结果。稳妥的做法是在捕获中断里先判断并处理溢出,再读捕获值,或者读之前短暂关一下中断。这个细节手册里提得不多,但实际测低频信号时经常碰到,表现为偶尔有一个周期的测量值偏差很大。我现在的习惯是捕获中断里统一用"先读状态、再读数据、最后清标志"的顺序,实测稳定。

7. 我个人在实际操作中的几点体会

定时器这块,我最大的感受是别怕看寄存器。库函数封装得再好,出问题时你还是得回到寄存器层面看每一位的含义。我建议初学时用CubeMX生成一版配置,然后对照生成的代码逐个寄存器看它配了什么、为什么这么配,看几遍就通了。比单纯抄例程强太多。

另外,示波器是定时器调试的半条命。PWM看波形、捕获看信号、编码器看AB相,没有示波器全靠猜,效率低还容易误判。手边没示波器的话,至少弄个逻辑分析仪,几十块的那种测数字信号够用了。

最后分享个小技巧:调定时器参数时,我习惯把PSC和ARR的推算过程写在注释里,比如"72MHz/72=1MHz, 1000计数=1kHz",过几个月回来看代码,一眼就知道当时怎么想的。定时器参数这东西,不写注释过阵子自己都看不懂,这个习惯帮我省了很多重新推导的时间。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进