【51单片机】程序实验14.I2C-EEPROM

发布于:2025-03-11 ⋅ 阅读:(16) ⋅ 点赞:(0)

主要参考学习资料:B站【普中官方】51单片机手把手教学视频

开发资料下载链接:http://www.prechin.cn/gongsixinwen/208.html

单片机套装:普中STC51单片机开发板A4标准版套餐7

I2C介绍

I2C物理层

I2C是Philip公司开发两线式同步串行总线,用于连接微控制器和外围设备,是微电子领域广泛采用的一种通信标准,具有接口线少、控制方法简单、器件封装形式小、通信速率较高的优点。

I2C总线的特点

①I2C总线有两根双向信号线,一根为时钟信号线SCL,实现收发同步;一根为数据信号线SDA,传输数据。

②I2C总线可以支持多设备连接,并支持多主机、多从机,而RS-232串口只支持单主机和多从机。

③设备在空闲时输出高阻态,而两个上拉电阻保证总线在所有设备空闲的状态下始终处于高电平。

④每个连到总线的设备都有独立的地址,主机可以利用地址访问不同的设备。

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

⑥I2C有三种传输模式,标准传输速率为100Kbit/s,快速传输速率为400Kbit/s,高速传输速率为3.4Mbit/s。

⑦连接到总线的设备数受到总线的最大电容400pF限制。

I2C总线的相关概念

主机:启动数据传输并产生时钟信号的设备。

从机:被主机寻址的设备。

多主机:同时有多个主机尝试控制总线。

仲裁:在多个主机同时尝试控制总线时只允许一个主机控制主线并使传输不被破坏的过程。

同步:两个或多个器件同步时钟信号的过程。

发送器:发送数据到总线的器件。

接收器:从总线接收数据的器件。

I2C协议层

数据有效性规定

总线进行数据传输的过程中,SCL为高电平时SDA的数据必须保持稳定,SCL为低电平时SDA的数据允许变化。

数据传输时以字节为单位,可以进行多字节发送。

起始和终止信号

起始信号:SCL为高电平时,SDA由高电平变为低电平;

终止信号:SCL为高电平时,SDA由低电平变为高电平。

起始信号和终止信号由主机发出作为数据传输的开始和结束,起始信号产生之后总线处于占用状态,终止信号产生之后总线处于空闲状态。

应答响应

请添加图片描述

主机传输完一个字节的数据之后会释放对SDA的控制,转而紧跟一个通过由从机控制SDA来实现的校验位。校验位即数据和地址传输过程中的响应,响应包括应答(ACK)和非应答(NACK)。从机接收到I2C传输的一个字节的数据或地址后,如果希望主机继续发送数据,就需要向主机发送一个ACK,ACK是一个特定的低电平脉冲信号;如果希望主机结束传输,就需要向主机发送一个NACK,主机则产生终止信号,NACK是一个特定的高电平脉冲信号。若从机无响应,SDA也会由于上拉电阻的作用处于高电平使主机结束数据传输。

在从机返回数据到主机的模式下,从机传输数据,而主机对数据进行响应,但是终止信号只能由主机产生,即主机作出非应答响应后紧接着发出终止信号。

总线的寻址方式

I2C总线寻址按照从机地址位数分为两种方式,一种为7位,一种为10位。

7位寻址时,第1位到第7位组成了从机的地址,第0位是数据传输的方向控制位。若第0位为0,则主机向从机写入数据;若为1,则主机向从机读取数据。

10位寻址和7位寻址兼容,而且可以结合使用。

以7位地址为例,当主机发送一个地址后,总线上的每个器件都会将第1位到第7位与自己的地址进行比对,如果一样则器件会判断自己被主机寻址了,进而根据第0位选择作为发送端还是接收端。

为了满足同时访问多个从机的情况,从机地址分为固定部分和可编程部分,可编程部分的位数决定了根据固定部分最多能同时访问到的从机数量,3位可编程位最多有 2 3 = 8 2^3=8 23=8个从机可以被同时访问。

数据传输

I2C传输的数据是广义的,既包括地址信号也包括真正的数据信号。每次数据传输必须由主机产生的起始信号开始,由主机产生的终止信号结束。若主机想继续占用总线进行新的数据传输,可不产生终止信号而是再次产生起始信号,对另一个从机进行寻址。主机在起始信号之后必须传输一个从机地址,最后一位是数据的传输方向决定读写方式。以下是三种传输情况:

S:起始信号;0/1:读写控制位;A/ A ˉ \bar A Aˉ:应答/非应答;P:终止信号。

灰色为主机产生的信号,白色为从机产生的信号。在主机向从机传输数据时,主机可以无视从机的应答情况选择产生终止信号或新的起始信号。

AT24C02介绍

AT24C02是板载使用的EEPROM芯片,其保存的数据不会因掉电而丢失,可存放重要数据。AT24C系列根据存储容量分为不同的型号,AT24C02即为2Kbit(256位)容量的芯片。

该芯片通过I2C总线进行操作,SCL和SDA为I2C总线接口管脚,通常会接对应的上拉电阻。A0/1/2为地址输入管脚,对应3个可编程地址位,因此最多可挂载8个相同的芯片到总线上,而高4位地址固定为1010。VSS为接地管脚,VCC为电源管脚。WP为写保护功能管脚,若接地则允许数据进行正常读写操作,若接VCC则只能读不能写。

上图为该芯片的通信时序图,符合I2C的通信时序,主要结合厂家芯片介绍作为程序中延时范围的参考。

硬件设计

51单片机通过P21、P20两个IO口来模拟I2C的时序与AT24C02芯片进行通信。由于只板载了一个AT24C02芯片,其三个可编程地址位均接地,默认为000。

实验14.I2C-EEPROM

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

多文件工程创建

本次实验涉及的功能较多,全部放在主函数文件中会使程序不利于阅读和管理。之前的实验项目都直接在文件夹中创建,为了便于后续代码的移植、管理和维护,我们需要创建多文件工程。

文件夹与组

前期操作与单文件工程相同,首先准备好存放项目的文件夹,在Keil中通过Project→New μVision Project将新项目保存在刚刚的文件夹中,芯片类型选择AT89C52,确认后弹出的小窗口点击否。

接下来在项目文件夹中创建几个文件夹对不同文件进行管理:

App:存放外设相关的驱动文件;

Obj:存放Keil编译产生的C语言、汇编、链接的列表清单、调试信息和烧录的hex文件等杂项(在Keil5版本中软件已经可以自动创建Objects和Listings,无需手动创建);

Public:存放不同程序共有的代码文件,例如延时函数、重定义;

User:存放主函数文件。

在Keil中创建组,以便在不同文件夹中创建文件,将系统给的Source Group 1组重命名,然后添加别的组:

创建main.c文件并保存到User文件夹:

双击左栏User或在组配置界面将main.c文件添加到User组:

同理创建public.c和public.h到Public文件夹,并将public.c添加到Public组中。public.h为头文件,通过调用源文件的头文件可以使用源文件定义的函数、变量等。后续其他头文件都将用到public.h头文件,在不同型号单片机之间移植程序时只需更改public.h头文件即可,十分方便。

额外的头文件需要手动添加其路径:

头文件与源文件

头文件的基本框架如下:

#ifndef _name_H //如果未定义当前头文件,执行下列语句至#endif
#define _name_H //定义当前头文件

/*
头文件内容可以是:
宏定义
声明源文件的函数
包含其他头文件
*/

#endif

该框架的作用为防止一个源文件重复包含同一个头文件。

源文件的基本框架如下:

#include “name.h”

/*
源文件的内容一般是定义函数
*/

public

public.c

#include "public.h"

//两个延时函数
void delay_10us(u16 time)
{
	while(time --);
}

void delay_ms(u16 ms)
{
	u16 i, j;
	for(i = ms;i > 0;i--)
		for(j = 110;j > 0;j--);
}

public.h

#ifndef _public_H
#define _public_H

//包含51单片机头文件
#include "reg51.h"

//类型重定义
typedef unsigned char u8;
typedef unsigned int u16;

//声明源文件中的延时函数
void delay_10us(u16 time);
void delay_ms(u16 ms);

#endif
key与smg

以同样的方式在App文件夹中创建key、smg文件夹,创建独立按键和动态数码管的驱动key.c、key.h、smg.c、smg.h到对应文件夹中,将key.c和smg.c添加到App组,并添加key.h和smg.h的头文件路径。

key.c

#include "key.h"

