基于51单片机的I2C-EEPROM实验

描述

1. I2C介绍

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

1.1 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限制。

高阻态这是一个数字电路里常见的术语,指的是电路的一种输出状态,既不是高电平也不是低电平, 如果高阻态再输入下一级电路的话,对下级电路无任何影响,和没接一样,如果用万用表测的话有可能是高电平也有可能是低电平,随它后面接的东西定。

MCU(Micro Control Unit),中文为微控制单元,又称单片微型计算机(Single Chip Microcomputer)或者单片机 ,是指将计算机的CPU、RAM、ROM、定时计数器和多种I/O接口集成在一片芯片上,形成芯片级的计算机。

I2C总线常涉及的术语

主机:启动数据传送并产生时钟信号的设备;
从机:被主机寻址的器件;
多主机:同时有多于一个主机尝试控制总线但不破坏传输;
主模式:用I2CNDAT支持自动字节计数的模式;位 I2CRM,I2CSTT,I2CSTP控制数据的接收和发送;
从模式:发送和接收操作都是由I2C模块自动控制的;
仲裁:是一个在有多个主机同时尝试控制总线但只允许其中一个控制总线并使传输不被破坏的过程;
同步:两个或多个器件同步时钟信号的过程;
发送器:发送数据到总线的器件;
接收器:从总线接收数据的器件。

1.2 I2C协议层

I2C的协议定义了通信的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。

(1)数据有效性规定

I2C总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。 如下图:

微控制器

每次数据传输都以字节为单位,每次传输的字节数不受限制。

(2)起始和停止信号

SCL线为高电平期间,SDA 线由高电平向低电平的变化表示起始信号;SCL线为高电平期间,SDA线由低电平向高电平的变化表示终止信号。 如下图:

微控制器

起始和终止信号都是由主机发出的,在起始信号产生后,总线就处于被占用的状态;在终止信号产生后,总线就处于空闲状态。

(3)应答响应

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

微控制器

每一个字节必须保证是8位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)。

由于某种原因从机不对主机寻址信号应答时(如从机正在进行实时性的处理工作而无法接收总线上的数据),它必须将数据线置于高电平,而由主机产生一个终止信号以结束总线的数据传送。

如果从机对主机进行了应答,但在数据传送一段时间后无法继续接收更多的数据时,从机可以通过对无法接收的第一个数据字节的“非应答”通知主机,主机则应发出终止信号以结束数据的继续传送。

当主机接收数据时,它收到最后一个数据字节后,必须向从机发出一个结束传送的信号。这个信号是由对从机的“非应答”来实现的。然后,从机释放SDA线,以允许主机产生终止信号。

这些信号中,起始信号是必需的,结束信号和应答信号都可以不要。

也就是说一个主机一个从机,主机向从机发送数据,首先主机产生一个起始信号,主机开始发送数据给从机,如果从机接收数据之后,还想主机继续发送数据,就发送一个应答给主机,主机收到应答信号后,继续向从机发送数据。如果从机接收数据之后,不想主机继续发送数据,终止数据传输,可以向主机发送一个非应答,主机就会产生一个停止信号,结束I2C的通信。

主机从从机读取数据时,主机发送一个起始信号,主机接收数据之后,也要产生一个应答或非应答,如果主机产生一个应答,表示想继续读取从机数据,从机接收到应答信号,从机继续发送数据给主机。主机如果不想读取从机数据,就会产生一个非应答后,会产生一个停止信号,从机接收到非应答信号,停止数据发送。

(4)总线的寻址方式

I2C总线寻址按照从机地址位数可分为两种,一种是7位,另一种是10位。采用7位的寻址字节(寻址字节是起始信号后的第一个字节)的位定义如下:

微控制器

D7~D1 位组成从机的地址。D0位是数据传送方向位,为“0”时表示主机向从机写数据,为“1”时表示主机由从机读数据。

