STM32CubeMX+Keil实战:第九届蓝桥杯嵌入式省赛计时器项目全解析

1. 项目背景与需求分析

第九届蓝桥杯嵌入式设计与开发省赛题目要求选手实现一个多功能计时器系统,这个看似简单的项目实际上涵盖了嵌入式开发的多个核心知识点。作为教学案例,它完美展示了如何将理论转化为实际产品。

该计时器需要具备以下核心功能:

  • 时间设置 :通过按键调整时、分、秒
  • 存储功能 :支持5组时间数据的EEPROM存储与读取
  • 显示界面 :LCD实时显示当前时间和存储位置
  • PWM输出 :计时过程中通过PWM控制LED亮度变化
  • 状态管理 :包含待机、设置、运行和暂停四种状态
// 典型的时间数据结构定义
typedef struct {
    uint8_t hour;
    uint8_t min; 
    uint8_t sec;
} TimeType;

TimeType currentTime = {0, 0, 0};

硬件平台采用CT117E-M4开发板,主要外设包括:

  • STM32F103系列MCU
  • 4个用户按键(B1-B4)
  • I2C接口的EEPROM(24C02)
  • 16x2字符LCD显示屏
  • 可调光LED

2. 开发环境搭建与工程配置

2.1 工具链准备

开发本项目的软件工具包括:

  1. STM32CubeMX :6.0+版本,用于外设配置和代码生成
  2. Keil MDK :5.25+版本,ARM编译器
  3. ST-Link Utility :用于程序下载和调试

提示:建议安装最新版STM32CubeF1固件包,确保支持所有使用的外设

2.2 CubeMX关键配置

在CubeMX中新建工程时,需特别注意以下配置项:

外设 配置项 参数 注意事项
GPIO 按键引脚 上拉输入 去抖动延时约50-100ms
I2C1 EEPROM接口 标准模式(100kHz) 必须配置PA6(SCL)/PA7(SDA)
TIM2 按键计时 基本定时器 用于长短按判断
TIM3 PWM生成 PWM模式1 通道1输出到LED
TIM4 秒计时 1ms中断 1000次=1秒
// CubeMX生成的I2C初始化片段
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 100000;
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;

2.3 工程目录结构

合理的文件组织能显著提高开发效率,建议采用如下结构:

/Project
  ├── /Core       // 主循环和中断处理
  ├── /Drivers    // HAL库和外设驱动
  ├── /EEPROM     // 24C02读写驱动
  ├── /LCD        // LCD显示驱动
  ├── /App        // 应用层模块
  │   ├── timer.c // 计时器逻辑
  │   ├── ui.c    // 用户界面
  │   └── ...
  └── /Inc        // 头文件

3. 核心功能实现详解

3.1 按键扫描与状态机

本项目需要处理多种按键组合和长短按操作,采用状态机是最佳解决方案。我们定义四种系统状态:

typedef enum {
    STANDBY,    // 待机状态
    SETTING,    // 时间设置
    RUNNING,    // 计时运行
    PAUSED      // 计时暂停
} SystemState;

SystemState currentState = STANDBY;

按键功能分配如下:

按键 短按功能 长按功能(>0.8s)
B1 切换存储位置
B2 进入设置模式 保存并退出设置
B3 时间值增加 快速增加时间值
B4 启动/暂停 复位计时器

长短按判断通过TIM2中断实现:

// 在TIM2中断回调函数中
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if(htim == &htim2) {
        time_cnt++;
        if(time_cnt >= 8) {  // 0.8s到达
            sec_08 = 1;
        }
    }
}

3.2 EEPROM数据存储

24C02 EEPROM的读写需要注意以下要点:

  1. 每次写操作后需要5-10ms的延时
  2. 地址范围0x00-0xFF
  3. 按页写入(每页8字节)

我们封装了读写函数:

void EEPROM_Write(uint8_t addr, uint8_t data) {
    uint8_t buf[2] = {addr, data};
    HAL_I2C_Master_Transmit(&hi2c1, 0xA0, buf, 2, 100);
    HAL_Delay(10);  // 关键延时
}

uint8_t EEPROM_Read(uint8_t addr) {
    uint8_t data;
    HAL_I2C_Master_Transmit(&hi2c1, 0xA0, &addr, 1, 100);
    HAL_I2C_Master_Receive(&hi2c1, 0xA0, &data, 1, 100);
    return data;
}

时间数据存储方案:

  • 每组时间占用3字节(时、分、秒)
  • 5组时间共占用15字节
  • 存储地址分配:
    组1: 0x00-0x02
    组2: 0x03-0x05
    组3: 0x06-0x08 
    组4: 0x09-0x0B
    组5: 0x0C-0x0E
    

