STM32-I2C软件模拟相关个人思考
1.I2C介绍
I2C是一种多主机、两线制、低速串行通信总线,广泛用于微控制器和各种外围设备之间的通信。它使用两条线路:串行数据线(SDA)和串行时钟线(SCL)进行双向传输。

2.时序

启动条件:SCL高电平时、SDA由高电平变为低电平
停止条件:SCL高电平时、SDA由低电平变为高电平
除此之外,不允许在SCL高电平时变换SDA电平。
数据传输:
主机发送起始信号后,通信开始,每次传输一字节数据,传输完毕后应答(ACK)
主机拉低SCL电平,此时可改变SDA的电平
主机拉高SCL电平,此时开始读取SDA电平
如此重复8次,一字节数据传输完毕,数据接收方在下一个CLK低电平在SDA上进行应答,0代表数据接收成功,1数据接收失败或通信结束。
通信时序

如图,主机发送完起始信号后,再次拉低SCL,并改变SDA的电平为0或1,电平改变完成后,主机拉高(释放)SCL,代表此时从机可以读取SDA数据,之后主机再拉高SLC重复操作,如此重复8次,就发送了一个字节。
主机发送完字节最后一位数据后,拉高(释放)SCL等待从机读取完成,然后拉低SCL(代表主机准备好接受ACK)并且拉高(释放)SDA,将SDA的控制权交给从机,此时收到该数据的从机会对SDA进行操作,0(拉低SDA)代表存在从机收到该字节数据,可以继续发送数据,1(拉高,或者无从机操作SDA)代表从机数据接收完毕或根本没有从机,主机发送停止信号结束通信。
读取与写入数据
仔细研究以上通信时序,我们发现时序为
Start(起始信号)+8位数据+ACK(应答)+Stop(结束信号)
那么我们怎么知道主机跟从机通信是要读取数据还是写入数据呢?
实际使用中,我们会按照约定格式进行通信。
①读/写标志位

a.主机发送一个起始信号Start
这时通信总线上的设备准备接受数据
b.接着发送7位从设备ID+读写位(0写1读)
从机开始解析数据,发现该ID是自己ID那就发送ACK(0,代表有从机对SDA进行了操作)拉低SDA线,继续进行通信。
如果SDA保持1,即没有任何从机作出反应,那主机就发送Stop信号结束通信
c.发送要访问的从设备寄存器地址
主设备告诉从设备我要访问你该地址的数据,从设备收到地址后再次回应一个ACK(拉低SDA,告诉主设备我收到地址数据了)
d.根据之前发送发送的读写位进行数据交换
如果是7位地址+0,即写操作,则数据为主机发送给从机,从机接收到后回复一个ACK(0,即有从机拉低SDA),直到主机发送完成,产生一个Stop信号。
如果是7位地址+1,即读操作。则此时数据为从机发送给主机,主机接收完后向从机发送ACK位,当主机读取完成后就产生一个NACK(1),之后发送Stop信号结束通信。?
e.产生一个结束信号Stop
主机在SCL高时拉高SDA(本质上为释放SCL与SDA两条线路,即不对该两条线路产生任何操作)
看上去十分美好,但实际使用中我们会发现,只有写操作是按照以上时序,而读操作会先发送写时序再重发Start信号再进行读取,这是为什么呢?