10位寻址和7位寻址兼容,而且可以结合使用。10位寻址不会影响已有的7位寻址,有7位和10位地址的器件可以连接到相同的I2C总线。我们就以7位寻址为例进行介绍。

当主机发送了一个地址后,总线上的每个器件都将头7位与它自己的地址比较,如果一样,器件会判定它被主机寻址,其他地址不同的器件将被忽略后面的数据信号。 至于是从机接收器还是从机发送器,都由R/W位决定的。从机的地址由固定部分和可编程部分组成。在一个系统中可能希望接入多个相同的从机,从机地址中可编程部分决定了可接入总线该类器件的最大数目。如一个从机的7位寻址位有4位是固定位,3位是可编程位,2^3=8这时仅能寻址8个同样的器件,即可以有8个同样的器件接入到该I2C总线系统中。

(5)数据传输

I2C总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。在起始信号后必须传送一个从机的地址(7位),第8位是数据的传送方向位(R/W),用“0”表示主机发送(写)数据(W), “1”表示主机接收数据(R)。每次数据传送总是由主机产生的终止信号结束。但是,若主机希望继续占用总线进行新的数据传送,则可以不产生终止信号,马上再次发出起始信号对另一从机进行寻址。

在总线的一次数据传送过程中,可以有以下几种组合方式:

微控制器

注意:有阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从机向主机传送。A表示应答,A非表示非应答(高电平)。S表示起始信号,Р表示终止信号。一个数据帧是9位。

微控制器

微控制器

由于51单片机没有硬件IIC接口,即使有硬件接口我们通常还是采用软件模拟I2C。 主要原因是硬件 IIC设计的比较复杂,而且稳定性不怎么好,程序移植比较麻烦,而用软件模拟IIC,最大的好处就是移植方便,同一个代码兼容所有单片机,任何一个单片机只要有IO口(不需要特定IO),都可以很快的移植过去。

EEPROM (Electrically Erasable Programmable read only memory)是指带电可擦可编程只读存储器。是一种掉电后数据不丢失的存储芯片。 EEPROM 可以在电脑上或专用设备上擦除已有信息,重新编程。一般用在即插即用。

2.AT24C02芯片介绍

AT24C02芯片也是EEPROM存储芯片,是一种掉电后数据不丢失的存储芯片。AT24C0、01/02/04/08/16...是一个1K/2K/4K/8K/16k位串行CMOS,内部含有128/256/512/1024/2048个8位字节,AT24C01有一个8字节页写缓冲器,AT24CO2/04/08/16有一个16 字节页写缓冲器。该器件通过I2C总线接口进行操作,它有一个专门的写保护功能。开发板上使用的是 AT24C02 (EEPROM) 芯片,此芯片具有I2C通信接口,芯片内保存的数据在掉电情况下都不丢失,所以通常用于存放一些比较重要的数据等。AT24CO2芯片管脚及外观图如下图所示:

微控制器

微控制器

芯片管脚说明如下图所示:

微控制器

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总线时序如下图所示:

微控制器

微控制器

开发板硬件连接

微控制器

从图中可以看出,芯片的SCL和SDA管脚是连接在单片机的P2.1和P2.0上,为了让IIC总线默认为高电平,通常会在IIC总线上接上拉电阻,在图中并没有看到SCL和SDA管脚有上拉电阻,这是因为开发板单片机IO都外接了10K上拉电阻,当单片机IO口连接到芯片的SCL和SDA脚时即相当于它们外接上拉电阻,所以此处可以省去。

**3.多文件工程创建
**

实现的功能是:系统运行时,数码管右 3 位显示 0,按矩阵按键 S1 键将数据写入到 EEPROM 内保存,按 S2 键读取 EEPROM 内保存的数据,按 S3 键显示数据加 1,按 S4 键显示数据清零,最大能写入的数据是 255。

微控制器

App文件夹:用于存放外设驱动文件,如LED、数码管、定时器等。
Obj文件夹:用于存放编译产生的 c/汇编/链接的列表清单、调试信息、hex文件、预览信息、封装库等文件。
Public文件夹:用于存放51单片机公共的文件,如延时、51头文件、变量类型重定义等。
User文件夹:用于存放用户主函数文件,如 main.c。

