51单片机——I2C-EEPROM实验,小白讲解,相互学习

I2C介绍

        I2C(Inter-Integrated Circuit)总线是由 PHILIPS 公司开发的两线式 串行总线,用于连接微控制器及其外围设备。是微电子通信控制领域广泛采用的 一种总线标准。它是同步通信的一种特殊形式,具有接口线少,控制方式简单, 器件封装形式小,通信速率较高等优点。I2C 总线只有两根双向信号线。一根是 数据线 SDA,另一根是时钟线 SCL。由于其管脚少,硬件实现简单,可扩展性强 等特点,因此被广泛的使用在各大集成芯片内。下面我们就从 I2C 的物理层与 协议层来了解 I2C。

I2C物理层

I2C通信设备常用的连接方式如下:

它的物理层有如下特点:

(1)它是一个支持多设备的总线。“总线”指多个设备共用的信号线。在一个I2C通信总线中,可连接多个I2C通讯设备,支持多个通讯主机及多个通讯从机。

(2)一个 I2C 总线只使用两条总线线路,一条双向串行数据线(SDA),一 条串行时钟线(SCL)。数据线即用来表示数据,时钟线用于数据收发同步。

(3)每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址 进行不同设备之间的访问。

(4)总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而 当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。

(5)多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定 由哪个设备占用总线。

 (6)具有三种传输模式:标准模式传输速率为 100kbit/s,快速模式为 400kbit/s,高速模式下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模 式。

(7)连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制。
下面我们来了解下 I2C 总线常用的一些术语:
主机:启动数据传送并产生时钟信号的设备;
从机:被主机寻址的器件;
多主机:同时有多于一个主机尝试控制总线但不破坏传输;
主模式:用 I2CNDAT 支持自动字节计数的模式; 位 I2CRM,I2CSTT,I2CSTP 控制数据的接收和发送;
从模式:发送和接收操作都是由 I2C 模块自动控制的;
仲裁:是一个在有多个主机同时尝试控制总线但只允许其中一个控制总线并 使传输不被破坏的过程;
同步:两个或多个器件同步时钟信号的过程;
发送器:发送数据到总线的器件;
接收器:从总线接收数据的器件。
I2C协议层
        I2C 的协议定义了通信的起始和停止信号、数据有效性、响应、仲裁、时钟 同步和地址广播等环节。下面我们就来简单介绍下。
1)数据有效性规定
        I2C 总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保 持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态 才允许变化。如下图:
每次数据传输都以字节为单位,每次传输的字节数不受限制。
2)起始和停止信号
        SCL 线为高电平期间,SDA 线由高电平向低电平的变化表示起始信号;SCL 线为高电平期间,SDA 线由低电平向高电平的变化表示终止信号。如下图:
        起始和终止信号都是由主机发出的,在起始信号产生后,总线就处于被占用 的状态;在终止信号产生后,总线就处于空闲状态。

 3)应答响应

每当发送器件传输完一个字节的数据后,后面必须紧跟一个校验位,这个校 验位是接收端通过控制 SDA(数据线)来实现的,以提醒发送端数据我这边已经 接收完成,数据传送可以继续进行。这个校验位其实就是数据或地址传输过程中 的响应。响应包括“应答(ACK)”和“非应答(NACK)”两种信号。作为数据接收 端时,当设备(无论主从机)接收到 I2C 传输的一个字节数据或地址后,若希望 对方继续发送数据,则需要向对方发送“应答(ACK)”信号即特定的低电平脉冲, 发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非 应答(NACK)”信号即特定的高电平脉冲,发送方接收到该信号后会产生一个停止 信号,结束信号传输。应答响应时序图如下:

每一个字节必须保证是 8 位长度。 数据传送时,先传送最高位( MSB),每 一个被传送的字节后面都必须跟随一位应答位(即一帧共有 9 位)
        由于某种原因从机不对主机寻址信号应答时(如从机正在进行实时性的处理
工作而无法接收总线上的数据),它必须将数据线置于高电平,而由主机产生一个终止信号以结束总线的数据传送。
        如果从机对主机进行了应答,但在数据传送一段时间后无法继续接收更多的 数据时,从机可以通过对无法接收的第一个数据字节的“非应答”通知主机,主 机则应发出终止信号以结束数据的继续传送。
        当主机接收数据时,它收到最后一个数据字节后,必须向从机发出一个结束 传送的信号。这个信号是由对从机的“非应答”来实现的。然后,从机释放 SDA 线,以允许主机产生终止信号。
这些信号中,起始信号是必需的,结束信号和应答信号都可以不要。
 