为什么不能直接
Start + 7位地址+读写位 + 寄存器地址进行读写呢?
答:我也不知道
可以理解第一次写操作是为了给从器件发送寄存器地址,但为何读不能在发送从机地址+读标志位之后发送寄存器地址我暂时没在网络上找到该问题的权威答案,不过有部分解释说是为了给被访问的从机预留一点时间做准备,将自己的寄存器指针调整到主机想要访问的地址上去,我个人认为这个解释确实合理。
线与特性:
I2C的时钟线(SCL)与数据线(SDA)都使用开漏输出(OD)+上拉电阻的方式进行驱动,即线上连接的IO口全都只有拉低电平的能力,当线上连接的任意一个引脚输出低电平时,整条线路都会接地处于低电平状态。而IO口输出高电平时,引脚处于浮空状态,此时线上为高电平,但只要线上连接的任意一个IO口处于低电平,那整条线路都会处于低电平。
| 主机引脚电平 | 从机引脚电平 | 传输线电平 |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 1 | 1 |
为何使用开漏输出上拉电阻?
既然都是简单的电平变换,那为什么I2C要使用开漏输出呢?直接使用推挽输出,不但省下一个上拉电阻,IO口还不会因上拉电阻影响到传输速率不是更好吗?这是我们就要仔细研究一下I2C运用场景了。
I2C因为被设计运用在多主机多从机的场景,所以不可避免地会遇到一个问题——总线冲突。
若因某种原因时序混乱导致一个IO口输出高电平,一个输出低电平,那么就会出现短路现象,有烧坏设备的风险。
正点原子软件模拟I2C引脚使用推挽输出?
那么问题来了,正点原子例程中软件模拟I2C的GPIO配置代码如下:
GPIO_Initure.Pin=GPIO_PIN_4|GPIO_PIN_5;
GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP;
GPIO_Initure.Pull=GPIO_PULLUP;
GPIO_Initure.Speed=GPIO_SPEED_FREQ_VERY_HIGH;
我们惊讶的发现,他们将引脚的输出方式设置为了推挽输出,这是为什么?
查阅了资料,理由如下:
1.该例程的使用场景为单主-单从,即整条线上只有一个主机、一个从机,SCL不存在争抢控制权的现象。
2.该例程中在主机交出SDA控制权时,SDA引脚被设置为了输入模式
u8 IIC_Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;
SDA_IN();//SDA输入模式
for(i=0;i<8;i++ )
{
IIC_SCL(0);
delay_us(2);
IIC_SCL(1);
receive<<=1;
if(READ_SDA)receive++;
delay_us(1);
}
if (!ack)
IIC_NAck();
else`
IIC_Ack();
return receive;
}
输入模式时,主机SDA引脚浮空,只对SDA上的电平进行读取,此时从机改变SDA上的电平是安全的。
综上,使用推挽输出只适用于单主从模式,并且该方式需要对SDA输入输出状态进行切换。
但实际上,因为SCL完全由主机控制,因此还是有可能出现
问题探究:
1.推挽模式读取引脚状态需要改变引脚输入输出状态,开漏难道不需要吗?
请看GPIO基本构造:

如图所示STM32引脚输出靠下方的两个MOS管进行强上拉、下拉。当使用推挽输出时会用到两个MOS管,无论输出高电平还是低电平都会使引脚保持强上拉、下拉状态,因此引脚输出高电平就会读到高电平,引脚输出低电平就会读到低电平。
而使用开漏输出时只会用到下方连接GND的NMOS管,也就是说该模式只有强下拉,如果给引脚输出高电平,那么此时引脚实际处于浮空状态,受连接线上的电平影响,此时读取的电平即为连接线上的电平。
2.引脚输出模式下真的可以读取引脚状态吗?
在STM32参考手册中输出模式配置介绍中可找到相关内容,在引脚配置为输出模式时,输入所用的施密特触发器是打开的,因此可以读取引脚状态。

3.示例代码
//需delay函数进行延时
#include "myIIC.h"
/**
* @brief 初始化IO口
* @param
* @retval
*/
void myIIC_Init(void)
{
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitTypeDef IIC_GPIO_STU;
IIC_GPIO_STU.Mode=GPIO_MODE_OUTPUT_OD;//硬件含上拉电阻,使用开漏模式
IIC_GPIO_STU.Pin=GPIO_PIN_8 | GPIO_PIN_9;
IIC_GPIO_STU.Pull=GPIO_PULLUP;
IIC_GPIO_STU.Speed=GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB,&IIC_GPIO_STU);
SDA_H;//保证总线初始化后为高电平
SCL_H;
}
/**
* @brief 开始信号,SCL为高时SDA由高变低
* @param
* @retval
*/
void myIIC_Start(void)
{
SDA_H;
SCL_H;
delay_us(2);
SDA_L;
delay_us(2);
SCL_L;
}
/**
* @brief 结束信号,SCL为高时SDA由低变高
* @param
* @retval
*/
void myIIC_Stop(void)
{
SCL_L;
SDA_L;//保证SCL高时SDA为低,这样SDA才能从能由低变高
delay_us(2);
SCL_H;
delay_us(2);
SDA_H;
delay_us(2);
}
//发送一字节数据
void myIIC_SendByte(uint8_t Byte)
{
//注意,本示例的函数都在运行结束时由主机钳住SCL(即拉低SCL),因此SDA可以直接进行数据变换
for(uint8_t i=0;i<8;i++)//直接变换SDA,因为SCL此时必定为低
{
if(Byte&(0x80>>i))SDA_H;//数据的高位先进行传输,此处的位操作目的在于取出对应传输位的值
else SDA_L;
delay_us(2);
SCL_H;
delay_us(2);//延时以保证从机能读取到数据
SCL_L;//从机读取完毕后主机拉低SCL,准备传输数据下一位
}
}
uint8_t myIIC_GetAck(void)
{
uint8_t ack=1;
SCL_L;//可去除,因为myIIC_SendByte运行结束后SCL必定为低
SDA_H;//开漏输出高电平即为总线浮空状态,也就是交出SDA控制权,此时SDA由从机进行改变
delay_us(2);//等待从机进行改变
SCL_H;//拉高SCL,主机对SDA进行读取
delay_us(2);
ack=SDA_Read;//读取SDA值
delay_us(2);
SCL_L;//再次拉高SCL
return ack;//返回应答值
}
void myIIC_SendAck(uint8_t Byte)
{
SCL_L;//可不加
if(Byte)SDA_H;
else SDA_L;
SCL_H;
delay_us(2);//等待从机读取
SCL_L;
}
uint8_t myIIC_GetByte(void)
{
uint8_t temp=0;
SCL_L;//可不加,因位此时SCL肯定为低
for(uint8_t i=0;i<8;i++)
{
SDA_H;//主机交出SDA控制权
delay_us(2);//等待从机修改SDA
SCL_H;//准备读取SDA
if(SDA_Read)temp|=(0x80>>i);//读取SDA并利用位操作置位temp对应位
delay_us(2);
SCL_L;//告诉从机可改变SDA
}
return temp;
}

浙公网安备 33010602011771号