1.新建一个工程

微控制器

选择对应的芯片型号

微控制器

微控制器

2.向工程添加文件

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

微控制器

新建分组,双击可以修改名字,用英文分组,完成后,点击OK

微控制器

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

微控制器

对分组添加相应的文件,点击小方框,选择要添加文件的分组,添加文件。

微控制器

同样的方法,将App、Public工程组中文件也添加进去。一般除了main.c没有对应的main.h的头文件,其他文件通常会有对应的.h头文件,例如public.h和public.c。

微控制器

头文件的书写

#ifndef _public_H  //public是与文件名相同  ifndef是c语言中的条件编译,意思是如果没有定义对应的头文件,就会定义该头文件
#define _public_H




#endif

主要目的是防止头文件的重复包含和编译。

配置魔术棒选项

这一步的配置工作非常重要,很多人编写完程序编译后发现找不到HEX文件,还有的人直接编译前面添加好文件的工程出现报错,这些问题都是在这个地方没有配置好导致的。

(1)C51选项卡配置,此处目的是将我们前面添加到工程组中的文件路径包括进来,否则程序中调用其他文件夹的头文件则会报错找不到头文件路径,具体步骤如下:

点击魔术棒

微控制器

微控制器

微控制器

头文件的作用一般是(1)包含其他的头文件;(2)声明全局变量;(3)声明自定义函数(4)自定义的变量类型(u8 u16),当其他程序中加载了该头文件就可以使用该函数和全局变量。

public.h文件

#ifndef _public_H//public是与文件名相同  
//ifndef是c语言中的条件编译,意思是如果没有定义对应的头文件,就会定义该头文件
//主要目的是防止头文件的重复包含和编译。
#define _public_H


#include "reg52.h" //加载reg52.h头文件
//定义类型别名
typedef unsigned int u16;
typedef unsigned char u8;


//声明两个延时函数
void delay_10us(u16 time);
void delay_ms(u16 time);


#endif

public.c文件

#include "public.h"


void delay_10us(u16 time)  //延时函数 time=1大概延时10us
{
  while(time--)
  ;
}


void delay_ms(u16 time)       //延时函数 time=1大概延时1ms
{
  u16 i,j;
  for(i=time;i >0;i--)
    for(j=110;j >0;j--)
    ;
}

(2)Output选项卡中把输出文件夹定位到我们实验目录下的 Obj文件夹,如果想在编译的过程中生成hex文件,那么、Create HEX File 选项勾上。配置如下:

微控制器

(3) Listing 选项卡中把输出文件夹也定位到我们实验目录下的Obj文件夹。其它设置默认,配置如下:

微控制器

同样的方式把其他文件夹的文件添加到对应的分组,并将头文件的路径添加上。

微控制器

4.实验内容和程序

实现的功能是:系统运行时,数码管右 3 位显示 0,按矩阵按键 S1 键将数据写入到 EEPROM 内保存,按 S2 键读取 EEPROM 内保存的数据,按 S3 键显示数据加 1,按 S4 键显示数据清零,最大能写入的数据是 255。

实验结果

微控制器

,时长00:36

[ ]

主函数main.c

#include "public.h"
#include "key.h"
#include "smg.h"
#include "at24c02.h"
//#include "iic.h"   at24c02函数内容调用了iic.h
/*
实现的功能是:系统运行时,数码管右 3 位显示 0,
按 S1 键将数据 写入到 EEPROM 内保存,
按 S2 键读取 EEPROM 内保存的数据,
按 S3 键显示数据加 1,
按 S4 键显示数据清零,最大能写入的数据是 255。
*/


