news 2026/11/3 5:30:21

STM32定时器三大实战模式:PWM输出、输入捕获与编码器模式详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32定时器三大实战模式:PWM输出、输入捕获与编码器模式详解

很多人在把 STM32 的定时器玩到能跑通例程之后,就卡在了一个很尴尬的位置:点灯、延时、串口收发都熟了,但一碰到“让电机按指定速度转”“读一个外部脉冲的频率”“用旋钮调亮度”这类需求,脑子里就只剩一个模糊的“好像要用定时器”,具体怎么配、配哪个通道、为什么这么配,全凭抄。我当年也是这样,PWM 能出波形,但占空比一改电机就抖;输入捕获能进中断,但测出来的频率总差那么一点;编码器模式更玄学,接上能读,方向偶尔反。后来把这些坑一个个踩过来,才发现问题几乎都出在对定时器内部那套“计数—比较—捕获”机制的理解不够透,而不是代码写错了。

这篇内容就是把我这些年对 STM32 定时器三个高频用法——PWM 输出、输入捕获、编码器模式——的理解和实操经验摊开讲。它适合已经能跑通基础外设、想真正把定时器用明白的嵌入式开发者,也适合正在做电机控制、传感器测速、人机交互旋钮这类项目、需要一套能直接抄作业的配置思路的人。我不会只给你寄存器值,而是把每个参数背后的“为什么”讲清楚,这样你换个芯片、换个频率也能自己推。下面所有代码以常见的 STM32F1/F4 系列 HAL 库为主线,寄存器层面的原理通用,换系列时注意时钟树和定时器位宽差异即可。

1. 先把定时器当成一台“会数数的机器”

在碰 PWM 和捕获之前,必须先把定时器的心智模型建立对。很多人配定时器是“照着 CubeMX 填”,填完不知道每个格子什么意思,一出问题就抓瞎。其实定时器没那么神秘,它的本质就是一台按固定节奏数数的机器,数到某个值就干一件事。你把这件事想清楚了,后面三个模式全是它的变体。

1.1 预分频、计数器和自动重装:三个参数决定一切

定时器的核心是三个东西:预分频器(PSC)、计数器(CNT)、自动重装寄存器(ARR)。我用一个生活化的类比:假设你有一个秒针在走的钟,PSC 决定“每几秒滴答一次”,CNT 是当前滴答到第几下,ARR 是“滴答到多少下就归零重来”。

  • PSC(预分频):把定时器的输入时钟先除以 (PSC+1)。比如 72MHz 的时钟,PSC 设 71,那计数器每 1μs 才加一。
  • CNT(计数器):实际在累加(或递减)的那个值,从 0 数到 ARR。
  • ARR(自动重装):数到这个值就产生溢出事件,然后 CNT 归零重新开始。

由此可以推出两个最常用的公式,这两个公式你必须刻在脑子里:

计数频率 = 定时器时钟 / (PSC + 1) 溢出频率(即中断/事件频率)= 计数频率 / (ARR + 1)

举个具体例子。系统时钟 72MHz,定时器挂在 APB1 上但经过倍频后仍是 72MHz,我想让定时器每 1ms 溢出一次(也就是 1kHz 的中断频率)。先定 PSC:让计数频率为 1MHz,即每 1μs 计一次,那 PSC = 72 - 1 = 71。再定 ARR:要 1ms 溢出,就是数 1000 次,ARR = 1000 - 1 = 999。这两个减一是新手最容易漏的,因为寄存器是从 0 开始计数的,写 71 实际是 72 分频。

提示:PSC 和 ARR 都是 16 位寄存器(部分高级定时器 ARR 可到 32 位),所以 PSC 最大 65535,意味着最小分频比是 1、最大是 65536。如果你算出来的 PSC 超过 65535,说明分频比太大,得换思路,比如用更高的时钟源或者级联定时器。

这里有个我踩过的坑值得说:很多人以为改了 ARR 就能立刻改变周期,其实不然。在向上计数模式下,如果定时器正在运行,新写入的 ARR 值要等到下一次溢出事件才会真正生效(除非你使能了 ARR 预装载)。这就是为什么有些人在运行中动态改频率,发现第一次改没反应、第二次才对。解决办法是使能TIMx_CR1里的 ARPE 位(自动重装预装载使能),让新值在更新事件时统一加载,行为就确定了。

