I2C工作原理I2C读写AT24C04

发布时间:2026/8/20 15:33:01
I2C工作原理I2C读写AT24C04 IIC工作原理IIC概述IIC物理层面IICInter-Integrated Circuit也称I²C 是一种两线式串行通信总线由Philips公司现NXP发明常用于MCU与传感器、LCD、EEPROM等外设通信。物理层只有两根线IIC是半双工同步通信​SCLSerial Clock Line​时钟线由主机产生控制通信节奏。​SDASerial Data Line​数据线双向传输数据。两根线都是开漏输出必须外接上拉电阻通常4.7kΩ或10kΩ这两个电阻可以做速率和功耗平衡一般不用芯片内部的上下拉因为阻值是固定的这样才能实现多设备共享总线。​​支持的总线速度模式速度常见应用标准模式SM100 kbps兼容性最广快速模式FM400 kbps大部分传感器快速模式FM1 Mbps高性能设备高速模式HS3.4 Mbps特殊场合硬件I2C vs 软件I2C 的原理差异对比硬件I2C软件I2CGPIO模拟时钟产生硬件定时器自动生成SCLCPU通过延时循环翻转GPIO电平检测硬件自动采样SDACPU逐位读取GPIO电平应答处理硬件自动检测ACKCPU手动读取SDA电平判断优点省CPU、速度快引脚任意、移植简单缺点引脚固定、配置复杂占用CPU、速度慢I2C标准时序参数软件I2C的延时时间不是随意设定的必须严格遵循I2C总线协议中的时序参数。如果延时太短从机无法正确采样延时太长通信速度过慢。不同速度模式下关键的时序参数不同参数含义标准模式(100kHz)快速模式(400kHz)快速模式(1MHz)tHIGHSCL高电平持续时间≥4.0µs≥0.6µs≥0.26µstLOWSCL低电平持续时间≥4.7µs≥1.3µs≥0.5µstSU;STA起始条件建立时间≥4.7µs≥0.6µs≥0.26µstHD;STA起始条件保持时间≥4.0µs≥0.6µs≥0.26µstSU;DAT数据建立时间≥250ns≥100ns≥50nstHD;DAT数据保持时间≥300ns≥300ns≥50nstSU;STO停止条件建立时间≥4.0µs≥0.6µs≥0.26µstBUF停止到起始的总线空闲时间≥4.7µs≥1.3µs≥0.5µs这些参数来自 ​NXP I2C-bus specification and user manual​UM10204建议用逻辑分析仪实测SCL频率调整延时值使之符合目标频率。总线上挂载多个I2C设备时延时时间​必须以最慢的那个设备为准​。例如一个设备支持400kHz另一个只支持100kHz → 必须用100kHz的延时IIC通信机制1. 主从架构​主机Master​启动通信、产生时钟、发送地址的设备通常是MCU。​从机Slave​响应主机请求的设备如LCD、传感器。2. 设备寻址每个从机有一个唯一的​7位地址​部分支持10位扩展通信时主机先发送地址只有匹配的从机才会响应。3. 数据帧结构IIC通信以帧为单位典型的一帧包含起始条件 | 7位地址 读写位 | 应答 | 8位数据 | 应答 | ... | 停止条件时序总线处于空闲时SCL、SDA线均处于高电平状态所以总线上的设备都处于开漏模式中的高阻态。当某个设备想要通信时就先在总线上发送start起始信号把SDA拉低此时会在SCL上产生时钟信号是高低电平交错的脉冲信号。起始条件STARTSCL高电平时SDA从高→低跳变表示总线开始通信。​停止条件STOPSCL高电平时SDA从低→高跳变表示总线释放。​数据传输DATA每次传输1个字节8位高位在前MSB first。发送方在SCL低电平时把数据放到SDA上。接收方在SCL高电平时采样SDA。应答位ACK or NACK1ACK应答2NACK非应答每传输完1个字节​接收方必须在第9个时钟周期拉低SDA​表示“收到”。如果接收方拉高SDANACK表示“未收到”或“不想继续接收”。完整通信流程示例写操作流程示例支持连续写入时每写入一个字节都要等待ACK// 伪代码示例 Start(); Send(DeviceAddress 1 | 0); // 写方向 WaitForACK(); // 等待从机ACK从机拉低SDA Send(RegisterAddress); WaitForACK(); // 等待从机ACK Send(Data1); WaitForACK(); // 等待从机ACK Send(Data2); WaitForACK(); // 等待从机ACK最后一个字节也要等待 Stop(); /* 主机准备发送 → 发起始条件 → 发地址写位 → 等待应答 ↓ 收到应答→ 否 → 发送停止条件 → 报错 ↓ 是 发送数据字节 → 等待应答 → 是否还有数据 ↓ 是 → 继续发送 ↓ 否 发送停止条件 → 通信结束 */以上为写模式如若需要转换读模式接收数据需要再发一次起始信号这里后面在IIC读写AT24C02时介绍读操作流程示例连续读取时主机接收最后一个字节发送NACK表示不想再接收其他字节发送ACK// 完整读操作示例 Start(); // 起始 Send(DevAddr 1 | 0); // 写方向 WaitForACK(); // 等待从机ACK Send(RegAddr); // 发送寄存器地址 WaitForACK(); // 等待从机ACK Start(); // 重启条件Restart★核心 Send(DevAddr 1 | 1); // 读方向 WaitForACK(); // 等待从机ACK ReadByte1(); SendACK(); // 读第1字节主机返回ACK还想继续读 ReadByte2(); SendACK(); // 读第2字节主机返回ACK ReadByteN(); SendNACK(); // 读最后1字节主机返回NACK不想再接收 Stop(); // 停止先写地址和寄存器然后重新起始再发地址读位从机才开始发送数据。IIC 读写AT24CxxE2PROM简介——AT24CxxAT24Cxx系列主要差异AT24Cxx系列是经典的 I2C 接口 EEPROM 芯片用起来简单可靠。不同的型号主要在容量和页写入大小上有所区别。如AT24C02也写入大小为8字节一次最多写入8字节超过需要分批次AT24C04页写入大小就是16字节连续写16个字节是完全正常的超过会截断。型号存储容量 (字节)页写入大小 (字节)可挂载数量 (I2C总线)AT24C0112888AT24C0225688AT24C04512164AT24C081024162AT24C162048161AT24C32/644096/8192328AT24C128/25616384/3276864(资料未明确提及)AT24C51265536128(资料未明确提及)写入关键点页写入机制这是E2PROM为了加快写入速度而设计的特性允许在一个写周期内连续写入一页的数据页缓冲区。正是因为AT24C04支持16字节的页写入所以你一次写入11个字节是它规定动作内的正常操作。地址指针回卷如果一次写入的数据超过了当前页的剩余空间地址指针会自动回卷到该页的开头覆盖之前刚写入的数据。地址指针在页内循环。比如AT24C02一次最多写入16字节而你的IIC时序发送的要写入的字节数为20个超出的会覆盖开始的因为地址指针回卷到了该页开头。所以编写程序时要实现跨页处理或分批写入。设备地址的差异为了在一条I2C总线上挂多个设备不同型号的硬件地址引脚A2, A1, A0定义不同。比如AT24C02可以用A0、A1、A2三个引脚组合出8个地址而AT24C04的A0脚是空脚No Connect只用A1和A2因此最多只能挂4个。为什么AT24C02只支持8字节页写入其他型号类似看最大页写入字节数AT24C02内部有个​8字节的临时缓冲区​写入时数据先放到这个缓冲区发送停止条件后芯片才开始真正写入EEPROM超过8字节缓冲区溢出超出的数据被丢弃读取操作读取操作没有像写入那样的单次最多16字节限制。但读操作并非完全不受限它的限制主要体现在另一个方面地址自动翻转Roll-over。AT24C04支持三种读操作其中顺序读Sequential Read是实现连续读取大量数据的方式。在顺序读模式下当你连续发送ACK信号应答信号给EEPROM时芯片内部的地址计数器会自动加1并持续输出下一个地址的数据。这个操作可以一直持续下去直到你发送一个NACK信号非应答信号来主动终止它。这个连续读取过程有一个关键行为当读取到达整个存储空间的最后一个地址对于AT24C04是地址511时地址计数器不会停止而是会回卷roll over到地址0然后继续输出数据。读取的限制在于​没有单次传输的字节数上限​理论上你可以一次读取从地址0到地址511的所有512个字节只要你不发送NACK信号。​存在的唯一限制是地址回卷​如果从地址400开始连续读200个字节当读到地址511后下一个读出的数据会是地址0的内容。这个行为是符合规范的不是错误。软件 IIC 写入读取AT24C04这里使用STM32F103C8T6芯片STM32CubeMx生成的HAL库代码实现编写分层结构APP-Driver-BSP-HAL实现四个文件“ bsp_e2prom.c/.h ” “driver_e2prom.c/.h”然后在“main.c中调用“driver_e2prom.h”实现分层解耦还有一个工具层次存放延时函数字符库等各层按需调用APP 层 (应用逻辑) ___↓ 调用 |Driver 层 (AT24C02、OLED、MPU6050等设备驱动) |↓ 调用 |BSP 层 (I2C总线接口 - 可以是硬件或软件实现) |- utility层├── 硬件I2C实现 (使用STM32 I2C外设) |└── 软件I2C实现 (GPIO模拟) |↓ 调用 |HAL 层 (GPIO、时钟、中断等) ___ |bsp_e2prom.c/.h/*--------------------------------------------------bsp_e2prom.h ----------------------------------------------------------*/ #ifndef __BSP_E2PROM_H__ #define __BSP_E2PROM_H__ #include main.h #include utility_delay.h #ifndef USE_SOFT_I2C #define USE_SOFT_I2C // 默认使用软件I2C #endif #ifndef USE_SOFT_I2C //如果没有定义 USE_SOFT_I2C,使用硬件I2C代码 #else //如果定义了 USE_SOFT_I2C,使用软件I2C代码 #define SOFT_WAIT_ACK_TIME 1000 /*--------------------软件IIC引脚定义--------------------*/ #define SOFT_IIC_SCL_PORT GPIOB #define SOFT_IIC_SDA_PORT GPIOB #define SOFT_IIC_SCL_PIN GPIO_PIN_4 #define SOFT_IIC_SDA_PIN GPIO_PIN_5 /*--------------------------时序-------------------------*/ #define SCL(x) HAL_GPIO_WritePin(SOFT_IIC_SCL_PORT,SOFT_IIC_SCL_PIN,(GPIO_PinState)x) #define SDA(x) HAL_GPIO_WritePin(SOFT_IIC_SDA_PORT,SOFT_IIC_SDA_PIN,(GPIO_PinState)x) #define SDA_READ() (HAL_GPIO_ReadPin(SOFT_IIC_SDA_PORT, SOFT_IIC_SDA_PIN) GPIO_PIN_SET) void I2C_Start(void); void I2C_Stop(void); int8_t I2C_SendACK(void); int8_t I2C_SendNACK(void); int8_t I2C_WaitACK(void); int8_t I2C_WaitNACK(void); void I2C_WriteByte(uint8_t byte); uint8_t I2C_ReadByte(void); #endif #endif /*--------------------------------------------------bsp_e2prom.c ----------------------------------------------------------*/ #include bsp_e2prom.h #ifndef USE_SOFT_I2C //如果没有定义 USE_SOFT_I2C,使用硬件I2C代码 #else //如果定义了 USE_SOFT_I2C,使用软件I2C代码 void I2C_Start(void) { SCL(1); // 1. SCL拉高准备产生起始条件 SDA(1); // 2. 确保SDA初始为高 udelay(4); // 3. 保持稳定 SDA(0); // 4. SCL高时SDA下降沿 → 起始条件 udelay(4); // 5. 保持起始条件 SCL(0); // 6. 拉低SCL准备发送数据位 } void I2C_Stop(void) { SDA(0); udelay(1); SCL(1); udelay(4); SDA(1); udelay(4); } int8_t I2C_SendACK(void) { SDA(0); // 拉低SDA ACK udelay(1); // 数据建立时间 SCL(1); // 拉高SCL从机在第9个时钟采样 udelay(4); // SCL高电平保持时间 SCL(0); // 拉低SCL准备下一帧 udelay(1); // SCL低电平保持时间 SDA(1); // 释放SDA总线 return 0; // 成功 } int8_t I2C_SendNACK(void) { SDA(1); // 拉高SDA NACK udelay(1); // 数据建立时间 SCL(1); // 拉高SCL从机在第9个时钟采样 udelay(4); // SCL高电平保持时间 SCL(0); // 拉低SCL准备下一帧 udelay(1); // SCL低电平保持时间 // 释放SDA总线,前面已是高电平 return 0; // 成功 } int8_t I2C_WaitACK(void) { uint32_t timeoutSOFT_WAIT_ACK_TIME; SDA(1); udelay(1); SCL(1); udelay(1); while(SDA_READ()!0) { if(--timeout0) { SCL(0); return -1; } udelay(1); } SCL(0); udelay(1); return 0; } int8_t I2C_WaitNACK(void) { uint32_t timeoutSOFT_WAIT_ACK_TIME; SDA(1); udelay(1); SCL(1); udelay(1); while(SDA_READ()0) { if(--timeout0) { SCL(0); return -1; } udelay(1); } SCL(0); udelay(1); return 0; } void I2C_WriteByte(uint8_t byte) { for(uint8_t i0;i8;i) { if(byte 0x80) { SDA(1); } else { SDA(0); } byte 1; SCL(1); udelay(4); SCL(0); udelay(4); } SDA(1); // 释放总线准备接收ACK } uint8_t I2C_ReadByte(void) { uint8_t byte 0x00; //接收字节 SDA(1); //释放SDA for(uint8_t i0;i8;i) { byte 1; //为新数据腾出一位 SCL(1); //SCL高电平读取数据 udelay(4); //等待数据稳定 if(SDA_READ()) { byte | 0x01; } udelay(4); SCL(0); udelay(1); } return byte; } #endif“driver_e2prom.c/.h”/*--------------------------------------------------driver_e2prom.h ----------------------------------------------------------*/ #ifndef __DRIVER_E2PROM_H__ #define __DRIVER_E2PROM_H__ #include main.h #include bsp_e2prom.h /*-------------------------AT24C04-----------------------*/ #define E2PROM_ADDR 0xa0 #define E2PROM_PAGE 16 int8_t Driver_E2PROM_Write_Page(uint8_t write_addr,uint8_t *p_data,uint8_t len); int8_t Driver_E2PROM_Write(uint8_t write_addr,uint8_t *data,uint8_t len); int8_t Driver_E2PROM_Read(uint8_t write_addr,uint8_t *data,uint8_t len); #endif /*--------------------------------------------------driver_e2prom.c ----------------------------------------------------------*/ #include driver_e2prom.h #ifndef USE_SOFT_I2C #else /** * brief 页内指定地址写多字节最多8个 * param write_addr:要写入EEPROM的地址 * param p_data:要写入数据的地址 * param len:要写入数据的长度 * return 暂无 */ int8_t Driver_E2PROM_Write_Page(uint8_t write_addr,uint8_t *p_data,uint8_t len) { I2C_Start(); I2C_WriteByte(E2PROM_ADDR); I2C_WaitACK(); I2C_WriteByte(write_addr); I2C_WaitACK(); for(int i0;i len;i) { I2C_WriteByte(p_data[i]); I2C_WaitACK(); } I2C_Stop(); mdelay(5); } /** * brief 指定地址写指定字节支持跨页写入 * param write_addr:要写入EEPROM的地址 * param p_data:要写入数据的地址 * param len:要写入数据的长度 * return 暂无 */ int8_t Driver_E2PROM_Write(uint8_t write_addr,uint8_t *p_data,uint8_t len) { uint8_t offset write_addr % E2PROM_PAGE; // 页内偏移 uint8_t max_len E2PROM_PAGE - offset; // 当前页可写字节数 if (len max_len) { // 单页写入 Driver_E2PROM_Write_Page(write_addr, p_data, len); } else { // 跨页写入 Driver_E2PROM_Write_Page(write_addr, p_data, max_len); Driver_E2PROM_Write_Page(write_addr max_len, p_data max_len, len - max_len); } } /** * brief 指定地址读多字节 * param write_addr:要读取的起始EEPROM地址 * param p_data:读取数据的缓冲区地址 * param len:要读取数据的长度 * return 暂无 */ int8_t Driver_E2PROM_Read(uint8_t write_addr,uint8_t *data,uint8_t len) { I2C_Start(); I2C_WriteByte(E2PROM_ADDR); I2C_WaitACK(); I2C_WriteByte(write_addr); I2C_WaitACK(); I2C_Start(); I2C_WriteByte(E2PROM_ADDR|0x01); I2C_WaitACK(); for(int i0;i len;i) { data[i]I2C_ReadByte(); if(ilen-1) { I2C_SendNACK(); } else { I2C_SendACK(); } } I2C_Stop(); } /** * brief 检测AT24C02是否存在 * return 0:存在, -1:不存在 */ int8_t Driver_AT24C02_Check(void) { I2C_Start(); I2C_WriteByte(E2PROM_ADDR); int8_t ret I2C_WaitACK(); I2C_Stop(); return ret; } #endif工具层的延时函数/*--------------------------------------------------utility_delay.h ----------------------------------------------------------*/ #ifndef __UTILITY_DELAY_H__ #define __UTILITY_DELAY_H__ #include stm32f1xx_hal.h void udelay(uint32_t us); void mdelay(uint32_t ms); uint64_t system_get_ns(void); #endif /*--------------------------------------------------utility_delay.c ----------------------------------------------------------*/ #include utility_delay.h void udelay(uint32_t us) { uint32_t t_old0; uint32_t t_now0; uint32_t t_cnt0; uint32_t load0; if (us 0) return; t_oldSysTick-VAL; loadSysTick-LOAD; /*LOAD1 ge clock 1ms n us n*(LOAD1)/1000 */ uint64_t ticks(uint64_t)us*(load1)/1000; // 至少需要 1 个 tick避免死循环 if (ticks 0) { ticks 1; } while(1) { t_nowSysTick-VAL; //book has tell why if(t_now!t_old) { if(t_nowt_old) { t_cntt_old-t_now; } else { t_cntt_old(load1)-t_now; } t_oldt_now; if(t_cntticks) { break; } } } } void mdelay(uint32_t ms) { while(ms--) { udelay(1000); } } uint64_t system_get_ns(void) { // 1. 获取毫秒数并转换为纳秒 uint64_t ns (uint64_t)HAL_GetTick() * 1000000ULL; // 2. 获取SysTick当前值递减计数器 uint32_t t_old SysTick-VAL; uint32_t load SysTick-LOAD; // 3. 计算当前周期内已过去的时间纳秒 // 注意SysTick是递减计数器从LOAD减到0 // 已过去的周期数 LOAD - t_old uint64_t elapsed_ns (uint64_t)(load - t_old) * 1000000ULL / (load 1); // 4. 总纳秒 已完成的毫秒对应的纳秒 当前周期内过去的纳秒 return ns elapsed_ns; }APP层 main.c (部分)static char g_str[20]; void main(void) { #define MAX_CH 20 uint8_t test[MAX_CH]; for(int i0;iMAX_CH;i) { test[i] 0i; } for(uint8_t i0;iMAX_CH;i) { sprintf(g_str[i],%c,test[i]); } LCD_DisplayStringLine(1, g_str); Driver_E2PROM_Write(0,test,MAX_CH); mdelay(10); uint8_t test2[MAX_CH]; for(int i0;iMAX_CH;i) { test2[i] 1i; } Driver_E2PROM_Read(0,test2,MAX_CH); for(uint8_t i0;iMAX_CH;i) { sprintf(g_str[i],%c,test2[i]); } LCD_DisplayStringLine(2, g_str); while(1) { } }