1.I2C原理
1.1介绍
I2C,即Inter-Integrated Circuit,是一种用于在电子设备之间进行短距离通信的串行通信协议。该协议由飞利浦公司(现在的恩智浦半导体)于1982年首次引入,旨在简化数字电路板之间的通信。
I2C使用两根导线,分别为数据线(SDA)和时钟线(SCL)。这两根线允许多个设备通过相同的总线进行通信,每个设备都有一个唯一的地址。这使得I2C非常适用于连接微控制器、传感器、存储器和其他数字设备。
在DK117S这块开发板上,官方设计成了这样:
左上是EEPROM,右下是MCP4017.
让我们来看看SDA与SCL这两根信号线在芯片中是如何映射的:
PB6没有I2C功能,所以赛方给了基础的驱动代码。
1.2 I2C的写操作
当SCL为高时,SDA由高转低为开始信号;SCL为高时,SDA由低转高为停止信号。如下图为停止信号:
开始信号发出后我们就可以在SDA上传输数据了,一般是先传控制字节(包含器件的固定编号、同类器件的第几个设备,有的还有页选择),之后具体看相应器件的手册是如何描述与该器件的通讯方法的。
下面是数据传输:
开始信号发出后就可以传输数据了,当SCL为低的时候信号是可以变化的(上图①,③的位置),在②的位置即SCL高电平数据的电平是不可以变化的(否则会被认为是开始或结束信号),在这位置就会采集该位数据的高低电平。
还有一个重要的信号:应答信号,在传输完8位数据后会有一个应答信号表示接受数据。不过有些是没有应答的,具体看对应器件的操作手册。
8位数据后在SCL为高时发出0即为应答,1就是不应答。
2. EEPROM
2.1 介绍
板子上的AT24C02是Atmel(现在是Microchip Technology)公司生产的一款串行EEPROM(Electrically Erasable Programmable Read-Only Memory)器件。EEPROM是一种非易失性存储器,允许在电源断电时保持存储的数据。AT24C02的"02"表示其容量为2千比特(2 Kbits),即256字节(32页,每页8字节)。
以下是AT24C02 EEPROM的主要特征和规格:
-
容量: 256字节(2千比特)。
-
串行接口: AT24C02使用I2C(Inter-Integrated Circuit)串行总线接口进行通信,这使得它能够轻松地与微控制器等设备连接。
-
工作电压范围: 1.7V 至 5.5V,这使得它适用于各种不同电源电压的应用。
-
存储器结构: 数据以8位字节的形式存储,每个字节都有一个唯一的地址。
-
读取和写入速度: AT24C02支持标准(100 kbps)和快速(400 kbps)I2C总线速率。
-
寿命: 支持高达百万次写入循环,具有良好的耐用性。
-
写保护功能: AT24C02允许对特定的存储块进行写保护,以防止误写。
2.2 写操作
2.2.1 字节写
字节写入:写入操作需要在设备地址字(控制字节:设备编号和设备选择,EEPROM——AT24C02是1010XXX(R/W),板子上就一块所以写入就是1010000=0xA0)和确认之后的8位数据字地址(数据要写入的地址)。在收到这个地址后,EEPROM将再次响应一个零(应答信号为0)。在收到8位数据字后,EEPROM将输出一个零,而寻址设备,如微控制器,必须以停止条件终止写入序列。此时,EEPROM进入对非易失性存储器的一个内部定时写入周期。在这个写入周期中,所有输入都被禁用,EEPROM在写入完成之前不会响应。
控制字节:
字节写入的序列:
上面提到了一个写入周期的概念:写入周期时间tWR是从写入序列的有效停止条件到内部清除/写入周期结束的时间。也就是说写入是需要时间的,如果刚刚完成写入数据就立即读取或写入是不行的,要等一段时间也就是tWR。
2.2.2 页写入
页写入就是我们要采用的写入操作,毕竟字节写入每次都要等一会,可以一次写入一页。
根据手册:
-
页写入和字节写入区别:
页面写入操作与字节写入操作类似,主器件通过发送写控制字节、字地址字节和第一个数据字节来启动页面写入。不同之处在于,主器件不会在第一个数据字节传输后发送停止条件,而是将多至一页的数据字节(8字节对于1K/2K型号,16字节对于4K、8K、16K型号)临时存储在片上页缓冲器中。 -
连续传输数据:
主器件在EEPROM确认接收第一个数据字节后,可以继续传输多达七个(1K/2K型号)或十五个(4K、8K、16K型号)数据字节。EEPROM将在接收每个数据字后响应一个零。 -
页边界处理:
EEPROM内部会自动递增数据字地址的低三位(1K/2K型号)或低四位(4K、8K、16K型号)。高位数据字地址保持不变,保留内存页面行位置。当数据字地址到达页面边界时,下一个字节将被放置在同一页面的开头。 -
停止条件:
页面写入序列由主器件通过发送停止条件来终止。停止条件的发送导致将临时存储在页缓冲器中的数据写入存储器。在写入周期期间,24XX器件不会对命令进行确认。 -
数据溢出处理:
如果主器件在产生停止条件之前要发送超出一页的数据,数据字地址计数器将计满并返回。这将导致之前接收的数据被覆盖。
页面写入操作使得主器件能够在一个写入周期内传输多个字节的数据,提高了数据传输的效率。然而,需要注意正确处理页面边界和数据溢出,以确保数据的正确存储。
页写入格式:
2.2.3页写入的代码实现
一步一步来,先写一页的,从手册的协议序列来看,应该是:开始信号→控制字节(器件地址+读写位)→等待从器件应答→存储地址字→等待从器件应答→数据帧1→数据帧2·····数据帧n→等待从器件应答→结束信号。
按照手册来看,AT24C02每页有8字节,另外如果本次写入超过八字节就会覆盖低地址的字节:
我们写入:ABCDEFGHI,那么这一串字母会在存储里呈现如下形式:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
I | B | C | D | E | F | G | H |
第八个字符I没有去地址8,而是区覆盖地址0的A.
那么我们现在面临一个问题如何解决这个覆盖。其实只要在写入第八个字符后跳出写入的循环就可以了,然后我们等待这八个字节写入存储(这八个字节其实是先写入缓存的),然后继续向下一页写剩余的字符,直到256字节写满。
代码如下:
void EEPROM_Write(unsigned char *buf, unsigned char addr, unsigned char len)
{
while(len)//长度计数,可以大于8
{
do{
I2CStart(); //I2C传输开始信号
I2CSendByte(0xa0);//发送控制字节1010 0000
if(!I2CWaitAck())//等待回应,其实也是在判断上一次页写入有没有完成
{
break;
}
I2CStop();
}while(1); //只要没有应答就一直循环这个操作
I2CSendByte(addr); //向器件发送写入地址
while(I2CWaitAck());//等待回应
while(len>0) //循环写入8字节数据,即一页
{
len--;
I2CSendByte(*buf++);
addr++;
while(I2CWaitAck());
if(addr%8==0)//写完8个数据跳出
{
break;
}
}
I2CStop();
}
}
2.3读操作
读操作分为三种:
- 当前地址读取
- 随机读取
- 连续读取(随机读取pro,也是我们要写的模式)
2.3.1 当前地址读取
24XX内置一个自动加1的地址计数器,该计数器保留最后一次访问字节的地址。因此,如果先前对地址“n”(n为任意合法地址)进行读或写操作,则下一条读操作命令将可能从地址n+1访问数据。
接收到R/W位设置为1的控制字节后,24XX器件发出确认信号,并发送8位数据字节。主器件不会对数据传输作出确认,但会产生停止条件,24XX器件即停止数据发送。
以上内容来自于操作手册,也就是说使用这个方法只能读取当前器件内部地址指针指向的地址。
开始信号→控制字节(器件地址以及R/W)→从器件的应答→器件发回当前地址的数据帧→主器件发送不确认信号→停止信号
2.3.2 随机读取
随机读操作允许主器件以随机方式访问任意存储单元。执行该类型的读操作,必须先设置字节地址(这里是说器件地址及R/W)。作为写操作的一部分,通过发送字节地址(存储地址字)给24XX器件来完成地址字节的设置。字节地址发送完后,主器件一接收到确认信号即产生启动条件。内部地址计数器设置完之后写操作即被终止。主器件再次发送控制字节(器件地址以及R/W,不同的是这次是读取,R/W位设置为1),而该字节中R/W位设置为1。之后24XX器件会发出确认信号,并发送8位数据字节。主器件不会对数据传输作出确认,但会产生停止条件,24XX器件即停止数据发送。在随即读取命令后,内部地址计数器递增,指向下一个地址单元。(以上内容来自操作手册)
2.3.3 连续读取
连续读操作的启动过程和随机读操作相同,只是在24Xx器件发送完第一个数据字节后,主器件发出确认信号,而在随机读操作中发送的是停止条件(这位就是写程序时的区别)。确认信号指示24XX器件发送下一个连续地址的数据字节。在向主器件发送完最后一个字节后,主器件不会产生确认信号,而是产生停止条件。为了可以进行连续读操作,24XX器件内置了一个地址指针,在每次操作完成后该指针加1。地址指针允许一次操作连续读取整个存储器的内容。在达到最后一个地址字节后,地址指针将计满返回到地址Ox00。
接下来看一下随机读取和连续读取的对比
2.3.4 随机读取的代码实现
void EEPROM_Read(unsigned char *buf, unsigned char addr, unsigned char len)
{
do{
I2CStart(); //I2C传输开始信号
I2CSendByte(0xa0);//发送控制字节1010 0000
if(!I2CWaitAck())//等待回应,其实也是在判断上一次页写入有没有完成
{
break;
}
I2CStop();
}while(1); //只要没有应答就一直循环这个操作
I2CSendByte(addr);
while(I2CWaitAck());//等待回应
I2CStart();
I2CSendByte(0xa1);//1010 0001
while(I2CWaitAck());//等待回应
while(len>0)
{
len--;
*buf++ = I2CReceiveByte();
if(len)//最后一个字节不应答
I2CSendAck();
else
I2CSendNotAck();
}
I2CStop();
}
附:将SDA_Outpu_Mode函数挪到下图位置,高频率情况会有问题