51单片机从零开始入门教程(存储器篇)

参考教程:[12-1] AT24C02(I2C总线)_哔哩哔哩_bilibili

1、存储器的分类:简单说,RAM的速度高于ROM,但是掉电后RAM中的数据会丢失,而ROM不会。(具体可以看《计算机组成原理》这门课,里面有很详细的介绍)

2、AT24C02是一种可以实现掉电不丢失的存储器,可用于保存单片机运行时想要永久保存的数据信息,存储介质——E2PROM,通讯接口——I2C总线,容量——256字节。

引脚

功能

VDD、GND

电源(1.8V~5.5V)

WE

写保护(高电平有效)

SCL、SDA

I2C接口

A0、A1、A2

I2C地址

3、内部结构框图:

4、I2C总线介绍:I2C总线(Inter IC BUS)是由Philips公司开发的一种通用数据总线,它有两根通信线——SCL(Serial Clock)、SDA(Serial Data),采取的是同步、半双工,带数据应答。

5、I2C电路规范:

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

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

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

弱上拉输入模式下,通信线被完全释放,也就是没有被任何设备控制时会处于高电平

弱上拉输入模式下,只要有一个设备给通信线置为低电平,也就是接地,那么即使其它设备想置通信线为高电平也无济于事

(4)左图是一个一主多从的结构,任何时候都是主机完全掌控SCL线(从机只有读取SCL线的权力),在空闲状态下主机可以主动发起对SDA的控制只有在主机给从机发送读取命令后(从机需要借助SDA向主机发送数据)或者从机应答的时候主机才会将SDA的控制权转交给从机

6、I2C时序结构(拆成六个部分解释):

(1)起始(左图)与结束(右图):

①起始条件:SCL高电平期间,SDA从高电平切换到低电平(SDA下降沿触发)。

②终止条件:SCL高电平期间,SDA从低电平切换到高电平(SDA上升沿触发)。

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

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

(4)发送应答(左图)与接收应答(右图):

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

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

7、I2C时序结构(合体版,注意各个方块的颜色):

(1)发送一帧数据(主机向从机发送数据):首先是起始标志(SCL高电平期间,SDA从高电平切换到低电平),接着是需要接收数据的从机的地址(后7位)以及读/写选择位(第0位,主机发送数据则需要置为0),接着主机接收从机的应答(数据0表示应答),然后主机开始向从机发送数据,每发送一个字节,主机就要接收一次从机的应答,接收到应答后主机才能发送下一个字节的数据给从机,这个过程反复执行,直到数据传送完成,主机收到最后一个应答,最后接上结束标志(SCL高电平期间,SDA从低电平切换到高电平)。

(2)接收一帧数据(主机从从机中读取数据):首先是起始标志(SCL高电平期间,SDA从高电平切换到低电平),接着是需要读取的数据所在从机的地址(后7位)以及读/写选择位(第0位,主机接收数据则需要置为1),接着主机接收从机应答(数据0表示应答),然后主机开始读取从机的数据,每读取一个字节,主机就要向从机发送一个应答信号,从机接收到应答后主机才能读取下一个字节的数据,这个过程反复执行,直到数据传送完成,从机收到非应答信号,最后接上结束标志(SCL高电平期间,SDA从低电平切换到高电平)。

(3)先发送再接收数据帧(复合格式):其实就是前两种方式的结合,只是只有一个结束标志而已。

(4)字节写:在WORD ADDRESS处写入数据DATA。

(5)随机读:读出在WORD ADDRESS处的数据DATA。

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

8、通过按键控制AT24C02数据存储:

(1)项目包含的文件:其中需要重写的都会在下面给出,未给出的沿用旧例出现过的即可(本例需要液晶屏模块、独立按键模块以及延时函数的代码文件)。

(2)补充缺失的代码文件,进行编译。

①I2C.h文件:

#ifndef __I2C_H__
#define __I2C_H__

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

#endif

②I2C.c文件:

#include <REGX52.H>

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

/**
  * @brief  I2C开始
  * @param  无
  * @retval 无
  */
void I2C_Start()  //起始标志
{
	I2C_SDA = 1;
	I2C_SCL = 1;
	//SDA和SCL刚开始都要处于高电平
	I2C_SDA = 0;
	I2C_SCL = 0;
	//SCL处于高电平时,SDA出现下降沿,这就是起始标志,然后SCL置为0,进入下一个部分
	//可以看到,除了起始部分外,其它部分开始时SCL都是处于低电平,所以结束时SCL要置为0(其它部分结束时SCL也要置为低电平)
}

/**
  * @brief  I2C停止
  * @param  无
  * @retval 无
  */
