文章目录
- 存储器的介绍
- AT24C02
- I2C总线
- I2C时序结构
- AT24C02数据帧
- AT24C02数据存储实例
存储器的介绍
存储器是计算机系统中的一种重要设备,用于存储程序和数据,它可以通过电子、磁性介质等技术来记录和保持数据。在这里,主要介绍的是随机存储器(RAM)和只读存储器(ROM)。
随机存储器(Random Access Memory,RAM)是一种可以读取和写入数据的存储器。 它的特点是可以随机访问其中的任意位置,并且读写速度较快。RAM通常由电子芯片制成,用于临时存储计算机运行时所需的程序和数据。
RAM可以分为静态随机存储器(Static RAM,SRAM)和动态随机存储器(Dynamic RAM,DRAM)两种类型。
静态随机存储器(SRAM):SRAM使用了触发器电路来存储数据,每个存储单元由多个触发器组成,具有较高的稳定性和快速的读写速度。由于使用了更多的电子元件,SRAM的密度较低,成本相对较高。它常用于高速缓存(Cache)等需要快速访问的场景。
动态随机存储器(DRAM):DRAM使用了电容器和开关电路来存储数据,每个存储单元由一个电容器和一个访问开关组成。由于电容器的电荷会随时间衰减,DRAM需要周期性地进行刷新操作来保持数据的稳定。DRAM具有较高的存储密度和较低的成本,但相对而言读写速度较慢。它常用于主存储器(内存)等需要较大容量的场景。
只读存储器(Read-Only Memory,ROM)是一种只能读取数据而无法写入或修改数据的存储器。ROM中的数据在制造时被写入,通常用于存储固化的程序代码、系统设置、固件等不需要频繁修改的数据。ROM的数据在断电后依然可以保持,具有非易失性。
ROM根据存储数据的方式可以分为多种类型,如只读存储器(ROM)、可编程只读存储器(PROM)、可擦除可编程只读存储器(EPROM)和电可擦除可编程只读存储器(EEPROM)等。这些ROM的特点是在制造或编程后,其存储的数据无法修改或只能经过特定方式修改。对于我们这款单片机来说,AT24C02就是E2PROM,电可擦除可编程ROM,可以通过编程来进行存储数据,数据根据具体的地址存储;断电之后,他还会保存在地址中,当我们需要读取时,可以通过相对应的地址取出数据;
AT24C02
AT24C02是一种2K位(256字节)的串行EEPROM(Electrically Erasable Programmable Read-Only Memory),常用于51单片机系统中的数据存储。
AT24C02采用I2C总线协议进行通信,通过两根线路(SCL和SDA)与单片机进行连接。它具有8位的设备地址,可通过引脚A0、A1和A2进行编程设置,从而允许多个AT24C02共存在同一I2C总线上。
对于AT24C02,在读写速度上支持400kHz标准模式,数据保存能力可达10年之久,而且有写入保护,可以防止误操作;
在单片机系统中,可以使用AT24C02来存储各种数据,如配置参数、校准数据、历史记录等。单片机可以使用相应的I2C库函数来向AT24C02写入或读取数据,以实现对其存储空间的读写操作。
需要注意的是,AT24C02是一种非易失性存储器,即使在断电情况下也可以保持存储的数据。因此,它适用于需要长期保存数据的应用场景。
上图时AT24C02的内部结构框图,红色部分为它的存储位置;
I2C总线
I2C,全称为Inter-Integrated Circuit,是一种串行通信协议,用于在电子设备之间进行数字数据传输。它由飞利浦半导体(现在的NXP半导体)公司在1980年代开发,并于1982年发布。I2C采用了两根导线(一根用于传输数据,另一根用于传输时钟信号)来连接设备,这使得多个设备可以通过同一组导线进行通信。
I2C协议有两个重要的角色:主设备(Master)和从设备(Slave)。主设备负责控制通信的序列和时钟信号的生成,而从设备则被动响应主设备的指令。
I2C支持多主设备和多从设备的连接,每个设备都有一个唯一的地址标识。主设备可以向从设备发送数据或者接收从设备的数据。在传输数据时,数据被分为多个字节,每个字节都会被从设备确认。
在51单片机中,AT24C02通过I2C总线与单片机进行数据通信;
通过I2C总线,单片机可以发送读取或写入命令给AT24C02,然后AT24C02会响应相应的操作。例如,单片机可以向AT24C02发送写入命令,并指定要写入的地址和数据,AT24C02会将数据写入指定地址的存储单元。而当单片机需要读取AT24C02中的数据时,它可以向AT24C02发送读取命令和要读取的地址,然后AT24C02会返回相应的数据给单片机。
I2C时序结构
I2C总线的时序结构一般分为6部分:起始条件、终止条件、发送数据、接收数据、接收应答和发送应答;
起始条件:主设备通过SCL(串行时钟线)保持高电平的同时,将SDA(串行数据线)由高电平转换为低电平,表示要启动一次通信;
代码:
void I2C_Start()
{
I2C_SDA=1;//表示SCL高电平之前SDA就是高电平了
I2C_SCL=1;//SCL高电平期间
I2C_SDA=0;//SDA降为低电平
I2C_SCL=0;//SCL回到低电平
}
发送一个字节:SCL低电平期间,主设备将数据位依次放到SDA线上(高位在前),然后拉高SCL,在SCL上升沿时从设备将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节;
代码:
void I2C_SendByte(unsigned char byte)
{
unsigned char i;
for(i=0;i<8;i++)
{
I2C_SDA=byte&(0x80>>i);//先将数据放到SDA上
I2C_SCL=1; //SCL为上升沿时,从设备读取数据
I2C_SCL=0;
}
}
接收一个字节:SCL低电平期间,从设备将数据位依次放到SDA线上(高位在前),然后拉高SCL,主设备将在SCL上升沿期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)
代码:
unsigned char I2C_ReceiveByte()
{
unsigned char i,byte=0x00;
I2C_SDA=1; //释放SDA,规定置于高电平
for(i=0;i<8;i++)
{
//在SCL上升沿期间,主设备读取SDA的数据,
I2C_SCL=1;
if(I2C_SDA){byte|=(0x80>>i);
//通过判断SDA位的数据是否为1,进行位赋值
//为1执行条件内容,将位变为1,为0则不变,仍为0;
I2C_SCL=0;
}
return byte;
}
发送应答:在接收完一个字节之后,主机在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答 ;
代码:
void I2C_SendAck(unsigned char AckBit)
{
I2C_SDA=AckBit;
//SDA发送应答,应答或者不应答
I2C_SCL=1;
//在SCL上升沿期间,从设备接收主设备发送应答
I2C_SCL=0;
}
接收应答:在发送完一个字节之后,主机在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA);主设备发送一个高电平的SDA并保持SCL线为高电平,使得从设备可以拉低SDA线来应答。如果接收方正确收到数据,它会发一个低电平的SDA信号应答;
代码:
unsigned char I2C_ReceiveAck()
{
unsigned char AckBit;
I2C_SDA=1; //释放SDA
I2C_SCL=1; //在SCL高电平期间
AckBit=I2C_SDA; //接收SDA的应答数据
I2C_SCL=0;
return AckBit;
}
终止条件:将SCL线保持高电平的同时,将SDA线由低电平转换为高电平,表示通信结束;
代码:
void I2C_Stop()
{
I2C_SDA=0;// 在SCL高电平之前SDA就为低电平
I2C_SCL=1; //SCL处于高电平期间
I2C_SDA=1;
}
AT24C02数据帧
数据帧是在通信中用于传输数据的基本单元。
对于AT24C02,数据帧包括一个地址字节和一个或多个数据字节;
上图为AT24C02数据帧的格式:
- 起始位(Start Bit):数据传输开始时,为低电平。
- 7位地址(Address):表示要访问的内存地址。AT24C02共有256个地址,即0x00到0xFF。
- R/W位(Read/Write Bit):用于选择读(高电平)或写(低电平)操作。
- 数据字节(Data Byte):在写操作中,数据字节表示要写入到指定地址的数据。在读操作中,数据字节表示从指定地址读取到的数据。
- 应答位(Acknowledge Bit):用于发送方向接收方确认数据传输的成功。
而这些数据帧的格式是遵循I2C总线的通信协议的;所以我们只需要运用I2O的时序结构,将它们对应进AT24C02的数据帧即可;
对于读操作,数据帧的格式如下:
起始位 + 7位地址 + R/W位(设置为高电平)+ 应答位 + 存储的地址标识 + 应答位 + 数据字节 + 应答位 + 起始位 +读取数据+ 应答位 + 停止位
对于写操作,数据帧的格式如下:
起始位 + 7位地址 + R/W位(设置为低电平)+ 应答位 + 存储的地址标识 + 应答位 + 数据字节 + 应答位 + 停止位
代码(利用到I2C总线时序写的代码):
写入字节:
#define AT24C02_ADDRESS 0xA0 //AT24C02的写操作地址
void AT24C02_WriteByte(unsigned char WordAddress,Data)
{
I2C_Start(); //起始条件
I2C_SendByte(AT24C02_ADDRESS); //主设备发送AT24C02的地址
I2C_ReceiveAck(); //从设备接收应答(确认地址)
I2C_SendByte(WordAddress); // 主设备向从设备发送地址
I2C_ReceiveAck(); //从设备接收应答
I2C_SendByte(Data); //主设备向从设备发送数据
I2C_ReceiveAck(); //从设备接收应答
I2C_Stop(); //终止条件
}
读取字节:
unsigned char AT24C02_ReadByte(unsigned char WordAddress)
{
unsigned char Data;
I2C_Start(); //起始条件
I2C_SendByte(AT24C02_ADDRESS); //主设备发送AT24C02的地址
I2C_ReceiveAck();//从设备接收应答(确认地址)
I2C_SendByte(WordAddress);// 主设备向从设备发送要读取字节的地址
I2C_ReceiveAck();//从设备接收应答
//以上操作完成了主设备向从设备要读取字节的地址的操作,是主设备
//告诉从设备的地址
//下面操作是从设备向主设备发送对应的字节操作,主设备从写入变
//为读取,所以要重新开始定起始条件
I2C_Start(); //起始条件
I2C_SendByte(AT24C02_ADDRESS|0x01); //主设备发送AT24C02的读取地址
I2C_ReceiveAck(); //从设备接收应答(确认地址
Data=I2C_ReceiveByte();//读取从设备对应地址的数据字节
I2C_SendAck(1); //主设备发送应答(地址对应正确发送回应给从设备)
I2C_Stop(); //终止条件
return Data;
}
AT24C02数据存储实例
写一个能通过独立按键让数字逐渐增加或者减少的程序,并且让这个程序能记住某一个数字,断电或者置0的时候可以恢复到这个数字;
代码:
Delay.h
#ifndef __DELAY_H__
#define __DELAY_H__
//获取一个毫秒为单位的延迟函数,填入对应数字表示延迟多少毫秒
void Delay(unsigned int xms);
#endif
Delay.c
void Delay(unsigned int xms)
{
unsigned char i, j;
while(xms--)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
}
Key.h
#ifndef __KEY_H__
#define __KEY_H__
unsigned char Key();
#endif
Key.c
#include <REGX52.H>
#include "Delay.h"
/**
* @brief 获取独立按键键码
* @param 无
* @retval 按下按键的键码,范围:0~4,无按键按下时返回值为0
*/
unsigned char Key()
{
unsigned char KeyNumber=0;
if(P3_1==0){Delay(20);while(P3_1==0);Delay(20);KeyNumber=1;}
if(P3_0==0){Delay(20);while(P3_0==0);Delay(20);KeyNumber=2;}
if(P3_2==0){Delay(20);while(P3_2==0);Delay(20);KeyNumber=3;}
if(P3_3==0){Delay(20);while(P3_3==0);Delay(20);KeyNumber=4;}
return KeyNumber;
}
LCD1602.h
#ifndef __LCD1602_H__
#define __LCD1602_H__
//用户调用函数:
void LCD_Init();
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char);
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String);
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length);
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
#endif
LCD1602.c
#include <REGX52.H>
//引脚配置:
sbit LCD_RS=P2^6;
sbit LCD_RW=P2^5;
sbit LCD_EN=P2^7;
#define LCD_DataPort P0
//函数定义:
/**
* @brief LCD1602延时函数,12MHz调用可延时1ms
* @param 无
* @retval 无
*/
void LCD_Delay()
{
unsigned char i, j;
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
/**
* @brief LCD1602写命令
* @param Command 要写入的命令
* @retval 无
*/
void LCD_WriteCommand(unsigned char Command)
{
LCD_RS=0;
LCD_RW=0;
LCD_DataPort=Command;
LCD_EN=1;
LCD_Delay();
LCD_EN=0;
LCD_Delay();
}
/**
* @brief LCD1602写数据
* @param Data 要写入的数据
* @retval 无
*/
void LCD_WriteData(unsigned char Data)
{
LCD_RS=1;
LCD_RW=0;
LCD_DataPort=Data;
LCD_EN=1;
LCD_Delay();
LCD_EN=0;
LCD_Delay();
}
/**
* @brief LCD1602设置光标位置
* @param Line 行位置,范围:1~2
* @param Column 列位置,范围:1~16
* @retval 无
*/
void LCD_SetCursor(unsigned char Line,unsigned char Column)
{
if(Line==1)
{
LCD_WriteCommand(0x80|(Column-1));
}
else if(Line==2)
{
LCD_WriteCommand(0x80|(Column-1+0x40));
}
}
/**
* @brief LCD1602初始化函数
* @param 无
* @retval 无
*/
void LCD_Init()
{
LCD_WriteCommand(0x38);//八位数据接口,两行显示,5*7点阵
LCD_WriteCommand(0x0c);//显示开,光标关,闪烁关
LCD_WriteCommand(0x06);//数据读写操作后,光标自动加一,画面不动
LCD_WriteCommand(0x01);//光标复位,清屏
}
/**
* @brief 在LCD1602指定位置上显示一个字符
* @param Line 行位置,范围:1~2
* @param Column 列位置,范围:1~16
* @param Char 要显示的字符
* @retval 无
*/
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char)
{
LCD_SetCursor(Line,Column);
LCD_WriteData(Char);
}
/**
* @brief 在LCD1602指定位置开始显示所给字符串
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param String 要显示的字符串
* @retval 无
*/
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=0;String[i]!='\0';i++)
{
LCD_WriteData(String[i]);
}
}
/**
* @brief 返回值=X的Y次方
*/
int LCD_Pow(int X,int Y)
{
unsigned char i;
int Result=1;
for(i=0;i<Y;i++)
{
Result*=X;
}
return Result;
}
/**
* @brief 在LCD1602指定位置开始显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~65535
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
LCD_WriteData(Number/LCD_Pow(10,i-1)%10+'0');
}
}
/**
* @brief 在LCD1602指定位置开始以有符号十进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:-32768~32767
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length)
{
unsigned char i;
unsigned int Number1;
LCD_SetCursor(Line,Column);
if(Number>=0)
{
LCD_WriteData('+');
Number1=Number;
}
else
{
LCD_WriteData('-');
Number1=-Number;
}
for(i=Length;i>0;i--)
{
LCD_WriteData(Number1/LCD_Pow(10,i-1)%10+'0');
}
}
/**
* @brief 在LCD1602指定位置开始以十六进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~0xFFFF
* @param Length 要显示数字的长度,范围:1~4
* @retval 无
*/
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i,SingleNumber;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
SingleNumber=Number/LCD_Pow(16,i-1)%16;
if(SingleNumber<10)
{
LCD_WriteData(SingleNumber+'0');
}
else
{
LCD_WriteData(SingleNumber-10+'A');
}
}
}
/**
* @brief 在LCD1602指定位置开始以二进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~1111 1111 1111 1111
* @param Length 要显示数字的长度,范围:1~16
* @retval 无
*/
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
LCD_WriteData(Number/LCD_Pow(2,i-1)%2+'0');
}
}
AT24C02.h
#ifndef __KEY_H__
#define __KEY_H__
void AT24C02_WriteByte(unsigned char WordAddress,Data);
unsigned char AT24C02_ReadByte(unsigned char WordAddress);
#endif
AT24C02.c
#include <REGX52.H>
#include"I2C.h"
#define AT24C02_ADDRESS 0xA0
void AT24C02_WriteByte(unsigned char WordAddress,Data)
{
I2C_Start();
I2C_SendByte(AT24C02_ADDRESS);
I2C_ReceiveAck();
I2C_SendByte(WordAddress);
I2C_ReceiveAck();
I2C_SendByte(Data);
I2C_ReceiveAck();
I2C_Stop();
}
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();
I2C_SendAck(1);
I2C_Stop();
return Data;
}
I2C.h
#ifndef __KEY_H__
#define __KEY_H__
void I2C_Start();//¶ÔI2C³õʼ»¯
void I2C_Stop();//¶ÔI2CÄ©¶Ë
void I2C_SendByte(unsigned char byte);//I2CÖ÷»ú·¢ËÍÒ»¸ö×Ö½Ú
unsigned char I2C_ReceiveByte(); //½ÓÊÕÒ»¸ö×Ö½Ú
void I2C_SendAck(unsigned char AckBit); //·¢ËÍÓ¦´ð
unsigned char I2C_ReceiveAck(); //½ÓÊÕÓ¦´ð
#endif
I2C.c
#include <REGX52.H>
sbit I2C_SCL=P2^1;
sbit I2C_SDA=P2^0;
void I2C_Start()
{
I2C_SDA=1;
I2C_SCL=1;
I2C_SDA=0;
I2C_SCL=0;
}
void I2C_Stop()
{
I2C_SDA=0;
I2C_SCL=1;
I2C_SDA=1;
}
void I2C_SendByte(unsigned char byte)
{
unsigned char i;
for(i=0;i<8;i++)
{
I2C_SDA=byte&(0x80>>i);
I2C_SCL=1;
I2C_SCL=0;
}
}
unsigned char I2C_ReceiveByte()
{
unsigned char i,byte=0x00;
I2C_SDA=1;
for(i=0;i<8;i++)
{
I2C_SCL=1;
if(I2C_SDA){byte|=(0x80>>i);}
I2C_SCL=0;
}
return byte;
}
void I2C_SendAck(unsigned char AckBit)
{
I2C_SDA=AckBit;
I2C_SCL=1;
I2C_SCL=0;
}
unsigned char I2C_ReceiveAck()
{
unsigned char AckBit;
I2C_SDA=1;
I2C_SCL=1;
AckBit=I2C_SDA;
I2C_SCL=0;
return AckBit;
}
main.c
#include <REGX52.H>
#include"AT24C02.h"
#include"I2C.h"
#include"LCD1602.h"
#include"Delay.h"
unsigned char KeyNum;
unsigned short 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);
Delay(5);
AT24C02_WriteByte(1,Num/256);
Delay(5);
LCD_ShowString(2,1,"Write OK");
Delay(1000);
LCD_ShowString(2,1," ");
}
if(KeyNum==4) //K4按键,从AT24C02读取数据
{
Num=AT24C02_ReadByte(0);
Num|=AT24C02_ReadByte(1)<<8;
LCD_ShowNum(1,1,Num,5);
LCD_ShowString(2,1,"Read OK ");
Delay(1000);
LCD_ShowString(2,1," ");
}
}
}
对于无符号的short类型数字,范围是0~65535,对应的有两个字节,所以写入数据要分别两部分,前8位和后8位,存储的地址可由自己选取,但在写入多个数据之间要延迟5ms,否则将会报错;