STM32C552定时器输入捕获:频率与占空比精确测量实战指南

📅 2026/7/10 2:19:05 👁️ 阅读次数 📝 编程学习
STM32C552定时器输入捕获:频率与占空比精确测量实战指南

🚀 30+款热门AI模型一站整合,DeepSeek/GLM/Qwen 随心用,限时 5 折。 👉 点击领海量免费额度

1. 先搞清楚输入捕获到底能测什么频率范围

STM32C552的定时器输入捕获功能,最直接的价值就是能精确测量外部信号的频率和占空比。但很多人一上来就卡在"为什么测不准"这个问题上,其实关键不是代码写错,而是没搞清楚输入捕获的测量边界。

输入捕获测量频率的核心原理很简单:通过捕获信号边沿(上升沿/下降沿)的时间戳,计算两个边沿之间的时间差。但这个时间差能测多宽的范围,完全取决于定时器的计数频率和计数器位数。

以STM32C552的TIM15为例,如果系统时钟是144MHz,预分频设置为143,那么定时器的实际计数频率就是1MHz(每个计数代表1微秒)。16位计数器的最大值是65535,所以单次能测量的最大周期就是65.535毫秒,对应最低频率约15.26Hz。

这意味着如果你想测量10Hz以下的低频信号,直接使用16位计数器就会遇到溢出问题。反过来,如果要测量100kHz以上的高频信号,1MHz的计数频率可能分辨率不够,误差会比较大。

所以第一步不是急着写代码,而是先算清楚:

  • 你要测的信号频率范围是多少?
  • 在这个范围内,定时器配置能不能覆盖?
  • 如果信号频率过低,是否需要处理计数器溢出?
  • 如果信号频率过高,是否需要提高定时器计数频率?

2. TIM15输入捕获的硬件连接和CubeMX配置

实际接线时,最容易出错的就是信号路径。很多人以为随便找个GPIO接上就能测,其实输入捕获通道有固定的引脚映射。

TIM15_CH1对应PA2引脚,但有些封装可能映射到其他引脚。在开始之前,一定要查数据手册确认具体的引脚分配。

CubeMX配置时,这几个参数最容易设置错误:

2.1 时钟树配置

先确认TIM15的输入时钟频率。如果系统时钟配置为144MHz,那么TIM15的时钟通常也是144MHz(除非APB总线有分频)。这个基础频率直接影响后续所有计算。

2.2 定时器基本参数

在TIM15配置界面:

  • Prescaler(预分频器):决定定时器的计数频率
  • Counter Period(自动重装载值):设置计数器的最大值
  • Clock Division(时钟分频):通常保持默认值1
  • Auto-reload preload(自动重载预装载):建议使能,避免更新时产生毛刺

关键计算:

// 假设系统时钟144MHz,想要1MHz计数频率 TIM15_PSC = (144000000 / 1000000) - 1 = 143 TIM15_ARR = 0xFFFF // 16位最大值

2.3 输入捕获通道配置

在TIM15的Channel1配置中:

  • Mode:选择Input Capture direct mode
  • IC Selection:选择Direct
  • IC Polarity:这是关键!如果要同时测量频率和占空比,选择Rising and Falling Edge
  • IC Prescaler:选择No division(每个边沿都捕获)
  • IC Filter:根据信号质量选择,信号干净时选择No Filter

极性选择的重要性:

  • Rising Edge Only:只捕获上升沿,只能测频率
  • Falling Edge Only:只捕获下降沿,同样只能测频率
  • Rising and Falling Edge:同时捕获上升沿和下降沿,能同时测频率和占空比

2.4 NVIC中断配置

必须开启TIM15全局中断,否则捕获事件无法触发中断回调函数。在NVIC配置中使能TIM15 global interrupt,并设置合适的优先级。

3. 输入捕获的核心代码实现

配置完成后,代码部分需要处理几个关键环节:初始化、中断回调、数据计算和溢出处理。

3.1 定时器初始化和启动

// 定时器参数配置函数 hal_status_t TIM15_IC_Config(uint32_t psc, uint32_t arr) { hal_tim_handle_t* htim15 = mx_tim15_gethandle(); if (htim15 == NULL) return HAL_ERROR; // 设置预分频器 if (HAL_TIM_SetPrescaler(htim15, psc) != HAL_OK) return HAL_ERROR; // 设置自动重装载值 if (HAL_TIM_SetPeriod(htim15, arr) != HAL_OK) return HAL_ERROR; // 计数器清零 if (HAL_TIM_SetCounter(htim15, 0) != HAL_OK) return HAL_ERROR; return HAL_OK; } // 启动输入捕获 hal_status_t TIM15_IC_Start(void) { hal_tim_handle_t* htim15 = mx_tim15_gethandle(); if (htim15 == NULL) return HAL_ERROR; // 启动通道1的输入捕获中断 if (HAL_TIM_IC_StartChannel_IT(htim15, HAL_TIM_CHANNEL_1) != HAL_OK) return HAL_ERROR; // 启动定时器计数器 if (HAL_TIM_Start(htim15) != HAL_OK) return HAL_ERROR; return HAL_OK; }

