一、问题背景:溢出累积误差从何而来
在STM32中,用输入捕获(Input Capture)测频率的常规方法是:
- 捕获两个上升沿,记录CCR1值,计算差值得到周期。
- 若信号周期大于定时器溢出周期(如72MHz下16位定时器最大周期约0.91ms),则需在溢出中断中累加溢出次数,最终周期 = 溢出次数 × 溢出周期 + 两次捕获差值。
这种方法存在一个致命缺陷:溢出中断响应延迟。当捕获边沿与溢出中断几乎同时发生时,软件可能先处理捕获再处理溢出,或反之,导致溢出计数多算或少算1,引入一个完整溢出周期的误差。对于低频信号(如1Hz),误差可达100%。
二、解决思路:从模式复位计数器
STM32定时器支持从模式(Slave Mode),其中复位模式(Reset Mode) 允许外部触发信号(如TI1FP1)自动将计数器CNT清零。
核心思想:将输入捕获通道配置为触发源,并开启从模式复位。这样,每个捕获边沿到来时,硬件自动将CNT清零,无需软件干预。两次捕获之间的计数值直接就是周期对应的时钟数,完全不需要溢出中断,从根本上消除了累积误差。
原理要点:
- 捕获边沿同时触发CNT复位,因此CCR1中保存的是“从上次复位到本次捕获”的计数值。
- 若信号周期小于定时器溢出周期,则一次捕获即可得到完整周期;若大于,则需结合溢出次数,但此时溢出次数是精确的(因为复位由硬件同步完成)。
- 更常用的做法是:测量两个相邻捕获值之差,但复位模式下每次捕获后CNT归零,因此直接读取CCR1即为周期(单位:时钟数)。
三、配置步骤(以STM32F103 TIM2 CH1为例)
3.1 硬件连接
- 信号输入至TIM2_CH1(PA0)。
3.2 CubeMX配置
- 选择TIM2,Clock Source设为Internal Clock。
- Channel1设为Input Capture direct mode。
- 参数设置:
- Prescaler = 71(72MHz/72=1MHz,即1us计数)。
- Counter Period = 65535(16位最大值)。
- Input Capture Channel1: Rising Edge,无滤波(或根据信号质量设置)。
- 触发源(Trigger Source)选择TI1FP1。
- 从模式(Slave Mode)选择Reset Mode。
- 使能TIM2中断(用于捕获中断,非溢出中断)。
3.3 关键寄存器配置(手动代码)
若使用标准库或寄存器操作,核心配置如下:
// 1. 时基配置
TIM2->PSC = 71; // 1MHz计数
TIM2->ARR = 0xFFFF; // 最大重载值
// 2. 输入捕获配置(CH1,上升沿)
TIM2->CCMR1 |= (1 << 0); // CC1S=01,映射到TI1
TIM2->CCMR1 |= (0 << 4); // 无滤波
TIM2->CCER |= (1 << 0); // 使能捕获
TIM2->CCER &= ~(1 << 1); // 上升沿捕获
// 3. 从模式复位配置
TIM2->SMCR |= (1 << 2); // TS=101,触发源TI1FP1
TIM2->SMCR |= (7 << 0); // SMS=111,复位模式
// 4. 使能捕获中断
TIM2->DIER |= (1 << 1); // CC1IE
NVIC_EnableIRQ(TIM2_IRQn);
// 5. 启动定时器
TIM2->CR1 |= (1 << 0);
四、完整代码示例(HAL库)
#include "stm32f1xx_hal.h"
TIM_HandleTypeDef htim2;
volatile uint32_t capture_value = 0;
volatile uint8_t capture_flag = 0;
void TIM2_Init(void) {
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
TIM_IC_InitTypeDef sConfigIC = {0};
TIM_SlaveConfigTypeDef sSlaveConfig = {0};
htim2.Instance = TIM2;
htim2.Init.Prescaler = 71;
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 0xFFFF;
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
HAL_TIM_Base_Init(&htim2);
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig);
HAL_TIM_IC_Init(&htim2);
// 输入捕获配置
sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
sConfigIC.ICFilter = 0;
HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_1);
// 从模式复位配置
sSlaveConfig.SlaveMode = TIM_SLAVEMODE_RESET;
sSlaveConfig.InputTrigger = TIM_TS_TI1FP1;
HAL_TIM_SlaveConfigSynchro(&htim2, &sSlaveConfig);
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);
}
// 捕获中断回调
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM2 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) {
capture_value = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
capture_flag = 1;
}
}
// 主循环中计算频率
float get_frequency(void) {
if (capture_flag) {
capture_flag = 0;
// 计数时钟为1MHz,capture_value即为周期(us)
if (capture_value > 0) {
return 1000000.0f / capture_value; // Hz
}
}
return 0.0f;
}
说明:由于从模式复位,每次捕获后CNT自动清零,因此capture_value直接就是信号周期对应的计数值(单位:1us)。无需任何溢出处理。
五、注意事项
- 信号频率范围:该方法要求信号周期小于定时器溢出周期(本例为65535us,约15.26Hz以上)。若信号频率更低,需降低预分频或使用32位定时器(如TIM5)。
- 捕获边沿选择:确保触发源与捕获通道极性一致(如TI1FP1对应上升沿)。
- 中断优先级:捕获中断应设置合理优先级,避免被其他高优先级中断长时间阻塞,导致捕获值被覆盖(但复位模式下即使覆盖,下一次捕获仍准确)。
- 输入滤波:若信号有噪声,适当配置ICFilter(如0x0F)可防止误触发复位。
- 首次捕获:启动后第一次捕获值可能不准确(因CNT初值不确定),建议丢弃第一次结果。
- 从模式复位与预分频:复位信号会清零CNT,但不会影响预分频器,因此计数时钟始终一致。
六、总结
利用STM32定时器的从模式复位功能,将输入捕获边沿作为硬件复位信号,可以彻底消除软件累加溢出次数带来的累积误差。该方法实现简单、精度高,特别适合中低频信号的频率测量。掌握这一技巧,能显著提升嵌入式测量系统的可靠性。
暂无评论,来抢沙发~