Bootstrap

基于STM32——软件模拟 IIC 通信(b站江协科技视频学习)

在b站上学习江协科技官方视频——STM32入门教程,对I2C通信协议进行总结。

学习视频:[10-3] 软件I2C读写MPU6050_哔哩哔哩_bilibili

软件IIC协议程序架构

软件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;							//返回定义应答位变量
}

仅供学习参考,侵删!
;