4)总线的寻址方式
        I2C 总线寻址按照从机地址位数可分为两种,一种是 7 位,另一种是 10 位。采用 7 位的寻址字节(寻址字节是起始信号后的第一个字节)的位定义如 下:
        D7~D1 位组成从机的地址。D0 位是数据传送方向位,为“ 0”时表示主机 向从机写数据,为“1”时表示主机由从机读数据。
        10 位寻址和 7 位寻址兼容,而且可以结合使用。10 位寻址不会影响已有 的 7 位寻址,有 7 位和 10 位地址的器件可以连接到相同的 I2C 总线。我们就以 7 位寻址为例进行介绍。
        当主机发送了一个地址后,总线上的每个器件都将头 7 位与它自己的地址比较,如果一样器件会判定它被主机寻址,其他地址不同的器件将被忽略后面 的数据信号。至于是从机接收器还是从机发送器,都由 R/W 位决定的。从机的地址由固定部分和可编程部分组成。在一个系统中可能希望接入多个相同的从 机,从机地址中可编程部分决定了可接入总线该类器件的最大数目。如一个从机 的 7 位寻址位有 4 位是固定位,3 位是可编程位,这时仅能寻址8个同样的 器件,即可以有 8 个同样的器件接入到该 I2C 总线系统中。
5)数据传输
        I2C 总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据 信号。在起始信号后必须传送一个从机的地址(7 位),第 8 位是数据的传送方向位(R/W),用“ 0”表示主机发送(写)数据(W),“ 1”表示主机接收数据(R)。每次数据传送总是由主机产生的终止信号结束。但是,若主机希望继续占用总线进行新的数据传送,则可以不产生终止信号,马上再次发出起始信号对另一从机进行寻址。
        在总线的一次数据传送过程中,可以有以下几种组合方式:
        a、主机向从机发送数据,数据传送方向在整个传送过程中不变
        注意:有阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从 机向主机传送。A 表示应答,A 非表示非应答(高电平)。S 表示起始信号,P 表 示终止信号。

         b、主机在第一个字节后,立即从从机读数据

        c、在传送过程中,当需要改变传送方向时,起始信号和从机地址都被重复 产生一次,但两次读/写方向位正好相反

        到这里我们就介绍完 I2C 总线,由于 51 单片机没有硬件 IIC 接口,即使有硬件接口我们通常还是采用软件模拟 I2C。主要原因是硬件 IIC 设计的比较复 杂,而且稳定性不怎么好,程序移植比较麻烦,而用软件模拟 IIC,最大的好处 就是移植方便,同一个代码兼容所有单片机,任何一个单片机只要有 IO 口(不 需要特定 IO),都可以很快的移植过去。
AT24C02 介绍
 AT24C01/02/04/08/16..是一个 1K/2K/4K/8K/16K 位串行 CMOS,内部含有128/256/512/1024/2048 个 8 位字节,AT24C01 有一个 8 字节页写缓冲器, AT24C02/04/08/16 有一个 16 字节页写缓冲器。该器件通过 I2C 总线接口进行 操作,它有一个专门的写保护功能。我们开发板上使用的是 AT24C02(EEPROM)芯片,此芯片具有 I2C 通信接口,芯片内保存的数据在掉电情况下都不丢失,所以通常用于存放一些比较重要的数据等。AT24C02 芯片管脚及外观图如下图所示:
芯片管脚说明如下图所示:
        AT24C02 器件地址为 7 位,高 4 位固定为 1010,低 3 位由 A0/A1/A2 信 号线的电平决定。 因为传输地址或数据是以字节为单位传送的,当传送地址时, 器件地址占 7 位,还有最后一位(最低位 R/W)用来选择读写方向,它与地址无关。其格式如下:
        我们开发板已经将芯片的 A0/A1/A2 连接到 GND,所以器件地址为 1010000,即 0x50(未计算最低位)。如果要对芯片进行写操作时,R/W 即为 0, 写器件地址即为 0XA0;如果要对芯片进行读操作时,R/W 即为 1,此时读器件 地址为 0XA1。开发板上也将 WP 引脚直接接在 GND 上,此时芯片允许数据正常 读写。
        I2C 总线时序如下图所示:

 

硬件设计 

本实验使用到硬件资源如下:
(1)独立按键(K1-K4)
(2)动态数码管
(3)EEPROM 模块电路(AT24C02)
        独立按键和动态数码管电路在前面章节都介绍过,这里就不再重复。下面我 们来看下开发板上 EEPROM 模块电路,如下图所示:
        从图中可以看出,芯片的 SCL 和 SDA 管脚是连接在单片机的 P2.1 和 P2.0 上, 在介绍 IIC 总线的时候我们说过,为了让 IIC 总线默认为高电平,通常会在 IIC 总线上接上拉电阻,在图中并没有看到 SCL 和 SDA 管脚有上拉电阻,这是因为开 发板单片机 IO 都外接了 10K 上拉电阻,当单片机 IO 口连接到芯片的 SCL 和 SDA 脚时即相当于它们外接上拉电阻,所以此处可以省去。
