12—2:AT24C02数据存储&秒表(定时器扫描按键数码管)

目录

1.课程记录

程序

I2C.c

start函数 

Stop函数

 I2C_SendByte函数

 注意:

I2C_ReceiveByte函数 

I2C_SendAck函数

I2C_ReceiveAck函数

小结

AT2C4C02.c

测试1

测试2 

小结

项目2-秒表(定时器扫描按键数码管)

按键key.c改造为定时器key

学习小结

参考资料:


1.课程记录

4:54

写三个模块,封装分层

I2C作为最底层

包括6个函数:

START, STOP

发送一个字节,接收一个字节

发送应答,接收应答

AT24C02调用I2C函数:

两个数据帧

在某个地址下写入数据,

在某个地址下读出数据,

main模块调用AT24C02模块

程序-项目1-AT24C02数据存储

I2C.c

0:7:25

start函数 

空闲时

空闲时,SCL=1,SDA=1

 上图中1对应 空闲时,SDA=1

2,对应另一种情况,  非空闲,在发送完数据后,接收应答,此时SDA=0,

因此在Start阶段, SDA有两种情况    SDA=1,  或者  SDA=0

//2025.6.28


#include <REGX52.H>

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

void I2C_Start(void)
{
	I2C_SDA=1;
	I2C_SCL=1;
	I2C_SDA=0;
	I2C_SCL=0;
}

Stop函数

情况1(空闲时)

情况2:

在接收应答后,进入停止阶段

说明 在停止阶段前, SDA可能是0或者1。

//2025.6.28


#include <REGX52.H>

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

void I2C_Start(void)
{
	I2C_SDA=1;
	I2C_SCL=1;
	I2C_SDA=0;
	I2C_SCL=0;
}

void I2C_Stop(void)
{
	I2C_SDA=0;
	I2C_SCL=1;
	I2C_SDA=1;
}

 I2C_SendByte函数

 11:14

注意:

在发送字节前,SCL已经为0

 

void I2C_SendByte(unsigned char Byte)
{
	I2C_SDA=Byte&0x80 ;
	//Byte(写入的字节数据),
	//通过与1000 0000进行与操作,得到最高位数据(因为数据写入是高位在前,低位在后)
	I2C_SCL=1; //SCL=1,此时读取SDA的内容,即Byte&0x80
	T2C_SCL=0; // 在读取完SDA内容后,SCL=0,不再读取SDA内容,此时允许SDA有数据变化
	
}
 注意:

这里

SCL置1后, 立马SCL置0, 

问题:  SCL高电平持续时间较短,能否在SCL高电平期间读取到数据?

参考手册内容  24C02.pdf

SCL 在5V电压下频率为 1000khz, 即1mhz,  周期为1us,

单片机IO口翻转速度最快为1us,频率为500khz,

因此上述 SCL=1,SCL=0 ,电平立马拉高然后立马拉低,  其频率小于SCL的极限范围,因此在该时间段可以读取数据

SCL高电平持续时间只要高于0.4us  ,即可满足,  现在使用的单片机STC89C52,最短持续时间为1us  ,满足要求

注意

 14:05

 

因为一个字节有8位数据,因此需要将上述模块调用8次,并且每调用一次,0x80需要右移一位

 

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

I2C_ReceiveByte函数 

接受数据,首先主机释放SDA,即SDA=1

注意:一般规定:仅在SCL高电平,才从SDA获取数据 

unsigned char I2C_ReceiveByte(void)
{
	unsigned char Byte=0x00;
	I2C_SDA=1;
	
	I2C_SCL=1;
	if(I2C_SDA) {Byte|=0x80;} //如果I2C_SDA为1,则Byte 第八位置1
	I2C_SCL=0;
	return Byte;
}

 


