目录
1. IIC协议简介
1.1 主从模式
1.2 2根通信线
2. IIC协议时序
2.1 起始条件和终止条件
2.2 发送一个字节
2.3 接收一个字节
2.4 应答信号
1. IIC协议简介
IIC协议是一个半双工、同步、一主多从、多主多从的串行通用数据总线。该通信模式需要2根线:SCL、SDA,即时钟线和数据线。
1.1 主从模式
IIC协议支持一主多从和多主多从,每个设备都有唯一的地址。
1.2 2根通信线
SDA:数据线,用于传输数据
SCL:时钟线,用于同步数据传输
接线时所有IIC设备的SCL连在一起,SDA连在一起,不同的设备,都是并联接在这两条线上(设备之间“线与”关系),I2C总线上的每个设备都自己一个唯一的地址,来确保不同设备之间访问的准确性。设备的SCL的SDA均要配置成开漏输出模式,SCL和SDA需要各添加一个上拉电阻,阻值一般为4.7KΩ左右。
这里补充一下,因为有看到过一个主机一个从机的情况下,可以设置为推挽输出模式,但是在一主多从,或者多主多从的情况下,推荐开漏输出,原因请看下文。
为了能理解为什么在IIC协议总线上,IO口模式需要设置为开漏输出模式,而不能使用推挽输出模式,可以看下GPIO口的硬件结构:
看输出驱动器部分,可以看到使用了两个MOS管,分别是P-MOS和N-MOS管,这些是STM32 GPIO输出模式的关键元件。在推挽输出模式下,P-MOS管和N-MOS管都工作,通过控制这些管的开关状态来实现高电平和低电平的输出。在开漏输出模式下,只有N-MOS管工作,用于输出低电平,而高电平的输出则需要通过外部上拉电阻实现。
推挽输出模式:
推挽输出结构是由两个MOS收到互补控制的信号控制。推挽输出的最大特点是可以真正能真正的输出高电平和低电平,在两种电平下都具有驱动能力。推挽输出模式中,N-MOS管和P-MOS管都工作,如果我们控制输出为0(低电平),则P-MOS管关闭,N-MOS管导通,输出低电平;若控制输出为1(高电平),则P-MOS管导通,N-MOS管关闭,输出高电平。外部上拉和下拉的作用是控制在没有输出时的IO口电平。
优点:驱动能力强,电平切换能力快(根据GPIO的波特率可用作模拟其他协议)。
缺点:多个推挽输出端口相连时,由于通路上阻抗较小电流会从IO的VDD流向另一个IO的GND,会发生短路进而对端口造成伤害。(这里就解释了为什么IIC不能使用推挽输出模式,如果多个从机都接到SDA线上,一个机器发送数据0,另一个机器发送数据1,则可能会发生短路进而毁坏IIC器件)。
开漏输出模式:
开漏输出时只有N-MOS管工作,只能输出低电平。当其输出高电平时没有驱动能力(电压会被外部阻抗拉低),需要借助外部上拉电阻完成对外驱动(通断N-MOS实现对路径上的电压控制),驱动能力取决于上拉电阻阻值。
如果我们控制输出为0(低电平),则P-MOS管关闭,N-MOS管导通,输出低电平;若控制输出为1(高电平),则P-MOS管和N-MOS管都关闭,输出指令就不会起到作用,此时I/O端口的电平就不由输出的高低电平决定,而是由I/O端口外部的上拉或者下拉决定。如果没有上拉或者下拉 IO口就处于悬空状态。
半双工:一根数据线,这根数据线既可以发送数据,也可以接收数据,但是不能同时发送和接收,所以叫做半双工通信。
代码如下:注意开漏输出模式
// PB11-->SDA
// PB10-->CLK
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引脚初始化后默认为高电平(释放总线状态)
}
2. IIC协议时序
2.1 起始条件和终止条件
IIC需要起始信号和终止信号。
/**
* 函 数: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写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,防止时序频率超过要求
}
2.2 发送一个字节
/**
* 函 数: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,主机开始发送下一位数据
}
}
2.3 接收一个字节
/**
* 函 数: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读SDA引脚电平
* 参 数:无
* 返 回 值:协议层需要得到的当前SDA的电平,范围0~1
* 注意事项:此函数需要用户实现内容,当前SDA为低电平时,返回0,当前SDA为高电平时,返回1
*/
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.4 应答信号
/**
* 函 数: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; //返回定义应答位变量
}