目录

1.存储器介绍

1.1RAM

1.2ROM

 1.2.1Mask ROM(掩膜ROM)

​编辑

1.2.2PROM(可编程ROM)

1.2.3EPROM(可擦除编程ROM)

1.2.4E2PROM(电可擦除可编程ROM)

1.2.5Flash(闪存)

2.地址总线

3.AT24C02芯片简介

4.I2C总线

4.1工作模式

4.2电路规范

4.3通过总线收发数据

4.4数据应答

4.5数据帧(针对AT24C02芯片)

4.5.1基本数据帧

4.5.2更复杂的数据帧(基于AT24C02)

5.代码配置

5.1I2C配置        

5.2AT24C02配置

6.代码总览

6.1main.c

6.2 Key

6.2.1Key.h

6.2.2Key.c

6.3LCD1602

6.3.1LCD1602.h

6.3.2LCD1602.c

6.4Delay

6.4.1Delay.h

6.4.2Delay.c

6.5I2C

6.5.1I2C.h

6.5.2I2C.c

6.6AT24C02

6.6.1AT24C02.h

6.6.2AT24C02.c


1.存储器介绍

        正如上图所示,存储器主要分为2大类,RAM(Random Access Memory,随机存取存储器)和ROM(Read-Only Memory,只读存储器)。RAM由于是随机存储,所以存储速度快,但是它存在掉电数据丢失的缺点;ROM就正好相反,它存储速度较慢,但是掉电不丢失

1.1RAM

        RAM主要分成两种:SRAM和DRAM。

        SRAM(Static RAM,静态RAM),速度比DRAM还更快,内部结构主要是使用触发器来完成数据存储,现在广泛使用于我们的电脑CPU等需要快速缓存的地方,缺点就是相对较贵。

        DRAM(Dynamic RAM,动态RAM),速度稍慢于SRAM,内部主要基于电容来存储数据,所以价格稍微更低。但是电容在存入电信号之后会放电导致电流失,所以DRAM就要每隔一段时间再把数据重新读入,从而完成数据的持续存储。

1.2ROM

        ROM分为很多种,有:Mask ROM(掩膜ROM),PROM(可编程ROM),EPROM(可擦除编程ROM),E2PROM(电可擦除可编程ROM),Flash(闪存),以及硬盘,软盘,光盘等

 1.2.1Mask ROM(掩膜ROM)

        作为最早出现的一种ROM,它的缺陷自然是很多的,首先它的主要结构是下面的这种二极管:

         并且,由于使用的是这种二极管为基础的电路,它存储的数据就无法被更改,所以得名只读存储器。Mask ROM的数据写入是不可逆的,即一旦数据被写入,就无法更改。这种类型的存储器通常用于存储固定内容,因为它的成本相对较低,且由于其不可擦写的特性,适合于大批量生产。Mask ROM的优点包括成本较低和稳定性较高,因为它们不像其他可擦写存储器那样需要额外的擦写机制。然而,它们的缺点是缺乏灵活性,一旦制造完成,就不能更改存储在其中的数据。

1.2.2PROM(可编程ROM)

        PROM,即Program ROM,也称为可编程ROM,它的主要基本原理也是基于二极管:

Pas P

        这里的蓝色二极管是根据我们写入的程序更改的,不同于Mask ROM,Mask ROM的数据是在厂家生产时就定下来了,PROM的数据则是在写入程序的时候生成,所有的电路都具备这样的组成,当我们写入数据为高电平的时候,会触发击穿电压,从而达到写入数据的目的,因为是把这个蓝色的二极管击穿,所以我们写入程序也叫“烧入程序”,名字就是由此而来。但是PROM依旧是不能够擦除数据,只能写入程序,所以它的局限性依旧很大。

1.2.3EPROM(可擦除编程ROM)

        由于时代的变迁和技术的革新,ROM的材料也开始变革,所以产生了EPROM,即可擦除编程ROM,它无非就是在PROM的二极管基础上更新材料,变成了可以恢复的一种材料,导致半导体击穿之后还可以通过一定的技术手段恢复,所以称为可擦除编程ROM

1.2.4E2PROM(电可擦除可编程ROM)

        由于技术的革新,产生了电可擦除编程ROM,E2PROM和前面的EPROM的区别就是:E2PROM可以通过电直接擦除数据,但是EPROM是不能用电直接擦除的,所以使用电擦除数据,也是一种使用电控制电的手段,也是现在大部分ROM存储的前身。

