蓝桥杯嵌入式G4实战:按键控制MCP4017电阻与AT24C02存储系统开发指南

在嵌入式系统开发中,I²C总线因其简单的两线制设计和多设备支持能力,成为连接各类外设的首选方案。本文将深入探讨如何基于STM32G4系列微控制器,构建一个完整的交互式电阻控制系统,通过按键调节MCP4017数字电位器阻值,利用ADC采集电压数据,并将结果存储至AT24C02 EEPROM中。这个实战项目不仅涵盖了蓝桥杯嵌入式竞赛的核心考点,更是工业控制系统中参数配置与数据记录的典型应用场景。

1. 系统架构设计与硬件配置

1.1 硬件模块选型与连接

本系统采用STM32G431RB作为主控制器,这是蓝桥杯嵌入式竞赛官方指定的开发板核心芯片。关键外设包括:

  • MCP4017:Microchip生产的7位数字电位器,127级可调,最大阻值100kΩ
  • AT24C02:2Kbit(256×8)串行EEPROM,支持100kHz(1.8V)和400kHz(≥2.5V)两种速度
  • ADC通道:PB14引脚配置为ADC输入,用于测量分压电路电压

硬件连接示意图:

设备 SDA引脚 SCL引脚 其他连接
MCP4017 PB7 PB6 VDD→3.3V, VSS→GND
AT24C02 PB7 PB6 A0-A2→GND
分压电路 - - 中间点→PB14

1.2 I²C总线初始化关键代码

void I2C_GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    __HAL_RCC_GPIOB_CLK_ENABLE();
    
    /* PB6(SCL), PB7(SDA) 配置为开漏输出 */
    GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    
    /* 初始状态置高 */
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6|GPIO_PIN_7, GPIO_PIN_SET);
}

注意:虽然STM32G4内置硬件I²C控制器,但蓝桥杯官方库采用GPIO模拟实现,需严格遵循时序要求。

2. MCP4017数字电位器控制实现

2.1 器件特性与寄存器配置

MCP4017采用7位线性电阻网络,电阻值计算公式为: [ R_{wiper} = R_{max} \times \frac{D}{127} ] 其中D为写入的数值(0-127),Rmax为100kΩ。

通信时序特点:

  1. 写操作地址字节:0x5E
  2. 读操作地址字节:0x5F
  3. 单字节数据传输,无子地址需求

2.2 完整驱动代码实现

#define MCP4017_WRITE_ADDR 0x5E
#define MCP4017_READ_ADDR  0x5F

void MCP4017_Write(uint8_t value)
{
    I2C_Start();
    I2C_SendByte(MCP4017_WRITE_ADDR);
    I2C_WaitAck();
    I2C_SendByte(value & 0x7F); // 确保最高位为0
    I2C_WaitAck();
    I2C_Stop();
}

uint8_t MCP4017_Read(void)
{
    uint8_t value;
    
    I2C_Start();
    I2C_SendByte(MCP4017_READ_ADDR);
    I2C_WaitAck();
    value = I2C_ReceiveByte();
    I2C_SendNAck();
    I2C_Stop();
    
    return value;
}

2.3 电阻-电压转换计算

在10kΩ分压电阻的典型应用中,输出电压计算如下:

float CalculateVoltage(uint8_t digital_value)
{
    float resistance = 787.4f * digital_value; // 单位:欧姆
    return 3.3f * resistance / (resistance + 10000.0f);
}

3. AT24C02数据存储方案

3.1 EEPROM特性与地址规划

AT24C02存储空间组织:

  • 256字节容量,分为32页,每页8字节
  • 器件地址:0xA0(写)/0xA1(读)
  • 写入周期:5ms(典型值)

建议的数据存储结构:

地址范围 用途 数据类型
0x00-0x01 电压值整数部分 uint16_t
0x02-0x05 电压值小数部分 float
0x06-0x07 电阻设置值 uint8_t

3.2 增强型存储函数实现

void EEPROM_WriteMulti(uint8_t addr, uint8_t *data, uint8_t len)
{
    uint8_t i;
    
    if(addr + len > 256) return; // 地址越界保护
    
    I2C_Start();
    I2C_SendByte(0xA0);
    I2C_WaitAck();
    I2C_SendByte(addr);
    I2C_WaitAck();
    
    for(i=0; i<len; i++) {
        I2C_SendByte(data[i]);
        I2C_WaitAck();
        HAL_Delay(1); // 页写入间隔
    }
    
    I2C_Stop();
    HAL_Delay(5); // 等待写入完成
}

void EEPROM_ReadMulti(uint8_t addr, uint8_t *buf, uint8_t len)
{
    uint8_t i;
    
    if(addr + len > 256) return;
    
    I2C_Start();
    I2C_SendByte(0xA0);
    I2C_WaitAck();
    I2C_SendByte(addr);
    I2C_WaitAck();
    
    I2C_Start();
    I2C_SendByte(0xA1);
    I2C_WaitAck();
    
    for(i=0; i<len-1; i++) {
        buf[i] = I2C_ReceiveByte();
        I2C_SendAck();
    }
    buf[len-1] = I2C_ReceiveByte();
    I2C_SendNAck();
    
    I2C_Stop();
}

4. 多任务按键管理系统

4.1 按键功能分配与消抖处理

开发板按键配置方案:

按键 引脚 功能
B1 PB0 读取EEPROM存储值
B2 PB1 设置电阻为中间值(64)
B3 PB2 电阻值递增(+10)
B4 PB3 保存当前设置到EEPROM

按键消抖状态机实现:

typedef struct {
    uint8_t  current_state;
    uint16_t debounce_cnt;
    uint8_t  single_flag;
    uint8_t  long_flag;
} Key_TypeDef;

Key_TypeDef keys[4];

void Key_Scan(void)
{
    static const uint16_t DEBOUNCE_TH = 20;
    static const uint16_t LONG_PRESS_TH = 1000;
    uint8_t i;
    
    for(i=0; i<4; i++) {
        uint8_t pin_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0<<i);
        
        if(pin_state == GPIO_PIN_RESET) {
            if(keys[i].debounce_cnt < 0xFFFF) keys[i].debounce_cnt++;
            
            if(keys[i].debounce_cnt == DEBOUNCE_TH) {
                keys[i].single_flag = 1;
            }
            else if(keys[i].debounce_cnt >= LONG_PRESS_TH) {
                keys[i].long_flag = 1;
                keys[i].debounce_cnt = LONG_PRESS_TH; // 防止溢出
            }
        }
        else {
            if(keys[i].debounce_cnt >= DEBOUNCE_TH && 
               keys[i].debounce_cnt < LONG_PRESS_TH) {
                // 短按已触发,不做处理
            }
            keys[i].debounce_cnt = 0;
        }
    }
}

4.2 主控制逻辑实现

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_ADC1_Init();
    MX_TIM3_Init();
    
    LCD_Init();
    I2C_GPIO_Init();
    
    uint8_t current_res = 0;
    float current_voltage = 0.0f;
    uint16_t adc_value = 0;
    
    while(1) {
        Key_Scan();
        
        // 按键1:读取存储值
        if(keys[0].single_flag) {
            keys[0].single_flag = 0;
            uint8_t stored_val = EEPROM_Read(0x06);
            MCP4017_Write(stored_val);
            current_res = stored_val;
        }
        
        // 按键2:设置中间值
        if(keys[1].single_flag) {
            keys[1].single_flag = 0;
            MCP4017_Write(64);
            current_res = 64;
        }
        
        // 按键3:电阻递增
        if(keys[2].single_flag) {
            keys[2].single_flag = 0;
            if(current_res < 127) current_res++;
            MCP4017_Write(current_res);
        }
        
        // 按键4:保存当前设置
        if(keys[3].single_flag) {
            keys[3].single_flag = 0;
            EEPROM_Write(0x06, current_res);
            
            HAL_ADC_Start(&hadc1);
            adc_value = HAL_ADC_GetValue(&hadc1);
            current_voltage = adc_value * 3.3f / 4095.0f;
            
            uint16_t vol_int = (uint16_t)current_voltage;
            uint16_t vol_frac = (uint16_t)((current_voltage - vol_int)*1000);
            
            EEPROM_Write(0x00, vol_int >> 8);
            EEPROM_Write(0x01, vol_int & 0xFF);
            EEPROM_Write(0x02, vol_frac >> 8);
            EEPROM_Write(0x03, vol_frac & 0xFF);
        }
        
        // LCD显示更新
        char disp_buf[20];
        sprintf(disp_buf, "Res: %3d/127", current_res);
        LCD_DisplayStringLine(Line1, (uint8_t*)disp_buf);
        
        float res_kohm = 0.7874f * current_res;
        sprintf(disp_buf, "R: %.2f kΩ", res_kohm);
        LCD_DisplayStringLine(Line2, (uint8_t*)disp_buf);
        
        HAL_ADC_Start(&hadc1);
        adc_value = HAL_ADC_GetValue(&hadc1);
        current_voltage = adc_value * 3.3f / 4095.0f;
        
        sprintf(disp_buf, "V: %.3f V", current_voltage);
        LCD_DisplayStringLine(Line3, (uint8_t*)disp_buf);
        
        HAL_Delay(50);
    }
}

5. 系统调试与性能优化

5.1 常见问题排查指南

  1. I²C通信失败

    • 检查上拉电阻(通常4.7kΩ)
    • 确认设备地址正确
    • 用逻辑分析仪捕获时序波形
  2. AT24C02写入异常

    • 确保写入间隔≥5ms
    • 页写入不超过8字节边界
    • 验证ACK信号是否正常
  3. ADC读数不稳定

    • 添加0.1μF去耦电容
    • 软件多次采样取平均
    • 检查参考电压稳定性

5.2 关键参数测量结果

测试数据记录表:

设定值 理论电阻(kΩ) 实测电阻(kΩ) 理论电压(V) 实测电压(V)
0 0.00 0.02 0.000 0.002
32 25.20 25.18 0.820 0.818
64 50.39 50.35 1.380 1.378
96 75.59 75.52 1.800 1.795
127 100.00 99.91 2.231 2.225

5.3 低功耗优化策略

  1. 间歇工作模式
void Enter_LowPowerMode(void)
{
    HAL_ADC_Stop(&hadc1);
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6|GPIO_PIN_7, GPIO_PIN_SET);
    HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
    SystemClock_Config(); // 唤醒后重新配置时钟
}
  1. 动态速率调整

    • 非关键操作时降低I²C时钟频率
    • ADC采样速率根据需求动态调整
  2. 智能唤醒机制

    • 按键中断唤醒MCU
    • 定时采集模式

在完成这个项目的过程中,最值得注意的经验是I²C总线的时序稳定性对系统可靠性影响极大。实际调试时,通过增加关键操作后的延时,并严格检查每个ACK信号,可以显著提高通信成功率。对于需要精确控制的应用,建议将电阻调节步进值存储在数组中,通过查表法实现非线性调节,这比简单的线性递增更能满足实际需求。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