一、I2C 初始化函数
关于 软件写I2C,主机和从机为什么配置成开漏输出和外置弱上拉电阻模式
·主机和从机的输入,无所谓,怎么都能输入。
·主机和从机的输出,如果设置为推挽输出模式,输出高电平时为强上拉,输出低电平时强下拉。但如果没有配置好主机和从机的发送时序,二者同时发送,一个发送高电平一个发送低电平会造成短路。另外从机有多个,也会冲突。 所以重点关注输出!(输出模式下也是可以输入的)
·设备的SCL和SDA均采用开漏输出模式,且各自外接上拉电阻。
避免了引脚模式的频繁切换,同时兼具输入和输出的功能。
输出低电平时下管导通是强下拉,输出高电平时由于上边没有接。
所有设备只能输出低电平而不能输出高电平,为了避免高电平造成的引脚浮空,就接上拉电阻拉到高电平(因为是电阻拉高所以是弱上拉)
规定所有的人不准向上拉杆子,只能向下拉杆子或者放手。
若想输出低电平,就向下拉杆子,弹簧拉不过你。
若想输出高电平,就放手,弹簧弹回高电平。
第一,完全杜绝了电路短路,无论怎么拉杆子或者放手,杆子都不会出现被同时强拉或强推的状态。
第二,避免了引脚模式的频繁切换,开漏加弱上拉模式,同时兼具输入输出功能。 (开漏模式下输出高电平就相当于断开引脚,所以在输入之前可以直接输出高电平,不需要再切换成输出模式。)
//初始化 IIC 协议
void MyI2C_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
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);
GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11); //开始默认都置高电平
}
二、I2C 起始条件
开始时要释放SCL和SDA,即写1。然后再把它们拉低。
先释放SDA,再释放SCL。
原因:如果起始条件之前,二者均为高电平,那先释放哪一个都无所谓;
但是为了兼容重复起始条件sr,sr最开始的SCL是低电平,SDA电平不敢确定,为保险起见,趁SCL低电平时拉高SDA(否则SCL高电平期间读数据,读到的SDA有可能是0)
myi2c.c
//控制 SCL 时钟线,输出高电平还是低电平。
void MyI2C_W_SCL(uint8_t BitValue){
GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);//BitValue非0即1
Delay_us(10);
}
//控制 SDA 数据线,输出高电平还是低电平。
void MyI2C_W_SDA(uint8_t BitValue){
GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
Delay_us(10);
}
//读取 SDA 数据线,输入高电平还是低电平。
uint8_t MyI2C_R_SDA(void){
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
Delay_us(10);
return BitValue;
}
// 开始信号
void MyI2C_Start(void){
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
MyI2C_W_SDA(0);
MyI2C_W_SCL(0);
}
三、I2C 停止条件
如果SCL和SDA都是低电平,先释放SCL再释放SDA就行。
但是这个时序单元开始时,SDA不一定是低电平,所以要 MyI2C_W_SDA(0)
//停止信号
void MyI2C_Stop(void){
MyI2C_W_SDA(0);
MyI2C_W_SCL(1);
MyI2C_W_SDA(1);
}
三、I2C 发送一个字节
每个时序单元的SCL都是以低电平开始低电平结束,这样这些单元拼接起来时SCL才能续得上。
【SCL低电平期间,主机将数据按高位先行依次放到SDA上。SCL低电平期间,SDA可以变化】
如果发送0,就拉低SDA;如果想发送1,就释放SDA。
【然后主机释放SCL(即高电平),从机读取数据。SCL高电平期间,SDA不能变化】
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i ++){
MyI2C_W_SDA(Byte & (0x80 >> i));
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
}
- MyI2C_W_SDA(Byte & 0x80 )即趁SCL是低电平,先把Byte的最高位放在SDA线上,写1还是0,取决于Byte的最高位。**“按位与”**就可以取出来,Byte =XXXXXXXX,0x80=1000000。
0x80 >> i,右移i位。 - 释放SCL,即SCL变为高电平
- 拉低SCL,以备下次发送。
四、I2C 接收一个字节
为防止主机干扰,因为SDA为0的话,线与特性默认为0,从机此时无论发1还是0都为0,故主机这里将SDA置1放手。
从机想发1,就释放SDA;从机想发0,就拉低SDA。
//接受一个 Byte数据
uint8_t MyI2C_ReceiveByte(void){
uint8_t i, Byte = 0x00;
MyI2C_W_SDA(1);
for (i = 0; i < 8; i ++){
MyI2C_W_SCL(1);//SCL高电平期间,主机就能读取数据
if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}
MyI2C_W_SCL(0);
}
return Byte;
}
if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}
如果读到的为1,就把Byte第一位置为1。
读取一位之后,把SCL拉低,然后从机就可以发送下一位数据了。
五和六、I2C 发送应答和接收应答
其实是发送字节和接受字节的简化版,
发送字节发8位,而发送应答是发一位;
接收字节发8位,而发送应答是发一位;
//发送 ACK 应答信号
void MyI2C_SendAck(uint8_t AckBit){
MyI2C_W_SDA(AckBit);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
相当于去掉了发送一个字节中的for循环,然后把发送的一位改成AckBit。
//接收 ACK 应答信号
uint8_t MyI2C_ReceiveAck(void){
uint8_t AckBit;//不用赋值
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
AckBit = MyI2C_R_SDA();//
MyI2C_W_SCL(0);
return AckBit;
}