软件设计
1,创建多文件工程
在电脑上创建一个实验文件夹,为了与教程配套,这里命名为“I2C-EEPROM 实验”,然后在该文件夹内新建 App、Obj、Public、User 四个文件夹(注:文件命名不能用中文),如下所 示:

App 文件夹: 用于存放外设驱动文件,如 LED、数码管、定时器等。
Obj 文件夹: 用于存放编译产生的 c/汇编/链接的列表清单、调试信息、 hex 文件、预览信息、封装库等文件。
Public 文件夹: 用于存放 51 单片机公共的文件,如延时、51 头文件、变量 类型重定义等。
User 文件夹: 用于存放用户主函数文件,如 main.c。
        为何要定义这么多文件夹呢?为何不按照之前实验那样直接在 main.c 文件 里面写代码?我们先来分析本章实验所需用到哪些功能程序,要用到独立按键、 数码管、EEPROM,所以需要将前面实验编写的独立按键、数码管、延时等程序全 部放到 main.c 中来,此时在不增加 EEPROM 驱动程序时,main.c 文件中的代码量已经非常冗长,极其不便于程序的阅读、移植和维护,因此可用多文件管理的 办法来使工程易于阅读、移植和维护。操作方法如下:
2,新建工程
        打开 KEILC51 软件,新建一个工程。将工程命名为 template 并保存在“I2C-EEPROM 实验”文件夹下,然 后选择芯片类型为“AT89C52”,不使用系统创建启动文件,如下所示:
3,向工程添加文件

         按照需要给工程分组并添加对应文件,这里我们在工程中分 3 组,User、App、 Publi,至于前面创建的 Obj 文件夹是在工程中无需体现,因为只是编译器生成 的一些中间文件和.hex 执行文件。通常在工程组的命名与创建的文件夹名保持 一致,方便查找到源文件位置。如下所示:

注:这个添加User、App、 Publi这三个文件名要和前面创建的这三个文件名一致,如果文件名写错是不会直接关联到前面创建的文件

添加成功后,在工程中就会出现刚才的分组列表,如下所示:

在 App 文件夹中根据不同外设分别创建了24c02,iic,key,smg四个文件夹保存各自驱动文件,这样非常方便程序移植和维护,如下图所示

然后就是给每个组添加对应的.c 源文件。

3.1,给User组添加main.c文件,如下图所示:

 将main.c文件添加到工程的User组中,如下所示

 

 可以选择到要添加的.c 文件(红色标记 5),然后鼠标左键双击也可直接添 加进去,就免去了点击红色标记 6 这步。添加好后,在右侧就会显示对应组中已 添加的文件,如下所示:

添加成功后左边的User组下面就会显示mian.c文件,如下所示:

3.2,同样的方法,将 App、Public 工程组中文件也添加进去。如下所示:
此时可以看到,在工程栏下每组都已经加入了刚才添加的源文件,如下所示:
3.3,除了User的main源文件不用添加头文件(.h),在APP,Public里面都要添加头文件,如下所示:
在Public文件中就会显示头文件public.h文件。
同上在APP中的文件中 24c02,iic,key,smg四个文件中添加对应的头文件如下所示
如果有遗漏的文件未添加进来,可按照上述方法重新添加。
4,配置魔术棒选项卡
        这一步的配置工作非常重要,很多人编写完程序编译后发现找不到 HEX 文 件,还有的人直接编译前面添加好文件的工程出现报错,这些问题都是在这个地 方没有配置好导致的。
(1)Output 选项卡中把输出文件夹定位到我们实验目录下的 Obj 文件夹,那么 Create HEX File 选项勾上。配置如下:
(2)Listing 选项卡中把输出文件夹也定位到我们实验目录下的 Obj 文件 夹。其它设置默认,配置如下:
(3)C51 选项卡配置,此处目的是将我们前面添加到工程组中的文件路径包括进来,否则程序中调用其他文件夹的头文件则会报错找不到头文件路径,具体步骤如下:
        添加的头文件路径是指,在 I2C-EEPROM 实验文件夹下里面,哪些文件夹内 含有.h 头文件,并且需要被调用到的,通常我们会把只要含有头文件的文件夹 都选择进去。比如本例程中 App 内含有很多子文件夹,它们里面都含有头文件, 因此要分别添加,Public 也含有头文件,所以也要添加。添加完成后如下:
               
