单片机EEPROM简介

在实际的应用中,保存在单片机 RAM 中的数据,掉电后就丢失了,保存在单片机的FLASH 中的数据,又不能随意改变,也就是不能用它来记录变化的数值。但是在某些场合,我们又确实需要记录下某些数据,而它们还时常需要改变或更新,掉电之后数据还不能丢失,比如我们的家用电表度数,电视机里边的频道记忆,一般都是使用 EEPROM 来保存数据,特点就是掉电后不丢失。我们板子上使用的这个器件是 24C02,是一个容量大小是 2Kbits,也就是 256 个字节的 EEPROM。一般情况下,EEPROM 拥有 30 万到 100 万次的寿命,也就是它可以反复写入 30-100 万次,而读取次数是无限的。

24C02 是一个基于 I2C 通信协议的器件,因此从现在开始,我们的 I2C 和我们的 EEPROM就要合体了。但是大家要分清楚,I2C 是一个通信协议,它拥有严密的通信时序逻辑要求,而 EEPROM 是一个器件,只是这个器件采样了 I2C 协议的接口与单片机相连而已,二者并没有必然的联系,EEPROM 可以用其它接口,I2C 也可以用在其它很多器件上。



单片机EEPROM单字节读写操作时序
EEPROM 写数据流程第一步,首先是 I2C 的起始信号,接着跟上首字节,也就是我们前边讲的 I2C 的器件地址,并且在读写方向上选择“写”操作。

第二步,发送数据的存储地址。24C02 一共 256 个字节的存储空间,地址从 0x00~0xFF,我们想把数据存储在哪个位置,此刻写的就是哪个地址。

第三步,发送要存储的数据第一个字节、第二个字节„„注意在写数据的过程中,EEPROM 每个字节都会回应一个“应答位 0”,来告诉我们写 EEPROM 数据成功,如果没有回应答位,说明写入不成功。

在写数据的过程中,每成功写入一个字节,EEPROM 存储空间的地址就会自动加 1,当加到 0xFF 后,再写一个字节,地址会溢出又变成了 0x00。EEPROM 读数据流程第一步,首先是 I2C 的起始信号,接着跟上首字节,也就是我们前边讲的 I2C 的器件地址,并且在读写方向上选择“写”操作。这个地方可能有同学会诧异,我们明明是读数据为何方向也要选“写”呢?刚才说过了,24C02 一共有 256 个地址,我们选择写操作,是为了把所要读的数据的存储地址先写进去,告诉 EEPROM 我们要读取哪个地址的数据。这就如同我们打电话,先拨总机号码(EEPROM 器件地址),而后还要继续拨分机号码(数据地址),而拨分机号码这个动作,主机仍然是发送方,方向依然是“写”。

第二步,发送要读取的数据的地址,注意是地址而非存在 EEPROM 中的数据,通知EEPROM 我要哪个分机的信息。

第三步,重新发送 I2C 起始信号和器件地址,并且在方向位选择“读”操作。

这三步当中,每一个字节实际上都是在“写”,所以每一个字节 EEPROM 都会回应一个“应答位 0”。

第四步,读取从器件发回的数据,读一个字节,如果还想继续读下一个字节,就发送一个“应答位 ACK(0)”,如果不想读了,告诉 EEPROM,我不想要数据了,别再发数据了,那就发送一个“非应答位 NAK(1)”。

和写操作规则一样,我们每读一个字节,地址会自动加 1,那如果我们想继续往下读,给 EEPROM 一个 ACK(0)低电平,那再继续给 SCL 完整的时序,EEPROM 会继续往外送数据。如果我们不想读了,要告诉 EEPROM 不要数据了,那我们直接给一个 NAK(1)高电平即可。这个地方大家要从逻辑上理解透彻,不能简单的靠死记硬背了,一定要理解明白。梳理一下几个要点:
A、在本例中单片机是主机,24C02 是从机;
B、无论是读是写,SCL 始终都是由主机控制的;
C、写的时候应答信号由从机给出,表示从机是否正确接收了数据;
D、读的时候应答信号则由主机给出,表示是否继续读下去。