3.3 精确计时实现

计时功能通过TIM4实现1ms中断,累计1000次为1秒:

// TIM4初始化(1ms中断)
htim4.Instance = TIM4;
htim4.Init.Prescaler = 7200-1;  // 72MHz/7200=10kHz
htim4.Init.CounterMode = TIM_COUNTERMODE_UP;
htim4.Init.Period = 10-1;       // 10kHz/10=1kHz(1ms)

在中断回调函数中处理秒计时:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    static uint16_t msCount = 0;
    
    if(htim == &htim4) {
        if(++msCount >= 1000) {
            msCount = 0;
            if(currentTime.sec > 0) {
                currentTime.sec--;
            } else {
                if(currentTime.min > 0) {
                    currentTime.min--;
                    currentTime.sec = 59;
                } else if(currentTime.hour > 0) {
                    currentTime.hour--;
                    currentTime.min = 59;
                    currentTime.sec = 59;
                } else {
                    // 计时结束处理
                    HAL_TIM_Base_Stop_IT(&htim4);
                }
            }
        }
    }
}

4. 进阶功能与优化技巧

4.1 PWM动态调光

计时过程中,LED亮度随剩余时间比例变化:

void Update_PWM(uint8_t hours, uint8_t mins, uint8_t secs) {
    uint32_t totalSec = hours*3600 + mins*60 + secs;
    uint32_t maxSec = currentPreset.hour*3600 
                     + currentPreset.min*60
                     + currentPreset.sec;
    float ratio = (float)totalSec / maxSec;
    
    // 计算PWM占空比(0-100)
    uint8_t duty = (uint8_t)(ratio * 100);
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, duty);
}

4.2 LCD显示优化

为提高显示刷新效率,可采用以下策略:

  1. 仅更新变化的部分
  2. 使用缓冲区减少直接操作LCD次数
  3. 合理布局显示内容

显示布局示例:

Line1: [No 1]      存储位置
Line4: 12:34:56    当前时间
Line5:   ^         设置指示符
Line7: Running     状态指示

4.3 低功耗设计

当系统处于待机状态时,可启用低功耗模式:

void Enter_LowPower(void) {
    // 关闭不必要的外设
    HAL_TIM_Base_Stop_IT(&htim4);
    HAL_TIM_PWM_Stop(&htim3, TIM_CHANNEL_1);
    
    // 配置唤醒源(如按键中断)
    HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
    
    // 唤醒后重新初始化
    SystemClock_Config();
    MX_GPIO_Init();
    // ...其他必要外设初始化
}

5. 调试技巧与常见问题

5.1 I2C通信故障排查

EEPROM无法读写时的检查清单:

  1. 确认CubeMX中I2C引脚配置正确
  2. 检查上拉电阻(通常4.7kΩ)
  3. 确保每次写操作后有足够延时
  4. 使用逻辑分析仪观察信号波形

5.2 按键响应异常处理

常见问题及解决方案:

现象 可能原因 解决方法
按键无反应 引脚配置错误 检查GPIO模式(应为上拉输入)
连按触发多次 消抖不足 增加去抖动延时(50-100ms)
长短按识别不准 定时器精度不够 调整TIM2中断周期(建议10ms)
组合键失效 扫描逻辑冲突 采用状态机重构按键处理

5.3 内存优化策略

当代码量较大时,可采取以下优化措施:

  1. 使用 -O2 编译优化选项
  2. 将不常用函数标记为 __weak
  3. 减少全局变量使用
  4. 合理使用 const 修饰符
// 优化示例:将字符串常量放入Flash
const char stateStr[4][8] = {
    "Standby",
    "Setting",
    "Running",
    "Paused "
};

6. 项目扩展与进阶方向

完成基础功能后,可考虑以下扩展:

  • 增加闹钟功能 :到达设定时间触发蜂鸣器
  • 添加串口通信 :通过PC端控制计时器
  • 实现RTC同步 :使用硬件RTC保持准确时间
  • 开发上位机软件 :可视化配置和监控

扩展硬件接口示例:

// 伪代码:串口命令处理
void UART_CommandHandler(uint8_t *cmd) {
    if(strcmp(cmd, "START") == 0) {
        currentState = RUNNING;
        HAL_TIM_Base_Start_IT(&htim4);
    }
    // 其他命令处理...
}

在实际教学中发现,学生最容易出错的是状态切换逻辑。建议在开发初期就绘制完整的状态转换图,并使用枚举变量严格管理系统状态。调试时可添加状态日志输出,帮助定位问题。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