#define EEPROM_ADDRESS 0 //宏定义数据存储的地址
void main()
{
  u8 key_value=0;  //保存按键的键值
  u8 save_value =0;//定义变量,存储数据的变量
  u8 save_buf[3];//三位数
   while(1)
  {
    key_value=key_juzhen_fanzhuan_scan();
    if(key_value==1)  //按 K1 键将数据 写入到 EEPROM 内保存
    {
      at24c02_write_one_byte(EEPROM_ADDRESS,save_value);


    }
    else if(key_value==2)  //按 K2 键读取 EEPROM 内保存的数据
    {
      save_value=at24c2_read_one_byte(EEPROM_ADDRESS);
    }
    else if(key_value==3)
    {
       save_value++;
       //不能一直加 8位二进制 最大值255
       if(save_value==255)//当数据大于255后会溢出 重新从0开始
         save_value=255;
    }
    else if(key_value==4)
    {
           save_value=0;
    }
    //数码管显示
    //对十进制数据进行取百位 十位 个位
    /*
    三位数提取 百位 假设三位数是100 100/100=1 取出百位
    十位提取 (100%100)/10 数据对100取余 ,再除以10 取出十位
    个位提取  (100%100)%10  数据对100取余,再对10取余 取出个位 
    */
//    save_buf[0]=save_value/100;//取出百位
//    save_buf[1]= save_value%100/10;// 取出十位
//    save_buf[2]= save_value%100%10;//取出个位
    save_buf[0]=gsmg[save_value/100];//取出百位
    save_buf[1]= gsmg[save_value%100/10];// 取出十位
    save_buf[2]= gsmg[save_value%100%10];//取出个位
      smg_display(save_buf,6);//数组名表示数组元素首地址
  }
}

Key.c 矩阵按键程序

#include "key.h"








u8 key_juzhen_fanzhuan_scan(void) //定义线翻转法函数
{
   /*
  线翻转法,就是使所有行线为低电平时,检测所有列线是否有低电平,
  如果有,就记录列线值;然后再翻转,使所有列线都为低电平,检测所有行线的值,
  由于有按键按下,行线的值也会有变化,记录行线的值。从而就可以检测到全部按键。
   */
    static u8 key_value=0;   
    //定义静态局部变量 key_value的值会赋完一次初值之后,key_value的值会保存上一次的结果值
    /*开发板 4*4矩阵键盘 行控制端口 P1.7 P1.6 P1.5 P1.4    列控制端口: P1.3 P1.2 P1.1 P1.0
     P1口输出默认是高电平,使所有行线为低电平  此时P1控制端 二进制 0000 1111  十六进制 0x0f
     找出按键的列值
     {
       按键在第1列时,此时P1控制端 二进制 0000 0111  十六进制 0x07     key_value=1
    按键在第2列时,此时P1控制端 二进制 0000 1011  十六进制 0x0b     key_value=2
    按键在第3列时,此时P1控制端 二进制 0000 1101  十六进制 0x0d     key_value=3
    按键在第4列时,此时P1控制端 二进制 0000 1110  十六进制 0x0e       key_value=4

     }  
     再使所有的列线为低电平  此时P1控制端 二进制 1111 0000  十六进制 0xf0
     {
    按键在第1行时,此时P1控制端 二进制  0111 0000  十六进制 0x70    按键的位置 key_value=对应的行数=key_value
    按键在第2行时,此时P1控制端 二进制  1011 0000  十六进制 0xb0    按键的位置 key_value=对应的行数+4*1 = key_value+4    第一行有4个按键
    按键在第3行时,此时P1控制端 二进制  1101  0000    十六进制 0xd0    按键的位置 key_value=对应的行数+4*2  = key_value+8  前两行有8个按键
    按键在第4行时,此时P1控制端 二进制  1110  0000    十六进制 0xe0      按键的位置 key_value=对应的行数+4*3  = key_value+12 前两行有12个按键
     }
     */
    KEY_Port=0x0f;       //使所有行线为低电平 
    if(KEY_Port!=0x0f)
    {
       delay_10us(1000);//延时10ms实现按键消抖
      //测试列
      if(KEY_Port!=0x0f) //实现消抖后再次检测
      {
      KEY_Port=0x0f;  //设置再次赋值 行线为低电平,方便进行检查 
         switch(KEY_Port)
         {
          case 0x07: key_value = 1;break;
          case 0x0b: key_value = 2;break;
          case 0x0d: key_value = 3;break;
          case 0x0e: key_value = 4;break;
         }
         //测试列
        KEY_Port=0xf0;//使所有的列线为低电平
        switch(KEY_Port)
         {
          case 0x70: key_value = key_value;break;
          case 0xb0: key_value = key_value+4;break;
          case 0xd0: key_value = key_value+8;break;
          case 0xe0: key_value = key_value+12;break;
         }
         while(KEY_Port!=0xf0);//等待按键松开 当按键松开时 KEY_Port = 0xf0 会跳出死循环  
      }


    }
     else
    key_value=0;
    return key_value;//返回函数值


}

