【单片机】C51中的I2C操作-Proteus+Keil4+C语言实现

article/2025/10/11 20:50:25

51单片机中I2C读写操作

开发板:普中51-单核-A2;

I2C器件-EEPROM:AT24C02、FM24C02(仿真EEPROM);

仿真软件:proteus;

开发环境:Keil4;

参考资料:开发板所附视频;

如有错误,感谢指正。如有侵权请联系博主。


首先需要了解I2C是什么。

1. I2C是什么

I2C Inter-Integrated circuit的简称,飞利浦公司于1980年代提出,为了让主板、嵌入式系统或手机用以连接低速周边外部设备而发展。

I2C具备多主机系统需要的总线裁决、高低速器件同步等功能,是一种高性能的串行总线。

串行总线用于串行通信,串行通信速率低,在数据通信吞吐量不是很大的微处理器中比较简易、方便、灵活;

并行总线用于并行通信,并行通信速度快,实时性好,但是占用的口线多,不适于小型化产品;

更多关于总线的内容可以参考:总线的定义, 并行总线和串行总线。

I2C允许相当大的工作电压,典型的电压基准为+3.3V或+5V。

I2C总线有两根双向信号线:SDASCL,SDA是数据线,用来传送数据;SCL是时钟线,用来传输CLK时钟信号,主设备向从设备发送。

如下所示,在I2C总线上可以连接多个器件,通常这些器件中有一个是主器件,其它的是从器件。

每个接到I2C总线上的器件都有唯一的地址,可以通过该地址识别主机想要通信的是哪个器件。

在这里插入图片描述

在多主机系统中,可能同时有几个主机想要主动传送数据,为了避免混乱,I2C总线通过总线仲裁,决定由哪一台主机控制总线。

80C51单片机应用系统中,通常是以80C51为主机,其它接口器件为从机的单主机情况。

I2C的模式

常用到的I2C总线以传输速率不同分为:

  • 标准模式:100Kbit/s
  • 低速模式:10Kbit/s
  • 快速模式:400Kbit/s
  • 高速模式:3.4Mbit/s

I2C的特征

I2C有如下特征:

  • 串行通信:所有的数据以bit为单位在SDA线上串行传输;
  • 同步通信:双方在用一个时钟下通信。同步通信的特征是通信线中有时钟信号CLK
  • 非差分:I2C速率不高,并且通信距离近,使用电平信号通信;
  • 低速率:I2C通常是同一个板子上的两个IC芯片间通信,数量不大,速率低。

I2C的总线状态

I2C总线有两种状态:空闲态和忙态。

  • 空闲态:没有设备发生通信,此时SDA和SCL均处于高电平状态;
  • 忙态:其中一个从设备和主设备通信,I2C总线被占用,其它从设备处于等待状态;

I2C总线通过上拉电阻接正电源,这样可以保证当总线空闲时,两根线均为高电平。连接到总线上的任一器件输出低电平,都将使总线信号变低,各器件的SDA和SCL是线的关系。

I2C数据传送

数据有效位规定

  • 进行数据传送时,时钟线SCL为高电平期间,数据线SDA保持稳定;
  • 只有时钟线SCL为低电平时,SDA上的高低电平才允许变化;
    在这里插入图片描述

起始信号和终止信号

起始信号和终止信号(由主机发出):

  • SCL为高电平期间,SDA由高电平变为低电平,表示起始信号;
  • SCL为高电平期间,SDA由低电平向高电平变化,表示终止信号;

接收端将SCL拉低,可以使主机处于等待状态;接收端将SCL拉高,可以使主机工作。

在这里插入图片描述

起始信号产生后,总线就处于忙态(被占用);终止信号产生后,总线处于空闲态。

字节传送与应答

  • 每个字节必须保证是8位,先传送最高位MSB,每一个被传送的字节后面都必须跟随一个应答位,即一帧有9位。

在这里插入图片描述

  • 如果从机在做其他操作等原因不能对主机寻址信号应答时,从机必须将SDA置为高电平,由主机产生终止信号结束数据传送;
  • 如果从机应答了主机,但数据传送一段时间后无法再接收更多数据,从机可以通过对无法接收的第一个数据字节的非应答通知主机,主机则应发出终止信号结束数据的继续传送;
  • 当主机接收数据,收到最后一个数据字节后,必须向从机发送结束传送信号,这个信号由对从机的非应答来实现的。然后从机释放SDA线,允许主机产生终止信号;

数据帧格式

I2C总线上传送的数据信号既包括地址信号,又包括数据信号。

注:如下图中数据传送方向,阴影表示数据由主机向从机传送,无阴影部分表示数据由从机向主机发送,A表示应答(低电平), A ‾ \overline{A} A表示非应答(高电平),S表示起始信号,P表示终止信号。

在这里插入图片描述

起始信号后必须传送一个7位的从机地址+1位数据传送方向,0表示主机发送数据,1表示主机接收数据。

然后可以传送一个字节的数据,每字节数据后会收到1位应答信号,数据传送结束会收到非应答信号。

每次数据传送都是由主机产生终止信号结束。但是如果主机希望继续占用总线进行新的数据传送,可以不产生终止信号,马上再次发出起始信号对另一个从机寻址。

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

  • 主机向从机发送数据,数据传送方向在整个传送过程中不变;

在这里插入图片描述

  • 主机接收从机的数据,主机发送完从机的地址后,立即从从机中读数据;

在这里插入图片描述

  • 传送过程中需要改变方向时,其实信号和从机地址都被重复产生一次,但两次读/写方向位相反;

在这里插入图片描述

总线寻址

I2C协议规定,采用7位的寻址字节,寻址字节的位定义:

在这里插入图片描述

D7~D1是从机地址,D0表示数据传送方向,0表示主机向从机写数据,1表示主机由从机读数据。

从机的地址由固定部分和可编程部分组成。

总线信号模拟

主机可以采用不带I2C总线接口的单片机,使用软件实现I2C总线的数据传送,即软件和硬件结合的信号模拟。

为保证数据传送的可靠性,标准的I2C总线的数据传送有严格的时序要求。如下。

  • 起始信号需要SCL&SDA保持高电平>4.7us后,SDA由高电平到低电平>4us;

  • 终止信号需要TSCL=H&SCL=L >4us,TSCL=H&SDA=L->H>4/7us;

  • 应答信号需要TSCL=H&SDA=0>4us;

  • 非应答信号需要TSCL=H&SDA=H > 4us;

在这里插入图片描述

数据写入

单片机操作时,首先发送该器件的7位地址码和1位写方向位0(共1个字节,8位),发送完后释放SDA线(拉高SDA),并在SCL线上产生第9个时钟信号,从器件产生应答信号作为确认是自己的地址,单片机收到应答后就可以传送数据了。

传送数据时,单片机首先发送一个字节的被写入器件的存储区的首地址,收到应答后,主机逐个发送各数据字节,每发送一个字节都要等待应答。

当写入的数据传送完后,主机发出终止信号以结束写入操作.

写入n个字节的数据格式如下:

在这里插入图片描述

数据读出

单片机操作时,首先发送该器件的7位地址码和1位写方向位0(共1个字节,8位),发送完后释放SDA线(拉高SDA),并在SCL线上产生第9个时钟信号,从器件产生应答信号作为确认是自己的地址。

然后再发送要读出器件的存储区的首地址,收到应答后,主机要重复一次起始信号并发出器件地址和读方向位,收到器件应答后就可以读出数据字节。

没读出一个字节,主机都要回复应答信号,当最后一个字节数据读完,主机返回非应答信号,并发出终止信号以结束读数据操作。

在这里插入图片描述

2. 模拟实现I2C读写数据

板子上的EEPROM是 AT24C02,其原理图如下:

在这里插入图片描述

实现功能:

  1. 设置4个独立按键K1,K2,K3,K4,分别用于I2C写入、I2C读取、数据累加、数据清零;
  2. 设置一个LCD数码管显示器,用于显示读取的数据;

原理图如下:

在这里插入图片描述

main.c

