作为一名参加过多次电子设计竞赛的嵌入式开发者,我想分享一下我在第十四届蓝桥杯嵌入式组省赛中的实战经验。这个项目基于STM32F103C8T6开发板,实现了按键输入、ADC采集、OLED显示和PWM输出等核心功能。虽然原题要求使用STM32G431,但我通过合理的硬件适配和软件优化,在更常见的STM32F103平台上完美复现了所有功能要求。
在实际开发中,我发现很多初学者容易陷入几个误区:一是过于依赖原厂示例代码的单一文件结构;二是对HAL库的理解停留在表面;三是缺乏模块化设计思维。本文将详细解析如何通过模块化设计、合理的硬件抽象和高效的算法实现,在资源有限的平台上完成复杂功能。
我选择STM32F103C8T6(俗称"蓝桥杯最小系统板")作为主控芯片,主要基于以下考虑:
显示模块选用0.96寸OLED(SSD1306驱动),相比原题的LCD方案:
以下是经过实测验证的硬件连接方案:
| 功能模块 | 引脚分配 | 备注 |
|---|---|---|
| 按键1 | PA11 | 带硬件消抖电路 |
| 按键2 | PA10 | 10kΩ上拉电阻 |
| 按键3 | PA9 | 低电平有效 |
| 按键4 | PA8 | 中断触发模式 |
| ADC输入 | PC0 | 接10kΩ电位器 |
| I2C_SCL | PB6 | 4.7kΩ上拉 |
| I2C_SDA | PB7 | 4.7kΩ上拉 |
| PWM输出1 | PA1 | TIM2_CH2 |
| PWM输出2 | PA2 | TIM2_CH3 |
| LED1 | PA12 | 限流电阻220Ω |
| LED2 | PA15 | 状态指示灯 |
注意:I2C总线必须加上拉电阻,否则通信不稳定。实测发现当总线长度超过15cm时,需要降低I2C时钟频率至100kHz以下。
我采用了严格的模块化设计,工程目录结构如下:
code复制L14-STM32/
├── Core/
│ ├── Src/
│ │ ├── main.c # 主程序
│ │ ├── gpio.c # 按键处理
│ │ ├── adc.c # 模拟量采集
│ │ ├── oled.c # 显示驱动
│ │ ├── tim.c # PWM生成
│ │ └── i2c.c # 通信协议
│ └── Inc/ # 对应头文件
├── Drivers/ # HAL库
└── MDK-ARM/ # Keil工程
这种结构的优势在于:
为管理各种状态参数,我定义了一个全局结构体:
c复制typedef struct {
uint8_t a; // 界面状态:1-数据 2-参数 3-记录
float c; // 实际电压值(0-3.3V)
uint8_t D2; // 参数指示:0-R 1-K
uint8_t D3; // 占空比控制:0-受控 1-锁定
float R, K; // 系统参数(1-10)
uint8_t N; // PWM模式切换次数
float MH, ML; // 高低频最大速度
uint8_t M; // 输出模式:0-L 1-H
float p; // 实时占空比(%)
float v, v1; // 实时速度及小数部分
} SystemState;
这种集中式管理避免了全局变量泛滥,也方便状态持久化和调试观察。
机械按键存在5-10ms的抖动期,我的解决方案是:
c复制uint8_t KEY_Scan(void) {
static uint8_t key_state = 0;
static uint32_t key_tick = 0;
// 状态0:等待按下
if(key_state == 0) {
if(按键按下条件) {
key_state = 1;
key_tick = HAL_GetTick();
}
}
// 状态1:消抖确认
else if(key_state == 1) {
if(HAL_GetTick() - key_tick > 20) { // 20ms消抖
if(按键仍按下) {
key_state = 2;
return 按键值;
} else {
key_state = 0; // 抖动误判
}
}
}
// 状态2:等待释放
else {
if(所有按键释放) {
key_state = 0;
}
}
return KEY_NO_PRESS;
}
实测表明,这种组合消抖方式可使误触发率降低至0.1%以下。
STM32F103的12位ADC理论上应有1mV分辨率,但实际受噪声影响精度会下降。我采用了以下优化措施:
c复制#define VREF 3.30f // 实测开发板3.3V电源实际值
float c = (HAL_ADC_GetValue(&hadc1) * VREF) / 4095.0f;
c复制#define FILTER_SIZE 8
float adc_filter_buf[FILTER_SIZE];
uint8_t filter_index = 0;
float ADC_Filter(float raw) {
static float sum = 0;
sum -= adc_filter_buf[filter_index];
adc_filter_buf[filter_index] = raw;
sum += raw;
filter_index = (filter_index + 1) % FILTER_SIZE;
return sum / FILTER_SIZE;
}
Vactual = (Vraw - offset) * VREF / (fullscale - offset)经过优化后,ADC采集的波动范围从±20mV降低到±2mV。
根据题目要求,PWM需要实现两种模式:
占空比控制算法:
c复制void PWM_Update(SystemState *sys) {
if(sys->D3 == 0) { // 受控模式
if(sys->HZ == 0) { // 低频
__HAL_TIM_SET_AUTORELOAD(&htim2, 4000-1);
float duty = 0.1f + 0.75f * (sys->c - 1.0f)/2.0f;
duty = duty < 0.1f ? 0.1f : (duty > 0.85f ? 0.85f : duty);
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, (uint32_t)(4000*duty));
} else { // 高频
__HAL_TIM_SET_AUTORELOAD(&htim2, 8000-1);
// 相同占空比计算逻辑...
}
}
// 速度计算
sys->v = (2 * 3.1415926f * sys->R * sys->p) / (100 * sys->K);
}
经验:STM32的ARR寄存器值=实际周期-1,CCR值=高电平时间-1。忘记这点会导致1个时钟周期的误差。
原厂提供的OLED驱动每次刷新全屏,我改进为局部刷新:
c复制void OLED_RefreshArea(uint8_t x, uint8_t y, uint8_t w, uint8_t h) {
uint8_t buf[128]; // 局部缓冲区
// 1. 只提取需要更新的显存区域
// 2. 通过I2C发送部分数据
// 刷新速度提升3倍(实测从15ms降至5ms)
}
设计了三类界面,通过按键切换:
code复制DATA
M=H P=45%
V=2.5
code复制PARA
R=1.5
K=2.0
code复制RECD
N=3
MH=5.2
ML=3.8
字体处理技巧:
现象:OLED时好时坏,有时完全无显示
排查步骤:
c复制hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 400000; // 400kHz
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; // Tlow/Thigh = 2
现象:实测频率与设定值有约5%偏差
原因分析:
c复制// 在SystemClock_Config()中确保:
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // 72MHz
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; // 36MHz
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // 72MHz
// TIM2挂在APB1上,基准频率36MHz
htim2.Init.Prescaler = 72 - 1; // 36MHz/72 = 500kHz
htim2.Init.Period = 4000 - 1; // 500kHz/4000 = 125Hz
现象:速度计算时小数部分异常
解决方案:
c复制// 在参数修改处添加:
if(new_R > 10.0f) new_R = 10.0f;
if(new_K < 1.0f) new_K = 1.0f;
将按键扫描放在1ms定时器中断中,而非主循环轮询:
c复制void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if(htim == &htim6) { // 1ms定时器
static uint8_t cnt = 0;
if(++cnt >= 10) { // 每10ms扫描一次
cnt = 0;
key_val = KEY_Scan();
}
}
}
通过const修饰符将字库存入Flash:
c复制static const uint8_t FONT_8x16[] __attribute__((at(0x0800F000))) = {
// 字模数据...
};
在无操作时进入低功耗模式:
c复制void Enter_StopMode(void) {
HAL_SuspendTick();
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
// 唤醒后重新初始化时钟
SystemClock_Config();
HAL_ResumeTick();
}
通过以上优化,系统待机电流从12mA降至0.5mA。
这个项目让我深刻体会到模块化设计的重要性。在比赛过程中,当需要调整ADC采样率时,得益于良好的架构设计,我只用了10分钟就完成了修改,而没有影响到其他功能。
几个值得分享的心得:
后续可扩展方向: