单片机IO管脚模拟I2C从机通信

  • Post author:
  • Post category:其他

1、思路:需要使用SDA管脚下降沿中断,通过I2C的通信协议可知,主机发送开始信号时,会先把SDA管脚拉低,所以从机在SDA下降沿会进入中断,然后做相关的操作。

2、优点:可以让没有硬件I2C的单片机通过此方式模拟通信;实测从机收发数据正常,无丢包现象;90%的代码都注释,避免初学者看不明白。

3、缺点:由于是模拟的方式,所以主机的速度不能太快,否则单片机处理不过来;SDA管脚的中断优先级尽量调到最高,避免接收数据时进入其它中断,导致丢包;接收过程在中断里面处理,所以占用中断的时间受传输速度和数据长度的影响。

4、补充:代码中有几个部分需要做超时等待处理,比如i2c_slave_wait_for_scl函数里面,建议超时时间是I2C时钟的1~2个周期以上(太短的话会通信错误),比如I2C的时钟是100K时,周期是10us。那么怎么能够得到10us的时间呢,可以在主循环里面通过管脚输出不同的电平,然后用示波器或者逻辑分析仪来看该管脚的高电平时间,如下代码所示,查看PB0的高电平时间就是i2c_slave_wait_for_scl函数耗时时间(测试时为了时间更准确,建议关闭所有中断)。

void mian()
{
    while(1)
    {
        //让PB0管脚输出高电平
        PB0=1;

        //SCL管脚确保一直是高,所以不为低时会等待超时才退出
        i2c_slave_wait_for_scl(0);

        //让PB0管脚输出低电平
        PB0=0;

        //SCL管脚确保一直是高,所以不为低时会等待超时才退出
        i2c_slave_wait_for_scl(0);   
    }
}

以下是I2C从机的完整的代码

i2c_slave.c

#include "i2c_slave.h"

//===================================================================
//							变量定义
//===================================================================
i2c_slave_t i2c_slave;




//I2C从机初始化(调用此函数初始化I2C从机)
void i2c_slave_init()
{
 	I2C_SDA     = 1;			//设置高电平
	I2C_SDA_PC  = 1;			//输入模式
	I2C_SDA_PU  = 1;			//打开上拉电阻
	
 	I2C_SCL     = 1;			//设置高电平
	I2C_SCL_PC  = 1;			//输入模式
	I2C_SCL_PU  = 1;			//打开上拉电阻

	//初始化I2C为空闲状态
	i2c_slave.state = I2C_STATE_IDLE;
	i2c_slave.flag.isSuccess = 0;
	

	//设置SDA管脚下降沿中断(不同单片机根据对应的语法设置即可)
	_integ = 0B00000010;				//INT0管脚下降沿中断
	_int0f = 0;							//清除INT0中断标志
	_int0e = 1;							//使能INT0中断	
	
	
	//给出默认的发送数据(测试用)
	uint8_t i;
	for(i=0;i<I2C_SLAVE_TX_DATA_SIZE;i++)
	{
		i2c_slave.tx_data[i]=i;	
	}
			
}





//SDA管脚中断函数(此函数在SDA下降沿中断里面调用)
void i2c_slave_sda_interrupt_callback()
{
	//关闭INT0中断(关闭SDA管脚的中断,每次通信中断一次即可)
	_int0e = 0;								
	
	
	//等待SCL管脚输出低电平	
	i2c_slave_wait_for_scl(0);
	if(!i2c_slave.flag.isSuccess)
	{
		//超时->退出
		goto end;
	}
	
	
	//检测到开始信号
	i2c_slave.state = I2C_STATE_START;
	
	//接收计次清0
	i2c_slave.rx_offs  = 0;
	
	while(i2c_slave.state == I2C_STATE_START)
	{
		volatile uint8_t val;
		//读从机地址 + 读/写位
		val = i2c_slave_read_byte();
		
		//是否读结束
		if(!i2c_slave.flag.isSuccess)
		{
			//是否检测到开始信号	
			if(i2c_slave.state == I2C_STATE_START)
			{
				continue;
			}
			
			//超时、停止->退出
			goto end;
		}
		
		//判断地址是否匹配
		if((val&0xFE) != I2C_SLAVE_ADDRESS)
		{
			//地址不匹配->退出
		 	goto end;			
		}
		

    	i2c_slave.state = I2C_STATE_DEVICE;

	    //发送ACK
	    i2c_slave_send_ack();
		if(!i2c_slave.flag.isSuccess)
		{
			//超时->退出
			goto end;
		}
		
		
		//当前为主机读操作
		if((val&1) != 0)
		{

			i2c_slave_send_data();
			if(!i2c_slave.flag.isSuccess)
			{
				if(i2c_slave.state == I2C_STATE_START)
				{
				  	continue;
				}
				else if(i2c_slave.state == I2C_STATE_NACK)
				{
					//主机接收/从机发送成功
					i2c_slave.flag.TxFinish = 1;
					//可以在主循环里面判断此标志做相关的发送成功处理
					
				  	goto end;	
				}	
				else
				{
				  	goto end;
				}
			}
		}
		//当前为主机写操作
		else
		{
			i2c_slave_receive_data();
			if(!i2c_slave.flag.isSuccess)
			{
				//主机发送开始信号
				if(i2c_slave.state == I2C_STATE_START)
				{
				  	continue;
				}
				//主机发送停止信号
				else if(i2c_slave.state == I2C_STATE_STOP)
				{
					//主机发送/从机接收成功
					i2c_slave.flag.RxFinish = 1;
					//可以在主循环里面判断此标志做相关的接收成功处理
					//接收的数据存储在i2c_slave.rx_data
					//接收的个数为i2c_slave.rx_offs
					
					goto end;		
				}
				//超时或者其他错误
				else
				{
				  	goto end;
				}	
			}
		}
	}	
  
	
end:
	//退出时把管脚恢复到上拉输入模式,确保总线为空闲状态
 	I2C_SDA     = 1;
	I2C_SDA_PC  = 1;
	I2C_SDA_PU  = 1;
	
 	I2C_SCL     = 1;
	I2C_SCL_PC  = 1;
	I2C_SCL_PU  = 1;
		
		
	//等待SCK和SDA拉高(出错退出时可能主机还在操作总线,此处会一直等待,如果主机一直不释放总线
	//则会卡死在中断,如果不加等待,则退出中断函数之后可能马上又进入中断,所以根据需要添加)
	//总线有多个设备时需要去掉以下代码
	while (I2C_SDA==0 || I2C_SCL==0);	
		
	//I2C状态为空闲	
 	i2c_slave.state = I2C_STATE_IDLE;


 	//清除中断标志
	_int0f = 0;
	//重新使能SDA管脚中断
	_int0e = 1;
}










//等待SCL管脚出现需要的电平
void i2c_slave_wait_for_scl(uint8_t level)
{
	uint8_t count = 0;
		
	//等待SCL管脚出现对应的电平
	while(I2C_SCL != level)
	{
		count ++;
		//此处等待的时间建议是I2C的1~2个周期,比如I2C的时钟是100K时,周期是10us
		if(count >= 50)
		{
			//超时(没有在规定的时间内等待到需要的电平)
			i2c_slave.state = I2C_STATE_TIMEOUT;
			
			//标志失败
			i2c_slave.flag.isSuccess = 0;
			return;
		}
	}
	
	//标志成功
	i2c_slave.flag.isSuccess = 1;
}









//I2C从机写1个字节
void i2c_slave_write_byte(uint8_t val)
{	
 	uint8_t i;
	for(i=0; i<8; i++)
	{
		//先写高位,所以与0x80
		if((val&0x80) != 0)
		{
			//配置SDA管脚输出高电平
			I2C_SDA_PC  = 0;	
			I2C_SDA		= 1;	
		}
		else
		{
			//配置SDA管脚输出低电平
			I2C_SDA_PC  = 0;
			I2C_SDA		= 0;	
		}
		
		val = val << 1;
		
		//等待主机读取(SCL上升沿读取)
		i2c_slave_wait_for_scl(1);
		if(!i2c_slave.flag.isSuccess)	return;	

		//等待SCL低电平出现
		i2c_slave_wait_for_scl(0);
		if(!i2c_slave.flag.isSuccess)	return;	
	}
	
  	//从机释放SDA管脚
	I2C_SDA_PC  = 1;
	I2C_SDA_PU  = 1;
	I2C_SDA		= 1;
}




//I2C从机读1个字节
uint8_t i2c_slave_read_byte()
{
	uint8_t i;
	uint8_t val=0;
	
	for(i=0; i<8; i++)
	{
		//等待SCL高电平出现	
		i2c_slave_wait_for_scl(1);
		if(!i2c_slave.flag.isSuccess)	return 0;	
		
		
		//保存数据,先收最高位,所以先左移后保存最低位
		val = val << 1;
		if(I2C_SDA)
		{
			val |= 0x01;
		}
		
		uint8_t count = 0;
		//等待SCL低电平出现
		while(I2C_SCL)
		{
			count++;
			//此处等待的时间建议是I2C的1~2个周期,比如I2C的时钟是100K时,周期是10us
			if(count >= 50)
			{	
				i2c_slave.state = I2C_STATE_TIMEOUT;
				i2c_slave.flag.isSuccess = 0;
				return 0;
			}
		
			//获取SDA管脚状态 
			uint8_t temp = 0;
			if(I2C_SDA)
			{
				temp=1;	
			}
		
			//SCL是否出现低电平
			if(I2C_SCL == 0)
			{
				//回去循环接收  
				break;
			}		
		
			//SDA管脚是否发生变化
			if((val & 1) != temp)
			{
				//SDA当前是高电平  
				if(temp)
				{
					i2c_slave.state = I2C_STATE_STOP;
				}
				//SDA当前是低电平
				else
				{
					i2c_slave.state = I2C_STATE_START;
				}
				
				i2c_slave.flag.isSuccess = 0;
				return 0;
			}
		}
    }
  
  return val;
}




//I2C从机发送ACK
void i2c_slave_send_ack()
{
	//从机的SDA管脚输出低做为ACK信号
	I2C_SDA_PC  = 0;
	I2C_SDA		= 0;
	
  
	//等待主机读取(SCL上升沿读取)
	i2c_slave_wait_for_scl(1);
	if(!i2c_slave.flag.isSuccess)	return;	

	
	
	//等待SCL低电平出现
	i2c_slave_wait_for_scl(0);
	if(!i2c_slave.flag.isSuccess)	return;	
	

	i2c_slave.flag.isSuccess = 1;	


  	//从机释放SDA管脚
	I2C_SDA_PC  = 1;
	I2C_SDA_PU  = 1;
	I2C_SDA		= 1;	
}


//I2C从机读取ACK
void i2c_slave_read_ack()
{
	//等待SCL高电平出现	
	i2c_slave_wait_for_scl(1);
	if(!i2c_slave.flag.isSuccess)	return;	
	
  
  	//读ACK
  	uint8_t val=0;
  	if(I2C_SDA)
  	{
  		val = 0x01;	
  	}
  

	uint8_t count = 0;
	//等待SCL低电平出现
	while(I2C_SCL)
	{
		count++;
		//此处等待的时间建议是I2C的1~2个周期,比如I2C的时钟是100K时,周期是10us
		if(count >= 50)
		{	
			i2c_slave.state = I2C_STATE_TIMEOUT;
			i2c_slave.flag.isSuccess = 0;
			return;
		}
	
		//获取SDA管脚状态 
		uint8_t temp = 0;
		if(I2C_SDA)
		{
			temp=1;	
		}
	
		//SCL是否出现低电平
		if(I2C_SCL == 0)
		{
			//回去循环接收  
			break;
		}		
	
		//SDA管脚是否发生变化
		if((val & 1) != temp)
		{
			//SDA当前是高电平  
			if(temp)
			{
				i2c_slave.state = I2C_STATE_STOP;
			}
			//SDA当前是低电平
			else
			{
				i2c_slave.state = I2C_STATE_START;
			}
			
			i2c_slave.flag.isSuccess = 0;
			return;
		}
	}
  
	if(val == 0x0)
	{
		i2c_slave.flag.isSuccess = 1;
	}
	else
	{
		i2c_slave.flag.isSuccess = 0;
		i2c_slave.state = I2C_STATE_NACK;
	}
}



