IIC笔记
I2C总线的特点
1、它是一个支持多设备的总线。总线上多个设备共用的信号线,在一条I2C的总线钟,可链接多个I2C的通讯设备,支持多个主机和多个从机
2、一条I2C总线只使用两条线路,一条是双向串行数据显(SDA),另外一条是串行时钟线(SCL)。双向串行数据线用于传输数据,串行时钟线用于数据收发同步
3、每一个连接到总线的设备都有一个独一无二的地址,主机可以通过这些地址进行不同设备的访问
4、总线通过上拉电阻接到电源。当I2C设备空闲时,会输出高组态,而当所有设备都空闲时,由上拉电阻把总线拉高成高电平
5、多个主机同时使用总线时,为了防止数据冲突,会利用仲裁的方式决定由哪个设备占用总线。
6、具有三种传输模式,标准模式下传输速率为100kps,快速模式下为400kps,高速模式下可达3.4Mbps,但目前大多数IIC设备不支持高速模式
这里其实我一直有一个问题,这个应答位到底是如何运行的??是一根线在等另外一根线的的反应吗?
在I2C总线上,数据传输是由时钟信号(SCL)驱动的。当发送8位数据后,SDA上的数据已经被发送完毕,然而,SDA并不是在SCL给出反应后再等待。相反,SDA上的每个数据位都在SCL的边沿上稳定。
具体来说,当传输8位数据时,每个数据位都在SCL的上升沿(rising edge)或下降沿(falling edge)上稳定。发送方(通常是主设备)在每个时钟边沿将一个位发送到SDA上,而接收方(通常是从设备)在每个时钟边沿读取SDA上的位。
所以,SDA并不是在等待SCL的反应。SDA上的数据在SCL的边沿上稳定,而总线的协议规定了数据的传输时机。整个I2C通信的节奏由SCL控制,而SDA上的数据在每个SCL的边沿进行相应的变化。
IIC约定
发送一个字节的数据后,对方(接收方)必须要返回一个ACK(应答位)
ACK:是在SDA数据线上的第9个时钟周期,(接收方)必须给SDA一个低电平。
低电平 表示 应答
高电平 表示 没有应答
发送方在发送完第8bit的数据后,一般要释放SDA线(SDA=1)、
这里我一直碰到一个问题,我使用的是正点原子的stm32F4,我使用软件IIC控制MPU6050,就使用了简单的应答实验:起始信号,发送0xA0,读取应答位,停止信号;这里我想读取这个应答位,发现一直为1; 是不是因为我没有释放SDA线???
数据如何发送呢?? 高电平/低电平
在何时发送?? 何时采集呢??
在SCL时钟线低电平的时候,可以改变SDA数据线的电平;
在SCL时钟线高电平点伤害,SDA数据线保持稳定。
===》
在时钟线SCL的下降沿,发送数据
在时钟线SCL的上升线,采集数据
数据的发送,MSB先发送,最高bit先发送。
IIC时钟频率,一般在 400k 以内,低速总线。
那么这里就有一个问题了,SCL时钟线由谁来控制呢??谁产生时钟呢??
其实谁来控制都无所谓,只要不是同时控制就好!!但是有很多设备,不具备控制时钟的能力,因为它可能没有时钟输出单元。
信号
起始信号
SCL时钟线保持高电平
SDL数据线从高到低的跳变
停止信号
SCL 保持高电平
SDA 从低到高电平的跳变
数据收发
Data: User Data , Device Addr
从物理层面来说,数据和地址,都是“数据”。
这个数据包括用户的真正的数据,也包括设备的地址,因为总线上有多个设备,其中一个发起起始信号,表示它要跟总线上的某个或多个设备通信。
它到底要和谁通信呢?
如果不指定,总线上所有设备都可以收到数据。 => IIC协议规定,每个IIC总线上的设备都必须有一个IIC设备地址(7bits/10bits),并且同一个IIC总线上的设备的地址必须不一样。
IIC给定数据收发是以字节为单位:8bits
设备地址 = 7 bits + R/W#(读写位,b0)
R: 发送方 读你的数据 1
W#:发送方 写数据给你 0
应答位
1、从机发送应答位给主机,来不及接收主机数据时发出非应答位,主机终止数据传输;而从机空闲时,从机发出应答(在每接收完一个字节的数据后,从机发送一个应答位,数据第九位为0),即主机根据从发机的应答位判断从机是否成功接收数据(主机发送数据时,通过检测应答位)。
2、主机收到最后一个数据后,向从机发送结束传送的信号(对丛机发送非应答,过程中会将SDA拉低),然后从机释放SDA线(防止SDA拉低以后主机无法将其拉高),从而允许主机产生终止信号(主机接收数据时)(发送应答)其次,主机在发送应答后,也要释放SDA线,防止将其拉低后影响从机后续的数据传送
具体的组合形式
1、主机发送数据
2、主机接收数据
3、主机先发送数据在接收数据
IIC笔记
I2C总线的特点
1、它是一个支持多设备的总线。总线上多个设备共用的信号线,在一条I2C的总线钟,可链接多个I2C的通讯设备,支持多个主机和多个从机
2、一条I2C总线只使用两条线路,一条是双向串行数据显(SDA),另外一条是串行时钟线(SCL)。双向串行数据线用于传输数据,串行时钟线用于数据收发同步
3、每一个连接到总线的设备都有一个独一无二的地址,主机可以通过这些地址进行不同设备的访问
4、总线通过上拉电阻接到电源。当I2C设备空闲时,会输出高组态,而当所有设备都空闲时,由上拉电阻把总线拉高成高电平
5、多个主机同时使用总线时,为了防止数据冲突,会利用仲裁的方式决定由哪个设备占用总线。
6、具有三种传输模式,标准模式下传输速率为100kps,快速模式下为400kps,高速模式下可达3.4Mbps,但目前大多数IIC设备不支持高速模式
这里其实我一直有一个问题,这个应答位到底是如何运行的??是一根线在等另外一根线的的反应吗?
在I2C总线上,数据传输是由时钟信号(SCL)驱动的。当发送8位数据后,SDA上的数据已经被发送完毕,然而,SDA并不是在SCL给出反应后再等待。相反,SDA上的每个数据位都在SCL的边沿上稳定。
具体来说,当传输8位数据时,每个数据位都在SCL的上升沿(rising edge)或下降沿(falling edge)上稳定。发送方(通常是主设备)在每个时钟边沿将一个位发送到SDA上,而接收方(通常是从设备)在每个时钟边沿读取SDA上的位。
所以,SDA并不是在等待SCL的反应。SDA上的数据在SCL的边沿上稳定,而总线的协议规定了数据的传输时机。整个I2C通信的节奏由SCL控制,而SDA上的数据在每个SCL的边沿进行相应的变化。
IIC约定
发送一个字节的数据后,对方(接收方)必须要返回一个ACK(应答位)
ACK:是在SDA数据线上的第9个时钟周期,(接收方)必须给SDA一个低电平。
低电平 表示 应答
高电平 表示 没有应答
发送方在发送完第8bit的数据后,一般要释放SDA线(SDA=1)、
这里我一直碰到一个问题,我使用的是正点原子的stm32F4,我使用软件IIC控制MPU6050,就使用了简单的应答实验:起始信号,发送0xA0,读取应答位,停止信号;这里我想读取这个应答位,发现一直为1; 是不是因为我没有释放SDA线???
数据如何发送呢?? 高电平/低电平
在何时发送?? 何时采集呢??
在SCL时钟线低电平的时候,可以改变SDA数据线的电平;
在SCL时钟线高电平点伤害,SDA数据线保持稳定。
===》
在时钟线SCL的下降沿,发送数据
在时钟线SCL的上升线,采集数据
数据的发送,MSB先发送,最高bit先发送。
IIC时钟频率,一般在 400k 以内,低速总线。
那么这里就有一个问题了,SCL时钟线由谁来控制呢??谁产生时钟呢??
其实谁来控制都无所谓,只要不是同时控制就好!!但是有很多设备,不具备控制时钟的能力,因为它可能没有时钟输出单元。
信号
起始信号
SCL时钟线保持高电平
SDL数据线从高到低的跳变
停止信号
SCL 保持高电平
SDA 从低到高电平的跳变
数据收发
Data: User Data , Device Addr
从物理层面来说,数据和地址,都是“数据”。
这个数据包括用户的真正的数据,也包括设备的地址,因为总线上有多个设备,其中一个发起起始信号,表示它要跟总线上的某个或多个设备通信。
它到底要和谁通信呢?
如果不指定,总线上所有设备都可以收到数据。 => IIC协议规定,每个IIC总线上的设备都必须有一个IIC设备地址(7bits/10bits),并且同一个IIC总线上的设备的地址必须不一样。
IIC给定数据收发是以字节为单位:8bits
设备地址 = 7 bits + R/W#(读写位,b0)
R: 发送方 读你的数据 1
W#:发送方 写数据给你 0
应答位
1、从机发送应答位给主机,来不及接收主机数据时发出非应答位,主机终止数据传输;而从机空闲时,从机发出应答(在每接收完一个字节的数据后,从机发送一个应答位,数据第九位为0),即主机根据从发机的应答位判断从机是否成功接收数据(主机发送数据时,通过检测应答位)。
2、主机收到最后一个数据后,向从机发送结束传送的信号(对丛机发送非应答,过程中会将SDA拉低),然后从机释放SDA线(防止SDA拉低以后主机无法将其拉高),从而允许主机产生终止信号(主机接收数据时)(发送应答)其次,主机在发送应答后,也要释放SDA线,防止将其拉低后影响从机后续的数据传送
具体的组合形式
1、主机发送数据
2、主机接收数据
3、主机先发送数据在接收数据
在I2C通信中,主机先发送数据再接收数据的情况通常用于读操作。在I2C协议中,读写的方向是通过通信的起始阶段确定的。如果主机希望从从机(设备)中读取数据,它首先会发送一个包含从机地址和读方向的起始帧,然后在之后的通信中,主机将发送时钟脉冲并接收从机发送的数据。
具体的步骤如下:
- 起始帧: 主机发送起始帧,包括从机地址和读方向的位(通常是最低有效位设置为1)。
- 从机应答: 从机收到主机的地址后,会发出应答信号。
- 主机发送数据: 主机在接收到从机的应答后,会继续发送一个或多个数据字节给从机。
- 从机应答: 每发送一个数据字节后,从机都会发出应答信号,表示已成功接收。
- 切换方向: 在发送完要读取的数据后,主机发出停止帧,然后重新发起一个起始帧,但这次将方向位设为读。
- 从机发送数据: 从机在接收到主机的读方向后,会发送数据给主机。
- 主机应答/非应答: 主机在接收到从机的每个数据字节后,可以选择发出应答信号或非应答信号。如果主机希望继续接收数据,它发送应答信号;如果主机不想再接收数据,它发送非应答信号,并通常跟随着停止帧。
这种先发送数据再接收数据的顺序用于读取从机的数据。
IIC软件模拟
个人认为下面这种IIC软件模拟的方式最好!!
/*引脚配置层*/
/**
* 函 数:I2C写SCL引脚电平
* 参 数:BitValue 协议层传入的当前需要写入SCL的电平,范围0~1
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平
*/
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue); //根据BitValue,设置SCL引脚的电平
Delay_us(10); //延时10us,防止时序频率超过要求
}
/**
* 函 数:I2C写SDA引脚电平
* 参 数:BitValue 协议层传入的当前需要写入SDA的电平,范围0~0xFF
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SDA为低电平,当BitValue非0时,需要置SDA为高电平
*/
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue); //根据BitValue,设置SDA引脚的电平,BitValue要实现非0即1的特性
Delay_us(10); //延时10us,防止时序频率超过要求
}
/**
* 函 数:I2C读SDA引脚电平
* 参 数:无
* 返 回 值:协议层需要得到的当前SDA的电平,范围0~1
* 注意事项:此函数需要用户实现内容,当前SDA为低电平时,返回0,当前SDA为高电平时,返回1
*/
uint8_t MyI2C_R_SDA(void)
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11); //读取SDA电平
Delay_us(10); //延时10us,防止时序频率超过要求
return BitValue; //返回SDA电平
}
/**
* 函 数:I2C初始化
* 参 数:无
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,实现SCL和SDA引脚的初始化
*/
void MyI2C_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB10和PB11引脚初始化为开漏输出
/*设置默认电平*/
GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11); //设置PB10和PB11引脚初始化后默认为高电平(释放总线状态)
}
/*协议层*/
/**
* 函 数:I2C起始
* 参 数:无
* 返 回 值:无
*/
void MyI2C_Start(void)
{
MyI2C_W_SDA(1); //释放SDA,确保SDA为高电平
MyI2C_W_SCL(1); //释放SCL,确保SCL为高电平
MyI2C_W_SDA(0); //在SCL高电平期间,拉低SDA,产生起始信号
MyI2C_W_SCL(0); //起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
}
/**
* 函 数:I2C终止
* 参 数:无
* 返 回 值:无
*/
void MyI2C_Stop(void)
{
MyI2C_W_SDA(0); //拉低SDA,确保SDA为低电平
MyI2C_W_SCL(1); //释放SCL,使SCL呈现高电平
MyI2C_W_SDA(1); //在SCL高电平期间,释放SDA,产生终止信号
}
/**
* 函 数:I2C发送一个字节
* 参 数:Byte 要发送的一个字节数据,范围:0x00~0xFF
* 返 回 值:无
*/
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i ++) //循环8次,主机依次发送数据的每一位
{
MyI2C_W_SDA(Byte & (0x80 >> i)); //使用掩码的方式取出Byte的指定一位数据并写入到SDA线
MyI2C_W_SCL(1); //释放SCL,从机在SCL高电平期间读取SDA
MyI2C_W_SCL(0); //拉低SCL,主机开始发送下一位数据
}
}
/**
* 函 数:I2C接收一个字节
* 参 数:无
* 返 回 值:接收到的一个字节数据,范围:0x00~0xFF
*/
uint8_t MyI2C_ReceiveByte(void)
{
uint8_t i, Byte = 0x00; //定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
MyI2C_W_SDA(1); //接收前,主机先确保释放SDA,避免干扰从机的数据发送
for (i = 0; i < 8; i ++) //循环8次,主机依次接收数据的每一位
{
MyI2C_W_SCL(1); //释放SCL,主机机在SCL高电平期间读取SDA
if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);} //读取SDA数据,并存储到Byte变量
//当SDA为1时,置变量指定位为1,当SDA为0时,不做处理,指定位为默认的初值0
MyI2C_W_SCL(0); //拉低SCL,从机在SCL低电平期间写入SDA
}
return Byte; //返回接收到的一个字节数据
}
/**
* 函 数:I2C发送应答位
* 参 数:Byte 要发送的应答位,范围:0~1,0表示应答,1表示非应答
* 返 回 值:无
*/
void MyI2C_SendAck(uint8_t AckBit)
{
MyI2C_W_SDA(AckBit); //主机把应答位数据放到SDA线
MyI2C_W_SCL(1); //释放SCL,从机在SCL高电平期间,读取应答位
MyI2C_W_SCL(0); //拉低SCL,开始下一个时序模块
}
/**
* 函 数:I2C接收应答位
* 参 数:无
* 返 回 值:接收到的应答位,范围:0~1,0表示应答,1表示非应答
*/
uint8_t MyI2C_ReceiveAck(void)
{
uint8_t AckBit; //定义应答位变量
MyI2C_W_SDA(1); //接收前,主机先确保释放SDA,避免干扰从机的数据发送
MyI2C_W_SCL(1); //释放SCL,主机机在SCL高电平期间读取SDA
AckBit = MyI2C_R_SDA(); //将应答位存储到变量里
MyI2C_W_SCL(0); //拉低SCL,开始下一个时序模块
return AckBit; //返回定义应答位变量
}
IIC模块(硬件IIC)
所谓硬件I2C对应芯片上的I2C外设,有相应I2C驱动电路,其所使用的I2C管脚也是专用的;软件I2C一般是用GPIO管脚,用软件控制管脚状态以模拟I2C通讯波形。
硬件I2C的效率要远高于软件的,而软件I2C由于不受管脚限制,接口比较灵活。
模拟I2C 是通过GPIO,软件模拟寄存器的工作方式,而硬件(固件)I2C是直接调用内部寄存器进行配置。如果要从具体硬件上来看,可以去看下芯片手册。因为固件I2C的端口是固定的,所以会有所区别。
至于如何区分它们:
可以看底层配置,比如IO口配置,如果配置了IO口的功能(IIC功能)那就是固件IIC,否则就是模拟
可以看IIC写函数,看里面有木有调用现成的函数或者给某个寄存器赋值,如果有,则肯定是固件IIC功能,没有的话肯定是数据一个bit一个bit模拟发生送的,肯定用到了循环,则为模拟。
根据代码量判断,模拟的代码量肯定比固件的要大。
- 硬件IIC用法比较复杂,模拟IIC的流程更清楚一些。
- 硬件IIC速度比模拟快,并且可以用DMA
- 模拟IIC可以在任何管脚上,而硬件只能在固定管脚上。
软件i2c是程序员使用程序控制SCL,SDA线输出高低电平,模拟i2c协议的时序。一般较硬件i2c稳定,但是程序较为繁琐,但不难。
硬件i2c程序员只要调用i2c的控制函数即可,不用直接的去控制SCL,SDA高低电平的输出。但是有些单片机的硬件i2c不太稳定,调试问题较多
硬件IIC也有优点:
硬件IIC的实现相对简单,无需编写复杂的代码;速度快,可以实现高速通信;实现简单,无需编写复杂的代码;稳定性好,不容易出现通信错误。
实现硬件IIC通讯
实现硬件IIC的代码需要使用STM32的内部硬件模块,具体步骤如下:配置GPIO用于IIC通讯,将SCL和SDA引脚分别配置为复用推挽输出模式;配置I2C控制器,包括I2C时钟频率,I2C地址,I2C工作模式等参数;启动I2C控制器,并发送数据或接收数据。
STM32F4XX IIC固件库的使用
(1) 初始化IIC引脚:GPIO复用
RCC_xxxxx : 使能时钟
GPIO_Init : 初始化GPIO口
GPIO_PinAFConfig : 配置GPIO的复用功能
(2) 初始化I2C控制器
RCC_xxxx :使能I2C时钟
I2C_Init :配置I2C
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);
I2Cx: 指定要初始的I2C控制器,如: I2C1, I2C2,...
I2C_InitStruct: 指向初始化结构体,其成员变量如下:
typedef struct
{
uint32_t I2C_ClockSpeed;
指定I2C总线的时钟频率,如:
100000 //100K
uint16_t I2C_Mode;
指定I2C协议还是SMBUS协议
I2C_Mode_I2C : 主模式
I2C_Mode_SMBusDevice : 从模式
I2C_Mode_SMBusHost
在主模式下,你可以作为主设备发起I2C通信。在从模式下,你通常是一个I2C外设。
uint16_t I2C_DutyCycle;
当i2C是fast mode,指定 Tlow: Thigh
I2C_DutyCycle_16_9
I2C_DutyCycle_2 : 通常选择这个
uint16_t I2C_OwnAddress1;
指定i2c控制器自身的地址,随你定,只要不跟同一i2c总线上的
其他设备相冲突就可以啦。
什么时候需要指定i2c设备地址呢?
当作为从设备时,需要I2C设备地址。
uint16_t I2C_Ack;
i2c控制器收到数据时,是否应答。
I2C_Ack_Enable
I2C_Ack_Disable
uint16_t I2C_AcknowledgedAddress;
指定应答地址是7bits还是10bits
I2C_AcknowledgedAddress_7bit
I2C_AcknowledgedAddress_10bit
}I2C_InitTypeDef;
(3) 配置I2C控制器的一些功能(如: 是否应答,中断使能,...)
(4) 开启I2C控制器
I2C_Cmd
初始化IIC
void I2C_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
I2C_InitTypeDef I2C_InitStructure;
// 使能GPIOB时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
// 配置PB8和PB9为复用模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 配置PB8和PB9的复用功能为I2C
GPIO_PinAFConfig(GPIOB, GPIO_PinSource8, GPIO_AF_I2C1);
GPIO_PinAFConfig(GPIOB, GPIO_PinSource9, GPIO_AF_I2C1);
// 使能I2C1时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
// 配置I2C速率
I2C_InitStructure.I2C_ClockSpeed = 100000; // I2C时钟速率为100kHz
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_OwnAddress1 = 0x00;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_Init(I2C1, &I2C_InitStructure);
// 使能I2C1
I2C_Cmd(I2C1, ENABLE);
}
写一个AT24C02的驱动层,为上提供操作AT24C02的接口函数
#include "stm32f4xx.h"
#include "stm32f4xx_i2c.h"
#define EEPROM_ADDR 0xA0 // AT24C02的I2C地址
void I2C_Configuration(void);
void EEPROM_WriteByte(uint16_t addr, uint8_t data);
uint8_t EEPROM_ReadByte(uint16_t addr);
void EEPROM_WritePage(uint16_t addr, uint8_t *data, uint8_t length);
void EEPROM_ReadPage(uint16_t addr, uint8_t *data, uint8_t length);
int main(void)
{
// 初始化系统时钟等
SystemInit();
// 配置I2C
I2C_Configuration();
// 在这里使用EEPROM的接口函数进行操作
uint16_t address = 0x0000;
uint8_t data_to_write = 0xAA;
EEPROM_WriteByte(address, data_to_write);
uint8_t data_read = EEPROM_ReadByte(address);
while (1)
{
// 在这里编写你的主程序逻辑
}
}
void I2C_Configuration(void)
{
// I2C初始化代码
}
void EEPROM_WriteByte(uint8_t addr, uint8_t data)
{
while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
//I2C_FLAG_BUSY是一个标志位,当它为1时,表示I2C线程正在忙中。
//因此,这个while循环会一直等待,直到I2C_FLAG_BUSY变成0,即I2C中断不再停止。
// 生成START条件
I2C_GenerateSTART(I2C1, ENABLE);
// 等待START条件生成
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
// 发送EEPROM的I2C地址,选择写模式
I2C_Send7bitAddress(I2C1, EEPROM_ADDR, I2C_Direction_Transmitter);
// 等待地址发送完成
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
// 发送要写入的地址
I2C_SendData(I2C1, addr);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
// 发送要写入的数据
I2C_SendData(I2C1, data);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
// 生成STOP条件
I2C_GenerateSTOP(I2C1, ENABLE);
}
uint8_t EEPROM_ReadByte(uint8_t addr)
{
uint8_t data;
while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY))
;
// 生成START条件
I2C_GenerateSTART(I2C1, ENABLE);
// 等待START条件生成
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
// 发送EEPROM的I2C地址,选择写模式
I2C_Send7bitAddress(I2C1, EEPROM_ADDR, I2C_Direction_Transmitter);
// 等待地址发送完成
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
// 发送要写入的地址
I2C_SendData(I2C1, addr);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
// 生成RESTART条件
I2C_GenerateSTART(I2C1, ENABLE);
// 等待RESTART条件生成
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
// 发送EEPROM的I2C地址,选择读模式
I2C_Send7bitAddress(I2C1, EEPROM_ADDR, I2C_Direction_Receiver);
// 等待地址发送完成
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
// 等待数据接收
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
data = I2C_ReceiveData(I2C1);
// 生成STOP条件
I2C_GenerateSTOP(I2C1, ENABLE);
return data;
}
void EEPROM_WritePage(uint16_t addr, uint8_t *data, uint8_t length)
{
// 在这里实现写一页数据的操作
}
void EEPROM_ReadPage(uint16_t addr, uint8_t *data, uint8_t length)
{
// 在这里实现读一页数据的操作
}
在I2C通信中,主机先发送数据再接收数据的情况通常用于读操作。在I2C协议中,读写的方向是通过通信的起始阶段确定的。如果主机希望从从机(设备)中读取数据,它首先会发送一个包含从机地址和读方向的起始帧,然后在之后的通信中,主机将发送时钟脉冲并接收从机发送的数据。
具体的步骤如下:
- 起始帧: 主机发送起始帧,包括从机地址和读方向的位(通常是最低有效位设置为1)。
- 从机应答: 从机收到主机的地址后,会发出应答信号。
- 主机发送数据: 主机在接收到从机的应答后,会继续发送一个或多个数据字节给从机。
- 从机应答: 每发送一个数据字节后,从机都会发出应答信号,表示已成功接收。
- 切换方向: 在发送完要读取的数据后,主机发出停止帧,然后重新发起一个起始帧,但这次将方向位设为读。
- 从机发送数据: 从机在接收到主机的读方向后,会发送数据给主机。
- 主机应答/非应答: 主机在接收到从机的每个数据字节后,可以选择发出应答信号或非应答信号。如果主机希望继续接收数据,它发送应答信号;如果主机不想再接收数据,它发送非应答信号,并通常跟随着停止帧。
这种先发送数据再接收数据的顺序用于读取从机的数据。
IIC软件模拟
个人认为下面这种IIC软件模拟的方式最好!!
/*引脚配置层*/
/**
* 函 数:I2C写SCL引脚电平
* 参 数:BitValue 协议层传入的当前需要写入SCL的电平,范围0~1
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平
*/
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue); //根据BitValue,设置SCL引脚的电平
Delay_us(10); //延时10us,防止时序频率超过要求
}
/**
* 函 数:I2C写SDA引脚电平
* 参 数:BitValue 协议层传入的当前需要写入SDA的电平,范围0~0xFF
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SDA为低电平,当BitValue非0时,需要置SDA为高电平
*/
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue); //根据BitValue,设置SDA引脚的电平,BitValue要实现非0即1的特性
Delay_us(10); //延时10us,防止时序频率超过要求
}
/**
* 函 数:I2C读SDA引脚电平
* 参 数:无
* 返 回 值:协议层需要得到的当前SDA的电平,范围0~1
* 注意事项:此函数需要用户实现内容,当前SDA为低电平时,返回0,当前SDA为高电平时,返回1
*/
uint8_t MyI2C_R_SDA(void)
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11); //读取SDA电平
Delay_us(10); //延时10us,防止时序频率超过要求
return BitValue; //返回SDA电平
}
/**
* 函 数:I2C初始化
* 参 数:无
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,实现SCL和SDA引脚的初始化
*/
void MyI2C_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB10和PB11引脚初始化为开漏输出
/*设置默认电平*/
GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11); //设置PB10和PB11引脚初始化后默认为高电平(释放总线状态)
}
/*协议层*/
/**
* 函 数:I2C起始
* 参 数:无
* 返 回 值:无
*/
void MyI2C_Start(void)
{
MyI2C_W_SDA(1); //释放SDA,确保SDA为高电平
MyI2C_W_SCL(1); //释放SCL,确保SCL为高电平
MyI2C_W_SDA(0); //在SCL高电平期间,拉低SDA,产生起始信号
MyI2C_W_SCL(0); //起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
}
/**
* 函 数:I2C终止
* 参 数:无
* 返 回 值:无
*/
void MyI2C_Stop(void)
{
MyI2C_W_SDA(0); //拉低SDA,确保SDA为低电平
MyI2C_W_SCL(1); //释放SCL,使SCL呈现高电平
MyI2C_W_SDA(1); //在SCL高电平期间,释放SDA,产生终止信号
}
/**
* 函 数:I2C发送一个字节
* 参 数:Byte 要发送的一个字节数据,范围:0x00~0xFF
* 返 回 值:无
*/
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i ++) //循环8次,主机依次发送数据的每一位
{
MyI2C_W_SDA(Byte & (0x80 >> i)); //使用掩码的方式取出Byte的指定一位数据并写入到SDA线
MyI2C_W_SCL(1); //释放SCL,从机在SCL高电平期间读取SDA
MyI2C_W_SCL(0); //拉低SCL,主机开始发送下一位数据
}
}
/**
* 函 数:I2C接收一个字节
* 参 数:无
* 返 回 值:接收到的一个字节数据,范围:0x00~0xFF
*/
uint8_t MyI2C_ReceiveByte(void)
{
uint8_t i, Byte = 0x00; //定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
MyI2C_W_SDA(1); //接收前,主机先确保释放SDA,避免干扰从机的数据发送
for (i = 0; i < 8; i ++) //循环8次,主机依次接收数据的每一位
{
MyI2C_W_SCL(1); //释放SCL,主机机在SCL高电平期间读取SDA
if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);} //读取SDA数据,并存储到Byte变量
//当SDA为1时,置变量指定位为1,当SDA为0时,不做处理,指定位为默认的初值0
MyI2C_W_SCL(0); //拉低SCL,从机在SCL低电平期间写入SDA
}
return Byte; //返回接收到的一个字节数据
}
/**
* 函 数:I2C发送应答位
* 参 数:Byte 要发送的应答位,范围:0~1,0表示应答,1表示非应答
* 返 回 值:无
*/
void MyI2C_SendAck(uint8_t AckBit)
{
MyI2C_W_SDA(AckBit); //主机把应答位数据放到SDA线
MyI2C_W_SCL(1); //释放SCL,从机在SCL高电平期间,读取应答位
MyI2C_W_SCL(0); //拉低SCL,开始下一个时序模块
}
/**
* 函 数:I2C接收应答位
* 参 数:无
* 返 回 值:接收到的应答位,范围:0~1,0表示应答,1表示非应答
*/
uint8_t MyI2C_ReceiveAck(void)
{
uint8_t AckBit; //定义应答位变量
MyI2C_W_SDA(1); //接收前,主机先确保释放SDA,避免干扰从机的数据发送
MyI2C_W_SCL(1); //释放SCL,主机机在SCL高电平期间读取SDA
AckBit = MyI2C_R_SDA(); //将应答位存储到变量里
MyI2C_W_SCL(0); //拉低SCL,开始下一个时序模块
return AckBit; //返回定义应答位变量
}
IIC模块(硬件IIC)
所谓硬件I2C对应芯片上的I2C外设,有相应I2C驱动电路,其所使用的I2C管脚也是专用的;软件I2C一般是用GPIO管脚,用软件控制管脚状态以模拟I2C通讯波形。
硬件I2C的效率要远高于软件的,而软件I2C由于不受管脚限制,接口比较灵活。
模拟I2C 是通过GPIO,软件模拟寄存器的工作方式,而硬件(固件)I2C是直接调用内部寄存器进行配置。如果要从具体硬件上来看,可以去看下芯片手册。因为固件I2C的端口是固定的,所以会有所区别。
至于如何区分它们:
可以看底层配置,比如IO口配置,如果配置了IO口的功能(IIC功能)那就是固件IIC,否则就是模拟
可以看IIC写函数,看里面有木有调用现成的函数或者给某个寄存器赋值,如果有,则肯定是固件IIC功能,没有的话肯定是数据一个bit一个bit模拟发生送的,肯定用到了循环,则为模拟。
根据代码量判断,模拟的代码量肯定比固件的要大。
- 硬件IIC用法比较复杂,模拟IIC的流程更清楚一些。
- 硬件IIC速度比模拟快,并且可以用DMA
- 模拟IIC可以在任何管脚上,而硬件只能在固定管脚上。
软件i2c是程序员使用程序控制SCL,SDA线输出高低电平,模拟i2c协议的时序。一般较硬件i2c稳定,但是程序较为繁琐,但不难。
硬件i2c程序员只要调用i2c的控制函数即可,不用直接的去控制SCL,SDA高低电平的输出。但是有些单片机的硬件i2c不太稳定,调试问题较多
硬件IIC也有优点:
硬件IIC的实现相对简单,无需编写复杂的代码;速度快,可以实现高速通信;实现简单,无需编写复杂的代码;稳定性好,不容易出现通信错误。
实现硬件IIC通讯
实现硬件IIC的代码需要使用STM32的内部硬件模块,具体步骤如下:配置GPIO用于IIC通讯,将SCL和SDA引脚分别配置为复用推挽输出模式;配置I2C控制器,包括I2C时钟频率,I2C地址,I2C工作模式等参数;启动I2C控制器,并发送数据或接收数据。
STM32F4XX IIC固件库的使用
(1) 初始化IIC引脚:GPIO复用
RCC_xxxxx : 使能时钟
GPIO_Init : 初始化GPIO口
GPIO_PinAFConfig : 配置GPIO的复用功能
(2) 初始化I2C控制器
RCC_xxxx :使能I2C时钟
I2C_Init :配置I2C
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);
I2Cx: 指定要初始的I2C控制器,如: I2C1, I2C2,...
I2C_InitStruct: 指向初始化结构体,其成员变量如下:
typedef struct
{
uint32_t I2C_ClockSpeed;
指定I2C总线的时钟频率,如:
100000 //100K
uint16_t I2C_Mode;
指定I2C协议还是SMBUS协议
I2C_Mode_I2C : 主模式
I2C_Mode_SMBusDevice : 从模式
I2C_Mode_SMBusHost
在主模式下,你可以作为主设备发起I2C通信。在从模式下,你通常是一个I2C外设。
uint16_t I2C_DutyCycle;
当i2C是fast mode,指定 Tlow: Thigh
I2C_DutyCycle_16_9
I2C_DutyCycle_2 : 通常选择这个
uint16_t I2C_OwnAddress1;
指定i2c控制器自身的地址,随你定,只要不跟同一i2c总线上的
其他设备相冲突就可以啦。
什么时候需要指定i2c设备地址呢?
当作为从设备时,需要I2C设备地址。
uint16_t I2C_Ack;
i2c控制器收到数据时,是否应答。
I2C_Ack_Enable
I2C_Ack_Disable
uint16_t I2C_AcknowledgedAddress;
指定应答地址是7bits还是10bits
I2C_AcknowledgedAddress_7bit
I2C_AcknowledgedAddress_10bit
}I2C_InitTypeDef;
(3) 配置I2C控制器的一些功能(如: 是否应答,中断使能,...)
(4) 开启I2C控制器
I2C_Cmd
初始化IIC
void I2C_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
I2C_InitTypeDef I2C_InitStructure;
// 使能GPIOB时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
// 配置PB8和PB9为复用模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 配置PB8和PB9的复用功能为I2C
GPIO_PinAFConfig(GPIOB, GPIO_PinSource8, GPIO_AF_I2C1);
GPIO_PinAFConfig(GPIOB, GPIO_PinSource9, GPIO_AF_I2C1);
// 使能I2C1时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
// 配置I2C速率
I2C_InitStructure.I2C_ClockSpeed = 100000; // I2C时钟速率为100kHz
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_OwnAddress1 = 0x00;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_Init(I2C1, &I2C_InitStructure);
// 使能I2C1
I2C_Cmd(I2C1, ENABLE);
}
写一个AT24C02的驱动层,为上提供操作AT24C02的接口函数
#include "stm32f4xx.h"
#include "stm32f4xx_i2c.h"
#define EEPROM_ADDR 0xA0 // AT24C02的I2C地址
void I2C_Configuration(void);
void EEPROM_WriteByte(uint16_t addr, uint8_t data);
uint8_t EEPROM_ReadByte(uint16_t addr);
void EEPROM_WritePage(uint16_t addr, uint8_t *data, uint8_t length);
void EEPROM_ReadPage(uint16_t addr, uint8_t *data, uint8_t length);
int main(void)
{
// 初始化系统时钟等
SystemInit();
// 配置I2C
I2C_Configuration();
// 在这里使用EEPROM的接口函数进行操作
uint16_t address = 0x0000;
uint8_t data_to_write = 0xAA;
EEPROM_WriteByte(address, data_to_write);
uint8_t data_read = EEPROM_ReadByte(address);
while (1)
{
// 在这里编写你的主程序逻辑
}
}
void I2C_Configuration(void)
{
// I2C初始化代码
}
void EEPROM_WriteByte(uint8_t addr, uint8_t data)
{
while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
//I2C_FLAG_BUSY是一个标志位,当它为1时,表示I2C线程正在忙中。
//因此,这个while循环会一直等待,直到I2C_FLAG_BUSY变成0,即I2C中断不再停止。
// 生成START条件
I2C_GenerateSTART(I2C1, ENABLE);
// 等待START条件生成
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
// 发送EEPROM的I2C地址,选择写模式
I2C_Send7bitAddress(I2C1, EEPROM_ADDR, I2C_Direction_Transmitter);
// 等待地址发送完成
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
// 发送要写入的地址
I2C_SendData(I2C1, addr);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
// 发送要写入的数据
I2C_SendData(I2C1, data);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
// 生成STOP条件
I2C_GenerateSTOP(I2C1, ENABLE);
}
uint8_t EEPROM_ReadByte(uint8_t addr)
{
uint8_t data;
while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY))
;
// 生成START条件
I2C_GenerateSTART(I2C1, ENABLE);
// 等待START条件生成
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
// 发送EEPROM的I2C地址,选择写模式
I2C_Send7bitAddress(I2C1, EEPROM_ADDR, I2C_Direction_Transmitter);
// 等待地址发送完成
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
// 发送要写入的地址
I2C_SendData(I2C1, addr);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
// 生成RESTART条件
I2C_GenerateSTART(I2C1, ENABLE);
// 等待RESTART条件生成
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
// 发送EEPROM的I2C地址,选择读模式
I2C_Send7bitAddress(I2C1, EEPROM_ADDR, I2C_Direction_Receiver);
// 等待地址发送完成
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
// 等待数据接收
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
data = I2C_ReceiveData(I2C1);
// 生成STOP条件
I2C_GenerateSTOP(I2C1, ENABLE);
return data;
}
void EEPROM_WritePage(uint16_t addr, uint8_t *data, uint8_t length)
{
// 在这里实现写一页数据的操作
}
void EEPROM_ReadPage(uint16_t addr, uint8_t *data, uint8_t length)
{
// 在这里实现读一页数据的操作
}
在这里插入图片描述