软件I2C-基于江科大源码进行的原理解析和改造升级

一、软件I2C的作用

软件I2C可以不用特定的端口,可以在I2C外设不够的时候使用,虽然没有硬件I2C的速度快,但是在一些要求低的工作中不足为谈

数据有效性:


I2C总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。
即:数据在SCL的上升沿到来之前就需准备好。并在在下降沿到来之
前必须稳定。

改变SCL和SDA线的状态

我选用的是PB0和PB1,给大家用非I2C外设GPIO口实践一下

/**
  * 函    数:I2C写SCL引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SCL的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平
  */
void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_0, (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_1, (BitAction)BitValue);		//根据BitValue,设置SDA引脚的电平,BitValue要实现非0即1的特性
	Delay_us(10);												//延时10us,防止时序频率超过要求
}

读SDA数据线状态函数

/**
  * 函    数:I2C读SDA引脚电平
  * 参    数:无
  * 返 回 值:协议层需要得到的当前SDA的电平,范围0~1
  * 注意事项:此函数需要用户实现内容,当前SDA为低电平时,返回0,当前SDA为高电平时,返回1
  */
uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1);		//读取SDA电平
	Delay_us(10);												//延时10us,防止时序频率超过要求
	return BitValue;											//返回SDA电平
}

GPIO初始化

这里最后把两条线的电平都置1了,这是将I2C保持空闲状态了

/**
  * 函    数: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_0 | GPIO_Pin_1;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);					//将PB0和PB1引脚初始化为开漏输出
	
	/*设置默认电平*/
	GPIO_SetBits(GPIOB, GPIO_Pin_0 | GPIO_Pin_1);			//设置PB0和PB1引脚初始化后默认为高电平(释放总线状态)
}

起始信号与停止信号

首先要保证,一定是SCL保持高电平期间,发生的跳变,才会被视为起始或者停止信号

/**
  * 函    数: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发送字节

发送字节的时候,时钟线为高电平期间,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,主机开始发送下一位数据
	}
}

代码详解

  1. 循环初始化
for (i = 0; i < 8; i++)

这个循环将运行8次,对应于一个字节中的8位。

  1. 取出数据位
MyI2C_W_SDA(Byte & (0x80 >> i));
    • 0x80 是二进制 10000000,表示最高位是1。
    • 0x80 >> i0x80 右移 i 位,得到一个新的掩码,这个掩码只有一个位是1,其余位都是0。
    • Byte & (0x80 >> i) 通过与操作来检查 Byte 的第 i 位是否为1。如果 Byte 的第 i 位是1,则结果为1;否则为0。
    • MyI2C_W_SDA(...) 函数根据上述结果设置 SDA 线的状态(1或0)。
  1. 拉高SCL
MyI2C_W_SCL(1);
    • 将 SCL 线拉高。此时,从机会读取 SDA 线上的状态。如果 SDA 为高电平,则读取到的数据位是1;如果 SDA 为低电平,则读取到的数据位是0。
  1. 拉低SCL
MyI2C_W_SCL(0);
    • 将 SCL 线拉低,表示当前数据位传输完成。此时可以安全地改变 SDA 线的状态,以准备传输下一个数据位。

I2C接收一个字节

/**
  * 函    数: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;							//返回接收到的一个字节数据
}

代码详解

  1. 变量初始化
uint8_t i, Byte = 0x00;
    • i 是循环计数器。
    • Byte 用于存储接收到的数据,并初始化为 0x00。这是因为我们需要一个干净的起点来构建最终的字节。
  1. 释放SDA
MyI2C_W_SDA(1);
    • 在开始接收数据之前,主机将 SDA 线拉高。这是为了确保 SDA 线处于高阻态,不会干扰从机的数据发送。
  1. 循环接收每一位
for (i = 0; i < 8; i++)
    • 这个循环会运行8次,对应于一个字节中的8位。
  1. 拉高SCL
MyI2C_W_SCL(1);
    • 将 SCL 线拉高。此时,从机会将数据写入 SDA 线上。
  1. 读取SDA并更新Byte
if (MyI2C_R_SDA() == 1) {  // 读取SDA数据
    Byte |= (0x80 >> i);  // 如果SDA为1,则将Byte的第i位置1
}
    • MyI2C_R_SDA() 函数读取 SDA 线的状态。
    • 如果 SDA 为高电平(逻辑1),则使用按位或操作将 Byte 的第 i 位置1。
    • 如果 SDA 为低电平(逻辑0),则 Byte 的第 i 位保持不变(即保持为0,因为 Byte 已经被初始化为 0x00)。
  1. 拉低SCL
MyI2C_W_SCL(0);
    • 将 SCL 线拉低,表示当前数据位已经接收完成。此时可以安全地改变 SDA 线的状态,以准备接收下一个数据位。
  1. 返回接收到的数据
return Byte;
    • 在所有8位都接收完成后,函数返回最终构建的字节 Byte

发送应答位

应答位一般会在发送数据的第八位后的第九个时钟期间产生

代码解析

1. 设置SDA线
MyI2C_W_SDA(AckBit);
  • MyI2C_W_SDA 是一个假设的函数,用于设置 SDA 线的状态。
  • AckBit 是要发送的应答位,可以是0或1。
    • 如果 AckBit 为0,表示应答(ACK),SDA 线会被拉低。
    • 如果 AckBit 为1,表示非应答(NACK),SDA 线会保持高电平。
2. 拉高SCL线
MyI2C_W_SCL(1);
  • MyI2C_W_SCL 是一个假设的函数,用于设置 SCL 线的状态。
  • 将 SCL 线拉高,使从机能够在 SCL 高电平期间读取 SDA 线上的状态。
  • 在 I2C 协议中,接收方(从机)会在 SCL 的上升沿(即 SCL 从低电平变为高电平)时读取 SDA 线上的状态。
3. 拉低SCL线
MyI2C_W_SCL(0);
  • 将 SCL 线拉低,结束当前的应答位传输,并准备进行下一个时序模块。
  • 这一步确保了 SCL 线在下一次数据传输之前处于低电平状态,以便正确地进行后续的操作。

工作原理

在 I2C 通信中,每个字节的数据传输完成后,接收方需要发送一个应答位来确认数据是否被成功接收。这个应答位是在 SCL 为高电平时通过 SDA 线传输的。

  1. 应答位(ACK)
    • 当接收方成功接收到一个字节后,它会在下一个 SCL 高电平期间将 SDA 拉低,表示应答。
    • 应答位为0(低电平)。
  1. 非应答位(NACK)
    • 如果接收方没有准备好接收更多数据,或者检测到错误,它会在下一个 SCL 高电平期间保持 SDA 为高电平,表示非应答。
    • 非应答位为1(高电平)。

接收应答位

/**
  * 函    数: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;							//返回定义应答位变量
}

代码解析

1. 定义应答位变量
uint8_t AckBit;
  • AckBit 用于存储接收到的应答位,初始化为未定义状态。
2. 释放SDA线
MyI2C_W_SDA(1);
  • MyI2C_W_SDA 是一个假设的函数,用于设置 SDA 线的状态。
  • 在接收应答位之前,主机将 SDA 线拉高,以避免干扰从机的数据发送。
  • 这一步确保了 SDA 线处于高阻态,允许从机控制 SDA 线的状态。
3. 拉高SCL线
MyI2C_W_SCL(1);
  • MyI2C_W_SCL 是一个假设的函数,用于设置 SCL 线的状态。
  • 将 SCL 线拉高,使从机能够在 SCL 高电平期间将应答位写入 SDA 线。
  • 在 I2C 协议中,从机会在 SCL 的上升沿(即 SCL 从低电平变为高电平)时将应答位放到 SDA 线上。
4. 读取SDA线
AckBit = MyI2C_R_SDA();
  • MyI2C_R_SDA 是一个假设的函数,用于读取 SDA 线的状态。
  • 读取 SDA 线上的状态并存储到 AckBit 变量中。
  • 如果 SDA 为低电平(逻辑0),则表示应答(ACK);如果 SDA 为高电平(逻辑1),则表示非应答(NACK)。
5. 拉低SCL线
MyI2C_W_SCL(0);
  • 将 SCL 线拉低,结束当前的应答位接收,并准备进行下一个时序模块。
  • 这一步确保了 SCL 线在下一次数据传输之前处于低电平状态,以便正确地进行后续的操作。
6. 返回应答位
return AckBit;
  • 返回接收到的应答位 AckBit

工作原理

在 I2C 通信中,每个字节的数据传输完成后,从机需要发送一个应答位来确认数据是否被成功接收。这个应答位是在 SCL 为高电平时通过 SDA 线传输的。

  1. 应答位(ACK)
    • 当从机成功接收到一个字节后,它会在下一个 SCL 高电平期间将 SDA 拉低,表示应答。
    • 应答位为0(低电平)。
  1. 非应答位(NACK)
    • 如果从机没有准备好接收更多数据,或者检测到错误,它会在下一个 SCL 高电平期间保持 SDA 为高电平,表示非应答。
    • 非应答位为1(高电平)。

源码

myI2C.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

/*引脚配置层*/

