在实际开发中,经常会遇到某些需要断电需要再次上电记忆的场合,这就需要掉电存储芯片了,最常用的EEPROM芯片就是AT24C02了,几乎成了每一块开发板的标配,但是有些时候,在一些低成本的场合,需要用类EEPROM或者flash来模拟EEPROM进行存储,AT24C02是可以进行字节擦写的,STC内部的EEPROM是不可以字节擦写的,他是按照512byte一个扇区来组织的,如下图所示,我们开发板选择的这块芯片分为了22个扇区。如果你要擦除数据,那么必须要一次性的擦除512字节才可以,这么难用,难用总比没有强吧,在好多产品上,我都见到过,好多掉电了上电依然保持的参数,有没有外置EEPROM芯片,只能用内部的或者来模拟了,我们来写一个程序,程序的结果是记忆上电次数,每上电一次,就累加一次,显示在数码管上面,OK,写好的代码如下所示:
image.png

<div style="text-align: left;">/*******************************************************************************
复制代码
* 文件名: 数码管显示上电计数值

* 描  述: 上电计数

* 功  能:数码管的使用

* 作  者:大核桃 597627977

* 版本号:1.0.1(2018.09.21)

*******************************************************************************/

#include "stc15w.h"//头文件

#include "intrins.h"


/*******************************************************************************

* 文件名: 重定义

* 描  述:

* 功  能:

* 作  者:大核桃

* 版本号:1.0.1(2018.09.21)

*******************************************************************************/

typedef unsigned char uint8;

typedef unsigned int  uint16;

typedef unsigned long uint32;


/*******************************************************************************

* 文件名:共阳数码管真值表

* 描  述:

* 功  能:

* 作  者:大核桃

* 版本号:1.0.1(2018.09.21)

*******************************************************************************/

code uint8 LedChar[] = {

0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,

0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e

};


uint16 counter;        //记忆上电次数,最大65535


/*******************************************************************************

* 文件名:单独位定义

* 描  述:

* 功  能:

* 作  者:大核桃

* 版本号:1.0.1(2018.09.21)

*******************************************************************************/

sbit LED0 = P1^0;//第1组LED

sbit LED1 = P1^1;//第2组LED

sbit LED2 = P1^2;//第3组LED

sbit LED3 = P1^3;//第4组LED

sbit LED4 = P1^4;//第5组LED

sbit LED5 = P3^2;//第6组LED

sbit LED6 = P0^0;//第7组LED

sbit LED7 = P0^1;//第8组LED


sbit LEDS1 = P3^3;//数码管1

sbit LEDS2 = P3^4;//数码管2

sbit LEDS3 = P3^6;//数码管3

sbit LEDS4 = P3^7;//数码管4


/*******************************************************************************

* 文件名:全局变量定义区域

* 描  述:

* 功  能:

* 作  者:大核桃

* 版本号:1.0.1(2017.05.23)

*******************************************************************************/

#define CMD_IDLE    0               //空闲模式

#define CMD_READ    1               //IAP字节读命令

#define CMD_PROGRAM 2               //IAP字节编程命令

#define CMD_ERASE   3               //IAP扇区擦除命令


#define ENABLE_IAP  0x82            //if SYSCLK<20MHz

/*******************************************************************************

* 文件名:函数前置声明

* 描  述:

* 功  能:

* 作  者:大核桃

* 版本号:1.0.1(2017.05.23)

*******************************************************************************/

void Mcu_Port_Init();

void LedScan();

void Delay500ms(); //24MHZ

void Time0_Init();//定时器0

void IapIdle();

uint8 IapReadByte(uint16 addr);

void IapProgramByte(uint16 addr, uint8 dat);

void IapEraseSector(uint16 addr);

#define Delay()                {_nop_();_nop_();_nop_();_nop_();}


/*******************************************************************************

* 文件名

* 描  述: 主函数

* 功  能:入口

* 作  者:大核桃

* 版本号:1.0.1(2017.05.23)

*******************************************************************************/

void main(void)

{

counter = IapReadByte(0x0000);//读取数据

counter++;//写


Mcu_Port_Init();//IO上电初始化

Time0_Init();


IapEraseSector(0x0000);//擦除数据

IapProgramByte(0x0000, counter);//写入数据



while(1);

}


/*******************************************************************************

* 文件名:void LedScan()

* 描  述: LED刷新

* 功  能:

* 作  者:大核桃

* 版本号:1.0.1(2017.05.23)

*******************************************************************************/

void LedScan()

{

static uint8 i = 0;


P2 = 0Xff;

switch(i)

{

case 0: LEDS4 = 0;LEDS1 = 1;P2 = LedChar[counter / 1000 % 10];i++;break;

case 1: LEDS1 = 0;LEDS2 = 1;P2 = LedChar[counter / 100 % 10];i++;break;

case 2: LEDS2 = 0;LEDS3 = 1;P2 = LedChar[counter / 10 % 10];i++;break;

case 3: LEDS3 = 0;LEDS4 = 1;P2 = LedChar[counter % 10];i = 0;break;


default:break;

}

}

/*******************************************************************************

* 文件名:void Time0_Init()

* 描  述: 定时器0初始化

* 功  能:10毫秒@11.0592MHz

* 作  者:大核桃

* 版本号:1.0.1(2017.05.23)

*******************************************************************************/

void Time0_Init(void)

{

AUXR &= 0x7F;                //定时器时钟12T模式

TMOD &= 0xF0;                //设置定时器模式

TMOD |= 0X01;      //确保不干扰其他配置

TH0 = 0xDC;                //设置定时初值

TL0 = 0x00;                //设置定时初值

ET0 = 1;

TR0 = 1;                //定时器0开始计时

EA = 1;

}

/*******************************************************************************

* 文件名:

* 描  述: 中断函数

* 功  能:10毫秒@11.0592MHz

* 作  者:大核桃

* 版本号:1.0.1(2017.05.23)

*******************************************************************************/

void ET0_IRQHandler() interrupt 1

{

TH0 = 0xDC;                //设置定时初值

TL0 = 0x00;                //设置定时初值

LedScan();

}

/*******************************************************************************

* 文件名:void Mcu_Port_Init()

* 描  述: io初始化

* 功  能:

* 作  者:大核桃

* 版本号:1.0.1(2017.05.23)

*******************************************************************************/

void Mcu_Port_Init()

{

//将P0口低二位配置为推挽输出

//234567位配置位高阻输入

P0M1 = 0xFC;//1111 1100

P0M0 = 0X03;//0000 0011

//P0 = 0X01;//第6个

//P0 = 0X02;//第7个

//高3位配置高阻输入,用作模拟口

//其他配置推挽输出,驱动LED

P1M1 = 0xE0;//1110 0000

P1M0 = 0X1F;//0001 1111

//P2口配置准双向口

P2M1 = 0X00;

P2M0 = 0X00;

P2 = 0Xff; //上电为1111 1111


//        //P54,P55口为推挽输出

P5M1 = 0X00;

P5M0 = 0X00;

P5 = 0xFF;


//P37,P36,3.2,P3.3 P3.4口为推挽输出

P3M1 = 0X00;

P3M0 = 0XFC;

P3 = 0X23; //0010 0111//第5个LED端口

        
LED0 = 0;//第1组LED,如果使能请置为1

LED1 = 0;

LED2 = 0;

LED3 = 0;

LED4 = 0;

LED5 = 0;

LED6 = 0;

LED7 = 0;

}


/*******************************************************************************

* 文件名:void Delay500ms()                //@24.000MHz

* 描  述:Y5内核延时

* 功  能:

* 作  者:大核桃

* 版本号:1.0.1(2017.05.23)

*******************************************************************************/

void Delay500ms()                //@24.000MHz

{

unsigned char i, j, k;


_nop_();

_nop_();

i = 46;

j = 153;

k = 245;

do

{

do

{

while (--k);

} while (--j);

} while (--i);

}

/*******************************************************************************

* 文件名:void IapIdle()

* 描  述:关闭IAP

* 功  能:

* 作  者:大核桃

* 版本号:1.0.1(2017.05.23)

*******************************************************************************/

void IapIdle()

{

IAP_CONTR = 0;                  //关闭IAP功能

IAP_CMD = 0;                    //清除命令寄存器

IAP_TRIG = 0;                   //清除触发寄存器

IAP_ADDRH = 0x80;               //将地址设置到非IAP区域

IAP_ADDRL = 0;

}

/*******************************************************************************

* 文件名:uint8 IapReadByte(uint16 addr)

* 描  述:从ISP/IAP/EEPROM区域读取一字节

* 功  能:

* 作  者:大核桃

* 版本号:1.0.1(2017.05.23)

*******************************************************************************/

uint8 IapReadByte(uint16 addr)

{

uint8 dat;                       //数据缓冲区


IAP_CONTR = ENABLE_IAP;         //使能IAP

IAP_CMD = CMD_READ;             //设置IAP命令

IAP_ADDRL = addr;               //设置IAP低地址

IAP_ADDRH = addr >> 8;          //设置IAP高地址

IAP_TRIG = 0x5a;                //写触发命令(0x5a)

IAP_TRIG = 0xa5;                //写触发命令(0xa5)

_nop_();                        //等待ISP/IAP/EEPROM操作完成

dat = IAP_DATA;                 //读ISP/IAP/EEPROM数据

IapIdle();                      //关闭IAP功能


return dat;                     //返回

}

/*******************************************************************************

* 文件名:void IapProgramByte(uint16 addr, uint8 dat)

* 描  述: 写一字节数据到ISP/IAP/EEPROM区域

* 功  能:

* 作  者:大核桃

* 版本号:1.0.1(2017.05.23)

*******************************************************************************/

void IapProgramByte(uint16 addr, uint8 dat)

{

IAP_CONTR = ENABLE_IAP;         //使能IAP

IAP_CMD = CMD_PROGRAM;          //设置IAP命令

IAP_ADDRL = addr;               //设置IAP低地址

IAP_ADDRH = addr >> 8;          //设置IAP高地址

IAP_DATA = dat;                 //写ISP/IAP/EEPROM数据

IAP_TRIG = 0x5a;                //写触发命令(0x5a)

IAP_TRIG = 0xa5;                //写触发命令(0xa5)

_nop_();                        //等待ISP/IAP/EEPROM操作完成

IapIdle();

}


/*******************************************************************************

* 文件名:void IapEraseSector(uint16 addr)

* 描  述: 扇区擦除

* 功  能:

* 作  者:大核桃

* 版本号:1.0.1(2017.05.23)

*******************************************************************************/

void IapEraseSector(uint16 addr)

{

IAP_CONTR = ENABLE_IAP;         //使能IAP

IAP_CMD = CMD_ERASE;            //设置IAP命令

IAP_ADDRL = addr;               //设置IAP低地址

IAP_ADDRH = addr >> 8;          //设置IAP高地址

IAP_TRIG = 0x5a;                //写触发命令(0x5a)

IAP_TRIG = 0xa5;                //写触发命令(0xa5)

_nop_();                        //等待ISP/IAP/EEPROM操作完成

IapIdle();

}



程序上电后的执行效果图片如下:可以看到程序记录上电12次,稍后我们详细的解析下这个程序。

image.png



关于数码管的一些问题
     一个8段的数码管其实就是8个小灯啊,我们知道LED是有方向的,只有加正向偏置电压才会点亮,正极的一端是阳极,负极的一端是阴极,如果我们把所有的阳极连到一个公共点,通过给其阴极一个低电位的方法能够点亮的,叫做共阳极数码管,那么共阴极数码管就是倒过来了,高电平点亮,所有的阴极连在一起,限流电阻是友情提供的,实际是没有的,如下图所示:
image.png


image.png

有人可能觉得,那这8个小灯是如何排列的啊?怎么看呢?客官,您别急,我来画一下,您就明白了。如下图所示,共阳极数码管示意图:

image.png

有了这张图,我们来看一下程序,就好办了,想一想,如果我要在数码管上显示一个数字0怎么弄呢?如果是共阳极数码管。我应该让ABCDEF都是0才可以,也即是说,点亮该段即可实现,那么结合我们前面所讲解的数字电路知识,最高位我们不管,默认1即可 就是说要显示一个0,那么八段从低到高依次是,a = 0,b = 0,c = 0,d = 0,e = 0,f = 0,g = 1,dot = 1;也就是二进制的1100_0000,16进制是0XC0,如果我们想要0-9这10个数字,那么是不是可以用同样的方式,算出来,好了,真值表就是这么来的,至于共阳极,取反一下就是了。我们新建一个无符号字符型数组,将我们算好的数据放进数组里面。

