一、I2C发送
1.电路结构(线与)
由于串口只能一对一通信,所以引入I2C。
7位地址:0000000——1111111,也就是从0——127,去掉一些特殊的地址,仍然能够同时和100多个从机通信。
SCL:主--->从
SDA:主--->从,从--->主
开漏输出就是为了实现逻辑线与功能
2.I2C通信协议
2.1整体流程:
2.2起始位和停止位:
2.3寻址阶段:
主机发送地址0x78,左移一位,给读写那一位留出空间,读为1,写为0。主机发送完要等待从机回应
2.4发送数据阶段:
发送方发完,接收方应答
3.模块使用
3.1波特率
3.2速度模式
3.3占空比
3.4 I2C初始化
这里需要特别注意,I2C使用之前一定要施加并释放复位信号,类似于手机的重启
4.代码
4.1 I2C内部结构总框图
4.2发送函数介绍
4.3发送函数编写
#1、首先等待总线空闲,因为并不能保证SCL和SDA都处于高电位(空闲)
#2、发送起始位并等待起始位发送完成
#3、寻址阶段:先清除AF标志位(因为后面要查看此标志位),再发送地址,等待从机回应(ADDR和AF),清除ADDR标志位。
这里发送地址那里要注意,清除ADDR的方式也要注意
#4、发送数据阶段:
发送之前:一定要先查看上一次发送时数据结束后是否接收(AF),同时确保数据寄存器为空再发送。
发送之后:发送完同样检查是否收到回应并且查看数据寄存器和移位寄存器为空(BTF)
#5、发送停止位
二、I2C接收
1.代码
1.1接收函数介绍
注意:发送前等待总线空闲,是为了争夺 I2C 总线控制权;接收数据时,控制权已经属于当前主机,只是在读取 SDA 电平,因此不需要等待总线空闲。不过注意:如果是“一次新的读操作”,比如读取传感器,也要先等空闲,因为你还是要先发送地址。只有连续接收数据阶段才不用等。
2.接收函数编写
#1、发送起始位等待发送完成
#2、寻址阶段
先清除AF 再发送地址 等待从机回应
#3、接收数据
讲接收数据之前先了解ACK和NAK,这块与发送数据不同。
特别需要注意的是:他们只作用于正在被接受的字节
停止位作用于正在被接收的字节之后
#3.1、size = 1时
由于清除ADDR之后就开始接收数据了,所以清除完ADDR字节正处于接收状态,从而必须马上发送NAK(ACK = 0)并且发送停止位。当字节完全被接受到移位寄存器中会自动被送到接受数据寄存器中,所以等待接收数据寄存器为非空状态就可以读取数据了
#3.2、size = 2时
第一个字节被读取完之后第二个字节自动进入移位寄存器。
#3.3、size = n时
ACK=1 是开关,不是按钮。表示的是“从现在开始,我每收到一个字节,都自动回 ACK。”
三、SI2C
1.SI2C初始化
因为要实现线与功能所以都是开漏模式
2.SI2C发送一个字节
2.1先建立几个函数方便后续对SCL和SDA操作
void SCL_Write(uint8_t Level) { if(Level == 1) { GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_SET); } else { GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_RESET); } } void SDA_Write(uint8_t Level) { if(Level == 1) { GPIO_WriteBit(GPIOA,GPIO_Pin_1,Bit_SET); } else { GPIO_WriteBit(GPIOA,GPIO_Pin_1,Bit_RESET); } } uint8_t SDA_Read(void) { if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_1) == Bit_SET) { return 1; } else { return 0; } } //延迟微妙 void Delay_us(uint32_t us) { uint32_t n = us*8; for(uint32_t i = 0;i < n; i++);//一次for循环1/8 us }2.2发送起始位和停止位函数
起始位:因为一开始SCL和SDA都是高电位(空闲),所以直接给SDA置低电位就可以
停止位:想要让SDA出现上升沿,但无法确定SDA的状态。所以先让SCL和SDA都置0,随后SCL置1,再SDA置1
void SendStart(void) { //假设SCL和SDA都是空闲——高电平 SDA_Write(0); Delay_us(1); } //SDA未知,所以先把SDA置低位才有上升沿 void SendStop(void) { //都置低位 SCL_Write(0); SDA_Write(0); Delay_us(1); //SCL置高位 SCL_Write(1); Delay_us(1); //SDA置高位 SDA_Write(1); Delay_us(1); }2.3发送一个字节
发送一个bit位
重点理解:SCL = 0,发送方准备发送数据。SCL = 1,接收方读取数据。
这里SCL先置0,是因为SCL = 1时改变SDA容易误认为起始位和停止位,所以SCL = 0时改变SDA。
发送一个字节代码:
特别注意:一个字节到一个bite 是如何转换的。
uint8_t SendByte(uint8_t Byte) { //发送一个字节,从高位向低位发 for(int8_t i = 7;i >= 0; i--) { //发送一个bite SCL_Write(0); if((Byte & (0x01<<i)) == 0) { SDA_Write(0); } else { SDA_Write(1); } Delay_us(1); SCL_Write(1); Delay_us(1); } //读取ACK或NAK SCL_Write(0);//SCL拉低,此时允许SDA改变 SDA_Write(1);//主机释放SDA,让从机控制 Delay_us(1); SCL_Write(1);//第9个时钟拉高,此时SDA上的应答有效 Delay_us(1); return SDA_Read();//返回从机的ACK或NAK }读取ACK和NAK也需要注意
核心是:主机释放SDA,从机拉低SDA回应。
主机想要释放SDA必须先SCL置0,然后SDA置1,紧接着SCL置1,读取SDA获得从机回应的ACK或NAK
3.SI2C读取一个字节
3.1读取一个bite
SCL_Write(0); // SCL拉低,进入数据准备阶段 SDA_Write(1);// 主机释放SDA,让从机控制SDA并放入要发送的0或1 Delay_us(10);// 等待一段时间,让SDA上的数据稳定 SCL_Write(1); // SCL拉高,进入数据读取阶段 Delay_us(10);// 等待信号稳定,保证读取的数据可靠 bit = SDA_Read();// 读取SDA电平,得到从机发送的1个bit(0或1)3.2读取一个字节
参数ACK表示主机想不想继续接收数据,1代表ACK继续,0代表NAK不继续
但是对于I2C的SDA来说,0代表继续,1代表停止
uint8_t ReceiveByte(uint8_t ACK) { uint8_t Byte = 0; for(int8_t i = 7;i >= 0; i--) { //读取一个bite SCL_Write(0);//SCL拉低,允许SDA改变 SDA_Write(1);//主机释放SDA,让从机控制 Delay_us(1); SCL_Write(1);//第1-8个时钟SCL拉高,SDA上的数据有效 Delay_us(1); //读取数据 if(SDA_Read() != 0) { Byte |= 0x01<<i;//给Byte的第i位赋1 } } //主机回复ACK/NAK SCL_Write(0);//SCL拉低,允许SDA改变 SDA_Write(!ACK);//ACK=1主机回应,对于SDA取反:SDA_Write(0) Delay_us(1); SCL_Write(1);//第9 个时钟SCL拉高,从机读取ACK/NAK Delay_us(1); return Byte; }4.SI2C发送数据
/** * @brief 软件I2C向从机连续发送多个字节数据 * @param Addr: I2C从设备7位地址 * @param TData: 待发送数据缓冲区指针 * @param Size: 需要发送的数据字节长度 * @retval int8_t 0:发送成功 -1:发送从机地址无应答 -2:发送数据时无应答 */ int8_t My_SI2C_SendBytes(uint8_t Addr,uint8_t *TData,uint16_t Size) { //1.发送I2C起始信号 SendStart(); //2.发送从机地址+写命令(Addr&0xFE 最低位清0代表写操作) if(SendByte(Addr & 0xfe) != 0) //SendByte返回非0表示从机无应答 { SendStop(); //发送停止位,结束本次通信 return -1; //寻址失败,返回错误码‑1 } //3.循环逐个发送缓冲区中的所有数据字节 for(uint16_t i = 0;i < Size; i++) { if(SendByte(TData[i]) != 0)//发送单字节,检测从机应答 { SendStop(); //出错则立即结束通信 return -2; //发送数据失败,返回错误码‑2 } } //4.全部数据发送完成,发送I2C停止信号,释放总线 SendStop(); return 0; //发送成功 }5.SI2C接收数据
/** * @brief 软件I2C从从机连续读取多个字节数据 * @param Addr: I2C从设备7位地址 * @param RData: 接收数据存放缓冲区指针 * @param Size: 需要读取的数据字节长度 * @retval int8_t 0:读取成功 -1:发送读地址无应答 */ int8_t My_SI2C_ReceiveBytes(uint8_t Addr,uint8_t *RData,uint16_t Size) { //1.发送I2C起始信号 SendStart(); //2.发送从机地址+读命令(Addr | 0x01 最低位置1代表读操作) if(SendByte(Addr | 0x01) !=0) //SendByte返回非0表示从机无应答 { SendStop(); //发送停止位,结束本次通信 return -1; //寻址失败,返回错误码‑1 } //3.接收前Size‑1个字节,读完每一字节主机回复ACK(继续读) for(uint16_t i = 0;i < Size-1; i++) { RData[i] = ReceiveByte(1);//参数1:主机发送应答ACK,通知从机继续发数据 } //读取最后1个字节,主机回复NACK,告诉从机不再读取数据 RData[Size-1] = ReceiveByte(0);//参数0:主机发送非应答NACK //4.全部数据接收完成,发送I2C停止信号,释放总线 SendStop(); return 0; //读取成功 }