那我们下面写一个程序,读取 EEPROM 的 0x02 这个地址上的一个数据,不管这个数据之前是多少,我们都将读出来的数据加 1,再写到 EEPROM 的 0x02 这个地址上。此外我们将 I2C 的程序建立一个文件,写一个 I2C.c 程序文件,形成我们又一个程序模块。大家也可以看出来,我们连续的这几个程序,Lcd1602.c 文件里的程序都是一样的,今后我们大家写1602 显示程序也可以直接拿过去用,大大提高了程序移植的方便性。

  • /******************************I2C.c 文件程序源代码******************************/
  • #include <reg52.h>
  • #include <intrins.h>
  • #define I2CDelay() {_nop_();_nop_();_nop_();_nop_();}
  • sbit I2C_SCL = P3^7;
  • sbit I2C_SDA = P3^6;
  • /* 产生总线起始信号 */
  • void I2CStart(){
  •     I2C_SDA = 1; //首先确保 SDA、SCL 都是高电平
  •     I2C_SCL = 1;
  •     I2CDelay();
  •     I2C_SDA = 0; //先拉低 SDA
  •     I2CDelay();
  •     I2C_SCL = 0; //再拉低 SCL
  • }
  • /* 产生总线停止信号 */
  • void I2CStop(){
  •     I2C_SCL = 0; //首先确保 SDA、SCL 都是低电平
  •     I2C_SDA = 0;
  •     I2CDelay();
  •     I2C_SCL = 1; //先拉高 SCL
  •     I2CDelay();
  •     I2C_SDA = 1; //再拉高 SDA
  •     I2CDelay();
  • }
  • /* I2C 总线写操作,dat-待写入字节,返回值-从机应答位的值 */
  • bit I2CWrite(unsigned char dat){
  •     bit ack; //用于暂存应答位的值
  •     unsigned char mask; //用于探测字节内某一位值的掩码变量

  •     for (mask=0x80; mask!=0; mask>>=1){ //从高位到低位依次进行
  •         if ((mask&dat) == 0){ //该位的值输出到 SDA 上
  •             I2C_SDA = 0;
  •         }else{
  •             I2C_SDA = 1;
  •         }
  •         I2CDelay();
  •         I2C_SCL = 1; //拉高 SCL
  •         I2CDelay();
  •         I2C_SCL = 0; //再拉低 SCL,完成一个位周期
  •     }

  •     I2C_SDA = 1; //8 位数据发送完后,主机释放 SDA,以检测从机应答
  •     I2CDelay();
  •     I2C_SCL = 1; //拉高 SCL
  •     ack = I2C_SDA; //读取此时的 SDA 值,即为从机的应答值
  •     I2CDelay();
  •     I2C_SCL = 0; //再拉低 SCL 完成应答位,并保持住总线
  •     //应答值取反以符合通常的逻辑:
  •     //0=不存在或忙或写入失败,1=存在且空闲或写入成功
  •     return (~ack);
  • }
  • /* I2C 总线读操作,并发送非应答信号,返回值-读到的字节 */
  • unsigned char I2CReadNAK(){
  •     unsigned char mask;
  •     unsigned char dat;

  •     I2C_SDA = 1; //首先确保主机释放 SDA
  •     for (mask=0x80; mask!=0; mask>>=1){ //从高位到低位依次进行
  •         I2CDelay();
  •         I2C_SCL = 1; //拉高 SCL
  •         if(I2C_SDA == 0){ //读取 SDA 的值
  •             dat &= ~mask; //为 0 时,dat 中对应位清零
  •         }else{
  •             dat |= mask; //为 1 时,dat 中对应位置 1
  •         }
  •         I2CDelay();
  •         I2C_SCL = 0; //再拉低 SCL,以使从机发送出下一位
  •     }
  •     I2C_SDA = 1; //8 位数据发送完后,拉高 SDA,发送非应答信号
  •     I2CDelay();
  •     I2C_SCL = 1; //拉高 SCL
  •     I2CDelay();
  •     I2C_SCL = 0; //再拉低 SCL 完成非应答位,并保持住总线
  •     return dat;
  • }

  • /* I2C 总线读操作,并发送应答信号,返回值-读到的字节 */
  • unsigned char I2CReadACK(){
  •     unsigned char mask;
  •     unsigned char dat;

  •     I2C_SDA = 1; //首先确保主机释放 SDA
  •     for (mask=0x80; mask!=0; mask>>=1){ //从高位到低位依次进行
  •         I2CDelay();
  •         I2C_SCL = 1; //拉高 SCL
  •         if(I2C_SDA == 0){ //读取 SDA 的值
  •             dat &= ~mask; //为 0 时,dat 中对应位清零
  •         }else{
  •             dat |= mask; //为 1 时,dat 中对应位置 1
  •         }
  •         I2CDelay();
  •         I2C_SCL = 0; //再拉低 SCL,以使从机发送出下一位
  •     }
  •     I2C_SDA = 0; //8 位数据发送完后,拉低 SDA,发送应答信号
  •     I2CDelay();
  •     I2C_SCL = 1; //拉高 SCL
  •     I2CDelay();
  •     I2C_SCL = 0; //再拉低 SCL 完成应答位,并保持住总线
  •     return dat;
  • }


I2C.c 文件提供了 I2C 总线所有的底层操作函数,包括起始、停止、字节写、字节读+应答、字节读+非应答。

  • /***************************Lcd1602.c 文件程序源代码*****************************/
  • #include <reg52.h>

  • #define LCD1602_DB P0
  • sbit LCD1602_RS = P1^0;
  • sbit LCD1602_RW = P1^1;
  • sbit LCD1602_E = P1^5;

  • /* 等待液晶准备好 */
  • void LcdWaitReady(){
  •     unsigned char sta;
  •     LCD1602_DB = 0xFF;
  •     LCD1602_RS = 0;
  •     LCD1602_RW = 1;
  •     do {
  •         LCD1602_E = 1;
  •         sta = LCD1602_DB; //读取状态字
  •         LCD1602_E = 0;
  •     }while (sta & 0x80); //bit7 等于 1 表示液晶正忙,重复检测直到其等于 0 为止
  • }
  • /* 向 LCD1602 液晶写入一字节命令,cmd-待写入命令值 */
  • void LcdWriteCmd(unsigned char cmd){
  •     LcdWaitReady();
  •     LCD1602_RS = 0;
  •     LCD1602_RW = 0;
  •     LCD1602_DB = cmd;
  •     LCD1602_E = 1;
  •     LCD1602_E = 0;
  • }
  • /* 向 LCD1602 液晶写入一字节数据,dat-待写入数据值 */
  • void LcdWriteDat(unsigned char dat){
  •     LcdWaitReady();
  •     LCD1602_RS = 1;
  •     LCD1602_RW = 0;
  •     LCD1602_DB = dat;
  •     LCD1602_E = 1;
  •     LCD1602_E = 0;
  • }
  • /* 设置显示 RAM 起始地址,亦即光标位置,(x,y)-对应屏幕上的字符坐标 */
  • void LcdSetCursor(unsigned char x, unsigned char y){
  •     unsigned char addr;

  •     if (y == 0){ //由输入的屏幕坐标计算显示 RAM 的地址
  •         addr = 0x00 + x; //第一行字符地址从 0x00 起始
  •     }else{
  •         addr = 0x40 + x; //第二行字符地址从 0x40 起始
  •     }
  •     LcdWriteCmd(addr | 0x80); //设置 RAM 地址
  • }
  • /* 在液晶上显示字符串,(x,y)-对应屏幕上的起始坐标,str-字符串指针 */
  • void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str){
  •     LcdSetCursor(x, y); //设置起始地址
  •     while (*str != '\0'){ //连续写入字符串数据,直到检测到结束符
  •         LcdWriteDat(*str++);
  •     }
  • }
  • /* 初始化 1602 液晶 */
  • void InitLcd1602(){
  •     LcdWriteCmd(0x38); //16*2 显示,5*7 点阵,8 位数据接口
  •     LcdWriteCmd(0x0C); //显示器开,光标关闭
  •     LcdWriteCmd(0x06); //文字不动,地址自动+1
  •     LcdWriteCmd(0x01); //清屏
  • }




  • /*****************************main.c 文件程序源代码******************************/
  • #include <reg52.h>
  • extern void InitLcd1602();
  • extern void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str);
  • extern void I2CStart();
  • extern void I2CStop();
  • extern unsigned char I2CReadNAK();
  • extern bit I2CWrite(unsigned char dat);
  • unsigned char E2ReadByte(unsigned char addr);
  • void E2WriteByte(unsigned char addr, unsigned char dat);

  • void main(){
  •     unsigned char dat;
  •     unsigned char str[10];

  •     InitLcd1602(); //初始化液晶
  •     dat = E2ReadByte(0x02); //读取指定地址上的一个字节
  •     str[0] = (dat/100) + '0'; //转换为十进制字符串格式
  •     str[1] = (dat/10%10) + '0';
  •     str[2] = (dat%10) + '0';
  •     str[3] = '\0';

  •     LcdShowStr(0, 0, str); //显示在液晶上
  •     dat++; //将其数值+1
  •     E2WriteByte(0x02, dat); //再写回到对应的地址上
  •     while (1);
  • }

  • /* 读取 EEPROM 中的一个字节,addr-字节地址 */
  • unsigned char E2ReadByte(unsigned char addr){
  •     unsigned char dat;

  •     I2CStart();
  •     I2CWrite(0x50<<1); //寻址器件,后续为写操作
  •     I2CWrite(addr); //写入存储地址
  •     I2CStart(); //发送重复启动信号
  •     I2CWrite((0x50<<1)|0x01); //寻址器件,后续为读操作
  •     dat = I2CReadNAK(); //读取一个字节数据
  •     I2CStop();
  •     return dat;
  • }
  • /* 向 EEPROM 中写入一个字节,addr-字节地址 */
  • void E2WriteByte(unsigned char addr, unsigned char dat){
  •     I2CStart();
  •     I2CWrite(0x50<<1); //寻址器件,后续为写操作
  •     I2CWrite(addr); //写入存储地址
  •     I2CWrite(dat); //写入一个字节数据
  •     I2CStop();
  • }


这个程序,以同学们现在的基础,独立分析应该不困难了,遇到哪个语句不懂可以及时问问别人或者搜索一下,把该解决的问题理解明白。大家把这个程序复制过去后,编译一下会发现 Keil 软件提示了一个警告:*** WARNING L16: UNCALLED SEGMENT, IGNORED FOR OVERLAY PROCESS,这个警告的意思是在代码中存在没有被调用过的变量或者函数,即 I2C.c 文件中的 I2CReadACK()这个函数在本例中没有用到。

大家仔细观察一下这个程序,我们读取 EEPROM 的时候,只读了一个字节就要告诉EEPROM 不需要再读数据了,读完后直接发送一个“NAK”,因此只调用了 I2CReadNAK()这个函数,而并没有调用 I2CReadACK()这个函数。我们今后很可能读数据的时候要连续读几个字节,因此这个函数写在了 I2C.c 文件中,作为 I2C 功能模块的一部分是必要的,方便我们这个文件以后移植到其他程序中使用,因此这个警告在这里就不必管它了。


单片机EEPROM多字节读写操作时序
我们读取 EEPROM 的时候很简单,EEPROM 根据我们所送的时序,直接就把数据送出来了,但是写 EEPROM 却没有这么简单了。给 EEPROM 发送数据后,先保存在了 EEPROM的缓存,EEPROM 必须要把缓存中的数据搬移到“非易失”的区域,才能达到掉电不丢失的效果。而往非易失区域写需要一定的时间,每种器件不完全一样,ATMEL 公司的 24C02 的这个写入时间最高不超过 5ms。在往非易失区域写的过程,EEPROM 是不会再响应我们的访问的,不仅接收不到我们的数据,我们即使用 I2C 标准的寻址模式去寻址,EEPROM 都不会应答,就如同这个总线上没有这个器件一样。数据写入非易失区域完毕后,EEPROM 再次恢复正常,可以正常读写了。

细心的同学,在看上一节程序的时候会发现,我们写数据的那段代码,实际上我们有去读应答位 ACK,但是读到了应答位我们也没有做任何处理。这是因为我们一次只写一个字节的数据进去,等到下次重新上电再写的时候,时间肯定远远超过了 5ms,但是如果我们是连续写入几个字节的时候,就必须得考虑到应答位的问题了。写入一个字节后,再写入下一个字节之前,我们必须要等待 EEPROM 再次响应才可以,大家注意我们程序的写法,可以学习一下。

之前我们知道编写多.c 文件移植的方便性了,本节程序和上一节的 Lcd1602.c 文件和I2C.c 文件完全是一样的,因此这次我们只把 main.c 文件给大家发出来,帮大家分析明白。

而同学们却不能这样,同学们是初学,很多知识和技巧需要多练才能巩固下来,因此每个程序还是建议大家在你的 Keil 软件上一个代码一个代码的敲出来。
/*****************************I2C.c 文件程序源代码*******************************/
(此处省略,可参考之前章节的代码)
/***************************Lcd1602.c 文件程序源代码*****************************/
(此处省略,可参考之前章节的代码)纯文本复制

  • /*****************************main.c 文件程序源代码******************************/
  • #include <reg52.h>
  • extern void InitLcd1602();
  • extern void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str);
  • extern void I2CStart();
  • extern void I2CStop();
  • extern unsigned char I2CReadACK();
  • extern unsigned char I2CReadNAK();
  • extern bit I2CWrite(unsigned char dat);
  • void E2Read(unsigned char *buf, unsigned char addr, unsigned char len);
  • void E2Write(unsigned char *buf, unsigned char addr, unsigned char len);
  • void MemToStr(unsigned char *str, unsigned char *src, unsigned char len);

  • void main(){
  •     unsigned char i;
  •     unsigned char buf[5];
  •     unsigned char str[20];
  •     InitLcd1602(); //初始化液晶
  •     E2Read(buf, 0x90, sizeof(buf)); //从 E2 中读取一段数据
  •     MemToStr(str, buf, sizeof(buf)); //转换为十六进制字符串
  •     LcdShowStr(0, 0, str); //显示到液晶上
  •     for (i=0; i<sizeof(buf); i++){ //数据依次+1,+2,+3...
  •         buf = buf + 1 + i;
  •     }
  •     E2Write(buf, 0x90, sizeof(buf)); //再写回到 E2 中
  •     while(1);
  • }
  • /* 将一段内存数据转换为十六进制格式的字符串,
  • str-字符串指针,src-源数据地址,len-数据长度 */
  • void MemToStr(unsigned char *str, unsigned char *src, unsigned char len){
  •     unsigned char tmp;

  •     while (len--){
  •         tmp = *src >> 4; //先取高 4 位
  •         if (tmp <= 9){ //转换为 0-9 或 A-F
  •             *str++ = tmp + '0';
  •         }else{
  •             *str++ = tmp - 10 + 'A';
  •         }
  •         tmp = *src & 0x0F; //再取低 4 位
  •         if (tmp <= 9){ //转换为 0-9 或 A-F
  •             *str++ = tmp + '0';
  •         }else{
  •             *str++ = tmp - 10 + 'A';
  •         }
  •         *str++ = ' '; //转换完一个字节添加一个空格
  •         src++;
  •     }
  • }
  • /* E2 读取函数,buf-数据接收指针,addr-E2 中的起始地址,len-读取长度 */
  • void E2Read(unsigned char *buf, unsigned char addr, unsigned char len){
  •     do { //用寻址操作查询当前是否可进行读写操作
  •         I2CStart();
  •         if (I2CWrite(0x50<<1)){ //应答则跳出循环,非应答则进行下一次查询
  •             break;
  •         }
  •         I2CStop();
  •     } while(1);
  •     I2CWrite(addr); //写入起始地址
  •     I2CStart(); //发送重复启动信号
  •     I2CWrite((0x50<<1)|0x01); //寻址器件,后续为读操作
  •     while (len > 1){ //连续读取 len-1 个字节
  •         *buf++ = I2CReadACK(); //最后字节之前为读取操作+应答
  •         len--;
  •     }
  •     *buf = I2CReadNAK(); //最后一个字节为读取操作+非应答
  •     I2CStop();
  • }
  • /* E2 写入函数,buf-源数据指针,addr-E2 中的起始地址,len-写入长度 */
  • void E2Write(unsigned char *buf, unsigned char addr, unsigned char len){
  •     while (len--){
  •         do { //用寻址操作查询当前是否可进行读写操作
  •             I2CStart();
  •             if (I2CWrite(0x50<<1)){ //应答则跳出循环,非应答则进行下一次查询
  •                 break;
  •             }
  •             I2CStop();
  •         } while(1);
  •         I2CWrite(addr++); //写入起始地址
  •         I2CWrite(*buf++); //写入一个字节数据
  •         I2CStop(); //结束写操作,以等待写入完成
  •     }
  • }


函数 MemToStr:可以把一段内存数据转换成十六进制字符串的形式。由于我们从EEPROM 读出来的是正常的数据,而 1602 液晶接收的是 ASCII 码字符,因此我们要通过液晶把数据显示出来必须先通过一步转换。算法倒是很简单,就是把每一个字节的数据高 4 位和低 4 位分开,和 9 进行比较,如果小于等于 9,则直接加„0‟转为 0~9 的 ASCII 码;如果大于 9,则先减掉 10 再加„A‟即可转为 A~F 的 ASCII 码。

函数 E2Read:我们在读之前,要查询一下当前是否可以进行读写操作,EEPROM 正常响应才可以进行。进行后,读最后一个字节之前的,全部给出 ACK,而读完了最后一个字节,我们要给出一个 NAK。

函数 E2Write:每次写操作之前,我们都要进行查询判断当前 EEPROM 是否响应,正常响应后才可以写数据。

单片机EEPROM的页写入
在向 EEPROM 连续写入多个字节的数据时,如果每写一个字节都要等待几 ms 的话,整体上的写入效率就太低了。因此 EEPROM 的厂商就想了一个办法,把 EEPROM 分页管理。24C01、24C02 这两个型号是 8 个字节一个页,而 24C04、24C08、24C16 是 16 个字节一页。我们开发板上用的型号是 24C02,一共是 256 个字节,8 个字节一页,那么就一共有 32 页。

分配好页之后,如果我们在同一个页内连续写入几个字节后,最后再发送停止位的时序。EEPROM 检测到这个停止位后,就会一次性把这一页的数据写到非易失区域,就不需要像上节课那样写一个字节检测一次了,并且页写入的时间也不会超过 5ms。如果我们写入的数据跨页了,那么写完了一页之后,我们要发送一个停止位,然后等待并且检测 EEPROM 的空闲模式,一直等到把上一页数据完全写到非易失区域后,再进行下一页的写入,这样就可以在很大程度上提高数据的写入效率。
/*****************************I2C.c 文件程序源代码*******************************/
(此处省略,可参考之前章节的代码)
/***************************Lcd1602.c 文件程序源代码*****************************/
(此处省略,可参考之前章节的代码)

  • /****************************eeprom.c 文件程序源代码*****************************/
  • #include <reg52.h>

  • extern void I2CStart();
  • extern void I2CStop();
  • extern unsigned char I2CReadACK();
  • extern unsigned char I2CReadNAK();
  • extern bit I2CWrite(unsigned char dat);

  • /* E2 读取函数,buf-数据接收指针,addr-E2 中的起始地址,len-读取长度 */
  • void E2Read(unsigned char *buf, unsigned char addr, unsigned char len){
  • do { //用寻址操作查询当前是否可进行读写操作
  •     I2CStart();
  •     if (I2CWrite(0x50<<1)){ //应答则跳出循环,非应答则进行下一次查询
  •         break;
  •     }
  •     I2CStop();
  • }while(1);

  •     I2CWrite(addr); //写入起始地址
  •     I2CStart();//发送重复启动信号
  •     I2CWrite((0x50<<1)|0x01); //寻址器件,后续为读操作
  •     while (len > 1){//连续读取 len-1 个字节
  •         *buf++ = I2CReadACK(); //最后字节之前为读取操作+应答
  •         len--;
  •     }
  •     *buf = I2CReadNAK(); //最后一个字节为读取操作+非应答
  •     I2CStop();
  • }
  • /* E2 写入函数,buf-源数据指针,addr-E2 中的起始地址,len-写入长度 */
  • void E2Write(unsigned char *buf, unsigned char addr, unsigned char len){
  •     while (len > 0){ //等待上次写入操作完成
  •         do { //用寻址操作查询当前是否可进行读写操作
  •             I2CStart();
  •             if (I2CWrite(0x50<<1)){ //应答则跳出循环,非应答则进行下一次查询
  •                 break;
  •             }
  •             I2CStop();
  •         } while(1);
  •     //按页写模式连续写入字节

  •         I2CWrite(addr); //写入起始地址
  •         while (len > 0){
  •             I2CWrite(*buf++); //写入一个字节数据
  •             len--; //待写入长度计数递减
  •             addr++; //E2 地址递增
  •             //检查地址是否到达页边界,24C02 每页 8 字节,
  •             //所以检测低 3 位是否为零即可
  •             if ((addr&0x07) == 0){
  •                 break; //到达页边界时,跳出循环,结束本次写操作
  •             }
  •         }
  •         I2CStop();
  •     }
  • }


