1. 项目概述
在嵌入式系统开发中,PWM(脉冲宽度调制)信号的测量是一个常见需求。无论是电机控制、LED调光还是电源管理,准确获取PWM的频率和占空比都是关键环节。本文将详细介绍如何使用STM32的输入捕获功能,通过HAL库实现PWM信号的精确测量。
我最近在一个智能家居项目中遇到了需要实时监测外部PWM信号的需求。经过多次调试和优化,最终形成了一套稳定可靠的测量方案。这个方案基于STM32F103C8T6(俗称"蓝色药丸")开发板,使用TIM2定时器的输入捕获功能,能够准确测量频率范围在1Hz-1MHz、占空比0-100%的PWM信号。
2. 硬件设计与原理
2.1 PWM信号基础
PWM信号由两个核心参数定义:
- 频率:信号完整周期重复的次数,单位Hz
- 占空比:高电平时间占整个周期的百分比
例如,一个频率为1kHz、占空比30%的信号,表示每1ms(1/1000秒)为一个周期,其中高电平持续0.3ms,低电平持续0.7ms。
2.2 STM32输入捕获原理
STM32的定时器输入捕获功能可以精确记录外部信号边沿发生的时刻。其工作原理是:
- 定时器以固定频率计数(本例中为1MHz)
- 当检测到指定边沿(上升或下降)时,将当前计数值锁存到捕获寄存器
- 触发中断通知CPU读取捕获值
通过交替捕获上升沿和下降沿,我们可以获得:
- 相邻上升沿的时间差 → 周期t
- 上升沿与下降沿的时间差 → 高电平时间t1
3. 开发环境配置
3.1 CubeMX设置
使用STM32CubeMX进行基础配置可以大幅减少底层代码编写量。关键配置步骤如下:
-
时钟配置:
- 将HCLK设置为72MHz(STM32F103的最大主频)
- 确保APB1定时器时钟为72MHz(TIM2挂载在APB1上)
-
TIM2配置:
- 选择通道1(PA0引脚)作为输入捕获
- 分频系数设为71(实际分频为72,得到1MHz计数频率)
- 自动重装载值设为最大值0xFFFF
- 启用输入捕获中断
注意:分频系数设置值为N-1,因此71对应实际分频72。这样72MHz时钟经分频后得到1MHz计数频率,每个计数代表1μs。
3.3 引脚配置
确保PA0引脚:
- 模式设置为"Input Capture direct mode"
- 不启用上拉/下拉电阻(外部信号应已有确定电平)
- 保持默认的GPIO速度
4. 软件实现详解
4.1 变量定义与初始化
在main.c文件中定义以下全局变量:
c复制volatile uint32_t falltime = 0; // 下降沿捕获值
volatile uint32_t periodValue = 0; // 周期值
volatile uint8_t dutyCycle = 0; // 占空比(0-100%)
volatile uint8_t captureState = 0; // 状态机状态
volatile uint8_t measurementReady = 0; // 测量完成标志
初始化代码:
c复制HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1); // 启动输入捕获中断
__HAL_TIM_SET_CAPTUREPOLARITY(&htim2, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING); // 初始设置为上升沿捕获
4.2 中断回调函数实现
核心逻辑在HAL_TIM_IC_CaptureCallback中实现,采用状态机设计:
c复制void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM2) {
switch (captureState) {
case 0: // 第一个上升沿
__HAL_TIM_SET_COUNTER(htim, 2); // 计数器复位(实测需要补偿2个周期)
captureState = 1;
// 切换为下降沿捕获
__HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING);
break;
case 1: // 下降沿捕获
falltime = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
captureState = 2;
// 切换为上升沿捕获
__HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING);
break;
case 2: // 第二个上升沿
periodValue = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
captureState = 0;
measurementReady = 1; // 设置测量完成标志
break;
}
}
}
调试心得:计数器复位值设为2而非0,是因为发现从边沿触发到实际计数器复位存在约2个时钟周期的延迟。这个补偿值在不同型号STM32上可能需要调整。
4.3 主循环处理
在主循环中处理测量结果:
c复制while (1) {
if (measurementReady) {
if (periodValue != 0) {
// 计算占空比(0-100%)
dutyCycle = (falltime * 100) / periodValue;
// 计算频率(Hz)
frequency = 72000000 / (72 * periodValue);
// 输出结果
printf("Freq: %d Hz, Duty: %d%%\r\n", frequency, dutyCycle);
}
measurementReady = 0; // 清除标志
}
HAL_Delay(10); // 适当延时减少CPU负载
}
5. 测量精度与误差分析
5.1 理论精度
基于1MHz的计数频率:
- 时间分辨率:1μs
- 频率测量误差:±1Hz(对于1kHz信号)
- 占空比误差:±0.1%(对于1kHz信号)
5.2 实际误差来源
-
中断延迟:
- 从边沿触发到中断服务程序执行存在延迟
- 解决方法:使用补偿值(如代码中的+2)
-
信号抖动:
- 输入信号可能存在噪声
- 解决方法:硬件滤波(RC电路)或软件数字滤波
-
时钟精度:
- 内部RC振荡器可能有±1%误差
- 高精度应用建议使用外部晶振
6. 性能优化技巧
6.1 提高测量上限频率
默认配置下最大可测频率约500kHz。如需测量更高频率:
- 减少分频系数(如设为35,得到2MHz计数频率)
- 使用定时器的PWM输入模式(自动测量周期和占空比)
- 启用DMA传输捕获值,减少中断开销
6.2 低频率测量优化
对于低于10Hz的信号:
- 增大分频系数(如720,得到100kHz计数频率)
- 使用32位定时器(如TIM2在某些型号支持32位计数)
- 实现周期计数+捕获的混合测量方法
7. 常见问题与解决方法
7.1 测量值不稳定
现象:同一信号连续测量结果波动较大
可能原因:
- 信号质量差(噪声或振铃)
- 中断优先级设置不当导致丢失边沿
解决方案:
- 在输入引脚添加10kΩ上拉/下拉电阻
- 并联100pF电容滤波
- 提高定时器中断优先级
7.2 无法捕获高频信号
现象:高频信号测量值明显偏小
可能原因:
- 中断处理时间超过信号周期
解决方案:
- 优化中断服务程序(减少不必要的操作)
- 使用寄存器操作替代HAL库函数
- 改用DMA方式传输捕获值
7.3 占空比计算异常
现象:占空比始终为0或100%
可能原因:
- 边沿极性设置错误
- 信号幅度不足
解决方案:
- 用示波器确认信号实际波形
- 检查TIMx_CCER寄存器中的CCxP位
- 确保信号电压在VIL和VIH之间
8. 扩展应用
8.1 多通道同时测量
通过配置多个定时器通道,可以同时测量多路PWM信号:
- 使用TIM2的通道1和通道2
- 为每个通道独立设置捕获极性
- 在回调函数中通过htim->Channel区分通道
8.2 与PWM生成联动
实现闭环控制时,可以同时使用定时器的输入捕获和PWM生成功能:
c复制// 配置TIM3输出PWM
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 50); // 50%占空比
// 配置TIM2输入捕获
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);
8.3 与RTOS集成
在FreeRTOS等实时系统中使用时需注意:
- 将中断优先级设置为高于RTOS系统中断
- 使用任务通知或队列传递测量结果
- 避免在中断中进行耗时操作
c复制// 在中断中发送通知
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR(xTaskHandle, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
经过实际项目验证,这套测量方案在工业控制、无人机电调信号检测等场景中表现稳定可靠。关键是要根据具体应用场景调整参数和优化实现细节。