输入捕获实验代码2
141.5.13 输入捕获实验代码(二)
/*
* 整体目的:补全 TIM2 输入捕获的溢出累计与结果换算,得到跨越计数器回绕的高电平宽度。
* 正确公式:总计数 = period_count×(ARR+1)+CCR2;本工程 ARR=0xFFFF,
* 所以每次溢出必须累计 0x10000=65536,而不是 0xFFFF=65535。
* 中断分工:捕获事件锁存边沿时刻;更新事件记录 CNT 回绕;主循环复制结果并负责打印。
* 代码范围:基于 CubeIDE/08IC 的 main.c 和 stm32f1xx_it.c 修正讲解;
* HAL_TIM_IRQHandler() 库内部不逐行展开。
*/
volatile uint8_t ic_state = 0U; /* 0等待上升沿、1测量中、2结果就绪。 */
volatile uint32_t period_count = 0U; /* 保存测量期间完整跨过的 TIM2 周期数。 */
volatile uint32_t ic_count = 0U; /* 保存下降沿捕获到的 CCR2 低 16 位计数。 */
uint64_t total_ticks = 0ULL; /* 使用 64 位保存乘加结果,避免长脉宽时 32 位溢出。 */
/* ==================== Core/Src/main.c:消费捕获结果 ==================== */
if (ic_state == 2U) /* 捕获回调已经锁存下降沿后,主循环处理结果。 */
{ /* 结果处理分支开始。 */
uint32_t periods_snapshot = period_count; /* 先复制共享的溢出次数,减少后续读写窗口。 */
uint32_t capture_snapshot = ic_count; /* 再复制 CCR2 捕获值,形成当前测量快照。 */
ic_state = 0U; /* 允许捕获回调开始下一帧测量。 */
total_ticks = /* 组合“完整周期高位”和“最后一圈低位”。 */
(uint64_t)periods_snapshot * /* 把周期数提升为 64 位后再乘,避免先发生 32 位溢出。 */
((uint64_t)__HAL_TIM_GET_AUTORELOAD(&htim2) + 1ULL) + /* 一圈是 ARR+1 个计数。 */
(uint64_t)capture_snapshot; /* 加上下降沿锁存的最后一圈计数值。 */
printf("High level duration: %lluus\r\n", /* 1 MHz 计数时总计数数值就是微秒数。 */
(unsigned long long)total_ticks); /* 显式匹配 %llu 所要求的参数类型。 */
} /* 结果处理分支结束。 */
/* ==================== Core/Src/main.c:更新事件记录溢出 ==================== */
void HAL_TIM_PeriodElapsedCallback( /* HAL 识别 TIM2 更新事件后调用的周期回调。 */
TIM_HandleTypeDef *htim) /* 参数指出是哪一个定时器发生了更新。 */
{ /* 周期完成回调开始。 */
if ((htim->Instance == TIM2) && /* 第一条件:事件必须来自 TIM2。 */
(ic_state == 1U)) /* 第二条件:必须处于上升沿与下降沿之间。 */
{ /* 有效溢出统计分支开始。 */
if (period_count == UINT32_MAX) /* 防止溢出计数自身继续加一而回绕为 0。 */
{ /* 测量超时/计数饱和分支开始。 */
ic_state = 2U; /* 强制结束本次异常长的测量,交给主循环处理。 */
} /* 测量超时分支结束。 */
else /* 溢出次数仍可安全递增。 */
{ /* 正常累计分支开始。 */
period_count++; /* CNT 每从 0xFFFF 回到 0,就记录一个 65536 拍周期。 */
} /* 正常累计分支结束。 */
} /* 有效溢出统计分支结束。 */
} /* 周期回调结束。 */
/* ==================== Core/Src/stm32f1xx_it.c:共享 IRQ 分发入口 ==================== */
extern TIM_HandleTypeDef htim2; /* 声明 TIM2 句柄实体定义在 tim.c。 */
void TIM2_IRQHandler(void) /* TIM2 更新和 CH2 捕获共用这个 NVIC 向量入口。 */
{ /* TIM2 IRQ 函数体开始。 */
HAL_TIM_IRQHandler(&htim2); /* 让 HAL 根据 SR/DIER 分发到捕获或周期回调。 */
} /* TIM2 IRQ 函数结束。 */
/* ==================== 初始化阶段:两类事件都要启动 ==================== */
HAL_TIM_Base_Start_IT(&htim2); /* 允许更新中断,用于维护 period_count。 */
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_2); /* 允许 CH2 捕获中断,用于取得上升/下降沿。 */
一、输入捕获实验代码分析
1.1 代码分析
下降沿到达时,硬件已经把当时的 CNT 锁存在 CCR2。HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2) 读取的是这个锁存值,不是读取函数执行瞬间的 CNT,因此中断响应延迟通常不会直接增加测量脉宽。
若测量期间 CNT 没有溢出,捕获值就是从上升沿清零到下降沿的计数数。若发生溢出,正确公式是:
总计数 = 溢出次数 × (ARR + 1) + 最后一次CCR2
总时间 = 总计数 × 单个计数时间
本实验 ARR=0xFFFF、每格 1 μs,所以:
us_count = period_count × 0x10000 + ic_count
工程代码使用了 period_count * 0xFFFF,每次溢出会少算 1 μs;应使用 0x10000,或者写成更通用的 (uint32_t)(__HAL_TIM_GET_AUTORELOAD(&htim2) + 1U)。
建议参与计算的变量至少使用 32 位:
volatile uint8_t ic_state;
volatile uint32_t period_count;
volatile uint32_t ic_count;
uint64_t total_ticks;
total_ticks =
(uint64_t)period_count *
((uint64_t)__HAL_TIM_GET_AUTORELOAD(&htim2) + 1ULL) +
ic_count;
使用 64 位中间量可避免长脉宽时 32 位乘加溢出。若只测 0.5~2.5 ms 舵机脉冲,CNT 不会在 65.536 ms 内溢出,period_count 正常应为 0。
主循环打印完成后把 ic_state 恢复为 0。为避免中断在主循环读取多个共享变量时恰好改写它们,可以在极短的临界区内复制一份快照,再开放中断并完成格式化、串口发送。
1.2 定时器中断回调函数分析
这里分析的是定时器周期回调函数 HAL_TIM_PeriodElapsedCallback()。它由更新事件触发,用于记录计数器回绕:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if ((htim->Instance == TIM2) && (ic_state == 1U)) {
period_count++;
}
}
必须同时判断 htim->Instance == TIM2,因为 HAL 的周期回调可能被多个定时器共用;还要只在状态 1 累计,避免等待上升沿或处理结果时误计。
TIM2 的更新事件和 CH2 捕获事件共用同一个 TIM2_IRQHandler(),但它们是两类独立事件。HAL_TIM_Base_Start_IT() 使能更新中断,用来累计 period_count;HAL_TIM_IC_Start_IT() 使能捕获中断,用来切换边沿并读取 CCR2。二者都启动后,TIM2_IRQHandler() 再将挂起标志交给 HAL_TIM_IRQHandler() 分发到对应回调。缺少其中任意一个启动调用,测量范围或边沿处理都会不完整。
相关中断路径为:
flowchart TD
A[TIM2事件] --> B{事件类型}
B -->|CH2边沿| C[SR.CC2IF]
B -->|CNT溢出| D[SR.UIF]
C --> E[HAL_TIM_IRQHandler]
D --> E
E --> F[清CC2IF]
F --> G[HAL_TIM_IC_CaptureCallback]
E --> H[清UIF]
H --> I[HAL_TIM_PeriodElapsedCallback]
当捕获边沿恰好落在计数器溢出附近时,CC2IF 与 UIF 可能同时挂起。此时“捕获发生在溢出前还是溢出后”会影响总计数,单纯依赖两个回调的执行先后可能产生一个模数的误差。可采用以下办法提高可靠性:
- 在同一中断上下文中同时读取
SR、CCR2和CNT,依据标志与计数位置判断先后; - 维护扩展时间戳,在捕获时把高位溢出计数与
CCR2原子组合; - 使用 PWM Input/从模式复位,让硬件在有效边沿自动复位计数器;
- 本实验只测小于 65.536 ms 的舵机高脉宽时,可把 ARR 设计得更大或确认不会溢出,从根本上避开边界。
CC2OF 也值得检查:它表示上一次捕获尚未及时处理,又有新边沿到来。出现该标志时,当前测量可能已经丢失边沿,不应继续把数据当作可靠结果。
工程在溢出次数过多时把状态强制置为完成并给出特殊值,这更接近超时保护。更清晰的实现应设置独立的错误码,例如“等待下降沿超时”,主循环收到后打印故障信息并重新同步到下一个上升沿,而不是把超时值伪装成正常脉宽。