遵循模块化的原则,我们把 EEPROM 的读写函数也单独写成一个 eeprom.c 文件。其中E2Read 函数和上一节是一样的,因为读操作与分页无关。重点是 E2Write 函数,我们在写入数据的时候,要计算下一个要写的数据的地址是否是一个页的起始地址,如果是的话,则必须跳出循环,等待 EEPROM 把当前这一页写入到非易失区域后,再进行后续页的写入。

  • /*****************************main.c 文件程序源代码******************************/
  • #include <reg52.h>

  • extern void InitLcd1602();
  • extern void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str);
  • extern void E2Read(unsigned char *buf, unsigned char addr, unsigned char len);
  • extern void E2Write(unsigned char *buf, unsigned char addr, unsigned char len);
  • void MemToStr(unsigned char *str, unsigned char *src, unsigned char len);

  • void main(){
  •     unsigned char i;
  •     unsigned char buf[5];
  •     unsigned char str[20];

  •     InitLcd1602(); //初始化液晶
  •     E2Read(buf, 0x8E, sizeof(buf)); //从 E2 中读取一段数据
  •     MemToStr(str, buf, sizeof(buf)); //转换为十六进制字符串
  •     LcdShowStr(0, 0, str); //显示到液晶上
  •     for (i=0; i<sizeof(buf); i++){ //数据依次+1,+2,+3...
  •         buf = buf + 1 + i;
  •     }
  •     E2Write(buf, 0x8E, sizeof(buf)); //再写回到 E2 中
  •     while(1);
  • }
  • /* 将一段内存数据转换为十六进制格式的字符串,
  • str-字符串指针,src-源数据地址,len-数据长度 */
  • void MemToStr(unsigned char *str, unsigned char *src, unsigned char len){
  •     unsigned char tmp;
  •     while (len--){
  •         tmp = *src >> 4; //先取高 4 位
  •         if (tmp <= 9){ //转换为 0-9 或 A-F
  •             *str++ = tmp + '0';
  •         }else{
  •             *str++ = tmp - 10 + 'A';
  •         }
  •         tmp = *src & 0x0F; //再取低 4 位
  •         if (tmp <= 9){  //转换为 0-9 或 A-F
  •             *str++ = tmp + '0';
  •         }else{
  •             *str++ = tmp - 10 + 'A';
  •         }
  •         *str++ = ' '; //转换完一个字节添加一个空格
  •         src++;
  •     }
  • }


多字节写入和页写入程序都编写出来了,而且页写入的程序我们还特地跨页写的数据,它们的写入时间到底差别多大呢。我们用一些工具可以测量一下,比如示波器,逻辑分析仪等工具。我现在把两次写入时间用逻辑分析仪给抓了出来,并且用时间标签 T1 和 T2 标注了开始位置和结束位置,如图 14-5 和图 14-6 所示,右侧显示的|T1-T2|就是最终写入 5 个字节所耗费的时间。多字节一个一个写入,每次写入后都需要再次通信检测 EEPROM 是否在“忙”,因此耗费了大量的时间,同样的写入 5 个字节的数据,一个一个写入用了 8.4ms 左右的时间,而使用页写入,只用了 3.5ms 左右的时间。

图 14-5  多字节写入时间


图 14-6  跨页写入时间