Key.h 头文件

#ifndef _key_H     //条件编译语句 如果没有定义头文件,就在下面重新定义头文件
#define _key_H
#include "public.h"  //加载公共部分Public中的头文件的内容


#define KEY_Port P1 //宏定义 无分号 将P1口定义为矩阵键盘的端口 
//开发板的数码管段选信号由P0口(P0.0-P0.7)控制   位选 P2口默认是输出高电平 默认选择LED8 第1个数码管
//定义数码管显示内容的数组


u8 key_juzhen_fanzhuan_scan(void);//定义线翻转法函数




#endif

Smg.c 数码管显示函数

#include "smg.h"
u8 gsmg[17]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,
      0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71,0x00};
      //共阴极数码管显示数据1-F,最后一个是数码管全灭  表示c语言中的续行符
void smg_display(u8 dat[],u8 pos)   //dat[]是数码管段选数据 显示0 1 2 3...
// pos 位选     1 2 3 4 5 6 7 8 第几个数据管
//更改数码管的显示 提供数据和数码管使用的位置
{
 u8 i=0;     //定义变量的时候赋初值,防止一些编译器识别未用的变量产生报错
 u8 pos_temp=pos-1;//
 for(i=pos_temp;i< 8;i++)
 {
    switch(i)  //用switch-case语句进行位选
  {
   case 7: W_C =0;W_B = 0;W_A = 0;break; //对应选择了Y0-LED1   最后一个数码管
   case 6: W_C =0;W_B = 0;W_A = 1;break; //对应选择了Y0-LED2
   case 5: W_C =0;W_B = 1;W_A = 0;break; //对应选择了Y0-LED3
   case 4: W_C =0;W_B = 1;W_A = 1;break; //对应选择了Y0-LED4
   case 3: W_C =1;W_B = 0;W_A = 0;break; //对应选择了Y0-LED5
   case 2: W_C =1;W_B = 0;W_A = 1;break; //对应选择了Y0-LED6
   case 1: W_C =1;W_B = 1;W_A = 0;break; //对应选择了Y0-LED7
   case 0: W_C =1;W_B = 1;W_A = 1;break; //对应选择了Y0-LED8  第一个数码管


  }


//  SMG_Port = gsmg[dat[i-pos_temp]];//段选信号  dat[]表示0 1 2 3 4 5 6 7 -F
  SMG_Port = dat[i-pos_temp];//段选信号  dat[]表示0 1 2 3 4 5 6 7 -F
  delay_10us(100);//大概1ms延时
  SMG_Port=0x00;//消隐
 }
}

Smg.h 头文件

#ifndef _smg_H
#define _smg_H
#include "public.h"


#define SMG_Port P0 //宏定义 将P0口定义SMG_Port 
//开发板的数码管段选信号由P0口(P0.0-P0.7)控制
//定义数码管显示内容的数组
//定义的是全局变量 gsmg 开头
extern u8 gsmg[17];   //对数组声明


