stm32-I2C

发布于 2024-10-26  84 次阅读


I2C 接口与 EEPROM 存储器

1. I2C 概述

  • I2C(Inter-Integrated Circuit) 是一种广泛应用的半双工、双线串行通信协议,由飞利浦公司在上世纪 80 年代推出。
  • I2C 总线采用 SCL(串行时钟线)SDA(串行数据线) 两条线进行数据传输。
    • SCL:由主设备产生的时钟信号,用于同步数据传输。
    • SDA:用于在主从设备之间传输数据,支持双向传输。
  • I2C 通信特点
    • 半双工:数据传输在一根线上,通信过程主从设备需协调进行发送和接收。
    • 多主多从:支持多个主设备和多个从设备挂载在同一总线上,通过设备地址进行区分。
    • 同步通信:数据传输由时钟信号同步,适用于短距离和板上通信。

2. 与 EEPROM 的通信

  • EEPROM(Electrically Erasable Programmable Read-Only Memory) 是一种电可擦除可编程只读存储器,可以在系统运行时对其进行读取和写入。
  • 常见的 I2C 接口 EEPROM,如 24C02、24C04 等,被广泛用于数据的存储和备份。
  • I2C EEPROM 通信特点
    • 设备地址:每个 EEPROM 具有固定的设备地址,通常前几位为固定值,后几位由硬件引脚(如 A0、A1、A2)决定,允许在同一总线上挂接多个 EEPROM。
    • 读写操作:按照 EEPROM 的通信协议,主设备需要在事务中发送开始信号、设备地址、存储地址,然后进行数据的读写操作。

3. I2C 配置步骤

在 STM32F103 中使用 I2C 接口,需要按照以下步骤进行配置:

  1. 使能 I2C 和 GPIO 时钟

    在使用外设之前,需要开启相应的时钟。

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);     // 使能 I2C1 时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);    // 使能 GPIOB 时钟
  2. 配置 GPIO 引脚为复用开漏功能

    I2C 总线采用开漏方式驱动,需要在硬件上通过上拉电阻将总线拉高。

    GPIO_InitTypeDef GPIO_InitStructure;
    
    // 配置 I2C1 的 SCL(PB6)和 SDA(PB7)引脚
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;    // 复用开漏输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
  3. 初始化 I2C 外设

    设置 I2C 的工作模式、时钟速度、地址模式等参数。

    I2C_InitTypeDef I2C_InitStructure;
    
    I2C_InitStructure.I2C_ClockSpeed = 100000;                      // 设置 I2C 时钟速度为 100kHz
    I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;                      // I2C 模式
    I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;              // 占空比 1:2(标准模式)
    I2C_InitStructure.I2C_OwnAddress1 = 0x00;                       // 主机自身地址(一般不使用,可设为 0)
    I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;                     // 使能应答
    I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;  // 7 位地址模式
    I2C_Init(I2C1, &I2C_InitStructure);
  4. 使能 I2C 外设

    在配置完成后,启用 I2C 外设。

    I2C_Cmd(I2C1, ENABLE);

4. 示例:I2C 与 EEPROM 的读写

以下示例演示了如何通过 I2C1 与 EEPROM(如 24C02)进行读写操作。

4.1 初始化 I2C1

void I2C1_Init(void) {
    // 1. 使能时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);     // I2C1 时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);    // GPIOB 时钟

    // 2. 配置 GPIO 引脚
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;   // PB6 - SCL, PB7 - SDA
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;          // 复用开漏输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    // 3. 初始化 I2C1
    I2C_InitTypeDef I2C_InitStructure;
    I2C_InitStructure.I2C_ClockSpeed = 100000;                      // 标准模式,时钟速度 100kHz
    I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;                      // I2C 模式
    I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;              // 占空比 2
    I2C_InitStructure.I2C_OwnAddress1 = 0x00;                       // 主机地址(可忽略)
    I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;                     // 使能应答
    I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;  // 7 位地址模式
    I2C_Init(I2C1, &I2C_InitStructure);

    // 4. 使能 I2C1
    I2C_Cmd(I2C1, ENABLE);
}

4.2 定义 EEPROM 的器件地址

#define EEPROM_ADDRESS 0xA0   // EEPROM 器件地址,写操作