//I2C从机发送一个字节
void i2c_slave_send_data()
{
	i2c_slave.tx_offs = 0;
loop:	
	i2c_slave_write_byte(i2c_slave.tx_data[i2c_slave.tx_offs]);
	if(i2c_slave.tx_offs < I2C_SLAVE_TX_DATA_SIZE) i2c_slave.tx_offs++;
			
	if(!i2c_slave.flag.isSuccess)
	{		
		return;	
	}
	
	i2c_slave_read_ack();
	if(i2c_slave.flag.isSuccess)
	{
		goto loop;		
	}	
}







//I2C从机接收一个字节
void i2c_slave_receive_data()
{
  	volatile uint8_t data;

loop:
	data = i2c_slave_read_byte();
	//是否读失败
	if(!i2c_slave.flag.isSuccess)	return;

	i2c_slave_send_ack();
	
	i2c_slave.rx_data[i2c_slave.rx_offs] = data;
	if(i2c_slave.rx_offs < I2C_SLAVE_RX_DATA_SIZE) i2c_slave.rx_offs++;
	
	if(i2c_slave.flag.isSuccess)
	{
		goto loop;	
	}

	
	i2c_slave.flag.isSuccess = 1;
}



对应的头文件

i2c_slave.h

#ifndef I2C_SLAVE_H_
#define I2C_SLAVE_H_


//===================================================================
//							I2C管脚定义
//===================================================================
#define I2C_SDA       		_pb2 	//管脚状态寄存器,		1:高电平 	0:低电平
#define	I2C_SDA_PC       	_pbc2	//管脚模式寄存器,		1:输入模式	0:输出模式
#define	I2C_SDA_PU       	_pbpu2	//管脚上拉电阻寄存器,	1:使能 		0:禁止

#define I2C_SCL       		_pb1    //管脚状态寄存器,		1:高电平 	0:低电平
#define	I2C_SCL_PC       	_pbc1   //管脚模式寄存器,		1:输入模式	0:输出模式	
#define	I2C_SCL_PU       	_pbpu1  //管脚上拉电阻寄存器,	1:使能 		0:禁止




//I2C的工作状态
#define I2C_STATE_IDLE    	0
#define I2C_STATE_NACK     	1	
#define I2C_STATE_ACK      	2
#define I2C_STATE_START   	3
#define I2C_STATE_STOP   	4
#define I2C_STATE_DEVICE  	5
#define I2C_STATE_TIMEOUT 	6


#define	I2C_SLAVE_ADDRESS		0x5C 	//从机地址


#define	I2C_SLAVE_RX_DATA_SIZE	10 		//接收缓存大小
#define	I2C_SLAVE_TX_DATA_SIZE	10 		//发送缓存大小


//===================================================================
//							数据类型定义
//===================================================================
typedef struct
{
	uint8_t state;				//状态				

	uint8_t	rx_data[I2C_SLAVE_RX_DATA_SIZE];
	uint8_t	rx_offs;

	uint8_t	tx_data[I2C_SLAVE_TX_DATA_SIZE];
	uint8_t	tx_offs;


	struct
	{
		uint8_t isSuccess : 1;  //操作成功标志
		uint8_t RxFinish  : 1;  //接收成功标志
		uint8_t TxFinish  : 1;  //发送成功标志
	}flag;
	
}i2c_slave_t;





//===================================================================
//							变量声明
//===================================================================
extern i2c_slave_t i2c_slave;




//===================================================================
//							函数声明
//===================================================================
void i2c_slave_init();
void i2c_slave_sda_interrupt_callback();
uint8_t i2c_slave_read_byte();
void i2c_slave_send_ack();
void i2c_slave_write_byte(uint8_t val);
void i2c_slave_receive_data();
void i2c_slave_send_data();
void i2c_slave_wait_for_scl(uint8_t level);


#endif 








版权声明:本文为dear_Wally原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。