一、问题现象与背景
在使用STM32定时器输入捕获模式测量PWM频率时,许多开发者会遇到频率值偶尔跳变、测量结果不稳定甚至完全错误的情况。典型现象包括:
- 低频信号测量时频率值突然翻倍或减半
- 高频信号测量时数据随机跳动
- 系统运行一段时间后测量完全失效
这些问题往往源于溢出中断与捕获中断的优先级冲突。本文将以STM32F103系列为例,深入分析冲突机理并提供完整解决方案。
二、原理讲解:输入捕获与溢出中断的协作机制
2.1 输入捕获测量频率的基本原理
输入捕获通过记录两次捕获事件之间的定时器计数值差来计算频率:
频率 = 定时器时钟 / (捕获差值 + 溢出次数 × 溢出周期)
当PWM信号周期较长时,定时器计数值会在两次捕获之间发生溢出,因此需要溢出中断来累计溢出次数。
2.2 中断优先级冲突的根源
STM32的NVIC支持中断嵌套,但若配置不当会导致:
- 捕获中断被溢出中断打断:在读取捕获值时发生溢出,导致计数值错乱
- 溢出中断丢失:捕获中断执行时间过长,错过溢出事件
- 共享变量竞争:两个中断同时修改溢出计数变量
关键矛盾在于:捕获中断需要快速响应以记录精确时间戳,而溢出中断需要可靠执行以确保计数准确。
三、配置步骤:优先级与中断服务程序设计
3.1 定时器基础配置
以TIM2通道1为例,配置为输入捕获模式,测量PWM频率:
void TIM2_Capture_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// PA0作为TIM2_CH1输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 时基配置:72MHz/72=1MHz计数频率,周期65535
TIM_TimeBaseStructure.TIM_Period = 0xFFFF;
TIM_TimeBaseStructure.TIM_Prescaler = 71;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// 输入捕获配置:上升沿捕获,不分频
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICFilter = 0x0;
TIM_ICInit(TIM2, &TIM_ICInitStructure);
// 使能捕获中断和溢出中断
TIM_ITConfig(TIM2, TIM_IT_CC1 | TIM_IT_Update, ENABLE);
// 关键:捕获中断优先级高于溢出中断
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 捕获中断
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM2, ENABLE);
}
3.2 中断服务程序优化设计
错误做法:在捕获中断中直接计算频率,导致执行时间过长。
正确做法:捕获中断只记录时间戳,主循环计算频率。
volatile uint32_t capture_value = 0;
volatile uint32_t overflow_count = 0;
volatile uint8_t capture_flag = 0;
volatile uint32_t total_ticks = 0;
void TIM2_IRQHandler(void)
{
// 先处理捕获中断(高优先级)
if (TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_CC1);
uint32_t current_capture = TIM_GetCapture1(TIM2);
// 原子操作:读取溢出计数并计算总 ticks
__disable_irq(); // 短暂关中断,确保数据一致性
total_ticks = overflow_count * 0x10000 + current_capture;
__enable_irq();
capture_value = current_capture;
capture_flag = 1;
}
// 后处理溢出中断(低优先级)
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
overflow_count++;
}
}
3.3 主循环频率计算
float measure_frequency(void)
{
static uint32_t last_total_ticks = 0;
float freq = 0;
if (capture_flag)
{
capture_flag = 0;
uint32_t current_total = total_ticks;
uint32_t diff = current_total - last_total_ticks;
last_total_ticks = current_total;
if (diff > 0)
{
// 定时器计数频率1MHz,diff即为周期(us)
freq = 1000000.0f / diff;
}
}
return freq;
}
四、完整代码示例与验证
4.1 完整工程结构
// main.c
#include "stm32f10x.h"
#include "stdio.h"
volatile uint32_t capture_value = 0;
volatile uint32_t overflow_count = 0;
volatile uint8_t capture_flag = 0;
volatile uint32_t total_ticks = 0;
// 初始化代码同上...
int main(void)
{
SystemInit();
USART1_Init(115200);
TIM2_Capture_Init();
while(1)
{
float freq = measure_frequency();
if (freq > 0)
{
printf("Frequency: %.2f Hz\r\n", freq);
}
Delay_ms(500);
}
}
4.2 验证方法
使用信号发生器输入1kHz方波,观察串口输出:
- 正常情况:频率稳定在1000Hz±1Hz
- 冲突未解决:频率在500Hz~2000Hz之间跳变
五、注意事项与最佳实践
- 优先级配置原则:捕获中断抢占优先级必须高于溢出中断,建议捕获设为1,溢出设为2
-
临界区保护:读取多字节共享变量时使用
__disable_irq()/__enable_irq()或原子操作 - 中断服务程序精简:捕获中断中只做数据记录,复杂计算移至主循环
- 溢出计数清零时机:每次成功捕获后根据应用需求决定是否清零溢出计数
- 定时器时钟配置:确保预分频值使计数频率适中,避免高频信号溢出频繁
- 调试技巧:使用GPIO翻转配合示波器观察中断响应时间,验证优先级配置
- 备选方案:对于极低频信号,可考虑使用定时器从模式(复位模式)自动清零计数器
通过以上配置与优化,可彻底解决输入捕获测量PWM频率时的中断优先级冲突问题,实现稳定可靠的频率测量。
暂无评论,来抢沙发~