1.2.5Flash(闪存)

        现在最为广泛使用的ROM存储手段,非常适合用作便携式设备的存储介质,如USB闪存驱动器、智能手机、平板电脑和其他嵌入式系统,广泛适用于各个地方,包括我们的51单片机,堪称ROM唯一真神。

        当然,除了上面这些,还有硬盘,软盘,光盘等ROM式的存储,我们这里就不多赘述了。

2.地址总线

        地址总线较为复杂,我们可以简化成上面的原理图进行说明:首先,地址总线上对应的是不同的设备,一般前面还会加一个译码器,通过对应的地址找到需要选中的设备,并且只能选中一行,这样就可以实现每次只有一个设备写入读取数据,就不会搞混了。选中设备后,设别就把数据写入到数据总线中去了。

3.AT24C02芯片简介

AT24C02芯片
存储介质: E2PROM
通讯接口: I2C 总线
容量: 256 字节

引脚

功能

VCCGND

电源(1.8V~5.5V

WP

写保护(高电平有效)

SCLSDA

I2C接口

A0A1A2

I2C地址

        AT24C02芯片是一个典型的掉电不丢失的ROM芯片,在单片机中,它的原理图是这样的:

        这里写的是EEPROM,其实就是我们前面所说的E2PROM,电可擦除编程ROM,这里的这个引脚好像和前面标注的不太一样,其实是一样的,只是不同的工程师对它有不同的命名罢了

原理图的E0,E1,E2其实就是上面的三个引脚A0,A1,A2,是数据的输入端

SCL和SDA还是一样的

VSS和VDD其实就是GND和VCC,这里原理图也是直接连接的,VSS对应GND,VDD对应VCC。

还有这里有一个WE,上面有一个横线,在这里就表示Write Eable,即写入使能且低电平有效,意思就是接低电平这个芯片才可以写入数据,在原来的引脚定义图里面其实是WP即Write Protect,写保护,高电平有效,意思就是高电平开启写保护,此时无法写入数据,相反的,想要写入数据就要低电平,在完美单片机的原理图里面,这个引脚接GND,即默认这个引脚就是低电平,无论什么时候都是可以直接写入数据的。

        所以,完美在使用单片机的时候其实只要配置SCL,SDA还有三根数据线就行了

        它们都是使用的I2C接口,所以我们这里就先了解一下I2C总线。

4.I2C总线

4.1工作模式

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

它使用两根通信线:SCLSerial Clock)、SDASerial Data

运作特点是:同步、半双工,带数据应答

        前面在讲串口通信的时候稍微提了一下I2C还有这个半双工和同步

        同步且半双工就是下面这样:两个设备通过一根时钟线约定时钟周期(同步),在使用通信的两个设备可以通过同一个时钟线互相传递数据。

        

        这就是I2C总线的大概工作模式。

4.2电路规范

        这里再讲一下I2C总线的电路规范

  • 所有I2C设备的SCL连在一起,SDA连在一起
  • 设备的SCLSDA均要配置成开漏输出模式
  • SCLSDA各添加一个上拉电阻,阻值一般为4.7KΩ左右
  • 开漏输出和上拉电阻的共同作用实现了“线与”的功能,此设计主要是为了解决多机通信互相干扰的问题

        这里介绍一下开漏输出

        开漏输出(Open-Drain Output)是一种特殊的电路输出结构,通常用于数字电路中,特别是在需要通过外部上拉电阻来实现逻辑高电平的场合。开漏输出结构的核心特点是输出端可以向外部电路提供低电平(逻辑0),但不能直接提供高电平(逻辑1),而是通过外部上拉元件来实现。

        这里画一个图来更好理解这个模式:

 

        就和完美单片机很多引脚一样,默认接了一个上拉电阻,这样的模式可以让我们想要输出低电平和高电平的时候,只要输入一点点的数字逻辑信号,让这个“开关”打开或者关闭,来达到输出1或者0的目的。

        

        还是回到这张图,当我们使用I2C通信多个设备的时候,我们就可以使用开漏输出的模式,比方说我们要控制IC1设备和CPU通信,那么首先设备会通过总线和设备IC1的地址找到IC1(我把这个过程称为配置对象过程)那么找到之后IC1就让IC1输出低电平,这样让总线上的电平变成低电平,那么这个时候就让其他所有的设备啥都不准干,表示我们已经确定了对象为IC1,然后其他的设备就不会再来干扰IC1的通信了。

        于是CPU通过数据线SDA和时钟线SCL输出数据,只要CPU要输出低电平的时候就置为低电平,这个时候总线就被置为低电平,然后我们的IC1就置为低电平了;假设CPU想要输出高电平,只要让CPU不做处理,总线上的电平被开漏输模式的上拉电阻拉高,我们就可以直接让IC1接收到高电平信号。

        这就是大概的流程,可能不太准确但是抽象地反应了开漏输出在I2C这种多设备通信中的作用。

        然后,我们就大概了解了I2C总线的通信方式了。

        其实对于上面的图,它对于我们的I2C总线的描述其实是较为局限的,事实上I2C总线的通信不仅仅局限于CPU和A之间的通信,其实还可以进行A和B,A和C,B和C……只要是两个挂载在总线上的设备都是可以进行相互通信的。

        所以,我们可以把上面的原理图再变更一下:

        然后,我们也知道了每次配置完对象之后,就只有两个设备相互通信,然后就可以把它简化一下:

4.3通过总线收发数据

        首先,I2C使用的是半双工的模式,那么,既然是半双工,半双工相互联通的两个设备之间是可以进行相互连接并相互通信的,现在我们假设有两个设备,分别为A和B,那么我们现在假设我们的主体是A,即我们把A设备当作主机,A设备作为第一人称,对于A设备来说,我们是有接收数据和发送数据两种状态的,下面我们就来解释一下接收和发送数据两种方式的具体操作,当然,下面的所有操作都是默认已经执行了通过地址链接了两部设备,即基于已经完成了对象匹配的过程。

        这里有几个规则概念需要了解一下:

        首先,我们的SCL是时钟线,类似之前了解过的SCLK(其实是一种东西),是通过周期频谱变化产生数据的,其次,在这里我们的SDA即数据传输线也是通过类似时间周期变化的模式来进行收发信号和数据。其实准确来说,SCL更类似于使能的一个触发器,在SCL时钟线变为高电平的时候,就会开始对SDA的数据检测,这个时候就是读入数据的时候,它会读入SDA的数据帧,然后放在缓存器中,直到读取结束。但是当SCL处于低电平的时候,SDA就可以随意变化,且不会影响数据的传输。

        I2C的时序是有规律的,首先要讲的就是收发数据起始条件和终止条件:

        我们规定,SCL在高电平的时候,读入的数据才是有效的,在SCL处于高电平的时候,SDA从高电平变成低电平,则说明数据读取工作的开始;反之,在这段时间中,SDA从低电平变成高电平,则表示数据读取工作的结束。

        当数据读取的开始条件被触发之后,就开始了缓存器对数据的读取了,这里有规定,就是在读取过程中(读取开始后到读取结束前),只要SCL为高电平,SDA就应该在SCL保持高电平的这段时间内保持不变,因为如果我们SDA在SCL持续为高电平的这段时间内发生了变化,那么这个变化就很有可能会被认为是读取开始或者读取结束的信息,这样就会导致数据读取的失误。

        所以,只要我们需要读取数据,在SCL为高电平的时候,SDA不能改变,只能为高电平表示1,低电平表示0,SCL为低电平的时候,SDA再调整高低电平,为下一个数据做准备。

        上面图中的SDA有两根线,表示的是两种不同的情况,正常情况下只会有其中一根SDA存在

        总之,我们就靠这样的规则读取数据,直到数据读取完毕。

        上面我们所说的“读取数据”指的是缓存器读取数据,即上面的理论对于我们的A发给B和B发给A都是适用的,上面的规则是一个通用的规则。

        这里另外说一下,作为数据接收端,我们的主机还要主动释放SDA的操作:

        在I2C总线中,A设备和B设备的SDA和SCL都是分别共用的,所以,假设我们的A设备作为数据接收端,那么数据就由B设备发送,这个时候,B设备想要发送数据都要通过向SDA发送0(表示0)和不发送0(表示1)这两种操作来传达数据。但是,我们操作的主机为A的时候,我们用A接收数据,我们就要用A主动释放SDA,就是我们要让A输出高电平,这样B发送任何数据都不会因为A而受到干扰。

        上面的可能有点不太好理解,我们再从反面理解一下,就是如果我们让A保持为低电平,即用A设备把总线SDA电平拉低,那么不管B设备发送高电平还是低电平,SDA总线都是处于被拉低的状态的,这样A接收到的数据就一直只有低电平0。所以,我们要先让A释放SDA,即让A向SDA发送高电平,之后再让B发送数据,这个时候A才可以接收得到准确的数据。

        这里就总结一下:

        SCL为时钟线,在SCL为高电平期间读取SDA上的数据,SDA在执行开始和结束读写命令的时候才可以在SCL为高电平期间变化,否则只能在SCL为高电平期间为高电平或者低电平两种状态,分别表示数据1和0。

        主机有发送数据和接收数据两种状态,但是两种状态都是由主机下达开始数据读写命令和停止读写命令。在开始数据读写命令下达之后的第一个低电平期间,假设主机为接收数据端,我们就要把主机释放SDA,即让主机对SDA输出高电平,这样才不会对从机发送的数据造成干扰,保证得到准确的数据。

