STM32定时器实战:用CubeMX和HAL库实现PWM测量与LCD显示(附完整代码)
在嵌入式开发中,定时器是最常用也最强大的外设之一。无论是简单的延时功能,还是复杂的PWM信号生成与捕获,定时器都能胜任。对于准备参加蓝桥杯嵌入式组比赛的开发者来说,掌握定时器的各种应用场景尤为重要。本文将带你完成一个完整的实战项目:利用STM32的定时器输入捕获功能测量外部PWM信号的频率和占空比,并通过LCD实时显示测量结果。
1. 项目概述与硬件准备
这个项目的主要目标是构建一个PWM信号分析仪,能够实时测量并显示输入PWM信号的频率和占空比。我们将使用STM32的定时器输入捕获功能来实现这一目标,并通过LCD模块直观地展示测量结果。
所需硬件组件:
- STM32开发板(本文以STM32G431为例)
- LCD显示屏(支持SPI或I2C接口)
- PWM信号源(可使用另一个定时器生成测试信号)
- 杜邦线若干
软件工具:
- STM32CubeMX(版本6.5.0或更高)
- Keil MDK-ARM或STM32CubeIDE
- STM32 HAL库
在开始之前,确保你已经熟悉STM32CubeMX的基本操作和HAL库的基本函数调用方式。如果你是完全新手,建议先完成几个简单的GPIO和定时器实验。
2. CubeMX工程配置
2.1 时钟树配置
首先打开CubeMX并创建新工程,选择你的STM32型号。进入时钟配置界面,根据你的开发板设置系统时钟。对于STM32G431,我们可以配置如下:
SYSCLK: 80MHz APB1 Timer clocks: 80MHz APB2 Timer clocks: 80MHz时钟配置直接影响定时器的精度,务必确保正确设置。保存时钟配置后,CubeMX会自动生成相应的初始化代码。
2.2 定时器配置
我们需要配置两个定时器:一个用于生成PWM信号(作为测试信号源),另一个用于捕获输入的PWM信号。
PWM生成定时器配置(TIM15):
- 选择TIM15,设置为PWM Generation CH1和CH2
- Prescaler: 79 (80分频,得到1MHz计数频率)
- Counter Mode: Up
- Period: 199 (200个计数周期,对应5kHz PWM)
- Pulse: 120 (占空比60%)
- CH2 Pulse: 50 (占空比25%)
输入捕获定时器配置(TIM2):
- 选择TIM2,设置为Input Capture direct mode和indirect mode
- Prescaler: 79 (80分频,1MHz计数频率)
- Counter Mode: Up
- Period: 65535 (最大计数值)
- Slave Mode: Reset Mode
- Trigger Source: TI1FP1
- Channel 1: Rising Edge, Direct TI
- Channel 2: Falling Edge, Indirect TI
这种配置实现了PWM输入模式,可以同时测量周期和占空比。TIM2会在上升沿复位计数器,在下降沿捕获通道2的值(高电平时间),在下一个上升沿捕获通道1的值(整个周期)。
2.3 LCD接口配置
根据你的LCD模块类型配置相应的接口。常见的有:
- SPI接口:配置SPI外设和CS、DC、RESET等控制引脚
- I2C接口:配置I2C外设
- 并行接口:配置FSMC或直接使用GPIO
确保在CubeMX中正确配置了所有必要的引脚,并生成初始化代码。
3. 代码实现
3.1 定时器初始化和启动
CubeMX生成的代码已经包含了定时器的基本配置,我们只需要添加启动代码:
void PWM_Input_Init(void) { // 启动PWM生成定时器 HAL_TIM_PWM_Start(&htim15, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim15, TIM_CHANNEL_2); // 启动输入捕获定时器 HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1); HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_2); // 如果需要,可以启动基本定时器用于计时 HAL_TIM_Base_Start_IT(&htim1); }3.2 输入捕获中断处理
输入捕获的核心逻辑在中断回调函数中实现。我们需要处理两个通道的捕获事件:
// 全局变量存储测量结果 uint32_t pwm_period_count = 0; uint32_t pwm_duty_count = 0; float pwm_duty = 0.0; uint16_t pwm_freq = 0; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if(htim->Instance == TIM2) { if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) { // 通道1捕获上升沿,获取周期 pwm_period_count = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1) + 1; pwm_freq = 1000000 / pwm_period_count; // 1MHz计数频率 } else if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2) { // 通道2捕获下降沿,获取高电平时间 pwm_duty_count = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2) + 1; pwm_duty = ((float)pwm_duty_count / pwm_period_count) * 100.0; } } }3.3 LCD显示实现
LCD显示部分需要根据你使用的具体LCD模块来实现。这里给出一个通用的显示函数框架:
void Update_LCD_Display(void) { char display_str[32]; // 清屏或局部刷新 LCD_Clear(WHITE); // 显示频率 sprintf(display_str, "Freq: %d Hz", pwm_freq); LCD_DisplayString(10, 50, (uint8_t *)display_str, BLACK, WHITE); // 显示占空比 sprintf(display_str, "Duty: %.1f %%", pwm_duty); LCD_DisplayString(10, 70, (uint8_t *)display_str, BLACK, WHITE); // 可以添加其他信息,如信号质量指示等 }4. 系统集成与调试
4.1 主循环实现
在主循环中,我们需要定期更新LCD显示,并可以添加一些调试信息:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); MX_TIM15_Init(); MX_LCD_Init(); PWM_Input_Init(); LCD_Init(); while (1) { Update_LCD_Display(); HAL_Delay(200); // 每200ms更新一次显示 } }4.2 常见问题与调试技巧
在实际开发中,你可能会遇到以下问题:
捕获不到信号:
- 检查GPIO配置是否正确,特别是复用功能
- 确认信号电压电平匹配(STM32通常是3.3V)
- 使用逻辑分析仪或示波器验证信号是否到达引脚
测量结果不准确:
- 检查定时器时钟配置是否正确
- 确保预分频器和周期值设置合理
- 对于高频信号,考虑使用更高性能的定时器
LCD显示异常:
- 验证LCD初始化序列是否正确
- 检查数据传输时序是否符合LCD规格要求
- 确保电源稳定,背光正常工作
调试建议:
- 使用STM32的调试功能,设置断点观察捕获值
- 在关键位置添加LED指示灯或串口打印调试信息
- 逐步验证每个功能模块,先确保PWM生成正确,再测试捕获功能
5. 进阶优化与扩展
5.1 多通道测量
如果需要同时测量多个PWM信号,可以配置多个定时器或使用一个定时器的多个通道:
// 配置TIM3为第二个输入捕获通道 HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1); HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_2);在回调函数中区分不同的定时器实例进行处理。
5.2 数字滤波
对于噪声较大的信号,可以启用定时器的输入捕获滤波器:
// 在CubeMX中配置Input Filter参数 sConfigIC.ICFilter = 6; // 设置适当的滤波值或者在软件中实现移动平均滤波:
#define FILTER_SIZE 5 uint32_t freq_buffer[FILTER_SIZE] = {0}; uint8_t buffer_index = 0; // 在捕获回调中 freq_buffer[buffer_index] = pwm_freq; buffer_index = (buffer_index + 1) % FILTER_SIZE; // 计算平均值 uint32_t avg_freq = 0; for(int i=0; i<FILTER_SIZE; i++) { avg_freq += freq_buffer[i]; } avg_freq /= FILTER_SIZE;5.3 自动量程切换
对于宽范围的频率测量,可以实现自动量程切换功能:
void Auto_Range_Adjust(void) { if(pwm_freq < 1000) { // 低频模式,增加预分频 htim2.Init.Prescaler = 799; // 100kHz计数 HAL_TIM_Base_Init(&htim2); } else { // 高频模式,减少预分频 htim2.Init.Prescaler = 79; // 1MHz计数 HAL_TIM_Base_Init(&htim2); } HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1); HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_2); }6. 完整工程代码结构
一个完整的STM32工程通常包含以下关键文件:
Project/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── tim.h │ │ └── lcd.h │ └── Src/ │ ├── main.c │ ├── tim.c │ └── lcd.c ├── Drivers/ │ ├── CMSIS/ │ └── STM32G4xx_HAL_Driver/ └── STM32CubeMX/ └── Project.ioc关键代码文件说明:
main.c: 包含主循环和系统初始化tim.c: 定时器配置和中断处理lcd.c: LCD驱动和显示函数Project.ioc: CubeMX工程配置文件
在蓝桥杯嵌入式比赛中,通常会提供基本的工程框架,你需要在此基础上添加功能模块。建议将不同功能封装成独立的.c/.h文件,保持代码结构清晰。