STM32单片机初学4-IIC通信(软件模拟)

IICInter-Integrated Circuit)又称I2C(习惯读“I方C”),是IICBus简称,中文名为集成电路总线,它是一种串行通信总线,使用多主从架构,由飞利浦公司在1980年代为了让主板嵌入式系统手机用以连接低速周边设备而发展。适用于IC间的短距离数据传输。

最初的IIC通信速率只有100Kbps(12.5KB/s),后来又发展到400Kbps(50KB/s),再后来发展到1Mbps和3.4Mbps(高速模式)。速度越快,对通信设备的要求就越高,所以很多IIC通信设备并不支持高速模式。

IIC是单片机最常用的通讯方式之一,如果单片机需要扩展EEPROM存储芯片,就会用到IIC通信来读写EEPROM的数据。

除此很多传感器(如温湿度传感器,加速度传感器等)、外设(如OLED屏幕、LCD显示板等)也会采用IIC与单片机进行数据的传输。

所以IIC是单片机中必不可少的学习内容。

本文标题特别强调了“软件模拟”IIC通信,那是不是也有硬件IIC通信?是的。

硬件IIC:我们知道,STM32外设丰富,包括硬件级的IIC通信,也就是在其内部集成有专门用于IIC通信的硬件模块,只要在程序中配置相关的参数,就能启用该模块,再通过官方已经做好的库函数,就能直接实现IIC通信了。

软件模拟IIC则是不使用内部的IIC通信模块,也不使用官方的库函数,完全靠软件模拟来实现IIC的通信方式。

由于STM32的硬件IIC存在着应答信号卡死的问题,所以很多开发者更喜欢使用软件模拟IIC通信。另外,软件模拟IIC还有方便移植到其他设备的优点,且可以自己封装想要的功能函数,相比硬件IIC使用更灵活。

那么软件模拟IIC到底是如何实现的呢?

首先知道一点,不管何种有线通信方式,都是通过通信线传输高低电平信号实现的。数据的最小单位为bit,1个bit能表示0或者1,高低电平正好能代表这两种状态。高低电平不断变换,就能表示不断变化的0和1,将其连起来,就组成了一连串的数据。

所以理论上,只要是能产生高低电平信号的东西,就能用来通信。而单片机的IO口不就能输出高低电平,所以只要我们通过程序让IO口按照一定的规律输出高低电平,就能实现通信了。这样的通信方式就称为软件模拟通信。上面提到的规律,就是通信协议。

为了让数据发送端发送的数据能被接收端识别,必须约定一种特定的协议,双方都按照协议的规定发送或者接受数据,我们把这种协议叫做通信协议。

通常,我们所说的通信方式既包括物理接口也包括其通信协议

物理接口:通俗地说就是要几根通信线,各通信线的定义与接法,比如IIC需要SCL与SDA两根通信线,缺少任何一根线都是无法通信的。

通信协议:包括各种信号、数据帧形式等定义。发送端将要发送的数据按照协议规定重新排列再发出去,接收端则按照协议将接收到的数据进行解析,得到正确的数据。未按照协议规定发送的数据接收端认为是无效数据,不做处理。

这就是不同通信方式的设备之间不能通信的原因。不同的通信方式有不同的通信协议,但可能采用相同的通信接口。比如RS485与RS422物理接口相同,但内部使用的通信协议可以不一样;RS232则与前两者的物理接口也不一样。

所以建立通信的条件有:物理接口,通信协议。

现在我们回到IIC通信。

IIC物理接口:IIC串行总线有两根信号线,一根是双向的数据线SDA,另一根是时钟线SCL。所有接到I2C总线设备上的串行数据SDA都接到总线的SDA上,各设备的时钟线SCL接到总线的SCL上,属于多主多从总线。主机一次只能跟一个从机通信,如果要跟多个从机传输数据,则需要采用轮询的方式。通常会给SDA和SCL外接上拉电阻,确保无数据传输时,总线处于闲置状态。但是不接也可以,可以使用STM32的片内上拉电阻。

这两根线中,SCL是时钟线,用来同步时钟信号;SDA是数据线,用来传输数据。

如果是硬件IIC,那么这两根线必须接到STM32的特定引脚上,以STM32F03C8T6为例,其IIC2的SCL为PB10,SDA为PB11,那从机的SCL就只能接在PB10,SDA只能接在PB11。

 但对于软件模拟IIC,就没有任何限制,你可以定义任意两个IO口分别作为SCL与SDA。