4.4数据应答

        除去由主机下达的开始读写和停止读写两个命令,一个字节是要主机发送或者接收8位数据的,这八位数据刚好组成一个字节,以一个字节为单位,写入完一个字节之后,就累计写了8位,这个时候就还需要紧跟一位作为应答位,0表示应答,1表示非应答,应答位由接收端向发送端发送,同样的,假设我们主机为发送端,在接收应答位之前,主机就要先释放SDA,否则会干扰应答位的数据传输导致错误。当然,假设主机作为接收端,那么应答位就由主机发送(这个时候主机自然就不需要做什么释放SDA的操作了,它只要发送这个应答或者非应答的数据就行了)

        

4.5数据帧(针对AT24C02芯片)

4.5.1基本数据帧

        对于主机而言,一共有发送数据帧和接收数据帧两种状态:

        这里我们发现,在S(即开始)之后,有一个SLAVE ADRESS+R/W的一个字节,这个字节就是我们前面说过的匹配对象的过程同时也确定了主机为接收端还是发送端。

        SLAVE ADRESS+R/W分为前面七个位表示地址和最后一位表示读/写模式:

        在不同的设备中,这里都有不同的含义,这里就主要用我们这次使用的AT24C02芯片为例:

        首先,对于AT24C02芯片而言,前面四位为它的固定位,这四个位是在I2C原厂商为AT24C02制定的固定地址位,为1 0 1 0,不能更改,后面的三位可以仔细看看:

        没错,就是一个东西,当我们一个电路里面有很多个AT24C02芯片的时候,为了避免混淆,我们就可以用这三位给它们编号:001,010,011,100……这样就可以分辨到底是和哪个芯片通信了。

       是的,这一个字节就是地址+主机模式选择位组成

        配置完地址位并确认有应答之后,后面就可以开始发送/接收数据,可以从字节1发到字节n……只要你的内存足够就可以完成

4.5.2更复杂的数据帧(基于AT24C02)

        先发送再接收数据帧,这个数据帧就是上面两种数据帧的简单组合,先主机发送给从机数据,这个数据就是主机向从机索要信息的一段数据,相当于主机向从机诉说要求,然后从机就可以根据主机的要求给主机发送对应的数据,这样就完成了主机得到想要的数据了。

        这里分为两个部分

        第一个部分:

        字节写,即一个精简版的发送数据帧,第一个SLAVE ADRESS仍然是设备地址,第二个WORD ADRESS是字地址,即后面写入DATA在设备中的位置编号,第三个DATA就是写入的数据,且为一个字节。所以叫字节写。

        第二个部分:

        随机读,随机读并不“随机”,其实就是前面的先发送再接收数据帧这个复合格式的缩减版,主要是主机先发送设备地址找到从机,再加上字地址指定读出想要读出的数据。然后指定完数据之后,从机就会发送数据了,这个时候主机就开始接收数据,即接收前面指定的那个数据,然后就可以成功读出这个我们想要的数据了。

5.代码配置

        做完前面的分析,现在我们就可以开始配置代码了,其实主要是把前面的道理搞懂,后面的代码其实是最简单的,学习单片机重要的永远是搞懂原理,而不是代码

5.1I2C配置        

        首先我们来配置I2C内的函数

        前面我们分析的时候,给过这么一张图,这里的基本数据帧主要就由几个部分组成

  • Start起始位
  • Stop停止位
  • Send发送字节(包含开始的配置地址)
  • Receive接收字节
  • RA主机接收从机的应答位
  • SA主机向从机发送应答位

        那么我们就按照这几个分类,来配置一下代码。

        这里我们还要先看一下这个引脚分布,我们要先定义的是SCL和SDA这两个引脚对应的IO口,其实是有IO口的共用的,正好就是LED的IO口,不过问题不大。

        所以根据引脚就有:

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;
	I2C_SCL = 0;
}

        这里只是大概的框架,我们还要看一下需不需要加点时间间隔让我们的CPU反应一下

        我们看到芯片手册里面的这样的数据:

        我们可以发现,这里最短时间限制少于1us,然而我们的单片机最短反应时间足足有1us,所以我们根本不需要担心时间不够的问题,直接使用,不需要延时,单片机完全有时间反应过来。

        然后我们可以写这个发送和接收的函数:

void I2C_SendByte(unsigned char Data)
{
	unsigned char i;
	for(i = 0;i < 8;i++)
	{
		if(Data&(0x80>>i))
		{
			I2C_SDA = 1;
		}
		else
		{
			I2C_SDA = 0;
		}
		I2C_SCL = 1;
		I2C_SCL = 0;
	}
}
unsigned char I2C_ReceiveByte()
{
	unsigned char i,Data = 0x00;
    I2C_SDA = 1;//主机释放SDA
	for(i = 0;i<8;i++)
	{
		I2C_SCL = 1;
		if(I2C_SDA)
		{
			Data |= 0x80>>i;
		}
		I2C_SCL = 0;
	}
	return Data;
}

        然后还有 应答位的接收和发送:

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;//ÊÍ·ÅSDA
	I2C_SCL = 1;
	AckBit = I2C_SDA;
    I2C_SCL = 0;
	return AckBit;
}

5.2AT24C02配置

        配置完成I2C的代码之后,我们就可以基于I2C的代码写AT24C02的发送接收数据帧的代码了,这里主要用字节写和随机读这两个。

        看上面的流程图,我们只要定义函数,再调用我们写的I2C函数就可以了。

#include "I2C.h"

#define AT24C02_ADRESS 0xA0

void AT24C02_SendByte(unsigned char WordAdress,Data)
{
	I2C_Start();
	
	I2C_SendByte(AT24C02_ADRESS);
	I2C_ReceiveAck();
	I2C_SendByte(WordAdress);
	I2C_ReceiveAck();
	I2C_SendByte(Data);
	I2C_ReceiveAck();
	
	I2C_Stop();
}

unsigned char AT24C02_ReceiveByte(unsigned char WordAdress)
{
	unsigned char Data;
	I2C_Start();
	
	I2C_SendByte(AT24C02_ADRESS);
	I2C_ReceiveAck();
	I2C_SendByte(WordAdress);
	I2C_ReceiveAck();
	
	I2C_Start();
	I2C_SendByte(AT24C02_ADRESS|0x01);
	I2C_ReceiveAck();
	Data = I2C_ReceiveByte();
	I2C_SendAck(1);
	
	I2C_Stop();
	return Data;
}

        到了这里,我们基本就可以拿这些函数去使用AT24C02芯片,感受ROM的魅力了。

        但是,还有一点:

 

        我们的写入函数时需要一定的时间的,这个时间在3ms左右,芯片手册里面说了,这个值最大是5ms,即在5ms内,这个芯片才可以完成写入操作,所以,我们就可以把在每次调用这个函数后加上Delay(5),确保数据被完整读入。

        这里是一点示例:

#include <REGX52.H>
#include "LCD1602.h"
#include "Key.h"
#include "AT24C02.h"
#include "Delay.h"
void main()
{
	unsigned char testnum = 1;
	LCD_Init();
	while(1)
	{
		unsigned keynum = KEY();
		if(keynum==1)
		{
			testnum --;
		}
		else if(keynum == 2)
		{
			testnum ++;
		}
		else if(keynum == 3)
		{
			AT24C02_WriteByte(0,testnum);
			Delay(5);
			LCD_ShowString(2,1,"Write OK");
		}
		else if(keynum == 4)
		{
			testnum = AT24C02_ReadByte(0);
			LCD_ShowString(2,1,"Read  OK");
		}
		LCD_ShowNum(1,1,testnum,3);
	}
}

        这段代码配合LCD显示还有独立按键,我们完成了一个断电不丢失的计数器,按Key1数字-1,按Key2数字+1,按Key3写入数据,按Key4读出数据。

6.代码总览

6.1main.c

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

