51单片机——I2C-EEPROM

I2C:总线标准或通信协议

EEPROM:AT24C02芯片

开发板板载了1个EEPROM模块,可实现IIC通信

1、EEPROM模块电路(AT24C02)

芯片的SCL和SDA管脚是连接在单片机的P2.1和P2.0上

2、I2C介绍

I2C(Inter-Integrated Circuit)总线是由PHILIPS公司开发的两线式串行总线,用于连接微控制器(MCU)及其外围设备。是微电子通信控制领域广泛采用的一种总线标准。它是同步通信的一种特殊形式,具有接口线少,控制方式简单,器件封装形式小,通信速率较高等优点

I2C 总线只有两根双向信号线。一根是数据线SDA,另一根是时钟线SCL

2.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限制(接5-6个没有问题)

2.2 I2C协议层(作用于MCU)

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

2.2.1 数据有效性规定

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

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

每一个字节必须保证是8位长度。数据传送时,先传送最高位

2.2.1.1 读数据的代码

SCL为高电平时才能读数据

//6、读字节

u8 iic_read_byte(u8 ack){

u8 i=0,receive=0;

for(i=0;i<8;i++){

IIC_SCL=0; //开始时SCL为低电平

delay_10us(1); //有一个延时,是因为高低电平变化时,会有一个变化时间,需要延时一下

IIC_SCL=1; //往后走变为高电平

receive<<=1;

if(IIC_SDA==1){

receive++;

}

delay_10us(1);

}

if(!ack){

iic_ack();

}else{

iic_nack();

}

return receive;

}

2.2.1.2 写数据的代码

void iic_write_byte(u8 dat){

u8 i=0;

IIC_SCL=0; //为0时,数据可以改变

//循环8次,将一个字节传出去

//要求:先传高位,再传低位

for(i=0;i<8;i++){

if((dat&0x80)>0){

IIC_SDA=1;

}else{

IIC_SDA=0;

}

dat<<=1; //把次高位变为最高位

delay_10us(1);

IIC_SCL=1;

delay_10us(1);

IIC_SCL=0;

delay_10us(1);

}

}

2.2.2 起始和停止信号

起始条件:SCL线为高电平期间,SDA线由高电平向低电平的变化表示起始信号

终止条件:SCL线为高电平期间,SDA线由低电平向高电平的变化表示终止信号

如下图所示:

2.2.2.1 起始信号

void iic_start(){

IIC_SDA=1;

IIC_SCL=1;

delay_10us(1);

IIC_SDA=0; //SDA先变为低电平,先写SDA

// delay_10us(1); //可以加,也可以不加

IIC_SCL=0; //拉低后,就准备发送和接收数据

// delay_10us(1); //可以加,也可以不加

}

2.2.2.2 停止信号

void iic_stop(){

IIC_SDA=0;

IIC_SCL=1;

delay_10us(1);

IIC_SDA=1;

IIC_SCL=1; //写不写都可以

}

2.2.3 应答响应

每当发送器件传输完一个字节(长度:8)的数据后,后面必须紧跟一个校验位,这个校验位是接收端通过控制 SDA(数据线)来实现的,以提醒发送端数据我这边已经接收完成,数据传送可以继续进行。这个校验位其实就是数据或地址传输过程中的响应

响应包括"应答(ACK)"和"非应答(NACK)"两种信号

作为数据接收端时,当设备(无论主从机)接收到I2C传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送"应答(ACK)"信号即特定的低电平脉冲, 发送方会继续发送下一个数据

若接收端希望结束数据传输,则向对方发送"非应答(NACK)"信号即特定的高电平脉冲,发送方接收到该信号后会产生一个停止信号,结束信号传输

应答响应时序图如下图所示:

发送应答:在接收完一个字节之后,主机在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答

接收应答:在发送完一个字节之后,主机在下一个时钟接收一位数据,判断从机是否应答,数据1表示非应答(主机在接收之前,需要释放SDA)

2.2.3.1 应答0

void iic_ack(){

IIC_SCL=0;

IIC_SDA=0; //应答

delay_10us(1);

IIC_SCL=1;

delay_10us(1);

IIC_SCL=0;

}

2.2.3.2 非应答1

void iic_nack(){

IIC_SCL=0;

IIC_SDA=1; //非应答

delay_10us(1);

IIC_SCL=1;

delay_10us(1);

IIC_SCL=0;

}

2.2.3.3 等待应答

返回值为0应答;返回值为1非应答

u8 iic_wait_ack(){

u8 time_temp=0; //注意:定义变量时放在上方,否则容易出现问题

IIC_SCL=1;

delay_10us(1);

//意外情况,没有应答

while(IIC_SDA){ //等待IIC_SDA出现低电平

time_temp++;

if(time_temp>100){ //超时了,强制退出

iic_stop();

return 1;

}

}

IIC_SCL=0;

return 0;

}

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

2.2.4 总线的寻址方式

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

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

(2)AT24C02器件地址为7位,高4位固定为1010,低3位由A0/A1/A2信号线的电平决定。因为传输地址或数据是以字节为单位传送的,当传送地址时,器件地址占7位,还有最后一位(最低位R/W)用来选择读写方向,它与地址无关。其格式如下图所示:

如果要对芯片进行写操作时,R/W 即为0,写器件地址即为0XA0;如果要对芯片进行读操作时,R/W 即为1,此时读器件地址为0XA1

(1)和(2)连起来看,两个图代表的意思一样

2.2.5 数据传输

在起始信号后必须传送一个从机的地址(7位),第8位是数据的传送方向位(R/W),用"0"表示主机发送(写)数据(W),"1"表示主机接收(读)数据(R)

2.2.5.1 写数据

有阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从机向主机传送。A 表示应答,A 非表示非应答(高电平)。S表示起始信号,P表示终止信号

void at24c02_write_one_byte(u8 addr,u8 dat){ //addr:at24c02的地址

iic_start(); //S

iic_write_byte(0xa0); //1010 0000 //从机地址和0

iic_wait_ack(); //A

iic_write_byte(addr); //指定地址 //寄存器(at24c02)地址

iic_wait_ack(); //A

iic_write_byte(dat); //数据

iic_wait_ack(); //A/A非

iic_stop(); //P

delay_ms(10);

}

2.2.5.2 读数据

有阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从机向主机传送。A 表示应答,A 非表示非应答(高电平)。S表示起始信号,P表示终止信号

u8 at24c02_read_one_byte(u8 addr){ //addr:at24c02的数据地址

u8 temp=0;

iic_start(); //S

iic_write_byte(0xa0); //1010 0000 //从机地址和0

iic_wait_ack(); //A

iic_write_byte(addr); //指定地址 //寄存器(at24c02)地址

iic_wait_ack(); //A/A非

iic_start(); //S

iic_write_byte(0xa1); //1010 0001 //从机地址和1

iic_wait_ack(); //A

temp=iic_read_byte(1); //读时从当前地址开始读(因为上方已经指定过at24c02的地址了,所以不需要再次指定at24c02的地址 )

iic_stop(); //P

return temp;

}

3、软件设计

3.1 创建多文件工程

3.1.1 创建文件夹

在电脑上创建一个实验文件夹,为了与教程配套,这里命名为"I2C-EEPROM实验",然后在该文件夹内新建App、Public、User三个文件夹,如下图所示:

Listings和Objects是软件自动生成的

App文件夹:用于存放外设驱动文件,如LED、数码管、定时器等(24c02、iic、key、smg四个文件夹)

Public文件夹:用于存放51单片机公共的文件,如延时、51头文件、变量类型重定义等。

User文件夹:用于存放用户主函数文件,如main.c

3.1.2 新建工程

首先打开KEILC51软件,新建一个工程,将工程命名为template并保存在"I2C-EEPROM实验"文件夹下,然后选择芯片类型为"AT89C52",不使用系统创建启动文件

3.1.3 向工程添加文件