unsigned char I2C_ReceiveByte(void)
{
	unsigned char i,Byte=0x00;
	I2C_SDA=1;
	for(i=0;i<8;i++)    //调用该模块8次
	{
		I2C_SCL=1;
		if(I2C_SDA) {Byte|=(0x80>>i);} 
		I2C_SCL=0;	
	}
	return Byte;
}

应答函数

I2C_SendAck函数

void I2C_SendAck(unsigned char AckBit) //AckBit 应答位,为0代表接收应答
{
	I2C_SDA=AckBit;
	I2C_SCL=1;
	I2C_SCL=0;
}

I2C_ReceiveAck函数

unsigned char I2C_ReceiveAck(void)
{
	unsigned char AckBit;
	I2C_SDA=1;  //主机释放SDA,从机进行控制SDA
	I2C_SCL=1;
	AckBit=I2C_SDA;
	I2C_SCL=0;
	return AckBit;
}

 个人:

感觉类似看折线图编程,判断SDA,SCL什么时候为1或0

小结

//2025.6.28


#include <REGX52.H>

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


/**
  * @brief  I2C开始
  * @param  无
  * @retval 无
  */
void I2C_Start(void)
{
	I2C_SDA=1;
	I2C_SCL=1;
	I2C_SDA=0;
	I2C_SCL=0;
}

/**
  * @brief  I2C停止
  * @param  无
  * @retval 无
  */
void I2C_Stop(void)
{
	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);//与操作
		I2C_SCL=1; 
		I2C_SCL=0; 
	}
}

/**
  * @brief  I2C接收一个字节
  * @param  无
  * @retval 接收到的一个字节数据
  */
unsigned char I2C_ReceiveByte(void)
{
	unsigned char i,Byte=0x00;
	I2C_SDA=1;
	for(i=0;i<8;i++)    //调用该模块8次
	{
		I2C_SCL=1;
		if(I2C_SDA) {Byte|=(0x80>>i);} 
		I2C_SCL=0;	
	}
	return Byte;
}

/**
  * @brief  I2C发送应答
  * @param  AckBit 应答位,0为应答,1为非应答
  * @retval 无
  */
void I2C_SendAck(unsigned char AckBit) //AckBit 应答位,为0代表接收应答
{
	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;  //主机释放SDA,从机进行控制SDA
	I2C_SCL=1;
	AckBit=I2C_SDA;
	I2C_SCL=0;
	return AckBit;
}

AT2C4C02.c

0:24:38

字节写函数

随机读函数

31:40

测试1

AT24C02.c

//2025.6.29

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

//定义从机地址(也相当于:地址+W)  地址查看硬件手册
#define AT24C02_ADDRESS 0xA0


//字节写函数
void AT24C02_WriteByte(unsigned char WordAddress,Data)
{
	unsigned char Ack;
	I2C_Start();
	I2C_SendByte(AT24C02_ADDRESS);
	Ack=I2C_ReceiveAck();
	if (Ack==0) P2=0x00;   //如果存在-接受应答,则led全亮
}


//随机读函数
unsigned char AT24C02_ReadByte(unsigned char WordAddress)
{
	unsigned char Data;
	
	
	return Data;
}

 main.c

//2025.6.28
#include <REGX52.H>
#include "LCD1602.h"
#include "Key.h"
#include "AT24C02.h"


void main()
{
	LCD_Init();
	LCD_ShowString(1,1,"Hello"); 
	AT24C02_WriteByte(0,0);
	while(1)
	{
	
	}
}

效果

led全亮。

测试2 

main.c

//2025.6.28
#include <REGX52.H>
#include "LCD1602.h"
#include "Key.h"
#include "AT24C02.h"
#include "Delay.h"

unsigned char Data;

void main()
{
	LCD_Init();
	LCD_ShowString(1,1,"Hello"); 
	//AT24C02_WriteByte 函数的参数:字节地址(范围:0~255),数据(需要写入的数据)
	AT24C02_WriteByte(1,101);
	Delay(5); // 注意:根据硬件手册,写周期至少需要5ms,如果没有加延时,则不会显示读取的数据,因为没有写入
	Data=AT24C02_ReadByte(1);
	LCD_ShowNum(2,1,Data,3); //在第2行第1列,显示Data,长度为3
	while(1)
	{
	
	}
}