实验代码
        本章所要实现的功能是:系统运行时,数码管右 3 位显示 0,按 K1 键将数据 写入到 EEPROM 内保存,按 K2 键读取 EEPROM 内保存的数据,按 K3 键显示数据加1,按 K4 键显示数据清零,最大能写入的数据是 255。
程序框架如下:
(1)编写按键检测功能
(2)编写数码管显示功能
(3)编写 IIC 驱动,包括起始、停止、应答信号等
(4)编写 AT24C02 读写功能
(5)编写主函数
1,Public 文件夹: 用于存放 51 单片机公共的文件,如延时、51 头文件、变量 类型重定义等。
        我们先定义公共文件夹Public文件下的public.c和public.h头文件。
   public.h文件代码如下:
#ifndef _public_H
//ifndef是C语言条件的编译,意思若_public_H没有被定义,则从“#ifndef”至“ 
//#endif”关键字之间的内容都有效,也就是说,这个头文件若被其它文件“#include”,
//它就会被包含到其该文件中,且头文件中紧接着使用“#define”关键字定义上面判断的标号“_public_H”。

#define _public_H

#include "reg52.h"	
//编译的是51,首先定义reg52头文件,调用reg52.h,这样在别的.C文件里面就不用
//在调用 include "reg52.h",直接调用 #include "public.h"文件就可以


typedef unsigned int u16;	//对系统默认数据类型进行重定义
typedef unsigned char u8;


void delay_10us(u16 ten_us);//定义延时函数 ,当传入ten_us=1时,大约是1us 
void delay_ms(u16 ms);	// 当传入ms=1时,大约是1ms ,


#endif

 public.c文件代码如下:

#include "public.h"


void delay_10us(u16 ten_us)	//当传入Ten_us=1时,大约延时10us
{
	while(ten_us--);	
}

/********************************************************************
***********
* 函 数 名 : delay_ms
* 函数功能 : ms 延时函数,ms=1 时,大约延时 1ms
* 输 入 : ten_us
* 输 出 : 无
*********************************************************************
**********/
void delay_ms(u16 ms)
{
	u16 i,j;
	for(i=ms;i>0;i--)
		for(j=110;j>0;j--);
}

2,App 文件夹:用于存放外设驱动文件,如 LED、数码管、定时器等。

        2.1,Key 文件夹按键检测函数

        key.h文件代码如下:

#ifndef _key_H
#define _key_H

#include "public.h"

//定义独立按键控制脚
sbit KEY1=P3^1;
sbit KEY2=P3^0;
sbit KEY3=P3^2;
sbit KEY4=P3^3;


//使用宏定义独立按键按下的键值
#define KEY1_PRESS	1
#define KEY2_PRESS	2
#define KEY3_PRESS	3
#define KEY4_PRESS	4
#define KEY_UNPRESS	0


u8 key_scan(u8 mode);

#endif

 key.c文件代码如下:

#include "key.h"
/********************************************************************
***********
* 函 数 名 : key_scan
* 函数功能 : 检测独立按键是否按下,按下则返回对应键值
* 输 入 : mode=0:单次扫描按键
mode=1:连续扫描按键
* 输 出 : KEY1_PRESS:K1 按下
KEY2_PRESS:K2 按下
KEY3_PRESS:K3 按下
KEY4_PRESS:K4 按下
KEY_UNPRESS:未有按键按下
*********************************************************************
**********/
u8 key_scan(u8 mode)
	{
		static u8 key=1;
		if(mode)key=1;//连续扫描按键
		if(key==1&&(KEY1==0||KEY2==0||KEY3==0||KEY4==0))//任意按键按下
	{
	delay_10us(1000);//消抖
	key=0;
	if(KEY1==0)
		return KEY1_PRESS;
	else if(KEY2==0)
		return KEY2_PRESS;
	else if(KEY3==0)
		return KEY3_PRESS;
	else if(KEY4==0)
		return KEY4_PRESS;
	}
	else if(KEY1==1&&KEY2==1&&KEY3==1&&KEY4==1) //无按键按下
	{
	key=1;
	}
	return KEY_UNPRESS;
}
.c .h 的格式及含义
通常.c 源文件开始会调用与之匹配的.h 头文件,这样在.c 源文件中即可使 用.h 头文件的内容。与 key.c 对应的头文件是 key.h,一般我们以文件形式存放 对应功能的驱动程序时,会创建 2 个文件,一个是.c 源文件,另一个是.h 头文 件。源文件通常存放的是外设的驱动程序,比如按键检测函数;而头文件通常用 来存放管脚定义、变量声明、函数声明等。
我们可以看下 key.h 和public.h头文件,头文件.h开始和结束位置 有如下这种结构:

#ifndef _key_H

#define _key_H

