前面把软件模拟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》和例程