1. 项目概述:从“捕获”一个信号说起
搞嵌入式开发,尤其是玩STM32的,定时器这玩意儿绝对是绕不开的核心外设。今天我们不聊那些基础的定时、PWM输出,来啃一块稍微硬点的骨头——输入捕获。这名字听起来有点抽象,其实干的事儿很具体:精准地测量一个外部信号的“时间”。比如,一个脉冲的高电平持续了多久?两个上升沿之间隔了多少微秒?一个PWM波的频率和占空比是多少?这些问题的答案,输入捕获都能给你抓出来。
我最早接触输入捕获是为了做一个转速测量的小模块,需要测电机编码器输出的脉冲频率。当时对着手册和例程照猫画虎,代码是跑起来了,但心里总是不踏实,中断里为什么这么写?计数值怎么换算成时间?遇到信号毛刺怎么办?这些问题不搞清楚,项目出点怪问题就只能抓瞎。后来在几个实际项目里反复折腾、踩坑、优化,才算把输入捕获从“会用”磨到了“理解”。这篇内容,我就把自己这些年实操STM32输入捕获的经验、步骤和避坑心得,掰开揉碎了讲给你听。无论你是刚接触STM32的新手,还是想深入理解定时器机制的老鸟,相信都能找到对你有用的干货。
我们以最经典的STM32F1系列为例,但其原理和思路通用于大多数STM32型号。核心工具是Keil MDK和一块STM32开发板,当然,一个能产生PWM或方波的信号源(比如另一块板子、函数发生器或者就用同一个定时器输出PWM自测)也是必需的。
2. 输入捕获的核心原理与设计思路
在撸起袖子写代码之前,我们必须先弄明白STM32的定时器是怎么实现“捕获”这个功能的。这决定了我们后续的配置和代码逻辑。
2.1 定时器的基础骨架:时基单元
你可以把STM32的每个通用定时器(如TIM2, TIM3, TIM4)想象成一个精密且自律的“计数器”。它的核心部件是一个16位或32位的向上/向下计数寄存器(CNT)。在时钟驱动下,这个CNT的值会规律地递增或递减。
- 时钟源:决定它“数”得多快。可以来自内部系统时钟(经过分频),也可以来自外部。
- 预分频器(PSC):对时钟源进行分频。比如系统时钟72MHz,PSC设为71,那么驱动CNT的时钟就是72M / (71+1) = 1MHz,此时CNT每1微秒变化一次。
- 自动重装载寄存器(ARR):设定CNT计数的上限。当CNT计数到ARR值(向上计数)或计数到0(向下计数)时,会产生一个“更新事件”,并且CNT重置,开始新一轮计数。这个“更新事件”的周期,就是定时器产生中断或DMA请求的基准。
时基单元(PSC和ARR)的配置,直接决定了我们“时间标尺”的精度和量程。例如,时钟1MHz,ARR设为999,那么CNT从0数到999再溢出,正好是1000微秒(1毫秒)。这个“1毫秒”就是我们测量外部信号时间的基础单位。
2.2 “捕获”的瞬间发生了什么?
输入捕获功能建立在时基单元之上。定时器的每个通道(比如CH1, CH2)都有一对“捕获/比较寄存器”(CCR)和相关的输入电路。
当外部信号通过GPIO引脚连接到定时器通道,并配置为输入捕获模式后,魔法就开始了:
- 边沿检测:你可以选择在信号的上升沿、下降沿或双边沿触发捕获。
- 瞬间冻结:当指定的边沿到来时,硬件会立即将当前CNT计数器的值,“抓拍”并复制到对应的捕获/比较寄存器(CCR)中。这个动作是硬件自动完成的,速度极快,精度极高。
- 产生中断:同时,如果使能了捕获中断,硬件会置位一个标志位,并可能触发中断。在中断服务程序里,我们就能安全地读取这个被“冻结”在CCR里的值。
举个例子:我们想测量一个高电平脉冲的宽度。可以这么做:
- 第一次配置为上升沿捕获,上升沿到来时,捕获值
CCR1=CNT当时的值t1。 - 在中断里,立刻将通道改为下降沿捕获。
- 下降沿到来时,再次捕获,
CCR1=CNT值t2。 - 那么高电平时间
= (t2 - t1) * 计数周期。如果计数周期是1微秒,(t2-t1)是500,那么高电平就是500微秒。
2.3 方案选型:为何选择中断方式而非DMA或查询?
实现输入捕获,常见有三种方式:
- 查询方式:主循环不断检查捕获标志位。效率低,实时性差,容易丢失边沿,不推荐用于精确测量。
- DMA方式:捕获发生时,硬件自动将CCR值通过DMA传输到指定内存数组。适合高速、连续、大批量的信号采集,不占用CPU。但对初学者来说配置复杂,且对于单次或低频测量有点“杀鸡用牛刀”。
- 中断方式:捕获发生时触发中断,在中断服务函数里读取CCR值并进行处理。这是我们本次重点讲解的方式。
为什么选中断方式?
- 平衡性:它在实时性和CPU占用率之间取得了很好的平衡。对于频率不是特别高(比如几百KHz以下)的信号,中断处理绰绰有余。
- 灵活性:在中断里我们可以进行复杂的逻辑判断,比如切换捕获边沿(测脉宽)、处理计数器溢出(测长周期)、计算频率和占空比等。
- 学习价值:理解了中断方式,就掌握了输入捕获最核心的硬件交互和数据处理流程,之后再学DMA方式会容易得多。
- 资源消耗:不需要额外的DMA通道,代码结构清晰,易于调试。
注意:中断方式的最大挑战是中断响应时间和中断服务程序(ISR)的执行速度。如果信号频率太高,中断过于频繁,可能导致CPU无法处理其他任务,甚至丢失捕获事件。因此,评估信号最高频率和ISR代码长度是关键。
3. 硬件连接与软件环境准备
3.1 硬件连接:引脚不是随便选的
STM32的定时器输入通道与GPIO引脚是复用的,有固定的映射关系,不能随意连接。以STM32F103C8T6的TIM2_CH1为例:
- 查数据手册:找到你的芯片型号对应的数据手册(Datasheet)或引脚定义图。对于TIM2_CH1,它可能映射到PA0、PA5、PA15等引脚(具体看型号和重映射)。
- 常用组合:对于基础实验,PA0(TIM2_CH1)和PA6(TIM3_CH1)是常用的输入捕获引脚。我们以PA0为例。
- 信号源连接:将待测信号(如PWM输出)连接到开发板的PA0引脚。如果使用另一块板子的PWM输出,确保共地。
- 上拉/下拉:根据待测信号特性,可能需要配置GPIO的内部上拉或下拉电阻。如果信号源是推挽输出且驱动能力强,可以不配。如果信号源是高阻态或开漏输出,建议启用上拉(对于通常为高电平,脉冲为低电平的信号)或下拉(对于通常为低电平,脉冲为高电平的信号),以避免引脚悬空引入噪声。
连接示意图:
信号源输出引脚 ----> STM32 PA0 (TIM2_CH1) 信号源地线 GND ----> STM32 GND3.2 软件环境与工程建立
- IDE:使用Keil uVision 5 (MDK-ARM)。确保已安装对应芯片的Device Family Pack。
- 固件库:使用标准外设库(Standard Peripheral Library)或HAL库均可。标准库更贴近硬件,利于理解原理;HAL库封装程度高,开发速度快。本文以标准库为例进行讲解,因为其配置过程更透明。如果你用HAL库,CubeMX生成的代码逻辑是相通的。
- 新建工程:
- 选择正确的芯片型号(如STM32F103C8)。
- 在项目管理中添加必要的库文件:
CMSIS核心文件,StdPeriph_Driver下的misc.c,stm32f10x_gpio.c,stm32f10x_rcc.c,stm32f10x_tim.c。 - 配置魔术棒(Options for Target):
Target标签页:正确设置晶振频率(如8MHz外部晶振)。C/C++标签页:在Define中填入USE_STDPERIPH_DRIVER, STM32F10X_MD(根据你的芯片型号选择)。Debug标签页:选择你的调试器(如ST-LINK),并勾选Run to main()。
- 准备测试信号:为了自检,我们可以先用同一个定时器的另一个通道输出一个已知参数的PWM波,然后用输入捕获去测量它。例如,用TIM2的CH2(PA1)输出1KHz,占空比50%的PWM,用CH1(PA0)去捕获测量。
4. 代码实现:从零配置输入捕获
我们以实现测量PWM波频率和占空比为例,分步拆解。
4.1 第一步:GPIO和定时器时钟使能
任何外设使用前,必须先打开它的时钟门控。
// 开启GPIOA和TIM2的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);注意:STM32F1的APB1总线挂载了TIM2~TIM7等定时器,APB2总线挂载了GPIOA~G和高级定时器TIM1、TIM8。时钟使能函数不同,别搞混了。
4.2 第二步:配置GPIO为复用输入模式
PA0要作为TIM2的输入通道,而不是普通的输入或输出。
GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // PA0 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入,根据信号调整。也可用GPIO_Mode_IN_FLOATING(浮空输入) // 关键:对于定时器输入,模式是复用功能,但标准库中输入捕获配置下,GPIO通常配置为输入模式即可。 // 更严谨的配置可以是:GPIO_Mode_AF_IN,但标准库中常用输入模式。 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 速度可选 GPIO_Init(GPIOA, &GPIO_InitStructure);4.3 第三步:配置定时器时基单元
这是决定测量精度的核心。假设我们使用72MHz的系统时钟(AHB),APB1预分频器为2,则TIM2的时钟为72MHz(因为APB1分频后若不为1,则定时器时钟会倍频)。我们配置为1MHz的计数频率,这样每个计数代表1微秒。
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; TIM_TimeBaseInitStructure.TIM_Period = 65535; // ARR值,16位定时器最大值。用于周期测量时,需考虑溢出。 TIM_TimeBaseInitStructure.TIM_Prescaler = 71; // PSC值,72M / (71+1) = 1MHz TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频,与输入滤波相关,通常DIV1 TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式 TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; // 重复计数器,高级定时器用,通用定时器设为0 TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);参数计算解析:
TIM_Prescaler = 71:预分频器寄存器值。实际分频系数 =PSC + 1。所以72MHz / (71+1) = 1MHz。此时,计数器CNT每增加1,时间过去1 / 1MHz = 1微秒。TIM_Period = 65535:自动重装载值。CNT从0计数到65535后溢出,产生更新事件。这个周期是65536 * 1us = 65.536 ms。这意味着,在不处理溢出的情况下,我们最多能测量65.536ms长的连续高电平或低电平。对于测量频率(周期通常小于65ms)够用,但对于测量单个脉冲宽度,如果信号周期很长,就需要处理计数器溢出。
4.4 第四步:配置输入捕获通道
这里我们配置TIM2的通道1。
TIM_ICInitTypeDef TIM_ICInitStructure; TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; // 通道1 TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; // 初始捕获边沿:上升沿 TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; // 输入映射到TI1(直接) TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; // 输入捕获预分频,每个边沿都捕获 TIM_ICInitStructure.TIM_ICFilter = 0x0; // 输入滤波器,0表示不滤波 TIM_ICInit(TIM2, &TIM_ICInitStructure);关键参数详解:
TIM_ICPolarity:捕获极性。Rising(上升沿),Falling(下降沿),Both(双边沿,需要高级定时器支持)。TIM_ICSelection:非常重要!它决定了引脚信号如何映射到捕获通道。TIM_ICSelection_DirectTI:输入信号直接连接到对应的TIx(例如,CH1对应TI1)。这是我们最常用的模式。TIM_ICSelection_IndirectTI:输入信号交叉连接到另一个TIx(例如,CH1可以配置为捕获TI2的信号)。这在一些特殊测量(如PWM输入模式)中用到。TIM_ICSelection_TRC:捕获内部触发信号。
TIM_ICPrescaler:捕获预分频。可以配置为每N个边沿捕获一次,用于降低中断频率。测频率/脉宽时通常设为DIV1(每次边沿都捕获)。TIM_ICFilter:数字滤波器。设置一个采样频率和采样次数,只有当连续N次采样值一致时才认为边沿有效,用于滤除高频毛刺。值越大,滤波效果越强,但会引入延迟。对于干净的信号可以设为0。
4.5 第五步:配置中断并开启定时器
我们需要使能捕获比较中断(CC1IE)和更新中断(UIE)。前者用于在边沿到来时读取CCR,后者用于处理计数器溢出。
// 使能捕获/比较1中断和更新中断 TIM_ITConfig(TIM2, TIM_IT_CC1 | TIM_IT_Update, ENABLE); // 配置NVIC(嵌套向量中断控制器) NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; // TIM2全局中断 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 子优先级 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); // 使能定时器 TIM_Cmd(TIM2, ENABLE);4.6 第六步:编写中断服务函数
这是逻辑的核心。我们要在中断里记录时间、切换边沿、处理溢出、计算参数。
首先,定义几个全局变量用于在中断和主程序间传递数据:
volatile uint32_t IC_Value1 = 0, IC_Value2 = 0; // 记录两次捕获值 volatile uint32_t Period = 0, Duty = 0; // 计算的周期和占空比(单位:us) volatile uint8_t Capture_State = 0; // 捕获状态机:0-等待上升沿,1-已捕获上升沿等待下降沿,2-已捕获下降沿等待上升沿 volatile uint16_t Overflow_Count = 0; // 溢出次数计数器然后,编写TIM2的中断服务函数:
void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) // 处理溢出中断 { Overflow_Count++; // 溢出次数加1 TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 清除更新中断标志 } if (TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET) // 处理捕获中断 { switch(Capture_State) { case 0: // 初始状态,等待第一个上升沿 IC_Value1 = TIM_GetCapture1(TIM2); // 读取上升沿时刻的CCR1值 Overflow_Count = 0; // 清空溢出计数器,开始新的测量周期 // 切换为下降沿捕获,准备测量高电平宽度 TIM_OC1PolarityConfig(TIM2, TIM_ICPolarity_Falling); Capture_State = 1; break; case 1: // 已捕获上升沿,等待下降沿(测量高电平宽度) IC_Value2 = TIM_GetCapture1(TIM2); Duty = (IC_Value2 - IC_Value1) + (Overflow_Count * 65536); // 计算高电平时间,考虑溢出 // 切换为上升沿捕获,准备测量周期 TIM_OC1PolarityConfig(TIM2, TIM_ICPolarity_Rising); Capture_State = 2; break; case 2: // 已捕获下降沿,等待下一个上升沿(测量周期) IC_Value1 = TIM_GetCapture1(TIM2); Period = (IC_Value1 - IC_Value2) + (Overflow_Count * 65536); // 计算整个周期时间,考虑溢出 Overflow_Count = 0; // 为下一次测量重置溢出计数器 // 状态可以回到0或保持2,取决于你想连续测周期还是重新开始 // 这里我们保持状态2,连续测量周期和占空比 // 下一个下降沿会触发状态1的计算 // 注意:此时IC_Value2还是上一个下降沿的值,用于计算下一个周期 // 更清晰的写法是引入更多变量,这里为简化逻辑,演示核心思想 // 实际项目中,建议用两个变量分别存储当前和上一次的捕获值 Capture_State = 1; // 准备测量下一个高电平 // 切换为下降沿捕获 TIM_OC1PolarityConfig(TIM2, TIM_ICPolarity_Falling); break; } TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); // 清除捕获中断标志 } }中断逻辑解析:
- 状态机驱动:使用
Capture_State变量来记录测量阶段,这是一个非常清晰且可靠的编程模式。 - 溢出处理:
Overflow_Count用于记录在两次捕获之间,定时器计数器溢出了多少次。计算时间差时,必须加上Overflow_Count * 65536(因为ARR=65535)。这是测量长周期信号不丢精度的关键。 - 边沿切换:通过
TIM_OC1PolarityConfig函数动态改变捕获边沿,从而实现一次上升沿、一次下降沿交替捕获,分别得到脉宽和周期信息。 - 时间计算:
Period和Duty的单位是“定时器计数次数”。因为我们配置了1MHz的计数时钟,所以这个值直接就是微秒数。频率Freq = 1000000 / Period(Hz),占空比百分比DutyCycle = (Duty * 100) / Period。
重要心得:在中断里,先读取捕获值,再改变捕获边沿。这个顺序不能错。因为中断是在边沿发生时触发的,此时CCR寄存器里已经是当前边沿的捕获值。读取完成后,再配置下一个要捕获的边沿。
4.7 第七步:主程序与测试
主程序初始化后,可以在一个循环里打印或处理计算出的Period和Duty。
#include "stm32f10x.h" #include "stdio.h" // 如果使用串口打印 // ... 上述初始化函数调用 ... int main(void) { // 系统时钟、GPIO、定时器、中断初始化(调用上面的函数) TIM2_InputCapture_Init(); // 初始化串口用于打印(略) USART1_Init(); printf("STM32 Input Capture Test Start...\r\n"); while(1) { // 由于Period和Duty是volatile变量,在中断中被更新,这里可以直接使用 if(Period != 0) // 避免除零错误 { float freq = 1000000.0 / Period; // 计算频率,单位Hz float duty_cycle = (Duty * 100.0) / Period; // 计算占空比,单位% printf("Freq: %.2f Hz, Duty: %.1f%%\r\n", freq, duty_cycle); } Delay_ms(500); // 延时500ms打印一次,避免刷屏 } }5. 高级话题与性能优化
基础功能实现后,我们可以探讨一些更深入的话题和优化技巧。
5.1 处理高频信号与中断优化
当信号频率很高时(比如几十KHz以上),频繁的中断会消耗大量CPU资源。优化方法:
- 精简ISR:中断服务函数里只做最必要的操作——读取CCR、切换状态、更新关键变量。复杂的计算(如浮点运算、printf)放到主循环中。
- 使用DMA:对于需要连续记录大量捕获值的应用(如波形分析),应使用输入捕获的DMA模式。将CCR寄存器与一个内存数组建立DMA通道,捕获发生时硬件自动传输,攒够一定数量后再由DMA中断或定时器统一处理,极大减轻CPU负担。
- 提高定时器时钟:在系统时钟允许的情况下,减小预分频器PSC,提高计数频率。这样在测量相同物理时间时,计数器差值更大,相对误差更小。但要注意ARR溢出会更快。
- 利用定时器溢出:对于频率测量,可以不测两个上升沿的时间,而是在固定时间窗口内(比如1秒),统计上升沿触发的捕获中断次数。这需要开启更新中断(1秒定时),并在更新中断里读取一个在捕获中断中自增的计数器。这种方法在测低频时精度高,测高频时也能稳定工作。
5.2 输入滤波与边沿检测抗干扰
工业环境中信号常有毛刺。STM32的输入捕获提供了硬件滤波器。
TIM_ICFilter参数:这个4位值(0x0~0xF)并非直接定义频率,而是定义采样频率和连续采样次数。- 采样频率
f_{SAMPLING}由TIMx_CR1中的CK_INT频率和TIMx_CCMR1中的ICxF位域共同决定(有公式,详见参考手册)。 - 例如,当
ICxF[3:0] = 0011(0x3) 时,表示以f_{DTS}/32的频率采样,且连续8次采样值一致才确认边沿。 - 建议:从0开始,如果发现误触发,逐渐增大滤波值,直到信号稳定。但要注意,过强的滤波会延迟边沿检测,引入测量误差。
- 采样频率
5.3 PWM输入模式:硬件自动测量周期和占空比
这是一个非常强大的硬件特性!它利用一个定时器的两个捕获通道,自动测量PWM波的周期和占空比,几乎不占用CPU。
原理:以TIM2为例,将PWM信号同时输入到TI1(CH1)和TI2(CH2)。
- 配置为“PWM输入模式”,实际上是将通道1设置为双边沿捕获,并触发内部从模式控制器复位定时器。
- 上升沿到来时,同时触发两个动作:a) 通道1捕获值(CCR1)被更新;b) 计数器CNT被清零。
- 下降沿到来时,通道2捕获值(CCR2)被更新。
- 这样,CCR1里存储的就是PWM的周期(计数器从0开始到下一个上升沿的计数值),CCR2里存储的就是高电平脉宽。只需要读取这两个寄存器,就能直接得到结果,而且硬件自动处理了计数器复位,无需软件处理溢出。
配置关键:
// 1. 将输入信号映射到TI1(使用TIM_ICSelection_DirectTI或TIM_ICSelection_TRC取决于具体连接) // 2. 配置TIMx_SMCR寄存器,选择触发源为TI1,从模式为复位模式。 // 3. 配置通道1为上升沿捕获,通道2为下降沿捕获。 // 在标准库中,有专门的函数 `TIM_PWMIConfig()` 来简化此配置。使用PWM输入模式可以极大地简化代码,提高测量效率和可靠性,特别适合固定测量PWM信号的场合。
6. 调试技巧与常见问题排查
即使代码逻辑正确,调试阶段也可能遇到各种问题。下面是一些实战中总结的排查清单。
6.1 问题速查表
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 完全无中断 | 1. 定时器时钟未使能。 2. NVIC未配置或优先级错误。 3. 中断标志未清除,导致后续中断被屏蔽。 4. GPIO模式配置错误(应为复用/输入)。 | 1. 检查RCC_APBxPeriphClockCmd。2. 检查 NVIC_Init配置,确保中断通道正确且使能。3. 在中断函数开头/结尾确认清除了对应标志位( TIM_ClearITPendingBit)。4. 用万用表或逻辑分析仪检查引脚是否有信号输入。 |
| 能进中断,但捕获值不变或乱跳 | 1. 捕获极性配置错误。 2. 信号本身不稳定,有毛刺。 3. 没有处理计数器溢出,导致计算错误。 4. 在中断中读取CCR的时机不对。 | 1. 确认TIM_ICPolarity与信号边沿匹配。2. 增加输入滤波 TIM_ICFilter,或检查信号源质量。3. 开启更新中断,并在计算时间差时加入 Overflow_Count * (ARR+1)。4. 确保在捕获中断发生后立即读取CCR。 |
| 测量结果精度差 | 1. 定时器时钟源不准(如内部RC振荡器)。 2. 预分频器PSC计算错误,导致计数周期不准确。 3. 中断响应延迟引入误差(对于极高频率信号)。 | 1. 使用外部晶振作为时钟源。 2. 重新计算系统时钟树和PSC值。 3. 对于高频信号,考虑使用PWM输入模式或DMA方式,减少软件干预。 |
| 只能测到上升沿,测不到下降沿(或反之) | 1. 在中断中忘记切换捕获极性。 2. 切换极性的函数用错(应用 TIM_OCxPolarityConfig,而非TIM_OCxPolarity初始化结构体)。3. 状态机逻辑混乱,导致边沿配置错误。 | 1. 在中断服务函数中,在读取CCR后,立即使用TIM_OC1PolarityConfig()切换极性。2. 单步调试,观察每次中断后 TIMx_CCER寄存器中CCxP位的变化。 |
| 测量长周期信号(>65ms)不准 | 未处理定时器溢出。 | 使能更新中断(TIM_IT_Update),并维护一个Overflow_Count变量。计算时间差时:差值 = CCR2 - CCR1 + Overflow_Count * (ARR + 1)。 |
6.2 必备调试工具
- 逻辑分析仪:几十块钱的USB逻辑分析仪(如Saleae克隆版)是调试数字信号的利器。可以同时抓取输入信号和某个GPIO(用来在中断里翻转,标记中断发生时刻)的波形,直观地看到信号边沿、中断响应延迟、捕获是否触发。
- Keil Debugger:在调试模式下,可以查看定时器相关寄存器的实时值(CNT, CCR1, CCR2, SR状态寄存器),单步执行中断服务函数,观察变量变化。
- 串口打印:将计算出的周期、占空比、溢出次数等关键信息通过串口打印到PC,是最基本的调试手段。
6.3 一个关键的实操心得:关于“影子寄存器”
在配置定时器参数(如PSC, ARR, CCR)时,需要了解“影子寄存器”概念。我们通过库函数写入的是预装载寄存器。在发生更新事件(UEV)时,预装载寄存器的值才会被转移到影子寄存器中真正生效。
TIM_ARRPreloadConfig(TIM2, ENABLE):使能ARR预装载。这意味着你可以在任何时候写ARR,但新值只在下次更新事件时才生效。这可以避免在定时器运行时修改ARR导致当前计数周期错乱。- 对于输入捕获的CCR寄存器:它没有预装载功能,写入的就是当前值。但作为捕获寄存器时,它是只读的,由硬件在边沿触发时写入。
在初始化时,通常我们会使能ARR预装载,确保参数同步更新。这个细节不影响基础功能,但在高级应用(如动态调整定时周期)时很重要。
7. 项目扩展与进阶思路
掌握了基本的输入捕获,你可以尝试以下更有挑战性的项目,它们能帮你更深入地理解定时器:
- 超声波测距:利用输入捕获测量HC-SR04超声波模块返回的高电平脉冲宽度,从而计算距离。这是输入捕获的经典应用。
- 编码器测速:使用定时器的编码器接口模式,配合光电或霍尔编码器,直接读取旋转方向和计数值,结合输入捕获或另一个定时器进行定时采样,可以计算出精确的转速。
- 红外遥控解码:NEC等红外协议使用特定脉宽组合表示0和1。用输入捕获测量每个脉冲的高低电平宽度,即可解码出按键值。
- 频率计/占空比仪:将上述代码完善,加上按键切换、OLED显示,做成一个便携式简易仪器。
- 多通道同步捕获:使用一个定时器的多个通道,同时捕获多个相关信号的时间点,用于分析信号间的相位差等。
输入捕获功能就像STM32定时器赋予你的一双“高速电眼”,能让你精确感知外部世界的时间信息。从理解原理、配置寄存器、编写中断服务函数,到处理溢出、抗干扰、优化性能,每一步都需要耐心和实践。希望这篇超详细的实操指南,能帮你打通任督二脉,下次再遇到需要“抓时间”的任务时,能够从容不迫,游刃有余。