目录
一、存储器
1、易失性存储器RAM
2、非易失性存储器ROM
3、存储器的简化模型
二、AT24C02
1、AT24C02介绍
2、引脚及应用电路
3、内部结构框图
三、I2C总线
1、I2C总线介绍
2、I2C电路规范
3、I2C时序结构
四、AT24C02数据存储
1、编写程序
2、实物展示
五、秒表(定时器扫描按键数码管)
1、编写程序
2、实物展示
优点:存储速度特别快
缺点:掉电丢失数据
SRAM(静态RAM):内部存储结构为锁存器,IS触发器,JK触发器,D触发器,用电路来存储数据,它的存储速度是所有存储器中最快的一个,一般用于电脑的CPU,高速缓存。
51单片机中的数据存储器RAM就是SRAM,它的容量相对较小,成本较高。
DRAM(动态RAM):它用电容来存储数据,电容充上电后就显示高电平,放完电后就是低电平,利用电容的充放电,以达到存储数据的目的。
由于电容存在漏电现象,在存储数据后,很快就会漏电漏完,所以需要配一个扫描电路,每隔一段时间,就读取电容数据,来给电容补电,所以DRAM称为动态RAM,需要定时刷新。
动态RAM相比静态RAM,它的成本更低,容量更大,电脑中的内存条,手机中的运行内存都是动态RAM。
优点:掉电不丢失数据
缺点: 存储速度较慢
当我们需要高速存储的时候,就把数据放RAM里面,程序运行时的数据都是存储在RAM里面。
当我们需要永久保存数据时,就把数据转存到ROM里面。
Mask ROM(掩膜ROM):最早的ROM,可以保证掉电不丢失,但是只能读,不能写。
PROM(可编程ROM):相比于Mask ROM,可以写入数据了,但是只能在出厂的时候写入一次数据,并且无法更改。
EPROM(可擦除可编程ROM):相比以上两个,既可编程数据,也可以擦除数据,但是擦除数据需要用紫外线照射30分钟。
E2PROM(电可擦除可编程ROM):在5V的低压电的情况下,几毫秒就能擦除数据。
Flash(闪存):用途十分广泛,包括单片机的程序存储器,U盘,内存卡,电脑的固态硬盘,手机的存储空间。
硬盘、软盘、光盘等:硬盘一般指电脑中的机械硬盘,靠磁介质来存储数据。
软盘是电脑早期用来存储数据的,软盘一般容量也很小。
光盘是用光信号来存储数据的。
在存储器内部中,实际上都是电路的一个网状结构,横向的线称为地址总线,纵向的线称为数据总线,这些横纵交错的节点一般默认是不连接的。
存储原理:当我们选择地址总线的第一行时,给第一行加上一个高电平1,剩下的地址总线暂时不接,然后接上前面三个节点,剩下的都不连,然后由第一根地址总线经过第一个节点读取数据总线为1,第二,第三个节点也都是1,后面由于节点没有连接,线都是悬空状态,因此都为0,这样在第一行地址总线下,就存储着数据1110 0000。第二行以及后面的地址总线以此类推。
Mask ROM:一开始节点是自动断开的,如果想要节点短路,就两根线之间接一个二极管,这样能防止其他电流干扰。
PROM:一开始使电路断开,就接两个二极管,没有电流通过,其中蓝色的比较特殊,容易被电流击穿;当我们要使电路短路,有电流通过时,就给电路接一个高电压,就会变成右边电路的一个状况。
这就是为什么有时候给单片机下载程序时,也叫烧入程序,早期的时候就是都用PROM这种存储方式,就需要把特殊的二极管给烧毁。
而EPROM被紫外线照射一段时间就会擦除程序,就是因为一些特殊二极管被烧毁后,被紫外线照射一段时间又会复原,因此又重新恢复了存储的功能。
AT24C02是一种可以实现掉电不丢失的存储器,可用于保护单片机运行时想要永久保存的数据信息。
存储介质:E2PROM
通讯接口:I2C总线(2是指平方的意思,因此念:I方C总线)
容量:256字节
VSS=GND VDD=VCC A0、A1、A2=E0、E1、E2
I2C总线(Inter IC BUS)是由菲利普公司开发的一种通用数据总线。
两根通信线:SCL(Serial Clock)、SDA(Serial Data)
是一种同步、半双工、带数据应答的数据总线。
同步:有单独的时钟线;
半双工:只有一根线进行来回通信,所以通信只能分时复用一根线;
带数据应答: 发送完一个字节数据后,要求接收数据方给一个应答。
通用的I2C总线,可以使各种设备的通信标准统一,对于厂家来说,使用成熟的方案可以缩短芯片设计周期、提高稳定性,对于应用者来说,使用通用的通信协议可以避免学习各种各样的自定义协议,降低了学习和应用的难度。
所有I2C设备的SCL连在一起,SDA连在一起。
SCL和SDA各外加一个上拉电阻,阻值一般为4.7KΩ左右;
设备的SCL和SDA均要配置成开漏输出模式。
引脚的弱上拉模式:当输入为0时,开关闭合,输出直接与GND连接;
当输入为1时,开关断开,高电平经过电阻,到输出,所以高电平没有低电平驱动能力强。
引脚的开漏输出模式:当输入为0时,开关闭合,输出直接与GND连接;
当输入为1时,开关断开,引脚呈浮空状态,实际上就是一种断开的状态,引脚什么都没有接,电平是不稳定的,极易受到外界干扰。
开漏输出和上拉电阻的共同作用实现了“线与”的功能,此设计主要是为了解决多机通信互相干扰的问题。
在开漏输出模式,如果CPU要对被控IC1进行通信,只需对其他连接设备引脚都输入1,相当于断开状态,这样就能使其他设备无法影响CPU和被控IC1的通信。
又因为开漏输出模式下,无法传输1的数据,因此在SCL和SDA外部各添加一个上拉电阻。
当CPU想发0时,就拉到低电平,当CPU想发送1时,就松手,不拉到低电平,就会被外部的电阻,自动拉到高电平,形成弱上拉模式,这样就能发送数据1。
(这里不理解没关系,主要看懂后面时序结构就能写代码)
起始条件:SCL高电平期间,SDA从高电平切换到低电平
终止条件:SCL高电平期间,SDA从低电平切换到高电平
发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位在前),然后拉高SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许又数据变化,依次循环上述过程8次,即可发送一个字节。
(主机:单片机,从机:AT24C02存储器或其他要传输数据的模块)
SDA这里两条线是指两种可能,SDA在SCL低电平时才会变化,因此在SCL第一个上拉高电平之前,SDA会变化成要发送的数据0或1,然后在SCL上拉成高电平后,从机会读取SDA发送的这个数据,读取完成后,SCL又变成低电平,SDA再变化成相应要发送的数据,然后SCL再上拉成高电平,再读取数据,以此类推,循环8次后,即可发送一个字节。
接收一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位在前),然后拉高SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)。
使SDA置1相当于释放SDA,释放时,主机是完全不干预通信线的,即把通信线的控制权交给从机,从机拿到控制权后,会把要发送的字节发送给主机。(图中紫线表示从机控制总线的部分)
发送应答:在接收完一个字节之后,主机在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答;
接收应答:在发送完一个字节之后,主机在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接受之前,需要释放SDA)。
4、数据帧
读数据帧中,读数据黄色块后,需要增加一个应答信号位。
写数据(W=0)=发送数据,读数据(R=1)=接收数据
AT24C02的固定地址为1010,可配置地址开发板上为(A0、A1、A2)000,所以
设备地址+W为0xA0,
设备地址+R为0xA1。
主函数
#include
#include "LCD1602.h"
#include "Key.h"
#include "AT24C02.h"
#include "Delay.h"
unsigned char KeyNum;
unsigned int Num;
void main()
{
LCD_Init();
LCD_ShowNum(1,1,Num,5);
while(1)
{
KeyNum=Key();
if(KeyNum==1) //K1按键,Num自增
{
Num++;
LCD_ShowNum(1,1,Num,5);
}
if(KeyNum==2) //K2按键,Num自减
{
Num--;
LCD_ShowNum(1,1,Num,5);
}
if(KeyNum==3) //K3按键,向AT24C02写入数据
{
AT24C02_WriteByte(0,Num%256); //将Num的低8位放入地址为0的寄存器
Delay(5);
AT24C02_WriteByte(1,Num/256); //将Num的高8位放入地址为1的寄存器
Delay(5);
LCD_ShowString(2,1,"Write OK");
Delay(1000);
LCD_ShowString(2,1," ");
}
if(KeyNum==4) //K4按键,从AT24C02读取数据
{
Num=AT24C02_ReadByte(0); //将从地址0读出的数据放到Num的低八位
Num|=AT24C02_ReadByte(1)<<8; //将从地址1读出的数据左移8位后放入Num的高八位
LCD_ShowNum(1,1,Num,5);
LCD_ShowString(2,1,"Read OK ");
Delay(1000);
LCD_ShowString(2,1," ");
}
}
}
AT24C02模块
#include
#include "I2C.h"
#define AT24C02_ADDRESS 0xA0 //这里不加;
//0xA0,AT24C02写入地址
//0xA1,AT24C02读取地址
/**
* @brief AT24C02写入一个字节
* @param WordAddress 要写入字节的地址
* @param Data 要写入的数据
* @retval 无
*/
void AT24C02_WriteByte(unsigned char WordAddress,Data)
{
I2C_Start(); //I2C开始
I2C_SendByte(AT24C02_ADDRESS); //发送设备地址
I2C_ReceiveAck(); //接收应答
I2C_SendByte(WordAddress); //发送寄存器地址
I2C_ReceiveAck(); //接收应答
I2C_SendByte(Data); //要写入的数据
I2C_ReceiveAck(); //接收应答
I2C_Stop(); //I2C停止
}
/**
* @brief AT24C02读取一个字节
* @param WordAddress 要读出字节的地址
* @retval 读出的数据
*/
unsigned char AT24C02_ReadByte(unsigned char WordAddress)
{
unsigned char Data;
I2C_Start();
I2C_SendByte(AT24C02_ADDRESS); //发送设备地址
I2C_ReceiveAck();
I2C_SendByte(WordAddress); //发送寄存器地址
I2C_ReceiveAck();
I2C_Start();
I2C_SendByte(AT24C02_ADDRESS|0x01); //将写设备地址变为读设备地址
I2C_ReceiveAck();
Data=I2C_ReceiveByte(); //将从AT24C02收到的数据赋给Data
I2C_SendAck(1); //发送应答信号给AT24C02
I2C_Stop(); //I2C停止
return Data;
}
I2C模块
#include
sbit I2C_SCL=P2^1;
sbit I2C_SDA=P2^0;
/**
* @brief I2C开始
* @param 无
* @retval 无
*/
void I2C_Start(void) //起始条件:SCL高电平期间,SDA从高电平切换到低电平
{
I2C_SDA=1;
I2C_SCL=1;
I2C_SDA=0;
I2C_SCL=0;
}
/**
* @brief I2C停止
* @param 无
* @retval 无
*/
void I2C_Stop(void) //终止条件:SCL高电平期间,SDA从低电平切换到高电平
{
I2C_SDA=0;
I2C_SCL=1;
I2C_SDA=1;
}
/**
* @brief I2C发送一个字节
* @param Byte 要发送的字节
* @retval 无
*/
void I2C_SendByte(unsigned char Byte)
{
unsigned char i;
for(i=0;i<8;i++)
{
I2C_SDA=Byte&(0x80>>i); //每次循环将Byte的一位写入
I2C_SCL=1; //由使用手册可知,SCL高电平宽度大于0.4us,便能读取数据位
//单片机机器周期为1us,因此不需要延时函数
I2C_SCL=0;
}
}
/**
* @brief I2C接收一个字节
* @param 无
* @retval 接收到的一个字节数据
*/
unsigned char I2C_ReceiveByte(void)
{
unsigned char i,Byte=0x00;
I2C_SDA=1; //使SDA置1相当于释放总线,主机是完全不干预通信线的,
//即把通信线的控制权交给从机
for(i=0;i<8;i++)
{
I2C_SCL=1;
if(I2C_SDA){Byte|=(0x80>>i);} //每次循环,都将读取I2C_SDA的数据并发送给Byte
I2C_SCL=0;
}
return Byte;
}
/**
* @brief I2C发送应答
* @param AckBit 应答位,0为应答,1为非应答
* @retval 无
*/
void I2C_SendAck(unsigned char AckBit)
{
I2C_SDA=AckBit;
I2C_SCL=1;
I2C_SCL=0;
}
/**
* @brief I2C接收应答位
* @param 无
* @retval 接收到的应答位,0为应答,1为非应答
*/
unsigned char I2C_ReceiveAck(void)
{
unsigned char AckBit;
I2C_SDA=1;
I2C_SCL=1;
AckBit=I2C_SDA;
I2C_SCL=0;
return AckBit;
}
AT24C02数据存储
主函数
#include
#include "Timer0.h"
#include "Key.h"
#include "Shumaguan.h"
#include "AT24C02.h"
#include "I2C.h"
#include "Delay.h"
unsigned char KeyNum;
unsigned char Min,Sec,MiniSec;
unsigned char RunFlag;
void main()
{
Timer0_Init();
while(1)
{
KeyNum=Key();
if(KeyNum==1) //按键1时,使秒表转动或停止
{
RunFlag=!RunFlag;
}
if(KeyNum==2) //按键2时,使秒表清零
{
Min=0,Sec=0,MiniSec=0;
}
if(KeyNum==3) //按键3时,使秒表的数值存入AT24C02
{
AT24C02_WriteByte(0,Min);
Delay(5);
AT24C02_WriteByte(1,Sec);
Delay(5);
AT24C02_WriteByte(2,MiniSec);
Delay(5);
}
if(KeyNum==4) //按键4时,将AT24C02中的数据读取到秒表
{
Min=AT24C02_ReadByte(0);
Sec=AT24C02_ReadByte(1);
MiniSec=AT24C02_ReadByte(2);
}
Shumaguan_SetBuf(1,Min/10); //分的十位
Shumaguan_SetBuf(2,Min%10); //分的个位
Shumaguan_SetBuf(3,11); //-
Shumaguan_SetBuf(4,Sec/10);
Shumaguan_SetBuf(5,Sec%10);
Shumaguan_SetBuf(6,11);
Shumaguan_SetBuf(7,MiniSec/10);
Shumaguan_SetBuf(8,MiniSec%10);
}
}
void Sec_Loop() //每隔10ms,调用1次函数
{
if(RunFlag) //RunFlag不为0时,秒表才转动
{
MiniSec++;
if(MiniSec>=100)
{
MiniSec=0;
Sec++;
if(Sec>=60)
{
Sec=0;
Min++;
if(Min>=60)
{
Min=0;
}
}
}
}
}
void Timer0_Routine() interrupt 1 //定时器T0的中断函数,当T0计数1ms溢出,就会跳到中断函数
{
static unsigned int T0Count1,T0Count2,T0Count3;
TL0 = 0x18; //每次溢出都要重新赋初值
TH0 = 0xFC;
T0Count1++;
if(T0Count1>=20) //定时器溢出20次时,也就是每20毫秒会执行下面功能
{
T0Count1=0;
Key_Loop(); //每隔20ms,检测一次按键值
}
T0Count2++;
if(T0Count2>=2) //每隔2ms,扫描一次数码管
{
T0Count2=0;
Shumaguan_Loop();
}
T0Count3++;
if(T0Count3>=10) //每隔10ms,MiniSec加1,100个MiniSec=1s
{
T0Count3=0;
Sec_Loop();
}
}
Key模块
#include
#include "Delay.h"
unsigned char Key_KeyNumber;
/**
* @brief 获取当前按键的状态,无消抖及松手检测
* @param 无
* @retval 按下按键的键码,范围:0,1~4,0表示无按键按下
*/
unsigned char Key_GetState()
{
unsigned char KeyNumber=0;
if(P3_1==0){KeyNumber=1;}
if(P3_0==0){KeyNumber=2;}
if(P3_2==0){KeyNumber=3;}
if(P3_3==0){KeyNumber=4;}
return KeyNumber;
}
/**
* @brief 按键驱动函数,在中断中调用
* @param 无
* @retval 无
*/
void Key_Loop(void) //定时器扫描按键
{
static unsigned char NowState,LastState;
LastState=NowState;
NowState=Key_GetState(); //每隔20ms获取一次按键状态
if(LastState==1 && NowState==0) //上个状态是按键按下,现在状态是没有按键按下(松手)
{ //两者同时满足时,使Key_KeyNumber为按键值
Key_KeyNumber=1;
}
if(LastState==2 && NowState==0)
{
Key_KeyNumber=2;
}if(LastState==3 && NowState==0)
{
Key_KeyNumber=3;
}if(LastState==4 && NowState==0)
{
Key_KeyNumber=4;
}
}
/**
* @brief 获取按键键码
* @param 无
* @retval 按下按键的键码,范围:1~4,0表示无按键按下
*/
unsigned char Key() //调用一次函数,将Key_KeyNumber值从这个函数返回,并且清零
{
unsigned char Temp=0;
Temp=Key_KeyNumber;
Key_KeyNumber=0;
return Temp;
}
Shumaguan模块
#include
#include "Delay.h"
//数码管显示缓存区
unsigned char Shumaguan_Buf[9]={0,10,10,10,10,10,10,10,10};
//数码管段码表
unsigned char ShumaguanTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x00,0x40};
/**
* @brief 设置显示缓存区
* @param Location 要设置的位置,范围:1~8
* @param Number 要设置的数字,范围:段码表索引范围
* @retval 无
*/
void Shumaguan_SetBuf(unsigned char Location,Number)
{
Shumaguan_Buf[Location]=Number;
}
/**
* @brief 数码管扫描显示
* @param Location 要显示的位置,范围:1~8
* @param Number 要显示的数字,范围:段码表索引范围
* @retval 无
*/
void Shumaguan_Scan(unsigned char Location,Number) //数码管显示函数,Location是第几个数码管,Number是显示的数字
{
P0=0x00; //如果放下面,打开段选后就清零了,不符合之前段选,延迟,清零的逻辑
switch(Location)
{ //通过switch函数判断输入的Location是1~8哪一个值
//再由后面的138译码器来选择对应的数码管
case 1:P2_4=1,P2_3=1,P2_2=1;break;
case 2:P2_4=1,P2_3=1,P2_2=0;break;
case 3:P2_4=1,P2_3=0,P2_2=1;break;
case 4:P2_4=1,P2_3=0,P2_2=0;break;
case 5:P2_4=0,P2_3=1,P2_2=1;break;
case 6:P2_4=0,P2_3=1,P2_2=0;break;
case 7:P2_4=0,P2_3=0,P2_2=1;break;
case 8:P2_4=0,P2_3=0,P2_2=0;break;
}
P0=ShumaguanTable[Number]; //Number输入0~9的任意数字,就会显示相应数字
//Delay(1);用中断函数每隔2ms扫描一次来实现Delay的功能
}
/**
* @brief 数码管驱动函数,在中断中调用
* @param 无
* @retval 无
*/
void Shumaguan_Loop() //每隔2ms,调用一次
{
static unsigned char i=1;
Shumaguan_Scan(i,Shumaguan_Buf[i]);
i++;
if(i>=9){i=1;}
}
秒表(定时器扫描按键数码管)