效果:

 

小结

//2025.6.29

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

//定义从机地址(也相当于:地址+W)  地址查看硬件手册
#define AT24C02_ADDRESS 0xA0

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


/**
  * @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); // AT24C02_ADDRESS为从机地址+W(0xA0),从机地址+R为0xA1
	I2C_ReceiveAck();
	Data=I2C_ReceiveByte();
	I2C_SendAck(1);
	I2C_Stop();
	return Data;
}

main.c

一个0~255,共256个存储单元 

46:33

//2025.6.28
#include <REGX52.H>
#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); //初始显示默认Num
	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) //按键3功能:存入数据
		{
			AT24C02_WriteByte(0,Num%256); //将Num的低8位存储在地址0
			Delay(5);
			AT24C02_WriteByte(1,Num/256);//将Num的高8位存储在地址1
			Delay(5);//写周期至少需要5ms
			LCD_ShowString(2,1,"Write OK");
			Delay(1000);
			LCD_ShowString(2,1,"        ");
		}
		if (KeyNum==4) //按键4功能:从AT24C02芯片读取Num显示在屏幕
		{
			Num=AT24C02_ReadByte(0);
			Num|=AT24C02_ReadByte(1)<<8; //读取Num的高八位,然后左移8位
			LCD_ShowNum(1,1,Num,5);
			LCD_ShowString(2,1,"Read OK");
			Delay(1000);
			LCD_ShowString(2,1,"        ");
		}
	}
}

 这里高位左移8为,挪到高8位,低八位为0,然后与NUM进行或操作,NUM填写入低八位

 

0:46:47

项目2-秒表(定时器扫描按键数码管)

 讲解:

左边main,  右边(从上往下)定时器模块,按键模块,数码管模块

因为按键,数码管模块均需要调用定时器,但是这样存在交叉耦合(耦合性高,不易于代码管理)(即每个模块之间存在相互调用,那么后续更改时一个模块,其他模块也要跟着修改),因此分别在按键,数码管各自写一个中断调用函数

在main模块,规定假设20ms调用一次按键,30ms调用一次数码管   

up:让主模块为子模块服务,自称为驱动函数 或者  循环电路函数

个人:还是不太理解

main.c

//2025.6.29

#include <REGX52.H>
//包含之前项目:按键,数码管,时钟,定时器
#include "Timer0.h"
#include "Nixie.h"
#include "Key.h"
#include "Delay.h"



void main()
{	
	Timer0_Init(); //初始化定时器
	while(1)
	{
	
	}
}


//每个1s调用一次中断
//定时器中断函数模版
void Timer0_Routine() interrupt 1
{
	static unsigned int T0Count;  //默认初值为0  //设置静态变量,在该函数内再次调用时,保留上一次的值
	TL0 = 0x66;		//设置定时初值
	TH0 = 0xFC;		//设置定时初值
	T0Count++;
	if(T0Count>=1000)  //  每隔一秒,led8个灯  闪烁一次
	{
		T0Count=0;
		P2=~P2;
	}
}

58:47

原来检测按键是否  按下  或者 松开 的原理:

按下:0-->1,  松开:1-->0,      在0与1的过渡之间存在抖动,因此需要延时20ms,在低电平(按下期间)期间,需要一直while(1)死循环,  因此不灵活,时间占用长

使用定时器检测按键是否  按下  或者 松开 的原理:

每隔20ms检测 一次按键状态值,  同时记录上一时刻按键状态值,与当前值进行对比,

0-->1:表示按下,  1-->0:表示松开, 

(注意:好像是 按键状态值为0,表示按下,)

按键key.c改造为定时器key

原来

仅依靠Delay函数,while()循环

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

/**
  * @brief  获取独立按键键码
  * @param  无
  * @retval 按下按键的键码,范围:1~4, 无按键按下时返回值为0
  */