............................(此处省略头文件定义的内容)
#endif

        

        它的功能是防止头文件被重复包含,避免引起编译错误。在头文件的开头, 使用“ #ifndef”关键字,判断标号“_key_H”是否被定义,若没有被定义,则 从“#ifndef”至“ #endif”关键字之间的内容都有效,也就是说,这个头文件 若被其它文件“#include”,它就会被包含到其该文件中,且头文件中紧接着使用“#define”关键字定义上面判断的标号“ _key_H”。当这个头文件被同一个文件第二次“#include”包含的时候,由于有了第一次包含中的“#define _key_H” 定义,这时再判断“#ifndef _led_H”,判断的结果就是假了,从“#ifndef” 至“#endif”之间的内容都无效,从而防止了同一个头文件被包含多次,编译时 就不会出现“redefine(重复定义)”的错误了。
        一般来说,我们不会直接在 C 的源文件写两个“#include”来包含同一个头 文件,但可能因为头文件内部的包含导致重复,这种代码主要是避免这样的问题。 如“key.h”文件中调用了#include “
public.h”头文件,可能我们写主程序的 时候会在 main 文件开始处调用#include “public.h”“key.h”,这个时候 “public.h”文件就被包含两次了,如果在头文件中没有这种机制,编译器就会报错。key.h 头文件中包含的 public.h 头文件是我们定义的,存放在 Public 文件 夹中,在前面也介绍其作用,主要用于存放 51 单片机开发中共用的一些程序或 变量类型的重定义等,比如延时函数,u16、u8 类型的定义等。其中最关键的是 包含了#include “reg52.h”,只要调用 public.h 即可包含 reg52.h。
     2.2,smg文件夹数码管显示函数
smg.h文件代码如下:
#ifndef _smg_H
#define _smg_H

#include "public.h"

#define SMG_A_DP_PORT	P0	//使用宏定义数码管段码口

//定义数码管位选信号控制脚
sbit LSA=P2^2;
sbit LSB=P2^3;
sbit LSC=P2^4;

#endif
smg.c文件代码如下:
#include "smg.h"

//共阴极数码管显示0~F的段码数据
u8 gsmg_code[17]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,
				0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};

void smg_display(u8 dat[],u8 pos)
{
	u8 i=0;
	u8 pos_temp=pos-1;/pos是1开始计数,i是从0开始 ,为了让pos和i一样,所以pos-1

	for(i=pos_temp;i<8;i++)
	{
	   	switch(i)//位选
		{

			case 0: LSC=1;LSB=1;LSA=1;break;
			case 1: LSC=1;LSB=1;LSA=0;break;
			case 2: LSC=1;LSB=0;LSA=1;break;
			case 3: LSC=1;LSB=0;LSA=0;break;
			case 4: LSC=0;LSB=1;LSA=1;break;
			case 5: LSC=0;LSB=1;LSA=0;break;
			case 6: LSC=0;LSB=0;LSA=1;break;
			case 7: LSC=0;LSB=0;LSA=0;break;
		}
		SMG_A_DP_PORT=gsmg_code[dat[i-pos_temp]];//传送段选数据
		delay_10us(100);//延时一段时间,等待显示稳定
		SMG_A_DP_PORT=0x00;//消影,为了消除下个数码管有效赋值的段码数据对上个数码管的影响
	}
}
该代码也很好理解,在前面动态数码管实验章节已介绍。不同的是,此处我 们做了一些改良,即给 smg_display 函数增加了 2 个形参,dat 和 pos,dat 是 一个 u8 类型的数组,方便外部传入要显示的数据;pos 是数码管从左开始第几 个位置开始显示,取值范围是 1-8,如果为 1,即从第一位数码管开始显示,最 多显示 8 位数据;如果取 2,即从第二位数码管开始显示,最多显示 7 位数据。
由于给函数增加了 2 个形参,所以该条语句就变为如下形式:
SMG_A_DP_PORT=gsmg_code[dat[i-pos_temp]];
        当确定好显示位置,pos_temp 值就确定,如果要显示的数据也确定,dat 数 组内的值就直接可以通过数码管段选数据赋值给 SMG_A_DP_PORT。
有些朋友可能看不懂为什么 dat[i-pos_temp]含义,因为动态扫描,for 循 环从 i 起始值开始,而一般外部传入的数组 dat 是从下标 0 开始,所以为 i-pos_temp。
        2.3 iiC文件夹I2C 读写字节函数
iic.h文件代码如下:
#ifndef _iic_H
#define _iic_H

#include "public.h"

sbit IIC_SCL=P2^1;//定义SCL	控制管脚
sbit IIC_SDA=P2^0;// 定义SDA控制管脚

