软件模拟IIC(主机篇Code详解)——定时器实现

一、I2C总线结构

image.png
特点:
1、I2C是一种半双工的同步传输协议,通过SCL和SDA两线进行主从机的数据交互。
2、存在“线与”机制。多主机时,总线具有“线与”的逻辑功能,只要有一个节点发送低电平,总线上就表现低电平的状态。
3、存在SDA回读机制。总线被启动后,多个主机在每发送一个数据位时都要对自己的输出电平进行检测,只要检测的电平与自己发出的电平一致,就会继续占用总线。
4、上拉电阻存在的意义:确保总线在空闲时为高电平
5、SCL和SDA需要设置为开漏口。设置为开漏口的目的:防止两个设备一个拉高,一个拉低,造成短路的现象。当设置为开漏口时,不存在P管,输出高电平的能力来源于VCC,这样总线上的电路就不会存在短路的风险。
image.png

二、根据I2C的时序进行代码的构建

主机发送数据给从机

image.png
1、主机发送数据时可以看成5个步骤,发送起始信号,发送地址码及读写位,接收返回信号、发送数据、发送停止位。
①主机发送起始信号。
②主机发送从设备地址(7bit) + 读写位(最低位)。0表示主机写,1表示主机读。
③主机接收从机的应答信号(ACK/nACK)。
④主机发送数据,发送完成后若接收到ACK信号则继续发送下一帧数据。
⑤主机接收到nACK信号后,发送停止信号。
**加粗样式
根据上述的步骤,在程序中定义相关的状态。SCL的时序由定时器产生。

typedef enum{	//i2c对应的状态
	S_Start = 0,	//起始
    S_Radio_Address,	//广播地址
    S_WriteReadBit,		//发送读写位
    S_WriteData,		//发送数据
    S_Read_Ack,			//读取应答信号
    //S_ReadData,		//读取数据		只针对主机发送,暂时可以忽略该成员
    //S_Write_Ack,		//发送应答信号	只针对主机发送,暂时可以忽略该成员
    S_Stop,				//停止信号
    S_Error,			//通讯过程中出现异常的处理标志
}Master_Step;

uint8_t		S_Master_Step;		//用于表示当前I2C的状态

1、起始信号

image.png
初始状态:S_Master_Step == S_Start,当SCL为高电平时,拉低SDA,产生起始信号。起始信号产生后S_Master_Step = S_Radio_Address。准备广播地址。

	if(S_Master_Step == Step_Start)
	{
			//I2C开始通讯
		if(Master_SCL == 1 && Master_SDA == 1)		//SCL为高,SDA空闲
		{
			Master_SDA = 0;		//SDA拉低
			Master_SCL_TIMEn	//SCL时序由定时器产生
			S_Master_Step = Step_Radio_Address;		//主机准备发送地址
		}
	}

2、地址广播

image.png
当S_Master_Step == S_Radio_Address时,进行地址广播。
可以看成主机发送一串数据给从机。

在SCL高电平期间

在SCL为高电平期间,SDA上的数据必须保持稳定,所以程序中不做处理。

		case	Step_Radio_Address:		//地址广播

		break;
在SCL低电平期间
		case	Step_Radio_Address:		//广播地址
				if(S_Master_BitCnt < 7)
				{
					S_Master_Buf = (Master_Message.Address) << S_Master_BitCnt;		//高位先行
					S_Master_BitCnt++;
					if(S_Master_Buf & 0x80)		//发送地址
						{
							Master_SDA = 1;
						}
					else
						{
							Master_SDA = 0;
						}
				}
				else		//最低位为读写位
				{
					S_Master_BitCnt = 0;
					S_Master_Step = Step_Read_Ack;		//master准备接收应答信号。
					if(S_Master_Buf & 0x80)		//LSB为读写位
						{
							U_Master_Status = Status_ReadData;		// 最低位为1表示:主机读取数据
						}
					else
						{
							U_Master_Status = Status_SendData;		// 最低位为0表示:主机写数据
						}
				}
		break;

3、主机接收应答信号

image.png
image.png
注意点:
①在接收应答信号的前一个CLK信号的低电平时间内,将SDA端口切换为输入态,用于接收从机的ACK/nACK信号。
②主机发送完地址+读写位,接收从机的应答信号后,在SCL高电平期间,识别主机将要做发送动作还是接收动作。
③若主机处于发送状态,且接收到从机返回的ACK信号,在SCL高电平期间,准备下一帧要发送的数据。此时S_Master_Step的状态指向Step_Write_Data(写数据)。
④当数据发送完成时,将S_Master_Step的状态切换为Step_Stop。

在SCL高电平期间
		case	Step_Read_Ack:
			if(Master_SDA == ACK)	//主机读取到ACK信号
				{
					if(U_Master_Status == Status_SendData)	// 主机写数据
						{
							S_Master_Step = Step_Write_Data;		// 状态指向写数据
							Master_SDA_OUT		//SDA端口设置为输出
							if(S_Master_ByteCnt < Master_Message.WriteSize)		//识别主机当前发送第几帧数据
							{
								S_Master_Buf = *(Master_Message.WriteData+S_Master_ByteCnt);	//将要发送的byte存放到S_Master_Buf
								S_Master_ByteCnt++;		// 发送的byte数目加1
							}
							else
							{
              	//主机数据发送完毕,准备发送停止信号,将状态切换为 Step_Stop
								S_Master_ByteCnt = 0;
								S_Master_Step = Step_Stop;
							}
						}
        	/* 注释代码为主机读取部分,暂时可以忽略 */
					//else if(U_Master_Status == Status_ReadData)		//主机读取数据
					//	{
					//		//主机读取数据
					//		{
					//			Master_SDA_IN		//SDA端口切换为输入态
					//			Master_Message.ReadData = Master_ReadDate;		//数组的地址存放到结构体中
					//			S_Master_BitCnt = 0;
					//			S_Master_Step = Step_Read_Data;		//状态指向读数据
					//		}
					//	}
				}
			else
				{
					//若在主机未发送完成,产生了nAck信号,可以查看S_Master_ByteCnt变量,得知主机发送到第几个byte
					S_Master_Step = Step_Error;		// 故障排查
				}
		break;
在SCL低电平期间
		case	Step_Read_Ack:		//主机读取到Ack信号
			if(!S_Master_BitCnt)
			{
				Master_SDA_IN				//当主机发送完数据后,SDA立刻切换为输入态,准备读取从机发送的ACK信号
			}
		break;

4、主机发送数据

image.png
image.png
注意点:
① 在SCL高电平期间,需要保持数据的稳定性,禁止操作SDA端口。可在高电平期间对发送bit进行计数,当发送1个byte后,将S_Master_Step的状态切换Step_Read_Ack(读取ACK/nACK信号)。
② 在SCL低电平期间,传输数据,改变SDA端口的电平(高位先行)。

在SCL高电平期间
		case	Step_Write_Data:		//主机发送数据
			S_Master_BitCnt++;			//记录发送bit的个数
			if(S_Master_BitCnt == 8)
			{
				S_Master_BitCnt = 0;
				S_Master_Step = Step_Read_Ack;		//8bit数据发送完成后,接收Slave的ack信号
			}
		break;
在SCL低电平期间
		case	Step_Write_Data:		//主机发送数据
				if(S_Master_BitCnt < 8)			//逐个bit发送
				{
					S_Master_Buf <<= S_Master_BitCnt;		//移位
					if(S_Master_Buf & Master_MSB)				//高位先行
						{
							Master_SDA = 1;
						}
					else
						{
							Master_SDA = 0;
						}
				}
		break;

5、主机发送停止信号

image.png

if(S_Master_Step == Step_Stop)
	{
			//I2C结束通讯
			Master_SCL_TIMDis		//关闭定时器,不再为SCL产生CLK
			Master_SDA_OUT		//SDA端口设置为输出
			Master_SCL = 1;		//拉高SCL端口
			Master_SDA = 1;		//拉高SDA端口
	}

主机向从机读取数据

image.png
1、主机读取数据时有6个步骤,发送起始信号,发送地址码及读写位、接收应答信号,接收数据、发送返回信号、发送停止位。
①主机发送起始信号。
②主机发送从设备地址(7bit) + 读写位(最低位)。0表示主机写,1表示主机读。
③主机接收从机的应答信号(ACK/nACK)。
④主机接收数据,接收完成后,根据需求对从机回复应答信号,若主机回复ACK信号,则继续接收下一帧数据。
⑤当主机回复nACK信号时,下一个时钟,主机产生停止信号。
加粗样式
根据上述的步骤,在程序中定义相关的状态。

typedef enum{	//i2c对应的状态
	S_Start = 0,	//起始
    S_Radio_Address,	//广播地址
    S_WriteReadBit,		//发送读写位
    S_WriteData,		//发送数据
    S_Read_Ack,			//读取应答信号
    S_ReadData,			//读取数据		
    S_Write_Ack,		//发送应答信号	
    S_Stop,				//停止信号
    S_Error,			//通讯过程中出现异常的处理标志
}Master_Step;

uint8_t		S_Master_Step;		//用于表示当前I2C的状态

1、起始信号

image.png
S_Master_Step == S_Start,当SCL为高电平时,拉低SDA,产生起始信号。起始信号产生后S_Master_Step = S_Radio_Address。准备广播地址。

	if(S_Master_Step == Step_Start)
	{
			//I2C开始通讯
		if(Master_SCL == 1 && Master_SDA == 1)		//SCL为高,SDA空闲
		{
			Master_SDA = 0;		//SDA拉低
			Master_SCL_TIMEn	//SCL时序由定时器产生
			S_Master_Step = Step_Radio_Address;		//主机准备发送地址
		}
	}

2、地址广播

image.png
当S_Master_Step == S_Radio_Address时,进行地址广播。

在SCL高电平期间

在SCL为高电平期间,SDA上的数据必须保持稳定,所以程序中不做处理。

		case	Step_Radio_Address:		//地址广播

		break;
在SCL低电平期间
		case	Step_Radio_Address:		//广播地址
				if(S_Master_BitCnt < 7)
				{
					S_Master_Buf = (Master_Message.Address) << S_Master_BitCnt;		//高位先行
					S_Master_BitCnt++;
					if(S_Master_Buf & 0x80)		//发送地址
						{
							Master_SDA = 1;
						}
					else
						{
							Master_SDA = 0;
						}
				}
				else		//最低位为读写位
				{
					S_Master_BitCnt = 0;
					S_Master_Step = Step_Read_Ack;		//master准备接收应答信号。
					if(S_Master_Buf & 0x80)		//LSB为读写位
						{
							U_Master_Status = Status_ReadData;		// 最低位为1表示:主机读取数据
						}
					else
						{
							U_Master_Status = Status_SendData;		// 最低位为0表示:主机写数据
						}
				}
		break;

3、接收应答信号

image.png
image.png
注意点:
①在接收应答信号的前一个CLK信号的低电平时间内,将SDA端口切换为输入态,用于接收从机的ACK/nACK信号。
②主机发送完地址+读写位,接收从机的应答信号后,在SCL高电平期间,识别主机将要做发送动作还是接收动作。
③若主机处于读取状态,且接收到从机返回的ACK信号,在SCL高电平期间,将SDA端口的状态切换为输入态,准备接收数据,此时S_Master_Step的状态指向Step_Read_Data(读数据)。

在SCL为高电平期间
		case	Step_Read_Ack:
			if(Master_SDA == ACK)	//主机读取到ACK信号
				{
        	/* 注释代码为主机发送部分,暂时可以忽略 */
				//	if(U_Master_Status == Status_SendData)	// 主机写数据
				//		{
				//			S_Master_Step = Step_Write_Data;		// 状态指向写数据
				//			Master_SDA_OUT		//SDA端口设置为输出
				//			if(S_Master_ByteCnt < Master_Message.WriteSize)		//识别主机当前发送第几帧数据
				//			{
				//				S_Master_Buf = *(Master_Message.WriteData+S_Master_ByteCnt);	//	Mastert_WriteDate: 数组地址, S_Master_ByteCnt:偏移量
				//				S_Master_ByteCnt++;
				//			}
				//			else
				//			{
        //      	//主机数据发送完毕,准备发送停止信号,将状态切换为 Step_Stop
				//				S_Master_ByteCnt = 0;
				//				S_Master_Step = Step_Stop;
				//			}
				//		}
				/*	else */
						if(U_Master_Status == Status_ReadData)		//主机读取数据
						{
								//主机读取数据
								Master_SDA_IN		//SDA端口切换为输入态
								Master_Message.ReadData = Master_ReadDate;		//数组的地址存放到结构体中
								S_Master_BitCnt = 0;
								S_Master_Step = Step_Read_Data;		//状态指向读数据
						}
				}
			else
				{
					//若在主机未发送完成,产生了nAck信号,可以查看S_Master_ByteCnt变量,得知主机发送到第几个byte
					S_Master_Step = Step_Error;		// 故障排查
				}
		break;
在SCL为低电平期间
		case	Step_Read_Ack:		//主机读取到Ack信号
			if(!S_Master_BitCnt)
			{
				Master_SDA_IN				//当主机发送完数据后,SDA立刻切换为输入态,准备读取从机发送的ACK信号
			}
		break;

4、接收数据

image.png
image.png
①在SCL为高电平期间,SDA上的数据有效,此时主机读取SDA端口的数据。
②在SCL为低电平时,主机作为接收方,禁止操作SDA端口。可在低电平期间对接收bit进行计数,当接收1个byt后,将S_Master_Step的状态切换为Step_Write_Ack(发送ACK/nACK信号)

在SCL为高电平期间
		case	Step_Read_Data:		//master read Data
			if(S_Master_BitCnt < 8)		//逐个接收bit
			{
				if(Master_SDA)
				{
					*(Master_Message.ReadData+S_Master_ByteCnt) |= 0x01;		//接收数据
				}
				*(Master_Message.ReadData+S_Master_ByteCnt) <<= S_Master_BitCnt;		//移位
				S_Master_BitCnt++;		// 记录接收bit的个数
			}
		break;
在SCL为低电平期间
		case	Step_Read_Data:		//master read Data
			if(S_Master_BitCnt == 8)		//完成1个byte的接收
			{
				S_Master_BitCnt = 0;
				S_Master_ByteCnt++;				//接收的Byte数加1
				S_Master_Step = Step_Write_Ack;		//主机准备产生Ack信号
			}
			if(S_Master_ByteCnt==Master_Message.ReadSize)		//主机完成接收(Master_Message.ReadSize:主机接收的长度)
			{
				S_Master_ByteCnt = 0;
				F_Master_nAck = 1;
					//接收数据完成,返回nAck信号
			}
	
		break;

5、发送ACK/nACK信号

①在SCL为低电平期间,主机切换SDA端口的电平,发送ACK信号或者nACK信号(9bit)。
②在SCL为高电平期间,若SDA端口输出低电平,继续接收下一帧数据,则将S_Master_Step的状态指向Step_Read_Data(接收数据)。若SDA端口输出高电平,则将S_Master_Step的状态指向Step_Stop(停止信号)。

在SCL为高电平期间
		case	Step_Write_Ack:		//master send ack
			Master_SDA_OUT				//SDA端口切换为输出态
			if(F_Master_Ack)			
			{
      	//此时主机以产生Ack信号,将SDA端口的状态切换为输入   
				Master_SDA_IN			//上拉电阻的存在,SDA端口切换为输入态后,SDA线上的电平仍为高电平。
				F_Master_Ack = 0;
				S_Master_Step = Step_Read_Data;		// 继续发送数据
			}
    	if(F_Master_nAck)
    	{
      	S_Master_Step = Step_Stop;		//产生停止信号
      }
		break;
在SCL为低电平期间
		case	Step_Write_Ack:		//master send ack
			if(F_Master_nAck)			//产生nAck标志
			{
				F_Master_nAck = 0;
				Master_SDA = 1;			//nAck
			}
			else
			{
				Master_SDA = 0;			//Ack
				F_Master_Ack = 1;
			}
		break;

6、停止信号

image.png

if(S_Master_Step == Step_Stop)
	{
			//I2C结束通讯
			Master_SCL_TIMDis		//关闭定时器,不再为SCL产生CLK
			Master_SDA_OUT		//SDA端口设置为输出
			Master_SCL = 1;		//拉高SCL端口
			Master_SDA = 1;		//拉高SDA端口
	}
  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值