蓝桥杯嵌入式备赛避坑指南:STM32G431的PWM、ADC与浮点数比较那些事儿
参加蓝桥杯嵌入式比赛的同学,往往会在STM32G431的开发过程中遇到一些看似简单却容易踩坑的技术点。本文将聚焦三个最容易出问题的环节:双路PWM配置、ADC校准和浮点数比较,结合实战经验给出解决方案。
1. 双路独立频率PWM的定时器配置陷阱
很多同学第一次使用STM32G431的TIM16和TIM17定时器实现双路PWM时,会遇到频率无法独立调节的问题。这里的关键在于理解定时器工作频率与PWM频率的关系。
1.1 定时器基础配置
正确的配置流程应该是:
- 使能定时器时钟
- 配置时基单元
- 配置PWM模式
- 使能通道输出
- 启动定时器
// TIM16配置示例(100Hz) htim16.Instance = TIM16; htim16.Init.Prescaler = 79; // 80MHz/(79+1)=1MHz htim16.Init.CounterMode = TIM_COUNTERMODE_UP; htim16.Init.Period = 9999; // 1MHz/(9999+1)=100Hz htim16.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&htim16); // TIM17配置示例(200Hz) htim17.Instance = TIM17; htim17.Init.Prescaler = 39; // 80MHz/(39+1)=2MHz htim17.Init.CounterMode = TIM_COUNTERMODE_UP; htim17.Init.Period = 9999; // 2MHz/(9999+1)=200Hz htim17.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&htim17);1.2 常见错误排查
错误现象1:两路PWM频率相同
检查点:确认TIM16和TIM17的Prescaler和Period参数是否独立设置
错误现象2:PWM无输出
检查点:
- GPIO是否配置为复用功能
- 定时器是否启动
- 通道输出是否使能
错误现象3:占空比调节不生效
检查点:CCR寄存器值是否在0-ARR范围内
2. ADC校准的重要性与实战技巧
ADC读数不准是嵌入式开发中的常见问题,特别是当发现电压测量值与实际值存在明显偏差时(如3.25V vs 3.30V),很可能是因为忽略了校准环节。
2.1 校准流程详解
完整的ADC校准应包含以下步骤:
- 上电后等待电压稳定
- 执行校准
- 验证校准结果
// ADC校准代码示例 HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED); HAL_Delay(100); // 等待校准完成 // 验证校准 uint32_t adcValue = 0; HAL_ADC_Start(&hadc1); if(HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) { adcValue = HAL_ADC_GetValue(&hadc1); } float voltage = adcValue * 3.3f / 4095.0f;2.2 校准前后的数据对比
| 条件 | 测量值(3.3V理论值) | 误差 |
|---|---|---|
| 未校准 | 3.25V | 1.5% |
| 已校准 | 3.30V | <0.1% |
2.3 进阶技巧
- 定期校准:温度变化会影响ADC精度,建议在温度变化较大时重新校准
- 参考电压:确保供电电压稳定,必要时使用外部参考电压源
- 采样时间:适当增加采样时间可以提高精度
3. 嵌入式C语言中浮点数比较的陷阱
在ADC值处理和PWM占空比计算中,浮点数比较是一个容易被忽视但会导致严重逻辑错误的问题。
3.1 为什么不能直接比较浮点数?
浮点数在内存中的存储方式决定了它存在精度问题。例如:
float a = 3.3f; float b = 1.1f + 1.1f + 1.1f; if(a == b) { // 这个判断很可能不成立 // ... }3.2 正确的比较方法
使用误差范围(EPSILON)进行比较:
#define EPSILON 1e-6f int float_equal(float a, float b) { return fabs(a - b) < EPSILON; } // 实际应用示例 if(float_equal(V, 0.0f)) { // 处理0V情况 } else if(float_equal(V, 3.3f)) { // 处理3.3V情况 } else { // 处理中间值 }3.3 不同场景下的EPSILON选择
| 应用场景 | 推荐EPSILON值 | 考虑因素 |
|---|---|---|
| 电压测量 | 1e-4 | 12位ADC精度 |
| 温度传感 | 1e-3 | 传感器精度 |
| 电机控制 | 1e-5 | 高精度需求 |
4. 综合应用:完整PWM控制流程
结合上述三个知识点,我们来看一个完整的PWM控制实现流程。
4.1 系统初始化
void System_Init(void) { // 1. 初始化硬件 HAL_Init(); SystemClock_Config(); // 2. ADC校准 HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED); // 3. PWM定时器初始化 MX_TIM16_Init(); MX_TIM17_Init(); HAL_TIM_PWM_Start(&htim16, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim17, TIM_CHANNEL_1); // 4. 默认占空比 __HAL_TIM_SET_COMPARE(&htim16, TIM_CHANNEL_1, 10); __HAL_TIM_SET_COMPARE(&htim17, TIM_CHANNEL_1, 10); }4.2 主循环处理
void Main_Process(void) { float voltage = Read_Voltage(); if(float_equal(voltage, 0.0f)) { Set_PWM_Duty(0, 0); } else if(float_equal(voltage, 3.3f)) { Set_PWM_Duty(100, 100); } else { float duty = voltage / 3.3f * 100.0f; Set_PWM_Duty(duty, duty); } }4.3 关键函数实现
float Read_Voltage(void) { uint32_t adcValue = 0; HAL_ADC_Start(&hadc1); if(HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) { adcValue = HAL_ADC_GetValue(&hadc1); } return adcValue * 3.3f / 4095.0f; } void Set_PWM_Duty(float duty1, float duty2) { // 限制占空比范围 duty1 = (duty1 < 0) ? 0 : (duty1 > 100) ? 100 : duty1; duty2 = (duty2 < 0) ? 0 : (duty2 > 100) ? 100 : duty2; __HAL_TIM_SET_COMPARE(&htim16, TIM_CHANNEL_1, duty1); __HAL_TIM_SET_COMPARE(&htim17, TIM_CHANNEL_1, duty2); }在实际项目中,我发现最容易出错的地方往往是最基础的环节。比如有一次调试时PWM完全无输出,花了两个小时才发现是GPIO复用功能没有正确配置。另一个常见的疏忽是忘记调用HAL_TIM_PWM_Start()函数,导致定时器虽然配置正确但没有实际运行。