1.2 时钟源从哪来:别在时钟树上翻车

定时器不是凭空数数的,它得有个时钟源。STM32 的定时器时钟来自 APB 总线,但这里有个容易忽略的细节:APB 预分频不为 1 时,定时器时钟会被自动倍频。以 F1 系列为例,APB1 最高 36MHz,如果 APB1 分频系数是 2,那定时器实际拿到的是 36×2 = 72MHz。这个倍频是硬件自动做的,你在 CubeMX 里看到的“APB1 Timer Clocks”才是定时器真正用的频率,别拿 APB1 的外设时钟去算,会差一倍。

我见过有人算 PWM 频率怎么算都对不上,最后发现是把 36MHz 当成定时器时钟了。所以配定时器前,第一件事是打开时钟树确认那个“Timer Clocks”的数字,而不是想当然。F4 系列同理,APB1 是 42MHz、倍频后 84MHz,APB2 是 84MHz、倍频后 168MHz,具体看你芯片型号。

另外,定时器还可以选外部时钟源(比如外部引脚输入的脉冲、其他定时器的溢出),这在做级联或者外部计数时有用,但日常 PWM 和捕获基本都用内部时钟,先把这个搞扎实。

1.3 高级定时器和通用定时器的差别,别选错

STM32 的定时器分好几档:基本定时器(TIM6/TIM7)、通用定时器(TIM2~TIM5)、高级定时器(TIM1/TIM8)。做 PWM 输出时,通用定时器一般够用,但如果你要驱动三相电机、需要互补输出和死区插入,就得上高级定时器。输入捕获和编码器模式通用定时器都支持,但要注意编码器模式只有带编码器接口的定时器才有(一般是 TIM2~TIM5 这类通用定时器,具体看手册的复用功能表)。

选型时我的经验是:先看这个定时器的通道数够不够(PWM 要几个通道、捕获要几个),再看有没有特殊功能(互补、死区、编码器接口),最后看引脚有没有被其他外设占用。别一上来就挑 TIM1,它虽然功能全但引脚往往和别的复用冲突,通用定时器反而更省心。

2. PWM 输出:不只是“配个占空比”那么简单

PWM 是定时器最常用的功能,但真正把它用稳的人不多。很多人停留在“改 CCR 改占空比”的层面,一旦涉及频率动态调整、多通道同步、死区控制就露怯。这一节我把 PWM 的生成机制、参数计算和几个实战坑讲透。

2.1 PWM 到底是怎么“比较”出来的

PWM 的本质是计数器和一个比较寄存器(CCR)比大小。以向上计数为例:CNT 从 0 数到 ARR,同时每个计数周期都和 CCR 比较一次,CNT < CCR 时输出一种电平,CNT ≥ CCR 时输出另一种电平。于是输出的高电平时间正比于 CCR,占空比 = CCR / (ARR+1)。

这里的关键理解是:ARR 决定频率,CCR 决定占空比,两者独立。频率 = 计数频率 / (ARR+1),占空比 = CCR / (ARR+1)。所以你想改频率就动 ARR(或 PSC),想改占空比就动 CCR,互不干扰。这个分离设计是 PWM 好用的根本原因。

但有个细节:PWM 模式还分 PWM1 和 PWM2。PWM1 是“CNT < CCR 时输出有效电平”,PWM2 反过来。而“有效电平”是高还是低,又由输出极性位(CCER 里的 CCxP)决定。这两层组合起来有四种情况,新手经常在这里绕晕。我的建议是:先固定用 PWM1 + 高电平有效,把逻辑理顺,需要反相时再单独调极性,别两个一起改,否则调半天不知道哪个起了作用。

2.2 频率与占空比的参数推导,给一套能直接套的算法

实际项目里最常见的需求是“我要一个 20kHz 的 PWM,占空比可调”。怎么定 PSC 和 ARR?我的推导顺序是这样的:

  1. 先确定定时器时钟,假设 72MHz。
  2. 定 PWM 频率目标,比如 20kHz。
  3. 算出“一个 PWM 周期需要多少个计数”:72MHz / 20kHz = 3600 个计数。
  4. 这 3600 就是 (ARR+1),所以 ARR = 3599。
  5. PSC 设 0(不分频),因为 3600 在 16 位范围内(最大 65535),没必要分频。