/*******************************************************************************
  • * 文件名:共阳数码管真值表
  • * 描  述:
  • * 功  能:
  • * 作  者:大核桃
  • * 版本号:1.0.1(2018.09.21)
  • *******************************************************************************/
  • code uint8 LedChar[] = {
  •         0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,
  •         0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e
  • };
  • 复制代码

    前面为什么要加一个CODE关键字呢?51单片机有好多关键字,默认都是蓝色标识,注意,这个表示这个关键字在单片机系统中已经有名字了,不能随便命名,CODE关键字的意思是将该部分代码放在FLASH里面,而不是放在RAM里面,节省了程序运行空间,放在FLASH里面的变量是不能在程序运行时改变的。

    关于数码管的扫描刷新
    我们了解一个常识,就是人的眼睛是不能够分辨刷新速度小于10MS的物体的,就算变化了,你也看不出来的,最好的例子,就是,拿手机拍电视录像,一条条的,就是因为手机拍摄的速度太快,而电视画面刷新的太慢造成的,而这样的现象,我们是看不见的。
    用数码管来显示数字,基本上都是动态扫描刷新,所谓动态扫描,也就是先在1数码管赋值,然后切换到2数码管,切换到3,来回切换,我们只要把刷新速度控制在10MS之内,那么人的眼睛也看不出来的,我们这个代码就是这样进行处理的,如下所示;
    /*******************************************************************************
  • * 文件名:void LedScan()
  • * 描  述: LED刷新
  • * 功  能:
  • * 作  者:大核桃
  • * 版本号:1.0.1(2017.05.23)
  • *******************************************************************************/
  • void LedScan()
  • {
  •         static uint8 i = 0;
  •         P2 = 0Xff;
  •         switch(i)
  •         {
  •                 case 0: LEDS4 = 0;LEDS1 = 1;P2 = LedChar[counter / 1000 % 10];i++;break;
  •                 case 1: LEDS1 = 0;LEDS2 = 1;P2 = LedChar[counter / 100 % 10];i++;break;
  •                 case 2: LEDS2 = 0;LEDS3 = 1;P2 = LedChar[counter / 10 % 10];i++;break;
  •                 case 3: LEDS3 = 0;LEDS4 = 1;P2 = LedChar[counter % 10];i = 0;break;
  •                 default:break;
  •         }
  • }
  • 复制代码

    我们用到了SWITCH语句,SWITCH是一条多选一语句,以CASE为分支,break语句作为结束。我们来看下开发的原理图,4个数码管分别是NLED0,NLED1,NLED2,NLED3,这个段码和位码是如何选择的呢?用万用表的二极管档位,我们知道二极管是单向导电的,我们又知道正向偏置是可以点亮小灯的,不断的变换万用表的表笔,将亮的段位和引脚记下来,按照提供的数码管引脚图就可以分出段码和位码来。
    image.png

    如果我们要显示一个1,打开对应的IO,那么我们只要对P2赋值P2 = LedChar[1]就好了;可是在实际应用中,我们需要显示的更加复杂,因此,只能这样动态进行赋值了,新建一个counter变量,然后将最低位的数码管显示个位,第二个数码管显示10位,第三个数码管显示百位,第四个数码管显示千位,依次这样,相除取余数即可实现。


    关于内部EEPROM
    这个代码,是从STC的客户端上复制下来的,稍微整理了一下,不需要深入学习,你只要知道有多少个扇区,每个扇区的起始地址,就可以了,必要时候,回来翻阅数据手册就可以搞定,使用的时候,一定要注意,同一扇区的数据会全部被擦除掉,如果不想全部擦除,一定要写到不同的扇区我们实现的功能是,先上电读取一次0X0000地址的数据,然后我们counter++,然后我们擦除0X0000地址的数据,在重新向0X0000地址写入一个新的数据就OK,注意,写入之前先擦除,不然写不进去的