IIC通信协议:IIC通信协议较简单,主要由起始信号、数据帧、应答信号,停止信号组成。

其发送数据的形式如下。

 传输数据前,STM32先产生一个起始信号,各从机接收到该信号后就知道主机要发送数据了,就会做好接受数据的准备。然后单片机开始发送一串数据(8位二进制数),然后进入等待应答状态,从机接收这串数据后,会返回一个应答信号给单片机,告诉单片机刚才的数据我已经接收到了。单片机收到应答信号后继续发送下一个数据帧,然后又进入等待应答状态,直到收到从机的应答信号。

一直循环下去,直到所有数据发送完。然后,单片机会发送一个停止信号,告诉从机,我的数据已经发送完了,你们可以休息了。从机收到停止信号后,就会停止接收数据,除非再次收到起始信号。当然,如果是与一个从机建立通信,发送完数据后也可以不发送停止信号,让通信一直保持,下次再发送数据就不需要再发送起始信号了。

但如果有多个从机的话,不发送停止信号的话会让总线一直处于占用状态,导致其他从机无法与单片机通信(就好像跟女朋友打电话聊完了要及时挂断,否则你的好兄弟就无法呼叫你)。所以多从机下,发完数据就要及时发出停止信号。

接下来详细讲解各个信号以及如何在程序里实现。

首先我们这里先定义SCL与SDA的引脚,以及两个引脚置高、置低的函数。

将PA1作为SCL,PA2作为SDA。

#define IIC_SCL_H() GPIO_SetBits(GPIOA,GPIO_Pin_1)	    //定义SCL置高为PA1置位		
#define IIC_SCL_L() GPIO_ResetBits(GPIOA,GPIO_Pin_1)    //定义SCL置低为PA1复位				

#define IIC_SDA_H() GPIO_SetBits(GPIOA,GPIO_Pin_2)		//定义SDA置高为PA2置位			
#define IIC_SDA_L() GPIO_ResetBits(GPIOA,GPIO_Pin_2)	//定义SDA置低为PA2复位		

延时函数:

void Delay_us(int Time)
{
	int i=0;
	while(Time--)
	{
		i=8;
		while(i--);
	}
	return;
}

由于SDA需要在输入输出模式之间切换(等待应答时需要将PA2切换成输入模式),所以还要定义这两个函数。输出模式用推挽输出即可,输入模式采用上拉输入模式(如果总线外接了上拉电阻,采用浮空输入也可以)。

关于STM32GPIO的模式可参考文章STM32_GPIO详解

void SDA_OUT(void)    //SDA输出模式
{
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin= GPIO_Pin_2;
    GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;        //推挽输出模式
    GPIO_Init(GPIOA, &GPIO_InitStructure);
}


void SDA_IN(void)       //SDA输入模式
{
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin= GPIO_Pin_2;
    GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;			//上拉输入模式
	GPIO_Init(GPIOA, &GPIO_InitStructure);
}

起始信号:

发送起始信号之前,肯定要保证IIC总线处于空闲状态,如果总线正忙,起始信号肯定不能起作用的。所以先说说IIC的空闲状态。

IIC的空闲状态下,SCL与SDA都处于高电平。这种状态下,总线被释放(单片机从机都没有拉低总线),无数据传输。

起始信号是SCL处于高电平时,SDA出现下降沿。如下图虚线框内所示。

图中电平变化的坡度是为了便于理解,实际上的坡度不明显。

用程序实现如下


void IIC_Start(void)
{
    SDA_OUT();		//SDA切换至输出模式
    IIC_SDA_H();		//SDA置高
    IIC_SCL_H();		//SCL置高
    Delay_us(1);
    IIC_SDA_L();		//SDA置低
    Delay_us(1);
}

程序中,SDA与SCL都置高后,延时了1us再将SDA置低,这是为了保证SDA置低时,SCL处于高电平状态(单片机发出置高的指令后,IO口并不能立马置高,会有短暂的延时),同时也是给IIC从机有足够的时间采集总线电平状态。像一些低速IIC设备,他们采集电平状态速度较慢,主机就得跟着减缓发送数据的速度,即加大电平变换时的延时。而对于高速IIC设备,则可以减小延时。这也是制约IIC通信速率的因素之一。 

停止信号:

停止信号是SCL处于高电平,SDA出现上升沿。如下图虚线框内所示。

 可以看到,停止信号过后,SDA与SCL都处于高电平状态,正好就是总线空闲状态。

用程序实现停止信号如下:

void IIC_Stop(void)
{
    IIC_SCL_H();		//SCL置高
    IIC_SDA_L();		//SDA置低
    Delay_us(1);
    IIC_SDA_H();		//SDA置高
    Delay_us(1);
}

数据帧:

数据帧是数据发送的最小单位。

每个数据帧承载1KB数据,也就是8bit,对应着一个8位二进制数,这里我把它称为数据串。

IIC的是先发高字节再发低字节。例如,有一个数据为11001010,IIC在数据串中就是1-1-0-0-1-0-1-0,数据发送的先后顺序也是按照上面的排列顺序。

在IIC的协议中规定:SCL为低电平时,SDA才允许变换状态;SCL为高电平时,才读取SDA状态。

通俗的讲:在SCL为高电平时,IIC总线上的设备才会读取SDA的状态,在SCL为低电平时,则绝对不读取。这样规定是为了让IIC总线上的设备保持时序同步。

所以数据串的时序为:SCL置低——SDA根据要传输的数据位是0还是1置低或高——SCL置高——延时——SCL置低——SDA根据要传输的数据位是0还是1置低或高...        以此循环八次,把8位数据传输完毕。

假设现在需要传输11001010这个8位二进制数,其电平变化应当如下:

用程序实现如下:

void IIC_WriteByte(u8 data)			//发送一个数据串,data为要发送的数据
{
    u8 i;
    SDA_OUT();			//SDA设置为输出模式
    for(i=0;i<8;i++)				//循环发送,每次循环发送1bit,循环8次就是1Byte
    {
        IIC_SCL_L();				//SCL置低
        if(data & 0x80)     //如果Data的第八位是高电平
            IIC_SDA_H();		//SDA置高
        else
            IIC_SDA_L();		//SDA置低
        IIC_SCL_H();				//SCL置高
        Delay_us(1);				//延时
        data<<=1;						//data左移一位
    }
}

应答信号

应答信号就是用于确认对方已经收到数据的信号。一个完整的数据帧由数据串与一个应答信号组成。

在数据串发送完成之后,将SCL置低,SDA置高电平,然后STM32将SDA的IO口切换成输入模式(前面的SDA置高电平可以省略,因为切换成输入模式后总线会自动变成高电平),再将SCL置高电平。然后延时等待,如果从机收到刚才发送的数据,则会将SDA拉低,这个低电平就是应答信号。

此时的STM32因为处于输入模式,就会收到应答信号,则认为上个数据发送成功,就再切回输出模式,继续下一帧的数据传输,或者发出停止信号。如果没有收到应答信号,则认为数据发送失败,可以重新发送一遍刚才的数据,或者直接发送停止信号。

接着刚才的数据帧表示应答信号如下:

从程序实现如下:

u8 IIC_Wait_Ask(void)
{
    int count=0;        
    IIC_SCL_L();		//SCL置低
    Delay_us(1);        //延时
    IIC_SDA_H();        //SDA置高
	SDA_IN();           //SDA切换成输入模式
    IIC_SCL_H();        //SCL置高
    Delay_us(1);        //延时,等待从机发出应答信号
    while(Read_IIC_SDA)    //如果SDA为低电平
    {
        count++;            //开始计数
        if(count>250)       //计数超过250,则认为数据发送失败,从机无响应
        {
            IIC_Stop();			//发出停止信号	
            return 1;           //函数返回1
        }   
    }
    IIC_SCL_L();           //如果SDA为高电平,则认为收到应答,SCL置低
    Delay_us(1);
    return 0;              //函数返回0
}

IIC数据传输就是以上述信号和数据帧为基础,不断循环,将数据一帧一帧发送出去。

这篇文章只是简单的讲了IIC的时序与各信号与数据帧的程序实现,实际的IIC运用还跟从机的寄存器地址、命令有关,在下一篇文章(IIC通信驱动OLED屏幕)中将以在OLED屏幕上显示"Hello world"为例,讲解STM32通过IIC与OLED屏幕的数据传输。


本文纯属个人理解,如有错误,欢迎指正

如果觉得本文有用,就点个赞吧~

猜你喜欢

转载自blog.csdn.net/qq_55203246/article/details/123944252