4. I2C总线与传感器驱动:I2C时序与寻址、EEPROM读写、温湿度传感器SHT30驱动移植

I2C总线,说白了就是两根线搞定一堆外设。SCL(时钟线)和SDA(数据线),挂多少个设备都行,只要地址不冲突。我刚开始用I2C时总觉得它慢,后来发现很多传感器、存储芯片都靠它,省引脚才是硬道理。

这一章我们不讲虚的。先搞懂I2C的时序和寻址,然后用EEPROM练手读写,最后移植SHT30温湿度传感器驱动。嗯,都是实战干货。

4.1 I2C总线协议基础

I2C是主从结构。STM32做主设备,外设做从设备。每次通信都由主设备发起,从设备只能响应。

4.1.1 物理层与电气特性

  • SCL:时钟线,由主设备控制
  • SDA:数据线,双向传输
  • 上拉电阻:一般4.7kΩ,总线电容大时用2.2kΩ
  • 标准模式:100kHz;快速模式:400kHz
我的经验:上拉电阻别乱选。线长超过10cm时,我习惯用2.2kΩ,否则波形上升沿太缓,容易丢数据。

4.1.2 时序与起始/停止条件

I2C的时序其实很简单。记住两个关键点:

  • 起始条件:SCL高电平时,SDA从高变低
  • 停止条件:SCL高电平时,SDA从低变高
  • 数据传输:SCL低电平时改变SDA,高电平时采样

为什么会这样设计?说白了就是为了让从设备能准确识别通信的开始和结束。我曾经在项目里因为起始条件没处理好,导致总线卡死,后来加了超时复位才解决。

4.1.3 寻址与应答机制

每个I2C从设备都有7位或10位地址。STM32常用7位地址。主设备发送地址后,从设备必须回复ACK(应答)才能继续。

地址位 说明
7位地址 高7位,如0x44(SHT30地址)
读写位 第8位:0=写,1=读
ACK/NACK 第9位:从设备拉低SDA为ACK
关键点:发送地址后一定要检查ACK。如果没收到ACK,说明设备不存在或地址错了。我调试时经常用逻辑分析仪看这一位。

4.2 EEPROM读写实战

EEPROM是I2C最经典的应用之一。AT24C02,2Kbit容量,256字节,够存配置参数。我们用它来练手读写。

4.2.1 AT24C02的寻址与页写

AT24C02的地址由硬件引脚A0、A1、A2决定。一般接地,地址为0x50(7位)。写操作分字节写和页写,页写一次最多16字节。

// AT24C02写一个字节
uint8_t I2C_WriteByte(uint8_t dev_addr, uint8_t mem_addr, uint8_t data) {
    // 发送起始条件
    I2C_Start();
    // 发送设备地址+写位
    I2C_SendByte(dev_addr << 1 | 0);
    if (!I2C_WaitAck()) return 1;
    // 发送存储器地址
    I2C_SendByte(mem_addr);
    if (!I2C_WaitAck()) return 1;
    // 发送数据
    I2C_SendByte(data);
    if (!I2C_WaitAck()) return 1;
    // 发送停止条件
    I2C_Stop();
    return 0;
}
注意:EEPROM写完后需要5ms左右的内部编程时间。连续写太快会丢数据。我踩过这个坑,后来加了延时或查询写完成标志。

4.2.2 连续读取与随机读取

读EEPROM有两种方式:

  • 当前地址读:直接读当前指针位置
  • 随机地址读:先写地址,再重新起始读

连续读时,每读一个字节主设备要回复ACK,直到最后一个字节回复NACK。嗯,这个细节容易忘。

// AT24C02连续读多个字节
void I2C_ReadMulti(uint8_t dev_addr, uint8_t mem_addr, uint8_t *buf, uint8_t len) {
    I2C_Start();
    I2C_SendByte(dev_addr << 1 | 0);  // 写地址
    I2C_WaitAck();
    I2C_SendByte(mem_addr);            // 写存储器地址
    I2C_WaitAck();
    I2C_Start();                       // 重新起始
    I2C_SendByte(dev_addr << 1 | 1);  // 读地址
    I2C_WaitAck();
    for (uint8_t i = 0; i < len; i++) {
        buf[i] = I2C_ReadByte();
        if (i == len - 1)
            I2C_SendNack();            // 最后回复NACK
        else
            I2C_SendAck();
    }
    I2C_Stop();
}

