江协科技 51单片机 AT24C02(I2C总线) 学习笔记——略带拓展

1.存储器介绍

所谓存储器,是指用来存储指令或者数据的部件。发展至今,计算机内部往往包含多种不同容量、不同用途的存储器。

1.1 RAM(易失性存储器)

RAM,(Random Access Memory ),它最大的特点是只能临时性地存储数据或指令,一旦计算机关闭或者意外断电,RAM 存储的数据将全部丢失。存储较快,程序运行时数据会存储在RAM中

SRAM 

SRAM 全称 Static Random-Access Memory,中文译为静态随机存取存储器。

SRAM 主要由晶体管构成,SRAM 中的每 1 个比特位都通常要用 6 个 CMOS 晶体管。

CPU读写SRAM速度快,51中的特殊功能寄存器、定义的数据、计算结果等都是SRAM。容量小成本高。

DRAM

DRAM,全程Dynamic Random Access Memory,中文译为动态随机存取存储器。

DRAM本质是一个集成电路,在电路板上,每个晶体管都和一个电容器对齐摆放,构成 1 个比特位,其中电容器充满电表示存储 1,无电表示存储 0;晶体管则充当开关的角色,它允许电路板读取或者修改当前电容器的状态。

DRAM 中会存在掉电的现象,导致相连电容器中的电量减少,存储的数据丢失。所以,每隔一段时间就要给他充电来刷新因为掉电丢失的数据,因此被称为动态。

CPU读写DRAM的速率较低,耗电高。

1.2 ROM(非易失性存储器)

ROM,Read-Only Memory 的缩写,中文可译为只读存储器。 

相比于RAM,ROM在断电时数据不会丢失 

Mask ROM(掩膜ROM) 

数据在制作时已经写入,后期不可以更改,很不方便。

PROM(可编程ROM) 

只可以写入一次数据,后期无法更改

EPROM(可擦除可编程ROM )

可以写入也可以修改,紫外线照射30min擦除。 

E2PROM (电可擦除可编程ROM ) 

 可以写入也可以修改,本节I2C就是此类,5V低压电可擦除。

Flash(闪存) 

功能强大,如单片机的程序存储,固态硬盘等。 

1.3 存储器简化模型

 存储时选择地址线,将其置1,其他置0,用数据线连接,形成网格交点。例如:取第一条地址线,连接第1、2、3个交点,那么第一行存储的数据就是11100000的一个字节。(简化模型)

MASK ROM的存储方法:当我们需要哪一位存储数据时就在这个节点上接一个二极管(二极管有两个电极,正极,又叫阳极;负极,又叫阴极,给二极管两极间加上正向电压时,二极管导通, 加上反向电压时,二极管截止。 二极管的导通和截止,则相当于开关的接通与断开 ) 那么数据就不会随意流向其它的位置。因此在厂家制造完成后它的数据就固定无法更改了。

PROM的存储方法:可以看到图中有两个二极管因此线路一定是断开的,但是蓝色的特殊二极管,可以被击穿,因此出厂后,高电压击穿是可以进行一次写入的,但也只能进行写一次,二极管被击穿是不可逆的。所以在写入数据的时候是需要加额外的高电压进行写入,因此到现在也有下载程序叫做烧录程序,烧毁击穿二极管。

2.AT24C02 

2.1 AT24C02介绍

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

存储介质:E2PROM

通讯接口:I2C总线

容量:256字节 

 

2.2 引脚及应用电路 

2.3 内部结构框图 

1.EEPROM:上面的存储器内部

2.X DEC:译码器,寻址

3.SERIAL MUX:数据输入输出端

4.Y DEC:译码器,帮助输出

5.DATA RECOVERY:擦除数据

6:DATA WORD ADDR/COUNTER:设置地址,里面有个寄存器是用来存储地址的,每写入和读出寄存器自动加一,读出不指定地址,默认拿出寄存器的地址

7、8、9:将I2C中的数据取出

3. I2C总线 

 3.1 I2C总线介绍

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


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


同步、半双工,带数据应答


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

IIC总线是一种多主机总线,连接在IIC总线上的器件分为主机和从机主机有权发起和结束一次通信,而从机只能被主机呼叫:当总线上有多个主机同时启用总线时,IIC也具备冲空检测和仲裁的功能来防止错误产生;每个连接到IIC总线上的器件都有一个唯一的地址(7bit),且每个器件都可以作为主机也可以作为从机(同一时刻只能有一个主机),总线上的器件增加和删除不影响其他器件正常工作;IIC总线在通信时总线上发送数据的器件为发送器,接收数据的器件为接收器:

