续:关于stm32通信协议:软件模拟SPI、软件模拟I2C的总结(fishing_8)

软件模拟I2C协议

前面把软件模拟SPI的代码贴完了,再接着贴软件模拟I2C的代码,我跑的实验是通过软件模拟I2C读写EEPORM(AT24C02),代码已经调通了的。

同样,首先是I2C的GPIO引脚初始化,这里要注意的是,引脚配置成输出模式(开漏输出),这是由I2C协议本身决定的。

static void GPIO_I2C_Init(void)
{
  /*定义一个GPIO_InitTypeDef类型的(基本IO)结构体*/
  GPIO_InitTypeDef  GPIO_InitStructure; 
  
/***** 使能 GPIO 时钟 *****/
	
 /* 使能 I2C引脚的GPIO时钟< EEPROM_I2C_SCL_GPIO_CLK and EEPROM_I2C_SDA_GPIO_CLK> */
 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);


  /* < 配置 EEPROM_I2C_1 引脚: I2C_SCL and I2C_SDA > */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;	
 /* 设置引脚模式为 输出功能*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; 
/* 设置引脚速率为50MHz */   
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;   
 /* 设置引脚类型为开漏输出*/
 GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;
 /* 设置引脚为无上拉 下拉模式*/
 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;  
	
 /* 调用库函数,使用上面配置的GPIO_InitStructure初始化GPIO_B*/
GPIO_Init(GPIOB, &GPIO_InitStructure);
	
Soft_I2CStop();//先给一个停止信号,复位I2C总线上所有的设备到待机模式
}

起始信号、停止信号,这里延时用的软件延时:

//I2C起始信号。在SCL高电平情况下,SDA由高到低
void Soft_I2CStart(void)
{
	GPIO_SetBits(GPIOB, GPIO_Pin_7);//SDA高
	GPIO_SetBits(GPIOB, GPIO_Pin_6);//SCL高
	Soft_delay(30);
	
	GPIO_ResetBits(GPIOB, GPIO_Pin_7);//SDA低
	Soft_delay(30);
	
	GPIO_ResetBits(GPIOB, GPIO_Pin_6);//SCL低
	Soft_delay(30);
}

//I2C停止信号。在SCL高电平情况下,SDA由低到高
void Soft_I2CStop(void)
{
	GPIO_ResetBits(GPIOB, GPIO_Pin_7);//SDA低
	GPIO_SetBits(GPIOB, GPIO_Pin_6);//SCL高
	Soft_delay(30);
	
	GPIO_SetBits(GPIOB, GPIO_Pin_7);//SDA高
}

等待从设备响应信号函数:

//等待从设备响应信号,返回0表示正常,返回1表示异常
uint8_t Soft_I2CWaitAck(void)
{
	uint8_t AckFlag;
	
	GPIO_SetBits(GPIOB, GPIO_Pin_7);//SDA高 //CPU释放SDA总线,由从设备控制响应
	Soft_delay(30);
	
	GPIO_SetBits(GPIOB, GPIO_Pin_6);//SCL高
	Soft_delay(30);
	
	if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7))//读SDA上引脚电平
	{
		AckFlag = 1;
	}
	else
	{
		AckFlag = 0;
	}
	GPIO_ResetBits(GPIOB, GPIO_Pin_6);//SCL低
	Soft_delay(30);
	return AckFlag;
}

MCU发出应答信号、非应答信号函数:

//CPU发出响应信号
void Soft_I2CAck(void)
{
	GPIO_ResetBits(GPIOB, GPIO_Pin_7);//SDA低,响应为低电平,继续接收
	Soft_delay(30);
	
	GPIO_SetBits(GPIOB, GPIO_Pin_6);//SCL高
	Soft_delay(30);
	
	GPIO_ResetBits(GPIOB, GPIO_Pin_6);//SCL低
	Soft_delay(30);
	
	GPIO_SetBits(GPIOB, GPIO_Pin_7);//SDA高 //cpu释放SDA总线
	
}

//CPU发出非响应信号
void Soft_I2CNAck(void)
{
	GPIO_SetBits(GPIOB, GPIO_Pin_7);//SDA高,响应为高电平,停止接收
	Soft_delay(30);
	
	GPIO_SetBits(GPIOB, GPIO_Pin_6);//SCL高
	Soft_delay(30);
	
	GPIO_ResetBits(GPIOB, GPIO_Pin_6);//SCL低
	Soft_delay(30);
}

基本的写一个字节和读一个字节:

//写(发送)一个字节
void Soft_I2CWrite(uint8_t Txdata)
{
	uint8_t i;
	for(i = 0;i < 8;i++)
	{
		if(Txdata & 0x80)
		{
			GPIO_SetBits(GPIOB, GPIO_Pin_7);//SDA高
		}
		else
		{
			GPIO_ResetBits(GPIOB, GPIO_Pin_7);//SDA低
		}
		Soft_delay(30);//SDA线上,准备好数据,在SCL为高电平时读取
		
		GPIO_SetBits(GPIOB, GPIO_Pin_6);//SCL高
		Soft_delay(30);
		
		GPIO_ResetBits(GPIOB, GPIO_Pin_6);//SCL低。为SDA切换数据做准备
		Soft_delay(30);
		
		if(i == 7)//释放SDA总线控制权
		{
			GPIO_SetBits(GPIOB, GPIO_Pin_7);//SDA高
		}
		
		Txdata <<= 1;
		Soft_delay(30);
	}
}

//读(接收)一个字节
uint8_t Soft_I2CRead(void)
{
	uint8_t i;
	uint8_t Rxdata = 0;
	
	for(i = 0;i < 8;i++)
	{
		Rxdata <<= 1;//第一位0左移还是0.		
		
		GPIO_SetBits(GPIOB, GPIO_Pin_6);//SCL高。数据的有效性,在SCL为高电平时,数据有效
		Soft_delay(30);
		
		if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7))
		{
			Rxdata |= 0x01;
		}
		
		GPIO_ResetBits(GPIOB, GPIO_Pin_6);//SCL低。为SDA切换电平做准备
		Soft_delay(30);		
		
	}
	return Rxdata;
}

最后,才是发送函数、接收函数:

void Soft_I2CSendBytes(uint8_t *_pWriteBuf, uint16_t Address, uint16_t ByteSize)
{
	uint16_t i;
	uint16_t InAddr,m;
	
	InAddr = Address;
	
	for(i = 0;i < ByteSize;i++)//当发送的第一个字节或是每页首地址,需要重新发送启动信号和地址(EEPROM数据手册中,页写入不能自动转到下一页)
	{
		if((i == 0) || ((InAddr & 7) == 0))//这里的&7很巧妙,确定是每页的首地址
		{
			Soft_I2CStop();
			
			for(m = 0;m < 1000;m++)
			{
				Soft_I2CStart();//起始信号
				
				Soft_I2CWrite(0xA0|0);//写设备地址
				
				if(Soft_I2CWaitAck() == 0)
				{
					break;
				}		
			}	
			if(m == 1000)
			{
				printf("EEPROM写入超时 ,下面执行的无意义");
			}			
			
			Soft_I2CWrite((uint8_t)InAddr);//写数据地址
			
			if(Soft_I2CWaitAck() != 0)
			{
				printf("EEPROM未响应2 \r\n");
			}					
		}
		
		Soft_I2CWrite(_pWriteBuf[i]);//发送数据
		
		if(Soft_I2CWaitAck() != 0)//等待应答
		{
			printf("写入错误,EEPROM未响应\r\n");
		}	
		
		InAddr++;//写入地址自加一

	}

	Soft_I2CStop();
}

void Soft_I2CReceiveBytes(uint8_t *_pReadBuf, uint16_t Address, uint16_t ByteSize)
{
	uint16_t i;
	
	Soft_I2CStart();//起始信号
	
	Soft_I2CWrite(0xA0|0);//写设备地址
	
	if(Soft_I2CWaitAck() != 0)//等待应答
	{
		printf(" 读操作出错,EEPROM未响应地址 1\r\n");
	}
	
	Soft_I2CWrite((uint8_t)Address);//写数据地址
	
	if(Soft_I2CWaitAck() != 0)
	{
		printf(" 读操作出错,EEPROM未响应地址 2\r\n");
	}	
	
	Soft_I2CStart();//反复起始信号

	Soft_I2CWrite(0xA0|1);//读设备地址
	
	if(Soft_I2CWaitAck() != 0)
	{
		printf(" 读操作出错,EEPROM未响应地址 3\r\n");
	}		
	
	for(i = 0;i < ByteSize;i++)
	{
		_pReadBuf[i] = Soft_I2CRead();//读数据
		
		if(i != ByteSize - 1)
		{
			Soft_I2CAck();//给从机应答
		}
		else
		{
			Soft_I2CNAck();//接收到最后一个,给从机非应答
		}
	}
	
	Soft_I2CStop();//停止信号
}

I2C协议的写操作:产生起始信号→写从设备地址,等待应答→写数据地址,等待应答→发送数据,等待应答→产生结束信号。

写操作比较好理解,要多注意的其实是读操作,如下:

I2C协议的读操作:产生起始信号→写从设备地址,等待应答→写数据地址,等待应答→产生起始信号→读设备地址,等待应答→读数据,并应答→产生结束信号。

读操作首先要写入设备地址和数据地址,然后才是读方向,而且读的时候记得要给从机应答。

**释放总线的话,就是将SDA线置高电平,因为I2C协议本身有上拉电阻的存在,所以空闲的时候SDA线、SCL线均是高电平,占用总线的时候就是低电平,用完后就得手动将电平置高。**这其实也就解释了为什么要用开漏模式,因为开漏模式只能产生低电平或者高阻状态,所以得外接上拉电阻将电平拉高,这也就解释I2C的物理连接。网上有讲的很好的文章,为什么使用开漏模式以及上拉,这方面可以去网上查查,我讲不太清楚。

建议有时间的,可以自己动手实现一下软件模拟SPI和软件模拟I2C,过一遍,会加深自己对协议的理解和掌握程度。

说明:这个软件模拟I2C读写EEPROM的代码,秉火的例程有提供完整的,所以我只是照着写了一遍,然后搬运上来,说下心得体会,勿喷;前面写的软件模拟SPI读写FLASH的代码,因为没有现成的,就自己写了下,过了一遍,参考了网上的代码和秉火的例程;主函数都没有贴,因为要注意的点都在子函数中有说。

参考资料:
秉火的《零死角玩转stm32——F429》和例程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值