如果目标频率很低,比如 50Hz(舵机常用),72MHz / 50 = 1,440,000,超过 16 位了,这时就得用 PSC 先分频。比如 PSC = 71(计数频率变 1MHz),那 1MHz / 50 = 20000,ARR = 19999,正好在范围内。核心原则是:优先保证 ARR 能提供足够的占空比分辨率,再用 PSC 把频率压下来。因为 ARR 越大,占空比能调的档位越多(分辨率 = 1/(ARR+1)),舵机这种需要精细角度的,ARR 尽量给大。

下面是一段 HAL 库的 PWM 初始化骨架,我加了注释说明每个参数为什么这么填:

TIM_HandleTypeDef htim3; TIM_OC_InitTypeDef sConfigOC = {0}; htim3.Instance = TIM3; htim3.Init.Prescaler = 71; // 72MHz/72 = 1MHz 计数频率 htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 19999; // 1MHz/20000 = 50Hz htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; // 动态改频率必备 HAL_TIM_PWM_Init(&htim3); sConfigOC.OCMode = TIM_OCMODE_PWM1; // 固定 PWM1,逻辑清晰 sConfigOC.Pulse = 1500; // 初始占空比 1500/20000 = 7.5% sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);

注意AutoReloadPreload我特意使能了,这就是前面说的动态改频率不生效问题的解药。运行中改占空比用__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, newCCR),改频率用__HAL_TIM_SET_AUTORELOAD,都很方便。

2.3 多通道同步与死区:驱动电机的必修课

单个 PWM 通道好办,但驱动直流电机 H 桥或者三相无刷电机时,你会遇到两个进阶需求:多通道同步和死区插入。

多通道同步指的是同一个定时器的多个通道共享同一个 CNT 和 ARR,所以它们天然同频同相,只是各自的 CCR 不同。这意味着你用 TIM3 的 CH1~CH4 出四路 PWM,频率自动一致,占空比各自独立,非常适合驱动多路负载。但要注意:不同定时器之间默认是不同步的,如果你需要两个定时器的 PWM 严格对齐,得用主从模式(一个定时器作为主,输出触发信号给从定时器)。

死区插入是高级定时器(TIM1/TIM8)的专属功能,用于互补 PWM 输出。所谓互补,就是一路高时另一路必须低,中间要留一段“两路都低”的死区时间,防止 H 桥上下管同时导通烧管子。死区时间由TIMx_BDTR寄存器的 DTG 位配置,计算稍微绕,它不是一个简单的线性值,而是分段的。以常见的配置为例,死区时间 = DTG 值 × 计数周期,但不同 DTG 区间的倍率不同,具体要查参考手册的“死区时间计算”表。我的经验是:先用 CubeMX 算好死区时间对应的 DTG 值,再手动核对一遍,因为不同系列的编码规则有细微差别,烧管子代价太大,宁可多验一次。

注意:死区时间不是越大越好。太大损失有效占空比、降低输出能力,太小起不到保护作用。一般根据 MOS 管的开关时间和驱动电路延迟来定,常见在几百纳秒到一两微秒之间。

2.4 我踩过的 PWM 坑:占空比抖动与最小脉宽

说两个实战里真金白银换来的教训。第一个是占空比抖动:有次做 LED 调光,发现低亮度时灯会闪。排查半天,原因是 CCR 设得太小(比如 ARR=1000,CCR=1),此时占空比 0.1%,但定时器在 CNT=0 和 CNT=1 之间的比较存在一个计数周期的量化误差,加上中断或其他任务干扰,输出就不稳。解决办法是保证最小 CCR 不要低于某个阈值,或者干脆提高 ARR 提升分辨率。LED 调光我一般 ARR 给到 1000 以上,低亮度用更高的分辨率去逼近。

第二个是最小脉宽限制。有些负载(比如某些舵机、某些驱动芯片)对最小高电平时间有要求,PWM 占空比太小可能根本驱动不起来。这时候不能只看占空比百分比,要算绝对时间:高电平时间 = 占空比 × 周期。比如 50Hz 舵机,周期 20ms,你要 0.5ms 的最小脉宽,那 CCR 至少得是 ARR 的 2.5%。这些边界值在项目初期就要算清楚,别等调不出来再回头找。

3. 输入捕获:把外部世界的脉冲“读”进来

PWM 是往外输出,输入捕获则是往里读。它的典型场景是测频率、测脉宽、测占空比、解码红外或超声波信号。相比 PWM,输入捕获的配置更绕,因为它涉及边沿检测、捕获寄存器和中断/ DMA 的配合,稍不注意就丢数据或者测不准。

3.1 捕获的硬件链路:从引脚到 CCR 寄存器

输入捕获的硬件路径是这样的:外部信号从定时器通道引脚进来,先经过边沿检测器(你配置成上升沿、下降沿还是双边沿),检测到指定边沿时,硬件会瞬间把当前 CNT 的值锁存到捕获比较寄存器 CCR 里,同时置位相应的捕获标志、可以触发中断。整个过程是硬件完成的,CPU 只需要事后去读 CCR。

这个“硬件锁存”是关键,它保证了捕获的时间精度不受软件延迟影响。你想想,如果靠中断里读 CNT,中断响应有延迟、有抖动,测出来就不准;而硬件在边沿到来的那一刻就把 CNT 存下来了,精度只取决于定时器的计数分辨率。所以输入捕获的精度上限 = 计数周期,你把计数频率设成 1MHz,那分辨率就是 1μs,测 1kHz 信号误差在千分之一量级,够用了。

捕获模式还分“直接捕获”和“间接捕获”。直接捕获就是上面说的,边沿来了锁存 CNT。间接捕获(PWM 输入模式)更聪明,它用两个通道配合,一个测周期(上升沿到上升沿),一个测高电平时间(上升沿到下降沿),硬件自动算出占空比,适合测 PWM 信号。这个模式在做遥控接收、传感器解码时特别好用,后面会讲。

3.2 测频率的两种思路:测周法 vs 计频法

测一个信号的频率,本质上有两条路,选错了精度会差很多。

测周法:测一个完整周期的时间 T,频率 = 1/T。做法是捕获两个相邻上升沿,两次 CCR 之差就是周期对应的计数值,乘以计数周期就是时间。这种方法适合低频信号,因为周期长、计数值大,相对误差小。比如测 100Hz 信号,周期 10ms,1MHz 计数下差值是 10000,误差 1 个计数就是万分之一。

计频法:固定一个闸门时间(比如 1 秒),数这段时间里来了多少个脉冲,脉冲数就是频率。这种方法适合高频信号,因为高频下测周法的差值太小、误差大,而计频法在固定时间内数到的脉冲多,相对误差小。

实际项目里怎么选?我的经验分界线大概在几十 kHz:低于这个用测周法,高于这个用计频法(或者用定时器的外部计数模式)。当然还要看你的计数频率,如果计数频率很高(比如 10MHz),测周法的适用范围也能往上推。核心是保证被测周期对应的计数值足够大,至少几百以上,误差才可控。

下面是一段测周法的捕获配置和中断处理骨架:

// 捕获回调:记录两次上升沿的 CCR 差值 volatile uint32_t capture1 = 0, capture2 = 0; volatile uint8_t edge_cnt = 0; volatile uint32_t period_ticks = 0; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) { if (edge_cnt == 0) { capture1 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); edge_cnt = 1; } else { capture2 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 处理计数器溢出:如果 capture2 < capture1 说明跨了一次溢出 if (capture2 >= capture1) period_ticks = capture2 - capture1; else period_ticks = (0xFFFF - capture1) + capture2 + 1; edge_cnt = 0; } } }

这段代码里那个“跨溢出”的判断是重点,也是新手最容易漏的。定时器是 16 位的,数到 65535 就归零,如果两次捕获之间正好跨过了归零点,直接相减会得到一个巨大的错误值。所以必须判断capture2 < capture1的情况,用补数的方式算真实差值。我见过太多人测低频信号时好时坏,就是栽在这里。

3.3 PWM 输入模式:一个通道测周期,一个测占空比

如果你要测的是一个 PWM 信号(比如遥控接收机输出的方波),用 PWM 输入模式最省事。它的原理是:同一个输入信号同时接到两个捕获通道(比如 CH1 和 CH2),CH1 配成上升沿捕获测周期,CH2 配成下降沿捕获测高电平时间,硬件自动把结果存到两个 CCR 里,你读出来一除就是占空比。

配置上有几个要点:两个通道要映射到同一个引脚(通过输入选择寄存器 TIxSEL),从模式设成“复位模式”(这样每次上升沿到来时 CNT 自动清零,方便测周期),然后使能两个通道的捕获。这样每次信号周期结束,你就能同时拿到周期值和高电平值。