3.2 I2C电路规范 

1.所有I2C设备的SCL连在一起,SDA连在一起。总线结构,在线上外接各种设备。如图中所示。

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

这里需要拓展一下上拉的知识,看不懂也没事,不影响我们编程。

这张图就是我们单片机IO口的输出线路,可以将控制器理解为一个电源连上一个无限大的电阻,Q1是一个控制开关的MOS口,当Q1打开时相当于此处不分压,所以输出低电平;当Q1关闭时,Q1处的电阻无限大,外部的VCC和控制器内部形成一个串联电路电阻相比于Q1很小,所以输出高电平。

我们可能在视频中常听到的强/弱上拉就是改变上拉电阻的阻值来达到控制输出电流电压大小的情况。

开漏输出模式就是将此图中的VCC和上拉电阻去掉,那么在Q1关闭时电路相当于断开,输出的电平是不稳定,非常容易被影响的。

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

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

现在假设我们需要第一个设备工作,我们让其它三个设备MOS关闭,因为是开漏输出模式,所以其内部相当于断开,导致其不会工作。而工作的设备我们可以MOS打开,让其和其它设备区分开。

(我的理解是CPU工作为1,工作设备要与CPU连接就置低电平0,不工作设备置1无电压差无法通信)

这是一个设备的内部电路图:

3.3 I2C时序结构

 接下来我们来介绍一下I2C六个时序结构,只要集齐了这六个时序结构,就可以召唤神龙(bushi)了! 

起始条件    终止条件

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

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

 

发送

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

接收 

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

发送应答    接收应答

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

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

3.4 I2C总线通信过程

■ 1.主机发送起始信号启用总线
■ 2. 主机发送一个字节数据指明从机地址和后续字节的传送方向

■ 3. 被寻址的从机发送应答信号回应主机
■ 4. 发送器发送一个字节数据
■ 5. 接收器发送应答信号回应发送器
■ … … (循环步骤4、5)
■ n. 通信完成后主机发送停止信号释放总线

3.5 I2C数据帧 

发送

接收

先发送再接收数据帧

 此处类似老师上课让学生回答问题的过程。

前两行开始,老师确定哪个学生,向他提问,学生收到答复,老师提出问题···

后两行老师确定这个学生,让他回答,学生收到回复,学生解答问题,老师收到回复···,结束

字节写:在WORD ADDRESS处写入数据DATA

随机读:读出在WORD ADDRESS处的数据DATA 

AT24C02的固定地址为1010,可配置地址本开发板上为000     所以SLAVE ADDRESS+W为0xA0,SLAVE ADDRESS+R为0xA1 

4.程序源码

整体思路

4.1 AT24C02的数据存储

4.1.1 效果

4.1.2 思路 

这个项目主要时让我们了解熟悉怎么编写AT24C02和I2C的功能代码,存储数字在LCD上显示。

4.1.3 代码

main.c

#include<REGX52.h>
#include "LCD1602.h"
#include "Delay.h"
#include "Key.h"
#include "I2C.h"
#include "AT24C02.h"

unsigned char KeyNum;
unsigned int Num;

void main()
{
	LCD_Init();
	LCD_ShowNum(1,1,Num,5);
	
	while(1)
	{
		KeyNum=Key();  //按下按键
		if(KeyNum==1)  
		{
			Num++;
			LCD_ShowNum(1,1,Num,5);
		}
		if(KeyNum==2)
		{
			Num--;
			LCD_ShowNum(1,1,Num,5);
		}
		if(KeyNum==3)
		{
			AT24C02_WriteByte(0,Num%256);  //将Num后八位写入0地址位(Num是int类型有16位)
			Delay(5);  //延迟5ms,否则速度过快单片机来不及写入
			AT24C02_WriteByte(1,Num/256);  //将Num前八位写入1地址位
			Delay(5);
			LCD_ShowString(2,1,"Write OK");
			//Dealy(1000);
			//LCD_ShowString(2,1,"        ");
		}
		if(KeyNum==4)
		{
			Num=AT24C02_ReadByte(0);  //将0位置存入的数据读给Num后八位
			Num|=AT24C02_ReadByte(1)<<8;  //将1位置存入的数据读给Num前八位
			LCD_ShowNum(1,1,Num,5);
			LCD_ShowString(2,1,"Read OK");
			//Dealy(1000);
			//LCD_ShowString(2,1,"       ");
		}
	}
}