void iic_start(void); //产生IIC起始信号
void iic_stop(void);  //产生 IIC 停止信号
void iic_ack(void);	//产生 ACK 应答
void iic_nack(void);//产生 NACK 非应答
u8 iic_wait_ack(void); //等待应答信号到来
void iic_write_byte(u8 dat);//IIC 发送一个字节
u8 iic_read_byte(u8 ack); //IIC 读一个字节


#endif

iic.c文件代码如下:

#include "iic.h"

/*******************************************************************************
* 函 数 名       : iic_start
* 函数功能		 : 产生IIC起始信号
* 输    入       : 无
* 输    出    	 : 无
*******************************************************************************/

void iic_start(void)  //这些起始停止信号都是没有返回值的,所以都是void类型
{
   IIC_SCL=1;//当IIC_SCL为高电平的时候
   IIC_SDA=1;
   delay_10us(1);//当传入ten_us=1时,大约是10us
   IIC_SDA=0;  //	当IIC_SCL为高电平的时候, IIC_SDA由高变为低电平这是
               //一个起始信号
   delay_10us(1);//按两个延时是根据我们这个时序,从高往低的时间,为了兼容我们的程序,延时适
                 //当放宽,延时10us,
   IIC_SCL=0;  //  IIC_SCL为低电平是钳住 I2C总线,也就是说总线处于一个工作的状态,准备发送 
               //或接收数据

}