void I2C_Stop()  //结束标志
{
	I2C_SDA = 0;
	I2C_SCL = 1;
	//SDA为了等一下出现上升沿要先置为0,SCL要保证处于高电平(两条语句顺序最好不要错,严格按照时序图安排)
	I2C_SDA = 1;
	//SCL处于高电平,SDA出现上升沿,这就是结束标志
	//SCL不用置为0,因为它的下一个部分一定是起始标志
}

/**
  * @brief  I2C发送一个字节
  * @param  Byte:需要发送的字节
  * @retval 无
  */
void I2C_SendByte(unsigned char Byte)  //主机(单片机)发送一个字节
{
	unsigned char i = 0;
	for(i = 0; i < 8; i++)
	{
		I2C_SDA = Byte & (0x80 >> i);  //从高位开始一位一位的将数据放在SDA上
		I2C_SCL = 1;  //从机在SCL高电平期间读取SDA上的数据
		I2C_SCL = 0;  //读取一位完毕,SCL降下来,等待SDA写好下一位数据
	}
}

/**
  * @brief  I2C接收一个字节
  * @param  无
  * @retval 接收到的一个字节数据
  */
unsigned char I2C_ReceiveByte()  //主机(单片机)接收一个字节
{
	unsigned char Byte = 0;
	unsigned char i = 0;
	I2C_SDA = 1;  //主机在接收之前,需要释放SDA
	
	for(i = 0; i < 8; i++)
	{
		I2C_SCL = 1;  //主机在SCL高电平期间读取SDA上的数据
		if(I2C_SDA)  //SDA的数据是低电平的话,Byte对应位不需要改动
		{
			Byte |= (0x80 >> i);  //从高位开始一位一位的读取SDA上的数据
		}
		I2C_SCL = 0;  //读取一位完毕,SCL降下来,等待SDA写好下一位数据
	}
	return Byte;
}

/**
  * @brief  I2C发送应答
  * @param  AckBit:应答位(0表示应答)
  * @retval 无
  */
void I2C_SendAck(bit AckBit)  //主机发送应答
	//(bit是C51特有的数据类型,和布尔类型差不多,但它占1位,布尔类型占1个字节)
{
	I2C_SDA = AckBit;  //主机将应答信号放在SDA上(0表示应答)
	I2C_SCL = 1;  //SCL置为高电平(此前它必须是低电平,其它部分结束时都已经将其置为低电平)
	I2C_SCL = 0;  //SCL在高电平时主机发送应答,发送完毕后SCL置为低电平
}

/**
  * @brief  I2C接收应答
  * @param  无
  * @retval 应答位(0表示应答)
  */
bit I2C_ReceiveAck()  //主机接收应答
	//(bit是C51特有的数据类型,和布尔类型差不多,但它占1位,布尔类型占1个字节)
{
	bit AckBit;
	I2C_SDA = 1;  //主机在接收之前,需要释放SDA
	I2C_SCL = 1;  //SCL置为高电平(此前它必须是低电平,其它部分结束时都已经将其置为低电平)
	AckBit = I2C_SDA;  //将从机在SDA上的应答信号存进AckBit,然后将其返回
	I2C_SCL = 0;  //接收完毕后SCL置为低电平
	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:要写入字节的地址
	* @param  Data:要写入的字节数据
  * @retval 无
  */
void AT24C02_WriteByte(unsigned char WordAddress, Data)  //往存储器中写一个字节数据(字节写)
{
	I2C_Start();  //开始标志
	I2C_SendByte(AT24C02_ADDRESS);
	//需要接收数据的从机的地址(后7位)以及读/写选择位(第0位,主机发送数据则需要置为0)
	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);
	//需要发送数据的从机的地址(后7位)以及读/写选择位(第0位,主机接收数据则需要置为1)
	I2C_ReceiveAck();    //主机接收从机的应答
	Data = I2C_ReceiveByte();  //从机发来的数据记录在Data中
	I2C_SendAck(1);       //主机发送非应答
	I2C_Stop();   //结束标志
	return Data;
}

⑤main.c文件:

#include <REGX52.H>
#include "Delay.h"
#include "key.h"
#include "LCD1602.h"
#include "AT24C02.h"

unsigned char KeyNum = 0;
unsigned int Num;  //2字节

