输入捕获实验代码2

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-10-08 01:26:37 Views: 1 Likes: 0 Comments: 0

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 可能同时挂起。此时“捕获发生在溢出前还是溢出后”会影响总计数,单纯依赖两个回调的执行先后可能产生一个模数的误差。可采用以下办法提高可靠性:

  1. 在同一中断上下文中同时读取 SR、CCR2 和 CNT,依据标志与计数位置判断先后;
  2. 维护扩展时间戳,在捕获时把高位溢出计数与 CCR2 原子组合;
  3. 使用 PWM Input/从模式复位,让硬件在有效边沿自动复位计数器;
  4. 本实验只测小于 65.536 ms 的舵机高脉宽时,可把 ARR 设计得更大或确认不会溢出,从根本上避开边界。

CC2OF 也值得检查:它表示上一次捕获尚未及时处理,又有新边沿到来。出现该标志时,当前测量可能已经丢失边沿,不应继续把数据当作可靠结果。

工程在溢出次数过多时把状态强制置为完成并给出特殊值,这更接近超时保护。更清晰的实现应设置独立的错误码,例如“等待下降沿超时”,主循环收到后打印故障信息并重新同步到下一个上升沿,而不是把超时值伪装成正常脉宽。