//独立按键扫描函数来自程序实验5
u8 key_scan(u8 mode)
{
	static u8 key = 1;
	if(mode)
		key = 1;
	if(key == 1 && (KEY1 == 0 || KEY2 == 0 || KEY3 == 0 || KEY4 == 0))
	{
		key = 0;
		delay_10us(1000);
		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;
}

key.h

#ifndef _key_H
#define _key_H

//需要用到public.h中的类型重定义
#include "public.h"

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

//声明源文件中按键扫描函数
u8 key_scan(u8 mode);

#endif

smg.c

程序实验4的动态数码管显示函数无法自定义显示的数字和位置,因此需要修改:

#include "smg.h"

//存储段码值
gseg_code[16] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0x77, 0x7c, 0x39, 0x5e, 0x79, 0x71};

//参数dat指定每个数码管显示的数字,pos指定从第几个数码管开始显示
void seg_display(u8 dat[], u8 pos)
{
	u8 i = 0;
	//数码管从0开始编号因此将pos减一
	u8 pos_temp = pos - 1;
	//数码管从pos_temp开始顺次显示
	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;
		}
		//将dat[]转换为段码值,并通过i-pos_temp使其从第0个数字开始显示
		SEG_A_DP_PORT = gseg_code[dat[i-pos_temp]];
		delay_10us(100);
		SEG_A_DP_PORT = 0x00;
	}
}

smg.h

#ifndef _smg_H
#define _smg_H

#include "public.h"

#define SEG_A_DP_PORT P0

//定义位选管脚
sbit LSA = P2^2;
sbit LSB = P2^3;
sbit LSC = P2^4; 

//在头文件中声明新变量时需使用extern关键字且不能赋值
extern u8 gseg_code[16];

void seg_display(u8 dat[], u8 pos);

#endif
iic

该驱动负责I2C总线的基本操作。

在App>icc中创建:

iic.h

#ifndef _iic_H
#define _iic_H

#include "public.h"

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

//声明源文件中的函数
void iic_start(void);
void iic_stop(void);
void iic_ack(void);
void iic_nack(void);
u8 iic_wait_ack(void);
void iic_write_byte(u8 dat);
u8 iic_read_byte(u8 ack);

#endif

iic.c

#include "iic.h"

//起始信号函数
void iic_start(void)
{
	//起始信号之前SCL和SDA均为高电平
	IIC_SCL = 1;
	IIC_SDA = 1;
	//SDA先产生下降沿,SCL再变为低电平
	delay_10us(1); //芯片延时为纳秒级,为兼容不同单片机放宽为微秒级
	IIC_SDA = 0;
	delay_10us(1);
	IIC_SCL = 0;
}

//终止信号函数
void iic_stop(void)
{
	//终止信号之前SDA先保持低电平,SCL后进入高电平
	IIC_SDA = 0; 
	IIC_SCL = 1;
	//SDA产生上升沿
	delay_10us(1);
	IIC_SDA = 1;
	delay_10us(1);
}

//应答信号函数
void iic_ack(void)
{
	//在SCL为低电平时更改SDA为低电平
	IIC_SCL = 0;
	IIC_SDA = 0;
	//SCL产生高电平脉冲
	delay_10us(1);
	IIC_SCL = 1;
	delay_10us(1);
	IIC_SCL = 0;
}

//非应答信号函数,将应答信号中SDA赋值改为1即可
void iic_nack(void)
{
	IIC_SCL = 0;
	IIC_SDA = 1;
	delay_10us(1);
	IIC_SCL = 1;
	delay_10us(1);
	IIC_SCL = 0;
}

//主机等待从机应答信号函数,返回应答情况
u8 iic_wait_ack(void)
{
	//计数器
	u8 time_temp = 0;
	//将SCL拉高以读取SDA信号
	IIC_SCL = 1;
	delay_10us(1);
	//读取SDA信号
	while(IIC_SDA)
	{
		time_temp++;
		//若SDA长时间为高电平则判断为非应答
		if(time_temp > 100)
		{
			//产生终止信号并返回应答情况
			iic_stop();
			return 1;
		}
	}
	//若SDA为低电平则判断为应答跳出循环
	//将SCL拉低以继续传输数据
	IIC_SCL = 0;
	//返回应答情况
	return 0;
}

//字节写入函数
void iic_write_byte(u8 dat)
{
	//控制8位字节依次传输的循环变量
	u8 i = 0;
	//将SCL拉低以写入
	IIC_SCL = 0;
	//将数据按位数从高到低传输
	for(i=0;i<8;i++)
	{
		//将数据和10000000进行与运算以判断最高位数字
		if((dat & 0x80) > 0)
			//最高位为1,与运算结果为10000000,写入1
			IIC_SDA = 1;
		else
			//最高位为0,与运算结果为00000000,写入0
			IIC_SDA = 0;
		//将数据左移一位使次高位成为下次写入的最高位
		dat <<= 1;
		//SCL产生高电平脉冲准备写入下一位
		IIC_SCL = 1;
		delay_10us(1);
		IIC_SCL = 0;
		delay_10us(1);
	}
}