#include "reg52.h"
#include "i2c.h"typedef unsigned char u8;
typedef unsigned int u16;sbit K1 = P3^1;  // K1 保存显示的数据
sbit K2 = P3^0;  // K2 读取保存的数据					   
sbit K3 = P3^2;  // K3 累加读取的数据 
sbit K4 = P3^3;  // K4 清零// 38 译码器
sbit LSA = P2^2;
sbit LSB = P2^3;
sbit LSC = P2^4;// 数码管段选数据
u8 code smgduan[] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};  // 显示0~9的值
char num=0,disp[4];// 延时函数
void delay(u16 i)
{while(i--);
}// 按键处理函数
void Keypros()
{if(K1==0) // 按键按下{delay(1000);if(K1==0)  // 按键按下{AT24C02Write(1,num);  // 写入数据}while(!K1);   // 独立按键是否松开	}if(K2==0) // 按键按下{delay(1000);if(K2==0)  // 按键按下{num = (char)AT24C02Read(1);	  // 读取数据}while(!K2);   // 独立按键是否松开	}if(K3==0) // 按键按下{delay(1000);if(K3==0)  // 按键按下{num++;  // 累加if(num > 255)num=0;}while(!K3);   // 独立按键是否松开	}if(K4==0) // 按键按下{delay(1000);if(K4==0)  // 按键按下{num=0;  // 清零}while(!K4);   // 独立按键是否松开	}
}void datapros()
{// 4~0分别存储个、十、百、千位disp[0] = smgduan[num/1000];disp[1] = smgduan[num%1000/100];disp[2] = smgduan[num%1000%100/10];disp[3] = smgduan[num%1000%100%10];
}// 数码管显示
void DigDisplay()
{u8 i = 0;for(i=0; i<4; i++){switch(i){case 0:LSA = 1;LSB = 1; LSC = 1; break;  //显示第0位case 1:LSA = 0;LSB = 1; LSC = 1; break;  //显示第1位case 2:LSA = 1;LSB = 0; LSC = 1; break;  //显示第2位case 3:LSA = 0;LSB = 0; LSC = 1; break;  //显示第3位}P0 = disp[i];  // 发送段码delay(100);  // 间隔一段时间扫描P0 = 0x00;  //消隐}
}void main()
{while(1){Keypros();datapros();DigDisplay();}  
}

i2c.h

#ifndef _I2C_H
#define _I2C_H#include <reg52.h>// 定义时钟线和数据线
sbit SCL = P2^1;
sbit SDA = P2^0;void I2CStart();
void I2CStop();
unsigned char I2CWriteByte(unsigned char dat);
unsigned char I2CReadByte();
void AT24C02Write(unsigned char addr,unsigned char dat);
unsigned char AT24C02Read(unsigned char addr);#endif

i2c.c

#include "i2c.h"// 单片机管脚模拟时许// 延时10us
void Delay10us(void)
{unsigned char a,b;for(b=1;b>0;b--)for(a=2;a>0;a--);
}// 模拟起始信号
void I2CStart()
{// SCL=H,SCL=H->L,delay4.7usSDA = 1;Delay10us();SCL=1;	// SDA/SCL = HDelay10us();SDA=0;	// SDA H->LDelay10us();SCL=0;  // SCL=L时,数据可以改变   Delay10us();
}// 模拟终止信号
void I2CStop()
{// SCL=H,SCL=L->H,delay4.7usSDA = 0;Delay10us();SCL=1;Delay10us();SDA=1;	// SDA L->HDelay10us();
}// 模拟发送数据
unsigned char I2CWriteByte(unsigned char dat)
{unsigned char a=0, b=0;for(a=0;a<8;a++){SDA = (dat>>7);  // 传送最高位dat = (dat<<1);  // 替换最高位Delay10us();SCL = 1;  // SCL=H, 数据保持稳定Delay10us();SCL = 0;  // SCL=L, 数据可以改变Delay10us();}SDA=1;  // 释放时钟线和数据线Delay10us();SCL=1;// Delay10us();while(SDA)   // 应答信号SDA=0 ,非应答SDA=1{	b++;if(b>200)  // 非应答超过200次,至少200us{SCL=0;   // 拉低时钟线Delay10us();return 0;   // 表示发送信号失败}}SCL=0;Delay10us();return 1;	
}// 模拟接收数据
unsigned char I2CReadByte()
{unsigned char a=0, dat=0;SDA=1;  // SDA=1拉高使其处于空闲状态Delay10us();for(a=0;a<8;a++){SCL=1;  // 保持数据稳定Delay10us();dat <<= 1;  // data=0 <<1  ->  0dat |= SDA;Delay10us();SCL=0;  // SDA数据可以改变Delay10us();}return dat;
}// AT24C02 EEPROM 读写函数
void AT24C02Write(unsigned char addr, unsigned char dat)
{// 1. 起始信号I2CStart();// 2. 器件地址I2CWriteByte(0xa0);// 3. 写入的首地址I2CWriteByte(addr);// 4. 写入数据I2CWriteByte(dat);// 5. 停止信号I2CStop();			
}unsigned char AT24C02Read(unsigned char addr)
{unsigned char dat=0;// 1. 起始信号I2CStart();// 2. 器件地址I2CWriteByte(0xa0);// 3. 写入的首地址I2CWriteByte(addr);// 再次发送器件地址时,需要重新发送起始信号// 4. 起始信号  I2CStart();// 5. 器件地址I2CWriteByte(0xa1);// 6. 读取数据dat = I2CReadByte();// 5. 停止信号I2CStop();return dat;			
}

结果:初始读取为7是因为之前有进行过I2C写入,默认没有对I2C操作前读取到的数据是0。

在这里插入图片描述

出现的问题:

  1. 刚开始定义数据时使用data,编译报错I2C.H(13): error C141: syntax error near ')';原因是不能声明变量名为data,data是C51中的关键字。C51中的关键字data,idata,xdata,pdata,bdata。
  2. proteus绘制总线使用方法:proteus中的标签及总线的使用方法。
  3. 独立按键:51单片机 4个独立按键控制LED灯 (protues仿真)(C语言版)。

http://chatgpt.dhexx.cn/article/u5HVJ7av.shtml

相关文章

51单片机 智能小车

主函数中extern char speed 是在整个工程中与别处的speed共用一个内存 sprintf字串格式化命令&#xff0c;主要功能是把格式化的数据写入某个字符串中。 一、小车组装、用面包板供电、单片机也可以用面包板供电&#xff0c;必须共地、分文件&#xff08;模块化编程&#xff09…

C51单片机 SIM900A 手机给门禁解锁 智能家居 【C程序代码已开源,更新至Ver4.0】...

为何要实施这个项目? 一般小区楼下,每幢都会有一个门禁系统, 可以使用钥匙打开门禁, 有的可以使用刷卡打开门禁, 有的可以使用密码打开门禁, 但是,长期下来,因为小区人多,锁的钥匙孔会被捅坏,物业修起来也很怠慢 使用密码按键解锁门禁,按键也会失灵 每家都会安装一个…

基于STC89C51单片机,CH340芯片的下载电路

前几天看到了卓晴公众号写的三篇关于STC系列单片机的下载电路&#xff0c;然后结合了其他的文章&#xff0c;对下载电路有了更加深入的了解&#xff0c;在这里我梳理一下自己的学习过程&#xff0c;附上完整的资料&#xff0c;方便日后查看&#xff0c;也方便有兴趣的同学交流讨…

C51单片机简易密码锁(课程设计)

已经过测试&#xff0c;全部可用。手机好像不能发博客&#xff0c;相册提取图片代码粘黏复制可用。 本密码锁用于开门关门。绿灯保持常亮状态&#xff0c;表示一直通电状态。如果处于开门状态&#xff0c;则红灯也会亮起。&#xff08;单片机上无法显示红绿灯&#xff0c;仿真…

51单片机的架构与原理(STC89C51)

前言 学习单片机&#xff0c;不管怎么学都是要走一些弯路的&#xff0c;有的人弯路走得多&#xff0c;有多人弯路走得少&#xff0c;有些人中途就放弃了&#xff0c;也有不少人成功上岸&#xff0c;掌握了各式各样的单片机、处理器。用51单片机来学习是最合适不过的&#xff0c…

C51单片机利用HC-05蓝牙模块实现手机点灯

蓝牙通信&#xff0c;实现手机端远程(15米)控制单片机&#xff0c;这听起来对于初学者的我来说&#xff0c;是那么不可思议&#xff0c;经过不断地实验&#xff0c;阅读其他大佬的文章&#xff0c;终于实现了。 在有基础的通信原理知识的前提下&#xff0c;知&#xff0c;HC-05…

基于C51单片机的ESP8266Wifi模块(ESP-01s)编程(详细)

1、模块简介 ESP-01S 是由安信可科技开发的 Wi-Fi 模块&#xff0c;该模块核心处理器为ESP8266。ESP8266 拥有完整的且自成体系的 Wi-Fi 网络功能&#xff0c;既能够独立应用&#xff0c;也可以作为从机搭载于其他主机 MCU 运行。当 ESP8266 独立应用时&#xff0c;能够直接从外…

(可远程)开源手机app控制c51单片机,附微信小程序控制

开源手机app控制c51单片机 第一 、先上效果图第二、原理讲解第三、下载程序到c51第四 app inventor 开发第五、微信小程序开发第六 可能遇到的一些小问题 第一 、先上效果图 第二、原理讲解 原理简述&#xff1a;利用发布订阅模式。第一步&#xff0c;新建主题&#xff0c;第二…

java实例内部类

内部类&#xff1a;可以将一个类定义在另一个类或者一个方法的内部。描述一个事物的一个完整结构的描述。内部类包括&#xff1a;实例内部类&#xff08;普通内部类&#xff09;&#xff0c;静态内部类&#xff0c;局部内部类&#xff08;定义在方法里&#xff0c;几乎不用&…

Java内部类介绍 - 局部内部类和匿名内部类

文章目录 内部类局部内部类匿名内部类匿名内部类介绍常见的使用形式开发中使用场景 内部类 局部内部类 局部内部类 &#xff08;鸡肋语法&#xff0c;了解即可&#xff09; 局部内部类, 在方法、代码块、构造器等执行体中创建一个类, 称为为局部内部类(但是我们一般都不会这样…

夯实Java基础系列8:深入理解Java内部类及其实现原理

本系列文章将整理到我在GitHub上的《Java面试指南》仓库&#xff0c;更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial 喜欢的话麻烦点下Star、Fork、Watch三连哈&#xff0c;感谢你的支持。 文章首发于我的个人博客&#xff1a; www.how2playlife.c…

初识JAVA内部类

1 内部类概述 如果一个类存在的意义就是为指定的另一个类&#xff0c;可以把这个类放入另一个类的内部。 就是把类定义在类的内部的情况就可以形成内部类的形式。 A类中又定义了B类&#xff0c;B类就是内部类,B类可以当做A类的一个成员看待: 2 特点 1) 内部类可以直接访问外…

java 内部类怎么调用成员变量_JAVA内部类之成员内部类

JAVA有一个特殊的类形式——内部类(这个词有点为难普通话不好的南方朋友)。今天我们就来聊聊内部类的一种&#xff1a;成员内部类。 如何定义成员内部类 那要如何定义成员内部类呢。 既然叫成员内部类&#xff0c;说明和成员变量是有类似的地方。从代码的层次结构上来看他是和成…

Java内部类(匿名内部类)

Java内部类&#xff08;匿名内部类&#xff09; 一、内部类二、静态内部类三、成员内部类四、局部内部类&#xff08;了解即可&#xff09;五、匿名内部类&#xff08;重要&#xff09; 一、内部类 1、概述 内部类就是一个定义在一个类里面的类&#xff0c;里面的类可以理解为…

Java 内部类的四种实现方式

Java 内部类的四种实现方式 前言 今天的话我将为大家介绍Java的四种内部类&#xff0c;他们分别是普通内部类&#xff0c;静态内部类&#xff0c;局部内部类&#xff0c;匿名内部类。 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、内部类是什么&…

Java内部类

Java内部类 内部类成员内部类局部内部类匿名内部类静态内部类常规面试案例内部类实例化对象重名变量的引用 内部类 是什么&#xff1f; 字面意思&#xff0c;在类的内部编写的类就叫内部类&#xff01; 为了方便区分、学习、理解我们一般将内部类分为以下几种&#xff01; 内部…

java内部类赋值_详解 Java 内部类

内部类在 Java 里面算是非常常见的一个功能了,在日常开发中我们肯定多多少少都用过,这里总结一下关于 Java 中内部类的相关知识点和一些使用内部类时需要注意的点。 从种类上说,内部类可以分为四类:普通内部类、静态内部类、匿名内部类、局部内部类。我们来一个个看: 普通…

java new 一个内部类_java内部类

java内部类的几种类型:成员内部类,静态内部类,方法内部类,匿名内部类。 成员内部类:成员内部类是类内部的非静态类。成员内部类不能定义静态方法和变量(final修饰的除外)。这是因为成员内部类是非静态的,类初始化的时候先初始化静态成员,如果允许成员内部类定义静态变量…

java中的内部类

java中的内部类主要分为两类四种&#xff1a; 第一类&#xff1a;定义在外部类局部位置上&#xff0c;分为局部内部类( 有 类名)、匿名内部类(没有类名)。 第二类&#xff1a;定义在外部类的成员位置上&#xff0c;分为成员内部类(没有static修饰)、静态内部类(使用static修饰…

java内部类最全详解

1、成员内部类 内部类与外部类的关系 a.成员内部类的创建需要依赖于外部类对象-&#xff08;成员方法必须通过对象调用&#xff09;&#xff0c;在没有外部类实例之前无法创建成员内部类对象 b.内部类与外部类相对独立&#xff0c;不是is a 的关系&#xff08;发动机-汽车&am…