☰
STM32定时器寄存器剖析:从PSC/ARR到PWM与捕获
2026/10/11 1:09:10 网站建设 项目流程

1. 定时器到底是个什么东西

前四章我们把GPIO和时钟树聊完了,我以为大家的寄存器之路已经顺了大半。结果第五章,定时器一来,好多人又开始打退堂鼓。我不止一次看到有人问我:"为什么我的延时函数在跑,但是PWM死活没输出?"或者"定时器中断进不去,是不是我烧录姿势不对?"

先给你吃颗定心丸:STM32的定时器,玩来玩去就是几个核心寄存器在互相配合——预分频器PSC、自动重装载寄存器ARR、计数器CNT、控制寄存器CR1。把这四兄弟搞明白,你就已经掌握大半边天了。

这一章,我用白话带你把这些寄存器挨个过一遍。你先放下对"计数""溢出""比较匹配"这些术语的恐惧。它们本质上是同一件事:让一个计数器在特定条件下产生一个事件,然后让这个事件去触发后续动作。这个动作可以是中断、可以是翻转引脚、也可以是启动一次DMA搬运。

1.1 从"数数"开始理解定时器

定时器最朴素的本质是一个数数的东西。你给它一个时钟源,它每个时钟沿把计数值加一。这个值存在CNT里。

你可以拿它当厨房定时器:到了设定时间就响铃(产生更新事件)。也可以拿它当停车场的人肉计数器:每进来一辆车,数字加一(外部脉冲计数)。还可以拿它当高速公路测速仪:两段固定距离中间测时间(输入捕获)。

STM32芯片里一般有好几类定时器:基本定时器(比如TIM6、TIM7)、通用定时器(TIM2~TIM5)、高级定时器(TIM1、TIM8)。它们的寄存器大同小异,只是能力不同。基本定时器只有计数功能,通用定时器加了输入捕获和输出比较,高级定时器又额外带了互补输出和刹车功能。你先玩好通用定时器,其他的都是锦上添花。

1.2 先背出这张"寄存器地图"

在翻寄存器之前,先把下面这张表存进脑子里。这章后面每一个字都在围绕这张表说事。

寄存器中文外号核心作用
PSC预分频器寄存器把输入时钟先分频,相当于把面包切成片再数
ARR自动重装载寄存器计数的终点,溢出周期的决定者
CNT计数器寄存器当前数到哪了,实时可读可写
CR1控制寄存器1总开关(CEN位)、计数方向、对齐模式
SR状态寄存器各种事件标志位,UIF是最常用的更新标志
DIER中断使能寄存器决定哪些事件能进中断
CCMR捕获/比较模式寄存器通道工作在捕获还是输出模式、PWM模式选择
CCER捕获/比较使能寄存器通道输出/捕获的总开关
CCRx捕获/比较寄存器比较值或捕获到的瞬间计数器快照

好,地图有了。下面挨个解释它们为什么那样设计。

2. 核心寄存器逐个击破——白话版释义

2.1 PSC与ARR:预分频和自动重装载的联合作战

先说你平时用得最多的两个:PSC和ARR。

PSC(Prescaler),预分频器。它把定时器驱动的原始时钟信号先放慢。比如你定时器时钟是72MHz,你希望它每1微秒数一次数,那就用72分频。关键是:实际分频系数是PSC寄存器的值加1。你写PSC=71,那就是72分频。这个"加1"的坑,几乎每个新手都要踩一次。

ARR(Auto-Reload Register),自动重装载寄存器。它决定了计数器数到哪里算"到头"。这个值同样是"加1"思维:你写ARR=999,计数器会从0数到999,一共数1000个计数值。到头的瞬间,产生一个更新事件UIF,然后计数器自动跳回0重新开始。

我当时学的时候用了这个类比才彻底记住:PSC是决定"一格刻度"是多少秒,ARR是决定"表盘上一共有多少个刻度"。一格的秒数乘以刻度总数,就是整个计时的周期。