//字节读取函数,参数ack控制读取完后响应何种应答
u8 iic_read_byte(u8 ack)
{
	//控制8位数据依次读取的循环变量
	u8 i = 0;
	//存储读取到的数据的变量
	u8 receive = 0;
	//将数据按位数从高到低读取
	for(i=0;i<8;i++)
	{
		//SCL产生高电平脉冲时进行读取
		IIC_SCL = 0;
		delay_10us(1);
		IIC_SCL = 1;
		//读取从最高位开始,因此每次左移一位
		receive <<= 1;
		//若SDA为高电平则将当前最低位由0加到1
		if(IIC_SDA)receive++;
	}
	//ack为0则非应答,为1则应答
	if(!ack)iic_nack();
	else iic_ack();
	//返回读取到的数据
	return receive;
}
at24c02

该驱动负责向AT24C02读写数据。板载只有一个AT24C02,3个可编程地址位默认接地,因而芯片的从机地址为1010000(0xA0)。AT24C02内部有256字节,也就有256个可以存放数据的字节地址,在读写数据之前,均需先传输指定芯片内部地址的数据再继续操作。

在App>at24c02中创建:

at24c02.h

#ifndef _at24c02_H
#define _at24c02_H

#include "public.h"

//声明源文件中的函数
void at24c02_write_1byte(u8 addr, u8 dat);
u8 at24c02_read_1byte(u8 addr);

#endif

at24c02.c

#include "at24c02.h"
#include "iic.h"

//单字节写入函数
void at24c02_write_1byte(u8 addr, u8 dat)
{
	//产生起始信号
	iic_start();
	//写入从机地址并指定写模式
	iic_write_byte(0xA0);
	iic_wait_ack();
	//写入要存放数据的芯片内部地址
	iic_write_byte(addr);
	iic_wait_act();
	//写入单字节数据
	iic_write_byte(dat);
	iic_wait_ack();
	//产生终止信号
	iic_stop();
	delay_ms(10);
}

//单字节读取函数
u8 at24c02_read_1byte(u8 addr)
{
	//存储数据的变量
	u8 temp = 0;
	//产生起始信号
	iic_start();
	//写入从机地址并指定写模式
	iic_write_byte(0xA0);
	iic_wait_ack();
	//写入要读取数据的芯片内部地址
	iic_write_byte(addr);
	iic_wait_ack();
	//产生新的起始信号以改变读写模式
	iic_start();
	//写入从机AT24C02地址并指定读模式
	iic_write_byte(0xA1);
	iic_wait_ack();
	//读取单字节数据并存入变量
	temp = iic_read_byte(0);
	//产生终止信号
	iic_stop();
	//返回读取到的数据
	return temp;
}

main.c

#include "public.h"
#include "key.h"
#include "smg.h"
#include "at24c02.h"
//无需包括iic.h,因为只在at24c02.h中用到

//宏定义EEPROM从机地址
#define EEPROM_ADDRESS 0

void main()
{
	//存储按键扫描返回值的变量
	u8 key_temp = 0;
	//存储数码管显示数据的变量
	u8 save_value = 0;
	//存储save_value各个位上数字的数组
	u8 save_buf[3];
	while(1)
	{
		//扫描按键
		key_temp = key_scan(0);
		//响应不同按键按下的情况
		switch(key_temp)
		{
			//K1按下,将数码管显示数据写入EEPROM
			case 1:at24c02_write_1byte(EEPROM_ADDRESS, save_value);break;
			//K2按下,从EEPROM读取数据显示到数码管
			case 2:save_value = at24c02_read_1byte(EEPROM_ADDRESS);break;
			//K3按下,数码管显示数据加一
			case 3:save_value++;if(save_value == 255)save_value = 255;break;
			//K4按下,数码管显示数据清零
			case 4:save_value = 0;break;
		}
		//将save_value按位拆分存储到数组中
		save_buf[0] = save_value / 100;
		save_buf[1] = save_value % 100 / 10;
		save_buf[2] = save_value % 10;
		//将save_value按位依次显示在后三个数码管中
		seg_display(save_buf, 6);
	}
}