unsigned char Key()
{
	unsigned char KeyNumber=0;
	
	if(P3_1==0){Delay(200);while(P3_1==0);Delay(200);KeyNumber=1;}
	if(P3_0==0){Delay(200);while(P3_0==0);Delay(200);KeyNumber=2;}
	if(P3_2==0){Delay(200);while(P3_2==0);Delay(200);KeyNumber=3;}
	if(P3_3==0){Delay(200);while(P3_3==0);Delay(200);KeyNumber=4;}
	
	return KeyNumber;
	
}

更改版本

 

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

unsigned char Key_KeyNumber;


unsigned char Key(void)
{
	unsigned char Temp=0; //设置一个中间变量,返回当前按键的值(表面是1~4其中哪个按键按下)
	Temp=Key_KeyNumber;
	Key_KeyNumber=0;  //初始化按键值,等待下一次按键的返回值
	return Temp;
}


/**
  * @brief  获取独立按键键码
  * @param  无
  * @retval 按下按键的键码,范围:1~4, 无按键按下时返回值为0
  */

unsigned char Key_GetState()
{
	unsigned char KeyNumber=0;
	
	if(P3_1==0){KeyNumber=1;}
	// 经过定时器扫描按键状态值(如设定每隔20ms扫描一次),检测按键P3^1引脚状态值为0,说明按键1处于按下状态,
	//此时将KeyNumber=1,表明是按键1按下
	if(P3_0==0){KeyNumber=2;}
	if(P3_2==0){KeyNumber=3;}
	if(P3_3==0){KeyNumber=4;}
	
	return KeyNumber;
}


void Key_Loop(void)
{
	static unsigned char NowState,LastState;  //静态变量
	//保存按键 上一个扫描时刻状态,当前扫描时刻状态
	LastState=NowState;
	NowState=Key_GetState();
	if(LastState==1 && NowState==0) 
	//针对按键1,如果上一个扫描时刻状态为按下,当前扫描时刻状态为松开,则表明按键1 有一次按键操作发送
	{
		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;
	}
	
}

测试:

main.c

//2025.6.29

#include <REGX52.H>
//包含之前项目:按键,数码管,时钟,定时器
#include "Timer0.h"
#include "Nixie.h"
#include "Key.h"

#include "Delay.h"

unsigned char KeyNum;
unsigned char Temp;


void main()
{	
	Timer0_Init(); //初始化定时器
	while(1)
	{
		KeyNum=Key();
		if (KeyNum) Temp=KeyNum;
		Nixie(1,Temp);  //按键按下,(按键1~4),第一个数码管显示对应按键数字
		
	}
}


//每个1s调用一次中断
//定时器中断函数模版
void Timer0_Routine() interrupt 1
{
	static unsigned int T0Count;  //默认初值为0  //设置静态变量,在该函数内再次调用时,保留上一次的值
	TL0 = 0x66;		//设置定时初值
	TH0 = 0xFC;		//设置定时初值
	T0Count++;
	if(T0Count>=20)  //  每隔20ms,扫描一次按键状态
	{
		T0Count=0;
		Key_Loop();
	}
}

 

定时器间隔20ms扫描键盘,将按下的按键对应的数字显示在数码管 

定时器扫描数码管

 原本

Nixie.c

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


unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71,0x00
};