(1)将含有.c文件的文件夹添加到工程中,这里我在工程中创建3组,User、App、Publi,通常在工程组的命名与创建的文件夹名保持一致,方便查找到源文件位置

(2)点击下图中的图标,创建新文件,Ctrl+S将文件重命名,并保存到对应的文件夹中

例如:创建新文件,Ctrl+S将文件重命名为public.c,并保存到public文件夹中

(3)这样每一个文件夹中都有一个.c和一个.h文件(文件名和文件夹名一样),之后需要将建好的文件添加到(1)创建的工程中

App:24c02.c、iic.c、key.c、smg.c

Public:public.c

User:main.c

3.1.4 配置魔术棒选项卡

(1)点击下图中的图标

(2) 点击Output选项卡,将CreateHEXFile选项勾上

(3)点击C51选项卡,将前面添加到工程组中的文件路径包括进来,否则程序中调用其他文件夹的头文件则会报错找不到头文件路径

3.2 实验代码

要实现的功能是:系统运行时,数码管右3位显示0,按K1键将数据写入到EEPROM内保存,按K2键读取EEPROM内保存的数据,按K3键显示数据加1,按K4键显示数据清零,最大能写入的数据是255

一般我们以文件形式存放对应功能的驱动程序时,会创建2个文件,一个是.c源文件,另一个是.h头文件。源文件(.c)通常存放的是外设的驱动程序,比如按键检测函数;而头文件(.h)通常用 来存放管脚定义、变量声明、函数声明

3.2.1 public文件

3.2.1.1 public.h

//头文件中放置函数的声明、全局变量的定义
#ifndef _public_H
#define _public_H

#include "reg52.h"

//全局变量

typedef unsigned int u16;

typedef unsigned char u8;

//两个延迟函数声明

void delay_10us(u16 us);

void delay_ms(u16 ms);
#endif

在头文件的开头,使用"#ifndef"关键字,判断标号"_public_H"是否被定义,若没有被定义,则从"#ifndef"至"#endif"关键字之间的内容都有效

这个头文件(public.h文件)若被其它文件"#include",它就会被包含到其该文件中,且头文件中紧接着使用"#define"关键字定义上面判断的标号"_public_H"。当这个头文件被同一个文件第二次"#include"包含的时候,由于有了第一次包含中的"#define _public_H" 定义,这时再判断"#ifndef _public_H",判断的结果就是假了,从"#ifndef" 至"#endif"之间的内容都无效,从而防止了同一个头文件被包含多次,编译时就不会出现"redefine(重复定义)"的错误了

3.2.1.2 public.c

#include "public.h"

void delay_10us(u16 us){

while(us--);

}

void delay_ms(u16 ms){

u16 i=0,j=0;

for(i=0;i<ms;i++){

for(j=0;j<110;j++);

}

}

3.2.2 独立按键

3.2.2.1 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;

u16 key_scan(u16 mode);
#endif

3.2.2.2 key.c文件

#include "key.h"

u16 key_scan(u16 mode){

static u16 key=1;

if(mode==1){

key=1;

}

if(key==1&&(KEY1==0||KEY2==0||KEY3==0||KEY4==0)){

delay_10us(1000);

key=0;

if(KEY1==0){

return 1;

}else if(KEY2==0){

return 2;

}else if(KEY3==0){

return 3;

}else if(KEY4==0){

return 4;

}

}else if(KEY1==1&&KEY2==1&&KEY3==1&&KEY4==1){

key=1;

return 0;

}

}

3.2.3 动态数码管

3.2.3.1 smg.h

#ifndef _smg_H

#define _smg_H

#include "public.h"

sbit LSA=P2^2;

sbit LSB=P2^3;

sbit LSC=P2^4;

#define SMG_A_DP_PORT P0

extern u8 gsmg_code[];

void smg_display(u8 save_buff[],u8 pos);

#endif

3.2.3.2 smg.c

#include "smg.h"

u8 gsmg_code[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};

//save_buff是一个u8类型的数组,方便外部传入要显示的数据

//pos是数码管从左开始第几个位置开始显示,取值范围是1-8

void smg_display(u8 save_buff[],u8 pos){

u16 i=0;

u16 pos_temp=pos-1;

for(i=pos_temp;i<8;i++){

//位选

switch(i){

case 0:

LSC=1,LSB=1,LSA=1; //7

break;

case 1:

LSC=1,LSB=1,LSA=0; //6

break;

case 2:

LSC=1,LSB=0,LSA=1; //5

break;

case 3:

LSC=1,LSB=0,LSA=0; //4

break;

case 4:

LSC=0,LSB=1,LSA=1; //3

break;

case 5:

LSC=0,LSB=1,LSA=0; //2

break;

case 6:

LSC=0,LSB=0,LSA=1; //1

break;

case 7:

LSC=0,LSB=0,LSA=0; //0

break;

}

SMG_A_DP_PORT=gsmg_code[save_buff[i-pos_temp]]; //save_buff[?](?:0、1、2)

delay_10us(100);

SMG_A_DP_PORT=0x00; //消隐

}

}

3.2.4 I2C读写字节函数

3.2.4.1 iic.h

#ifndef _iic_H

#define _iic_H

#include "public.h"

//定义管脚

sbit IIC_SCL=P2^1;

sbit IIC_SDA=P2^0;

//iic协议层的函数

//1、起始信号

void iic_start();

//2、停止信号

void iic_stop();

//3、应答

void iic_ack();

//4、非应答

void iic_nack();

//5、等待应答

u8 iic_wait_ack();

//6、读字节

u8 iic_read_byte(u8 ack);

//7、写字节

void iic_write_byte(u8 dat);

#endif

3.2.4.2 iic.c

#include "iic.h"

//iic协议层的函数

//1、起始信号

void iic_start(){

IIC_SDA=1;

IIC_SCL=1;

delay_10us(1);

IIC_SDA=0;

// delay_10us(1); //可以加,也可以不加

IIC_SCL=0; //拉低后,就准备发送和接收数据

// delay_10us(1); //可以加,也可以不加

}

//2、停止信号

void iic_stop(){

IIC_SDA=0;

IIC_SCL=1;

delay_10us(1);

IIC_SDA=1;

IIC_SCL=1; //写不写都可以

}

//3、应答

void iic_ack(){

IIC_SCL=0;

IIC_SDA=0; //应答

delay_10us(1);

IIC_SCL=1;

delay_10us(1);

IIC_SCL=0;

}

//4、非应答

void iic_nack(){

IIC_SCL=0;

IIC_SDA=1; //非应答

delay_10us(1);

IIC_SCL=1;

delay_10us(1);

IIC_SCL=0;

}

//5、等待应答,返回值为0应答;返回值为1非应答

u8 iic_wait_ack(){

u8 time_temp=0; //注意:定义变量时放在上方,否则容易出现问题

IIC_SCL=1;

delay_10us(1);

//意外情况,没有应答

while(IIC_SDA){ //等待IIC_SDA出现低电平

time_temp++;

if(time_temp>100){ //超时了,强制退出

iic_stop();

return 1;

}

}

IIC_SCL=0;

return 0;

}

//6、读字节

u8 iic_read_byte(u8 ack){

u8 i=0,receive=0;

for(i=0;i<8;i++){

IIC_SCL=0;

delay_10us(1);

IIC_SCL=1;

receive<<=1;

if(IIC_SDA==1){

receive++;

}

delay_10us(1);

}

//注意:看看测试时和案例是否一样(没有区别)

if(!ack){

iic_ack();

}else{

iic_nack();

}

return receive;

}

//7、写字节

void iic_write_byte(u8 dat){

u8 i=0;

IIC_SCL=0; //为0时,数据可以改变

//循环8次,将一个字节传出去

//要求:先传高位,再传低位

for(i=0;i<8;i++){

if((dat&0x80)>0){

IIC_SDA=1;

}else{

IIC_SDA=0;

}

dat<<=1; //把次高位变为最高位

delay_10us(1);

IIC_SCL=1;

delay_10us(1);

IIC_SCL=0;

delay_10us(1);

}

}

