在嵌入式系统开发中,精确测量外部信号的频率和占空比是常见需求。这次我在蓝桥杯嵌入式竞赛项目中,基于STM32实现了PWM信号的捕获功能。这个功能看似基础,但实际开发中会遇到不少细节问题,比如定时器配置、捕获模式选择、数值计算等。下面我将完整分享从硬件配置到代码实现的全部过程,包括那些官方手册不会告诉你的实战经验。
我使用的是蓝桥杯官方指定的STM32G431RB开发板,这款MCU内置了丰富的高级定时器,特别适合做精确的信号捕获。核心参数如下:
提示:建议使用信号发生器时,初始测试频率设置在1kHz左右,占空比50%,这样便于验证基础功能是否正常。
在CubeMX中,我选择了TIM3作为输入捕获定时器,具体参数设置如下:
这样配置后,定时器的实际计数频率等于APB1总线时钟频率(在我的配置中是170MHz)。
关键配置点在于两个捕获通道的设置差异:
通道1(PA6):
通道2(PA7):
这种双通道组合测量方式可以同时捕获周期和高电平时间,是测量占空比的经典方法。
必须开启定时器的全局中断和捕获/比较中断:
注意:如果忘记开启中断,回调函数将永远不会被触发,这是新手常犯的错误。
在main.c中,需要启动定时器和两个捕获通道:
c复制HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_2); // 直接模式
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1); // 间接模式
这里有个细节:必须先启动直接模式通道,再启动间接模式通道,否则可能导致首次捕获值异常。
核心逻辑都在HAL_TIM_IC_CaptureCallback回调函数中:
c复制uint16_t freq; // 捕获到的频率(单位:Hz)
uint16_t value; // 直接捕获值(周期)
uint16_t value1; // 间接捕获值(高电平时间)
float duty; // 占空比(百分比)
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef* htim)
{
if (htim->Instance == TIM3 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2)
{
value = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2); // 周期
value1 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 高电平时间
__HAL_TIM_SetCounter(htim, 0); // 重置计数器
if (value == 0) return; // 避免除零错误
// 计算频率(假设定时器时钟为1MHz)
freq = 1000000 / value;
// 计算占空比
duty = ((float)value1 / (float)value) * 100;
}
}
这里有个关键点:代码中假设定时器时钟是1MHz,但实际上我的配置是170MHz。这是因为我额外做了以下处理:
如果不做这个分频,计算公式应该改为:
c复制freq = SystemCoreClock / (htim3.Init.Prescaler + 1) / value;
在task.c中,我将测量结果显示在LCD上:
c复制void lcd_proc()
{
lcddisplay(Line2, " PA1 F:%d D:%.f", Freq_PA1, Duty_PA1);
lcddisplay(Line3, " PA6 F:%d D:%.f", Freq_PA6, Duty_PA6);
lcddisplay(Line4, " PA7_F:%d D:%.f ", freq, duty);
}
显示格式说明:
初期测试时发现测量值不稳定,可能有以下原因:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 频率值跳动大 | 信号噪声 | 增加硬件滤波或软件均值滤波 |
| 占空比不准 | 边沿检测抖动 | 调整输入捕获滤波器参数 |
| 偶尔捕获失败 | 中断优先级冲突 | 调整NVIC优先级 |
我最终采用的解决方案是:
定时器捕获模式有其频率上限,主要受以下因素限制:
实测建议:
经过多次测试,我总结了几个提升测量精度的技巧:
一个实用的动态分频实现示例:
c复制void adjust_prescaler(uint32_t expected_freq)
{
uint32_t optimal_prescaler = SystemCoreClock / expected_freq / 65536;
__HAL_TIM_SET_PRESCALER(&htim3, optimal_prescaler);
}
这个基础功能可以扩展出许多实用应用:
以电机测速为例,只需要将编码器信号接入捕获引脚,然后在计算频率后转换为RPM:
c复制// 假设编码器每转输出600个脉冲
float rpm = freq * 60 / 600;
为确保测量准确性,我设计了以下验证方法:
测试数据示例:
| 输入频率 | 输入占空比 | 测量频率 | 测量占空比 | 误差 |
|---|---|---|---|---|
| 1kHz | 50% | 1000Hz | 50.1% | 0.1% |
| 10kHz | 25% | 9998Hz | 25.2% | 0.2% |
| 100kHz | 75% | 99.95kHz | 75.3% | 0.3% |
对于竞赛项目,良好的代码结构很重要:
code复制/Project
/Core
/Src
main.c // 主循环和初始化
stm32g4xx_it.c // 中断服务程序
tim.c // 定时器配置
/Inc
// 对应头文件
/Drivers
/User
task.c // 业务逻辑
lcd.c // 显示驱动
measurement.c // 测量算法
特别建议将测量相关代码单独放在measurement.c中,便于复用和维护。