"); //-->
在单片机的开发过程中,经常会使用IIC接口连接外部传感器获得相应的数据。一旦我们的IIC接口数目较多而单片机固有的IIC接口不够的情况,这时一个单片机普通IO口模拟IIC的做法可以解决我们的尴尬。这篇博客详细的介绍STM32F103的IO口模拟IIC的详细做法。
首先,我们需要认真分析下IIC协议。
IIC协议是需要很严格的划分一个主机和从机。在实际使用过程中,通常控制器为主机,各种传感器为从机。如果是两个控制器之间采用IIC进行数据传输,那么一定要进行主从机的分配,以免因为主从机状态不确定而导致通讯不能正常。
IIC协议规定采用IIC协议进行数据的传输需要两条信号线,一条是时钟时钟信号线,也就是我们常说的SCLK,一条是数据信号线SDA。主从机之间的数据传输完全依靠这两个信号的配合。同时,只有主机才能进行时钟信号的生成,其实这样是为了防止由于时钟的导致数据不能进行传输。
在IIC协议中,从机有唯一的地址,如果从机为一个传感器,通常该地址分为两部分:第一部分为传感器固定好的高四位,第二部分为自己灵活配置的三位A0 ,A1和A2和读写确定位,通过对这三个管脚的配置可实现8个地址的分配和对从机的读写操作。具体怎么实现我们下文分析。
IIC协议是一个真正的多主机总线如果两个或更多主机同时初始化数据传输可以通过冲突检测和仲裁防止数据被破坏,至于其传送速度, 串行的8 位双向数据传输位速率在标准模式下可达100kbit/s 快速模式下可达400kbit/s 高速模式下可达3.4Mbit/s,完全可以满足常规设计需求。
在IIC协议中,需要注意以下四点:
1、开始信号,在时钟高电平期间,数据由高变低时就是为协议开始的信号。
2、结束信号,在时钟高电平期间,数据由低变高时就是为协议结束的信号。
开始和结束信号如下图所示。
图一
3、应答/非应答信号。当主机发送一个字节后从机需要进行一个应答信号,即我们所谓的ASK/NASK信号,以此来判断信号是否完成了传输。
4、数据何时存储何时发送
IIC总线是以串行方式传输数据,从数据字节的最高位开始传送,每一个数据位在SCL上都有一个时钟脉冲相对应。在时钟线高电平期间数据线上必须保持稳定的逻辑电平状态,高电平为数据1,低电平为数据0。只有在时钟线为低电平时,才允许数据线上的电平状态变化,如下图所示:
图二
为深入理解C语言编写的单片机IO模拟IIC程序,利用stm32f103驱动24C256进行说明。
在主机方面,单片机首先要完成管脚的配置:
在宏定义中,由于数字信号用0和1表示数字信息,因此SCL和SDA在数值上只表现为0和1.设置如下。
#define I2C_SCL_0 GPIO_ResetBits(GPIOB,GPIO_Pin_15)
#define I2C_SCL_1 GPIO_SetBits(GPIOB,GPIO_Pin_15)
#define I2C_SDA_0 GPIO_ResetBits(GPIOB,GPIO_Pin_14)
#define I2C_SDA_1 GPIO_SetBits(GPIOB,GPIO_Pin_14)
当SDA为输入的时候,程序需要读取IO口的状态,因此使用GPIO_ReadInputDataBit来读取单片机IO口的状态。
#define RD_I2C_IO GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_14)
同时,为了程序在运行过程中状态的稳定,需要设置一个延时的参数,宏定义如下
#define DF_I2C_TCY 2
//初始化IIC,在开始传输时保持时序的稳定,需要将SCL和SDA都设置为高电平,其中FM24C256的SCL 与PB15 连接, SDA与PB14连接。C程序如下:
GPIO_InitTypeDef GPIO_InitStruct_I2C;
void IIC_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE );
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14|GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD ; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_14|GPIO_Pin_15); //PB14,PB15 输出高
}
当配置好之后,需要设置时钟和数据的方向问题,因为时钟只是主机输出,而数据SDA需要具有输出和输入功能的切换,C预言具体实施如下:
配置SCL为输出的C语言格式:
void I2C_SCL_OUT(void)
{
GPIO_InitStruct_I2C.GPIO_Pin = GPIO_Pin_15;
GPIO_InitStruct_I2C.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStruct_I2C.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStruct_I2C);
}
配置 SDA为输出的C语言格式:
void I2C_SDA_OUT(void)
{
GPIO_InitStruct_I2C.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStruct_I2C.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStruct_I2C.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStruct_I2C);
}
而SDA在读从机的时候,需要将SDA设置为为输入模式,因此该管脚需要设置为输入模式,C语言程序如下:
void I2C_SDA_IN(void)
{
GPIO_InitStruct_I2C.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStruct_I2C.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStruct_I2C.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStruct_I2C);
}
设置好上面的参数后,IIC的传输功能函数基本完成,现在开始进入协议构建阶段。
在图一中,我们看出IIC起始条件为SCL为高电平的时候,SDA由1突变为0;C语言实现程序如下:
void Start_I2C(void)
{
I2C_SDA_OUT();
I2C_SCL_OUT();
I2C_SDA_1;
I2C_SCL_1;
delay_us(DF_I2C_TCY);
I2C_SDA_0;
delay_us(DF_I2C_TCY);
I2C_SCL_0;
}
完成IIC启动后,将SCL保持低电平,用于SDA数据的加载,在协议中,只有在SCL为低电平阶段,才能进行SDA数据的变换。
结束IIC传送数据,在SCL高电平阶段,SDA由0变为1.而SDA数据的加载需要在SCL为低电平0阶段进行。
void Stop_I2C(void)
{
I2C_SDA_OUT();
I2C_SCL_OUT();
I2C_SCL_0;
I2C_SDA_0;
delay_us(DF_I2C_TCY);
I2C_SCL_1;
delay_us(DF_I2C_TCY);
I2C_SDA_1;
I2C_SDA_IN();
}
在IIC程序设计中,都是以8bit为基础进行数据的传输。在主机发送时候,也是每次发送一字节,这个模块的设计流程为移位操作,具体的C语言程序设计如下:
void SendByte_I2C(u8 shu)
{
u8 i;
I2C_SDA_OUT();
I2C_SCL_OUT();
for(i=0;i<8;i++)
{
I2C_SCL_0;
if(shu&0x80)
{
I2C_SDA_1;
}
else
{
I2C_SDA_0;
}
shu = shu<<1;
delay_us(DF_I2C_TCY);
I2C_SCL_1;
delay_us(DF_I2C_TCY);
}
I2C_SCL_0;
}
基本的思路为:SCL在为0时,可以进行SDA数据的配置,当SCL为1时,SDA数据一定要锁定。其次为数据的移位,将待发送数据与0x80进行与运算,获得最高位的数据,通过8次循环完成1byte的数据发送。
在IIC接收接收一字节的程序中,也是以移位的方式进行,注意此时需要将SDA端口设置为输入模式,读取单片机IO口的状态进行数据的获取。具体C语言程序设计如下:
u8 RcvByte_I2C(void)
{
u8 c;
u8 i;
c = 0x00;
I2C_SCL_OUT();
I2C_SDA_IN();
I2C_SDA_1;
for(i=0;i<8;i++)
{
I2C_SCL_0;
delay_us(DF_I2C_TCY);
I2C_SCL_1;
delay_us(DF_I2C_TCY);
c = c<<1;
if(RD_I2C_IO)
{
c = c + 0x01;
}
I2C_SCL_0;
delay_us(DF_I2C_TCY);
}
I2C_SCL_OUT();
I2C_SCL_0;
return(c);
}
当IIC进行主机获取数值时,主机需要等待从机的应答信号,以此来判断从机是否完成了数据的接收。从主机方看,为IIC等待ASK函数,具体C语言程序设计如下:
IIC_Wait_Ack(void)
{
u8 ucErrTime=0;
I2C_SDA_IN(); //SDA数据输入
I2C_SCL_OUT();
delay_us(DF_I2C_TCY);
I2C_SCL_1;
delay_us(DF_I2C_TCY);
I2C_SDA_IN();
while(RD_I2C_IO)
{
ucErrTime++;
if(ucErrTime>255)
{
Stop_I2C();
return 1;
}
}
I2C_SCL_0;
I2C_SDA_IN();
return 0;
}
在该函数中,通过 延时等待从机的ACK是否发送出来,如果发送出来,则函数返回0,主机可继续发送数据,如果返回1,则从机没有应答,此时需要停止IIC数据传输。防止出现错误数据。
由于IIC为双向数据通信,当从机发送完数据,主机也需要发送应答信号来说我接收到你的信息了,此时从机才可变为接收状态,接收来自主机的数据。C语言程序如下:
void ACK_I2C(void)
{
u16 t;
t = 255;
I2C_SDA_IN();
I2C_SCL_OUT();
I2C_SDA_1;
delay_us(DF_I2C_TCY);
I2C_SCL_1;
delay_us(DF_I2C_TCY);
I2C_SDA_IN();
while(RD_I2C_IO)
{
t--;
if(t==0)
{
Stop_I2C();
return;
}
}
I2C_SDA_IN();
I2C_SCL_0;
}
当IIC程序运行到主机读取从机数据完成,需要停止此次数据传输时,主机发送一个发出主无应答信号,从机接收到后就停止发送数据,之后主机即可发送停止信号,停止此次数据的传输。C语言程序设计如下:
void NACK_I2C(void)
{
I2C_SDA_OUT();
I2C_SCL_OUT();
I2C_SDA_1;
delay_us(DF_I2C_TCY);
I2C_SCL_1;
delay_us(DF_I2C_TCY);
I2C_SCL_0;
I2C_SDA_IN();
}
在IIC程序设计中,需要向从机的某个地址进行数据的写入,该函数通常将将写入的地址和写入的数据作为参数,通过上面IIC功能模块函数完成数据一个字节的写入,C语言程序设计如下:
void WR_ByteI2C(u16 add,u8 shu)
{
u8 cHByte;
u8 cLByte;
u8 cmd;
cmd = 0xa0;//地址信息,从机的地址,本例程中为eeprom的从机地址
cHByte = add/256;
cLByte = add%256;
Start_I2C();//启动
SendByte_I2C(cmd);//发写命令
ACK_I2C();
SendByte_I2C(cHByte);//发写地址
ACK_I2C();
SendByte_I2C(cLByte);//发写地址
ACK_I2C();
SendByte_I2C(shu);
ACK_I2C();
Stop_I2C();
}
对于IIC读取从机一个地址的数据,需要将从机待读取地址作为参数,返回为读取到的数据,具体C语言程序如下:
u8 RD_ByteI2C(u16 add)
{
u8 c;
u8 cHByte;
u8 cLByte;
u8 cmd;
cmd = 0xa0;
cHByte = add/256;
cLByte = add%256;
Start_I2C();//启动
SendByte_I2C(cmd);//发写命令
ACK_I2C();
SendByte_I2C(cHByte);//发写地址
ACK_I2C();
SendByte_I2C(cLByte);//发写地址
ACK_I2C();
Start_I2C();//启动
SendByte_I2C(cmd+1);//
ACK_I2C();
c = RcvByte_I2C();
NACK_I2C();
Stop_I2C();
return(c);
}
从机方面,FM24c256为存储器,在硬件电路上设置好地址后,调用前面写好的函数即可实现数据的读写:
具体的从AT24CXX指定地址读出一个数据,具体C语言程序设计如下(该过程需要对IIC协议有明确的认识,单字节传送,多字节传送):
u8 AT24CXX_ReadOneByte(u16 ReadAddr)
{
u8 temp=0;
Start_I2C();
SendByte_I2C(0XA0); //发送写命令1010 000 R/W
IIC_Wait_Ack();
SendByte_I2C(ReadAddr>>8);//发送高地址
IIC_Wait_Ack();
SendByte_I2C(ReadAddr%256); //发送低地址
IIC_Wait_Ack();
Start_I2C();
SendByte_I2C(0XA1); //进入接收模式
IIC_Wait_Ack();
temp=RcvByte_I2C();
Stop_I2C();//产生一个停止条件
return temp;
}
对于在AT24CXX指定的地址写入一个数据,只要一句IIC的协议操作,即可完成, void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite)
{
Start_I2C();
if(EE_TYPE>AT24C16)
{
SendByte_I2C(0XA0); //发送写命令
IIC_Wait_Ack();
SendByte_I2C(WriteAddr>>8);//发送高地址
}else
{
SendByte_I2C(0XA0+((WriteAddr/256)<<1)); //发送器件地址0XA0,写数据
}
IIC_Wait_Ack();
SendByte_I2C(WriteAddr%256); //发送低地址
IIC_Wait_Ack();
SendByte_I2C(DataToWrite); //发送字节
IIC_Wait_Ack();
Stop_I2C();//产生一个停止条件
delay_ms(10);
}
其实,对于单片机IO口模拟IIC接口是一个古老而又常见的问题,在IIC接口不够的情况下,模拟IIC接口是常用的方法。
不论是什么情况,IO口模拟也好,直接使用特定IIC接口也好,只有对IIC协议有深刻的理解,才能将程序完好的写出来。希望通过这篇博文,能让你明白其中的道理。
*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。
eleaction01 阅读:2954