4.3 向 EEPROM 写入一个字节数据

void EEPROM_WriteByte(uint8_t addr, uint8_t data) {
    // 1. 产生启动信号
    I2C_GenerateSTART(I2C1, ENABLE);
    // 2. 等待 EV5 事件(起始条件已发送)
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    // 3. 发送 EEPROM 器件地址(写)
    I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);
    // 4. 等待 EV6 事件(地址已发送并应答)
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

    // 5. 发送存储地址
    I2C_SendData(I2C1, addr);
    // 6. 等待 EV8_2 事件
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

    // 7. 发送数据
    I2C_SendData(I2C1, data);
    // 8. 等待 EV8_2 事件
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

    // 9. 产生停止信号
    I2C_GenerateSTOP(I2C1, ENABLE);
}

4.4 从 EEPROM 读取一个字节数据

uint8_t EEPROM_ReadByte(uint8_t addr) {
    uint8_t data;

    // 1. 产生启动信号
    I2C_GenerateSTART(I2C1, ENABLE);
    // 2. 等待 EV5 事件
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    // 3. 发送 EEPROM 器件地址(写)
    I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);
    // 4. 等待 EV6 事件
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

    // 5. 发送存储地址
    I2C_SendData(I2C1, addr);
    // 6. 等待 EV8_2 事件
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

    // 7. 产生重复启动信号
    I2C_GenerateSTART(I2C1, ENABLE);
    // 8. 等待 EV5 事件
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    // 9. 发送 EEPROM 器件地址(读)
    I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Receiver);
    // 10. 等待 EV6 事件
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));

    // 11. 禁用应答
    I2C_AcknowledgeConfig(I2C1, DISABLE);
    // 12. 产生停止信号
    I2C_GenerateSTOP(I2C1, ENABLE);

    // 13. 等待 EV7 事件,读取数据
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
    data = I2C_ReceiveData(I2C1);

    // 14. 重新使能应答,以备下次通讯
    I2C_AcknowledgeConfig(I2C1, ENABLE);

    return data;
}

4.5 示例:测试 EEPROM 读写

int main(void) {
    uint8_t write_data = 0x55;    // 要写入的数据
    uint8_t read_data;

    I2C1_Init();  // 初始化 I2C1

    // 向 EEPROM 地址 0x10 处写入数据
    EEPROM_WriteByte(0x10, write_data);
    // 简单延时,等待写入完成(EEPROM 写入需要一定时间)
    Delay_ms(5);  // 自定义延时函数

    // 从 EEPROM 地址 0x10 处读取数据
    read_data = EEPROM_ReadByte(0x10);

    // 检查读取的数据是否与写入的数据一致
    if (read_data == write_data) {
        // 读取成功
    } else {
        // 读取失败
    }

    while (1) {
        // 主循环
    }
}

5. 注意事项

  • 总线拉高:I2C 总线采用开漏驱动方式,需要在 SCL 和 SDA 线上接上拉电阻(通常为 4.7kΩ 到 10kΩ),否则总线无法正常工作。
  • 时序要求:EEPROM 等 I2C 设备对通信时序有严格要求,应按照器件手册的通信流程编写代码。
  • 等待时间:EEPROM 在写入数据后,需要一定的时间完成内部擦写操作(通常为 5ms 左右),在写入后需要适当延时。
  • 应答位控制:在读取最后一个字节数据前,需要禁用应答位,防止从设备继续发送数据。
  • 多字节读写:对于需要连续读写多个字节的数据,可以编写对应的函数,实现批量数据的传输。

6. 扩展:I2C 库函数的简化处理

为了简化 I2C 通信的代码量和复杂性,可以编写封装的 I2C 读写函数,简化主程序的调用。同时,也可以考虑使用 I2C 的中断或 DMA 模式,提高通信效率。

// I2C 发送数据函数
void I2C_WriteBytes(I2C_TypeDef* I2Cx, uint8_t device_addr, uint8_t reg_addr, uint8_t* data, uint16_t length);

// I2C 接收数据函数
void I2C_ReadBytes(I2C_TypeDef* I2Cx, uint8_t device_addr, uint8_t reg_addr, uint8_t* data, uint16_t length);

通过封装,可使主程序的调用更加简洁明了。

最后更新于 2024-11-17