1. IIC是什么
IIC(I2C:Inter-Integrated Circuit 集成电路总线)最初由飞利浦(Philips)半导体(后并入NXP)在1982年提出,主要目的是提供一种简单的、成本低廉的串行总线,用于MCU和电视机原件进行通信。它在最初是由一个Master(可以多Master)和可以多达127个Slave在由两根线(SDA,SCL)组成的总线上进行通讯。这样MCU就可以不用再单独和各个设备通讯,而仅仅用两根线就可以和所有外围设备愉快的通信了。这两根线分别是数据线(SDA)和时钟线(SCL)。如此这般,大大节约了芯片引脚数目,方便主板布线,节省了整体成本。良好的设计,慢慢让它得到了广泛应用
IIC也是属于通信的一种,并且也是串行通信(按 bit 收发数据),带数据应答,支持总线挂载多设备(一主多从,多主多从)
半双工:任意时刻只有一个设备可以发送数据,每个设备都有一个唯一的 I2C 地址,用来标识 I2C 设备同步:有一个时钟信号
好处:如果传输设备产生中断,传输双方都能定格在暂停的时刻,可以过一段时间再来继续传输,不会对传输造成影响,对时间要求不严格,可以极大的降低单片机对硬件电路的依赖,即使没有硬件电路的支持,也可以很方便的用软件手动翻转电平来实现通信
异步的好处:省一根时钟线,节省资源 缺点:对时间要求严格,对硬件电路的依赖比较严重
总线:可以连接多个设备
IIC属于串行总线通信:
只有两根线
一根数据线 SDA:Serial DAta 串行数据线
一根时钟线 SCL:Serial CLock 串行时钟线
SDA:串行数据线
数据传输按 bit 位,1 bit 接 1 bit 的在 SDA 线上串行传输
先传送最高 bit(MSB)
SCL:串行时钟线
传递时钟信号
为什么需要时钟线? ===> 用来同步信号用的
同步:约定好发送数据只能在时钟线低电平时,接收(采样)数据只能在时钟线高电平时
所以,IIC是半双工通信:因为只有一根数据线SDA,在发送数据的时候就不能接收数据,否则收的数据就是自己发出去的
IIC通信设备都会挂载在 SDA 和 SCL 总线上,或者说在 SDA 和 SCL 总线上会挂载很多 IIC 设置。那么任意时刻,只能有一个设备向总线上发送数据,但是接收没有限制,都可以收
为了让数据精准到达(而不是广播的形式发送),我们给 IIC 总线上的每一个设备都给一个唯一的地址,这个地址就是设备地址,用来区分不同的 IIC 设备
2. IIC物理特点
- 它是一个支持多设备的总线。"总线" 指多个设备共用的信号线。在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机
- 一个 I2C 总线只使用两条总线线路,一条双向串行数据线(SDA),一条串行时钟线 (SCL),数据线即用来表示数据,时钟线用于数据收发同步
- 每个连接到总线的设备都有一个独立的地址(7 / 10bit),主机可以利用这个地址进行不同设备之间的访问
- 总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平
- 多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线
- 具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式
- 连接到相同总线的 I2C 数量受到总线的最大电容 400pF 限制
总线仲裁:
I2C总线上可能在某一时刻有多个主控设备要同时向总线发送START信号,这种情况叫做总线竞争,I2C总线具有多主控能力,可以对发生在SDA线上的总线竞争进行仲裁,决定谁的信号有效,其他的设备就立刻处于一个“监听模式”
其仲裁原则是这样的:节点在发送1位数据后,比较总线上所呈现的数据与自己发送的是否一致。是,继续发送;否则,退出竞争。SDA线的仲裁可以保证 I2C 总线系统在多个主节点同时企图控制总线时通信正常进行并且数据不丢失。总线系统通过仲裁只允许一个主节点可以继续占据总线 (建立在线与逻辑上实现的,当总线上只要有一个设备输出低电平,整条总线便处于低电平状态)
3. IIC协议(时序图)
IIC 数据通信的大概流程(时序)图:
- 总线空闲(空闲指没有数据通信是总线的状态) 我们约定:IIC 总线在空闲(ldle)时,SDA 和 SCL 都处于高电平(通过在总线 上接一个上拉电阻来实现) 接下来如果有一个设备需要给另一个设备发送数据的话, 就需要一个起始信号
- 起始信号:用来表示我要往总线上发送数据啦! SCL 时钟线保持高电平 SDA 数据线从高到低的跳变
有没有可能两个或两个以上设备同时发送起始信号呢? 有可能,所以需要总线仲裁:决定谁的信号有效 如果有两个或两个以上的设备同时发送START信号,这个时候,就需要 "总线仲裁",它会决定谁的 START 信号是有效的,其他的设备就立刻处于一个 "监听" 模式 比如:在发送起始信号前,判断 IIC 总线是否空闲 怎么做: time_out = SCL_T;// 超时时间为一个 SCL 周期 while (SCL == 1 && SDA == 1 && time_out--); 解析:如果一个 SCL 周期内,SCL 和 SDA 都是高电平,那么说明 就没有人往总线上发送数据例子:模拟 IIC 的起始信号(在没有I2C控制器的情况下,如C51): /* 空闲 */ SCL = 1; SDA = 1; delay(); /* 起始信号 */ SDA = 0; delay();- 发送数据:user data,device data 这个数据包含用户真正发送的数据,也包括设备地址(指定通信方) 因为总线上有多个设备,其中一个发起一个起始信号,表示它要跟总线上的某个设备或多个设备通信 它到底跟谁通信呢?如果不指定,总线上所有设备都可以收到数据 所以 IIC 协议规定,每个 IIC 总线上的设备都必须有一个 IIC 设备地址(7bits / 10bits),并且,同一个 IIC 总线上的设备地址必须不一样 IIC中数据(包括设备地址)的发送都是按 8bits 进行发送 设备的地址 = 7bits + R / W#(读写位,占最低 1bit ---> bit0) bit0:0 W 表示"我"要给指定地址的设备写入数据 bit0:1 R 表示"我"要从指定地址的设备里读取数据 例如:设备 B 的地址是 101 0001,CPU 要发送数据 0x55 给设备A CPU:START 1010 0001 0101 0101 1010 001:地址 0:写 (发送) 0101 0101:发送的数据 发送完一个字节(8bits)数据后,对方(接收方)必须要返回一个 ACK(应答位) ACK:在 SDA 数据线上的第 9 个时钟周期,接收方给 SDA 一个低电平 但是这里存在一个问题,就是如果数据的最后一个 bit 本身就是一个低电平,那么 SDA 线此时的电平状态就是 0,这个时候,不管接收方应答还是不应答,发送方可能都会认为对方应答啦。怎么解决: 发送方在发送完 8bits 数据后,一般都会释放 SDA 数据线(SDA = 1) 在第 9 个时钟周期时,接收方就会给 SDA 一个低电平表示应答(表示我已经收到了) 例如: CPU(发送方):STATR 1010 0010 0101 0101 A(接收方): ACK ACK
总结数据的发送规则:
数据发送其实就是根据要发送的数据的 bit 位 的情况给 SDA 线低电平或高电平 先发送 MSB(最高位)
发送数据时,更改数据线的要求如下:
IIC 协议定: 在 SCL 时钟线低跳变的时候,可以改变 SDA 数据线电平 所以发送是下降沿触发,每个下降沿可以发送 1bits 数据 在 SCL 时钟线高电平的时候,SDA 数据线保持稳定 所以接收是上升沿触发,每个上升沿到来,就会去 SDA 上采集 1bits 数据
- 停止信号:STOP SCL 保持高电平 SDA 从低电平到高电平跳变
所以一帧 IIC 数据如下:
发送:START + data(7bit addr + 1bit 0W) + data(8bit data) + ... + STOP
1bit ACK 1bit ACK
接收:START + data(7bit addr + 1bit 1R) + data(8bit data) + ... + STOP
1bit ACK 1bit ACK
有另外一个问题:
SDA 线一般是谁要发送数据出去就由谁来控制,那么 SCL 时钟线应该由谁来控制?
谁控制都可以,只要不同时控制,但是很多设备,不具备控制时钟的能力
因为它可能没有时钟单元(没有时钟输出功能)。所以,在STM32中一般是由CPU作为时钟输出(控制者)
所以通过谁控制 SCL 线我们为 IIC 通信设备区分不同的角色:
IIC 主设备:Master
在一次 IIC 通讯过程中,产生 IIC 时钟输出的设备,它控制 IIC 总线的传输速率
IIC 从设备:Slave
在一次 IIC 通讯过程中,被动接收 IIC 时钟的设备
细分的话就会有:Master-send 主发 Master-Receive 主收
就是说时钟提供者既可以收也可以发
Slave-Send 从发 Slave-Receive 从发
IIC 总线上的时钟频率一般在 几十K hz — 400K hz,频率越低通信速度越慢,但是越稳定,"就低不就高"
IIC 时序图如下:
![]()
4. IIC模拟时序
在一些芯片上(如:C51)它没有 IIC 总线,也没有 IIC 控制器,那么它能不能和一个 IIC 接口的模块进行通信呢?
当然可以,我们只需要用两个 GPIO 口来模拟 SDA 和 SCL 即可
/* IIC_Send_Start:发送 IIC 起始信号 */ void IIC_Send_Start(void) { /* 空闲 */ SCL = 1; SDA = 1; delay(IIC_T); // IIC_T:IIC时钟信号的周期,delay(IIC_T):延时一个时钟周期 /* 起始 */ SDA = 0; delay(IIC_T); } /* IIC_Send_Stop:发送 IIC 停止信号 SCL 保持高电平 SDA 从低电平到高电平跳变 */ void IIC_Send_Stop(void) { SCL = 1; SDA = 0; delay(IIC_T); SDA = 1; delay(IIC_T); } /* "Master-Send":主发 IIC_Send_Byte: 将一个字节的数据发送出去 @ch:要发送的数据,1个字节 @返回值: 发送并成功返回1(表示接收方收到回了一个ACK) 失败返回0(表示接收方没有收到,没有返回ACK) */ int IIC_Send_Byte(unsigned char ch) { /* MSB(最高位先发),并且是在SCL下降沿时发送1bit */ int i; for (i = 7; i >= 0; i--) { // 8个SCL时钟周期发送8bits SCL = 0; // 周期开始,下降沿发送 SDA = (ch >> i) & 0x01; // 发送一个bit过去 delay(IIC_T / 2); // 等待约半个时钟周期 SCL = 1; // 对方在上升沿采集 delay(IIC_T / 2); // 延时等待一会让对方有时间接收 } /* 发送方在发送完8bits数据后,一般都会释放SDA数据线 */ /* 同时第9个时钟周期,等待接受方回应一个ACK低电平 */ SCL = 0; // 第9个周期开始 SDA = 1; // 释放SDA数据线 delay(IIC_T / 2); // 等待接收方应答,接收方回复ack(SDA -> 0) SCL = 1; if (SDA) { return 0; // 代表无人应答,发送失败 } else { return 1; // 代表有人应答,发送成功 } delay(IIC_T / 2); } /* "Master-Receive":主收 IIC_Recv_Byte:从IIC总线上接收一个字节 @返回值:将接收到的字节返回 */ unsigned char IIC_Recv_Byte(void) { // 接收:先接收到的是最高bit,陆续收到 8个bit,在第九个周期发送ACK unsigned char ch = 0; int i = 0; for (i = 7; i >= 0; i--) { SCL = 0; // 给半个周期的低电平,让对方发送数据 delay(IIC_T / 2); SCL = 1; // 拉高准备去采集数据 if (SDA) { ch |= 1 << i; // 如果接收到的是高电平,则将对应bit位置为1 } delay(IIC_T / 2); } /* 接收到8个bit后,应该要回复应答 */ SCL = 0; delay(t); // 延时一段非常短的时间让对方释放数据线 SDA = 0; // 回复应答信号ACK delay(IIC_T / 2 - t); // 以上操作总共花掉半个时钟周期 SCL = 1; // 让对方采集应答信号 delay(IIC_T / 2); return ch; } /* IIC_Write_Data:向指定的IIC设备写入数据 @addr:7bit的目标IIC设备的地址 @str:要发送的数据字符串 @len:要发送的数据字符串的长度 @返回值:发送成功返回1,失败返回0 */ int IIC_Write_Data(unsigned char addr, char *str, int len) { /* 发送起始信号 */ IIC_Send_Start(); /* 发送设备地址 */ // bit0->0表示写入数据 bit1-bit7:IIC设备地址 int ret = IIC_Send_Byte((addr << 1) | 0); if (ret == 0) { // 代表没有应答 IIC_Send_Stop(); // 发送停止信号 return 0; } /* 发送数据 */ int i; for (i = 0; i < len; i++) { ret = IIC_Send_Byte(str[i]); if (ret == 0) { // 代表没有应答 IIC_Send_Stop(); // 发送停止信号 return 0; } } /* 发送停止信号 */ IIC_Send_Stop(); return 1; }
5. STM32F4xx IIC控制器
STM32F4xx 有三个 IIC 控制器,有三条 IIC 总线
IIC 控制器是一个 IIC 的设备,它负责产生 IIC 的时序以及协议逻辑
在STM32F4xx中,CPU 与 IIC 是通过系统总线通信的
如果没有 IIC 控制器,那么 CPU 只能通过 GPIO 口来模拟,即 IIC 的时序、协议逻辑等等都需要软件代码去模拟
比如:8051 单片机就没有 IIC 控制器,所以,C51 的 IIC 都是通过软件代码模拟实现的
IIC 控制器原理,大概如图:CR:Control Register 控制寄存器
SR:Status Register 状态寄存器
STM32F4xx 的 IIC 控制器既可以作为主模式又可以作为从模式:作为主模式的主发时序图(7bit):
作为主模式的主收时序图(7bit):
6. STM32F4xx I2C固件库函数
IIC 控制器 的 SDA 和 SCL 其实都是通过 GPIO 引脚复用功能而来的
(1) 初始化 I2C 引脚
a. 使能 GPIO 分组时钟 RCC_AHB1PeriphClockCmd(); b. SDA 和 SCL 对应的 GPIO 初始化为复用模式 GPIO_Init(); 【SDA只能配置为输出开漏】 c. 配置 GPIO 复用成什么功能 GPIO_PinAFConfig(); /* SCL和SDA线都需要具有能够输出高低电平的能力 SCL对应的GPIO口配置为开漏或者推挽都可以,因为根据原理图可知 SCL和SDA上都已经外接了上拉电阻具有输出高低电平的能力 但是SDA对应的GPIO口却只能配置为开漏模式,我们知道SDA不仅需要 具有输出高低电平的能力还要具有获取输入的能力(比如要去获取应答) 通过中文手册183页<7.3.10输出配置>可知,在GPIO配置为输出模式时, 实际上输入回路上的施密特触发器是处于开启状态的,也就意味着此时 GPIO是能够获取输入的,但如果配置为输出推挽,当要实现输入检测时, 就会受到输出电路没有关闭的影响,因为之前的输出电平是存在的,造成 输入电路和输出电路的短接等现象,所以只能配置为开漏模式,就算当CPU输 出1,由于N-MOS管处于关闭状态,IO端口的电平将完全由外部电路决定,因此 CPU可以在输入数据寄存器中读到外部电路的信号而不是自己输出的1 */(2) 初始化 I2C 控制器
a. 使能 IIC 时钟 RCC_AHB1PeriphClockCmd(); or RCC_AHB2PeriphClockCmd(); b. 初始化 IIC void I2C_Init(I2C_TypeDef *I2Cx, I2C_InitTypeDef *I2C_InitStruct); @I2Cx:指定IIC控制器编号 I2C1、I2C2、I2C3 @I2C_InitStruct:指向I2C初始化信息结构体 typedef struct { uint32_t I2C_ClockSpeed; 指定IIC总线通信时钟频率 100K ~ 400K 越低越稳定,但速度也越慢 uint16_t I2C_Mode; 指定IIC模式 I2C_Mode_I2C I2C模式 <--- 选这个 I2C_Mode_SMBusDevice 设备模式 I2C_Mode_SMBusHost 主机模式 uint16_t I2C_DutyCycle; 指定时钟线低电平和高电平的比率 I2C_DutyCycle_16_9 低电平/高电平 = 16/9 <--- 选这个 I2C_DutyCycle_2 低电平/高电平 = 2/1 uint16_t I2C_OwnAddress1; 指定I2C控制器的地址,根据I2C_AcknowledgedAddress来定 一般随便指定,但是要与其它设备不同 只有在作为从设备时用得到(作为主设备不需要) uint16_t I2C_Ack; 在收到I2C总线的数据时,是否回复ACK ===> 根据模块手册选择 I2C_Ack_Enable 回复 I2C_Ack_Disable 不回复 uint16_t I2C_AcknowledgedAddress; 指定I2C控制器自身地址长度,是7bits还是10bits的 I2C_AcknowledgedAddress_7bit 地址是7bits I2C_AcknowledgedAddress_10bit 地址是10bits } I2C_InitTypeDef;(3) 配置 I2C 控制器的其他功能
比如:中断等(暂时用不到),其中有一个函数接下来会用到: 配置I2C控制器是否产生应答位 void I2C_AcknowledgeConfig(I2C_TypeDef *I2Cx, FunctionalState NewState); @I2Cx:指定IIC控制器编号 @NewState: ENABLE:当主机收到一个字节的数据后,会自动发送一个ACK应答位 DISABLE:当主机收到一个字节的数据后,不会产生ACK应答位(4) 开启 I2C 控制器
I2C_Cmd();(5) I2C 总线读写流程
a. 发送起始信号 void I2C_GenerateSTART(I2C_TypeDef *I2Cx, FunctionalState NewState) ================================================================================ b. 获取指定事件 ErrorStatus I2C_CheckEvent(I2C_TypeDef *I2Cx, uint32_t I2C_EVENT) @I2Cx:指定I2C控制器编号 @I2C_EVENT:指定要获取的事件 /* --EV5 */ 发送起始信号后的应答事件 I2C_EVENT_MASTER_MODE_SELECT /* --EV6 */ 发送从地址的应答事件 I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED 主发模式 I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED 主收模式 /* --EV7 */ 主机可以读数据啦 I2C_EVENT_MASTER_BYTE_RECEIVED /* --EV8 */ I2C_EVENT_MASTER_BYTE_TRANSMITTING 数据正在发送中 /* --EV8_2 */ I2C_EVENT_MASTER_BYTE_TRANSMITTED 数据已经发送完成 具体要等待的事件根据时序图来找宏 @返回值: ERROR 获取的事件未发生 SUCCESS 获取的事件已经发生 -------------------------------------------------------------------------------- 比如:发送起始信号后需要等待EV5 I2C_GenerateSTART(I2C1, ENABLE); // 向I2C1发送起始信号 while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS); ================================================================================ c. 发送一个7bits的从设备地址 void I2C_Send7bitAddress(I2C_TypeDef *I2Cx, uint8_t Address, uint8_t I2C_Direction) @I2Cx:指定I2C控制器编号 @Address:7bit设备地址,【注意:在填入参数的时候需要左移一位】 @I2C_Direction:读写模式 I2C_Direction_Transmitter 发送数据模式 I2C_Direction_Receiver 接收数据模式 ================================================================================ d. 发送数据 void I2C_SendData(I2C_TypeDef *I2Cx, uint8_t Data) @I2Cx:指定I2C控制器编号 @Data:要发送的数据,1个字节 ================================================================================ e. 接收数据 uint8_t I2C_ReceiveData(I2C_TypeDef *I2Cx) @返回值:返回从I2C接收到的1个字节数据 ================================================================================ f. 产生停止信号 void I2C_GenerateSTOP(I2C_TypeDef *I2Cx, FunctionalState NewState) ================================================================================ g. 获取I2C控制器的状态标志 FlagStatus I2C_GetFlagStatus(I2C_TypeDef *I2Cx, uint32_t I2C_FLAG) @I2Cx:指定I2C控制器编号 @I2C_FLAG:指定状态标志位 I2C_FLAG_BUSY // 表示I2C总线是否忙碌 如果被设置,则表示总线忙碌,不能发送起始信号 所以在发送起始信号前,需要判断总线是否忙碌 如: /* 检测I2C总线是否忙绿 */ while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY) == SET); /* 不忙碌才能发送起始信号 */ ================================================================================ h. 清除I2C控制器的状态标志 void I2C_ClearFlag(I2C_TypeDef *I2Cx, uint32_t I2C_FLAG)
7. AT24C02
在M4开发板有一个采用 IIC 进行通信的 EEPROM 存储器芯片 AT24C02
EEPROM:是一个小容量的存储器芯片,一般只存储 几K 的数据,在实际产品应用中,一般用来存储一些其它模块的 ID,MAC,版本号 ......
所以我们可以验证一下:看是否能写入数据到AT24C02中,再读取出来
AT24C02详情请看数据手册,在中文手册中介绍了:AT24C02/AT24C04/AT24C08/AT24C16/AT24C32/AT24C64
后面那个一直变化的数字表示的是其存储容量如:AT24C02的存储容量为2K
数据手册 (<<24C02中文资料.PDF>>)中应该懂的知识点:a. 器件地址 (IIC 设备地址)
AT24C02:7bits地址
根据<图8:器件地址>可知24C02的地址为:
1 0 1 0 A2 A1 A0 R/W高四位的地址是固定的,其中A2/A1/A0是器件地址位,由外部输入信号的电平决定
通过M4原理图可知,M4上的24C02的A0/A1/A2引脚接地
IIC_SCL ----- I2C1_SCL ----- PB8 IIC_SDA ----- I2C1_SDA ----- PA9
所以M4上的24C02的设备地址为:1 0 1 0 0 0 0 R/W
为什么AT24C02不把它的 I2C 设备地址写死呢? 为什么要浪费我三个引脚呢?
写死就有可能与其他设备冲突,因为目前 I2C 设备地址都是由各个厂商来自行定的。 通过 A0/A1/A2 可以设置成不同的设备地址,IIC 中每个设备的地址都必须唯一
b. 内部存储结构 (大小多少 片内地址)
24C02一共 2K bits (2*1024/8 = 256 Bytes),分为 32 Pages,每页 8 Bytes
每页有自己的页地址(2*2*2*2*2 = 32 ---> 5bits ---> 因为一共有32页)每个字节也有字节地址(2*2*2 = 8 ---> 3bits ---> 因为一页一共有8字节)
所以24C02的存储单元地址为:8bits = 5bits_pd + 3bits_wd
word_addr(8bit) b7 b6 b5 b4 b3 b2 b1 b0页码 页内地址
c. AT24C02的读写操作AT24C02的读写操作可以分为: 写操作时序: 1. 字节写(写一个字节) 2. 页写(写一页) 应答查询: 读操作时序: 1. 当前地址读 2. 随机读 3. 顺序读写操作时序:
1. 字节写(写一个字节) // 在手册第8页
举个例子:
假设需要往 AT24C02 内部字节地址为 0x55 处写一个数据 0xAA
MCU:START 从设备地址(1010 000 0/W) 字节地址(0x55) 数据(0xAA) STOP
24C02: A A A
2. 页写(写一页 ---> 8Bytes)
AT24C02一页有8个字节,一次写操作最多可以连续写8个字节
AT24C02在页写时序时,每写入一个字节后,页内地址会自动+1
需要注意的是地址仅低 3bit 加 1,所以不能跨页举个例子:
① 向0x00地址处写入”12345678”
word_addr 数据
00000 000 1
00000 001 2
00000 010 3
...
00000 111 8
② 向0x03地址处写入”12345678”
word_addr 数据
00000 011 1
00000 100 2
00000 101 3
00000 110 4
00000 111 5
00000 000 6 ---> 超出页码后,后面写入的数据会覆盖前面的
00000 001 7
00000 010 8
举个例子:
假设需要往AT24C02内部字节地址为0x00处写入数据0x01,0x02 ... 0x08
那如果我想在0x05—0x0C上写入0x01,0x02 ... 0x08该怎么办呢?
只能分两次写入,先在0x05地址上写入0x01,0x02,0x03
再去0x08地址上写入0x04,0x05,0x06,0x07,0x08
3. 应答查询
读操作时序(可以跨页):
读操作与写操作初始化相同,只是器件地址中的 读 / 写 选择位应为 "1"
内部地址计数器保存着上次访问(已经操作过了)时最后一个地址加1的值,只要芯片有电,该地址就一直保存
1. 当前地址读
2. 随机读(指定读)
一般在读取之前,需要写一个字地址(伪写),此时还未操作,内部地址计数器保存着这个地址,表示下次从设备的哪里开始读。如果读之前,不写字节地址,而直接读就会从芯片内部的(word_addr)处开始读 word_addr相当于光标
3. 顺序读
代码实现:
AT24C02.h
#ifndef __AT24C02_H__ #define __AT24C02_H__ #include "stm32f4xx.h" #include "usart.h" #include "systick.h" /* 初始化AT24C02的通信端口 IIC_SCL ----- I2C1_SCL ----- PB8 IIC_SDA ----- I2C1_SDA ----- PA9 */ void AT24C02_Init(void); /* 等待某个事件发生(有限等待,不能死等) @I2Cx:要等待是哪个总线上的设备 @I2C_EVENT:要等待的是什么事件 @timeout:最多等待多久(超时时间,单位us) 返回值: 等待的事件发生返回0 没有发生返回-1 */ int Wait_IIC_Event(I2C_TypeDef *I2Cx, uint32_t I2C_EVENT, int timeout); /* 用来判断 总线 是否忙碌 @返回值: 能通信返回0 不能通信返回-1 */ int AT24C02_Is_Busy(void); /* 等待 AT24C02 内部写完成 将数据发送过去之后,24C02需要时间写入到addr指定的内部存储器上 此时我们可以启动应答查询(24C02手册第9页<3.应答查询>) 返回值: 写入完成 0 还在写 -1 时序: MCU:START IIC_DEV_ADDR AT24C02: A */ int Wait_24C02_Finished_To_Write(void); /* 字节写(写一个字节): 用来往AT24C02指定的存储空间中写入一个字节的数据 @addr:指定要写入到AT24C02的哪个存储单元中去 @data:要写入的数据(一个字节) @返回值: 成功返回0 失败返回-1 时序: MCU:START 从设备地址(1010 0000/w) 字节地址(0x55) 数据(0xAA) STOP AT24C02: A A A */ int Write_A_Byte_To_24C02(u8 addr, u8 data); /* 用来从AT24C02指定的存储空间中获取一个字节的数据 @addr:指定要访问的AT24C02的存储单元地址(字地址) @返回值:那个字节地址上的数据 */ u8 Read_A_Byte_From_24C02(u8 addr); /* 用来从AT24C02中读取数据 @addr:字地址,指定从哪个地址开始读取数据 @data:从24C02中读取出来的数据保存到其指向的空间中去 @count:要读取的字节数 @返回值:成功返回读取到的字节数,失败返回-1 */ int Read_Bytes_From_24C02(u8 addr, u8 *data, int count); /* 页写: 用来向AT24C02中写入数据 @addr:写入数据的起始地址 @data:要写入的数据 @count:要写入的字节数 @返回值:成功返回写入的字节数,失败返回-1 */ int Write_Bytes_To_24C02(u8 addr, u8 *data, int count); #endifAT24C02.c
#include "AT24C02.h" /* 初始化 AT24C02 的通信端口 IIC_SCL ----- I2C1_SCL ----- PB8 IIC_SDA ----- I2C1_SDA ----- PB9 */ void AT24C02_Init(void) { // (1)初始化 I2C 引脚 // a.使能 GPIO 分组时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); // b.SDA 和 SCL 对应的 GPIO 初始化为复用模式 GPIO_InitTypeDef g; g.GPIO_Mode = GPIO_Mode_AF; // 复用功能 g.GPIO_OType = GPIO_OType_OD; // SDA只能配置成输出开漏 /* SCL和SDA线都需要具有能够输出高低电平的能力 SCL对应的GPIO口配置为开漏或者推挽都可以,因为根据原理图可知 SCL和SDA上都已经外接了上拉电阻具有输出高低电平的能力 但是SDA对应的GPIO口却只能配置为开漏模式,我们知道SDA不仅需要 具有输出高低电平的能力还要具有获取输入的能力(比如要去获取应答) 通过中文手册183页<7.3.10输出配置>可知,在GPIO配置为输出模式时, 实际上输入回路上的施密特触发器是处于开启状态的,也就意味着此时 GPIO是能够获取输入的,但如果配置为输出推挽,当要实现输入检测时, 就会受到输出电路没有关闭的影响,因为之前的输出电平是存在的,造成 输入电路和输出电路的短接等现象,所以只能配置为开漏模式,就算当CPU输 出1,由于N-MOS管处于关闭状态,IO端口的电平将完全由外部电路决定,因此 CPU可以在输入数据寄存器中读到外部电路的信号而不是自己输出的1 */ g.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9; g.GPIO_PuPd = GPIO_PuPd_UP; // 引脚外部已经接了一个上拉电阻 // 再配置为上拉可以提高IIC的驱动能力 g.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &g); // c.配置 GPIO 复用成什么功能 GPIO_PinAFConfig(GPIOB, GPIO_PinSource8, GPIO_AF_I2C1); GPIO_PinAFConfig(GPIOB, GPIO_PinSource9, GPIO_AF_I2C1); // (2)初始化 IIC 控制器 // a.使能 IIC 时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); // b.初始化IIC I2C_InitTypeDef i; i.I2C_Ack = I2C_Ack_Enable; i.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; // 通过24C02说明书可知5V时24C02频率为1MHz,一般为400KHz i.I2C_ClockSpeed = 400000; i.I2C_DutyCycle = I2C_DutyCycle_16_9; i.I2C_Mode = I2C_Mode_I2C; I2C_Init(I2C1, &i); // (3)配置 I2C 控制器的其他功能 // 如:中断配置,ACK配置... // I2C_AcknowledgeConfig(I2C1, DISABLE); // 可以设置为不自动回ACK // (4)开启 I2C 控制器 I2C_Cmd(I2C1, ENABLE); } /* 等待某个事件发生(有限等待,不能死等) @I2Cx:要等待是哪个总线上的设备 @I2C_EVENT:要等待的是什么事件 @timeout:最多等待多久(超时时间,单位us) 返回值: 等待的事件发生返回0 没有发生返回-1 */ int Wait_IIC_Event(I2C_TypeDef *I2Cx, uint32_t I2C_EVENT, int timeout) { // 当事件没有发生或者没有超时的时候继续等待 while (I2C_CheckEvent(I2Cx, I2C_EVENT) == ERROR && timeout--) { // 每隔1us判断一次事件有没有产生或者有没有超时 // 所以超时时间为(timeout)us,即这个函数只会等待(timeout)us delay_us(1); } return (timeout == -1) ? -1 : 0; } /* 用来判断 总线 是否忙碌 @返回值: 能通信返回0 不能通信返回-1 */ int AT24C02_Is_Busy(void) { // 循环判断20次是否总线繁忙 int time = 20; while (time--) { // 发送起始信号 I2C_GenerateSTART(I2C1, ENABLE); if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_MODE_SELECT, 1000) == -1) { // 等待超时,事件EV5(发送起始信号后的应答事件)并未产生 I2C_GenerateSTOP(I2C1, ENABLE); // 发送停止信号 continue; // 发起下一轮繁忙判断 } // 总线此时不繁忙,发送 7bit 的设备地址 // 主要目的是为了测试IIC总线上有没有这个设备以及此设备是否坏了 I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter); if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED, 1000) == 0) { // 代表EV6事件(发送从地址的应答事件)发生 I2C_GenerateSTOP(I2C1, ENABLE); return 0; } I2C_GenerateSTOP(I2C1, ENABLE); // 发送停止信号 } return -1; } /* 等待 AT24C02 内部写完成 将数据发送过去之后,24C02需要时间写入到addr指定的内部存储器上 此时我们可以启动应答查询(24C02手册第9页<3.应答查询>) 返回值: 写入完成 0 还在写 -1 时序: MCU:START IIC_DEV_ADDR AT24C02: A */ int Wait_24C02_Finished_To_Write(void) { int cnt = 20; while (cnt--) { // 判断总线是否繁忙 if (AT24C02_Is_Busy() == -1) { // 打印提示信息 printf("IIC Bus is Busy!\r\n"); continue; } // 总线此时不繁忙,发送起始信号 I2C_GenerateSTART(I2C1, ENABLE); // 等待事件EV5(发送起始信号后的应答事件)产生 if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_MODE_SELECT, 1000) == -1) { // 发送停止信号 I2C_GenerateSTOP(I2C1, ENABLE); continue; } // 发送从设备地址(器件地址) I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter); // 等待事件EV6(发送从地址的应答事件)发生 if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED, 1000) == -1) { // 内部写未完成 I2C_GenerateSTOP(I2C1, ENABLE); continue; // 没有应答的话开始下一次判断 } I2C_GenerateSTOP(I2C1, ENABLE); return 0; // 有应答代表写入完成 } return -1; } /* 字节写(写一个字节) 用来往AT24C02指定的存储空间中写入一个字节的数据 @addr:指定要写入到AT24C02的哪个存储单元中去 @data:要写入的数据(一个字节) @返回值: 成功返回0 失败返回-1 时序: MCU:START 从设备地址(1010 0000/w) 字节地址(0x55) 数据(0xAA) STOP AT24C02: A A A */ int Write_A_Byte_To_24C02(u8 addr, u8 data) { // 1. 先等IIC总线不繁忙(Not Busy)时才能发送数据 // while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY) == SET); // 上面为第一种方式通过标志位去判断总线是否繁忙,但这种方式判定过于简单 // 实际工程中,一般不会死等(万一要是从设备是坏的就会死循环),而是限时等待 // 第二种方式就是通过事件去判断总线是否繁忙 if (AT24C02_Is_Busy() == -1) { printf("IIC Bus is Busy!\r\n"); return -1; } // 发送起始信号 I2C_GenerateSTART(I2C1, ENABLE); // 等待事件 EV5(发送起始信号后的应答事件)产生 if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_MODE_SELECT, 1000) == -1) { // 发送停止信号 I2C_GenerateSTOP(I2C1, ENABLE); return -1; } // 发送从设备地址(器件地址) I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter); // 等待事件EV6(发送从地址的应答事件)发生 if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED, 1000) == -1) { // 发送停止信号 I2C_GenerateSTOP(I2C1, ENABLE); return -1; } // 发送字地址 I2C_SendData(I2C1, addr); // 等待事件EV8_2(数据发送完成) if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED, 1000) == -1) { // 发送停止信号 I2C_GenerateSTOP(I2C1, ENABLE); return -1; } // 发送真正要存储的数据 I2C_SendData(I2C1, data); // 等待事件EV8_2(数据发送完成) if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED, 1000) == -1) { // 发送停止信号 I2C_GenerateSTOP(I2C1, ENABLE); return -1; } // 停止信号 I2C_GenerateSTOP(I2C1, ENABLE); // 将数据发送过去之后,24C02需要时间写入到addr指定的内部存储器上 // 此时我们可以启动应答查询(24C02手册第9页<3.应答查询>) int flag = Wait_24C02_Finished_To_Write(); printf("flag = %d\r\n", flag); return 0; } /* 用来从AT24C02指定的存储空间中获取一个字节的数据 @addr:指定要访问的AT24C02的存储单元地址(字地址) @返回值:那个字节地址上的数据 */ u8 Read_A_Byte_From_24C02(u8 addr) { // 先等 IIC 总线不繁忙(Not Busy)时才能发送数据 if (AT24C02_Is_Busy() == -1) { printf("IIC Bus is Busy!\r\n"); return 0; } // 发送起始信号 I2C_GenerateSTART(I2C1, ENABLE); // 等待事件EV5(发送起始信号后的应答事件)产生 if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_MODE_SELECT, 1000) == -1){ // 发送停止信号 I2C_GenerateSTOP(I2C1, ENABLE); return 0; } // 发送从设备地址(1010 0000/w) I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter); // 等待事件EV6(发送从地址的应答事件)发生 if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED, 1000) == -1) { // 发送停止信号 I2C_GenerateSTOP(I2C1, ENABLE); return 0; } // 发送读取的数据的地址 I2C_SendData(I2C1, addr); // 等待事件EV8_2(数据发送完成) if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED, 1000) == -1) { // 发送停止信号 I2C_GenerateSTOP(I2C1, ENABLE); return 0; } // 发送起始信号 I2C_GenerateSTART(I2C1, ENABLE); // 等待事件EV5(发送起始信号后的应答事件)产生 if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_MODE_SELECT, 1000) == -1) { // 发送停止信号 I2C_GenerateSTOP(I2C1,ENABLE); return 0; } // 发送从设备地址(1010 0001/r) I2C_Send7bitAddress(I2C1, 0xA1, I2C_Direction_Receiver); // 等待事件EV7(主机有数据可以读)发生 if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED, 1000) == -1) { // 发送停止信号 I2C_GenerateSTOP(I2C1, ENABLE); return 0; } // 读取数据 unsigned char data = I2C_ReceiveData(I2C1); // 发送停止信号 I2C_GenerateSTOP(I2C1, ENABLE); return data; } /* 用来从AT24C02中读取数据 @addr:字地址,指定从哪个地址开始读取数据 @data:从24C02中读取出来的数据保存到其指向的空间中去 @count:要读取的字节数 @返回值:成功返回读取到的字节数,失败返回-1 */ int Read_Bytes_From_24C02(u8 addr, u8 *data, int count) { // 判断count是否合法 // 如:从地址0xFF(addr)处读取1(count)个字节,此时count就是非法的 // 256-addr表示能够读取的最多的字节数 count = count < (256 - addr) ? count : (256 - addr); printf("Want To Read %d Bytes!\r\n", count); if (AT24C02_Is_Busy() == -1) { printf("IIC Bus is Busy!\r\n"); return -1; } I2C_GenerateSTART(I2C1, ENABLE); // 等待事件EV5(发送起始信号后的应答事件)产生 if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_MODE_SELECT, 1000) == -1) { I2C_GenerateSTOP(I2C1,ENABLE); return -1; } I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter); // 等待事件EV6(发送从地址的应答事件)发生 if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED, 1000) == -1) { I2C_GenerateSTOP(I2C1, ENABLE); return -1; } // 发送读取的数据的地址 I2C_SendData(I2C1, addr); // 等待事件EV8_2(数据发送完成) if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED, 1000) == -1) { I2C_GenerateSTOP(I2C1, ENABLE); return -1; } I2C_GenerateSTART(I2C1, ENABLE); if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_MODE_SELECT, 1000) == -1) { I2C_GenerateSTOP(I2C1, ENABLE); return -1; } I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Receiver); if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED, 1000) == -1) { I2C_GenerateSTOP(I2C1, ENABLE); return -1; } int i; // 先读取count-1个字节,因为最后一个字节的处理不同(最后一个字节不回复) for (i = 0; i < count - 1; i++) { // 等待事件EV7(主机有数据可以读)发生 if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED, 1000) == -1) { I2C_GenerateSTOP(I2C1, ENABLE); return -1; } data[i] = I2C_ReceiveData(I2C1); } // 由图可知:最后一个字节不回复,只有当对面没有收到应答,对方才会停止发送数据 I2C_AcknowledgeConfig(I2C1, DISABLE); if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED, 1000) == -1) { I2C_GenerateSTOP(I2C1, ENABLE); return -1; } data[i++] = I2C_ReceiveData(I2C1); I2C_GenerateSTOP(I2C1, ENABLE); // 开启自动回复 I2C_AcknowledgeConfig(I2C1, ENABLE); return i; } /* 页写: 用来向AT24C02中写入数据 @addr:写入数据的起始地址 @data:要写入的数据 @count:要写入的字节数 @返回值:成功返回写入的字节数,失败返回-1 */ int Write_Bytes_To_24C02(u8 addr, u8 *data, int count) { // 当前已写入的字节数 int bytes = 0; // 判断count是否合法 count = count < (256 - addr) ? count : (256 - addr); page_write: if (AT24C02_Is_Busy() == -1) { printf("IIC Bus is Busy!\r\n"); return -1; } I2C_GenerateSTART(I2C1, ENABLE); if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_MODE_SELECT, 1000) == -1) { I2C_GenerateSTOP(I2C1, ENABLE); return -1; } I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter); if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED, 1000) == -1) { I2C_GenerateSTOP(I2C1, ENABLE); return -1; } // 发送字节地址 I2C_SendData(I2C1, addr); if(Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED, 1000) == -1) { I2C_GenerateSTOP(I2C1, ENABLE); return -1; } int i; // 发送数据 // 计算出当前写入的位置addr距离该页的末尾有多少个字节 // 8 - 页内地址(addr的低3bit) 0000 0111 int page_bytes = 8 - (addr & 0x7); // 开始写入当前页 for (i = 0; i < page_bytes && bytes < count; i++) { I2C_SendData(I2C1, data[bytes]); if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED, 1000) == -1) { I2C_GenerateSTOP(I2C1, ENABLE); return -1; } bytes++; } I2C_GenerateSTOP(I2C1, ENABLE); if (bytes < count) { // 还没有写完,此时需要跨页了(addr的高5bit需要+1,低5bit从0开始) addr = ((addr >> 3) + 1) << 3; int flag = Wait_24C02_Finished_To_Write(); printf("flag = %d\r\n", flag); goto page_write; } int flag = Wait_24C02_Finished_To_Write(); printf("flag = %d\r\n", flag); return bytes; }main.c
#include "stm32f4xx.h" #include "systick.h" #include "usart.h" #include "AT24C02.h" int main(void) { USART1_Init(9600); AT24C02_Init(); // while (1) { // // Write_A_Byte_To_24C02(0x50, 0x80); // // printf("0x%02x\r\n", Read_A_Byte_From_24C02(0x50)); // // delay_ms(3000); // } // while (1) { // Write_A_Byte_To_24C02(0xFB, 0x80); // Write_A_Byte_To_24C02(0xFD, 0x90); // Write_A_Byte_To_24C02(0xFF, 0x70); // unsigned char data[10] = {0}; // int n = Read_Bytes_From_24C02(0xF8, data, 10); // printf("n == %d\r\n", n); // for (int i = 0; i < n; i++) { // printf("data[%d] : 0x%02x\r\n", i, data[i]); // } // delay_ms(3000); // } while (1) { unsigned char buf[10] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09}; Write_Bytes_To_24C02(0x0F, buf, 10); unsigned char data[10] = {0}; int n = Read_Bytes_From_24C02(0x0F, data, 10); for (int i = 0; i < n; i++) { printf("data[%d] : 0x%02x\r\n", i, data[i]); } delay_ms(3000); } }
















1047

被折叠的 条评论
为什么被折叠?



