五、IIC协议

在开始这个之前,我们需要找到一份完整的IIC协议说明
这个应该是比较详细的
这个在我看来算是补充了

强调一点

这里我们所写的是软件iic,并且是主机的iic程序
对于从机是如何接收的我没有做研究,硬件IIC的话就是主从属性设置,软模的话从机就要使用边缘触发中断了

首先了解一下这个协议

平时我们用的是两根线SDA/SCL。
SDA我们叫数据线,SCL我们叫时钟线。
在我看来它是一种半双工的通讯协议,毕竟一条线上不可能同时收发数据。
描述的IIC特点
在这里插入图片描述

在开始之前,我们先看一个大图,整个一帧数据的概览
在这里插入图片描述
看看关键地方
START、MSB、SCL上的数字、ACK、STOP、Sr/P
大概就是这几个,解释一下
首先IIC协议规定:SDA上传输的数据必须在SCL为高电平期间保持稳定,SDA上的数据只能在SCL为低电平期间变化。IIC期间在脉冲上升沿把数据放到SDA上,在脉冲下降沿从SAD上读取数据。这样的话,在SCL高电平期间,SDA上的数据是稳定的。在脉冲下降沿之后的保持时间以后,SDA上的数据可以变化,直到脉冲上升沿之前。

START是一个起始信号,主机告诉从机我要发送信号了。

MSB(Most Significant Bit),意为最高有效位;LSB(Least Significant Bit),意为最低有效位,这里这么大个MSB我以为有什么重要意义,但是仔细想了一下,好像就是数据的最高位,它代表IIC是从最高位开始传输数据的。

SCL上的数字代表的是几位的数据,很明显在SCL为高电平时SDA上的高低电平一个位数据的0/1,也不难看出,一帧数据是有九位的。

ACK为应答请求,是一个低电平信号;NACK为非应答请求是一个高电平信号。网上有把非应答请求叫做应答非的,我是不赞成他们的这种看法的,首先主机发送的应该是一个请求信号,所以我在应答和非应答后面都加上了请求二字,而理所当然的是应答请求或非答请求的时钟,都由当前主器件发生。应答信号请求是当前从器件希望得到从机一个应答来判断是否可以继续传输数据(或者说是传输有没有成功),而非应答请求信号是指主器件直接忽略从器件是否成功接收到数据而不请求从机的应答。

STOP是停止位,代表一次通信结束,它的SDA电平可高可低,他们代表的信息是Sr/P。
Sr/P表示,在STOP信号里,如果把SDA拉高则直接停止(这个就是P),如果把SDA拉低则立马衔接下一次传输(这个就是Sr)
一般我们都是直接设置为P这种,STOP后再START是一样。

硬件上,先看这个图

在这里插入图片描述SDA线上的数据必须在时钟的高电平周期保持稳定。数据线的高或低电平状态只有在SCL线的时钟信号是低电平时才能改变。为了保持高电平的稳定状态, 我们通常加上上拉电阻。

IIC引脚的准备与初始化

SDA与SCL的初始化,先设置默认的输出,然后SDL为数据线偶尔会用作输入,SDL需要在输入和输出间切换。
初始化与相应的准备程序为


#define IIC_SCLset         GPIO_SetBits(GPIOB, GPIO_Pin_6)    
#define IIC_SDAset         GPIO_SetBits(GPIOB, GPIO_Pin_7)
#define IIC_SCLreset       GPIO_ResetBits(GPIOB, GPIO_Pin_6)  
#define IIC_SDAreset       GPIO_ResetBits(GPIOB, GPIO_Pin_7)
#define READ_SDA   GPIO_ReadInputDataBit(GPIOB , 7)   //SDA读


GPIO_InitTypeDef GPIO_InitStructure;//定义结构体变量