sbit W_C = P2^4;//sbit定义位变量  位控制选择数码管
sbit W_B = P2^3; //sbit定义位变量
sbit W_A = P2^2;//sbit定义位变量
void smg_display(u8 dat[],u8 pos); //声明数码管动态扫描显示函数
#endif

Iic.c

#include "iic.h"


//IIC起始和停止信号和应答信号函数
/*
SCL线为高电平期间,SDA 线由高电平向低电平的变化表示起始信号;
SCL线为高电平期间,SDA线由低电平向高电平的变化表示终止信号。
每当发送器件传输完一个字节的数据后,后面必须紧跟一个校验位,
这个校验位是接收端通过控制SDA(数据线)来实现的,
以提醒发送端数据我这边已经接收完成,数据传送可以继续进行。
这个校验位其实就是数据或地址传输过程中的响应。
响应包括"应答(ACK)"和"非应答(NACK)"两种信号。
作为数据接收端时,当设备(无论主从机)接收到I2C传输的一个字节数据或地址后,
若希望对方继续发送数据,则需要向对方发送"应答(ACK)"信号即特定的低电平脉冲,
发送方会继续发送下一个数据;若接收端希望结束数据传输,
则向对方发送"非应答(NACK)"信号即特定的高电平脉冲
,发送方接收到该信号后会产生一个停止信号,结束信号传输。
每一个字节必须保证是8位长度。数据传送时,先传送最高位(MSB),
每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)。*/


void iic_start(void)  //根据起始信号的时序编写iic的起始信号函数
{

  //根据数据手册的时间参数级或者us SDA由高电平编程低电平  
  IIC_SDA=1;//如果把该条语句放在SCL后面,第二次读写会出现问题
  delay_10us(1);
  IIC_SCL=1;  //SCL为高电平
  delay_10us(1);
  IIC_SDA=0;
  delay_10us(1);
   IIC_SCL=0; //SCL为低电平时,表示占用总线,总线处于工作状态
  delay_10us(1);
}


void iic_stop(void) //iic的停止信号
{
  IIC_SDA=0; //如果把该条语句放在SCL后面,第二次读写会出现问题
    //根据数据手册的时间参数级或者us SDA由低电平编程高电平
   delay_10us(1);  
  IIC_SCL=1; //SCL为高电平
  delay_10us(1);
  IIC_SDA=1;
  delay_10us(1);


}
/*
 响应包括"应答(ACK)"和"非应答(NACK)"两种信号。
 作为数据接收端时,当设备(无论主从机)接收到I2C传输的一个字节数据或地址后,
 若希望对方继续发送数据,则需要向对方发送"应答(ACK)"信号即特定的低电平脉冲,
 发送方会继续发送下一个数据;若接收端希望结束数据传输,
 则向对方发送"非应答(NACK)"信号即特定的高电平脉冲,
 发送方接收到该信号后会产生一个停止信号,结束信号传输。
*/


//主机发送数据给从机 的应答信号和非应答信号
void iic_ack(void) //iicACK应答信号函数
{
   //ACK是特殊的低电平信号
  //由时序图编写
  IIC_SCL=0;
  IIC_SDA=0;
  delay_10us(1);
  IIC_SCL=1;
  delay_10us(1);
  IIC_SCL=0;


}
void iic_noack(void) //iic非应答信号函数
{
   //NACK是特殊的高电平信号
  //由时序图编写
  IIC_SCL=0;
  IIC_SDA=1;
  delay_10us(1);
  IIC_SCL=1;
  delay_10us(1);
  IIC_SCL=0;


}


