在嵌入式系统开发中,PWM信号频率测量是个经典问题。就拿蓝桥杯嵌入式竞赛来说,选手经常需要处理电机转速检测、传感器信号采集等任务,这些都离不开对PWM信号的精确测量。我当年第一次参赛时,就被这个需求难住了——用普通输入捕获模式测量低频信号时,总是遇到定时器溢出的问题,数据跳变得像过山车一样刺激。
传统方法有个致命缺陷:当信号周期超过定时器最大计数值时,需要手动处理溢出中断。这不仅增加代码复杂度,还引入了累计误差。直到我发现STM32定时器的从模式复位机制,问题才迎刃而解。这个机制的精妙之处在于,它能自动在每次信号上升沿复位计数器,相当于给每个测量周期都做了"归零"操作。实测下来,在1MHz定时器时钟下,测量710Hz~22kHz范围的信号,误差可以控制在0.1%以内。
开发板上的R39(PB4)和R40(PA15)是两个PWM输出口,我用示波器实测它们的频率范围分别是710Hz-22.4kHz和630Hz-22.0kHz。这里有个重要计算:最大周期Tmax=1/最小频率=1/710≈0.00141秒。系统时钟经过80分频后,定时器时钟为1MHz(每个计数代表1μs),16位定时器最大计数值65535对应0.065535秒,远大于信号周期,所以完全不用担心溢出问题。
核心在于配置TIM3的从模式复位功能:
这种配置下,定时器会变成"自律型"工作模式——每次检测到上升沿就自动清零计数器,同时记录当前计数值。相当于硬件层面实现了"每个周期都是独立测量"的效果。
打开CubeMX新建工程,选择对应型号后:
这才是关键步骤:
建议创建独立的PWM处理模块:
code复制/Drivers
/BSP
bsp_pwm.c
bsp_pwm.h
把TIM3相关初始化代码从生成的tim.c中提取出来,放到bsp_pwm.c中集中管理。特别注意:所有中断共享的句柄变量(如htim3)需要在头文件中用extern声明。
在stm32f1xx_it.c中添加:
c复制void TIM3_IRQHandler(void) {
HAL_TIM_IRQHandler(&htim3);
}
然后在main.c中实现回调函数:
c复制void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {
if(htim->Instance == TIM3) {
PWM_T_Count = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
// 单位换算:计数值*1us = 周期(μs)
LCD_ShowNum(30, 100, PWM_T_Count, 5);
}
}
c复制HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);
c复制uint32_t freq = 1000000 / PWM_T_Count; // 单位Hz
实际测试时发现,当旋钮接近极限位置时,测量值会出现跳变。这是因为机械触点产生了抖动。解决方法有两种:
推荐的中值滤波实现:
c复制#define FILTER_SIZE 5
uint32_t filter_buf[FILTER_SIZE];
void HAL_TIM_IC_CaptureCallback(...) {
static uint8_t index = 0;
filter_buf[index++] = HAL_TIM_ReadCapturedValue(...);
if(index >= FILTER_SIZE) index = 0;
uint32_t sorted[FILTER_SIZE];
memcpy(sorted, filter_buf, sizeof(sorted));
bubble_sort(sorted, FILTER_SIZE); // 实现简单的冒泡排序
PWM_T_Count = sorted[FILTER_SIZE/2]; // 取中值
}
如果需要同时测量两路PWM(如R39和R40),可以采用TIM3的双通道捕获:
c复制if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) {
// 处理通道1数据
} else if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2) {
// 处理通道2数据
}
当需要测量更高频率时,可以:
对于电池供电设备:
在去年蓝桥杯省赛中,有个题目要求实时显示电机转速并通过串口上报。我的解决方案就是基于这个复位捕获方案:
这个方案的优势在于: