# 蓝桥杯嵌入式第十三届省赛详解教程
一、赛题概述 本题是一个基于STM32的密码锁系统,主要功能包括: 密码输入与验证(3位数字密码) LCD界面切换显示 PWM波形输出控制 串口通信修改密码 LED状态指示与报警 定时器多任务管理 核心考点 : 1. 定时器中断与时间片轮询 2. 串口通信协议设计与数据解析 3. PWM波形参数动态调整 4. 状态机设计 二、系统架构设计 2.1 状态机设计 系统有两个主要界面状态: 状态转换逻辑 : ui=0 → ui=1 :密码输入…
FIELD_GUIDE
FIELD GUIDE
Use the guide rail to jump between sections.
一、赛题概述
本题是一个基于STM32的密码锁系统,主要功能包括:
- 密码输入与验证(3位数字密码)
- LCD界面切换显示
- PWM波形输出控制
- 串口通信修改密码
- LED状态指示与报警
- 定时器多任务管理
核心考点:
- 定时器中断与时间片轮询
- 串口通信协议设计与数据解析
- PWM波形参数动态调整
- 状态机设计
二、系统架构设计
2.1 状态机设计
系统有两个主要界面状态:
u8 ui = 0; // 0: 密码输入界面(PSD) 1: 状态显示界面(STA)
状态转换逻辑:
ui=0→ui=1:密码输入正确,按下KEY4ui=1→ui=0:5秒无操作自动退出(TIM6定时器计时)
2.2 全局变量设计
// 密码相关
u8 b1 = '@', b2 = '@', b3 = '@'; // 当前输入的密码
u8 password[3] = {'1','2','3'}; // 正确密码
u8 password_cnt = 0; // 密码错误次数
// PWM参数
u32 pa1_frq = 1000; // PWM频率
u8 pa1_duty = 50; // PWM占空比
// 标志位
_Bool key4_flag = 0; // 密码验证成功标志
_Bool led2_flag = 0; // LED2闪烁标志
// 串口接收
u8 rx_data, rx_pointer; // 接收缓冲
u8 rx_buff[10]; // 接收数组
三、定时器详解(重点)
3.1 定时器架构设计
本系统使用了两个定时器:
- TIM6:基本定时器,用于系统时基和中断任务
- TIM2:通用定时器,用于PWM波形输出
3.2 TIM6 - 系统时基定时器
3.2.1 定时器配置
TIM6通常配置为1ms中断一次,作为系统的基准时钟:
// 在main.c中初始化
HAL_TIM_Base_Start_IT(&htim6); // 启动TIM6中断
CubeMX配置要点:
- Prescaler(预分频):根据系统时钟计算,使计数频率为1MHz
- Counter Period(自动重装载值):999(1MHz / 1000 = 1kHz,即1ms)
- 使能TIM6全局中断
3.2.2 定时器中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim == &htim6)
{
HAL_TIM_Base_Start_IT(&htim6); // 重新启动定时器
// 任务1:5秒自动退出登录
if(key4_flag == 1)
{
time6_tick++;
if(time6_tick >= 5000) // 5000ms = 5秒
{
time6_tick = 0;
key4_flag = 0;
b1 = '@';
b2 = '@';
b3 = '@';
ui = 0; // 返回密码输入界面
}
}
// 任务2:LED2闪烁控制
if(led2_flag)
{
led2_tick++;
if(led2_tick % 100 == 0) // 每100ms翻转一次
{
led_num ^= 0x02; // LED2翻转
}
if(led2_tick == 5000) // 5秒后停止闪烁
{
led2_tick = 0;
led2_flag = 0;
led_num &= ~0x02; // 关闭LED2
}
}
}
}
关键技术点:
-
时间计数器设计:
time6_tick:用于5秒自动退出计时led2_tick:用于LED2闪烁计时- 每1ms中断一次,计数器+1,达到5000即为5秒
-
LED闪烁实现:
if(led2_tick % 100 == 0) // 每100ms执行一次 { led_num ^= 0x02; // 异或操作实现翻转 }- 使用取模运算控制执行频率
- 异或操作实现LED状态翻转
-
注意事项:
- ⚠️ 代码中
if(led2_tick % 100)应该是if(led2_tick % 100 == 0) - 当前写法会导致LED闪烁频率异常
- ⚠️ 代码中
3.3 TIM2 - PWM定时器
3.3.1 PWM原理
PWM(脉冲宽度调制)通过改变高电平占总周期的比例来控制输出:
- 频率(Frequency):1秒内脉冲的次数
- 占空比(Duty Cycle):高电平时间占总周期的百分比
3.3.2 PWM参数动态调整
void PWM_proc()
{
if(uwTick - pwm_tick < 100) return; // 100ms更新一次
pwm_tick = uwTick;
if(key4_flag) // 密码正确
{
pa1_frq = 2000; // 频率2000Hz
pa1_duty = 10; // 占空比10%
// 设置自动重装载值(决定频率)
__HAL_TIM_SET_AUTORELOAD(&htim2, 1e6 / pa1_frq - 1);
// 设置比较值(决定占空比)
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2,
1e6 / pa1_frq * pa1_duty / 100);
}
else // 密码错误或未登录
{
pa1_frq = 1000; // 频率1000Hz
__HAL_TIM_SET_AUTORELOAD(&htim2, 1e6 / pa1_frq - 1);
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2,
1e6 / pa1_frq * pa1_duty / 100);
}
}
PWM计算公式详解:
假设TIM2的时钟频率为1MHz(1,000,000 Hz):
-
ARR(自动重装载值)计算:
ARR = 时钟频率 / 目标频率 - 1 ARR = 1,000,000 / 2000 - 1 = 499 -
CCR(比较值)计算:
CCR = 500 × 10 / 100 = 50 -
实际输出:
- 周期:500个计数周期 = 0.5ms = 2000Hz
- 高电平:50个计数周期 = 10%占空比
3.3.3 CubeMX配置要点
- 选择TIM2,启用Channel 2为PWM模式
- Prescaler设置为使计数频率为1MHz
- 在main.c中启动PWM:
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
四、串口通信详解(重点)
4.1 通信协议设计
4.1.1 数据帧格式
格式:旧密码-新密码
示例:123-789
长度:7个字符
协议规则:
- 第0-2位:旧密码(3位数字)
- 第3位:分隔符
- - 第4-6位:新密码(3位数字)
4.1.2 协议验证流程
接收数据 → 长度检查 → 格式检查 → 旧密码验证 → 修改密码
4.2 串口中断接收
4.2.1 中断回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
rx_buff[rx_pointer++] = rx_data; // 存储接收的字节
HAL_UART_Receive_IT(&huart1, &rx_data, 1); // 重新启动接收
rx_tick = uwTick; // 记录接收时间戳
}
关键技术点:
-
中断接收机制:
- 每接收1个字节触发一次中断
- 必须在回调函数中重新启动接收:
HAL_UART_Receive_IT() - 使用指针
rx_pointer记录接收位置
-
时间戳记录:
rx_tick = uwTick:记录最后一次接收时间- 用于判断数据帧接收完成(超时机制)
-
初始化启动:
// 在main.c中 HAL_UART_Receive_IT(&huart1, &rx_data, 1);
4.3 数据解析处理
4.3.1 完整的解析函数
void RX_proc()
{
// 超时判断:50ms内无新数据,认为接收完成
if(uwTick - rx_tick < 50) return;
rx_tick = uwTick;
// 检查1:数据长度和格式
if(rx_pointer == 7 && rx_buff[3] == '-')
{
// 检查2:前3位是否为数字
for(u8 i = 0; i < 3; i++)
{
if(!(rx_buff[i] >= '0' && rx_buff[i] <= '9'))
{
printf("前3个字符为非数字,请重新输入\r\n");
rx_pointer = 0;
memset(rx_buff, 0, sizeof(rx_buff));
return;
}
// 检查3:后3位是否为数字
if(!(rx_buff[i + 4] >= '0' && rx_buff[i + 4] <= '9'))
{
printf("后3个字符为非数字,请重新输入\r\n");
rx_pointer = 0;
memset(rx_buff, 0, sizeof(rx_buff));
return;
}
// 检查4:旧密码是否正确
if(!(rx_buff[i] == password[i]))
{
printf("输入的前3位不正确!\r\n");
rx_pointer = 0;
memset(rx_buff, 0, sizeof(rx_buff));
return;
}
}
// 所有检查通过,修改密码
printf("密码修改成功\r\n");
password[0] = rx_buff[4];
password[1] = rx_buff[5];
password[2] = rx_buff[6];
}
else if(rx_pointer > 0)
{
printf("请输入7个字符,中间用"-"分隔\r\n");
}
// 清空接收缓冲区
rx_pointer = 0;
memset(rx_buff, 0, sizeof(rx_buff));
}
4.3.2 超时机制详解
if(uwTick - rx_tick < 50) return;
工作原理:
- 每次接收到字节时,更新
rx_tick = uwTick - 在主循环中不断检查:当前时间 - 最后接收时间 > 50ms
- 超过50ms无新数据,认为一帧数据接收完成
- 开始解析数据
为什么需要超时机制?
- 串口是字节流传输,无法直接判断一帧数据何时结束
- 通过时间间隔判断数据帧边界
- 50ms是经验值,可根据实际情况调整
4.3.3 数据验证流程图
接收完成(超时50ms)
↓
长度=7 且 第3位='-' ?
↓ 是
前3位都是数字 ?
↓ 是
后3位都是数字 ?
↓ 是
前3位=当前密码 ?
↓ 是
修改密码成功
↓
清空缓冲区
4.4 printf重定向
4.4.1 重定向实现
#include <stdio.h>
struct __FILE
{
int handle;
};
FILE __stdout;
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (u8 *)&ch, 1, 50);
return ch;
}
原理:
- 重写
fputc函数,将字符输出重定向到串口 printf内部会调用fputc逐字符输出- 实现后可直接使用
printf进行串口调试
4.4.2 使用示例
printf("密码修改成功\r\n");
printf("当前频率:%dHz\r\n", pa1_frq);
注意事项:
- 使用
\r\n作为换行符(Windows风格) - 中文字符需要确保编码一致(通常使用GBK)
- 大量printf会影响实时性,调试完成后应删除
五、时间片轮询架构
5.1 轮询机制设计
系统使用时间片轮询方式管理多个任务:
while(1)
{
LCD_proc(); // LCD显示任务
KEY_proc(); // 按键扫描任务
LED_proc(); // LED控制任务
PWM_proc(); // PWM更新任务
RX_proc(); // 串口数据处理任务
}
5.2 时间片控制
每个任务都有独立的时间片控制:
u32 lcd_tick = 0;
void LCD_proc()
{
if(uwTick - lcd_tick < 100) return; // 100ms执行一次
lcd_tick = uwTick;
// 任务代码...
}
时间片分配:
- LCD显示:100ms
- 按键扫描:20ms
- LED控制:10ms
- PWM更新:100ms
- 串口处理:50ms(超时判断)
5.3 时间片设计原则
-
按键扫描频率最高(20ms):
- 需要快速响应用户操作
- 实现按键消抖
-
显示更新适中(100ms):
- 人眼刷新率约60Hz(16ms)
- 100ms足够流畅,降低CPU占用
-
PWM更新较慢(100ms):
- PWM参数变化不频繁
- 降低不必要的寄存器写入
六、按键处理技巧
6.1 按键状态检测
void KEY_read()
{
// 读取当前按键值
if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == 0)
key_value = 1;
else if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == 0)
key_value = 2;
else if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) == 0)
key_value = 3;
else if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == 0)
key_value = 4;
else
key_value = 0;
// 计算按键事件
key_down = key_value & (key_value ^ key_old); // 按下沿
key_up = ~key_value & (key_value ^ key_old); // 松开沿
key_old = key_value; // 保存旧值
}
6.2 边沿检测算法
按下沿检测:
key_down = key_value & (key_value ^ key_old);
真值表分析:
| key_old | key_value | key_value ^ key_old | key_down |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 0 | 1 | 1 | 1 ✓ |
| 1 | 0 | 1 | 0 |
| 1 | 1 | 0 | 0 |
只有从0→1时,key_down才为1,实现按下沿检测。
6.3 密码输入逻辑
if(key_down == 1 && ui == 0)
{
if(b1 == '@')
b1 = '0' - 1; // 初始化为'/',+1后变为'0'
if(b1 == '9')
b1 = '0' - 1; // 循环回'0'
b1++;
}
循环逻辑:
@ → 0 → 1 → 2 → ... → 9 → 0 → ...
七、常见问题与优化
7.1 定时器相关问题
问题1:LED闪烁频率不对
// 错误写法
if(led2_tick % 100) // 会在1-99时执行,频率过高
// 正确写法
if(led2_tick % 100 == 0) // 只在100的倍数时执行
问题2:定时器中断未触发
- 检查是否启动中断:
HAL_TIM_Base_Start_IT(&htim6) - 检查NVIC是否使能
- 检查中断优先级配置
7.2 串口通信问题
问题1:接收不到数据
- 检查波特率配置是否一致
- 检查是否启动中断接收:
HAL_UART_Receive_IT() - 检查TX/RX引脚是否正确
问题2:数据丢失
- 增大接收缓冲区
- 减小超时时间
- 使用DMA接收
问题3:中文乱码
- 统一编码格式(GBK或UTF-8)
- 检查串口助手编码设置
7.3 PWM输出问题
问题1:无PWM输出
- 检查是否启动PWM:
HAL_TIM_PWM_Start() - 检查引脚复用配置
- 检查ARR和CCR值是否合理
问题2:频率不准确
- 确认定时器时钟频率
- 重新计算ARR值
- 使用示波器验证
八、代码优化建议
8.1 魔术数字优化
// 优化前
if(time6_tick >= 5000)
// 优化后
#define AUTO_LOGOUT_TIME 5000 // 5秒自动退出
if(time6_tick >= AUTO_LOGOUT_TIME)
8.2 函数封装
// 封装密码验证函数
_Bool verify_password(u8 b1, u8 b2, u8 b3)
{
return (b1 == password[0] &&
b2 == password[1] &&
b3 == password[2]);
}
// 使用
if(verify_password(b1, b2, b3))
{
ui = 1;
// ...
}
8.3 状态机优化
使用枚举类型代替魔术数字:
typedef enum {
UI_PASSWORD = 0,
UI_STATUS = 1
} UI_State;
UI_State current_ui = UI_PASSWORD;
九、调试技巧
9.1 串口调试
// 在关键位置添加调试信息
printf("KEY_DOWN: %d\r\n", key_down);
printf("Password: %c%c%c\r\n", b1, b2, b3);
printf("PWM Freq: %d, Duty: %d\r\n", pa1_frq, pa1_duty);
9.2 LED指示调试
// 使用LED指示程序运行状态
led_num |= 0x80; // LED8常亮表示程序运行
LED_disp(led_num);
9.3 逻辑分析仪
使用逻辑分析仪观察:
- PWM波形参数
- 串口通信时序
- 按键抖动情况
十、总结
10.1 核心知识点
-
定时器应用:
- 基本定时器中断(TIM6)
- PWM定时器配置(TIM2)
- 时间片轮询架构
-
串口通信:
- 中断接收机制
- 数据帧协议设计
- 超时判断与解析
- printf重定向
-
状态机设计:
- 界面切换逻辑
- 标志位管理
- 自动退出机制
10.2 蓝桥杯备赛建议
-
熟练掌握HAL库:
- 定时器配置与中断
- 串口收发与中断
- PWM输出控制
-
代码模板积累:
- 按键扫描模板
- 串口协议解析模板
- 时间片轮询框架
-
调试能力培养:
- 串口调试技巧
- 逻辑分析仪使用
- 问题定位方法
-
时间管理:
- 先实现基本功能
- 再优化细节
- 预留测试时间
附录:完整工程结构
项目目录/
├── Core/
│ ├── Src/
│ │ ├── main.c # 主函数,初始化
│ │ ├── user.c # 用户逻辑代码
│ │ └── stm32g4xx_it.c # 中断服务函数
│ └── Inc/
│ ├── user.h # 用户头文件
│ └── main.h
├── Drivers/ # HAL库驱动
└── Middlewares/ # LCD等中间件
关键文件说明:
main.c:硬件初始化,启动定时器和串口中断user.c:所有业务逻辑,包括LCD、按键、LED、PWM、串口处理user.h:函数声明和宏定义
作者:蓝桥杯嵌入式备赛团队 日期:2026年3月 版本:v1.0
本教程基于蓝桥杯嵌入式第十三届省赛真题编写,重点讲解定时器和串口通信的实现细节,适合备赛选手学习参考。