这个模式我在做遥控信号解码时用得最多,因为它把“测周期”和“测脉宽”两件事一次搞定,CPU 负担小。但要注意:从模式的复位触发源要选对,选上升沿触发复位,否则 CNT 清零时机不对,测出来的周期就乱了。这个配置在 HAL 里对应TIM_SlaveConfigTypeDef的InputTrigger和SlaveMode两个字段,别填反。

3.4 捕获精度与抗干扰:几个容易被忽略的细节

输入捕获测不准,除了溢出问题,还有几个常见原因。第一是信号抖动,机械按键、长线传输的脉冲边沿会有毛刺,硬件捕获会把毛刺也当成边沿。解决办法是开启输入滤波(TIMx_CCMR里的 ICxF 位),设置一个滤波采样频率和次数,把窄于若干周期的毛刺滤掉。滤波不是越强越好,太强会把真实的高频信号也滤掉,要根据信号频率和噪声特性折中。

第二是中断延迟导致的丢捕获。如果你在中断里做太多事,下一次边沿来了还没处理完,捕获标志会被覆盖,数据就丢了。我的做法是中断里只做“读 CCR、存数组、清标志”这三件事,复杂计算放到主循环里做。如果信号频率很高,中断太频繁,就上 DMA——让 DMA 把每次捕获的 CCR 自动搬到内存数组里,CPU 定期批量处理,这样几乎不占 CPU。

第三是计数频率的选择。前面说精度上限是计数周期,但计数频率也不是越高越好。太高了 16 位计数器很快就溢出,溢出处理变复杂;太低了精度不够。我的经验是让“被测信号的一个周期”对应几百到几万个计数比较合适,既保证精度又不至于频繁溢出。

4. 编码器模式:让定时器自己“读懂”旋转

编码器模式是定时器里比较特殊的一个功能,它把定时器变成了一个正交解码器,专门读增量式编码器的 A/B 两相信号。这个功能在电机测速、旋钮调节、位置检测里用得极多,而且用好了几乎不占 CPU——硬件自动根据 A/B 的相位关系加一或减一,你只管定期读 CNT 就行。

4.1 正交信号的相位逻辑:为什么能自动判方向

增量式编码器输出两路方波 A 和 B,两者相位差 90 度。正转时 A 领先 B,反转时 B 领先 A。定时器的编码器接口就是利用这个相位差来判断方向的:它在 A 和 B 的每个边沿都采样另一相的电平,根据组合决定 CNT 是加还是减。

具体来说,编码器模式可以配置成“在 TI1 边沿计数”“在 TI2 边沿计数”或“在 TI1 和 TI2 都计数”。最后一种就是所谓的四倍频,因为 A 和 B 各有上升沿下降沿,一个完整周期能数到 4 个计数,分辨率最高。这也是我项目里最常用的配置,因为分辨率越高,低速时测速越准。

这里有个容易混淆的点:CNT 的方向不是固定的。正转时 CNT 递增,反转时 CNT 递减,所以 CNT 可能从 0 减到 65535(下溢)。读位置的时候不能只看 CNT 的绝对值,要处理方向。我的做法是定期读 CNT 并和上次的值做差,差值考虑溢出后就是这段时间的净位移,正负号就是方向。这样既拿到了速度(位移/时间),又拿到了方向。

4.2 编码器模式的初始化:几个必须配对好的参数

编码器模式的 HAL 初始化比 PWM 稍微特殊,因为它要配置从模式为编码器模式,还要设置两个输入通道的极性。下面是一段典型配置:

TIM_Encoder_InitTypeDef sEncoderConfig = {0}; htim2.Instance = TIM2; htim2.Init.Prescaler = 0; // 编码器模式一般不分频 htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 65535; // 用满 16 位量程 htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_Encoder_Init(&htim2, &sEncoderConfig); sEncoderConfig.EncoderMode = TIM_ENCODERMODE_TI12; // 四倍频 sEncoderConfig.IC1Polarity = TIM_ICPOLARITY_RISING; sEncoderConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI; sEncoderConfig.IC1Prescaler = TIM_ICPSC_DIV1; sEncoderConfig.IC1Filter = 6; // 滤波,抗抖动 sEncoderConfig.IC2Polarity = TIM_ICPOLARITY_RISING; sEncoderConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI; sEncoderConfig.IC2Prescaler = TIM_ICPSC_DIV1; sEncoderConfig.IC2Filter = 6; HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL);

