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


    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();
    	}  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113

    i2c.h

    #ifndef _I2C_H
    #define _I2C_H
    
    #include 
    
    // 定义时钟线和数据线
    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    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.7us
    	SDA = 1;
    	Delay10us();
    	SCL=1;	// SDA/SCL = H
    	Delay10us();
    	SDA=0;	// SDA H->L
    	Delay10us();
    	SCL=0;  // SCL=L时,数据可以改变   
    	Delay10us();
    }
    
    // 模拟终止信号
    void I2CStop()
    {
    	// SCL=H,SCL=L->H,delay4.7us
    	SDA = 0;
    	Delay10us();
    	SCL=1;
    	Delay10us();
    	SDA=1;	// SDA L->H
    	Delay10us();
    }
    
    // 模拟发送数据
    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  ->  0
    		dat |= 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;			
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140

    结果:初始读取为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语言版)
  • 相关阅读:
    如何找出最优的【SVC】核函数和参数值—以乳腺癌数据集为例
    【数据分享】1901-2022年1km分辨率逐年降水栅格数据(免费获取/全国/分省)
    DRV8812RHDR 集成电机驱动器 8.2-45V 28QFN
    vue面试题
    给电脑换上鸿蒙字体吧~
    【Hello Algorithm】 暴力递归到动态规划 -- 总结
    Windows下安装配置Nginx
    FFmpeg开发笔记(十八)FFmpeg兼容各种音频格式的播放
    保姆级教程,教你AI数字人应该怎么制作?!
    Spring Boot Security自带功能齐全的HttpFirewall防火墙
  • 原文地址:https://blog.csdn.net/sinat_41752325/article/details/127710996