算一笔账:时钟72MHz,PSC=71(分频后1MHz,周期1微秒),ARR=999。定时器周期 = 1000 × 1微秒 = 1毫秒,也就是1ms进一次更新中断。这个公式你值得钉在桌前:

定时器溢出周期 = (PSC + 1) × (ARR + 1) / 定时器输入时钟频率

为什么PSC和ARR都要加1再乘?因为寄存器从0开始计。你写1,代表的是2个计数值;你写999,代表的是1000个计数值。这个思维如果不扭转过来,后面算PWM频率、算捕获时间都会出错。

2.2 CNT、SR、DIER:看看现在数到哪了、发生了什么事

CNT寄存器不需要你写太多东西,它更多是被读的。调试的时候,你可以在调试器里实时观察CNT的跳动,来确认定时器时钟到底有没有跑起来。它是一个24位或16位的寄存器(具体看你用的芯片型号),数到ARR就回零。

SR寄存器,全称状态寄存器。它里面每一位代表一种事件:UIF更新事件、CC1IF捕获/比较事件、TIF触发事件等等。你要在中断服务函数里清除UIF标志,方法永远是往UIF位写0。很多人写成"读SR寄存器然后清标志",其实正确做法是直接给SR的对应位写0,或者先读SR再读CNT也能走走硬件路径清除部分标志。

DIER是中断使能寄存器。这玩意儿是个"门卫":SR里的标志产生了,但如果DIER对应的位没打开,中断请求根本不会发到NVIC那里。所以排查"定时器为什么进不了中断"的时候,先检查两个地方:DIER对应位有没有置1,NVIC里定时器中断通道有没有使能。

2.3 CR1与CCMR:总开关和通道工作模式

CR1寄存器对初学者来说,需要关注的其实就三位:

  • CEN(bit 0):定时器总开关。置1启动计数,清0停止。
  • DIR(bit 1):计数方向。0是向上计数,1是向下计数。做标准定时用向上就对了。
  • CMS(bit 5~6):对齐模式。做中心对齐PWM的时候选"模式1"或"模式2"或"模式3",不用PWM就别管它。

还有一个很容易被忽略的ARPE位(bit 7)。这个位决定ARR是否带"影子寄存器"。如果ARPE=1,你对ARR的修改会在下一次更新事件才真正生效;如果ARPE=0,改动立即生效。预分频器PSC有同样的影子机制。影子寄存器这个设计是为了防止你在计数器运行途中改参数,导致一个周期突然变得不伦不类。如果你只是初始化阶段配置一次,这个位可以不关心;但你要在运行中动态调PWM频率,就得明白它。

CCMR寄存器是通道模式寄存器。这个寄存器的名字取得特别容易误导人——它既管捕获(Capture)也管比较(Compare)。你可以通过它的OCxM位(输出比较模式位)来选择PWM模式1还是模式2,也可以通过ICxPSC位(输入捕获分频器)来设置捕获边沿分频。

后面讲到PWM和输入捕获的时候,我们再细抠这两个寄存器的位。

2.4 配置序列:一个规范到能背下来的顺序

寄存器学到这里,我猜你已经手痒想动手了。但在写代码之前,先把配置顺序捋顺。我总结了一个"五步走",照着写基本不会漏:

  1. 打开RCC里对应定时器的时钟(比如TIM2的时钟使能位)。
  2. 设置PSC和ARR,确定定时周期。
  3. 配置DIER,决定哪些事件能进中断(或者不依赖中断,轮询SR)。
  4. 配置NVIC,使能这个定时器的中断通道并设置优先级。
  5. 最后,把CR1的CEN置1,启动定时器。

为什么CEN放最后?因为你前面所有配置都是在给定时器"上膛",“开保险”的动作必须放到最后一步。如果先启动定时器再改参数,可能会让第一个计数周期使用错误的PSC/ARR值。

3. 实操演示:从零配置一个1ms定时中断

我拿某个开发板上最常见的TIM2来演示,芯片主频72MHz。目标:每1ms进一次定时器中断,中断里翻转一个IO口,实现LED以500ms周期闪烁(即翻转LED引脚,亮250ms灭250ms就近似1Hz,如果每次中断翻转一次,则翻转周期2ms,亮500ms灭500ms需要翻转500次,之后你再根据自己的需求延长)。