//判断应答信号的函数 就是主机读取IIC_SDA的电平
//函数设置一个返回值 1表示非应答  0表示应答
u8 iic_wait_ack(void)
{
   /*第一步:因为SCL为高电平的时候数据才是稳定的,将SCL设置为高电平.
    第二步;延时一点时间进行读取SDA的电平.
    第三步:利用while循环   while(IIC_SDA) 当IIC_SDA=0时,响应是ACK应答信号会跳出循环,
    将SCL设置为低电平,数据可以修改,等待下一次数据的变化,并且返回函数值0.
    当IIC_SDA=1时表示响应是NACK非应答信号会执行循环体里面的函数,一直循环,设置一个变量,用来计时
    的时间,不能一直等待,当计时超过一定的时间,就认为SDA是高电平,此时是非应答,要产生停止信号
  */
  u8 time_temp=0;
  IIC_SCL=1;
  delay_10us(1);
  while(IIC_SDA)
   {
    time_temp++;//等价与time_step=time_step+1
    if(time_temp >100)
    {
      iic_stop();
      return 1;
    }


   }
     IIC_SCL=0;
  return 0;
}
//IIC的读写字节数据函数
//IIC的写字节函数
void iic_write_byte(u8 dat)
{
 //要写数据或地址 从高位开始写 SCL低电平数据可以变化 SCL高电平的时候数据稳定,就可以发送出去
  //一个字节有8位,因此要一位一位的写用循环
  u8 i=0;
  IIC_SCL=0;//SCL是低电平 数据可以修改 先写高位
  for(i=0;i< 8;i++)
  {
     /*获取dat最高位的 dat&0x80   d7 d6 d5 d4 d3 d2 d1 d0  & 1 0 0 0 0 0 0 0
  根据与运算  dat的低7位都为0 当d7=1 时逻辑表达式的值为1 当d7=0时,逻辑表达式的值为0
  */
  if((dat&0x80) >0) //dat最高位是1
    IIC_SDA=1; //传输数据是1
  else
    IIC_SDA=0;  //传输数据是0
/*对dat的次高位变成最高位 移位运算符dat< < 1 表示dat左移一位但是不会改变dat的值,
 表达式的值是dat左移一位的结果  左移移位运算符 dat< <=1      表示dat左移一位将结果赋值给dat*/
   dat< <=1;
  delay_10us(1);//延时10us
    IIC_SCL=1; //数据要进行传输 SCL为高电平 数据稳定 可以进行数据传输
  delay_10us(1);//延时10us
  IIC_SCL=0; //重新SCL设置为0 为下一位的数据修改做准备 
  delay_10us(1);//延时10us
  }


}
//IIC的读字节函数
u8 iic_read_byte(u8 ack)  
//IIC的读字节函数,要读取数据,因此由函数的返回值,一个字节的数据u8
//读取数据需要看返回的响应信号 1 ACK 继续读取 0 NACK不读取数据
{
  u8 i=0;
  u8 receive_dat=0;//接收数据
  for(i=0;i< 8;i++)
  {
    IIC_SCL=0;//SCL为低电平 可以修改数据
    delay_10us(1);
    IIC_SCL=1;  //SCL为高电平 数据稳定 可以进行读取
    receive_dat< <=1;
    if(IIC_SDA)  //开始读取数据 判断SDA管脚电平 SDA==1 得到一个信号
    //对最低位进行读取
    receive_dat++;
    delay_10us(1);  
  /*例如  i=0 SDA=1 receive_dat=0+1=1  高位      先读取高位
      i=1 SDA=1 receive_dat=1+1=2  次高位
    1 二进制  0000 0001
    2 二进制  0000 0010
    与实际不符合 实际应该是0000 0011 并且需要进行移位最后11移动最高位
    即 receive_dat进行左移 假设i=0 SDA=1  0000 0001 
                  i=1 SDA=0  0000 0010  
                  添加  receive_dat< <=1;左移语句
    假设从机数据是 1001 0001
    i    receive_dat< <=1;    receive_dat  主机从从机读取的数据
    i=0  0000 0000      0000 0001     最高位数据 SDA=1
    i=1  0000 0010      0000 0010     次高位数据 SDA=0
    i=2  0000 0100      0000 0100     SDA=0
    i=3  0000 1000      0000 1001     SDA=1
    i=4  0001 0010      0001 0010        SDA=0
    i=5  0010 0100      0010 0100         SDA=0
    i=6  0100 1000      0100 1000        SDA=0
    i=7  1001 0000      1001 0001        SDA=1
    数据传输完成*/
  }
  //对响应信号进行处理 看主机是否需要继续向从机读取数据 ACK=0 继续读取数据 ACK=1停此读取数据
  if(!ack) // ack=0时发送一个ack
  iic_noack();
  else
     iic_ack();    // ack=1  !ack=0 时发送一个nack


  return receive_dat;//返回读取的数据
}