/********************************************************************
***********
* 函 数 名 : iic_stop
* 函数功能 : 产生 IIC 停止信号
* 输 入 : 无
* 输 出 : 无
*********************************************************************
**********/
void iic_stop(void)
{
   IIC_SCL=1;//当IIC_SCL为高电平的时候
   IIC_SDA=0;
   delay_10us(1);//当传入ten_us=1时,大约是10us
   IIC_SDA=1;  //	 当IIC_SCL为高电平的时候,IIC_SDA由低变为高电平这是一个终止信号

}
/********************************************************************
***********
* 函 数 名 : iic_ack
* 函数功能 : 产生 ACK 应答
* 输 入 : 无
* 输 出 : 无
*********************************************************************
**********/
void iic_ack(void)
{
	IIC_SCL=0;//当SCL为低电平时候,SDA的数据线是可以更改的
	IIC_SDA=0; //SDA 为低电平 是应答信号
	delay_10us(1);
	IIC_SCL=1; //当SCL为高电平的时候,SDA的数据是稳定,从机就可以读取这个稳定的信号 
	delay_10us(1);
	IIC_SCL=0;	//SCL为低电平,等待下一次主机数据的发送
}
/********************************************************************
***********
* 函 数 名 : iic_nack
* 函数功能 : 产生 NACK 非应答
* 输 入 : 无
* 输 出 : 无
*********************************************************************
**********/
void iic_nack(void)
{
	IIC_SCL=0; //当SCL为低电平时候,SDA的数据线是可以更改的
	IIC_SDA=1; //SDA 为高电平  的时候是非应答
	delay_10us(1);
	IIC_SCL=1;
	delay_10us(1);
	IIC_SCL=0;
}
/********************************************************************
***********
* 函 数 名 : iic_wait_ack
* 函数功能 : 等待应答信号到来
* 输 入 : 无
* 输 出 : 1,接收应答失败
0,接收应答成功
*********************************************************************
**********/
u8 iic_wait_ack(void)
{
	u8 time_temp=0;
	IIC_SCL=1;//要读取电平SCL为高电平的时候,数据是稳定的,第一步先让SCL=1,SCL设置位高电平
	delay_10us(1); //高电平的时候我们都要给一些延时,延时以后在开始读取
	while(IIC_SDA) //读取用while循环,直到等到我们要求的信号,等待 SDA 为低电平
	{
		time_temp++; //如果IIC_SDA为高电平,则不停的在里面循环,这里设置一个时间time_temp
		if(time_temp>100)//当time_temp>100时候,超时则强制结束 IIC 通信
		{
		iic_stop();	 //IIC_SDA=1一直为高电平,就是非应答,超时则直接终止停止信号,
                     //如果SDA=0,则不会进入执行iic_stop()
		return 1;  //如果IIC_SDA=1就终止信号,就返回1,返回1表示的是非应答
		}
	}
	IIC_SCL=0; //如果SDA等于0,是应答信号,则不会进入执行iic_stop(),为假,跳出while循环,
               //让IIC_SCL=0,可以进行数据操作
	return 0; //如果IIC_SDA=0就终止信号,就返回0,返回0表示的是应答,不会停止,继续发送
}
/********************************************************************
***********
* 函 数 名 : iic_write_byte
* 函数功能 : IIC 发送一个字节
* 输 入 : dat:发送一个字节
* 输 出 : 无
*********************************************************************
**********/
void iic_write_byte(u8 dat)	//写入没有返回值,就是void类型,写数据要传入参数,定义是U8类型的dat
{
	u8 i=0;
	IIC_SCL=0;//SCL为低电电平的时候,数据是可以更改的,
			  //所以SDA的数据可以变化,当 SCL为高电平的时候,数据是稳定的,可以发送出去
			  //要写入数据,所以要先让 SCL=0为低电平
	for(i=0;i<8;i++) //循环 8 次将一个字节传出,先传高再传低位一位一位的发送
	                 //因为dat是一个字节是8位,要把每个位移到8位,所以这里循环8次
	{
		if((dat&0x80)>0)//这用一个与(&)运算,都为真是真,如果dat的高位是1,则大于0,
                        //IIC_SDA=1,否则IIC_SDA=0
			IIC_SDA=1;
		else
			IIC_SDA=0;
		dat<<=1;//如果传送下一位的时候,在if((dat&0x80)>0)始终对比的是最高位,如果要传送
                //dat=10010001,第一次对比最高位1,传入了1,次高位0
		       //就要让0 变成最高位,要变成最高位,就要dat想左移一位,dat<<=1,dat=10010001
               //变成dat=00100010,因为if((dat&0x80)>0)始终对比的是最高位,
		      //是一位一位的传,在if((dat&0x80)>0)里面dat=00000000为高位是0则IIC_SDA=0
		
		delay_10us(1);
		IIC_SCL=1;//当写入一位数据后,SCL为高电平数据是稳定的,可以发送出去
		delay_10us(1);
		IIC_SCL=0;	//SCL=0SCL为低电电平的时候,数据是可以更改的,继续写入下一位数据
		delay_10us(1);
	}
}
/********************************************************************
***********
* 函 数 名 : iic_read_byte
* 函数功能 : IIC 读一个字节
* 输 入 : ack=1 时,发送 ACK,ack=0,发送 nACK
* 输 出 : 应答或非应答
*********************************************************************
**********/
u8 iic_read_byte(u8 ack)//读取的时候是有返回值的,所以定义各个U8类型的函数
                       //要读取的从机的数据之后,是否要继续读取,主机发送一个ack,nACK ,
                       //这里设置一个入口参数u8 ack的变量,读取之后是要继续读取,还是停止,
                       //通过ack变量来设置ack=1 时,发送 ACK,ack=0,发送 nACK
{
	u8 i=0;
	u8 receive=0;//读取数据后要保存,这里设置receive变量来保存读取的是数据,赋予初值0
	for(i=0;i<8;i++ ) //读取也是一位一位地读,一个字节8为,所以这里循环 8 次将一个字节读出,
                     //先读高再传低位
	{
		IIC_SCL=0; //SCL为低电电平的时候,数据是可以更改的
		delay_10us(1);
		IIC_SCL=1; //在SCL为高电平,数据是稳定的,数据不会改变,开始读取数据
		receive<<=1; //	for(i=0;i<8;i++ )是先读高再传低位,读取的高位,保存的是在receive是
                     //低位,所以要把每次读取的数据左移这样就把第一次读取保存在receive 的数
                    //据,移到最高位
		if(IIC_SDA)receive++; //怎么读取?就判断IIC_SDA这个管脚的电平,如果IIC_SDA=1就是1,
 //如果IIC_SDA=0就是0 ,不执行if语句receive++不会加如果我们要读取的数据是11000000,读取数据也
//是先读高位,那么第一次读取receive=1,如果在读次高位1的时候,receive++=2,2换算8位16进制=11,
//是不对的,因为前两位是11,receive++是最低位加1,所以我们在下去次读取的时候,对receive<<=1,
//在就是10,第二次receive++就变成receive++=11,依次读取8位数据

		delay_10us(1);
	}
	if (!ack)  //这里的意思,规定了0就是 nack,1就是ack
		iic_nack();
	else
		iic_ack();
	return receive;	//读取的数据保存在receive里面返回出去
}
	

        2.3 24c02文件夹AT24C02 读写字节函数

24c02.h文件代码如下;

#ifndef _24c02_H
#define _24c02_H
#include "public.h"
void at24c02_write_one_byte(u8 addr,u8 dat);

u8 at24c02_read_one_byte(u8 addr);	

#endif

24c02.c文件代码如下;

