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 接口,需要按照以下步骤进行配置:
-
使能 I2C 和 GPIO 时钟
在使用外设之前,需要开启相应的时钟。
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); // 使能 I2C1 时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 使能 GPIOB 时钟
-
配置 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);
-
初始化 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);
-
使能 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);
通过封装,可使主程序的调用更加简洁明了。
Comments NOTHING