1. 从“数数”到“掐表”:输入捕获到底在干什么?
搞嵌入式开发,尤其是玩STM32的,定时器这个外设绝对是绕不开的核心。PWM输出、延时、计时,这些基础功能大家都很熟了。但说到“输入捕获”,很多刚上手的朋友可能会觉得有点抽象:它到底有什么用?简单来说,你可以把它理解成一个极其精准的“掐表”工具。
想象一下,你面前有一个信号发生器,它输出一个方波,你想知道这个方波的频率是多少,或者高电平持续了多长时间。用人眼和普通秒表去测,误差太大,而且对于微秒(μs)甚至纳秒(ns)级别的时间,根本无能为力。STM32的定时器输入捕获功能,就是为解决这类问题而生的。它能在外部信号的特定边沿(比如上升沿或下降沿)到来的瞬间,像拍照一样,“咔嚓”一下把当前定时器的计数值“捕获”并锁存到一个专用的寄存器里。你只需要读取两次捕获值,做个减法,再结合定时器的计数频率,就能精确计算出两次边沿之间的时间间隔。
这个功能的应用场景非常广泛。比如,测量旋转编码器的转速(通过测量脉冲周期)、测量超声波测距模块回波的高电平时间、解码红外遥控器的NEC协议(测量脉冲宽度)、甚至实现一个高精度的频率计。它把时间测量这个事,从软件轮询的“苦力活”,变成了硬件自动完成的“精细活”,不仅精度高,还不占用CPU资源。今天,我就结合自己这些年调STM32定时器的经验,把输入捕获这个功能的里里外外、从配置到避坑,给你掰开揉碎了讲清楚。
2. 硬件架构与核心寄存器:理解“捕获”的物理基础
在写代码之前,我们必须先搞清楚STM32定时器为了实现输入捕获,在硬件上做了哪些设计。这就像开车前得知道油门、刹车、方向盘在哪一样。STM32的通用定时器(如TIM2, TIM3, TIM4, TIM5)和高级定时器(TIM1, TIM8)都支持输入捕获功能,其核心硬件逻辑围绕几个关键部分展开。
2.1 输入通道与捕获/比较寄存器
每个定时器通常有4个独立的输入捕获通道(CH1-CH4)。每个通道都对应着一对“捕获/比较寄存器”,比如TIMx_CCR1对应通道1。这个寄存器是“双向”的:当通道配置为输出模式时,它是比较寄存器,用于产生PWM;当配置为输入捕获模式时,它就变成了捕获寄存器。
关键点来了:捕获是如何触发的?每个输入通道的信号,会先经过一个“输入滤波器和边沿检测器”。你可以配置滤波器来消除信号上的毛刺(比如设置采样频率和事件次数),配置边沿检测器来指定在信号的上升沿、下降沿,还是双边沿触发捕获。
当指定的边沿事件发生时,硬件会立刻执行一个原子操作:将当前定时器计数器(TIMx_CNT)的值,复制到对应的捕获/比较寄存器(TIMx_CCRx)中。同时,硬件会置位一个标志位(TIMx_SR寄存器中的CCxIF位),如果使能了中断或DMA,还会产生相应的请求。这个过程完全由硬件完成,速度极快,精度只取决于定时器的时钟基准。
2.2 定时器计数模式与溢出处理
输入捕获的精度和量程,直接依赖于定时器计数器(TIMx_CNT)的工作方式。最常用的模式是向上计数模式:计数器从0开始,每个时钟周期加1,一直加到自动重装载值(TIMx_ARR),然后产生溢出事件并清零重新开始。
这里就引出了输入捕获测量中的一个经典问题:如何测量长于一次定时器溢出周期的时间?例如,定时器ARR设为65535,时钟频率为1MHz,那么一次溢出的周期是65.535ms。如果你要测一个100ms的脉冲宽度,计数器肯定会溢出至少一次。
解决方案是结合溢出中断。我们可以在使能捕获中断的同时,也使能定时器的更新(溢出)中断。在更新中断服务函数里,对一个全局的溢出计数器(比如capture_overflow_count)进行加1。这样,当我们捕获到上升沿和下降沿时,得到的就不仅仅是CCRx寄存器的值,而是一个“扩展”的时间值:总时间 = 溢出次数 * (ARR + 1) + 第二次捕获值 - 第一次捕获值。
这里有个细节需要注意:捕获值是在溢出前还是溢出后读取的?如果第一次捕获发生在计数器为60000时,然后计数器溢出归零,又在1000时发生第二次捕获。单纯用1000 - 60000会得到一个负数(在无符号数运算中会变成一个很大的正数),结果显然是错的。因此,我们的计算必须考虑溢出计数器的状态。通常的逻辑是,在捕获中断里,除了读取CCRx,还要立刻读取当前的溢出计数器值,共同组成一个完整的时间戳。
2.3 输入通道与GPIO的映射关系
这不是软件配置,而是硬件设计。STM32的定时器输入通道对应的GPIO引脚是固定的,你需要查阅芯片的数据手册(Datasheet)或引脚复用映射表(Alternate Function Mapping)来确定。例如,TIM2的通道1,可能对应PA0、PA5或PA15,具体是哪个,取决于你的芯片型号和封装。在CubeMX工具里,这个选择是可视化的,但理解其背后的硬件连接依然重要,尤其是在排查“为什么捕获不到信号”这类问题时。
3. HAL库驱动下的配置全流程与代码逐行解析
理论懂了,我们上手实战。这里我以STM32CubeIDE和HAL库为例,因为这是目前最主流、最快捷的开发方式。我们目标是配置TIM2的通道1来测量一个PWM信号的高电平宽度(即上升沿到下降沿的时间)。
3.1 使用CubeMX进行图形化配置
- 启用定时器时钟:在
Pinout & Configuration的System Core->RCC中,根据你的板子配置好高速外部时钟(HSE)。 - 配置定时器基础参数:
- 在
Timers中找到TIM2。 Clock Source选择Internal Clock。输入捕获是测量外部信号,但定时器本身需要内部时钟来驱动计数器。Prescaler(预分频器):这是决定精度的关键。假设系统主频是72MHz,我们希望定时器计数频率为1MHz(即每个计数代表1μs)。那么预分频值应设置为72MHz / 1MHz - 1 = 71。HAL库的公式是定时器时钟 = 系统时钟 / (PSC + 1)。Counter Mode:选择Up(向上计数)。Counter Period(即ARR):设置为最大值65535(对于16位定时器)。这样一次溢出周期是65.535ms,在测量常见PWM信号(如50Hz舵机信号)时足够用。如果你需要测量更长时间,就要结合溢出中断。auto-reload preload:通常使能,这可以避免在修改ARR时产生毛刺。
- 在
- 配置输入捕获通道:
- 在TIM2的配置界面,切换到
Channel1,选择Input Capture direct mode。 ICPolarity:选择Rising Edge(我们先捕获上升沿)。IC Selection:选择Direct。这意味着通道1的信号直接连接到捕获单元。另一种Indirect模式是交叉连接,用于特殊场景。IC Prescaler:捕获分频器。选择No division(每次边沿都捕获)。你也可以选择每2次、4次、8次边沿捕获一次,用于滤除噪声或测量低频信号。IC Filter:输入滤波器。如果信号比较干净,设为0(不滤波)。如果信号有毛刺,可以适当增大,例如设为4,表示以fDTS频率采样4次,结果一致才认为有效边沿。
- 在TIM2的配置界面,切换到
- 配置NVIC(嵌套向量中断控制器):
- 在
NVIC Settings中,勾选TIM2 global interrupt。这样定时器溢出(更新事件)和捕获事件都会触发这个中断。我们需要在中断服务函数里区分它们。 - 更精细的做法是,在代码中单独使能捕获比较中断(
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1)),它会自动配置NVIC。
- 在
- 生成代码:点击
Generate Code,CubeMX会帮你初始化时钟树、GPIO和定时器外设。
3.2 关键代码编写与逻辑剖析
CubeMX生成的代码搭建了框架,但核心逻辑需要我们自己在main.c或单独的模块中实现。
// 定义全局变量,用于记录状态和时间 volatile uint32_t g_capture_start = 0; // 上升沿捕获值 volatile uint32_t g_capture_end = 0; // 下降沿捕获值 volatile uint32_t g_capture_overflow = 0; // 溢出计数器 volatile uint8_t g_capture_done = 0; // 测量完成标志 volatile uint8_t g_is_first_captured = 0; // 是否已捕获到上升沿 // 定时器溢出中断回调函数(在stm32xx_it.c中,HAL库会调用此回调) void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { g_capture_overflow++; // 每次溢出,计数器加1 } } // 输入捕获中断回调函数 void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) { if (g_is_first_captured == 0) { // 第一次捕获,即上升沿 // 记录上升沿时的“完整时间戳” g_capture_start = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); g_capture_start += (g_capture_overflow << 16); // 假设16位定时器,组合成扩展值 // 切换为下降沿捕获 __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING); g_is_first_captured = 1; // 标记已捕获上升沿 } else { // 第二次捕获,即下降沿 // 记录下降沿时的“完整时间戳” g_capture_end = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); g_capture_end += (g_capture_overflow << 16); // 计算高电平脉宽 uint32_t pulse_width_ticks = g_capture_end - g_capture_start; // 转换为时间(单位:微秒),假设定时器时钟为1MHz float pulse_width_us = (float)pulse_width_ticks; // 因为1 tick = 1us // 这里可以将pulse_width_us用于后续处理,或者通过串口打印 // 重置状态,准备下一次测量 __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING); g_is_first_captured = 0; g_capture_done = 1; // 设置完成标志 // 注意:这里没有清零g_capture_overflow,因为它是一个持续累加的全局时间基准。 // 在长时间测量中,只要两次捕获间隔内溢出次数不超过65535次,计算就是正确的。 } } } // 主函数中的初始化与启动 int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); // CubeMX生成的初始化函数 // 启动定时器基础计数(溢出中断由此使能) HAL_TIM_Base_Start_IT(&htim2); // 启动输入捕获中断,并指定通道和初始边沿(上升沿) HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1); while (1) { if (g_capture_done) { g_capture_done = 0; // 处理测量结果,例如通过串口打印 // printf("High Level Width: %.2f us\n", pulse_width_us); } // 其他主循环任务 } }代码逻辑深度解析:
- 时间戳的组合:
g_capture_start = CCR1值 + (g_capture_overflow << 16)。这里做了一个简化假设:定时器是16位的(ARR=65535),所以一次溢出的“时间长度”是65536个计数。我们将溢出次数左移16位,相当于乘以65536,再与当前的CCR1值相加,就得到了一个扩展的32位时间戳。这是处理溢出的核心思想。 - 动态切换边沿极性:这是测量脉宽的标准做法。在上升沿捕获回调中,立刻将极性改为下降沿,这样硬件就会在下一个下降沿再次触发中断。这种方法比使用两个独立的通道(一个抓上升沿,一个抓下降沿)更节省资源,且避免了两个通道间可能存在的微小偏移误差。
HAL_TIM_ACTIVE_CHANNEL_1:这是一个HAL库宏,用于在共用中断服务函数时,判断是哪个通道触发了中断。因为多个捕获通道可能共用同一个中断向量。- 中断服务函数要快进快出:在
HAL_TIM_IC_CaptureCallback中,我们只做了读取寄存器、切换极性、设置标志位等最必要的操作。复杂的计算(如单位转换)和输出(如串口打印)应放到主循环中根据标志位来处理,避免长时间占用中断影响系统实时性。
4. 从测量到应用:频率、占空比与高级测量模式
掌握了基本脉宽测量,我们就可以构建更复杂的应用。
4.1 测量信号频率与占空比
测量频率通常有两种方法:
- 测周法:测量一个完整周期的时间(即相邻两个同向边沿,如上升沿到上升沿的时间T),频率
f = 1 / T。这种方法在低频时精度高。实现上,只需在上述脉宽测量代码中,将第二次捕获的边沿也设为上升沿即可。 - 测频法:在固定闸门时间(比如1秒)内,统计信号上升沿的次数N,频率
f = N。这种方法在高频时精度高,但需要另一个定时器来产生精确的闸门信号,并在闸门时间内对捕获中断进行计数。
测量占空比则需要三个时间点:上升沿A,下降沿B,下一个上升沿C。
- 高电平时间
Th = B - A - 周期
T = C - A - 占空比
Duty = (Th / T) * 100%这需要捕获三个边沿,逻辑上稍微复杂一些,但原理完全相同。你可以使用一个状态机(g_capture_state)来跟踪当前是第几次捕获,并分别记录A、B、C三个时间戳。
4.2 使用从模式实现硬件自动测量
前面的方法依赖中断和软件计算,在测量极高频率的信号时,频繁的中断可能成为瓶颈。STM32定时器提供了一个更强大的功能:从模式触发。
我们可以配置定时器工作在“复位从模式”。将输入捕获通道(如TIM2_CH1)配置为触发源(TRGI),并设置为上升沿触发。当上升沿到来时,定时器计数器(TIMx_CNT)会立刻被清零。同时,我们启用另一个通道(如TIM2_CH2)在下降沿捕获。这样,当下降沿到来时,通道2捕获寄存器(CCR2)里存储的值,直接就是高电平的脉宽计数值,完全不需要软件干预和溢出计算!
配置要点:
- 主定时器(TIMx)的
Slave Mode Controller选择Reset Mode。 Trigger Source选择TI1FP1(即通道1的滤波后信号)。- 通道1配置为输入,上升沿触发(用于复位CNT)。
- 通道2配置为输入捕获,下降沿触发(用于捕获CNT值)。
这种方法将整个测量过程硬件化,精度和实时性达到极致,CPU只在测量完成后去读取一下CCR2寄存器即可。非常适合对实时性要求苛刻的应用。
4.3 输入滤波与数字防抖
在实际电路中,机械开关或长线传输会引入毛刺。定时器的输入滤波器就是第一道硬件防线。前面提到的IC Filter参数,其原理是:定时器内部有一个以fDTS(通常由定时器时钟分频而来)频率运行的采样器,它会连续采样输入信号N次(N为滤波值),只有当N次采样结果一致时,才认为边沿有效。例如,fDTS=72MHz,Filter=4,则只有当信号电平稳定至少4 * (1/72MHz) ≈ 55.6ns不变,边沿才会被确认。这能有效滤除窄毛刺。
注意:滤波器会引入固定的延迟。这个延迟在计算绝对时间时需要被考虑(虽然通常很小),但在测量边沿之间的相对时间(如脉宽)时,只要两个边沿使用相同的滤波器设置,延迟就会相互抵消,不影响结果。
5. 实战调试与常见问题深度排坑
理论完美,代码写完,一上电发现没数据?或者数据跳得厉害?别急,这些都是必经之路。下面是我总结的几个高频问题点和排查思路。
5.1 问题一:完全捕获不到任何信号
排查链路:
- 信号是否真的存在?这是第一步也是最容易忽略的一步。用示波器或者逻辑分析仪,直接测量你连接到STM32引脚上的信号。确认信号电压幅度(是否是3.3V CMOS电平?)、频率是否在预期范围内、是否有明显的畸变。
- GPIO配置是否正确?检查CubeMX或代码中,该引脚是否确实配置为了对应定时器的“Alternate Function”模式,而不是普通的输入/输出。检查上拉/下拉电阻配置,如果外部信号是开漏输出,MCU这边可能需要使能内部上拉。
- 定时器时钟是否使能?确认
__HAL_RCC_TIMx_CLK_ENABLE()被调用(CubeMX通常已处理)。可以用调试器查看TIMx->CR1寄存器的值,确认定时器已使能(CEN位为1)。 - 中断是否使能?检查
HAL_TIM_IC_Start_IT()是否被成功调用。在调试器中,查看TIMx->DIER寄存器,确认CCxIE(捕获中断使能)和UIE(更新中断使能)位是否为1。同时检查NVIC,确认TIMx_IRQn中断已启用。 - 边沿极性是否设反?如果你的信号是低电平有效,却配置了上升沿捕获,那肯定抓不到。用示波器看边沿方向,与代码配置对比。
5.2 问题二:测量值不稳定,跳动很大
排查链路:
- 信号质量问题:再次祭出示波器。观察信号边沿是否陡峭?是否有振铃或过冲?电源地上是否有噪声?不干净的信号是精度杀手。可能需要在硬件上增加RC滤波或施密特触发器整形。
- 定时器时钟源不稳定:如果定时器使用的是内部RC振荡器(HSI),其精度和温漂可能较差。对于高精度测量,务必使用外部晶振(HSE)作为系统时钟源,并确保定时器时钟是从PLL稳定分频而来。
- 中断干扰与延迟:如果系统中有更高优先级的中断长时间执行,可能会延迟输入捕获中断的响应。虽然捕获值由硬件锁存不会丢,但中断处理函数中读取溢出计数器的时机可能滞后,导致组合时间戳出错。确保捕获中断优先级足够高,且中断服务函数执行时间极短。
- 未考虑计数器溢出:这是新手最容易栽跟头的地方。如果你的测量时间可能超过
(ARR+1)/定时器时钟频率,就必须实现溢出计数逻辑。检查你的代码是否在更新中断中增加了溢出计数器,并且在计算时间戳时是否正确组合了溢出计数和CCRx值。一个简单的验证方法:故意输入一个周期很长的信号(大于溢出周期),看测量结果是否正确。 - 滤波器和分频器设置不当:过强的滤波器(Filter值过大)会扭曲信号的真实边沿位置,尤其在信号频率较高时。捕获分频器(IC Prescaler)如果设成了每N次边沿捕获一次,而你以为是每次边沿都捕获,也会导致读数成倍数关系错误。
5.3 问题三:测量值存在固定偏差
- 系统延迟:从信号进入引脚,到经过输入滤波器、边沿检测器,最终触发捕获,硬件本身有数个时钟周期的延迟。这个延迟在数据手册的“输入延迟”参数中会有说明,通常是ns级别。在绝大多数应用中可以忽略,但在纳秒级超精密测量中需要校准。
- 软件开销:从捕获发生到进入中断服务函数,再到读取CCRx寄存器,这中间有中断响应时间(由芯片性能和中断优先级决定)。这个延迟不影响CCRx寄存器本身的值,因为硬件在边沿发生的瞬间就已经把CNT值锁存到CCRx了。软件延迟影响的是你读取溢出计数器的时机。因此,务必在捕获中断中第一时间读取CCRx和溢出计数器,将它们“绑定”为一个原子操作。
一个实用的调试技巧:使用定时器本身的PWM输出功能进行自检。用同一个定时器的另一个通道(如TIM2_CH2)输出一个已知频率和占空比的PWM信号,然后将这个输出引脚通过杜邦线短接到输入捕获引脚(TIM2_CH1)。这样你就可以用输入捕获功能去测量自己产生的信号,理论值和测量值应该完全吻合。这是验证整个输入捕获配置和代码逻辑最直接、最可靠的方法。
最后,关于资源选择,对于简单的脉宽或频率测量,通用定时器(TIM2-TIM5)绰绰有余。如果需要同时测量多路信号,或者用到互补通道、死区插入等高级功能,才会考虑高级定时器(TIM1, TIM8)。输入捕获功能本身并不复杂,其核心在于对硬件自动化的理解和对边界条件(尤其是溢出)的严谨处理。希望这篇近万字的梳理,能帮你把这个“精准掐表”的工具用得得心应手。