目录
1. 软件I2C代码编写
由于软件I2C不受引脚限制,随便找两个普通的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通讯的时序进行配置软件I2C,了解I2C通讯:
1.1 I2C起始
首先配置起始和终止条件,根据I2C通讯的时基单元我们可以了解到,起始条件下,SCL高电平期间,SDA从高电平切换到低电平;终止条件下,SCL高电平期间,SDA从低电平切换到高电平。
方法一
我们可以通过GPIO_ResetBits()和GPIO_SetBits()的方式来控制引脚的电平,假如我们要将,PB10配置成SCL引脚,PB11配置成SDA引脚,则我们可以编写代码:
void MyI2C_Start(void)
{
GPIO_SetBits(GPIOB, GPIO_Pin_11); //释放SDA,确保SDA为高电平
GPIO_SetBits(GPIOB, GPIO_Pin_10); //释放SCL,确保SCL为高电平
GPIO_ResetBits(GPIOB, GPIO_Pin_11); //在SCL高电平期间,拉低SDA,产生起始信号
GPIO_ResetBits(GPIOB, GPIO_Pin_10); //起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
}
方法二
或者我们可以采用宏定义的方法,先对PB10和PB11的高低电平状态进行宏定义:
#define SDA_H() GPIO_SetBits(GPIOB, GPIO_Pin_11);
#define SCL_H() GPIO_SetBits(GPIOB, GPIO_Pin_10);
#define SDA_L() GPIO_ResetBits(GPIOB, GPIO_Pin_11);
#define SCL_L() GPIO_ResetBits(GPIOB, GPIO_Pin_10);
然后使用定义:
void MyI2C_Start(void)
{
SDA_H(); //释放SDA,确保SDA为高电平
SCL_H(); //释放SCL,确保SCL为高电平
SDA_L(); //在SCL高电平期间,拉低SDA,产生起始信号
SCL_L(); //起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
}
方法三
另一种有参宏定义:
#define MyI2C_W_SDA(x); GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)(x));
#define MyI2C_W_SCL(x); GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)(x));
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写SCL引脚电平:
其中,BitValue 协议层传入的当前需要写入SCL的电平,范围0~1,此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平:
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_10, (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_11, (BitAction)BitValue); //根据BitValue,设置SDA引脚的电平,BitValue要实现非0即1的特性
Delay_us(10); //延时10us,防止时序频率超过要求
}
调用以上封装函数:
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也拉低,即为了占用总线,也为了方便总线时序的拼接
}
1.2 I2C终止
这里我们可以直接调用方法四中的封装:
void MyI2C_Stop(void)
{
MyI2C_W_SDA(0); //拉低SDA,确保SDA为低电平
MyI2C_W_SCL(1); //释放SCL,使SCL呈现高电平
MyI2C_W_SDA(1); //在SCL高电平期间,释放SDA,产生终止信号
}
1.3 发送一个字节
发送一个字节: SCL低电平期间,主机将数据位依次放到SDA线上(高位先行) ,然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。
Byte 要发送的一个字节数据,范围:0x00~0xFF,通过与0x80按位与的方式取出最高位数据,由于我们在方法四的封装函数void MyI2C_W_SDA(uint8_t BitValue);其中BitValue的取值为非0即1的特性,因此我们可以直接使用MyI2C_W_SDA(Byte & 0x80);通过与上不同的位得到一个字节数据,代码如下:
void MyI2C_SendByte(uint8_t Byte)
{
MyI2C_W_SDA(Byte & 0x80);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
MyI2C_W_SDA(Byte & 0x40);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
MyI2C_W_SDA(Byte & 0x20);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
MyI2C_W_SDA(Byte & 0x10);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
MyI2C_W_SDA(Byte & 0x08);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
MyI2C_W_SDA(Byte & 0x04);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
MyI2C_W_SDA(Byte & 0x02);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
MyI2C_W_SDA(Byte & 0x01);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
我们可以通过for循环简化上述操作,通过对0x80右移实现上述操作:
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.4 接收一个字节
SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)。
I2C读SDA引脚电平:
我们需要先创建一个读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电平
}
此函数需要用户实现内容,当前SDA为低电平时,返回0,当前SDA为高电平时,返回1。
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.5 发送应答
发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答。
void MyI2C_SendAck(uint8_t AckBit)
{
MyI2C_W_SDA(AckBit); //主机把应答位数据放到SDA线
MyI2C_W_SCL(1); //释放SCL,从机在SCL高电平期间,读取应答位
MyI2C_W_SCL(0); //拉低SCL,开始下一个时序模块
}
1.6 接收应答
接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)。
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; //返回定义应答位变量
}