STC15单片机-通过EEPROM恢复PWM亮度

通过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扇区默认数据

数据手册说到:

  1. 3个基本命令----字节读,字节编程,扇区擦除
  2. 字节编程:将“1”写成“1”或“0”,将“0”写成“0”。如果某字节是FFH,才可对其进行字节编程。如果该字节不是FFH,则须先将整个扇区擦除,因为只有“扇区擦除”才可以将“0”变为“1”。
  3. 扇区擦除:只有“扇区擦除”才可能将“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中保存,再将该扇区擦除,然后将须保留的数据写回该扇区,所以每个扇区中用的字节数越少越好,操作起来越灵活方便;

扇区中任意一个字节的地址都是该扇区的地址,无需求出首地址.

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值