void main()
{
	LCD_Init();  //液晶屏初始化
	LCD_ShowNum(1,1,Num,5);  //显示Num的初值
	while(1)
	{
		KeyNum = Key();
		if(KeyNum == 1)  //按键1给当前Num值+1
		{
			Num++;
			LCD_ShowNum(1,1,Num,5);  //显示当前Num值
		}
		if(KeyNum == 2)  //按键2给当前Num值-1
		{
			Num--;
			LCD_ShowNum(1,1,Num,5);  //显示当前Num值
		}
		if(KeyNum == 3)  //按键3将Num值写入存储器
		{
			AT24C02_WriteByte(0,Num%256);  //先写低8位
			Delay(5);
			//写的过程比程序执行的过程慢很多,每次写操作需要延时5ms(参考手册上的值),否则可能会出错
			AT24C02_WriteByte(1,Num/256);  //再写高8位
			Delay(5);
			LCD_ShowString(2,1,"Write OK");  //输出Write OK显示
			Delay(1000);    //提示信息显示1秒足矣
			LCD_ShowString(2,1,"        ");  //清空Write OK显示
		}
		if(KeyNum == 4)  //按键4从存储器读出Num值并输出(并将存储器中的Num值赋给当前Num)
		{
			Num = AT24C02_ReadByte(0);        //先读低8位
			Num |= AT24C02_ReadByte(1) << 8;  //再读高8位
			LCD_ShowNum(1,1,Num,5);    //显示存储器中的Num值
			LCD_ShowString(2,1,"Read OK");  //输出Read OK显示
			Delay(1000);    //提示信息显示1秒足矣
			LCD_ShowString(2,1,"       ");  //清空Read OK显示
		}
	}
}

(3)将生成的.hex文件下载到开发板中,根据main.c文件中的注释进行调试即可(建议测试按键4的功能时先给单片机断电,然后再通电,以此验证AT24C02即使断电也能保存数据)。

9、通过按键控制秒表,且将数据记录在AT24C02:

(1)之前的key.c文件中用来检测按键是否被按下的函数存在死循环,如果按住按键不松开,那么程序会一直卡在死循环中,这样会影响其它模块以及整个程序的进行,为了解决这个问题,可以使用定时器扫描按键,程序会每隔一段时间检测按键状态,不会卡在死循环中。

(2)项目包含的文件:其中需要重写的都会在下面给出,未给出的沿用旧例出现过的即可(本例需要延时函数的代码文件以及上例中的I2C和AT24C02文件)。

(3)补充缺失的代码文件,进行编译。

①key.h文件:

#ifndef __KEY_H__
#define __KEY_H__

unsigned char Key();
void Key_Loop();

#endif

②key.c文件:

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

unsigned char Key_KeyNumber;

unsigned char Key()
{
	unsigned char Temp;
	Temp = Key_KeyNumber;
	//Key_KeyNumber记录被按下的按键键码,当中断结束后该函数会将键码值返回给主函数
	Key_KeyNumber = 0;
	//按键按下一次并松开后,只做出一次行为,而键码值返回一次后正好执行了一次对应的行为
	//为了防止按键松开后重复执行多次行为,Key_KeyNumber返回一次之后要清零
	return Temp;
}

unsigned char Key_GerState()
{
	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;
}

void Key_Loop()
{
	static unsigned char NowState, LastState;  //定义两个变量记录按键的当前状态和前20ms的状态
	LastState = NowState;  //把前20ms的按键状态赋给LastState
	NowState = Key_GerState();  //NowState获取当前按键状态
	if(LastState == 1 && NowState == 0)  //如果按键1前20ms被按下,且当前松开按键
	{
		Key_KeyNumber = 1;  //Key_KeyNumber记录被按下的按键键码
	}
	if(LastState == 2 && NowState == 0)  //如果按键2前20ms被按下,且当前松开按键
	{
		Key_KeyNumber = 2;  //Key_KeyNumber记录被按下的按键键码
	}
	if(LastState == 3 && NowState == 0)  //如果按键3前20ms被按下,且当前松开按键
	{
		Key_KeyNumber = 3;  //Key_KeyNumber记录被按下的按键键码
	}
	if(LastState == 4 && NowState == 0)  //如果按键4前20ms被按下,且当前松开按键
	{
		Key_KeyNumber = 4;  //Key_KeyNumber记录被按下的按键键码
	}
}

③Nixie.h文件:

#ifndef __NIXIE_H__  //如果没有定义__NIXIE_H__,就定义#endif前的代码
#define __NIXIE_H__  //定义__NIXIE_H__,配合#ifndef防止代码段被重复定义

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

#endif  //代码段结束

④Nixie.c文件:

#include <REGX52.H>

unsigned char Nixie_Buf[9]={0,10,10,10,10,10,10,10,10};

//数码管段码表
unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x00,0x40};
//0x00为不显示,0x40为“-”,其余全是数字

