文章目录
- 前言
- AT24C02简介
- 引脚介绍
- 器件寻址
- 寻址流程
- 器件地址的组成
- 其他I2C器件的地址组成
- 通信流程
- 1.完整的传输过程
- 2.初始化管脚
- 3.起始信号
- 4.停止信号
- 5.MCU发送8位数据
- 5.MCU接收应答位
- 6.MCU接收八位数据
- 7.MCU发送应答位
- AT24C02写一个字节
- AT24C02读一个字节
- 实现的效果
- 总结
前言
前面已经介绍了使用GPIO模拟时序驱动WS2812B和DHT11的模块,这些模块都是单线传输的方式,本文我们继续使用模拟的方式来实现一下常用的IIC驱动时序的模拟,具体使用到的器件是AT24C02。
AT24C02简介
AT24C02是一种串行EEPROM ,它具有2K bit的存储容量,可以存储256个byte的数据。AT24C02采用12C总线协议进行通信, 具有低功耗、高可靠性和易于使用的特点。它广泛应用于各种电子设备中,如智能卡、存储器扩展模块、工 业控制系统。
AT24CXX是一个系列,xx的不同,代表了芯片的存储容量不同,根据下方的介绍,可以知道,使用的这款AT24C02具有页写的功能,每页可以写入8个字节,一共是256/8=32页。
另外,手册还介绍了AT24C02的三种封装,分别是8脚的直插封装、8脚的贴片封装、以及5脚的贴片封装。这个板子上选择是SOP-8的贴片型。
引脚介绍
既然提到了封装,那么就接着介绍一下这个8脚封装各个引脚的作用吧,如下图所示:
其中位于芯片左侧的A0-A2是三个器件地址的输入引脚,AT24C02使用的是I2C的通信方式,因此会有通信地址,I2C的通信地址一般都是由多个位拼接组成的八位数据,具体组成放到后面的器件寻址来介绍。
然后是SDA、SCL想必大家对于这俩引脚都很熟悉了,SDA,I2C通信的数据脚,SCL,I2C的时钟脚,用于MCU给AT24C02提供同步通信得时钟基准。
然后是WP,写保护,这个引脚的作用是用于实际产品,写入数据后开启,避免被其他人给修改了。这里没有开启,因为在编程过程中需要用到和修改,所以就接地允许正常写入操作。
最后是VCC、GND,为了保证通信得速率,这里最好还是选用5V的供电,这样能保证AT24C02能接收的时钟频率尽可能高,但笔者这里使用的是3.3V的供电,也够用了。
器件寻址
寻址流程
I2C是一种通信总线,同一组I2C的数据线可以挂接多个从器件,如上图的Slave1,和Slave2,MCU通过不同的器件地址进行选择具体的通信对象;当MCU需要在地址位89的从机种读取数据时,就会线发送一个89+R,然后总线上的设备识别是否是自己的地址,不是就不管,是就会给主机一个应答,进而进行后续的数据交换。
器件地址的组成
前面提到了这里发送的器件地址是有多个位拼接而成的。为了方便理解,先看一个逻辑分析仪抓取的I2C写数据的时序图。
其中第一个八位数据就是通信地址。
为了方便理解,放大一点来看,如下图所示,这个八位数据的地址一共分为了三个部分,
1、前四位为一个部分(玫红色的区域),这四位是由厂商固定的,例如这里的AT24C02对应的就是“1010”,这部分需要根据不同厂商的芯片手册去查看;
2、紧接着的三位分别是A2、A1、A0,有没有很眼熟,在上面的引脚介绍中就提到了,这三位是由用户的电路设计决定的,目的就是留给用户来设计,实现一个总线挂接多个从机的功能,由于这个板子只是用到了一个AT24C02所以此处在硬件上直接给了“000”,当然也可以是000—111中间的任意一个,根据对应的电平拉高拉低对应的引脚即可;
3、最后一位是读写位,当最后一位是0时,表示是写入数据,最后一位是1时,表示是读取数据。
写数据时的地址
读数据时的地址
厂家设计的地址。
其他I2C器件的地址组成
当然,不同的IIC设备的地址构成会有一些细小的差异,但是总体上是一致的,最后一位控制读写,前面7位是实际的地址。
这里在举一个常见的例子。下图所示是SHT20温湿度传感器的引脚介绍,可以发现,虽然他也是使用I2C通信,但是它并没有预留上面AT24C02的物理地址设置位,也就是说,它不能通过IO口来设置通信地址。
那么其通信地址是怎么组成的呢,在芯片手册中提到了,它的七位地址是“1000 000”,后面再接一位读写位就组成了SHT20的读写地址。
好了,搞定了这些东西后,就该来模拟时序对芯片进行操作了。
通信流程
1.完整的传输过程
对于这类使用标准协议驱动的外设,其控制时序都是遵循对应的协议时序,这里再简单回顾一下I2C通信得完整数据传输过程。
如下图所示:
整个通信过程包括了开始信号、器件地址、读写位、应答位、数据位、以及停止信号。需要注意这里的应答位,当MCU发送数据时,应答位有AT24C02产生,MCU需要检测应答位,而当AT24C02上传数据到MCU时,MCU每次接收完数据要输出一个ACK或者非ACK给AT24C02,以便AT24C02用来判断是否继续发送数据。
还是用真实波形来看吧。
下图是实际使用MCU读取AT24C02第10个字节的数据的通信过程,使用的是数据手册中的随机读时序图。
1、首先MCU控制产生一个起始信号;
2、紧接着发送八位数据的地址信息(写地址);
3、地址匹配的AT24C02发送一个应答位;
4、MCU写入需要读取的字节所在的字地址(10号地址(0x0a));
5、AT24C02接收到字地址后返回一个应答信号;
6、MCU在此发送起始信号;
7、MCU发送读数据8位地址;
9、地址匹配的AT24C02发送一个应答位;
10、AT24C02开始发送10号字地址的数据(0x41);
11、MCU发送一个NACK,告诉从机不用继续发送了;
12、MCU发送停止信号,结束本次通信。
根据上面的通信流程,可以将整个通信流程分为多个部分来分别实现,接下来就结合代码来进行拼凑。
2.初始化管脚
可以发现,I2C作为一个半双工通信,与前面的1-Wire有着一些类似,而且I2C的SCL与SDA也都有上拉电阻,MCU连接SDA的引脚也是既要做输入,又要做输出,为了避免来回切换模式,当然最好初始化为开漏模式。
而MCU与SCL相连接的引脚,它起到的作用是提供时序边沿,让主从机有一个同步时序。时钟信号可以看做是一个一个高低脉冲产生的,因此与SCL相连的管脚只需要初始化为推挽输出模式即可。
查询原理图可以知道,使用的引脚是:
SCK:PB8 ----- 通用推挽输出
SDA:PB9 ----- 通用开漏输出
于是初始化GPIO的代码如下:
/*********************************
函数名:Iic_Pin_Init
函数功能:IIC引脚初始化
形参:void
返回值:void
备注:
SCK:PB8 ----- 通用推挽输出
SDA:PB9 ----- 通用开漏输出 既要输出也要输入
**********************************/
void Iic_Pin_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//初始化GPIOB端口的时钟
//gpio配置
GPIO_InitTypeDef GPIO_InitStructure; //定义了一个结构体变量
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//通用推挽输出
GPIO_Init(GPIOB,&GPIO_InitStructure );
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;//开漏输出
GPIO_Init(GPIOB,&GPIO_InitStructure );
//空闲状态
IIC_SDA_H;
IIC_SCL_H;
}
3.起始信号
接下来就是控制两个IO实现各个部分的时序模拟了。首先是起始信号,先看手册中的描述:
起始信号由MCU产生,信号条件是:
在 SCL 线是高电平时 SDA 线从高电平向低电平切换,表示起始信号;
但是上面的图中没有提到具体的时间要求,是没有要求吗,还是说随便设置,答案是否定的。对于这种标准的通信协议是由严格的时序要求的,尤其是I2C,不能随便给值。在芯片手册种有如下要求:
为了保险,选择5us代码如下:
/*********************************
函数名:Iic_Start
函数功能:IIC的起始条件
形参:void
返回值:void
备注:
**********************************/
void Iic_Start(void)
{
//准备条件
IIC_SCL_L; //为了让数据线高电平
Systick_Delay_us(5);
IIC_SDA_H;
//起始条件
IIC_SCL_H;
Systick_Delay_us(5);
IIC_SDA_L; //输出低电平
Systick_Delay_us(5);
IIC_SCL_L; //安全模式
}
4.停止信号
SCL 是高电平时,SDA 线由低电平向高电平切换表示停止条件;与起始信号一样,停止信号一般也是由MCU产生的。同样具体的时间在数据手册种会给出,(不同厂家的数据手册这里的时间可能不太一样,根据自己芯片的手册微调即可,大于最小时间就可以了)。
这里是4us,我们再留点余量,选个5us。
/*********************************
函数名:Iic_Stop
函数功能:IIC的停止条件
形参:void
返回值:void
备注:
**********************************/
void Iic_Stop(void)
{
//准备条件
IIC_SCL_L; //为了让数据线高电平
Systick_Delay_us(5);
IIC_SDA_L;
//停止条件
IIC_SCL_H;
Systick_Delay_us(5);
IIC_SDA_H;
Systick_Delay_us(5);
}
5.MCU发送8位数据
起始信号后面紧接着的就是8位数据,这里数据逻辑0和逻辑1就是高低电平,不再是前面WS2812B与DHT11的那种特殊格式了。
那么怎样发送数据位呢,来看手册中的解释,简单来说就是SCL高电平读取数据,SCL低电平写入数据。
同样的,高低点的持续时间也是有要求的。
知道了一个位的写入方式,8位数据的发送直接上一个循环就可以了。
/*********************************
函数名:Iic_Send_Data
函数功能:IIC发送数据
形参:u8 data 待发送的数据
返回值:void
备注:
**********************************/
void Iic_Send_Data(u8 data)
{
u8 i;
for(i=0;i<8;i++)
{
IIC_SCL_L; //拉低时钟线发送数据
Systick_Delay_us(5);
if(data & (0x80>>i))
{
IIC_SDA_H;
}
else
{
IIC_SDA_L;
}
IIC_SCL_H; //主机帮从机拉高时钟线
Systick_Delay_us(5);
}
IIC_SCL_L; //安全模式
}
5.MCU接收应答位
MCU发送数据后,AT24C02,每次接收到8位数据就会产生一个应答信号,MCU需要检测AT24C02有没有产生应答;前面一直是MCU发送数据占用数据线,此时,需要交给AT24C02来控制SDA线产生应答信号,但是AT24C02本身无法控制时钟线,而SDA的数据改变又必须在SCL处于电平时才能改变,因此需要MCU再产生一个时钟脉冲,给AT24C02提供产生应答信号的脉冲。
那么代码如下:
/*********************************
函数名:Iic_Rcv_Ack
函数功能:IIC接收应答函数
形参:void
返回值:u8
备注:0:ACK
1:NACK
**********************************/
u8 Iic_Rcv_Ack(void)
{
IIC_SCL_L;
Systick_Delay_us(5);
IIC_SDA_H; //进入空闲(此时NOMS被屏蔽,拉高并不输出) 可以接收数据
//开始接收
IIC_SCL_L; //主机帮从机拉低时钟线 从机此时可以控制SDA线
Systick_Delay_us(5);
IIC_SCL_H; //拉高时钟线接收数据
Systick_Delay_us(5);
if(IIC_SDA_IN)//判断SDA的电平,为高应答失败
{
IIC_SCL_L; //安全模式
return 1;
}
else
{
IIC_SCL_L; //安全模式,为低应答成功
return 0;
}
}
6.MCU接收八位数据
前面搞定了发送过程的各个部分,除了MCU发送数据,还需要从AT24C02 种读取数据,同样的,AT24C02是不能自己控制SCL线的,需要有MCU提供时钟,而SDA此时就交给AT24C02操作了,MCU只需要在SCL高电平时去读取SDA线的高低电平即可获取到数据。也是循环读取8次即可,高位在前。
/*********************************
函数名:Iic_Rcv_Data
函数功能:IIC接收数据
形参:void
返回值:u8 data 接收到的8位数据
备注:
**********************************/
u8 Iic_Rcv_Data(void)
{
u8 data=0,i;
IIC_SCL_L;
Systick_Delay_us(5);
IIC_SDA_H; //进入空闲 才可以接收数据
for(i=0;i<8;i++)
{
IIC_SCL_L; //主机帮从机拉低时钟线 从机可以发送数据
Systick_Delay_us(5);
IIC_SCL_H; //拉高时钟线接收数据
Systick_Delay_us(5);
data = data << 1;
if(IIC_SDA_IN)
{
data |=0x01;
}
}
IIC_SCL_L; //安全模式
return data;
}
7.MCU发送应答位
同样的,AT24C02再发送了8位数据后,MCU也需要给AT24C02一个ACK来告诉AT24C02继续发送或者给一个NACK来告诉AT24C02停止发送。
/*********************************
函数名:Iic_Send_Ack
函数功能:IIC发送应答函数
形参:u8 ack 0:正常应答 1:非正常应答
返回值:void
备注:
**********************************/
void Iic_Send_Ack(u8 ack)
{
IIC_SCL_L; //拉低时钟线发送数据
Systick_Delay_us(5);
if(ack)//非正常应答
{
IIC_SDA_H;
}
else//正常应答
{
IIC_SDA_L;
}
IIC_SCL_H; //主机帮从机拉高时钟线
Systick_Delay_us(5);
IIC_SCL_L; //安全模式
}
AT24C02写一个字节
上面已经将整个通信流程的各个部分搞定了,接下来就是根据芯片手册时序图去组合就可以了,首先是对指定字地址写入一个八位数据,时序图如下图所示:
来看下逻辑分析仪抓取的实际波形:
为了方便大家查看,这里就不框出来做批注了,对照着应该可以看明白。这个图与上一个图是一一对应的关系。
此时序图是向器件地址为50的从机的10号字地址写入了一个0x41(‘A’);
代码如下:
/*********************************
函数名:At24c02_Write_Byte
函数功能:AT24C02写一个字节
形参:u32 innr_addr , u8 data
返回值:u8
备注:
**********************************/
u8 At24c02_Write_Byte(u32 innr_addr,u8 data)
{
Iic_Start(); //起始信号
Iic_Send_Data(AT24C02_ADDR_W); //发送写器件地址
if(Iic_Rcv_Ack()) //判断是否正常响应
{
Iic_Stop();
return 1;
}
Iic_Send_Data(innr_addr); //发送要写入数据的地址
if(Iic_Rcv_Ack()) //判断是否正常响应
{
Iic_Stop();
return 2;
}
Iic_Send_Data(data); //发送要写入数据的地址
if(Iic_Rcv_Ack()) //判断是否正常响应
{
Iic_Stop();
return 3;
}
Iic_Stop(); //结束通信
Systick_Delay_ms(10); //写周期
return 0;
}
AT24C02读一个字节
有写必有读,随机读一个字节的数据会比写入稍微负责一点点,中间要做一次切换,不过也是使用之前的模块拼凑起来的。
手册中的描述如下:
实际的波形如下:
这个图在前面介绍了,这里也不做赘述了。
除此之外还有页写、按顺序读,这些也都是根据之前的那些内容进行拼接起来就可以了,这里就不再赘述,大家可以自己去尝试一下。
实现的效果
首先对AT24C02的第10个字节地址写入0X41(‘A’),然后将其读取出来,并用串口打印,结果如下图,可以正常的读取到写入的A。
总结
关于这个板子使用GPIO模拟I2C实现对EEPROM的写入和读取的操作就介绍到这里,后面还有一个红外发射管也是用的IO模拟,除此之外就全部是使用的控制器来实现的了,这写在后面会一一介绍到,文中如有不足欢迎批评指正。