void I2C_Config(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_7|GPIO_Pin_6;  //SDA|SCL
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;	 //设置推挽输出模式
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;	  //设置传输速率
	GPIO_Init(GPIOB,&GPIO_InitStructure); 	    /* 初始化GPIO */
	GPIO_SetBits(GPIOB, GPIO_Pin_6);
	GPIO_SetBits(GPIOB, GPIO_Pin_7);
}


void SDA_OUT(void)
{
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;	 //设置推挽输出模式
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;	  //设置传输速率
	GPIO_Init(GPIOB,&GPIO_InitStructure); 	   /* 初始化GPIO */
}


void SDA_IN(void)
{
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPD;	 //设置推挽输出模式
	GPIO_Init(GPIOB,&GPIO_InitStructure); 	   /* 初始化GPIO */
}

起始位

在这里插入图片描述首先看起始位
在SCL时钟线为默认的高电平时,SDA数据线由默以高电平拉低,这就是一个起始位,它告诉总线上的从器件,数据开始了。而当起始信号产生后,SCL立马拉低为接下来数据传输做准备,因为SCL在高电平时SDA才为有效位,所以在传输下一字节前SCL应该拉低。注意此时SDA为输入模式。

void IIC_Start(void)
{
	SDA_OUT(); 
	IIC_SDAset;	
	IIC_SCLset;
	
	delay_us(4);
 	IIC_SDAreset; 
	delay_us(4);
	IIC_SCLreset;
}

关于延时的解释
网上有这种解释
在这里插入图片描述

这里面有一个delay_us(4);定义一个不精确延时就行。我看有的地方没有加这个延时,特地去看了一下,这个延时主要影响的是IIC通信的速率,不同版本的IIC协议速率不同,延时主要加在。但是IIC速率也就是时钟频率是完全由主设备掌控的,因为时钟是由主器件发出的。

顺着图再看到的是一帧数据

也就是SCL上标注的数字,之所以上面的SDA是那个鬼样子,是因为这些时候它是高低变换的,高代表1低代表0,这个最好理解不过。
现在看看它这九个字节的数据是怎么用的,这个就要谈到它的消息传递协议了。
最后一个字节是应答位这个我们待会讲,所以实际上SDA上的数据是以八位一字节的格式传输的。
在这里插入图片描述
关于数据传输,需要注意的是这八位数据是以SDA第一次拉高到最后一次拉低总共变换8次为脉冲时钟来计算的,SDA只需要保证在SCL在高电平时表示相应的高低电平就行,高1低0。并且数据是高位在前传输的,这很重要。
数据传输肯定是有发有收的
这部分的代码

void IIC_SendByte(uint8_t data)
{                        
    uint8_t t;   
    SDA_OUT(); 	    
    IIC_SCLreset; 
    for(t=0;t<8;t++)
    {              
			if((data&0x80)>>7)
				IIC_SDAset;
			else
				IIC_SDAreset;
			data<<=1;
			delay_us(1);			
			IIC_SCLset;
			delay_us(1);
			IIC_SCLreset;	
			delay_us(1);
    }	 
} 


uint8_t IIC_ReadByte(uint8_t ack)
{
	uint8_t i,receive=0;
	SDA_IN(); 
    for(i=0;i<8;i++ )
	{
        IIC_SCLreset; 
        delay_us(1);
        IIC_SCLset;
        receive<<=1;
        if(READ_SDA)receive++;
        delay_us(1); 
    }					 
    if(ack)
        IIC_Ack();
    else
        IIC_NAck();
    return receive;
}
		if((data&0x80)>>7)
			IIC_SDAset;
		else
			IIC_SDAreset;
		data<<=1;
		这段代码是核心,自己琢磨琢磨,位操作没什么好讲的,但是数据一涉及这些就很有意思。

还有一个需要注意的地方就是可以发现在读操作里有应答相关的代码,我的解释是下面这个图。
也就是说这个接收可能有应答也可能是没有应答,所以这个后面干脆就加了点东西。

在这里插入图片描述

再看一个字节后的应答位

在这里插入图片描述

