1. 蓝桥杯嵌入式开发实战:PWM信号生成与采集系统设计
作为一名参加过多次电子设计竞赛的嵌入式开发者,我想分享一套在蓝桥杯竞赛中验证过的PWM信号生成与采集系统方案。这个系统通过STM32CubeMX配置实现了PWM信号的可调输出、两路模拟信号采集、脉冲信号捕获以及LCD人机交互功能,特别适合需要快速搭建嵌入式控制系统的场景。
系统核心功能包括:
- 通过TIM3产生频率可调(1000-2000Hz)、占空比可调(10%-80%)的PWM信号
- 通过ADC1和ADC2采集两路模拟信号(R37和R38电位器输入)
- 通过TIM2输入捕获功能测量外部脉冲信号频率
- 通过4个按键实现参数设置和模式切换
- 通过LCD实时显示系统状态和测量数据
- 通过LED指示灯显示系统工作状态
2. 硬件设计与CubeMX配置详解
2.1 时钟树配置要点
在STM32CubeMX中配置时钟树时,我推荐采用以下参数:
- 使用外部高速时钟(HSE)作为时钟源
- 系统时钟配置为72MHz(根据具体芯片型号可能有所不同)
- APB1总线时钟配置为36MHz(TIM2/TIM3挂载在此总线上)
- APB2总线时钟配置为72MHz(ADC1/ADC2挂载在此总线上)
注意:时钟配置直接影响定时器精度和ADC采样率,务必确保各外设时钟不超过其最大允许值。
2.2 GPIO配置规范
根据项目需求,我们需要配置以下GPIO:
- PA7:TIM3_CH2,PWM信号输出
- PA15:TIM2_CH1,脉冲信号输入捕获
- PB0-PB2、PA0:按键输入,配置为上拉输入模式
- PC8-PC15:LED控制,配置为推挽输出
- ADC通道:PA1(ADC1_IN11)、PA3(ADC2_IN15)
2.3 定时器配置技巧
2.3.1 PWM生成定时器(TIM3)配置
- 时钟源:内部时钟
- 模式:PWM模式2(便于占空比计算)
- 预分频器(PSC):0(不分频)
- 自动重载值(ARR):根据所需频率动态计算
- 脉冲宽度(CCR):根据所需占空比动态计算
计算公式:
code复制ARR = (定时器时钟频率 / 目标频率) - 1
CCR = (ARR + 1) * (占空比百分比 / 100)
2.3.2 输入捕获定时器(TIM2)配置
- 时钟源:内部时钟
- 通道1:输入捕获模式,上升沿触发
- 预分频器:71(72MHz/72=1MHz计数频率)
- 自动重载值:65535(最大计数值)
- 开启捕获中断
频率计算原理:
code复制频率 = 1MHz / 捕获值(CCR1)
2.3.3 系统定时器(TIM6)配置
- 时钟源:内部时钟
- 预分频器:7199(72MHz/7200=10kHz)
- 自动重载值:99(10kHz/100=100Hz,即10ms中断)
- 开启更新中断
3. 关键代码实现与优化
3.1 按键处理机制
项目中实现了短按、长按检测功能,通过定时器中断每10ms检测一次按键状态。以下是优化后的按键处理代码:
c复制struct Key {
uint16_t age; // 按键按下时长计数
uint8_t press; // 按键按下标志
uint8_t short_flag; // 短按标志
uint8_t long_flag; // 长按标志
};
struct Key keys[4] = {0};
uint8_t key_read() {
if(!HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0)) return 1;
if(!HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1)) return 2;
if(!HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2)) return 3;
if(!HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0)) return 4;
return 0;
}
void key_scan() {
uint8_t key = key_read();
if(key != 0) {
keys[key-1].age++;
if(keys[key-1].age == 2) keys[key-1].press = 1;
} else {
for(int i=0; i<4; i++) {
if(keys[i].age > 200) keys[i].long_flag = 1;
if(keys[i].press && !keys[i].long_flag)
keys[i].short_flag = 1;
keys[i].age = 0;
keys[i].press = 0;
}
}
}
3.2 PWM输出控制
动态调整PWM频率和占空比的实现:
c复制void pwm_update(uint16_t freq, uint8_t duty) {
// 限制频率范围
if(freq < 1000) freq = 1000;
if(freq > 2000) freq = 2000;
// 限制占空比范围
if(duty < 10) duty = 10;
if(duty > 80) duty = 80;
uint16_t arr = (1000000 / freq) - 1;
uint16_t ccr = (duty * (arr + 1)) / 100;
__HAL_TIM_SetAutoreload(&htim3, arr);
__HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_2, ccr);
}
3.3 模拟信号采集与处理
两路ADC采集及数据处理:
c复制float adc_read(ADC_HandleTypeDef *hadc) {
uint16_t adc_val;
HAL_ADC_Start(hadc);
if(HAL_ADC_PollForConversion(hadc, 10) == HAL_OK) {
adc_val = HAL_ADC_GetValue(hadc);
return 3.3f * adc_val / 4095.0f;
}
return 0.0f;
}
void adc_process() {
float adc1 = adc_read(&hadc1); // R38
float adc2 = adc_read(&hadc2); // R37
// 根据电位器位置计算目标频率和占空比
float target_freq = 1000.0f + adc1 * 1000.0f; // 1000-2000Hz
float target_duty = 10.0f + adc2 * 70.0f; // 10%-80%
// 量化处理(步进式调整)
uint16_t freq_step = (uint16_t)((target_freq - 1000.0f) / FS) * FS + 1000;
uint8_t duty_step = (uint8_t)((target_duty - 10.0f) / DS) * DS + 10;
pwm_update(freq_step, duty_step);
}
4. 系统功能实现与调试技巧
4.1 人机交互界面设计
LCD显示界面分为三个视图模式:
- PWM监控视图:显示当前输出的PWM参数和系统状态
- 异常记录视图:显示异常发生时的参数和持续时间
- 参数设置视图:允许调整DS、DR、FS、FR等系统参数
视图切换通过按键1短按实现,按键2短按用于锁定/解锁PWM输出,长按用于复位计时器。
4.2 输入捕获频率测量
脉冲信号频率测量通过TIM2输入捕获实现,关键点在于:
- 配置TIM2时钟为1MHz(预分频71)
- 捕获上升沿,测量相邻两个上升沿之间的时间差
- 频率 = 1MHz / 捕获值
c复制void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {
if(htim->Instance == TIM2) {
uint16_t capture = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
if(capture > 0) {
DF = 1000000 / capture; // 计算频率(Hz)
__HAL_TIM_SetCounter(htim, 0); // 重置计数器
}
}
}
4.3 异常检测与处理
系统实现了简单的异常检测机制,当测量频率与设定频率偏差超过1000Hz时,记录异常状态:
c复制void error_check() {
int32_t diff = DF - CF; // 测量频率 - 设定频率
if(diff < 0) diff = -diff;
if(diff < 1000) {
error_flag = 0;
} else if(!error_flag) {
error_flag = 1;
CF_err = CF;
CD_err = CD;
DF_err = DF;
XF = diff;
runtime_err = runtime;
}
}
5. 常见问题与解决方案
5.1 PWM输出不稳定
可能原因及解决方案:
- 定时器时钟配置错误:检查TIM3的时钟源和分频设置
- 自动重载值计算错误:确保ARR = (时钟频率/目标频率) - 1
- GPIO配置错误:确认PA7配置为TIM3_CH2的复用功能
5.2 ADC采样值跳动大
优化建议:
- 在ADC初始化后执行校准:
c复制HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED);
HAL_ADCEx_Calibration_Start(&hadc2, ADC_SINGLE_ENDED);
- 添加软件滤波算法,如滑动平均滤波:
c复制#define FILTER_SIZE 8
float adc_filter(float new_val) {
static float buffer[FILTER_SIZE] = {0};
static uint8_t index = 0;
static float sum = 0;
sum -= buffer[index];
buffer[index] = new_val;
sum += buffer[index];
index = (index + 1) % FILTER_SIZE;
return sum / FILTER_SIZE;
}
5.3 输入捕获测量不准确
调试技巧:
- 确认TIM2的预分频设置正确(1MHz计数频率)
- 检查输入信号质量,必要时添加硬件滤波
- 对于低频信号,可改为测量多个周期的时间再计算频率
5.4 按键响应不灵敏
优化方案:
- 添加按键消抖处理:
c复制#define DEBOUNCE_TIME 5 // 50ms
uint8_t key_debounce(uint8_t pin) {
static uint8_t count[4] = {0};
static uint8_t state[4] = {1};
uint8_t current = HAL_GPIO_ReadPin(GPIOB, pin);
if(current != state[pin]) {
count[pin]++;
if(count[pin] >= DEBOUNCE_TIME) {
state[pin] = current;
count[pin] = 0;
return state[pin];
}
} else {
count[pin] = 0;
}
return 2; // 无变化
}
- 调整按键检测阈值时间(当前短按>20ms,长按>2s)
6. 系统优化与扩展建议
6.1 性能优化方向
- 使用DMA传输ADC采样数据,减少CPU开销
- 对关键代码段进行优化,减少中断处理时间
- 添加看门狗定时器,提高系统可靠性
6.2 功能扩展建议
- 增加串口通信功能,实现远程监控和控制
- 添加SD卡存储,记录系统运行数据
- 实现PID控制算法,提高系统响应性能
- 增加更多的保护机制,如过流、过压检测
在实际比赛中,这套系统架构经过验证能够稳定工作,关键是要理解每个模块的工作原理,并在调试时耐心排查问题。特别是在处理定时器和中断时,要注意优先级设置和中断处理时间的控制,避免出现不可预期的行为。