/**
  * 函    数:I2C写SCL引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SCL的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平
  */
void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_0, (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_1, (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_1);		//读取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_0 | GPIO_Pin_1;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);					//将PB10和PB11引脚初始化为开漏输出
	
	/*设置默认电平*/
	GPIO_SetBits(GPIOB, GPIO_Pin_0 | GPIO_Pin_1);			//设置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;							//返回定义应答位变量
}

myI2C.h

#ifndef __MYI2C_H
#define __MYI2C_H

void MyI2C_Init(void);
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_SendByte(uint8_t Byte);
uint8_t MyI2C_ReceiveByte(void);
void MyI2C_SendAck(uint8_t AckBit);
uint8_t MyI2C_ReceiveAck(void);

#endif
科大自化协软件模拟I2C读写MPU6050的过程可以分为两个主要步骤。首先,需要学习I2C协议规则,并使用软件模拟的方式实现协议。这包括了了解I2C协议的引脚配置、时序要求等相关内容。其次,需要学习STM32I2C外设,并使用硬件实现协议。这一步骤可以进一步学习STM32I2C外设和协议的硬件实现方式。\[1\]\[2\] 在软件模拟I2C读写MPU6050的过程中,可以采用多层的模块架构。最底层是I2C协议层,主要关注引脚配置、时序要求等与协议相关的内容。其次是MPU6050的驱动层,主要关注如何读写寄存器、配置寄存器以及读取数据等与驱动相关的内容。最后是主函数应用层,通过调用MPU6050的驱动函数获取数据,并根据需要进行进一步的功能设计。\[3\] 因此,科大自化协软件模拟I2C读写MPU6050的过程包括学习I2C协议规则、软件模拟实现协议,以及学习STM32I2C外设并使用硬件实现协议。在软件模拟过程中,需要关注引脚配置、时序要求等与协议相关的内容。在硬件实现过程中,需要学习STM32I2C外设和协议的硬件实现方式。最后,通过多层的模块架构,实现对MPU6050的读写操作,并根据需要进行进一步的功能设计。 #### 引用[.reference_title] - *1* *2* *3* [【STM32学习】——I2C通信协议&MPU6050姿态传感器&软件I2C读写MPU6050](https://blog.csdn.net/weixin_51658186/article/details/129821841)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值