几个参数值得展开说。EncoderMode选TI12就是四倍频,选TI1或TI2是二倍频。IC1Filter和IC2Filter是输入滤波,机械编码器(比如旋钮)抖动大,滤波要给强一点(比如 6~10),光电编码器信号干净可以给 0~2。滤波太弱会多计数,太强会漏掉快速转动,要按实际器件调。

Period设成 65535 是为了用满量程,这样一次能记录的最大位移最大,减少溢出处理频率。但要注意:如果编码器转得很快、CNT 频繁溢出,你就得提高读取频率,否则溢出次数多了,靠差值算位移会出错(因为一次溢出和多次溢出没法从单次差值区分)。我的经验是读取周期要保证“一个周期内的最大位移小于半量程”,这样差值不会歧义。

4.3 从 CNT 到速度:溢出处理和测速周期的选择

编码器模式读出来的是位置(CNT),要得到速度还得做一步“差分除以时间”。这一步有两个坑。

第一个是溢出处理。前面说了 CNT 会上下溢出,做差的时候要用有符号的方式处理。我的做法是把 CNT 转成 int16_t 再相减,利用补码的特性自动处理溢出:int16_t delta = (int16_t)(now - last);这样无论正转反转、无论是否跨过 0 点,差值都是对的(前提是一个周期内位移不超过 32767)。这个技巧很实用,比手动判断溢出方向简洁得多。

第二个是测速周期的选择。测速周期太短,低速时一个周期内可能一个计数都没有,速度算出来是 0 或者跳变;太长,速度响应滞后,控制环路会振荡。我的经验是让“最低速时一个测速周期内也有几十个计数”,这样量化误差可控。比如编码器 1000 线四倍频后 4000 计数/转,最低转速 10 转/分 = 0.167 转/秒,一个 10ms 周期内约有 6.7 个计数,勉强够用;如果要求更低速,就得拉长周期或者提高编码器分辨率。

下面是一个测速的骨架,用定时器中断定期采样:

volatile int32_t speed_counts_per_period = 0; static int16_t last_cnt = 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM4) { // 假设 TIM4 做 10ms 定时 int16_t now = (int16_t)__HAL_TIM_GET_COUNTER(&htim2); int16_t delta = (int16_t)(now - last_cnt); // 补码自动处理溢出 last_cnt = now; speed_counts_per_period = delta; // 10ms 内的净计数 } }

这段代码把“读位置、算差值、处理溢出”三件事压缩成了两行,靠的就是补码相减这个技巧。实际项目里我还会对这个 delta 做一点滑动平均滤波,抑制机械抖动带来的毛刺。

4.4 编码器模式的边界:什么时候它不够用

编码器模式虽好,但不是万能的。它有几个边界你得心里有数。第一,它只适合增量式编码器,绝对式编码器(比如 SSI、BiSS 接口的)得用别的方案。第二,它占用一个完整的定时器,如果你的定时器资源紧张,得权衡。第三,高速时要注意计数上限,虽然四倍频分辨率高,但如果转速极高、CNT 溢出太频繁,读取跟不上就会丢数,这时候要么提高读取频率,要么换用带更高位宽的定时器。

还有一个实际经验:机械旋钮用编码器模式时,一定要加硬件或软件消抖。机械触点抖动会产生大量假边沿,光靠输入滤波有时不够,我一般还会在软件里对 delta 做阈值判断,太小的变化当成噪声丢掉。这个阈值根据你的编码器线数和旋钮手感来定,没有统一值,得实测。

5. 三个模式背后的共通逻辑与选型判断

把 PWM、输入捕获、编码器模式分开讲完,其实它们背后是同一套机制的不同用法。这一节我把这层关系点破,帮你在遇到新需求时能自己判断该用哪个模式、怎么配。

5.1 一张表看清三者的寄存器复用关系

这三个模式看起来差别很大,但用的硬件资源高度重叠,理解这一点能帮你在定时器资源紧张时做取舍。

模式核心用的寄存器是否占用 CCR是否用从模式典型定时器
PWM 输出ARR、CCR、CCMR(输出部分)是(每通道一个)否通用/高级
输入捕获CCR、CCMR(输入部分)、CCER是(每通道一个)可选(PWM 输入模式要用)通用/高级
编码器模式CCR、CCMR、SMCR是(占用两个通道)是(编码器模式)带编码器接口的通用定时器

从表里能看出,CCR 是三者共用的核心——PWM 用它定占空比,捕获用它存时间戳,编码器用它配合判方向。所以一个定时器的通道数直接决定了你能同时干几件事。比如 TIM3 有 4 个通道,你可以 CH1/CH2 出 PWM、CH3 做捕获,但编码器模式要占两个通道,就不能和别的功能随便混用。

5.2 需求到模式的映射:几个典型场景的判断

遇到一个新需求,怎么快速判断用哪个模式?我总结了几条经验规则。

要“输出一个可控的方波”→ PWM 输出。不管是调光、调速、驱动蜂鸣器还是舵机,本质都是输出方波,用 PWM。判断依据是“信号方向是从 MCU 往外”。

要“测量外部信号的频率/脉宽/占空比”→ 输入捕获。判断依据是“信号方向是从外往 MCU”,且你需要知道它的时间特性。如果只是数脉冲个数(比如流量计),可以用定时器的外部计数模式,比捕获更省事。

要“读取旋转的位置和方向”→ 编码器模式。判断依据是“信号是正交的 A/B 两相”。如果只有单相脉冲(比如普通霍尔测速),用输入捕获或外部计数就行,不需要编码器模式。

这里有个容易混淆的场景:测电机转速。如果你用的是带编码器的电机,用编码器模式最准;如果只有霍尔传感器(每转几个脉冲),用输入捕获测脉冲频率也能算转速,但分辨率低。选哪个取决于你手上的传感器和精度要求,不是越复杂越好。

5.3 资源紧张时的取舍:一个定时器能干几件事

实际项目里定时器经常不够用,这时候就得精打细算。我的经验是:一个定时器的多个通道可以同时干不同的活,但要注意模式兼容性。比如同一个定时器,CH1 出 PWM、CH2 做输入捕获,这是可以的,因为它们用的是不同的通道和不同的 CCMR 配置位。但编码器模式要占用两个通道且改变从模式,就不能和 PWM 随便混了。

如果实在不够用,还有几个办法:用基本定时器(TIM6/TIM7)做纯定时中断,把通用定时器省下来做 PWM 和捕获;用软件模拟简单的 PWM(精度要求不高时);或者选引脚更多的型号。这些取舍没有标准答案,得看你的具体约束。

6. 调试这三个模式时我常用的手段

最后分享一些调试技巧,这些是文档里不会写、但实际排查问题时特别管用的东西。

6.1 用示波器和逻辑分析仪验证输出

PWM 输出配好之后,第一件事是用示波器看波形,确认频率和占空比对不对。没有示波器的话,逻辑分析仪也行,它能测频率和占空比。我见过有人 PWM 配完不验证,直接接负载,结果频率差十倍把负载烧了。先空载验证波形,再接负载,这是铁律。

输入捕获调试时,逻辑分析仪更有用,因为它能同时看输入信号和 MCU 的响应,确认捕获时刻对不对。如果捕获值不对,先看输入信号本身干不干净,再看边沿配置对不对,最后看溢出处理。

6.2 用调试器实时看 CNT 和 CCR

调试器(比如常见的在线调试工具)可以实时查看外设寄存器,这个功能排查定时器问题特别好用。PWM 不出波形,先看 CNT 在不在动(不动说明定时器没启动或时钟没开),再看 CCR 值对不对,再看输出使能位有没有置位。捕获测不准,看 CCR 里的值是不是在合理范围变化。编码器读数不对,手动转一下看 CNT 是加还是减,方向反了就把某一相的极性改一下。

我习惯在调试时把 CNT、ARR、CCR 都加到 watch 窗口,一边操作一边看数值变化,比盲猜快得多。

6.3 几个高频问题的快速定位表

现象可能原因排查方向
PWM 无输出定时器没启动/通道没使能/引脚复用没配查 CNT 是否在动、CCER 使能位、GPIO 复用
PWM 频率不对时钟源算错/PSC 或 ARR 算错核对时钟树 Timer Clocks、重算公式
捕获值跳变信号抖动/溢出没处理/中断延迟开输入滤波、检查溢出判断、精简中断
编码器方向反A/B 相极性配反改 IC1/IC2 的 Polarity
编码器多计数机械抖动/滤波太弱加强输入滤波、软件加阈值

这张表是我这些年遇到问题后总结的,大部分定时器相关的怪现象都能在里面找到方向。当然具体问题还得具体分析,但有个排查顺序能少走很多弯路。

6.4 一个容易被忽略的点:中断优先级和实时性

最后说个软件层面的坑。定时器的捕获中断、更新中断如果优先级配得低,被其他高优先级中断打断,就可能丢捕获或者溢出处理不及时。我的做法是:捕获中断优先级给高一点(因为它对实时性要求高),更新中断可以低一点。如果用了 DMA,那中断压力就小很多,优先级可以放宽。这个细节在功能调试阶段不明显,一到系统集成、多个外设同时跑的时候就暴露出来,提前规划好能省很多事。

我个人在实际项目里的体会是,定时器这三个模式真正难的不是配置,而是建立正确的心智模型——知道 CNT 在怎么动、CCR 在跟谁比、边沿来了硬件做了什么。模型对了,换个芯片、换个频率、换个需求,你都能自己推出来该怎么配。反过来,如果只是抄配置,那每次遇到新情况都得重新查一遍,而且出了问题也不知道从哪查起。所以我在带新人的时候,总是先让他们把“数数的机器”这个概念吃透,再去看具体模式,效果比直接给例程好得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 5:27:32

从613条建议到3条行动:清单式内容的筛选与落地方法

收藏夹里躺着两百多个 PDF&#xff0c;真正打开过的不到十个——这个状态在我和周围人身上都持续了好几年。前阵子一份叫《高性价比人生指南》的文档在各个群里疯传&#xff0c;613 条建议&#xff0c;从几点起床、钱怎么花、话怎么说&#xff0c;到跟谁交往、怎么选方向&#…

作者头像 李华
网站建设 2026/11/3 5:27:32

ChatGPT桌面版闪退排查:日志、缓存与硬件加速

闪退这个事&#xff0c;说大不大&#xff0c;说小不小。我在帮身边朋友和同事看桌面客户端问题时&#xff0c;遇到最多的就是这一类&#xff1a;双击图标&#xff0c;窗口闪一下就没了&#xff1b;或者聊到一半&#xff0c;界面突然消失&#xff0c;留下一脸懵的人盯着桌面。这…

作者头像 李华
网站建设 2026/11/3 5:27:19

2026新版PVF修改工具:解包打包、字段配置与双端同步

1. 先搞清楚 PVF 到底是什么&#xff1a;文件结构与加载链路PVF 是这款横版动作网游客户端里最核心的数据包格式&#xff0c;几乎所有你能在游戏里看到的东西——装备属性、技能倍率、怪物血量、掉落表、任务条件、NPC 对话文本——最终都躺在这么一两个.pvf文件里。2026 新版 …

作者头像 李华
网站建设 2026/11/3 5:27:13

别再相信后见之明:如何用决策档案和概率校准做真正的复盘

如果说我过去复盘踩过的坑里最大的一个&#xff0c;那就是——从来不复盘。项目标题里这个词hindsight&#xff0c;翻成中文就是"后见之明"&#xff0c;听起来是个好词&#xff0c;事后能看明白嘛。但我做了几年个人决策记录和定期回顾之后&#xff0c;必须说句大实话…

作者头像 李华
网站建设 2026/11/3 5:27:11

语音活动检测(VAD)技术原理与工程实践

1. 语音活动检测技术全景解析在嘈杂的会议室里&#xff0c;我们经常遇到这样的场景&#xff1a;远程参会的同事说话时断时续&#xff0c;背景键盘声却异常清晰。这正是语音活动检测&#xff08;VAD&#xff09;技术要解决的核心问题——如何像人耳一样精准区分语音与非语音片段…

作者头像 李华
网站建设 2026/11/3 5:26:37

移动端MCP落地指南:从场景、架构到真机踩坑

1. mobile-mcp 是什么&#xff0c;为什么值得关注MCP&#xff08;Model Context Protocol&#xff0c;模型上下文协议&#xff09;是过去一年多里 AI 工程领域绕不开的一个词。它定义了 AI 应用与外部工具、数据源之间的标准化通信方式&#xff0c;让模型不再是"只会对话的…

作者头像 李华