BACK_TO_BASE
Engineering Notebook // Build Log
/
20:27:27
/
NOTEBOOK_ENTRY

# 蓝桥杯嵌入式第十三届省赛详解教程

一、赛题概述 本题是一个基于STM32的密码锁系统,主要功能包括: 密码输入与验证(3位数字密码) LCD界面切换显示 PWM波形输出控制 串口通信修改密码 LED状态指示与报警 定时器多任务管理 核心考点 : 1. 定时器中断与时间片轮询 2. 串口通信协议设计与数据解析 3. PWM波形参数动态调整 4. 状态机设计 二、系统架构设计 2.1 状态机设计 系统有两个主要界面状态: 状态转换逻辑 : ui=0 → ui=1 :密码输入…

Notebook Time
2 min
Image Frames
0
View Tracks
80
STM32
FIELD_GUIDE

FIELD GUIDE

Use the guide rail to jump between sections.

一、赛题概述

本题是一个基于STM32的密码锁系统,主要功能包括:

  • 密码输入与验证(3位数字密码)
  • LCD界面切换显示
  • PWM波形输出控制
  • 串口通信修改密码
  • LED状态指示与报警
  • 定时器多任务管理

核心考点

  1. 定时器中断与时间片轮询
  2. 串口通信协议设计与数据解析
  3. PWM波形参数动态调整
  4. 状态机设计

二、系统架构设计

2.1 状态机设计

系统有两个主要界面状态:

u8 ui = 0;  // 0: 密码输入界面(PSD)  1: 状态显示界面(STA)

状态转换逻辑

  • ui=0ui=1:密码输入正确,按下KEY4
  • ui=1ui=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 定时器架构设计

本系统使用了两个定时器

  1. TIM6:基本定时器,用于系统时基和中断任务
  2. 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
            }
        }
    }
}

关键技术点

  1. 时间计数器设计

    • time6_tick:用于5秒自动退出计时
    • led2_tick:用于LED2闪烁计时
    • 每1ms中断一次,计数器+1,达到5000即为5秒
  2. LED闪烁实现

    if(led2_tick % 100 == 0)  // 每100ms执行一次
    {
        led_num ^= 0x02;  // 异或操作实现翻转
    }
    
    • 使用取模运算控制执行频率
    • 异或操作实现LED状态翻转
  3. 注意事项

    • ⚠️ 代码中 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):

  1. ARR(自动重装载值)计算

    ARR = 时钟频率 / 目标频率 - 1
    ARR = 1,000,000 / 2000 - 1 = 499
    
  2. CCR(比较值)计算

    CCR = 500 × 10 / 100 = 50
  3. 实际输出

    • 周期:500个计数周期 = 0.5ms = 2000Hz
    • 高电平:50个计数周期 = 10%占空比

3.3.3 CubeMX配置要点

  1. 选择TIM2,启用Channel 2为PWM模式
  2. Prescaler设置为使计数频率为1MHz
  3. 在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. 中断接收机制

    • 每接收1个字节触发一次中断
    • 必须在回调函数中重新启动接收:HAL_UART_Receive_IT()
    • 使用指针 rx_pointer 记录接收位置
  2. 时间戳记录

    • rx_tick = uwTick:记录最后一次接收时间
    • 用于判断数据帧接收完成(超时机制)
  3. 初始化启动

    // 在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;

工作原理

  1. 每次接收到字节时,更新 rx_tick = uwTick
  2. 在主循环中不断检查:当前时间 - 最后接收时间 > 50ms
  3. 超过50ms无新数据,认为一帧数据接收完成
  4. 开始解析数据

为什么需要超时机制?

  • 串口是字节流传输,无法直接判断一帧数据何时结束
  • 通过时间间隔判断数据帧边界
  • 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 时间片设计原则

  1. 按键扫描频率最高(20ms):

    • 需要快速响应用户操作
    • 实现按键消抖
  2. 显示更新适中(100ms):

    • 人眼刷新率约60Hz(16ms)
    • 100ms足够流畅,降低CPU占用
  3. 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_oldkey_valuekey_value ^ key_oldkey_down
0000
0111 ✓
1010
1100

只有从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 核心知识点

  1. 定时器应用

    • 基本定时器中断(TIM6)
    • PWM定时器配置(TIM2)
    • 时间片轮询架构
  2. 串口通信

    • 中断接收机制
    • 数据帧协议设计
    • 超时判断与解析
    • printf重定向
  3. 状态机设计

    • 界面切换逻辑
    • 标志位管理
    • 自动退出机制

10.2 蓝桥杯备赛建议

  1. 熟练掌握HAL库

    • 定时器配置与中断
    • 串口收发与中断
    • PWM输出控制
  2. 代码模板积累

    • 按键扫描模板
    • 串口协议解析模板
    • 时间片轮询框架
  3. 调试能力培养

    • 串口调试技巧
    • 逻辑分析仪使用
    • 问题定位方法
  4. 时间管理

    • 先实现基本功能
    • 再优化细节
    • 预留测试时间

附录:完整工程结构

项目目录/
├── 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

本教程基于蓝桥杯嵌入式第十三届省赛真题编写,重点讲解定时器和串口通信的实现细节,适合备赛选手学习参考。