AT24C02与I2C总线(十一)

目录

一、存储器

 1、易失性存储器RAM

2、非易失性存储器ROM

3、存储器的简化模型

二、AT24C02

1、AT24C02介绍

2、引脚及应用电路

3、内部结构框图 

三、I2C总线

 1、I2C总线介绍

2、I2C电路规范

3、I2C时序结构

四、AT24C02数据存储

1、编写程序

2、实物展示

五、秒表(定时器扫描按键数码管)

1、编写程序

2、实物展示


一、存储器

AT24C02与I2C总线(十一)_第1张图片

 1、易失性存储器RAM

优点:存储速度特别快

缺点:掉电丢失数据

SRAM(静态RAM):内部存储结构为锁存器,IS触发器,JK触发器,D触发器,用电路来存储数据,它的存储速度是所有存储器中最快的一个,一般用于电脑的CPU,高速缓存。

51单片机中的数据存储器RAM就是SRAM,它的容量相对较小,成本较高。

DRAM(动态RAM):它用电容来存储数据,电容充上电后就显示高电平,放完电后就是低电平,利用电容的充放电,以达到存储数据的目的。

由于电容存在漏电现象,在存储数据后,很快就会漏电漏完,所以需要配一个扫描电路,每隔一段时间,就读取电容数据,来给电容补电,所以DRAM称为动态RAM,需要定时刷新。

动态RAM相比静态RAM,它的成本更低,容量更大,电脑中的内存条,手机中的运行内存都是动态RAM。

2、非易失性存储器ROM

优点:掉电不丢失数据

缺点: 存储速度较慢

当我们需要高速存储的时候,就把数据放RAM里面,程序运行时的数据都是存储在RAM里面。

当我们需要永久保存数据时,就把数据转存到ROM里面。

Mask ROM(掩膜ROM):最早的ROM,可以保证掉电不丢失,但是只能读,不能写。

PROM(可编程ROM):相比于Mask ROM,可以写入数据了,但是只能在出厂的时候写入一次数据,并且无法更改。

EPROM(可擦除可编程ROM):相比以上两个,既可编程数据,也可以擦除数据,但是擦除数据需要用紫外线照射30分钟。

E2PROM(电可擦除可编程ROM):在5V的低压电的情况下,几毫秒就能擦除数据。

Flash(闪存):用途十分广泛,包括单片机的程序存储器,U盘,内存卡,电脑的固态硬盘,手机的存储空间。

硬盘、软盘、光盘等:硬盘一般指电脑中的机械硬盘,靠磁介质来存储数据。

软盘是电脑早期用来存储数据的,软盘一般容量也很小。

光盘是用光信号来存储数据的。

3、存储器的简化模型

AT24C02与I2C总线(十一)_第2张图片

 在存储器内部中,实际上都是电路的一个网状结构,横向的线称为地址总线,纵向的线称为数据总线,这些横纵交错的节点一般默认是不连接的。

存储原理:当我们选择地址总线的第一行时,给第一行加上一个高电平1,剩下的地址总线暂时不接,然后接上前面三个节点,剩下的都不连,然后由第一根地址总线经过第一个节点读取数据总线为1,第二,第三个节点也都是1,后面由于节点没有连接,线都是悬空状态,因此都为0,这样在第一行地址总线下,就存储着数据1110 0000。第二行以及后面的地址总线以此类推。

Mask ROM:一开始节点是自动断开的,如果想要节点短路,就两根线之间接一个二极管,这样能防止其他电流干扰。

PROM:一开始使电路断开,就接两个二极管,没有电流通过,其中蓝色的比较特殊,容易被电流击穿;当我们要使电路短路,有电流通过时,就给电路接一个高电压,就会变成右边电路的一个状况。

这就是为什么有时候给单片机下载程序时,也叫烧入程序,早期的时候就是都用PROM这种存储方式,就需要把特殊的二极管给烧毁。

而EPROM被紫外线照射一段时间就会擦除程序,就是因为一些特殊二极管被烧毁后,被紫外线照射一段时间又会复原,因此又重新恢复了存储的功能。

二、AT24C02

1、AT24C02介绍

AT24C02是一种可以实现掉电不丢失的存储器,可用于保护单片机运行时想要永久保存的数据信息。

存储介质:E2PROM

通讯接口:I2C总线(2是指平方的意思,因此念:I方C总线)

容量:256字节

AT24C02与I2C总线(十一)_第3张图片

2、引脚及应用电路

AT24C02与I2C总线(十一)_第4张图片

VSS=GND        VDD=VCC        A0、A1、A2=E0、E1、E2

AT24C02与I2C总线(十一)_第5张图片