3.1 先算分频系数和重装载值

已知条件:定时器输入时钟72MHz。目标周期1ms。

先用前面那条公式反推:

(PSC + 1) × (ARR + 1) / 72,000,000 = 0.001秒

选项太多了,怎么选?我建议先把PSC定到让计数器时钟变得"好算"。把PSC设为71,那计数时钟就是72MHz/72 = 1MHz。1MHz下,数1000个计数值就是1ms。

所以:

  • PSC = 71
  • ARR = 999
  • 中断周期 = (71 + 1) × (999 + 1) / 72,000,000 = 1ms

如果你想让PWM的频率刚好是20kHz,你就要反过来先定ARR和PSC的组合。这就是为什么我反复强调"先理解公式,再碰寄存器"。

3.2 直接操作寄存器的初始化代码

下面这段代码,全部用寄存器操作,不依赖标准外设库。

/* 使用TIM2,输出比较/中断方式实现1ms定时 */ void TIM2_Init(void) { /* 1. 开启TIM2时钟 */ RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; /* 2. 配置预分频器:72MHz / (71+1) = 1MHz */ TIM2->PSC = 71; /* 3. 自动重装载值:计数1000个,得到1ms周期 */ TIM2->ARR = 999; /* 4. 更新事件产生时,影子寄存器才加载PSC/ARR的新值(可选) */ TIM2->CR1 |= TIM_CR1_ARPE; /* 5. 清一次更新标志位,防止刚初始化就进中断 */ TIM2->SR &= ~TIM_SR_UIF; /* 6. 使能更新中断 */ TIM2->DIER |= TIM_DIER_UIE; /* 7. 配置NVIC:使能TIM2全局中断,抢占优先级设为2 */ NVIC_SetPriority(TIM2_IRQn, 2); NVIC_EnableIRQ(TIM2_IRQn); /* 8. 启动定时器 */ TIM2->CR1 |= TIM_CR1_CEN; }

配置完之后,中断服务函数长这样:

void TIM2_IRQHandler(void) { /* 先判断是不是更新中断 */ if (TIM2->SR & TIM_SR_UIF) { /* 这里必须清除标志位,注意是写0 */ TIM2->SR &= ~TIM_SR_UIF; /* 用户代码:翻转LED引脚 */ GPIOA->ODR ^= GPIO_ODR_ODR0; } }

3.3 中断标志位清除顺序的讲究

上面代码里有一行看着不起眼却极其重要:

TIM2->SR &= ~TIM_SR_UIF;

这行必须在用户代码之前清掉,还是之后清?我的建议是先清再执行用户逻辑。原因很简单:如果用户逻辑耗时较长,而期间定时器又产生了新的UIF标志,你在末尾清标志可能会把新产生的那一次事件也清掉,导致丢一次中断。

我自己就吃过这个亏——把标志清除放在中断函数最后,结果用户代码里有个延时操作,中断到来时标志已经被置位,最后清标志把新的一并清掉了,导致中断周期看起来忽长忽短。改到前面之后,问题立刻消失。

还有一个小技巧:有些芯片手册提到,读SR寄存器再读CNT,硬件会自动清除UIF标志。但这个行为跟具体的芯片版本有关,手动写0清除才是最保险的写法。千万别依赖自动清除这个"彩蛋"。

4. PWM输出和输入捕获:两大高频需求的寄存器差异

定时中断只是定时器最基础的用法。真正让定时器发光发热的,是它的**输出比较(PWM)和输入捕获(测量脉冲)**功能。这两个场景,同样只需要改几个寄存器位。

4.1 从纯定时到输出PWM,只需动四个位

我们已经能产生1ms中断了。如果想输出一个频率1kHz、占空比75%的PWM波,怎么改?

先理清思路:PWM要的不是"到了就中断",而是"当计数器CNT达到某个特定值CCR的时候,翻转/设置输出引脚电平"。

