18.1 内部 E2PROM 简介
单片机在运行时数据均存储在内部 RAM(随机存储器)中,在掉电时无法保存数据。前面提到过可以通过增加外部存储器 AT24C01 芯片的方式解决,但因为需要增加外部电路,性价比并不高,因此不推荐该方法。STC89C51、52 内部都自带有 2K 字节的 E2PROM。可通过对 STC 单片机内部的 E2PROM 编程来实现,这样节省了片外资源,使用也比较方便。
STC 单片机内部的 E2PROM 并不是真正的 E2PROM,而是用 DATA FLASH 模拟出来的,因此操作方法与普通 E2PROM 不同。STC 单片机内部的 E2PROM 采用的是 IAP(在应用编程)技术实现读写操作,擦写次数可达 100,000 次以上。所谓 IAP 指程序在运行时程序存储器可有程序本身进行擦写。IAP 是相对 ISP 而言的,下面进行详细的分析。
18.2 ISP 和 IAP 区别
ISP:In System Programable 是指在系统编程,通俗的讲,就是片子已经焊板子上,不用取下,就可以简单而方便地对其进行编程。比如我们通过电脑给 STC 单片机下载程序。
(相关资料图)
IAP:In Application Programable 是指在应用编程,就是片子提供一系列的机制(硬件/软件上的)当片子在运行程序的时候可以提供一种改变存储器数据的方法。通俗点讲,也就是说程序自己可以往程序存储器里写数据或修改程序。这种方式的典型应用就是用一小段代码来实现程序的下载,实际上单片机的 ISP 功能就是通过 IAP 技术来实现的,即片子在出厂前就已经有一段小的 boot 程序在里面,片子上电后,开始运行这段程序,当检测到上位机有下载要求时,便和上位机通信,然后下载程序到程序存储区。
以 STC89C52 为例进行分析,存储空间包括 8KB flash 程序存储空间、512B RAM 数据存储空间、2KB E2PROM 存储空间。在 51 单片机中采用的是数据和程序存储地址空间并行的哈佛结构,地址分配如下所示:
8KB flash 地址:0——1FFFH 512B RAM 地址:0——0200H 2KB E2PROM 地址:2000H——27FFH
ISP 操作对象为 8KB flash,IAP 的操作对象为 2KBE2PROM,IAP 不能对 flash 进行读写操作。IAP 在读写操作的结果为,将要写入的值与 E2PROM 中原来的值进行与操作然后将结果存入。例如在地址 2000H 处第一次成功写入 11010110,第二次写入 00111010,读出的结果将会是这两个结果的相与 0010010,因此如果写入数据前该处数据不为 FFH,那么写入的数据将会不正确。IAP 的擦除操作的功能就是将数据变为 FFH,但擦除操作是以扇区为基本操作单位的,STC89C52 的 E2PROM 扇区地址安排如下表所示。每个扇区的大小为 512B。
数据存储操作按照以下步骤进行:
1. 写操作之前先将对应扇区的有效数据读取到 RAM 中暂存(这步不是必须的); 2. 对整个扇区进行擦除操作,擦除后该扇区的数据均为 FFH; 3. 将要写入的字节写入; 4. 将暂存的数据写入;
STC 单片机 IAP 程序操作步骤如下:
1. 配置 ISP_CONTR 寄存器,使能第 7 位 ISPEN,让 ISP_IAP 功能生效,并配置低三位的等待时间; 2. 写指令:读/写/擦除,3 个命令; 3. 赋值 ISP_ADDRH 和 ISP_ADDRL 的地址值,分别为所要操作位置的地址高低位; 4. 关闭总中断 EA,因为下面要写的 2 个触发指令必须是连续操作; 5. 执行 ISP_IAP 触发指令,触发后才能进行读写; 6. 打开总中断 EA,关闭 ISP_IAP 功能,清除相关寄存器。
IAP 及 E2PROM 新增特殊功能寄存器如下图所示:
1. ISP_DATA:ISP/IAP 数据寄存器ISP/IAP 操作时的数据存储器,ISP/IAP 从 Flash 读出来的数据存放在此处,向 Flash 写的数据也需要放在此处。2. ISP_ADDRH/ISP_ADDRL:ISP/IAP 地址寄存器分别为地址的高、低八位,复位值为 0x0000。3. ISP_CMD:ISP/IAP 命令寄存器MS1 MS0=00 待机模式,无数据读写操作;
MS1 MS0=01 从应用程序区对”Data Flash/E2PROM 区”进行字节读命令
MS1 MS0=10 从应用程序区对”Data Flash/E2PROM 区”进行字节写命令
MS1 MS0=11 从应用程序区对”Data Flash/E2PROM 区”进行扇区擦除命令
4. ISP_TRIG:ISP/IAP 命令触发寄存器在 ISPEN(ISP_CONTR.7)=1 时,对 ISP_TRIG 先写入 0x46,再写入 0xB9,ISP/IAP 功能才会生效。5. ISP_CONTR:ISP/IAP 控制寄存器ISPEN:ISP/IAP 功能允许位。ISPEN=0,禁止 ISP/IAP 读、写、擦除操作。ISPEN=1,允许 ISP/IAP 读、写、擦除操作。 SWBS:0 表示,软件从应用程序区启动,1 表示,从系统 ISP 监控程序区启动。需与 SWRST 配合使用。 SWRST:0 不操作,1 表示产生软件系统复位,硬件自动复位。 SWBS=1,SWRST=1 时,表示在应用程序区软件复位并从系统 ISP 监控程序区开始执行程序。SWBS=0,SWRST=1 时,表示在应用程序区软件复位并从应用程序区开始处执行程序。 B2~B0 表示在读、写、擦除操作过程中 CPU插入的等待时间,推荐选择如下所示。
18.3 E2PROM 驱动函数编写
前面已经讲解了与内部 E2PROM 有关的 6 个寄存器的功能,下面我们结合这些寄存器编写驱动函数,因为在正常的 reg52.h 中并没有对上述 6 个特殊功能寄存器进行声明,所以首先得进行声明以及名字字节定义,如下代码所示:
/****************特殊功能寄存器声明****************/ sfr ISP_DATA = 0xE2;sfr ISP_ADDRH = 0xE3;sfr ISP_ADDRL = 0xE4;sfr ISP_CMD = 0xE5;sfr ISP_TRIG = 0xE6;sfr ISP_CONTR = 0xE7;/******************定义命令字节******************/ #define read_cmd 0x01 //读命令#define wirte_cmd 0x02 //写命令#define erase_cmd 0x03 //擦除命令/****定义操作等待时间以及允许IAP操作*******/ #define enable_waitTime 0x82 //系统工作时钟< 20MHz 时
接下来两个函数分别为关闭、开启 ISP/IAP 功能函数,以便后续调用,如下所示:
void ISP_IAP_disable(void)//关闭ISP_IAP{EA=1;//恢复中断ISP_CONTR = 0x00; ISP_CMD = 0x00; ISP_TRIG = 0x00;}void ISP_IAP_trigger()//开启{EA=0; //下面的2条指令必须连续执行,故关中断ISP_TRIG = 0x46;//送触发命令字0x46ISP_TRIG = 0xB9;//送触发命令字0xB9}
如上所示,在开启功能也成为功能触发函数时需要关闭系统中断 EA,保证命令字 0x46、0xB9 被连续写入。单片机对 E2PROM 的操作包括读、写以及擦除,读数据操作步骤如下所示:
1. 清零数据寄存器 ISP_DATA,这一步不是必须的; 2. 向寄存器 ISP_CMD 写入读数据命令; 3. 允许 ISP/IAP,并给出操作等待时间; 4. 发送要读取的目标数据的存储地址; 5. 开启 ISP/IAP 功能; 6. 读出 ISP_DATA 中的数据并保存; 7. 关闭 ISP/IAP 功能;
上面讲解的是读取单个字节的步骤,如需读取多个字节的数据只需重复第 4 到第 6 步,读数据函数如下所示:
void ISP_IAP_readData(uint beginAddr, uchar* pBuf, uint dataSize) //读取数据{ISP_DATA=0; //清零,不清也可以ISP_CMD = read_cmd; //指令:读取ISP_CONTR = enable_waitTime;//开启ISP_IAP,并送等待时间while(dataSize--) //循环读取{ISP_ADDRH = (uchar)(beginAddr > > 8); //送地址高字节ISP_ADDRL = (uchar)(beginAddr & 0x00ff); //送地址低字节ISP_IAP_trigger(); //触发beginAddr++; //地址++*pBuf++ = ISP_DATA; //将数据保存到接收缓冲区}ISP_IAP_disable();//关闭ISP_IAP功能}
写数据函数与读数据函数类似,如下所示:
void ISP_IAP_writeData(uint beginAddr,uchar* pDat,uint dataSize) //写数据{ISP_CONTR = enable_waitTime; //开启ISP_IAP,并送等待时间ISP_CMD = wirte_cmd; //送字节编程命令字while(dataSize--){ISP_ADDRH = (uchar)(beginAddr > > 8); //送地址高字节ISP_ADDRL = (uchar)(beginAddr & 0x00ff);//送地址低字节ISP_DATA = *pDat++;//送数据beginAddr++;ISP_IAP_trigger();//触发}ISP_IAP_disable(); //关闭}
擦除扇区函数如下所示:
void ISP_IAP_sectorErase(uint sectorAddr)//扇区擦除{ISP_CONTR = enable_waitTime; //开启ISP_IAP;并送等待时间ISP_CMD = erase_cmd; //送扇区擦除命令字ISP_ADDRH = (uchar)(sectorAddr > > 8); //送地址高字节ISP_ADDRL = (uchar)(sectorAddr & 0X00FF);//送地址低字节ISP_IAP_trigger();//触发ISP_IAP_disable();//关闭ISP_IAP功能}
值得注意的是:在擦除扇区函数中,地址只需在该扇区范围内即可,不要求发送该扇区的首地址。到此我们编写完成了所有函数,因此将函整合到完整的驱动代码中。"Drive_Eeprom.h "代码如下:
#ifndef __Eeprom_H__#define __Eeprom_H__extern void ISP_IAP_disable(void);//关闭ISP_IAPextern void ISP_IAP_trigger();//触发extern void ISP_IAP_readData(unsigned int beginAddr, unsigned char* pBuf, unsigned int dataSize);//读取数据extern void ISP_IAP_writeData(unsigned int beginAddr,unsigned char* pDat,unsigned int dataSize);//写数据extern void ISP_IAP_sectorErase(unsigned int sectorAddr);//扇区擦除#endif
"Drive_Eeprom.c "代码如下:
#include< reg52.h >#define uint unsigned int#define uchar unsigned char/****************特殊功能寄存器声明****************/ sfr ISP_DATA = 0xE2;sfr ISP_ADDRH = 0xE3;sfr ISP_ADDRL = 0xE4;sfr ISP_CMD = 0xE5;sfr ISP_TRIG = 0xE6;sfr ISP_CONTR = 0xE7;/******************定义命令字节******************/ #define read_cmd 0x01 //读命令#define wirte_cmd 0x02 //写命令#define erase_cmd 0x03 //擦除命令/****定义操作等待时间以及允许IAP操作*******/ #define enable_waitTime 0x82 //系统工作时钟< 20MHz 时void ISP_IAP_disable(void)//关闭ISP_IAP{EA=1;//恢复中断ISP_CONTR = 0x00; ISP_CMD = 0x00; ISP_TRIG = 0x00;}void ISP_IAP_trigger()//触发{EA=0; //下面的2条指令必须连续执行,故关中断ISP_TRIG = 0x46;//送触发命令字0x46ISP_TRIG = 0xB9;//送触发命令字0xB9}void ISP_IAP_readData(uint beginAddr, uchar* pBuf, uint dataSize) //读取数据{ISP_DATA=0; //清零,不清也可以ISP_CMD = read_cmd; //指令:读取ISP_CONTR = enable_waitTime;//开启ISP_IAP,并送等待时间while(dataSize--) //循环读取{ISP_ADDRH = (uchar)(beginAddr > > 8); //送地址高字节ISP_ADDRL = (uchar)(beginAddr & 0x00ff); //送地址低字节ISP_IAP_trigger(); //触发beginAddr++; //地址++*pBuf++ = ISP_DATA; //将数据保存到接收缓冲区}ISP_IAP_disable();//关闭ISP_IAP功能}void ISP_IAP_writeData(uint beginAddr,uchar* pDat,uint dataSize) //写数据{ISP_CONTR = enable_waitTime; //开启ISP_IAP,并送等待时间ISP_CMD = wirte_cmd; //送字节编程命令字while(dataSize--){ISP_ADDRH = (uchar)(beginAddr > > 8); //送地址高字节ISP_ADDRL = (uchar)(beginAddr & 0x00ff);//送地址低字节ISP_DATA = *pDat++;//送数据beginAddr++;ISP_IAP_trigger();//触发}ISP_IAP_disable(); //关闭}void ISP_IAP_sectorErase(uint sectorAddr)//扇区擦除{ISP_CONTR = enable_waitTime; //开启ISP_IAP;并送等待时间ISP_CMD = erase_cmd; //送扇区擦除命令字ISP_ADDRH = (uchar)(sectorAddr > > 8); //送地址高字节ISP_ADDRL = (uchar)(sectorAddr & 0X00FF);//送地址低字节ISP_IAP_trigger();//触发ISP_IAP_disable();//关闭ISP_IAP功能}
18.4 E2PROM 应用
下面我们下一个小的应用程序来验证我们驱动函数,函数实现的功能为记录开发板上电的次数。并把上电的次数,显示到 1602 液晶显示器上,主函数如下图所示:
#include< reg52.h >#include"Drive_1602.h"#include"Drive_Eeprom.h"#define uchar unsigned char#define uint unsigned intsbit DU = P2^7;//数码管段选、位选引脚定义sbit WE = P2^6;sbit DU_L = P2^3;uchar pbuf[5] = {0};//数据缓冲区uchar str[8] = {0};//字符临时变量void main(){P3=0;P0 = 0;//关闭所有数码管WE = 1;WE = 0;DU_L = 0;Init_1602();//1602初始化ISP_IAP_readData(0x21f0,pbuf,sizeof(pbuf)); //读取内部存储器中数值pbuf[0]++;str[0] = pbuf[0]/100 + "0";str[1] = (pbuf[0]%100)/10 + "0";str[2] = pbuf[0]%10 + "0";str[4] = "�"; Disp_1602_str(1,1,str);//显示上电次数ISP_IAP_sectorErase(0x2000); //扇区擦除,一块512字节ISP_IAP_writeData(0x21f0,pbuf,sizeof(pbuf)); //写EEPROMwhile(1);}
将程序下载到单片机开发板观察现象是否与预想的一致。
18.5 本章小结
本章详细介绍了单片机内部EEPROM的读写原理及驱动程序的编写。