从蓝桥杯赛题到工业级开发:STM32 HAL库下EEPROM与状态机的工程化实践

在嵌入式开发领域,竞赛题目往往浓缩了实际工程中的典型问题。第九届蓝桥杯嵌入式赛题中的EEPROM读写和复杂按键处理,恰恰反映了工业场景中两个高频需求:非易失性数据存储和人机交互设计。本文将跳出"解题"思维,系统讲解如何将这些赛题技能转化为可复用的工程模块。

1. EEPROM驱动的工业级实现

AT24C02这类I2C接口EEPROM在智能仪表、IoT设备中广泛应用,但直接调用HAL库的原始API往往存在隐患。我们通过CubeMX配置和代码封装来实现可靠存储。

1.1 CubeMX的精准配置要点

在Pinout & Configuration标签页中,需要特别注意:

  • I2C模式选择 I2C 而非SMbus
  • 时钟速度设置为 100kHz (标准模式)
  • 启用 I2C中断 (可选但推荐)

关键配置参数对照表:

参数项 推荐值 说明
Timing Settings 0x2000090E 100kHz标准模式时序
No Stretch Mode Disable 保持时钟延展功能
Address Size 7-bit AT24C02标准地址模式

注意:PA6(SCL)、PA7(SDA)必须配置为开漏输出模式,硬件设计时务必外接4.7kΩ上拉电阻

1.2 增强型读写函数封装

原始HAL库的I2C函数缺乏错误恢复机制,我们封装带重试功能的版本:

#define EEPROM_RETRY_TIMES 3

uint8_t eeprom_read_byte(uint16_t addr) {
    uint8_t data = 0;
    HAL_StatusTypeDef status;
    uint8_t retry = 0;
    
    do {
        uint8_t dev_addr = 0xA0 | ((addr >> 8) & 0x02);
        status = HAL_I2C_Mem_Read(&hi2c1, dev_addr, addr & 0xFF, 
                                 I2C_MEMADD_SIZE_8BIT, &data, 1, 100);
        if(status != HAL_OK) {
            HAL_Delay(5);
            I2C_Reset(&hi2c1);
        }
    } while(status != HAL_OK && ++retry < EEPROM_RETRY_TIMES);
    
    return data;
}

void eeprom_write_byte(uint16_t addr, uint8_t data) {
    HAL_StatusTypeDef status;
    uint8_t retry = 0;
    
    do {
        uint8_t dev_addr = 0xA0 | ((addr >> 8) & 0x02);
        status = HAL_I2C_Mem_Write(&hi2c1, dev_addr, addr & 0xFF,
                                  I2C_MEMADD_SIZE_8BIT, &data, 1, 100);
        if(status != HAL_OK) {
            HAL_Delay(10);
            I2C_Reset(&hi2c1);
        } else {
            HAL_Delay(5); // 写入周期等待
        }
    } while(status != HAL_OK && ++retry < EEPROM_RETRY_TIMES);
}

关键增强特性:

  • 自动重试机制 :应对I2C总线干扰
  • 硬件复位恢复 :调用 I2C_Reset() 函数重置外设
  • 写入延迟保护 :确保EEPROM完成内部编程

2. 状态机按键处理的工程化设计

赛题中的长短按、嵌套按键需求,在工业HMI设计中极为常见。传统if-else嵌套方式难以维护,我们采用状态机模式实现。

2.1 按键状态机建模

定义按键状态转移图:

[IDLE] -- 按下 --> [PRESSED] -- 释放(<800ms) --> [SHORT_PRESS]
               |                  
               -- 持续按压(≥800ms) --> [LONG_PRESS]

对应的状态枚举和数据结构:

typedef enum {
    KEY_IDLE,
    KEY_PRESSED,
    KEY_SHORT_PRESS,
    KEY_LONG_PRESS
} KeyState;

typedef struct {
    GPIO_TypeDef* port;
    uint16_t pin;
    KeyState state;
    uint32_t press_tick;
    uint8_t long_press_flag;
} KeyHandle;

2.2 通用按键处理框架

实现跨项目的可复用模块:

void key_scan(KeyHandle* key) {
    switch(key->state) {
        case KEY_IDLE:
            if(HAL_GPIO_ReadPin(key->port, key->pin) == GPIO_PIN_RESET) {
                key->state = KEY_PRESSED;
                key->press_tick = HAL_GetTick();
            }
            break;
            
        case KEY_PRESSED:
            if(HAL_GPIO_ReadPin(key->port, key->pin) == GPIO_PIN_SET) {
                key->state = KEY_SHORT_PRESS;
            } 
            else if(HAL_GetTick() - key->press_tick >= 800) {
                key->state = KEY_LONG_PRESS;
                key->long_press_flag = 1;
            }
            break;
            
        case KEY_SHORT_PRESS:
        case KEY_LONG_PRESS:
            key->state = KEY_IDLE;
            break;
    }
}

// 使用示例
KeyHandle btn1 = {KEY_B1_GPIO_Port, KEY_B1_Pin, KEY_IDLE, 0, 0};

void main_loop() {
    key_scan(&btn1);
    
    if(btn1.state == KEY_SHORT_PRESS) {
        // 短按处理
    }
    else if(btn1.long_press_flag) {
        btn1.long_press_flag = 0;
        // 长按处理
    }
}

优势特性:

  • 无阻塞设计 :避免 HAL_Delay() 影响系统实时性
  • 时间戳判定 :精确计算按压时长
  • 状态自动复位 :无需手动清除标志位

3. 工程实践中的进阶技巧

3.1 EEPROM页写入优化

AT24C02具有16字节页写入能力,批量写入时效率提升显著:

void eeprom_page_write(uint16_t addr, uint8_t* data, uint8_t len) {
    uint8_t dev_addr = 0xA0 | ((addr >> 8) & 0x02);
    uint8_t page_offset = addr % 16;
    uint8_t can_write = 16 - page_offset;
    
    if(len > can_write) {
        HAL_I2C_Mem_Write(&hi2c1, dev_addr, addr, I2C_MEMADD_SIZE_8BIT,
                         data, can_write, 100);
        HAL_Delay(5);
        eeprom_page_write(addr + can_write, data + can_write, len - can_write);
    } else {
        HAL_I2C_Mem_Write(&hi2c1, dev_addr, addr, I2C_MEMADD_SIZE_8BIT,
                         data, len, 100);
        HAL_Delay(5);
    }
}

3.2 复合按键状态机

处理按键组合场景(如B2+B3同时按下):

typedef struct {
    KeyHandle* keys;
    uint8_t count;
    uint32_t combo_mask;
} KeyGroup;

void key_group_scan(KeyGroup* group) {
    uint32_t current_mask = 0;
    
    for(int i=0; i<group->count; i++) {
        key_scan(&group->keys[i]);
        if(group->keys[i].state != KEY_IDLE) {
            current_mask |= (1 << i);
        }
    }
    
    if(current_mask == group->combo_mask) {
        // 触发组合键功能
    }
}

4. 调试与性能优化

4.1 I2C总线故障排查

常见问题及解决方案:

现象 可能原因 解决方法
读取数据全为0xFF 上拉电阻过大 减小上拉电阻至4.7kΩ以下
随机读取失败 时序不符合器件要求 调整I2C_TIMING寄存器值
连续写入失败 未遵守写周期等待 写入后延迟5ms以上
地址无应答 器件地址配置错误 确认A0/A1/A2引脚电平

4.2 状态机调试技巧

添加调试输出辅助分析:

const char* key_state_str[] = {
    "IDLE", "PRESSED", "SHORT_PRESS", "LONG_PRESS"  
};

void key_scan_debug(KeyHandle* key, const char* name) {
    KeyState prev = key->state;
    key_scan(key);
    
    if(prev != key->state) {
        printf("[%s] %s -> %s\n", name, key_state_str[prev], 
               key_state_str[key->state]);
    }
}

在STM32CubeIDE中,通过SWD接口实时输出状态变化,配合逻辑分析仪捕获GPIO波形,可快速定位复杂按键逻辑问题。

Logo

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

更多推荐