应答请求分为应答请求和非应答请求
前面提到过应答请求或非答请求的时钟,都由当前主器件发生。
所以这里为什么会有应答请求和非应答请求两个的实现代码,原因在于主机可以选择我需不需要通过这个应答请求来判断或者是从机有没有产生应答的信号,如果从机没有产生应答信号的能力或者主机不需要校验这个应答,那么我们自然是希望总线继续以默认方式去传输数据的。
所以就产生了一个非应答请求位,说白了就是直接把SDA拉高,就当从机产生了应答一样,使得总线的数据可以继续传输。
这里说的主机应答/非应答信号是主机产生一个请求让从机表示一下说数据传输到位,从机的应答信号是指从机的这个表示,主机请求从机应答,可能有点绕,但并不是不好理解。
在应答之前正是一个字节的数据传输完成,所以这时候SCL是低电平,在下一次SCL变为高电平时,SDA为高电平则是请求非应答信号,SDA为低电平则是请求应答信号,此时SDA为输出模式。

应答信号发送后,主机应该开始读取从机发出的应答信号判断数据是否通信成功,这个时候应该提前吧SCL与SDA拉低,如果此时从机SDA是低电平表示可以继续传输数据,而如果此时SDA是高电平表示有错误应该停止发送数据。这部分叫做等待应答,很明显,如果主机发送的是一个应答请求,那么我就应该根据从机的响应判断是否传输有效,否则就发送停止位结束这次通信;如果主机发送一个非应答信号我应该直接过,不需要去判断从机的响应,从机也不会应答。说白了就是IIC_WaitAck只在发送了Ack后判断,NAck后是不需要加IIC_WaitAck判断的,注意此时SDA应该为输入模式
代码

void IIC_Ack(void)
{
	IIC_SCLreset;
	SDA_OUT();
	IIC_SDAreset;
	delay_us(1);
	IIC_SCLset;
	delay_us(2);
	IIC_SCLreset;
}



void IIC_NAck(void)
{
	IIC_SCLreset;
	SDA_OUT();
	IIC_SDAset;
	delay_us(1);
	IIC_SCLset;
	delay_us(1);
	IIC_SCLreset;
}	

uint8_t IIC_WaitAck(void)
{
	uint8_t ucErrTime=0;
	SDA_IN(); 
	IIC_SDAset;delay_us(1);	   
	IIC_SCLset;delay_us(1);;	 
	while(READ_SDA)
	{
		ucErrTime++;
		if(ucErrTime>250)
		{
			IIC_Stop();
			return 1;
		}
	}
	IIC_SCLreset;   
	return 0;  
} 

这部分代码可能需要好好去理解。

最后的就是停止位了

在这里插入图片描述
产生停止位的情况有两种
要么在主机发送应答请求时从机的应答信号为高电平(这个时候可能是前一字节数据发送产生了错误),IIC_WaitAck会产生一个停止信号直接结束这次数据传输。
要么是成功完成了这次数据传输。
看波形,此时应该是应答请求或者非应答请求后,SDA处于高电平,SCL处于低电平
再看stop停止位
首先在SCL为高电平的状态下,SDA由低电平拉高,这就是一个停止位,它告诉总线上的从器件,数据结束了。而当停止号产生后相当于一个传输周期结束,SCL与SDA才应该回到默认的高电平状态,为下一次的start操作准备。注意此时SDA为输入模式。

void IIC_Stop(void)
{
	SDA_OUT();
	IIC_SCLreset;
	IIC_SDAreset; 
    delay_us(4);
	IIC_SCLset; 
	IIC_SDAset;
    delay_us(4);							   	
}

读取和发送的操作

iic肯定是读写一体的,它是半双工的通信协议
首先说一下写
读数据很简单,注意高位在前就行

好了

好了

好了

上面最基本的几个我们已经搞定,接下来就得看协议规定的发送和接收的方式