3、内部结构框图 

AT24C02与I2C总线(十一)_第6张图片

三、I2C总线

 1、I2C总线介绍

I2C总线(Inter IC BUS)是由菲利普公司开发的一种通用数据总线。

两根通信线:SCL(Serial Clock)、SDA(Serial Data)

是一种同步、半双工、带数据应答的数据总线。

同步:有单独的时钟线;

半双工:只有一根线进行来回通信,所以通信只能分时复用一根线;

带数据应答: 发送完一个字节数据后,要求接收数据方给一个应答。

通用的I2C总线,可以使各种设备的通信标准统一,对于厂家来说,使用成熟的方案可以缩短芯片设计周期、提高稳定性,对于应用者来说,使用通用的通信协议可以避免学习各种各样的自定义协议,降低了学习和应用的难度。

2、I2C电路规范

AT24C02与I2C总线(十一)_第7张图片

所有I2C设备的SCL连在一起,SDA连在一起。

SCL和SDA各外加一个上拉电阻,阻值一般为4.7KΩ左右;

设备的SCL和SDA均要配置成开漏输出模式。

引脚的弱上拉模式:当输入为0时,开关闭合,输出直接与GND连接;

当输入为1时,开关断开,高电平经过电阻,到输出,所以高电平没有低电平驱动能力强。

AT24C02与I2C总线(十一)_第8张图片

引脚的开漏输出模式:当输入为0时,开关闭合,输出直接与GND连接;

当输入为1时,开关断开,引脚呈浮空状态,实际上就是一种断开的状态,引脚什么都没有接,电平是不稳定的,极易受到外界干扰。

AT24C02与I2C总线(十一)_第9张图片

开漏输出和上拉电阻的共同作用实现了“线与”的功能,此设计主要是为了解决多机通信互相干扰的问题。

在开漏输出模式,如果CPU要对被控IC1进行通信,只需对其他连接设备引脚都输入1,相当于断开状态,这样就能使其他设备无法影响CPU和被控IC1的通信。

又因为开漏输出模式下,无法传输1的数据,因此在SCL和SDA外部各添加一个上拉电阻。

当CPU想发0时,就拉到低电平,当CPU想发送1时,就松手,不拉到低电平,就会被外部的电阻,自动拉到高电平,形成弱上拉模式,这样就能发送数据1。

(这里不理解没关系,主要看懂后面时序结构就能写代码)

3、I2C时序结构

起始条件:SCL高电平期间,SDA从高电平切换到低电平

终止条件:SCL高电平期间,SDA从低电平切换到高电平

AT24C02与I2C总线(十一)_第10张图片

发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位在前),然后拉高SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许又数据变化,依次循环上述过程8次,即可发送一个字节。

(主机:单片机,从机:AT24C02存储器或其他要传输数据的模块)

AT24C02与I2C总线(十一)_第11张图片

 SDA这里两条线是指两种可能,SDA在SCL低电平时才会变化,因此在SCL第一个上拉高电平之前,SDA会变化成要发送的数据0或1,然后在SCL上拉成高电平后,从机会读取SDA发送的这个数据,读取完成后,SCL又变成低电平,SDA再变化成相应要发送的数据,然后SCL再上拉成高电平,再读取数据,以此类推,循环8次后,即可发送一个字节。

接收一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位在前),然后拉高SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)。

AT24C02与I2C总线(十一)_第12张图片

使SDA置1相当于释放SDA,释放时,主机是完全不干预通信线的,即把通信线的控制权交给从机,从机拿到控制权后,会把要发送的字节发送给主机。(图中紫线表示从机控制总线的部分)

发送应答:在接收完一个字节之后,主机在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答;

接收应答:在发送完一个字节之后,主机在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接受之前,需要释放SDA)。

AT24C02与I2C总线(十一)_第13张图片

4、数据帧

AT24C02与I2C总线(十一)_第14张图片

AT24C02与I2C总线(十一)_第15张图片

读数据帧中,读数据黄色块后,需要增加一个应答信号位。

写数据(W=0)=发送数据,读数据(R=1)=接收数据

AT24C02的固定地址为1010,可配置地址开发板上为(A0、A1、A2)000,所以

设备地址+W为0xA0,

设备地址+R为0xA1。

四、AT24C02数据存储

1、编写程序

主函数

#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;
}

2、实物展示

AT24C02数据存储


五、秒表(定时器扫描按键数码管)

1、编写程序

主函数

#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;}
}
	 

2、实物展示

秒表(定时器扫描按键数码管)

你可能感兴趣的:(51单片机学习,单片机,嵌入式硬件,51单片机)