STM32(十二)通过I2C总线向EEPROM(AT24C02 )读写数据的过程
一、概述
(1)背景
- I2C(IIC,Inter-Integrated Circuit)总线是由Philips公司开发的一种简单、双向二线制同步串行总线。
- 它只需要两根线即可在连接于总线上的器件之间传送信息。
- 主器件用于启动总线传送数据,并产生时钟以开放传送的器件,此时任何被寻址的器件均被认为是从器件。
- I2C总线简化了硬件电路PCB布线,降低了系统成本,提高了系统可靠性。因为I2C芯片(如mpu6050、ft5x06等)除了这两根线和少量中断线,与系统再没有连接的线,用户常用IC可以很容易形成标准化和模块化,便于重复利用。
(2)传输方向
- 在总线上主和从、发和收的关系不是恒定的,而取决于此时数据传送方向。
- 如果主机要发送数据给从器件,则主机首先寻址从器件,然后主动发送数据至从器件,最后由主机终止数据传送。
- 如果主机要接收从器件的数据,首先由主器件寻址从器件.然后主机接收从器件发送的数据,最后由主机终止接收过程。在这种情况下,主机负责产生定时时钟和终止数据传送。
(3)速度
- 连接到相同总线上的IC数量只受总线最大电容的限制,串行的8位双向数据传输位速率在标准模式下可达100Kbit/s,快速模式下可达400Kbit/s,高速模式下可达3.4Mbit/s。
- 总线具有极低的电流消耗.抗高噪声干扰,增加总线驱动器可以使总线电容扩大10倍,传输距离达到15m;兼容不同电压等级的器件,工作温度范围宽。
(4)地址
I2C总线上的每一个设备都可以作为主设备或者从设备,而且每一个设备都会对应一个唯一的地址(地址通过物理接地或者拉高,可以从I2C器件的数据手册得知,如AT24C02芯片,7位地址依次1010xxx, 最低三位可配,如果全部物理接地,则该设备地址为0x50),主从设备之间就通过这个地址来确定与哪个器件进行通信,在通常的应用中,我们把STM32作为主设备,把挂接在总线上的其他设备都作为从设备。
二、AT240C02 EEPROM介绍
(1)特点
- 宽范围的工作电压1.8V~5.5V低电压技术
- 1mA典型工作电流- 1uA典型待机电流·存储器组织结构
- 24C02,256 X8(2K bits)- 24C04,512×8(4K bits)- 24C08,1024 × 8 (8K bits)-24C16,2048 ×8(16K bits)-24C32,4096 X8(32K bits)- 24C64,8192 × 8(64K bits)
- 2线串行接口,完全兼容l2C总线
- I2C时钟频率为1 MHz (5V),400 kHz (1.8V,2.5V,2.7V)施密特触发输入噪声抑制
- 硬件数据写保护
- 内部写周期(最大5 ms)可按字节写
- 页写:8字节页(24C02),16字节页(24C04/08/16),32字节页(24C32/64)可按字节,随机和序列读
- 自动递增地址
- 高可靠性擦写寿命:100万次-数据保持时间:100年
有规格书可知,EEPROM的读写速率是100KHZ.
(2)引脚说明
AT24C02芯片,7位地址依次1010xxx, 最低三位(A0~A2)可配,由原理图可知三个脚物理接地,地址为1010000,即该设备地址为0x50)。
(3)起始和停止时序
数据和时钟线都为高则称总线处在空闲状态。当SCL为高电平时SDA的下降沿(高到低叫做起始条件(START,简写为S),SDA的上升沿(低到高)则叫做停止条件(STOP,简写为P)。参见图5。
(4)数据有效性
SCL高电平时SDA数据有效,低电平时SDA数据交换,数据变换为高电平或者低电平。
(5)应答信号:
三、读写操作
1、写操作
(1).字节写
写操作要求在接收器件地址和ACK应答后,接收8位的字地址。接收到这个地址后EEPROM应答"0",然后是一个8位数据。在接收8位数据后EEPROM应答"0",接着必须由主器件发送停止条件来终止写序列。
此时EEPROM进入内部写周期twR,数据写入非易失性存储器中,在此期间所有输入都无效。直到写周期完成,EEPROM才会有应答(见图9)。
(2)页写
- 24C02器件按8字节/页执行页写,24C04/08/16器件按16字节/页执行页写,24C32/64器件按32字节/页执行页写。
- 页写初始化与字节写相同,只是主器件不会在第一个数据后发送停止条件,而是在EEPROM的ACK以后,接着发送7个(24C02数据。EEPROM收到每个数据后都应答“0”。最后仍需由主器件发送停止条件,终止写序列(见图10)。接收到每个数据后,字地址的低3位(24C02)内部自动加1,高位地址位不变,维持在当前页内。当内部产生的字地址达到该页边界地址时,随后的数据将写入该页的页首。如果超过8个(24C02)数据传送给了EEPROM,字地址将回转到该页的首字节,先前的字节将会被覆盖。
MCU向AT24CT02写数据时,MCU为主机,EEPROM为从机。由上图可知:
- 主机通过SDA先发一个起始信号。
- 发设备地址寻找设备,并设置是写入还是读取。
- 从机回一个Ack应答信号。
- 主机发送要写入的EEPROM片内地址。
- 从机回Ack应答信号。
- 主机写数据到从机,每写一byte,从机回发一个ack应答。
- 主机发送停止信号,结束数据写入。
(3)代码分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | #include "iic.h" /*软件模拟IIC写数据到EEPROM*/ GPIO_InitTypeDef GPIO_InitStructure; #define SDA_OUT PBout(9) #define SDA_IN PBin(9) #define SCL_OUT PBout(8) //初始化SDL、SCL GPIO void Iic_AT24C02_Init( void ) { //1.初始化时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); //2.初始化硬件 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_8; //PB8 PB9 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; //输出模式 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Fast_Speed; //速度 快速 25MHz GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉 GPIO_Init(GPIOB,&GPIO_InitStructure); //空闲状态高电平 SDA_OUT = 1; SCL_OUT = 1; } //设置引脚模式 void sda_pin_mode(GPIOMode_TypeDef mode) { GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PB9 GPIO_InitStructure.GPIO_Mode = mode; //模式 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Fast_Speed; //速度 快速 25MHz GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉 GPIO_Init(GPIOB,&GPIO_InitStructure); } //IIC起始信号 void iic_start( void ) { //保证SDA引脚为输出模式 sda_pin_mode(GPIO_Mode_OUT); //保证开始是一个高电平 SDA_OUT = 1; SCL_OUT = 1; delay_us(5); //AT24C02的读写速度为100kHZ,一个周期为10us. SDA_OUT = 0; //时钟线为高电平期间,数据线由高到低 delay_us(5); SCL_OUT = 0; //高速总线上的从机,有通信的设备 delay_us(5); } //发送数据 void iic_send_byte(uint8_t d) // 10101110 { int32_t i; //保证SDA引脚为输出模式 sda_pin_mode(GPIO_Mode_OUT); SDA_OUT = 0; SCL_OUT = 0; delay_us(5); for (i=7;i>=0;i--) { if (d & (0x1<<i)) SDA_OUT = 1; else SDA_OUT = 0; delay_us(5); SCL_OUT = 1; delay_us(5); //当前数据是可靠的,告诉从机可以读取数据 SCL_OUT = 0; delay_us(5); //当前数据是不可靠的,正在准备 } } //应答信号 uint8_t iic_wait_ack( void ) { uint8_t ack = 0; //保证SDA引脚为输入模式 sda_pin_mode(GPIO_Mode_IN); SCL_OUT = 1; delay_us(5); //当前数据是可靠的,主机可以访问 if (SDA_IN == 0) ack = 0; //有应答 要 else ack = 1; //无应答 不要了 SCL_OUT = 0; delay_us(5); return ack; } void iic_stop( void ) { //保证SDA引脚为输出模式 sda_pin_mode(GPIO_Mode_OUT); SDA_OUT = 0; SCL_OUT = 1; delay_us(5); SDA_OUT = 1; //时钟线为高电平期间,数据线由低到高 delay_us(5); } int32_t at24c02_write(uint8_t word_addr,uint8_t *buf,uint8_t len) { uint8_t ack = 0; uint8_t *p = buf; //发送起始信号 iic_start(); //设备寻址,设备写访问地址为 10100000 = 0xA0 iic_send_byte(0xA0); //等待应答 ack = iic_wait_ack(); if (ack) { printf ( "devices address failed!\r\n" ); return -1; } //告诉从机我要访问的数据存储地址 iic_send_byte(word_addr); //等待应答 ack = iic_wait_ack(); if (ack) { printf ( "word address failed!\r\n" ); return -2; } //连续写数据 while (len--) { //写入数据 iic_send_byte(*p); //等待应答 ack = iic_wait_ack(); if (ack) { printf ( "data failed!\r\n" ); return -3; } p++; } //发送停止信号 iic_stop(); printf ( "write success!\r\n" ); return 0; } |
2、读操作
读操作与写操作初始化相同,只是器件地址中的读/写选择位应为"1"。有三种不同的读操作方式:当前地址读,随机读和顺序读。
(1)当前地址读
- 内部地址计数器保存着上次访问时最后一个地址加1的值。只要芯片有电,该地址就一直保存。当读到最后页的最后字节,地址会回转到0;当写到某页尾的最后一个字节,地址会回转到该页的首字节。
- 接收器件地址(读/写选择位为"1")、EEPROM应答ACK后,当前地址的数据就随时钟送出。主器件无需应答"O",但需发送停止条件(见图12)。
2.随机读
随机读需先写一个目标字地址,一旦EEPROM接收器件地址和字地址并应答了ACK,主器件就产生一个重复的起始条件。
然后,主器件发送器件地址(读/写选择位为"1"),EEPROM应答ACK,并随时钟送出数据。主器件无需应答"O",但需发送停止条件(见图13)。
- 主机通过SDA先发一个起始信号。
- 发设备地址寻找设备,并设置是写入(因为要写入读取数据的地址)。
- 写入要读取的地址
- 从机回一个Ack应答信号。
- 主机再发一个起始信号开始读
- 主机发送要写入的EEPROM片内地址(读写位设置为读)。
- 读数据。
- 主机发送停止信号,结束数据读取。
3.顺序读
顺序读可以通过“当前地址读"或“随机读"启动。主器件接收到一个数据后,应答ACK。只要EEPROM接收到ACK,将自动增加字地址并继续随时钟发送后面的数据。若达到存储器地址末尾,地址自动回转到0,仍可继续顺序读取数据。
主器件不应答"0",而发送停止条件,即可结束顺序读操作(见图14)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | #include "iic.h" GPIO_InitTypeDef GPIO_InitStructure; void Iic_AT24C02_Init( void ) { //1.初始化时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); //2.初始化硬件 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_8; //PB8 PB9 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; //输出模式 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Fast_Speed; //速度 快速 25MHz GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉 GPIO_Init(GPIOB,&GPIO_InitStructure); //空闲状态高电平 SDA_OUT = 1; SCL_OUT = 1; } void sda_pin_mode(GPIOMode_TypeDef mode) { GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PB9 GPIO_InitStructure.GPIO_Mode = mode; //模式 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Fast_Speed; //速度 快速 25MHz GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉 GPIO_Init(GPIOB,&GPIO_InitStructure); } void iic_start( void ) { //保证SDA引脚为输出模式 sda_pin_mode(GPIO_Mode_OUT); //保证开始是一个高电平 SDA_OUT = 1; SCL_OUT = 1; delay_us(5); SDA_OUT = 0; //时钟线为高电平期间,数据线由高到低 delay_us(5); SCL_OUT = 0; //高速总线上的从机,有通信的设备 delay_us(5); } void iic_send_byte(uint8_t d) // 10101110 { int32_t i; //保证SDA引脚为输出模式 sda_pin_mode(GPIO_Mode_OUT); SDA_OUT = 0; SCL_OUT = 0; delay_us(5); for (i=7;i>=0;i--) { if (d & (0x1<<i)) SDA_OUT = 1; else SDA_OUT = 0; delay_us(5); SCL_OUT = 1; delay_us(5); //当前数据是可靠的,告诉从机可以读取数据 SCL_OUT = 0; delay_us(5); //当前数据是不可靠的,正在准备 } } uint8_t iic_recv_byte( void ) { int8_t i; uint8_t data; //保证SDA引脚为输入模式 sda_pin_mode(GPIO_Mode_IN); //当前数据不可靠,切换数据 SCL_OUT = 0; delay_us(5); for (i=7;i>=0;i--) //MSB 10101111 { SCL_OUT = 1; delay_us(5); //当前数据可靠,读把 if (SDA_IN == 1) { data |= (0x1<<i); } else { data &= ~(0x1<<i); } SCL_OUT = 0; delay_us(5); //当前数据不可靠,切换数据 } return data; } void iic_ack(uint8_t ack) { //保证SDA引脚为输出模式 sda_pin_mode(GPIO_Mode_OUT); SDA_OUT = 0; SCL_OUT = 0; delay_us(5); //当前数据不可靠 //发送高/低电平 SDA_OUT = ack; delay_us(5); //准备数据 SCL_OUT = 1; delay_us(5); //当前数据可靠,然后从机可以访问 SCL_OUT = 0; delay_us(5); //当前数据bu可靠,切换数据 } uint8_t iic_wait_ack( void ) { uint8_t ack = 0; //保证SDA引脚为输入模式 sda_pin_mode(GPIO_Mode_IN); SCL_OUT = 1; delay_us(5); //当前数据是可靠的,主机可以访问 if (SDA_IN == 0) ack = 0; //有应答 要 else ack = 1; //无应答 不要了 SCL_OUT = 0; delay_us(5); return ack; } void iic_stop( void ) { //保证SDA引脚为输出模式 sda_pin_mode(GPIO_Mode_OUT); SDA_OUT = 0; SCL_OUT = 1; delay_us(5); SDA_OUT = 1; //时钟线为高电平期间,数据线由低到高 delay_us(5); } int32_t at24c02_write(uint8_t word_addr,uint8_t *buf,uint8_t len) { uint8_t ack = 0; uint8_t *p = buf; //发送起始信号 iic_start(); //设备寻址,设备写访问地址为 10100000 = 0xA0 iic_send_byte(0xA0); //等待应答 ack = iic_wait_ack(); if (ack) { printf ( "devices address failed!\r\n" ); return -1; } //告诉从机我要访问的数据存储地址 iic_send_byte(word_addr); //等待应答 ack = iic_wait_ack(); if (ack) { printf ( "word address failed!\r\n" ); return -2; } //连续写数据 while (len--) { //写入数据 iic_send_byte(*p); //等待应答 ack = iic_wait_ack(); if (ack) { printf ( "data failed!\r\n" ); return -3; } p++; } //发送停止信号 iic_stop(); printf ( "write success!\r\n" ); return 0; } int32_t at24c02_read(uint8_t word_addr,uint8_t *buf,uint8_t len) { uint8_t ack = 0; uint8_t *p = buf; //发送开始信号 iic_start(); //设备寻址,设备写访问地址 0xA0 iic_send_byte(0xA0); //等待应答 ack = iic_wait_ack(); if (ack) //1 无应答 0 应答 { printf ( "devices address failed!\r\n" ); return -1; } //告诉从机我要访问的数据存储地址 iic_send_byte(word_addr); //等待应答 ack = iic_wait_ack(); if (ack) //1 无应答 0 应答 { printf ( "word address failed!\r\n" ); return -2; } //再次发送开始信号 iic_start(); //设备寻址,设备读访问地址 0xA1 iic_send_byte(0xA1); //等待应答 ack = iic_wait_ack(); if (ack) //1 无应答 0 应答 { printf ( "devices address failed!\r\n" ); return -3; } //连续接收数据 len = len - 1; while (len--) { *p++ = iic_recv_byte(); //发送应答 iic_ack(0); } //接收最后一个字节 *p = iic_recv_byte(); //发送不应答 iic_ack(1); //发送停止信号 iic_stop(); printf ( "read success!\r\n" ); return 0; } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)