Iic.h

#ifndef _iic_H
#define _iic_H




#include "public.h"
//利用I/O模拟IIC的时序 开发板的P2.1连接SCL时钟线 P2.0连接SDA双向数据传输线
sbit IIC_SCL=P2^1;
sbit IIC_SDA=P2^0;
//函数声明
void iic_start(void);//根据起始信号的时序编写iic的起始信号函数
void iic_stop(void); //iic的停止信号
void iic_ack(void); //iicACK应答信号函数
void iic_noack(void); //iic非应答信号函数
//判断应答信号的函数 就是主机读取IIC_SDA的电平
//函数设置一个返回值 1表示非应答  0表示应答
u8 iic_wait_ack(void);
//IIC的写字节函数
void iic_write_byte(u8 dat);
//IIC的读字节函数
u8 iic_read_byte(u8 ack);
#endif

At24c02.c 程序

#include "at24c02.h"
#include "iic.h"
//at24c02的读写程序,根据IIC协议层数据传输编写
void at24c02_write_one_byte(u8 addr,u8 dat) 
//24C02有256个字节 要写到写入地址  和写入的数据    主机写入从机 主机向从机发送数据
{
   /* 第一步:发送起始信号 
  AT24C02器件地址为7位,高4位固定为1010,
  低3位由A0/A1/A2信号线的电平决定。
  因为传输地址或数据是以字节为单位传送的,
  当传送地址时,器件地址占7位,还有最后一位(最低位R/W)用来选择读写方向,它与地址无关。 
     写AT24C02引脚二进制 1010 0000  十六进制:0xA0
     读AT24C02引脚二进制 1010 0001  十六进制:0xA1


     */
  iic_start();//发送起始信号  后第一个字节是从机地址+读写位
  iic_write_byte(0xA0);
  //从机发的ACK   主机需要等待ACK
  iic_wait_ack();   //等待一个ACK
  iic_write_byte(addr);  //发送写地址的传输
  //因为数据传输方向没有改变 因此不需要从新产生起始信号和
  iic_wait_ack();    //等待一个ACK
  iic_write_byte(dat);//写入一个字节数据
  iic_wait_ack();   //等待一个ACK
  iic_stop();//传输一个字节后 主机发出停止信号
  delay_ms(10);//延时10ms  将数据存储
}


//读字节函数    就是主机向从机读取数据  即从机向主机发送数据 
//数据的传输方向改变
u8 at24c2_read_one_byte(u8 addr)   //读取数据的地址
{  //首先写入读取数据的存放的地址 然后改变数据传输方向  读取数据传输方向改变
   u8 temp=0;//读取数据存储变量
   iic_start();//主机发送起始信号
  iic_write_byte(0xA0);//  第一个字节是从机地址+读写位
  iic_wait_ack();    //等待一个ACK
  iic_write_byte(addr);  //发送读数据存放地址的传输
  iic_wait_ack();    //等待一个ACK
   //开始读取数据   数据传输方向改变
    iic_start();//主机发送起始信号
  iic_write_byte(0xA1);//  第一个字节是从机地址+读写位
  iic_wait_ack();    //等待一个ACK
  temp=iic_read_byte(0);  //1 ACK 继续读取 0 NACK不读取数据
  iic_stop();//产生停止信号
  return temp;
}

At24c02.h 头文件

#ifndef _at24c02_H
#define _at24c02_H
#include "public.h"
void at24c02_write_one_byte(u8 addr,u8 dat) ;
u8 at24c2_read_one_byte(u8 addr);   //读取数据的地址
#endif
打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分