4.3 SHT30温湿度传感器驱动移植

SHT30是Sensirion的经典数字温湿度传感器。精度高、体积小、I2C接口。我做过一个环境监测项目,用了20多个SHT30,稳定性不错。

4.3.1 SHT30的I2C地址与命令集

SHT30的7位地址固定为0x44(ADDR引脚接GND)或0x45(接VCC)。常用命令:

命令 代码(16位) 说明
测量(高重复性) 0x2C06 时钟拉伸使能
测量(低重复性) 0x2C10 快速模式
读取状态寄存器 0xF32D 检查设备状态
软复位 0x30A2 复位传感器

4.3.2 驱动移植步骤

移植SHT30驱动,说白了就是封装I2C读写函数。我习惯分三层:

  1. 硬件层:I2C起始、停止、收发字节
  2. 驱动层:SHT30初始化、触发测量、读取数据
  3. 应用层:温湿度计算、数据滤波
// SHT30触发测量(高重复性)
uint8_t SHT30_StartMeasure(void) {
    uint8_t cmd[2] = {0x2C, 0x06};
    I2C_Start();
    I2C_SendByte(SHT30_ADDR << 1 | 0);
    if (!I2C_WaitAck()) return 1;
    I2C_SendByte(cmd[0]);
    I2C_WaitAck();
    I2C_SendByte(cmd[1]);
    I2C_WaitAck();
    I2C_Stop();
    return 0;
}

// 读取温湿度数据
uint8_t SHT30_ReadData(float *temp, float *hum) {
    uint8_t buf[6];
    I2C_Start();
    I2C_SendByte(SHT30_ADDR << 1 | 1);
    I2C_WaitAck();
    for (uint8_t i = 0; i < 6; i++) {
        buf[i] = I2C_ReadByte();
        if (i == 5) I2C_SendNack();
        else I2C_SendAck();
    }
    I2C_Stop();
    // 计算温湿度
    uint16_t raw_temp = (buf[0] << 8) | buf[1];
    uint16_t raw_hum  = (buf[3] << 8) | buf[4];
    *temp = -45.0f + 175.0f * raw_temp / 65535.0f;
    *hum  = 100.0f * raw_hum / 65535.0f;
    return 0;
}
避坑指南:SHT30测量需要等待约15ms。我刚开始没加延时,读出来的数据全是0。后来查手册才发现,触发测量后必须等数据就绪。

4.4 知识体系结构图

下面这张图把本章的核心逻辑串起来了。从I2C总线到EEPROM,再到SHT30,层层递进。

I2C总线与传感器驱动知识体系 I2C总线协议 时序与起始/停止 寻址与应答机制 读写操作流程 EEPROM(AT24C02)读写 SHT30温湿度传感器 驱动移植与数据采集 核心:理解I2C时序 → 掌握EEPROM读写 → 移植SHT30驱动 每一层都依赖底层协议,建议从硬件层开始调试

4.5 调试技巧与常见问题

I2C调试说难不难,说简单也容易卡住。我总结几个常见问题:

  • 总线卡死:SDA被拉低不放。解决办法:模拟9个时钟脉冲复位从设备
  • 无ACK响应:地址错误或设备未上电。用逻辑分析仪看波形
  • 数据错位:字节顺序搞反。SHT30数据是大端,注意高低字节
  • 测量值跳变:电源噪声或布线过长。加100nF去耦电容
曾经踩过的坑:有一次SHT30读出来的湿度一直是99.9%,查了半天发现是I2C速率设成了400kHz,而传感器供电电压只有3.0V。降速到100kHz后恢复正常。嗯,供电不足时别跑高速。

这一章的内容就到这里。I2C总线是嵌入式开发的基本功,EEPROM和SHT30是两个典型应用。你动手写一遍代码,跑通波形,比看十遍书都管用。