在STM32的库函数应用中,很少使用硬件I2C,大部分时间我们都会应用模拟IO的高低电平,来仿I2C总线。这是因为,首先模拟IO的灵活性大,任意两个IO就可以;其次,硬件I2C的BUG比较多,偶会会出现不知名问题和故障。那么,从这章开始我们学习模拟I2C来通信AT24C02。
- IO的初始化
代码如下:
void eeprom_I2C_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/*使能时钟 */
RCC_APB2PeriphClockCmd (RCC_APB2Periph_GPIOB, ENABLE);
/*****************这里可以是任意IO*******************/
/* 初始化SCL和SDA*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 ;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // SCA
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // SDA
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
固定的初始化格式,模拟IO的时候 IO的模式为 开漏输出。其他根据引脚来配置。
二.I2C总线的GPIO模拟
- 我们首先看下I2C总线协议,我们知道I2C总线每次通信的开始,都是来源于起始信号;每次通信的结束来源于停止信号。如下图1所示。

图1 I2C的开始和停止信号
从图1中我们看到,开始信号是在SCL为高电平的时候,SDA从高电平到低电平;停止信号与开始信号相反,SLC为高电平的时候,SDA从低电平到高电平。
首先在在对应的.h文件中定义宏:
#define eeprom_I2C_SCL_Set GPIO_SetBits(GPIOB, GPIO_Pin_6) #define eeprom_I2C_SCL_Reset GPIO_ResetBits(GPIOB, GPIO_Pin_6) #define eeprom_I2C_SDA_Set GPIO_SetBits(GPIOA, GPIO_Pin_6) #define eeprom_I2C_SDA_Reset GPIO_ResetBits(GPIOA, GPIO_Pin_6) #define eeprom_I2C_SDA_Read() GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6)
我们定义 ..Set 为高电平;..Reset为低电平;..Read()为读取IO状态。
有了宏定义,我们根据图1,模拟开始信号,代码如下:
void eeprom_I2C_Start(void)
{
/*****I2C的起始信号:SCL在高电平时候,SDA从高电平到低电平,然后SCL变低电平***********/
eeprom_I2C_SDA_Set; // SDA至高电平
eeprom_I2C_SCL_Set; // SCL至高电平
delay_for_eeprom(); // 这里的延迟是考虑,CPU执行快,需要等待上面完成结果。
eeprom_I2C_SDA_Reset; // SDA产生低电平
delay_for_eeprom();
eeprom_I2C_SCL_Reset; // 拉低SCL
delay_for_eeprom();
}
中间延迟函数,是非精准延迟函数,根据经验测试而来,大家可以自己修改时间进行尝试,主要为了等待执行结果。图1中起始信号最后并没有拉低SCL信号,这里拉低了是因为按照SCL顺序都是持续循环的来,我们顺着周期来也就是该低电平了。
停止信号代码如下:
void eeprom_I2C_Stop(void)
{
/*****I2C停止信号:SCL在高电平时候,SDA从低电平拉高*************/
eeprom_I2C_SDA_Reset;
eeprom_I2C_SCL_Set;
delay_for_eeprom();
eeprom_I2C_SDA_Set;
}
2. 有了其实启停信号后,我们还需要接收数据、发送数据、接收应答信号和发送应答信号。在看这些信号前,同样的我们先看I2C通信协议,我们首先看图2的数据有效性。

图2 I2C数据的有效性
从图2中可知,只有当SCL在高电平的时候,SDA的电平才会有效,在SCL为低的时候,SDA电平无效,也就是说,在SCL为低的时候,我们会变换SDA电平。
有了数据有效性的概念我们就可以学习,数据的发送和接收了。我们知道I2C每一次发送数据或者接收数据都是1个字节,8bit;我们首先看发送1个字节数据,代码如下:
void eeprom_I2C_SendByte(unsigned char Send_Buffer)
{
unsigned char i;
for(i=0; i<8; i++) //一个字节8位
{
if(Send_Buffer & 0x80) // 0x80==1000 0000 也就是说罢Send_Buffer的最高位取出来
{
eeprom_I2C_SDA_Set; //最高位为1 发送高
}
else
{
eeprom_I2C_SDA_Reset; //最高位为0,发送低电平
}
delay_for_eeprom();
/********考虑数据的有效性*************/
eeprom_I2C_SCL_Set; //数据都是在SCL高电平时候有效,在高电平时发送。
delay_for_eeprom();
eeprom_I2C_SCL_Reset; //数据在SCL低电平时候 进行电平转换, 发送完毕后 进行拉低。
if (i==7)
{
eeprom_I2C_SDA_Set; // 最后一位数据发送完毕后,总线拉高,表示总线空闲
}
Send_Buffer<<= 1; // 左移1位 把下一位 移到最高位,进行发送前的准备。 下次循环开始进行判断最高位电平。
delay_for_eeprom();
}
}
待发送的数据Send_Buffer,我们需要1位1位的发送,那么我们就需要把数据的每一位取出来然后发送出去。Send_Buffer&0x80 这个就是把最高位电平取出来。0x80二进制为10000000;Send_Buffer的最高位和10000000相与,Send_Buffer最高位为高即为高,为低即为低。发送后Send_Buffer向左位移1位,这样就这Send_Buffer第二位就位移到最高位在于0x80相与,如此循环直到Send_Buffer发送完毕。其他部分注释很明白了。
同样的我们接收数据道理是一样的。代码如下:
unsigned char eeprom_I2C_ReadByte(void)
{
unsigned char i;
unsigned char Read_Data=0;
for(i=0; i<8; i++) //一个字节8位 开始读取
{
Read_Data<<= 1; //数据左移1位 为下一位数据做准备。
eeprom_I2C_SCL_Set; //数据有效性,高电平 数据有效。
delay_for_eeprom();
if(eeprom_I2C_SDA_Read()) //读取当前电平值
{
Read_Data = Read_Data + 0x01; //高电平时 +1
// Read_Data++; //与上面相同
}
else
{
Read_Data = Read_Data; //这一句可以省略
}
eeprom_I2C_SCL_Reset; //拉低SCL
delay_for_eeprom();
}
return Read_Data; //取完8bit数据后,返回该数据
}
每读取一次电平,存放到Read_Data,然后左移一位,然后在存放到Read_Data,直到循环完成。
3.我们在看下ACK应答信号的产生和非应答ACK信号的产生。ACK应答信号产生图3:

图3 ACK产生图
如图3所示,ACK应答信号的产生就是,在发送数据后,数据线拉低,并保持在SCL高电平期间。反之,非应答信号是在发送数据后,数据线保持高电平。因此,应答信号代码如下:
void eeprom_I2C_Ack(void)
{
eeprom_I2C_SDA_Reset; //产生应答信号
delay_for_eeprom();
eeprom_I2C_SCL_Set; //数据的有效性
delay_for_eeprom();
eeprom_I2C_SCL_Reset; //数据的有效性
delay_for_eeprom();
eeprom_I2C_SDA_Set; //释放掉控制权
}
非应答信号的产生代码如下:
void eeprom_I2C_NAck(void)
{
eeprom_I2C_SDA_Set; //产生非应答信号
delay_for_eeprom();
eeprom_I2C_SCL_Set; //数据的有效性
delay_for_eeprom();
eeprom_I2C_SCL_Reset; //数据的有效性
delay_for_eeprom();
}
最后我们还需要读取从机发送过来应答信号,前面我们知道ACK信号的产生,那么只需要读取SDA是否拉低就可以了。
代码如下:
signed char eeprom_I2C_ReadAck(void)
{
signed char re_ack;
eeprom_I2C_SDA_Set; //释放掉控制权
delay_for_eeprom();
eeprom_I2C_SCL_Set; //数据的有效性
delay_for_eeprom();
if(!eeprom_I2C_SDA_Read()) //如果是低电平
{
re_ack = 1; //接收到应答信号
}
else re_ack = 0; //如果是高电平 ,表示无应答信号
eeprom_I2C_SCL_Reset; //数据的有效性
delay_for_eeprom();
return re_ack;
}
其中re_ack=1时候 表示读取应答成功,反之失败。
到这里,模拟I2C基本状态我们已经学习完毕了,下一章节,我们在这基础上,学习完成 任意数据的读取和任意数据的写入,思维和硬件I2C是一样的。具体我们下章节见。
原创文章,作者:小峰,如若转载,请注明出处:http://www.wfblog.com/archives/402.html