I2C.h

#ifndef __I2C_H__
#define __I2C_H__

void I2C_Start(void);
void I2C_Stop(void);
void I2C_SendByte(unsigned char Byte);
unsigned char I2C_ReceiveByte(void);
void I2C_SendAck(unsigned char AckBit);
unsigned char I2C_ReceiveAck(void);

#endif

 I2C.c

#include <REGX52.H>

sbit I2C_SCL=P2^1;
sbit I2C_SDA=P2^0;

/**
   * @brief   开始
   * @param		无
   * @retval	无
   */
void I2C_Start(void)  //开始
{
	I2C_SDA=1;  //确保SDA是在高电平
	I2C_SCL=1;  //确保SDA是在高电平
	I2C_SDA=0;  //先拉低SDA
	I2C_SCL=0;	//再拉低SCL
}

/**
   * @brief   停止
   * @param		无
   * @retval	无
   */
void I2C_Stop(void)  //停止
{
	I2C_SDA=0;  //确保SDA是在低电平,SCL在各个状态结束时都是低电平不用置0
	I2C_SCL=1;  //先拉低SCL
	I2C_SDA=1;	//再拉低SDA
}

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(void)  //接收一个字节
{
	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;  //1位应答,0为未应答
	I2C_SCL=1;
	I2C_SCL=0;
}
	
/**
   * @brief   接收应答
   * @param		无
   * @retval 	接收到的应答位
   */
unsigned char I2C_ReceiveAck(void)  //接收应答
{
	unsigned char AckBit;
	I2C_SDA=1;
	I2C_SCL=1;
	AckBit=I2C_SDA;  //应答为1,为应答为0
	I2C_SCL=0;
	return AckBit;
}

AT24C02.h

#ifndef __AT24C02_H__
#define __AT24C02_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  

/**
   * @brief   	AT24C02写入一个字节
   * @param	  	WordAddress 要写入数据的地址 ,Data 要写入的数据
   * @retval 	无
   */
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();
}