void Nixie_SetBuf(unsigned char Location, Number)
{
	Nixie_Buf[Location] = Number;  //更改第Location个数码管显示的数字
}

//数码管显示子函数
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];	//段选
}

void Nixie_Loop()
{
	static unsigned char i = 1;
	Nixie_Scan(i,Nixie_Buf[i]);  //扫描其中一个数码管(第i个数码管显示Nixie_Buf[i])
	i++;
	if(i>=9)  //依靠中断函数调用该函数,反复扫描8个数码管
		i=1;
}

⑤main.c文件:

#include <REGX52.H>
#include "key.h"
#include "Delay.h"
#include "Nixie.h"
#include "Timer0.h"
#include "AT24C02.h"

unsigned char KeyNum;
unsigned char Min,Sec,MiniSec;
unsigned char RunFlag;

void main()
{
	Timer0_Init();  //初始化定时器0
	while(1)
	{
		KeyNum = Key();  //获取键码值
		if(KeyNum == 1)  //如果刚刚按键1被按下且已松开
		{
			RunFlag = !RunFlag;  //开始计时or暂停计时
		}
		if(KeyNum == 2)  //如果刚刚按键2被按下且已松开
		{
			RunFlag = 0;  //暂停计时,且计时数据清零
			Min = 0;
			Sec = 0;
			MiniSec = 0;
		}
		if(KeyNum == 3)  //如果刚刚按键3被按下且已松开
		{
			//将当前计时数据写进存储器
			AT24C02_WriteByte(0,Min);
			Delay(5);
			AT24C02_WriteByte(1,Sec);
			Delay(5);
			AT24C02_WriteByte(2,MiniSec);
			Delay(5);
		}
		if(KeyNum == 4)  //如果刚刚按键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()  //计时
{
	if(RunFlag)
	{
		MiniSec++;
		if(MiniSec>99)
		{
			MiniSec = 0;
			Sec++;
			if(Sec>=60)
			{
				Sec = 0;
				Min++;
				if(Min>=60)
				{
					Min = 0;
				}
			}
		}
	}
}

void Timer0_Routine()  interrupt 1  //CPU响应中断后执行的函数
{
	static unsigned int T0Count1 = 0, T0Count2 = 0, T0Count3 = 0;  //定义计数器
	
	T0Count1++;
	T0Count2++;
	T0Count3++;
	
	if(T0Count1 >= 20)  //每20个中断信号(20ms)执行一次下面的代码段(20ms正好是按键消抖需要的时间)
	{
		Key_Loop();
		T0Count1 = 0;
	}
	if(T0Count2 >= 1)  //每1个中断信号(1ms)执行一次下面的代码段
	{
		Nixie_Loop();
		T0Count2 = 0;
	}
	if(T0Count3 >= 10)  //每10个中断信号(10ms)执行一次下面的代码段
	{
		Sec_Loop();    //计时数据增加10ms
		T0Count3 = 0;
	}
	
	//每次中断结束都要重置计数单元
	TH0 = 0xFC;  //定时器0的计数单元高8位
	TL0 = 0x66;  //定时器0的计数单元低8位
}

(4)将生成的.hex文件下载到开发板中,根据main.c文件中的注释进行调试。

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
学习51单片机C语言需要有一定的基础知识,首先要了解C语言的基本语法和编程规范。可以通过阅读相关的书籍和资料,网上视频教程来系统地学习C语言的基础知识。掌握了C语言的基础知识之后,就可以开始学习51单片机的相关知识。 其次,需要了解51单片机的硬件结构、指令集和编程环境。可以通过查阅51单片机的相关资料和学习笔记来了解51单片机的基本知识和编程环境的搭建。学习过程中可以通过实验和练习加深理解,掌握51单片机的基本原理和编程方法。 学习过程中还需要具备一定的动手能力和实践经验,可以通过购买一些实验套件进行实际的操作和编程练习。通过实际操作可以更加深入地理解单片机的工作原理和编程方法,同时也可以提高自己的动手能力和解决问题的能力。 另外,学习51单片机C语言还需要有一定的毅力和耐心,因为学习过程中可能会遇到各种各样的困难和问题,需要持之以恒地克服这些困难。可以多参与一些相关的社区和论坛,向其他有经验的人请教和交流,可以更快地解决问题和提高自己的学习效率。 总之,学习51单片机C语言需要持续地学习和实践,掌握C语言的基础知识、了解51单片机的硬件结构和编程环境,提高动手能力和解决问题的能力,同时要有毅力和耐心,相信通过不懈的努力一定能够掌握这门技术。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zevalin爱灰灰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值