在b站上学习江协科技官方视频——STM32入门教程,对I2C通信协议进行总结。
学习视频:[10-3] 软件I2C读写MPU6050_哔哩哔哩_bilibili
软件IIC协议程序架构
IIC通信协议 MyI2C.c 程序由 GPIO初始化(I2C初始化)、GPIO基本读写函数(I2C写SCL引脚电平、I2C写SDA引脚电平、I2C读SDA引脚电平)以及6个时序基本单元(起始条件、终止条件、发送一个字节、接收一个字节、发送应答、接收应答)组成。
1. GPIO初始化、GPIO基本读写函数
其中 GPIO_SetBits(GPIOx, GPIO_Pin_x); // 将引脚设置为高电平
GPIO_ResetBits(GPIOx, GPIO_Pin_x); // 将引脚设置为低电平
GPIO_WriteBit(GPIOx, GPIO_Pin_x, (BitAction)BitValue); // 将引脚设置为指定的电平
GPIO_ReadInputDataBit(GPIOx, GPIO_Pin_x); // 读取引脚的电平
// GPIO初始化
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写SCL引脚电平
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue); //根据BitValue,设置SCL引脚的电平
Delay_us(10); //延时10us,防止时序频率超过要求
}
// I2C写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引脚电平
uint8_t MyI2C_R_SDA(void)
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11); //读取SDA电平
Delay_us(10); //延时10us,防止时序频率超过要求
return BitValue; //返回SDA电平
}
2. 6个时序基本单元——起始条件
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也拉低,即为了占用总线,也为了方便总线时序的拼接
}
3. 6个时序基本单元——终止条件
void MyI2C_Stop(void)
{
MyI2C_W_SDA(0); //拉低SDA,确保SDA为低电平
MyI2C_W_SCL(1); //释放SCL,使SCL呈现高电平
MyI2C_W_SDA(1); //在SCL高电平期间,释放SDA,产生终止信号
}
这里需要注意的是,为什么起始条件函数中,为什么要先将SDA置高电平,再将SCL置1 ? 我们观察一下终止条件函数里面,如果SDA原本就是低电平,先将SCL置1,再SDA置1, 就与终止条件函数一致了。这会导致在整个时序图中会分不清这一小段是 终止条件 还是 重复起始条件。
4. 6个时序基本单元——I2C 发送一个字节
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
MyI2C_W_SDA(Byte & 0x80); //取出Byte的最高位数据并写入到SDA线
MyI2C_W_SCL(1); //释放SCL,从机在SCL高电平期间读取SDA
MyI2C_W_SCL(0); //拉低SCL,主机开始发送下一位数据
MyI2C_W_SDA(Byte & 0x40); //取出Byte的最高位数据并写入到SDA线
MyI2C_W_SCL(1); //释放SCL,从机在SCL高电平期间读取SDA
MyI2C_W_SCL(0); //拉低SCL,主机开始发送下一位数据
MyI2C_W_SDA(Byte & 0x20); //取出Byte的最高位数据并写入到SDA线
MyI2C_W_SCL(1); //释放SCL,从机在SCL高电平期间读取SDA
MyI2C_W_SCL(0); //拉低SCL,主机开始发送下一位数据
......
}
// 循环写法
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,主机开始发送下一位数据
}
}
这里需要注意的是,在SCL为高电平的时候不允许SDA输入数据。因此 使用 MyI2C_W_SDA(Byte & (0x80 >> i)); 来将数据写入SDA,会在SCL是低电平时写入,具体什么时候写在程序上不用去管它。最后将SCL置1再置0进行一次读取。
这里的 & 运算操作是将最高位提取出来,比如:
设Byte = 1010 1001 , Byte & 0x80 = 1010 1001 & 1000 0000 = 1000 0000
Byte & 0x40 = 1010 1001 & 0100 0000 = 0000 0000
又因为MyI2C_W_SDA(uint8_t BitValue)的(BitAction)BitValue 是一个枚举类型,只能写入0或1。
于是就可以将Byte从最高位依次写入到SDA上了。
5. 6个时序基本单元——I2C 接收一个字节
I2C接收一个字节,需要在数据传输过程中,主机需要释放SDA,从机将数据写入SDA中(同样不用管)。在SCL置高电平期间,主机读取SDA的数据保存下来。
于是对于主机操作而言可分为:释放SDA + 在SCL期间读取并保存SDA上的值
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; //返回接收到的一个字节数据
}
需要注意的是,if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);} 是判断SDA上数据字节哪个位置的值是否为1,若是 则将该位置(0x80 >> i) 置1保存到 Byte 中。
Byte |= 0x80 :就一位而言,与操作 & 可以看做是乘法运算,如 1 & 1 = 1 , 1 & 0 = 0 , 0 & 0 = 0
或操作 | 可以看做是加法运算,如 0 | 0 = 0 ,1 | 0 = 1 ,0 | 1 = 1 , 1 | 1 = 1
设Byte = 1010 1000 , Byte | 0x80 = 1010 1001 | 1000 0000 = 1010 1001
Byte | 0x40 = 1010 1001 | 0100 0000 = 1110 1001
6. 6个时序基本单元——发送应答、接收应答
应答(ACK)是告诉对方是否继续进行数据传输。若应答置0,表示继续;应答置1,表示结束。
对于主机而言,主机在发送完一个字节后,将应答信号放在SDA上,ACK=0,表示发送完毕;ACK=1,表示继续发送。
主机在接收完一个字节后,释放SDA(避免干扰从机发送ACK),从机把应答信号放在SDA上,在读取数据期间(SCL=1,SCL=0),赋值最后返回读取的ACK值。
程序部分相当于I2C 发送一个字节 和 I2C 接收一个字节的一次循环:
// I2C发送应答位
void MyI2C_SendAck(uint8_t AckBit)
{
MyI2C_W_SDA(AckBit); //主机把应答位数据放到SDA线
MyI2C_W_SCL(1); //释放SCL,从机在SCL高电平期间,读取应答位
MyI2C_W_SCL(0); //拉低SCL,开始下一个时序模块
}
// I2C接收应答位
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; //返回定义应答位变量
}