/**
   * @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();
	I2C_SendAck(1);
	I2C_Stop();
	return Data;
}

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');
	}
}

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

4.2 AT24C02计时 

接下来,我们将会改进之前动态数码管的实现,使用定时器来扫描,然后实现一个具有记忆功能的秒表。

4.2.1 效果

4.2.2 思路(重要)

 我们本次的项目主要打算使用矩阵键盘和数码管来实现计时秒表。 

1.数码管:负责显示时间

2.矩阵键盘:相当于秒表上的按钮,对秒表进行操作

3.AT24C02:负责存储时间,下一次开机时能读取回上回的数据

这次我们打算改进矩阵键盘数码管的代码 ,因为Delay函数使用时会带来诸多不便,所以使用定时器来对这两个设备进行扫描。

我们在定时器中断函数中另外定义两个用于计时的局部变量Count2和Count3,定时器的中断周期设定为1ms,Count2、Count3在定时器的每次中断中自增。

Count2在自增二十次时,也就是20ms后被触发。执行扫描矩阵键盘判断有无按键按下,这里用到了两个变量now和last来分别表示现状态和前一个状态,那么可以通过比较前后两个状态来判断是否按下。

为什么是20ms:相当于取代了按键去抖的过程。

扫描状态有两种情况,

1:1->2->4

(1)now:2,last:1,两状态相同,无法判断

(2)now:4,last:2,从高电平变为了低电平,判断按下

2:1->3-4

(1)now:3,last:1,从高电平变为了低电平,判断按下

(2)now:4,last:3,前面已经判断按下,无需考虑

 

Count2在自增二次时,也就是2ms后被触发。我们定义了分钟:Min,秒:Sec,分秒:MiniSec,通过定时器自增;同时定义了Nixie_Buf[9]这个数组来代表要显示的时间,每次主函数中while循环一次将分钟、秒、分秒重新写入一回。那么每2ms会重新扫描更新一遍数码管。

上述这些操作主要包含在定时器中断函数,Nixie.c,Key.c中。

4.2.3 代码 

main.c

#include <REGX52.h>
#include "Nixie.h"
#include "AT24C02.h"
#include "Delay.h"
#include "I2C.h"
#include "Key.h"
#include "Timer0.h"

unsigned char KeyNum;
unsigned char Min,Sec,MiniSec;
unsigned char RunFlag;  //运行标志位

void main()
{
	Timer0_Init();
	while(1)
	{
		KeyNum=Key();
		
		if(KeyNum==1)  //松开第一个按键时,开始或暂停
		{
			RunFlag=!RunFlag;
		}
		
		if(KeyNum==2)  //松开第二个按键时,置0
		{
			Min=0;
			Sec=0;
			MiniSec=0;
		}
		
		if(KeyNum==3)  //松开第三个按键时,写入AT24C02
		{
			AT24C02_WriteByte(0,Min);
			Delay(5);  //延时5ms,否则单片机来不及反应
			AT24C02_WriteByte(1,Sec);
			Delay(5);  //延时5ms,否则单片机来不及反应
			AT24C02_WriteByte(0,MiniSec);
			Delay(5);  //延时5ms,否则单片机来不及反应
		}  
		
		if(KeyNum==4)  //松开第四个按键时,读取AT24C02
		{
			Min=AT24C02_ReadByte(0);
			Sec=AT24C02_ReadByte(1);
			MiniSec=AT24C02_ReadByte(2);
		}
		
		//设置数码管显示
		Nixie_SetBuf(1,Min/10);
		Nixie_SetBuf(2,Min%10);
		Nixie_SetBuf(3,11);
		Nixie_SetBuf(4,Sec/10);
		Nixie_SetBuf(5,Sec%10);
		Nixie_SetBuf(6,11);
		Nixie_SetBuf(7,MiniSec/10);
		Nixie_SetBuf(8,MiniSec%10);
	}
}

//时钟功能
void Sec_Loop()
{
	if(RunFlag)  
	{
		MiniSec++;
		if(MiniSec>=100)
		{
			MiniSec=0;
			Sec++;
			if(Sec>=60)
			{
				Sec=0;
				Min++;
				if(Min>=60)
				{
					Min=0;
				}
			}
		}
	}
}

//定时器中断函数
void Timer0_Routine() interrupt 1
{
	static unsigned int T0Count1,T0Count2,T0Count3;
	TL0 = 0x18;		//设置定时初值
	TH0 = 0xFC;		//设置定时初值
	T0Count1++;
	T0Count2++;
	T0Count3++;
	
	//每隔20ms后执行一次按键扫描
	if(T0Count1>=20) 
	{
		T0Count1=0;
		Key_Loop();
	}
	
	//每隔2ms后执行一次数码管扫描
	if(T0Count2>=2) 
	{
		T0Count2=0;
		Nixie_Loop();
	}
	
	//每隔10msMiniSec加一
	if(T0Count3>=10) 
	{
		T0Count3=0;
		Sec_Loop();
	}
}

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();
void Key_Loop();
unsigned char Key_GetState();

#endif

Key.c

#include <REGX52.H>
#include "Delay.h"

unsigned char Key_KeyNumber;

unsigned char Key()
{
	//因为Key_KeyNumber每次读取完需要置0
	//所以先定义一个temp暂存要返回的按键键码
	unsigned char temp=0;
	temp=Key_KeyNumber;
	Key_KeyNumber=0;  //置0
	return temp;
}

/**
  * @brief  获取按键键码
  * @param  无
  * @retval 按下按键的键码,范围:0~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;
}

//按键循环扫描操作,在主函数中放在定时器中断函数内,可以避免Delay函数的使用
void Key_Loop()  
{
	//定义静态变量NowState和LateState存储现在按键状态和上一次按键状态【0或(1234)非0】
	//通过比较这两次状态可以判断按键是否按下和是否松开,不用执行按键去抖
	static unsigned char NowState,LastState;
	LastState=NowState;  //更新LastState
	NowState=Key_GetState();  //更新NowState
	
	//上一次扫描状态为1,这一次为0,证明按键按下
	if(LastState==1 && NowState==0)  
	{
			Key_KeyNumber=1;
	}	
	//上一次扫描状态为2,这一次为0,证明按键按下
	if(LastState==2 && NowState==0)
	{
			Key_KeyNumber=2;
	}				
	//上一次扫描状态为3,这一次为0,证明按键按下
	if(LastState==3 && NowState==0)
	{
			Key_KeyNumber=3;
	}				
	//上一次扫描状态为4,这一次为0,证明按键按下
	if(LastState==4 && NowState==0)
	{
			Key_KeyNumber=4;
	}				
}

Nixie.h

#ifndef __NIXIE_H__
#define __NIXIE_H__

void Nixie_Scan(unsigned char Location,Number);
void Nixie_Loop();
void Nixie_SetBuf(unsigned char Location,Number);

#endif

 Nixie.c

#include <REGX52.H>
#include "Delay.h"	//包含Delay头文件

//显示缓存区
unsigned char Nixie_Buf[9]={0};

//数码管段码表
unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x00,0x40};

//设置数码管各位的段码
void Nixie_SetBuf(unsigned char Location,Number)
{
	Nixie_Buf[Location]=Number;
}

//数码管扫描
void Nixie_Scan(unsigned char Location,Number)
{
	P0=0x00;				//段码清0,消影
	switch(Location)		//位码输出
	{
		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=NixieTable[Number];	//段码输出
}

//数码管循环扫描操作,在主函数中放在定时器中断函数内,可以避免Delay函数的使用
void Nixie_Loop()
{
	//定义静态变量i控制位码,Buf数组控制段码,扫描八位显示
	static unsigned char i=1;
	Nixie_Scan(i,Nixie_Buf[i]);
	i++;
	if(i>=9)  i=1;
}

 Timer0.h

#ifndef __TIMER0_H__
#define __TIMER0_H__

void Timer0Init(void);

#endif

Timer0.c

#include <REGX52.H>

/**
  * @brief  定时器0初始化,1毫秒@12.000MHz
  * @param  无
  * @retval 无
  */