void main()
{
	unsigned char testnum = 1;
	LCD_Init();
	while(1)
	{
		unsigned keynum = KEY();
		if(keynum==1)
		{
			testnum --;
		}
		else if(keynum == 2)
		{
			testnum ++;
		}
		else if(keynum == 3)
		{
			
			AT24C02_WriteByte(0,testnum);
			Delay(5);
			LCD_ShowString(2,1,"Write OK");
		}
		else if(keynum == 4)
		{
			testnum = AT24C02_ReadByte(0);
			LCD_ShowString(2,1,"Read  OK");
		}
		LCD_ShowNum(1,1,testnum,3);
	}
}



6.2 Key

6.2.1Key.h

#ifndef __KEY_H__
#define __KEY_H__

unsigned char KEY();

#endif

6.2.2Key.c

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

sbit KEY1 = P3^1;
sbit KEY2 = P3^0;
sbit KEY3 = P3^2;
sbit KEY4 = P3^3;

unsigned char KEY()
{
	unsigned char KeyNum = 0;
		if(KEY1 == 0)
		{
			Delay(20);
			while(KEY1 == 0);
			Delay(20);
			KeyNum = 1;
		}
		else if(KEY2 == 0)
		{
			Delay(20);
			while(KEY2 == 0);
			Delay(20);
			KeyNum = 2;
		}
		else if(KEY3 == 0)
		{
			Delay(20);
			while(KEY3 == 0);
			Delay(20);
			KeyNum = 3;
		}
		else if(KEY4 == 0)
		{
			Delay(20);
			while(KEY4 == 0);
			Delay(20);
			KeyNum = 4;
		}
		return KeyNum;
}

6.3LCD1602

6.3.1LCD1602.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

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

6.4Delay

6.4.1Delay.h

#ifndef __DELAY_H__
#define __DELAY_H__

void Delay(unsigned int xms);

#endif

6.4.2Delay.c

#include <REGX52.H>
#include <INTRINS.H>

void Delay(unsigned int xms)		//@11.0592MHz
{
	while(xms--)
	{
		unsigned char i, j;
		_nop_();
		i = 2;
		j = 199;
		do
		{
			while (--j);
		} while (--i);
	}
}

6.5I2C

6.5.1I2C.h

#ifndef __I2C_H__
#define __I2C_H__

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

#endif

6.5.2I2C.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;
	I2C_SCL = 0;
}

void I2C_SendByte(unsigned char Data)
{
	unsigned char i;
	for(i = 0;i < 8;i++)
	{
//		if(Data&(0x80>>i))
//		{
//			I2C_SDA = 1;
//		}
//		else
//		{
//			I2C_SDA = 0;
//		}
		I2C_SDA = Data&(0x80>>i);
		I2C_SCL = 1;
		I2C_SCL = 0;
	}
}

unsigned char I2C_ReceiveByte()
{
	unsigned char i,Data = 0x00;
	I2C_SDA = 1;//ˍ؅SDA
	for(i = 0;i<8;i++)
	{
		I2C_SCL = 1;
		if(I2C_SDA)
		{
			Data |= (0x80>>i);
		}
		I2C_SCL = 0;
	}
	return Data;
}

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;//ˍ؅SDA
	I2C_SCL = 1;
	AckBit = I2C_SDA;
	I2C_SCL = 0;
	return AckBit;
}

6.6AT24C02

6.6.1AT24C02.h

#ifndef __AT24C02_H__
#define __AT24C02_H__

void AT24C02_WriteByte(unsigned char WordAdress,Data);
unsigned char AT24C02_ReadByte(unsigned char WordAdress);

#endif

6.6.2AT24C02.c

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

#define AT24C02_ADRESS 0xA0

void AT24C02_WriteByte(unsigned char WordAdress,unsigned char Data)
{
	unsigned char Ack;
	I2C_Start();
	
	I2C_SendByte(AT24C02_ADRESS);
	I2C_ReceiveAck();
	I2C_SendByte(WordAdress);
	I2C_ReceiveAck();
	I2C_SendByte(Data);
	I2C_ReceiveAck();
	
	I2C_Stop();
}

unsigned char AT24C02_ReadByte(unsigned char WordAdress)
{
	unsigned char Data;
	I2C_Start();
	
	I2C_SendByte(AT24C02_ADRESS);
	I2C_ReceiveAck();
	I2C_SendByte(WordAdress);
	I2C_ReceiveAck();
	
	I2C_Start();
	I2C_SendByte(AT24C02_ADRESS|0x01);
	I2C_ReceiveAck();
	Data = I2C_ReceiveByte();
	I2C_SendAck(1);
	
	I2C_Stop();
	return Data;
}

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