所以,你从"定时器中断"切换到"PWM输出"模式,本质上做了四件事:

  1. 在CCMR里把通道设为PWM模式1(OCxM位=110)。
  2. 在CCER里把对应通道的输出使能打开(CCxE=1)。
  3. 设置CCR寄存器的值:占空比 = CCR / (ARR + 1)。
  4. 配置对应的GPIO引脚为复用功能输出。

以TIM2的CH1为例,产生20kHz PWM,占空比50%:

void TIM2_PWM_Init(void) { RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; /* PA0 配置为复用输出(以某个具体芯片为例) */ GPIOA->MODER &= ~GPIO_MODER_MODER0_Msk; GPIOA->MODER |= (GPIO_MODER_MODER0_1); // 0b10: 复用功能 GPIOA->AFR[0] &= ~GPIO_AFRL_AFSEL0_Msk; GPIOA->AFR[0] |= (0x01) << 0; // 复用功能AF1对应TIM2_CH1 /* 预分频:72MHz / (1+1) = 36MHz,太宽了,不适合20kHz,下面重新算 */ /* 如果你坚持用PSC=1,则计数时钟36MHz,ARR=1799时,频率20kHz */ TIM2->PSC = 1; TIM2->ARR = 1799; /* 清计数器 */ TIM2->CNT = 0; /* 通道1输出比较模式,PWM模式1 */ TIM2->CCMR1 |= (0x6 << 4); // OC1M = 110 /* 使能CH1输出 */ TIM2->CCER |= TIM_CCER_CC1E; /* 设置50%占空比:CCR = (ARR+1)/2 = 900 */ TIM2->CCR1 = 900; /* 启动 */ TIM2->CR1 |= TIM_CR1_CEN; }

注意我这个例子里的PSC=1是为了让你看清楚"PSC和ARR如何搭配出频率",实际工程中我更推荐先用PSC把计数周期设置为微秒量级(比如PSC=71),再让ARR去精确控制频率。这样调起来更直观。

4.2 输入捕获:怎么用寄存器抓到频率与脉宽

再说输入捕获。这个功能可以测量外部信号的频率、周期、脉宽。它的原理很简单:通道引脚出现指定边沿(上升沿/下降沿)时,硬件会瞬间把CNT当前的数值复制到CCRx寄存器,并置位CCxIF标志。

你只需要做三件事:

  1. 在CCMR里设置输入捕获滤波器和分频——这其实是用ICxPSC位设置"要计数1个边沿、2个边沿还是4个边沿才触发捕获"。
  2. 在CCER里设置捕获极性,也就是捕获上升沿还是下降沿。
  3. 在CCER里使能捕获通道(CCxE=1)。

测量频率的思路:捕获到第一个上升沿时,读CCR1记为t0;捕获到第二个上升沿时,读CCR1记为t1。把两次的差值换算成时间(乘以计数时钟周期),倒数就是频率。关键前提是:两次捕获之间,CNT不能被重置,否则差值就没有意义了。

这里有个读寄存器的坑:在32位的定时器上,CCR寄存器是32位的。但在某些16位定时器上,CCR寄存器是16位。如果你在捕获中断里先读高16位再读低16位,在两次读之间如果发生了捕获,数据就会错位。解决方法是读两次,比较结果是否一致,或者直接用一次32位读操作。

4.3 高级定时器那些"进阶位"要心里有数

如果你用的是TIM1或者TIM8这类高级定时器,本章的配置序列依旧适用,但有三个额外注意点:

第一,它们的重复计数寄存器RCR。默认情况下RCR=0,也就是每次更新事件都产生中断。如果你把RCR设置为N,那定时器要溢出N+1次才产生一次更新事件。这在做长定时时很有用。

第二,刹车寄存器BDTR。高级定时器的PWM输出在默认情况下是被禁止的,你必须设置BDTR的MOE主输出使能位为1,PWM信号才能真正输出到引脚。我见过有人配置TIM1的PWM,查了半天查不出问题,就是漏了这个MOE位。

第三,高级定时器的通道通常有互补输出CHxN,用来驱动全桥电路。如果你用不到,完全不用管它。但如果你有一天要做一个带刹车功能的电机驱动,这套互补输出和死区插入的寄存器位就很值钱了。

5. 常见问题速查:定时器调试避坑实录

5.1 定时器完全没反应

优先级最高的检查项永远是RCC时钟有没有开。我见过太多人把NVIC配好了、中断函数写了,结果忘了给RCC->APB1ENR对应位置1。定时器连时钟都没有,后面全是白搭。

再检查CR1的CEN是否为1。这个位常常被调试工具复位成0,或者你在别的代码里误操作把整个CR1覆盖了。我建议在初始化函数末尾加一句读回校验,打印CR1的值,确认CEN=1。

5.2 改了ARR和PSC,但新参数不生效

这个现象十有八九是影子寄存器引起的。PSC和ARR都有影子缓存,只有当更新事件到来的时候,新值才会被加载到真正参与的寄存器里。如果你在跑起来之后动态改ARR,却没等到下一个更新事件就急着去读,读到的还是旧值。

解决方式两种:要么在修改前先产生一次软件更新事件(EGR寄存器的UG位),要么等到中断服务函数里同步修改。我习惯在中断里改ARR做变频控制,因为这个时机天然就对齐了更新边界。

5.3 中断一直进来,根本停不下来

先看一眼SR寄存器,是不是UIF一直被置1。如果是,说明清除标志的代码没跑到或在清除之前又有新的更新事件产生。我的排查顺序是这样的:

  1. 中断函数开头第一行就是清标志,别的什么都不做,看中断频率是否恢复正常。
  2. 如果恢复正常,说明用户代码阻塞太长,把清标志的时机往后拖了。
  3. 如果还是疯狂触发,检查DIER有没有误配置,比如不光是UIE,连CCIE也一起开了。

还有一个很隐蔽的问题:最后启动定时器之前,忘了清一次SR。初始化时SR里的UIF位可能是个随机值,一使能中断就会立刻进一次中断。我写代码的习惯就是CEN之前必须清一次UIF。

下面这张速查表是我每次帮人远程调定时器问题都会发过去的那张:

现象优先检查项常见坑
定时器不计数RCC时钟、CR1.CEN忘了开时钟
定时周期不对PSC/ARR计算忘了加1
中断不触发DIER、NVIC中断通道没使能
PWM无输出CCER.CCxE、GPIO复用、BDTR.MOE(高级定时器)GPIO没配复用
更新中断风暴SR清标志顺序用户逻辑耗时太长

5.4 读CNT或CCR的值偶尔跳变

如果你在应用中需要高精度测量,建议把CNT读两次,取一致的那次作为结果。因为你在读一个正在自增的寄存器时,完全可能遇到"读低16位的前一瞬间刚好进位到高16位"这种边界情况。虽然概率很低,但做工业控制的时候,一次误读可能导致整个控制环跳变,这个代价实在不值得。

6. 给自己的一个学习路径建议

这章内容不少,但回头想想,整个定时器真正需要你动手配置的位,其实不超过20个。比GPIO只多一点点,只是它们组合在一起时,能力就完全不同了。

我强烈建议你今天找一块板子,打开一个空的工程,手写一遍这段初始化代码。不要复制粘贴,不要用CubeMX生成。你先算好PSC和ARR,然后逐行写寄存器配置,最后在中断里把LED点亮、熄灭。完成这一步,你对定时器的信心会上一个台阶。

写完这章,我突然想起自己当初第一次点亮LED时,用的是别人封装好的函数,三行代码调完就跑了,完全没搞懂背后发生了什么。后来被一个问题逼到不得不翻开参考手册,才意识到:外设库帮你省掉的每一行,都是你之后理解问题的坎。现在你把这些坎一个一个迈过去,以后再看任何基于定时器的例程,你会觉得他们写的全是"废话",因为你连寄存器层面都懂透透的了。

动手去改,不要怕烧板子。板子烧不了,知识不烧可学不会。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询