#include "24c02.h"
#include "iic.h"	
/********************************************************************
***********
* 函 数 名 : at24c02_write_one_byte
* 函数功能 : 在 AT24CXX 指定地址写入一个数据
* 输 入 : addr:写入数据的目的地址
dat:要写入的数据
* 输 出 : 无
*********************************************************************
**********/
void at24c02_write_one_byte(u8 addr,u8 dat)	 //写入每个数据都要制定一个地址,这个函数要指
                                             //定一个地址ddr,写入数据dat
{
	iic_start();  //产生IIC起始信号
	iic_write_byte(0XA0); //发送写命令,根据AT24C02器件地址,0XA0是写入地址传入的参数,
                          //0XA1是读取地址传入的参数
	iic_wait_ack();//等待应答信号到来,这是从机发的,要等待应答应答信号,如果从机返回的是
                   //应答信号,主机则继续写入数据
	iic_write_byte(addr);//发送写地址
	iic_wait_ack();
	iic_write_byte(dat); //发送字节
	iic_wait_ack();
	iic_stop(); //产生一个停止条件
	delay_ms(10);
}

/********************************************************************
***********
* 函 数 名 : at24c02_read_one_byte
* 函数功能 : 在 AT24CXX 指定地址读出一个数据
* 输 入 : addr:开始读数的地址
* 输 出 : 读到的数据
*********************************************************************
**********/
u8 at24c02_read_one_byte(u8 addr)//入口参数,指定地址addr
{
	u8 temp=0;
	iic_start(); //产生IIC起始信号
	iic_write_byte(0XA0); //发送写命令
	iic_wait_ack();
	iic_write_byte(addr); //发送写入需要读取的地址
	iic_wait_ack();
	iic_start();//因为要读取方向发生改变,所以起始信号要重来一次
	iic_write_byte(0XA1); //进入读取接收模式
	iic_wait_ack();
	temp=iic_read_byte(0); //读取字节存入在temp里面,读取之后要结束,iic_read_byte()就
                            //传入一个参数ack=0是,iic_read_byte(0)根据iic_read_byte()
                           //函数,ack取反一下是,就是nack。
	iic_stop(); //产生一个停止条件
	return temp; //返回读取的数据
}

2.4 user文件夹main.c主函数

  main.c文件代码如下

/********************************************************************
****************** 实验名称:I2C-EEPROM 实验
接线说明:
实验现象:下载程序后,数码管右 4 位显示 0,按 K1 键将数据写入到 EEPROM 内保存,
按 K2 键读取 EEPROM 内保存的数据,按 K3 键显示数据加 1,按 K4 键显示数据清
零,
最大能写入的数据是 255。
注意事项:
*********************************************************************
******************/
#include "public.h"
#include "key.h"
#include "smg.h"
#include "24c02.h"

#define EEPROM_ADDRESS 0//定义数据存入 EEPROM 的起始地址,要改变数据写入的地址,只需在这里修改就可以

void main()
{
	 u8 key_temp=0;//定义一个变量保存按键的键值
	 u8 save_value=0;//定义一个要写入的值,初值0
	 u8 save_buf[3]; //定义一个显示三位的数组
	 while(1)
	 {
	   key_temp=key_scan(0);//key_scan(0)是单词扫描,key_scan(1)连续扫描
	                        //返回对应键值保存在key_temp变量里面
	   if(key_temp==KEY1_PRESS)	//通过按下K1,把save_value数据写入到地址EEPROM_ADDRESS里面
                                //保存
	   {
	    	at24c02_write_one_byte(EEPROM_ADDRESS,save_value);//EEPROM_ADDRESS是写入数据
                                                              //指定的地址
	   }
 	   else if(key_temp==KEY2_PRESS)//通过按下K2,把地址EEPROM_ADDRESS里面的数据读出来,保存
                                    //在save_value里面
	   {
	   		save_value=at24c02_read_one_byte(EEPROM_ADDRESS);		
	   }
	   else if(key_temp==KEY3_PRESS)//通过按下K3,数据save_value加1
	   {
	   		save_value++;
			if(save_value==255)save_value=255;

	   }
	   else if(key_temp==KEY4_PRESS)//通过按下K0,数据save_value清零
	   {
	   		save_value=0;
	   }
	   save_buf[0]=save_value/100; //获取百位的数字,假如ave_value是245除以100等于2,百位上
                                  //就是2
	   save_buf[1]=save_value%100/10; //获取十位上的数字,假如ave_value是245,%100是取除
                                     //以100的余数,就是45,在除以10,十位上就是4
	   save_buf[2]=save_value%100%10;//获取个位上的数字,假如ave_value是245,%100是取除以
   //100的余数,就是45,在%100是取除以100的余数就是5,个位上就是5
	   smg_display(save_buf,6);//如上举例save_buf[2,4,5],在共阴极数码管显示0~F的段码数据,
//就是245,6是,从到右第6个数码管开始显示

	 }


}
  • 5
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

生活就是抡大锤

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

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

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

打赏作者

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

抵扣说明:

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

余额充值