3.2 计数器溢出处理

16位计数器最大到65535就会归零,所以两次捕获值相减时必须考虑溢出情况:

static uint32_t TIM15_GetDiff(uint32_t start, uint32_t end) { if (end >= start) { return end - start; // 正常情况,直接相减 } else { return (0xFFFF + 1 - start) + end; // 发生溢出,分段计算 } }

这个函数是测量准确性的关键。如果没有溢出处理,当信号周期超过65.535ms时,计算结果会完全错误。

3.3 输入捕获中断回调函数

这是最核心的逻辑部分,需要区分上升沿和下降沿,并计算周期和占空比:

// 全局变量定义 static volatile uint32_t last_rise = 0; // 上一次上升沿捕获值 static volatile uint32_t current_rise = 0; // 当前上升沿捕获值 static volatile uint32_t current_fall = 0; // 当前下降沿捕获值 static volatile uint8_t rise_valid = 0; // 上升沿有效标志 static volatile uint8_t fall_valid = 0; // 下降沿有效标志 void HAL_TIM_IC_Callback(hal_tim_handle_t* htim, hal_tim_channel_t channel) { if ((htim != mx_tim15_gethandle()) || (channel != HAL_TIM_CHANNEL_1)) { return; // 确保是TIM15通道1的中断 } uint32_t capture_value = HAL_TIM_ReadCapturedValue(htim, HAL_TIM_CHANNEL_1); uint32_t pin_level = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_2); // 读取PA2当前电平 if (pin_level == GPIO_PIN_SET) { // 当前为高电平,说明刚发生上升沿 if (rise_valid == 0) { last_rise = capture_value; // 第一次上升沿,只记录不计算 rise_valid = 1; fall_valid = 0; } else { current_rise = capture_value; // 计算周期(上升沿到上升沿) uint32_t period = TIM15_GetDiff(last_rise, current_rise); if ((period != 0) && (fall_valid != 0)) { // 计算高电平时间(上升沿到下降沿) uint32_t high_time = TIM15_GetDiff(last_rise, current_fall); if (high_time <= period) { // 计算频率和占空比 uint32_t freq = 1000000 / period; // 1MHz计数频率,单位Hz uint32_t duty = (high_time * 100) / period; // 占空比百分比 // 更新测量结果(需要加中断保护) __disable_irq(); measured_freq = freq; measured_duty = duty; measurement_ready = 1; __enable_irq(); } } last_rise = current_rise; fall_valid = 0; } } else { // 当前为低电平,说明刚发生下降沿 if (rise_valid != 0) { current_fall = capture_value; fall_valid = 1; } } }

3.4 主循环中的数据处理

在主循环中,需要安全地读取测量结果:

while (1) { if (measurement_ready) { uint32_t freq, duty; // 关中断读取变量,避免数据不一致 __disable_irq(); measurement_ready = 0; freq = measured_freq; duty = measured_duty; __enable_irq(); printf("频率: %lu Hz, 占空比: %lu%%\r\n", freq, duty); HAL_Delay(1000); // 每秒打印一次 } }

4. 实际测量中的精度问题和优化方案

即使代码完全正确,实际测量时还是会有误差。这些误差主要来自几个方面:

4.1 时钟精度误差

STM32的内部RC振荡器精度大约在1%左右,如果对频率测量精度要求高,建议使用外部晶振。外部晶振的精度通常可以达到10-50ppm,比内部RC振荡器高两个数量级。

4.2 量化误差

这是数字测量的固有误差。比如1MHz的计数频率,每个计数代表1微秒,那么测量结果的最小分辨率就是1微秒。对于1kHz的信号(周期1ms),量化误差最大可达0.1%。

降低量化误差的方法:

  • 提高定时器计数频率(减小预分频值)
  • 测量多个周期求平均值
  • 使用更高位的计数器(有些STM32型号有32位定时器)

4.3 信号边沿抖动

如果被测信号有噪声,边沿检测会有抖动,导致测量结果不稳定。

解决方法:

  • 在CubeMX中配置输入滤波(Input Filter)
  • 软件上多次测量取平均值
  • 硬件上增加RC滤波电路

4.4 中断响应延迟

中断响应和处理需要时间,这会影响高频率信号的测量精度。

优化建议:

  • 中断服务函数尽量简洁
  • 使用DMA传输捕获值(如果支持)
  • 对于高频信号,考虑使用定时器的编码器模式或其他专用测量方式

5. 不同频率范围的配置建议

根据你要测量的频率范围,需要调整定时器配置:

5.1 低频信号测量(1Hz-100Hz)

对于低频信号,最大的问题是计数器溢出。解决方案:

// 降低计数频率,扩展测量范围 #define TIM15_PSC 1439 // 计数频率100kHz #define TIM15_ARR 0xFFFF // 此时每个计数代表10微秒 // 最大可测量周期:65535 * 10us = 0.65535秒 → 最低频率1.53Hz

5.2 中频信号测量(100Hz-10kHz)

这是输入捕获最擅长的范围,使用1MHz计数频率:

#define TIM15_PSC 143 // 计数频率1MHz #define TIM15_ARR 0xFFFF // 每个计数1微秒,适合测量周期100us-10ms的信号

5.3 高频信号测量(10kHz-100kHz)

高频信号需要更高的计数频率以减少量化误差:

#define TIM15_PSC 71 // 计数频率2MHz #define TIM15_ARR 0xFFFF // 每个计数0.5微秒,提高分辨率

5.4 超高频信号(100kHz以上)

对于100kHz以上的信号,输入捕获可能不是最佳选择,因为:

  • 中断响应时间成为瓶颈
  • 量化误差相对较大
  • 系统负担较重

替代方案:

  • 使用定时器的PWM输入模式(专门测量高频PWM)
  • 使用外部计数器和定时器配合
  • 考虑使用频率计数器芯片

6. 常见问题排查指南

当测量结果不正常时,按这个顺序排查:

6.1 完全无输出

  1. 检查硬件连接

    • 信号源是否正常工作
    • 接线是否正确(信号→PA2,地线→GND)
    • 用示波器确认信号是否到达STM32引脚
  2. 检查CubeMX配置

    • TIM15时钟是否使能
    • GPIO是否正确配置为复用功能
    • NVIC中断是否使能
    • 输入捕获通道配置是否正确
  3. 检查代码初始化

    • 定时器参数配置函数是否调用
    • 输入捕获启动函数是否调用
    • 主循环中是否处理测量结果

6.2 测量值不稳定

  1. 信号质量问题

    • 检查信号是否有噪声
    • 尝试增加输入滤波器
    • 确认信号幅度符合STM32要求(0-3.3V)
  2. 代码逻辑问题

    • 检查溢出处理函数是否正确
    • 确认边沿判断逻辑
    • 检查变量是否使用volatile修饰
  3. 系统负载问题

    • 确认没有其他高优先级中断影响
    • 检查是否在中断中进行了复杂操作

6.3 测量值有固定偏差

  1. 时钟精度问题

    • 检查系统时钟配置
    • 考虑使用外部晶振
    • 校准时钟频率
  2. 计算公式问题

    • 确认计数频率计算正确
    • 检查预分频值设置
    • 验证频率计算公式

6.4 只能测频率不能测占空比

  1. 极性配置问题

    • 确认CubeMX中设置为Rising and Falling Edge
    • 检查代码中的边沿判断逻辑
  2. 下降沿捕获问题

    • 确认下降沿也能进入中断
    • 检查下降沿处理代码是否执行

7. 进阶应用:多通道测量和自动量程

掌握了单通道测量后,可以进一步扩展功能:

7.1 多通道同时测量

TIM15有多个输入捕获通道,可以同时测量多个信号:

// 启动多个通道 HAL_TIM_IC_StartChannel_IT(htim15, HAL_TIM_CHANNEL_1); HAL_TIM_IC_StartChannel_IT(htim15, HAL_TIM_CHANNEL_2); // 在回调函数中区分通道 void HAL_TIM_IC_Callback(hal_tim_handle_t* htim, hal_tim_channel_t channel) { switch (channel) { case HAL_TIM_CHANNEL_1: // 处理通道1 break; case HAL_TIM_CHANNEL_2: // 处理通道2 break; } }

7.2 自动量程功能

对于未知频率的信号,可以实现自动量程:

// 先快速测量大致频率 uint32_t quick_measure_freq(void) { // 使用高计数频率快速测量 TIM15_IC_Config(71, 0xFFFF); // 2MHz // 测量几个周期 // 返回大致频率 } // 根据大致频率选择最优配置 void auto_range_config(uint32_t approx_freq) { if (approx_freq < 100) { TIM15_IC_Config(1439, 0xFFFF); // 100kHz,适合低频 } else if (approx_freq < 10000) { TIM15_IC_Config(143, 0xFFFF); // 1MHz,适合中频 } else { TIM15_IC_Config(71, 0xFFFF); // 2MHz,适合高频 } }

7.3 使用DMA减少CPU负担

对于需要连续测量的应用,可以使用DMA自动传输捕获值:

// 启动输入捕获DMA模式 HAL_TIM_IC_Start_DMA(htim15, HAL_TIM_CHANNEL_1, buffer, buffer_size); // DMA传输完成回调函数 void HAL_TIM_IC_CaptureCallback(hal_tim_handle_t* htim) { // 处理DMA缓冲区中的数据 }

输入捕获功能真正落地时,最该关注的不是代码能不能编译通过,而是测量结果的稳定性和准确性。建议先用信号发生器产生已知频率的信号进行验证,确认测量系统工作正常后再接入实际信号。

对于长期使用的应用,还要考虑异常情况的处理,比如信号丢失、频率突变等情况,确保系统在各种条件下都能稳定工作。

🚀 30+款热门AI模型一站整合,DeepSeek/GLM/Qwen 随心用,限时 5 折。 👉 点击领海量免费额度