通过EEPROM实现掉电后恢复PWM灯亮度
EEPROM介绍
EEPROM (Electrically Erasable Programmable Read-Only Memory),电可擦除可编程只读存储器,一种掉电后数据不丢失的存储芯片。
EEPROM 可以在电脑上或专用设备上擦除已有信息,重新编程。一般用在即插即用
EEPROM的发展
ROM:
在微机的发展初期,BIOS都存放在ROM(Read Only Memory,只读存储器)中。ROM内部的资料是在ROM的制造工序中,在工厂里用特殊的方法被烧录进去的,其中的内容只能读不能改,一旦烧录进去,用户只能验证写入的资料是否正确,不能再作任何修改。如果发现资料有任何错误,则只有舍弃不用,重新订做一份。ROM是在生产线上生产的,由于成本高,一般只用在大批量应用的场合。
PROM:
由于ROM制造和升级的不便,后来人们发明了PROM(Programmable ROM,可编程ROM)。最初从工厂中制作完成的PROM内部并没有资料,用户可以用专用的编程器将自己的资料写入,但是这种机会只有一次,一旦写入后也无法修改,若是出了错误,已写入的芯片只能报废。PROM的特性和ROM相同,但是其成本比ROM高,而且写入资料的速度比ROM的量产速度要慢,一般只适用于少量需求的场合或是ROM量产前的验证。
EPROM:
EPROM(Erasable Programmable ROM,可擦除可编程ROM)芯片可重复擦除和写入,解决了PROM芯片只能写入一次的弊端。EPROM芯片有一个很明显的特征,在其正面的陶瓷封装上,开有一个玻璃窗口,透过该窗口,可以看到其内部的集成电路,紫外线透过该孔照射内部芯片就可以擦除其内的数据,完成芯片擦除的操作要用到EPROM擦除器。EPROM内资料的写入要用专用的编程器,并且往芯片中写内容时必须要加一定的编程电压(VPP=12~24V,随不同的芯片型号而定)。EPROM的型号是以27开头的,如27C020(8*256K)是一片2M Bits容量的EPROM芯片。EPROM芯片在写入资料后,还要以不透光的贴纸或胶布把窗口封住,以免受到周围的紫外线照射而使资料受损。
EEPROM:
由于EPROM操作的不便,后来出主板上BIOS ROM芯片大部分都采用EEPROM(Electrically Erasable Programmable ROM,电可擦除可编程ROM)。EEPROM的擦除不需要借助于其它设备,它是以电子信号来修改其内容的,而且是以Byte为最小修改单位,不必将资料全部洗掉才能写入,彻底摆脱了EPROM Eraser和编程器的束缚。EEPROM在写入数据时,仍要利用一定的编程电压,此时,只需用厂商提供的专用刷新程序就可以轻而易举地改写内容,所以,它属于双电压芯片。借助于EEPROM芯片的双电压特性,可以使BIOS具有良好的防毒功能,在升级时,把跳线开关打至“on”的位置,即给芯片加上相应的编程电压,就可以方便地升级;平时使用时,则把跳线开关打至“off”的位置,防止CIH类的病毒对BIOS芯片(BIOS是Basic Input Output System的缩写,意思是基本输入输出系统,是用于计算机开机过程中各种硬件设备的初始化和检测的芯片,容量是1M或2M甚至8M。)的非法修改。所以,仍有不少主板采用EEPROM作为BIOS芯片并作为自己主板的一大特色 。
STC15系列单片机EEPROM的应用
STC15系列单片机内部集成了大容量的EEPROM,其与程序空间是分开的。利用ISP/IAP技术可将内部Data Flash当EEPROM,擦写次数在10万次以上。EEPROM可分为若干个扇区,每个扇区包含512字节。使用时,建议同一次修改的数据放在同一个扇区,不是同一次修改的数据放在不同的扇区,不一定要用满。数据存储器的擦除操作是按扇区进行的。
EEPROM可用于保存一些需要在应用过程中修改并且掉电不丢失的参数数据。在用户程序中,可以对EEPROM进行字节读/字节编程/扇区擦除操作。在工作电压VCC偏低时,建议不要进行EEPROM/IAP操作。
IAP及EEPROM新增特殊功能寄存器介绍
要使用到IAP对EEPROM进行操作,则需要配置以下的寄存器,除了PCON可以通过烧录软件STC-ISP进行选择而不需要编程配置,寄存器的详细用法可看数据手册
EEPROM扇区默认数据
数据手册说到:
- 3个基本命令----字节读,字节编程,扇区擦除
- 字节编程:将“1”写成“1”或“0”,将“0”写成“0”。如果某字节是FFH,才可对其进行字节编程。如果该字节不是FFH,则须先将整个扇区擦除,因为只有“扇区擦除”才可以将“0”变为“1”。
- 扇区擦除:只有“扇区擦除”才可能将“0”擦除为“1”。
所以扇区内如果从来没有被写入的话,默认的数据是FFH,因为只有为1,才能通过字节编程将其改变为1或0,就得到自己想写入的数据,如果本来是0的话,就只能被写成0,无法写入自己的数据;
在写入数据之前,都要擦除一次扇区
EEPROM空间大小及地址
手上开发板的型号是STC15L2K32S2,字节数是29K,扇区数58个,起始地址:0000h,结束地址:73FFh
数据手册中介绍完不同型号的EEPROM空间大小后,就有扇区的地址分布图
程序
文件结构
main.c -> 主函数文件,包含 main 函数等;
Public.c -> 公共函数文件,包含 Delay 延时函数等;
Sys_init -> 系统初始化函数,包含 GPIO 初始化函数等;
LED.c -> LED 外设函数,包含 LED 打开、关闭函数等;
Timer0.c -> 定时器函数,包含定时器初始化,中断函数等;
KEY1.c -> 按键 1 函数,包含按键检测,中断函数等;
KEY2.c -> 按键 2 函数,包含按键状态机检测函数等;
PWM.c -> PWM 初始化、亮度调节、占空比储存与恢复函数等;
IAP.c -> 字节读、字节写、扇区擦除等函数。
程序思路
1.占空比备份检测到按键 2 有动作,调整占空比改变 PWM 灯亮度后,备份占空比。
2.占空比恢复,亮度恢复上电进行 PWM 初始化时,恢复占空比,恢复亮度。
IAP.h
主要是宏定义要存储占空比的扇区地址,后面备份时用到的写入次数,以及结构体,包含IAP操作的函数指针
#ifndef __IAP_H_
#define __IAP_H_
#define IAP_PWM_DUTY_ADDR (uint16_t)0x0000 //PWM占空比存储地址,使用第一个扇区
#define IAP_CNT (uint8_t)10 //写入字节的最大次数
//定义结构体类型
typedef struct
{
uint8_t ucIAP_Flag; //IAP操作标志位
uint8_t ucIAP_Cnt; //IAP操作计数
uint8_t (*IapReadByte)(uint16_t );
void (*IapProgramByte)(uint16_t,uint8_t);
void (*IapEraseSector)(uint16_t);
}IAP_t;
/* extern variables-----------------------------------------------------------*/
extern IAP_t IAP;
/* extern function prototypes-------------------------------------------------*/
#endif
/********************************************************
End Of File
********************************************************/
IAP.c
通过IAP对EEPROM的操作,分别是读取一个字节,写入一个字节和擦除扇区,寄存器的参数根据数据手册来配置,数据手册这一章节的最后也有示例代码
/* Includes ------------------------------------------------------------------*/
#include <main.h>
/* Private define-------------------------------------------------------------*/
#define CMD_IDLE 0 //空闲、待机模式,无IAP操作
#define CMD_READ 1 //IAP字节读命令
#define CMD_PROGRAM 2 //IAP字节编程命令
#define CMD_ERASE 3 //IAP扇区擦除命令
#define ENABLE_IAP 0x83 //使能IAP,设置CPU等待时间
/* Private variables----------------------------------------------------------*/
static uint8_t IapReadByte(uint16_t addr);
static void IapProgramByte(uint16_t addr,uint8_t dat);
static void IapEraseSector(uint16_t addr);
/* Public variables-----------------------------------------------------------*/
IAP_t IAP =
{
FALSE,
0,
IapReadByte,
IapProgramByte,
IapEraseSector
};
/* Private function prototypes------------------------------------------------*/
/*
* @name IapIdle
* @brief 关闭IAP
* @param None
* @retval None
*/
static void IapIdle()
{
IAP_CONTR = 0; //关闭IAP功能
IAP_CMD = CMD_IDLE; //清除命令寄存器,MS1和MS0为0,待机模式,无ISP操作
IAP_TRIG = 0; //清除触发寄存器
IAP_ADDRH = 0xFF; //将地址设置到非IAP地区
IAP_ADDRL = 0xFF;
}
/*
* @name IapReadByte
* @brief 读取一个字节
* @param addr:要读取字节的地址
* @retval uint8_t:返回读到的字节数据
*/
static uint8_t IapReadByte(uint16_t addr)
{
uint8_t dat;
IAP_CONTR = ENABLE_IAP; //使能IAP,最高位IAPEN置1,并设置CPU等待时间
IAP_CMD = CMD_READ; //设置IAP命令,对EEPROM区进行字节读
IAP_ADDRL = addr; //设置IAP读取的低地址,IAP_ADDRL只会存储addr的低8位
IAP_ADDRH = addr >> 8; //设置IAP读取的高地址,将addr右移8位,则将低8位全部移出去了,剩下了高8位,赋给IAP_ADDRH
IAP_TRIG = 0x5a; //按数据手册说法要写触发命令
IAP_TRIG = 0xa5;
_nop_(); //等待操作完成
dat = IAP_DATA; //读数据
IapIdle(); //关闭IAP功能,该函数可以不调用,只是出于安全考虑而使用,后面函数同理
return dat; //返回读取到的数据
}
/*
* @name IapProgramByte
* @brief 写入一个字节
* @param addr:要被写入的地址
* @param dat:写入的数据
* @retval None
*/
static void IapProgramByte(uint16_t addr,uint8_t dat)
{
IAP_CONTR = ENABLE_IAP; //使能IAP,最高位IAPEN置1,并设置CPU等待时间
IAP_CMD = CMD_PROGRAM; //设置IAP命令,对EEPROM区进行字节编程
IAP_ADDRL = addr; //设置IAP写入的低地址
IAP_ADDRH = addr >> 8; //设置IAP写入到高地址
IAP_DATA = dat; //写入数据
IAP_TRIG = 0x5a; //写触发命令
IAP_TRIG = 0xa5;
_nop_(); //等待操作完成
IapIdle(); //关闭IAP功能
}
/*
* @name IapEraseSector
* @brief 扇区擦除(每扇区为512字节)
* @param addr:要擦除的扇区地址
* @retval None
*/
static void IapEraseSector(uint16_t addr)
{
IAP_CONTR = ENABLE_IAP; //使能IAP,最高位IAPEN置1,并设置CPU等待时间
IAP_CMD = CMD_ERASE; //设置IAP命令,对EEPROM区进行扇区擦除
IAP_ADDRL = addr; //设置IAP擦除扇区的低地址
IAP_ADDRH = addr >> 8; //设置IAP擦除扇区的高地址
IAP_TRIG = 0x5a; //写触发命令
IAP_TRIG = 0xa5;
_nop_(); //等待操作完成
IapIdle(); //关闭IAP功能
}
/********************************************************
End Of File
********************************************************/
PWM.h
在结构体内增加了两个函数指针,分别是通过IAP备份占空比和恢复占空比
#ifndef __PWM_H_
#define __PWM_H_
//定义表示占空比的枚举类型
typedef enum
{
Duty_0 = (uint8_t)0,
Duty_20 = (uint8_t)20,
Duty_40 = (uint8_t)40,
Duty_60 = (uint8_t)60,
Duty_80 = (uint8_t)80,
Duty_100 = (uint8_t)100
}PWM_Value_t;
//定义结构体类型
typedef struct
{
PWM_Value_t Duty; //PWM占空比
void (*PWM_Init)(); //PWM初始化
void (*PWM_LED_Adjust_Brightness)(); //调整PWM亮度
void (*IAP_Duty_Backup)(uint16_t,uint8_t); //通过IAP备份占空比
uint8_t (*IAP_Duty_Restore)(uint16_t); //通过IAP恢复占空比
}PWM_t;
/* extern variables-----------------------------------------------------------*/
extern PWM_t PWM;
/* extern function prototypes-------------------------------------------------*/
#endif
/********************************************************
End Of File
********************************************************/
PWM.c
将设置占空比的CCAP0H和CCAP0L赋值部分写成一个函数,在PWM初始化后和按键调整灯亮度函数最后调用该函数设置PWM;新增两个函数,分别是通过IAP备份占空比和恢复占空比,所用到的函数实现在IAP.c源文件中
/* Includes ------------------------------------------------------------------*/
#include <main.h>
/* Private define-------------------------------------------------------------*/
#define CCP_S1 BIT5
#define CCP_S0 BIT4
#define EPC0H BIT1
#define EPC0L BIT0
/* Private variables----------------------------------------------------------*/
static void PWM_Init();
static void PWM_LED_Adjust_Brightness();
static void PWM_Duty_Set(uint8_t PWM_Duty);
static void IAP_Backup(uint16_t IAP_Addr,uint8_t IAP_Data);
static uint8_t IAP_Restore(uint16_t IAP_Addr);
/* Public variables-----------------------------------------------------------*/
PWM_t PWM =
{
Duty_20,
PWM_Init,
PWM_LED_Adjust_Brightness,
/*初始化的函数名跟函数指针名不一样,这是因为这两个函数当作通用函数,其他文件也能使用,
函数指针的名字仅拿来当PWM备份占空比使用*/
IAP_Backup,
IAP_Restore
};
/* Private function prototypes------------------------------------------------*/
/*
* @name PWM_Init
* @brief PWM初始化
* @param None
* @retval None
*/
static void PWM_Init()
{
//选择管脚,因为开发板的PWM灯接到了P3.5口
//将BIT5的CCP_S1清0,BIT4的CCP_S0置1,即可将CCP切换到P3.5管脚
AUXR1 &= ~(CCP_S1);
AUXR1 |= (CCP_S0);
//CCON里都是一些标志位,全置0即可
/*CMOD 的BIT7置0,设置空闲模式下PAC计数器继续工作;BIT3、BIT2、BIT1置为110,
系统时钟6分频,SYSclk/6*/
CCON = 0x00;
CMOD = 0x0C;
//用于保存PCA装载值的16位计数器都清零
CL = 0;
CH = 0;
//BIT6置1,允许比较器功能;BIT1置1,允许CCP0脚用作脉宽调节输出
//PCA_PWM0的BIT7和BIT6置0,使模块工作于8位PWM模式,BIT1和BIT0的EPC0H和EPC0L清0
CCAPM0 = 0x42;
PCA_PWM0 = 0x00;
//恢复占空比
PWM.Duty = PWM.IAP_Duty_Restore(IAP_PWM_DUTY_ADDR);
//设置占空比
PWM_Duty_Set(PWM.Duty);
CR = 1;
}
/*
* @name PWM_LED_Adjust_Brightness
* @brief PWM灯调整亮度
* @param None
* @retval None
*/
static void PWM_LED_Adjust_Brightness()
{
if(KEY2.KEY_Flag == TRUE)
{
//单击 亮度 0-20-40-60-80-100-0 循环调节
//双击 亮度 100
//长按 亮度 0
if(KEY2.Click == TRUE)
{
switch (PWM.Duty)
{
case Duty_0: PWM.Duty = Duty_20; break;
case Duty_20: PWM.Duty = Duty_40; break;
case Duty_40: PWM.Duty = Duty_60; break;
case Duty_60: PWM.Duty = Duty_80; break;
case Duty_80: PWM.Duty = Duty_100;break;
case Duty_100: PWM.Duty = Duty_0; break;
default: PWM.Duty = Duty_0; break;
}
}
//检测双击
else if(KEY2.Double_Click == TRUE)
{
PWM.Duty = 100;
}
//检测长按
else if(KEY2.Press == TRUE)
{
PWM.Duty = 0;
}
//设置占空比,调整亮度
PWM_Duty_Set(PWM.Duty);
//标志位清零
KEY2.KEY_Flag = FALSE;
KEY2.Click = FALSE;
KEY2.Double_Click = FALSE;
KEY2.Press = FALSE;
//备份占空比
PWM.IAP_Duty_Backup(IAP_PWM_DUTY_ADDR,PWM.Duty);
}
}
/*
* @name PWM_Duty_Set
* @brief PWM灯调整亮度
* @param None
* @retval None
*/
static void PWM_Duty_Set(uint8_t PWM_Duty)
{
uint8_t Temp_Value = 0;
/*初始化时PWM.Duty的值为Duty_20,单击按下后,进入该switch语句
PWM.Duty 被修改为Duty_40,占空比变量Temp_Value被赋值153,然后
跳出switch语句,后面对CCAP0H赋值,输出占空比
下次再单击按键,进入switch,匹配case Duty_40,所以PWM.Duty会
再次被改变为Duty_60,占空比输出102即60%*/
switch (PWM_Duty)
{
case Duty_0: break;
case Duty_20: Temp_Value = 204; break;
case Duty_40: Temp_Value = 153; break;
case Duty_60: Temp_Value = 102; break;
case Duty_80: Temp_Value = 51; break;
case Duty_100: break;
default: PWM_Duty = Duty_0; break;
}
//亮度调节
//占空比为0%,全输出低电平,PWM灯灭
if(PWM.Duty == 0)
{
PCA_PWM0 |= (EPC0H);
CCAP0H = 0xFF;
PCA_PWM0 |= (EPC0L); //置1则表示9位数,加上CCAP0L最大去到511
CCAP0L = CCAP0H;
}
//占空比为100%,全输出高电平,PWM灯全亮
else if(PWM.Duty == 100)
{
PCA_PWM0 &= ~(EPC0H);
CCAP0H = 0x00;
PCA_PWM0 &= ~(EPC0L);
CCAP0L = 0x00;
}
else
{
//根据Temp_Value设置的值修改占空比
PCA_PWM0 &= ~(EPC0H); //~0000 0010 -> 1111 1101 即让EPC0H为0
CCAP0H = Temp_Value;
PCA_PWM0 &= ~(EPC0L); //因为上一步将BIT0,即EPC0L位置1了,所以也让该位清0后,CCAP0H再赋值给CCAP0L
CCAP0L = CCAP0H;
}
}
/*
* @name IAP_Backup
* @brief 通过IAP备份
* @param IAP_Addr:备份地址
* @param IAP_Data:备份数据
* @retval None
*/
static void IAP_Backup(uint16_t IAP_Addr,uint8_t IAP_Data)
{
IAP.ucIAP_Cnt = IAP_CNT; //写入的次数,多次写入都失败的话,单片机会死机的,所以要保证次数
while(IAP.ucIAP_Cnt--)
{
IAP.IapEraseSector(IAP_Addr); //写数据前要把扇区擦除
IAP.IapProgramByte(IAP_Addr,IAP_Data); //写入数据
IAP.ucIAP_Flag = TRUE; //假如写成功了
if(IAP.IapReadByte(IAP_Addr) != IAP_DATA)
{
IAP.ucIAP_Flag = FALSE; //不成功
}
//判断是否写成功
if(IAP.ucIAP_Flag == TRUE) //若写成功,则退出循环
{
break;
}
}
}
/*
* @name IAP_Restore
* @brief 通过IAP恢复
* @param IAP_Addr:恢复地址
* @retval 恢复值
*/
static uint8_t IAP_Restore(uint16_t IAP_Addr)
{
//考虑产品稳定性,定义了两个变量,读两次
uint16_t Read_IAP_Para1,Read_IAP_Para2;
uint8_t IAP_Data = 0;
//从EEPROM读取数据
Read_IAP_Para1 = IAP.IapReadByte(IAP_Addr);
Read_IAP_Para2 = IAP.IapReadByte(IAP_Addr);
//判断两次读取是否一致
if(Read_IAP_Para1 == Read_IAP_Para2)
{
if(Read_IAP_Para1 != 0xFF) //如果读出来不是FF说明之前已经有备份
{
IAP_Data = Read_IAP_Para1;
}
else
{
IAP_DATA = Duty_0; //从来就没备份写入过数据,就让灯灭
}
}
//两次读取不一致
else
{
IAP_Data = Duty_0; //让PWM灯灭,也可以做其他处理,比如重启
}
return IAP_Data;
}
/********************************************************
End Of File
********************************************************/
数据手册中给出的要注意的细节
扇区擦除,没有字节擦除,只有扇区擦除,512字节/扇区,每个扇区用得越少越方便;
如果要对某个扇区进行擦除,而其中有些字节的内容需要保留,则需将其先读到单片机内部的RAM中保存,再将该扇区擦除,然后将须保留的数据写回该扇区,所以每个扇区中用的字节数越少越好,操作起来越灵活方便;
扇区中任意一个字节的地址都是该扇区的地址,无需求出首地址.