void Nixie(unsigned char Location,unsigned char Number)
{
	switch(Location)  //位选
	{
		case 1:P2_4=1;P2_3=1;P2_2=1;break; // 4+2+1==7,????,???7????
		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(1);
	P0=0X00;  //段清零  //显示数字,延时1ms,再清零
	
}

改造后

1:14:10

Nixie_SetBuf()

 三个数码管显示相同数字

1:16:00

//2025.6.29

#include <REGX52.H>
//包含之前项目:按键,数码管,时钟,定时器
#include "Timer0.h"
#include "Nixie.h"
#include "Key.h"

#include "Delay.h"

unsigned char KeyNum;
unsigned char Temp;


void main()
{	
	Timer0_Init(); //初始化定时器
	while(1)
	{
		KeyNum=Key();
		if(KeyNum)
		{
			Nixie_SetBuf(1,KeyNum);
			Nixie_SetBuf(2,KeyNum);
			Nixie_SetBuf(3,KeyNum);
		}	
	}
}


//每个1s调用一次中断
//定时器中断函数模版
void Timer0_Routine() interrupt 1
{
	static unsigned int T0Count1,T0Count2;  //默认初值为0  //设置静态变量,在该函数内再次调用时,保留上一次的值
	TL0 = 0x66;		//设置定时初值
	TH0 = 0xFC;		//设置定时初值
	T0Count1++;
	if(T0Count1>=20)  //  每隔20ms,扫描一次按键状态
	{
		T0Count1=0;
		Key_Loop();
	}
	
	//扫描数码管
	T0Count2++;
	if(T0Count2>=2)  //  每隔2ms,扫描一次数码管
	{
		T0Count2=0;
		Nixie_Loop();
	}
}

总结-项目实现

//2025.6.29

#include <REGX52.H>
//包含之前项目:按键,数码管,时钟,定时器
#include "Timer0.h"
#include "Nixie.h"
#include "Key.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) //启动,暂停 键
		{
			RunFlag=!RunFlag;
		}
		if(KeyNum==2) //归零
		{
			Min=0;
			Sec=0;
			MiniSec=0;
		}
		if(KeyNum==3) //写入
		{
			AT24C02_WriteByte(0,Min);
			Delay(5);//写入周期
			AT24C02_WriteByte(1,Sec);
			Delay(5);
			AT24C02_WriteByte(2,MiniSec);
			Delay(5);
		}
		if(KeyNum==4) //读取
		{
			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(void)
{
	if(RunFlag) //启动标志
	{
		MiniSec++;
		if(MiniSec>=100) //10*100=1000ms
		{
			MiniSec=0;
			Sec++;
			if(Sec>=60)
			{
				Sec=0;
				Min++;
				if(Min>=60)
				{
					Min=0;
				}
			}
		}
	}
}


//每个1s调用一次中断
//定时器中断函数模版
void Timer0_Routine() interrupt 1 // 每隔1ms调用一次,(调用下述函数)
{
	static unsigned int T0Count1,T0Count2,T0Count3;  //默认初值为0  //设置静态变量,在该函数内再次调用时,保留上一次的值
	TL0 = 0x66;		//设置定时初值
	TH0 = 0xFC;		//设置定时初值
	T0Count1++;
	if(T0Count1>=20)  //  每隔20ms,扫描一次按键状态
	{
		T0Count1=0;
		Key_Loop();
	}
	
	//扫描数码管
	T0Count2++;
	if(T0Count2>=2)  //  每隔2ms,扫描一次数码管
	{
		T0Count2=0;
		Nixie_Loop();
	}
	//扫描计时
	T0Count3++;
	if(T0Count3>=10)  //  每隔2ms,扫描一次数码管
	{
		T0Count3=0;
		Sec_Loop();
	}
}

 

 看来2天半敲完,期间确实有休息5min,学5min,玩10min,  惭愧。

学习小结

1.硬件知识

2.代码模块化编程  (这个很重要,对于编写复杂功能的项目,需要将模块分卡编写)

3.在写项目时,要边写,边测试,把一个个模块确定好,不能都写完了在验证,这样很可能之前代码全部白费。 

参考资料:

1.[12-2] AT24C02数据存储&秒表(定时器扫描按键数码管)_哔哩哔哩_bilibili

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值