iic它当然不是一次只传八个字节那么简单。
应答的设计就是为了在star与stop信号之间传输几个字节的数据
那具体应该是几个字节数据,这几个字节的数据有能代表什么呢?
看这张图
在这里插入图片描述它代表完整的一个iic通信的过程。
我们再来看看它里面的生字
ADDRESS、R/W、DATA
大概就是这几个,解释一下
ADDRESS代表从机的地址,我们都知道IIC总线上是挂载了许多设备的,设备之间就是按照这个来区分的
R/W表示读/写操作,1表示读,0表示写
DATA则表示用户数据

ADDRESS与R/W共用一个字节,ADDRESS占7位,R/W占1位,并且这个表示读写和地址的必在第一个字节。

DATA则比较随意,这个按需求,不一定只是一个字节

但是需要注意的是,在读某个地址的寄存器的值的时候,它应该在
但是并不是所有的IIC设备都是直接通过地址来读的,比如mpu6050这种就是器件地址+寄存器地址这里有一篇博客可以看看它的代码
所以对于IIC设备的操作还是要去看从器件的手册了解从器件的读写方式,它是变化的

不是具体的协议不实现完,而是协议本身十分灵活,具体操作需要具体写,然后这边提供几个参考的代码

/**********************
从 指定设备 指定寄存器 读取一个值
***********************/
uint8_t IIC_ReadByteFromSlave(uint8_t I2C_Addr,uint8_t reg,uint8_t *buf)
{
	IIC_Start();	
	IIC_SendByte(I2C_Addr);
	if(IIC_WaitAck()) 
	{
		IIC_Stop();
		return 1;
	}
	IIC_SendByte(reg); 
	IIC_WaitAck();	  
	
	IIC_Start();
	IIC_SendByte(I2C_Addr+1); 		   
	IIC_WaitAck();
	*buf=IIC_ReadByte(0);
    IIC_Stop();
	return 0;
}



/**********************
将一个值写入 指定设备 指定寄存器
***********************/
uint8_t IIC_WriteByteToSlave(uint8_t I2C_Addr,uint8_t reg,uint8_t data)
{
	IIC_Start();
	IIC_SendByte(I2C_Addr); 
	if(IIC_WaitAck())
	{
		IIC_Stop();
		return 1;
	}
	IIC_SendByte(reg); 
    IIC_WaitAck();
	IIC_SendByte(data); 
	if(IIC_WaitAck())
	{
		IIC_Stop(); 
		return 1;
	}
	IIC_Stop(); 
    
	return 0;
}



/**********************
读取 指定设备 指定寄存器 的length个值
***********************/
uint8_t IIC_ReadMultByteFromSlave(uint8_t dev, uint8_t reg, uint8_t length, uint8_t *data)
{
    uint8_t count = 0;
	uint8_t temp;
	IIC_Start();
	IIC_SendByte(dev); 
	if(IIC_WaitAck())
	{
		IIC_Stop(); 
		return 1; 
	}
	IIC_SendByte(reg); 
    IIC_WaitAck();	  
	IIC_Start();
	IIC_SendByte(dev+1); 
	IIC_WaitAck();
    for(count=0;count<length;count++)
	{
		if(count!=(length-1))
            temp = IIC_ReadByte(1); 
		else  
            temp = IIC_ReadByte(0); 
        
		data[count] = temp;
	}
    IIC_Stop(); 
    return 0;
}


/**********************
将多个字节写入 指定设备 指定寄存器
***********************/
uint8_t IIC_WriteMultByteToSlave(uint8_t dev, uint8_t reg, uint8_t length, uint8_t* data)
{
    
 	uint8_t count = 0;
	IIC_Start();
	IIC_SendByte(dev); 
	if(IIC_WaitAck())
	{
		IIC_Stop();
		return 1; 
	}
	IIC_SendByte(reg); 
    IIC_WaitAck();	  
	for(count=0;count<length;count++)
	{
		IIC_SendByte(data[count]); 
		if(IIC_WaitAck()) 
		{
			IIC_Stop();
			return 1; 
		}
	}
	IIC_Stop(); 
    
	return 0;
}





  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值