void Timer0Init(void)
{
	TMOD &= 0xF0;		//设置定时器模式
	TMOD |= 0x01;		//设置定时器模式
	TL0 = 0x18;		//设置定时初值
	TH0 = 0xFC;		//设置定时初值
	TF0 = 0;		//清除TF0标志
	TR0 = 1;		//定时器0开始计时
	ET0=1;
	EA=1;
	PT0=0;
}

AT24C02.h

#ifndef __AT24C02_H__
#define __AT24C02_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  

/**
   * @brief   	AT24C02写入一个字节
   * @param	  	WordAddress 要写入数据的地址 ,Data 要写入的数据
   * @retval 	无
   */
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();
}

/**
   * @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();
	I2C_SendAck(1);
	I2C_Stop();
	return Data;
}

I2C.h

#ifndef __I2C_H__
#define __I2C_H__

void I2C_Start(void);
void I2C_Stop(void);
void I2C_SendByte(unsigned char Byte);
unsigned char I2C_ReceiveByte(void);
void I2C_SendAck(unsigned char AckBit);
unsigned char I2C_ReceiveAck(void);

#endif

 I2C.c

#include <REGX52.H>

sbit I2C_SCL=P2^1;
sbit I2C_SDA=P2^0;

/**
   * @brief   开始
   * @param		无
   * @retval	无
   */
void I2C_Start(void)  //开始
{
	I2C_SDA=1;  //确保SDA是在高电平
	I2C_SCL=1;  //确保SDA是在高电平
	I2C_SDA=0;  //先拉低SDA
	I2C_SCL=0;	//再拉低SCL
}

/**
   * @brief   停止
   * @param		无
   * @retval	无
   */
void I2C_Stop(void)  //停止
{
	I2C_SDA=0;  //确保SDA是在低电平,SCL在各个状态结束时都是低电平不用置0
	I2C_SCL=1;  //先拉低SCL
	I2C_SDA=1;	//再拉低SDA
}

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(void)  //接收一个字节
{
	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;  //1位应答,0为未应答
	I2C_SCL=1;
	I2C_SCL=0;
}
	
/**
   * @brief   接收应答
   * @param		无
   * @retval 	接收到的应答位
   */
unsigned char I2C_ReceiveAck(void)  //接收应答
{
	unsigned char AckBit;
	I2C_SDA=1;
	I2C_SCL=1;
	AckBit=I2C_SDA;  //应答为1,为应答为0
	I2C_SCL=0;
	return AckBit;
}

 来个免费的赞吧,这对我真的很重要!!!!!!!!!!!

 

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值