第2-8讲:片内EEPROM读写
-
- 学习目的
- 了解STC8A8K64D4片内EEPROM的分布和特点。
- 掌握STC8A8K64D4片内EEPROM分配以及读、写和擦除。
- 片内EEPROM概述
开发产品的时候,我们经常会遇到需要保存数据的应用场景,如一些重要的记录信息或软/硬件配置信息等。这些保存的数据在掉电的情况下是不能丢失的,并且,在需要的时候,用户也可以对数据进行更改。
STC8A8K64D4系列单片机考虑到了这样的应用场景,他利用 ISP/IAP 技术可将内部 Data Flash当 EEPROM,从而实现数据的掉电不丢失存储,EEPROM按照扇区组织的,每个扇区的大小是512字节,支持的擦写次数在 10 万次以上。
- 片内EEPROM分布
STC8A8K64D4 系列单片机片内集成了64K 字节的Flash(EEPROM),Flash和EEPROM的大小取决于具体的单片机型号,如下表所示。
不同型号的单片机的Flash和EEPROM大小不同,但是他们的和都是64K字节,该系列单片机中,STC8A8K64D4比较特殊,他的EEPROM大小是可以由用户设定的。
表1:STC8A8K64D4 系列单片机片内Flash和EEPROM分布
单片机型号 | EEPROM大小 | EEPROM扇区数量 |
STC8A8K16D4 | 48K | 96 |
STC8A8K32D4 | 32K | 64 |
STC8A8K48D4 | 16K | 32 |
STC8A8K60D4 | 4K | 8 |
STC8A8K64D4 | 用户自定义,大小必须是512的倍数。 |
- 片内EEPROM特点
- STC8A8K64D4 系列单片机片内EEPROM 的擦除是以扇区为单位进行的。
- EEPROM是逐字节读写的,写之前,写入地址所在的扇区需要先执行扇区擦除操作。这是因为EEPROM 的写操作只能将字节中的各个位由1 写为 0,而不能将0写为1,因此,写之前需要通过扇区擦除操作将整个扇区的位都写为1。
- EEPROM操作时间
- 读取 1 字节:4 个系统时钟(使用 MOVC 指令读取更方便快捷)。
- 编程 1 字节:约 30~40us(实际的编程时间为 6~7.5us,但还需要加上状态转换时间和各种控制信号的 SETUP 和 HOLD 时间)擦除 1 扇区( 512 字节): 约 4~6ms。
EEPROM 操作所需时间是硬件自动控制的,用户只需要正确设置 IAP_TPS 寄存器即可。IAP_TPS=系统工作频率/1000000(小数部分四舍五入进行取整)。
- 例如: 系统工作频率为 12MHz,则 IAP_TPS 设置为 12。
- 又例如: 系统工作频率为 22.1184MHz,则 IAP_TPS 设置为 22。
- EEPROM的访问
EEPROM 的访问方式有两种: IAP 方式和 MOVC 方式。IAP 方式可对 EEPROM 执行读、写、擦除操作,但 MOVC 只能对 EEPROM 进行读操作,而不能进行写和擦除操作。无论是使用 IAP 方式还是使用MOVC方式访问 EEPROM,首先都需要设置正确的目标地址。IAP方式时,目标地址与 EEPROM 实际的物理地址是一致的,均是从地址 0000H 开始访问,但若要使用 MOVC 指令进行读取 EEPROM 数据时,目标地址必须是在 EEPROM 实际的物理地址的基础上还有加上程序大小的偏移。
- 应用建议:
- 使用EEPROM存储数据时,可以考虑将关联性不强的数据分别存放在不同的扇区。这样,当某类数据需要更新时,就不需要对其他不相关的数据存放扇区进行操作。
- 存储数据时,应对数据更新的频率进行评估,以保证产品的生命周期内擦写次数不超过芯片支持的最大值(10万次)。如果数据更新频率高,可以考虑将数据在不同的扇区轮换存放,或者在同一个扇区的不同地址轮换存放,以降低使用的EEPROM扇区的擦写次数。
- 软件设计
- EEPROM读写步骤
- 软件设计
EEPROM操作需要按照下面几个步骤进行,包含规划写入数据的EEPROM空间、擦除EEPROM、写EEPROM以及读EEPROM。
图1:EEPROM读写操作步骤
-
-
-
- 规划写入数据的EEPROM空间
-
-
操作EEPROM之前,我们需要从片内Flash中划分出EEPROM空间,STC8A8K64D4系列单片机中,STC8A8K64D4型号单片机的EEPROM是需要用户自己设定的,其他型号都是已经分配好的,固定大小的。
STC8A8K64D4型号单片机的EEPROM空间划分下载程序时在STC-ISP 软件中设置的,如下图所示。
图2:EEPROM划分方法
EEPROM 分配总是从顶端向下分配的,下图是STC8A8K64D4型号单片机分配4K字节EEPROM的示例。
图3:分配4K字节EEPROM
我们在分配EEPROM空间时需要注意以下几点:
- 分配的EEPROM空间必须是512的倍数,即分配的单位是扇区。
- 分配的EEPROM空间不能占用程序存储的空间。
-
-
- 擦除EEPROM扇区
-
-
EEPROM擦除扇区时,只能使用IAP方式。擦除流程中需要注意的是,执行扇区擦除时,输入该扇区的任意地址即可,如擦除EEPROM的第一个扇区,地址输入“0x0000~0x01FF”中的任意地址都可以。通常,我们编程时,为了方便指定擦除的扇区,习惯于使用扇区的首地址作为擦除该扇区的地址,如擦除EEPROM的第一个扇区,地址输入“0x0000”。
图4:EEPROM擦除流程
- 使能和关闭IAP
EEPROM擦除扇区前需要使能IAP,擦除完成后需要关闭IAP。使能和关闭IAP由“EEPROM 控制寄存器( IAP_CONTR)”的位7(IAPEN)控制,如下图所示。
EEPROM 控制寄存器( IAP_CONTR):
- 设置等待时间
等待时间由“EEPROM 等待时间控制寄存器(IAP_TPS)”的位5~位0(IAPTPS[5:0])设置,如下图所示。
EEPROM 等待时间控制寄存器( IAP_TPS):
IAP_TPS=系统工作频率/1000000(小数部分四舍五入进行取整)。如本例中使用的系统工作频率为 24MHz,因此 IAP_TPS 设置为 24。
- 设置IAP命令(擦除扇区)
IAP命令由“EEPROM 命令寄存器(IAP_CMD)”的位1~位0(CMD[1:0])设置,如下图所示。
EEPROM 命令寄存器(IAP_CMD):
- CMD[1:0]:发送EEPROM操作命令
- 00: 空操作。
- 01: 读 EEPROM 命令。读取目标地址所在的 1 字节。
- 10: 写 EEPROM 命令。写目标地址所在的 1 字节。 注意:写操作只能将目标字节中的 1 写为 0,而不能将 0 写为 1。一般当目标字节不为 FFH 时,必须先擦除该目标地址所在的扇区。
- 11:擦除 EEPROM。擦除目标地址所在的扇区。注意:擦除操作会一次擦除 1个扇区(512 字节),整个扇区的内容全部变成 FFH。
- 设置扇区地址
扇区地址由“EEPROM 地址寄存器(IAP_ADDR)”设置,地址是16位的,IAP_ADDRH 保存扇区地址的高字节, IAP_ADDRL保存扇区地址的低字节,如下图所示。
EEPROM 地址寄存器(IAP_ADDR):
- 扇区地址设置示例:擦除EEPROM的第一个扇区(使用该扇区的任意地址都可以,习惯上使用起始地址)
IAP_ADDRH = 0xF0; //地址的高8位写入IAP_ADDRH寄存器
IAP_ADDRL = 0x00; //地址的高8位写入IAP_ADDRL寄存器
- 触发EEPROM操作
完成前面的4步配置之后,向EEPROM 触发寄存器(IAP_TRIG)先写入 5AH,再写入 A5H即可触发扇区擦除。EEPROM 触发寄存器(IAP_TRIG)如下图所示。
EEPROM 触发寄存器(IAP_TRIG):
写完触发命令后, CPU 会处于 IDLE 等待状态,直到相应的 IAP 操作执行完成后 CPU 才会从 IDLE 状态返回正常状态继续执行 CPU 指令。
-
-
-
- 写EEPROM
-
-
EEPROM编程只能使用IAP方式。EEPROM编程步骤如下图所示,他和擦除步骤类似,图中红色字体标注的步骤是和擦除操作不一样的。这里,我们只描述不一样的部分,其他步骤读者参照擦除操作即可。
图5:EEPROM写流程
- 设置IAP命令(编程EEPROM)
编程EEPROM 时,“EEPROM 命令寄存器(IAP_CMD)”的位1~位0(CMD[1:0])写入数值“10”。
编程时需要注意,待编程的地址所在的EEPROM扇区必须是擦除过的,否则,可能会导致错误。
- 设置EEPROM编程地址
和擦除操作地址一样,EEPROM编程地址也是由“EEPROM 地址寄存器(IAP_ADDR)”设置的。每次编程完成后,IAP_ADDRL 和 EEPROM 命令寄存器IAP_CMD 的内容不变,即地址不会自动递增,因此,在连续的地址上批量写入数据时,需手动更新地址寄存器IAP_ADDRH 和寄存器 IAP_ADDRL 的值。
- 待编程数据写入IAP_DATA寄存器
待编程数据在触发 EEPROM 的写操作前,必须写入到“EEPROM 数据寄存器(IAP_DATA)”,每次只能写入一个字节数据。
-
-
-
- 读EEPROM
-
-
EEPROM读取数据可以使用IAP方式或 MOVC 方式。IAP方式读步骤如下图所示,他和擦除、写步骤类似,图中红色字体标注的步骤是和擦除、写操作不一样的。这里,我们只描述不一样的部分,其他步骤读者参照擦除、写操作即可。
图6:EEPROM读流程
- 设置IAP命令(读EEPROM)
读EEPROM 时,“EEPROM 命令寄存器(IAP_CMD)”的位1~位0(CMD[1:0])写入数值“01”。
- 设置读地址
EEPROM读地址同样是由“EEPROM 地址寄存器(IAP_ADDR)”设置的。每次读完成后,IAP_ADDRL 和 EEPROM 命令寄存器IAP_CMD 的内容不变,即地址不会自动递增,因此,在连续的地址上批量读出数据时,需手动更新地址寄存器IAP_ADDRH 和寄存器 IAP_ADDRL 的值。
- 从IAP_DATA寄存器读出数据
读 EEPROM时,读命令执行完成后读出的 EEPROM 数据保存在 IAP_DATA 寄存器中。程序中,可以从IAP_DATA 寄存器获取读取的数据。
从EEPROM读取数据,还可以使用MOVC方式,MOVC方式读取速度要快于IAP方式。使用MOVC方式读取时,需要注意地址和IAP模式是不一样的,MOVC方式需要加上偏移地址。如下图所示,当从Flash划分4K字节的EEPROM后,IAP方式操作EEPROM是从地址0开始的,MOVC方式操作EEPROM的地址是从0xF000开始的。
图7:IAP方式和MOVC方式操作EEPROM时的地址
-
-
- EEPROM读写实验
-
- 注:本节的实验是在“实验2-6-1:串口1数据收发实验”的基础上修改,本节对应的实验源码是:“实验2-8-1:片内EEPROM读写”。
-
-
- 实验内容
-
-
本例中,从Flash中划分4K字节的EEPROM(共8个扇区)。程序中检测KEY1按键的状态,当KEY1按键按下后,擦除EEPROM的第1个扇区,然后从该扇区的起始地址开始连续写入256字节数据,之后分别通过IAP方式和MOVC方式读出数据并通过串口输出。
-
-
-
- 代码编写
-
-
- 新建一个名称为“eeprom.c”的文件及其头文件“eeprom.h”并保存到工程的“Source”文件夹,并将“eeprom.c”加入到Keil工程中的“SOURCE”组。
- 引用头文件
因为在“main.c”文件中使用了“eeprom.c”文件中的函数,所以需要引用下面的头文件“eeprom.h”。
代码清单:引用头文件
- //引用EEPROM的头文件
- #include "eeprom.h"
- 编写禁止访问EEPROM的函数
使用IAP方式操作EEPROM时,每次操作完成后都需要关闭对EEPROM的访问,并清零相关的寄存器,为了方便程序操作,我们编写一个禁止访问EEPROM的函数“EEPROM_Disable ()”供EEPROM操作调用,代码清单如下。
代码清单:禁止访问EEPROM
- /**************************************************************************
- * 描 述 : 禁止访问EEPROM
- * 参 数 : 无
- * 返回值 : 无
- *************************************************************************/
- void EEPROM_Disable(void)
- {
- IAP_CONTR = 0x00; //禁止EEPROM操作
- IAP_CMD = 0x00; //清零EEPROM命令寄存器
- IAP_TRIG = 0x00; //清零EEPROM触发寄存器,防止ISP/IAP命令误触发
- IAP_ADDRH = 0xFF; //将地址设置到非IAP区域,防止误操作
- IAP_ADDRL = 0xFF;
- }
- 擦除EEPROM扇区
因为EEPROM的编程原理是只能将各个bit由1写为 0,而不能将0写为1。因此在EEPROM编程之前,为了保证写入的正确性,如果扇区的内容不是0xFF,则必须将对应的扇区擦除,使得扇区的内容全部恢复为0xFF,扇区擦除代码清单如下。
代码清单:擦除指定的EEPROM扇区
- /***************************************************************************
- * 描 述 : 擦除指定的EEPROM扇区
- * 参 数 : EE_address: 待擦除扇区的首地址(使用扇区内任意地址都可以,习惯上使用首地址)
- * 返回值 : 无
- **************************************************************************/
- void EEPROM_SectorErase(u16 EE_address)
- {
- EA = 0; //禁止总中断
- IAP_CONTR |= 0x80; //允许EEPROM操作
- IAP_TPS = 24; //系统时钟使用的是:24MHz,IAP_TPS=24000000/1000000 = 24
- IAP_CMD = 0x03; //写入命令:扇区擦除命令
- IAP_ADDRH = EE_address / 256; //地址寄存器高8位赋值
- IAP_ADDRL = EE_address % 256; //地址寄存器低8位赋值
- IAP_TRIG = 0x5A; //触发EEPROM操作,IAP_TRIG先写入0x5A,再写入0xA5
- IAP_TRIG = 0xA5;
- _nop_(); //空操作,延时
- EEPROM_Disable(); //禁止访问EEPROM
- EA = 1; //开启总中断
- }
- 批量写入数据
批量写入数据实现了以给定的地址作为起始地址连续写入指定长度数据的功能,代码清单如下。注意写入数据时不要超过划分的EEPROM的边界。
代码清单:批量写入数据
- /***************************************************************************
- * 描 述 : 以指定的EEPROM地址为起始地址连续写入指定长度的数据
- * 参 数 : EE_address: EEPROM地址
- p_buf: 指向存放待写入数据的缓存
- w_size: 写入的字节数
- * 返回值 : 无
- ***************************************************************************/
- void EEPROM_write_bytes(u16 EE_address,u8 *p_buf,u16 w_size)
- {
- EA = 0; //禁止总中断
- IAP_CONTR |= 0x80; //允许EEPROM操作
- IAP_TPS = 24; //系统时钟使用的是:24MHz,IAP_TPS=24000000/1000000 = 24
- IAP_CMD = 0x02; //写入命令:写EEPROM命令
- do //循环写入数据
- {
- IAP_ADDRH = EE_address / 256; //地址寄存器高8位赋值
- IAP_ADDRL = EE_address % 256; //地址寄存器低8位赋值
- IAP_DATA = *p_buf; //待编程数据写入EEPROM数据寄存器
- IAP_TRIG = 0x5A; //触发EEPROM操作,IAP_TRIG先写入0x5A,再写入0xA5
- IAP_TRIG = 0xA5;
- _nop_(); //空操作,延时
- EE_address++;
- p_buf++;
- }while(--w_size);
- EEPROM_Disable(); //禁止访问EEPROM
- EA = 1; //开启总中断
- }
- 批量读取数据
批量读取数据实现了从以给定的EEPROM地址为起始地址连续读出指定长度的数据的功能,读出的数据保存到p_buf指向的缓存。
读取数据可以使用IAP方式或MOVC方式,下面是两种方式的代码清单,读者可以对比一下,不难发现,MOVC方式比IAP方式简单,并且读取速度快于IAP方式。
代码清单:批量读取数据:IAP方式
- /***************************************************************************
- * 描 述 : IAP方式读取数据。以给定的EEPROM地址为起始地址连续读出指定长度的数据,
- * : 读出的数据保存到p_buf指向的缓存
- * 参 数 : EE_address: EEPROM地址
- * : p_buf: 指向保存读出数据的缓存
- * : r_size: 读出的字节数
- * 返回值 : 无
- ***************************************************************************/
- void EEPROM_read_bytes_iap(u16 EE_address,u8 *p_buf,u16 r_size)
- {
- EA = 0; //禁止总中断
- IAP_CONTR |= 0x80; //允许EEPROM操作
- IAP_TPS = 24; //系统时钟使用的是:24MHz,IAP_TPS=24000000/1000000 = 24
- IAP_CMD = 0x01; //写入IAP命令:读EEPROM
- do
- {
- IAP_ADDRH = EE_address / 256; //地址寄存器高8位赋值
- IAP_ADDRL = EE_address % 256; //地址寄存器低8位赋值
- IAP_TRIG = 0x5A; //触发EEPROM操作,IAP_TRIG先写入0x5A,再写入0xA5
- IAP_TRIG = 0xA5;
- _nop_(); //空操作,延时
- *p_buf = IAP_DATA; //读出EEPROM数据寄存器的值保存到指定缓存
- EE_address++; //地址加1
- p_buf++; //数据缓存地址加1
- }while(--r_size);
- EEPROM_Disable(); //禁止访问ISP/IAP
- EA = 1; //开启总中断
- }
MOVC方式读取EEPROM的代码清单如下:
代码清单:批量读取数据:MOVC方式
- /***************************************************************************
- * 描 述 : MOVC方式读取数据。以给定的EEPROM地址为起始地址连续读出指定长度的数据,
- * : 读出的数据保存到p_buf指向的缓存
- * 参 数 : EE_address: 给定的读取数据的起始地址
- * : p_buf: 指向保存读出数据的缓存
- * : r_size: 读出的字节数
- * 返回值 : 无
- **************************************************************************/
- void EEPROM_read_bytes_movc(u16 EE_address,u8 *p_buf,u16 r_size)
- {
- u8 code *p_dat;
- p_dat = (u8 *)EE_address;
- while(r_size--) //循环读出数据
- {
- *p_buf = *p_dat; //读出ISP/IAP数据寄存器的值送往指定缓存
- p_dat++;
- p_buf++;
- }
- }
主函数中,检测到KEY1按键按下后,擦除EEPROM第一个扇区,接着从该扇区的起始地址开始连续写入256字节数据,之后分别通过IAP方式和MOVC方式读出数据并通过串口输出。
本例中,从Flash中划分4K字节的EEPROM(共8个扇区)。IAP方式读取数据时,第一个扇区的起始地址是0x0000。使用MOVC方式读取数据时,第一个扇区的起始地址是0xF000。程序代码清单如下。
代码清单:EEPROM读写测试
- //定义EEPROM读写软件缓存,大小各为256字节
- xdata u8 read_buf[256],write_buf[256];
- /**************************************************************************
- 功能描述:主函数
- 参 数:无
- 返 回 值:int类型
- **************************************************************************/
- int main(void)
- {
- u8 btn_val,j;
- u16 i,test_len;
- P2M1 &= 0x3F; P2M0 &= 0x3F; //设置P2.6~P2.7为准双向口(指示灯D1和D2)
- P7M1 &= 0xF9; P7M0 &= 0xF9; //设置P7.1~P7.2为准双向口(指示灯D4和D3)
- P3M1 &= 0xFE; P3M0 &= 0xFE; //设置P3.0为准双向口(串口1的RxD)
- P3M1 &= 0xFD; P3M0 |= 0x02; //设置P3.1为推挽输出(串口1的TxD)
- P3M1 &= 0x3F; P3M0 &= 0x3F; //设置P3.6~P3.7为准双向口(按键KEY2和KEY1)
- P0M1 &= 0x5F; P0M0 &= 0x5F; //设置P0.5,P0.7为准双向口(按键KEY4和KEY3)
- //如果按键电路上没有外部上拉电阻,需要开启GPIO的片内上拉。
- //开发板的按键电路设计了上拉电阻,因此,无需开启片内上拉
- // P_SW2 |= 0x80; //将EAXFR位置1,以访问在XDATA区域的扩展SFR
- // P0PU |= 0xA0; //开启P0.5、P0.7的上拉电阻
- // P3PU |= 0xC0; //开启P3.6、P3.7的上拉电阻
- // P_SW2 &= 0x7F; //将EAXFR位置0,恢复访问XRAM
- uart1_init(); //串口1初始化
- EA = 1; //使能总中断
- test_len = 256; //EEPROM读写测试长度为256个字节
- while(1)
- {
- btn_val=buttons_scan(0); //获取开发板用户按键检测值,不支持连按
- //按下KEY1:测试按页写入。扇区0的第一页写入256个字节,之后读出数据并通过串口输出
- if(btn_val == BUTTON1_PRESSED)
- {
- led_toggle(LED_1); //翻转指示灯D1的状态,指示按键按下
- //写之前需要先执行擦除操作,擦除EEPROM第一个扇区
- EEPROM_SectorErase(0x0000);
- //测试数据赋值
- j = 0;
- for(i=0;i<256;i++)write_buf[i] = j++;
- //写入测试数据
- EEPROM_write_bytes(0x0000,write_buf,test_len);
- //读出测试数据:IAP方式
- //在EEPROM的首地址为0x1000处读取5个字节存入buffer数组中
- EEPROM_read_bytes_iap(0x0000,read_buf,test_len);
- //串口打印读取的数据
- for(i=0;i<test_len;i++)printf("%02bX ",read_buf[i]);
- printf("\r\n");
- //读出测试数据:MOVC方式
- EEPROM_read_bytes_movc(0xF000,read_buf,test_len);
- //串口打印读取的数据
- for(i=0;i<test_len;i++)printf("%02bX ",read_buf[i]);
- }
- }
- }
-
-
- 硬件连接
-
-
本实验需要使用LED指示灯D1、按键KEY1和USB转串口,按下下图所示短接对应的跳线帽。
图8:D1和KEY1跳线帽短接
图9:USB转串口跳线帽短接
-
-
-
- 实验步骤
-
-
- 解压“…\第3部分:配套例程源码”目录下的压缩文件“实验2-8-1:片内EEPROM读写”,将解压后得到的文件夹拷贝到合适的目录,如“D\STC8”(这样做的目的是为了防止中文路径或者工程存放的路径过深导致打开工程出现问题)。
- 双击“…\eeprom\Project\object”目录下的工程文件“eeprom.uvproj”。
- 点击编译按钮编译工程,编译成功后生成的HEX文件“eeprom.hex”位于工程的“…\eeprom\project”目录下。
- 打开STC-ISP软件下载程序,下载使用内部IRC时钟,IRC频率选择:24MHz,用户EEPROM大小设置为4K。
- 电脑上打开串口调试助手,选择开发板对应的串口号,将波特率设置为9600bps。
- 程序运行后,按下KEY1按键,会擦除EEPROM的第一个扇区,接着从第一个扇区起始地址开始连续写入256个字节数据,之后分别使用IAP方式和MOVC方式读出数据并通过串口输出数据。
图11:串口接收的数据