3.2.5 AT24C02读写字节函数

3.2.5.1 at24c02.h

#ifndef _at24c02_H

#define _at24c02_H

#include "public.h"

#include "iic.h"

//写入数据函数

void at24c02_write_one_byte(u8 addr,u8 dat);

//读数据函数

u8 at24c02_read_one_byte(u8 addr);

#endif

3.2.5.2 at24c02.c

#include "at24c02.h"

//写

void at24c02_write_one_byte(u8 addr,u8 dat){ //addr:at24c02的地址

iic_start();

iic_write_byte(0xa0); //1010 0000

iic_wait_ack();

iic_write_byte(addr); //指定地址

iic_wait_ack();

iic_write_byte(dat);

iic_wait_ack();

iic_stop();

delay_ms(10);

}

//读

u8 at24c02_read_one_byte(u8 addr){ //addr:at24c02的数据地址

u8 temp=0;

iic_start();

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(1); //读时从当前地址开始读

iic_stop();

return temp;

}

3.2.6 main.c

#include "public.h"

#include "smg.h"

#include "key.h"

#include "iic.h"

#include "at24c02.h"

#define EEPROM_ADDRESS 0 //不超过255即可

/*

系统运行时,数码管右3位显示0

按K1键将数据写入到EEPROM内保存

按K2键读取EEPROM内保存的数据

按K3键显示数据加1,最大能写入的数据是255(0-255)

按K4键显示数据清零

*/

void main(){

u8 key_temp=0;

u8 save_value=0; //可以不设为0

u8 save_buff[3];

while(1){

key_temp=key_scan(0);

if(key_temp==1){ //保存数据(写)

at24c02_write_one_byte(EEPROM_ADDRESS,save_value);

}else if(key_temp==2){ //读取数据

save_value=at24c02_read_one_byte(EEPROM_ADDRESS);

}else if(key_temp==3){ //数据+1

save_value++;

if(save_value==255){

save_value=255;

}

}else if(key_temp==4){ //数据清零

save_value=0;

}

//save_value是一个十进制的值

//让数码管显示数据,需要得到save_value个位、十位和百位的值

save_buff[0]=save_value/100; //百位

save_buff[1]=save_value/10%10; //十位

save_buff[2]=save_value%10; //个位

smg_display(save_buff,6);

}

}

相关推荐
SundayBear7 小时前
零基础入门MQTT协议
c语言·单片机
嗯嗯=8 小时前
STM32单片机学习篇9
stm32·单片机·学习
麦托团子12 小时前
51单片机学习笔记10-点阵屏
51单片机
松涛和鸣12 小时前
DAY63 IMX6ULL ADC Driver Development
linux·运维·arm开发·单片机·嵌入式硬件·ubuntu
想放学的刺客15 小时前
单片机嵌入式试题(第23期)嵌入式系统电源管理策略设计、嵌入式系统通信协议栈实现要点两个全新主题。
c语言·stm32·单片机·嵌入式硬件·物联网
猫猫的小茶馆16 小时前
【Linux 驱动开发】五. 设备树
linux·arm开发·驱动开发·stm32·嵌入式硬件·mcu·硬件工程
jghhh0117 小时前
基于上海钜泉科技HT7017单相计量芯片的参考例程实现
科技·单片机·嵌入式硬件
恶魔泡泡糖17 小时前
51单片机外部中断
c语言·单片机·嵌入式硬件·51单片机
意法半导体STM3218 小时前
【官方原创】如何基于DevelopPackage开启安全启动(MP15x) LAT6036
javascript·stm32·单片机·嵌入式硬件·mcu·安全·stm32开发
v_for_van18 小时前
STM32低频函数信号发生器(四通道纯软件生成)
驱动开发·vscode·stm32·单片机·嵌入式硬件·mcu·硬件工程