第19章 I2C-EEPROM实验
第十九章 I2C-EEPROM实验
1. 导入
这一章我们来学习如何使用 51 单片机的 IO 口模拟 I2C 时序, 并实现与AT24C02( EEPROM) 之间的双向通信。 开发板板载了 1 个 EEPROM 模块, 可实现IIC 通信。
本章要实现的功能是: 系统运行时, 数码管右 3 位显示 0, 按 K1 键将数据写入到 EEPROM 内保存, 按 K2 键读取 EEPROM 内保存的数据, 按 K3 键显示数据加 1, 按 K4 键显示数据清零, 最大能写入的数据是 255。
2. I2C介绍
I2C( Inter- Integrated Circuit) 总线是由 PHILIPS 公司开发的两线式串行总线, 用于连接微控制器及其外围设备。 是微电子通信控制领域广泛采用的一种总线标准。 它是同步通信的一种特殊形式, 具有接口线少, 控制方式简单,器件封装形式小, 通信速率较高等优点。 I2C 总线只有两根双向信号线。 一根是数据线 SDA, 另一根是时钟线 SCL。 由于其管脚少, 硬件实现简单, 可扩展性强等特点, 因此被广泛的使用在各大集成芯片内。
目前阶段我们不需要了解更多。可以参考:
I2C总线协议详解(特点、通信过程、典型I2C时序)-CSDN博客
【51单片机快速入门指南】4: 软件 I2C-电子工程世界 (eeworld.com.cn)
51单片机---I2C通信协议(含源码,小白可入)_ii2c写保护-CSDN博客
3. AT24C02介绍
AT24C01/02/04/08/16...是一个 1K/2K/4K/8K/16K 位串行 CMOS, 内部含有128/256/512/1024/2048 个 8 位字节, AT24C01 有一个 8 字节页写缓冲器, AT24C02/04/08/16 有一个 16 字节页写缓冲器。 该器件通过 I2C 总线接口进行操作, 它有一个专门的写保护功能。 我们开发板上使用的是 AT24C02( EEPROM)芯片, 此芯片具有 I2C 通信接口, 芯片内保存的数据在掉电情况下都不丢失,所以通常用于存放一些比较重要的数据等。 AT24C02 芯片管脚及外观图如下图所示:
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 总线时序如下图所示:
更多可以参考文件夹下面的芯片手册或者:嵌入式——EEPROM(AT24C02)_at24c02时序-CSDN博客
4. 硬件设计
本实验使用到硬件资源如下:
-
独立按键( K1-K4)
-
动态数码管
-
EEPROM 模块电路( AT24C02)
独立按键和动态数码管电路在前面章节都介绍过, 这里就不再重复。 下面我们来看下开发板上 EEPROM 模块电路, 如下图所示:
从上图中可以看出, 该电路是独立的, 芯片的 SCL 和 SDA 管脚接至 J4 端子上, 在介绍 IIC 总线的时候我们说过, 为了让 IIC 总线默认为高电平, 通常会在IIC 总线上接上拉电阻, 在图中可以看到 SCL 和 SDA 管脚有上拉电阻。
由于该模块电路是独立的, 所以 24C02 芯片的 SCL 和 SDA 管脚可以使用任意单片机管脚连接, 为了与我们例程程序配套, 这里使用单片机的 P2.0 管脚连接芯片的 SDA 脚, 使用单片机的 P2.1 脚连接芯片的 SCL 脚。
4. 模块化工程介绍
为了后期方便管理和维护,我们需要模块化编程,但是前面为什么一直没有提到-因为代码量小,只需要一个main.c就可以搞定,但是在实际工作中,工程结构和代码量肯定不会这么简单,所以我们需要分类也就是模块化。
在电脑上创建一个实验文件夹, 为了与教程配套, 这里命名为“ I2C-EEPROM实验” , 然后在该文件夹内新建 App、 Obj、 Public、 User 四个文件夹, 如下所示:
-
App文件夹:用于存放外设驱动文件,如LED、数码管、定时器等
-
Obj文件夹:用于存放编译产生的 c/汇编/链接的列表清单、 调试信息、 hex文件、 预览信息、 封装库等文件。
-
Public:用来存放51单片机公共的文件,如延时函数、51头文件、变量类型重定义等
-
User:用于存放用户主函数文件,如main.c
51单片机模块化编程_单片机分层设计,一个模块一个文件么-CSDN博客
5. 软件设计
从这一章起,我们的代码量要增多了,难度倒是没有增加多少,因为就是把以前的实验进行整合即把我们使用过的模块同时使用,以实现更强大的功能
本章所要实现的功能是: 系统运行时, 数码管右 3 位显示 0, 按 K1 键将数据写入到 EEPROM 内保存, 按 K2 键读取 EEPROM 内保存的数据, 按 K3 键显示数据加1,按 K4 键显示数据清零, 最大能写入的数据是 255。程序框架如下:
-
编写按键检测功能(我们已经写过)
-
编写数码管显示功能(我们已经写过)
-
编写 IIC 驱动, 包括起始、 停止、 应答信号等
-
编写 AT24C02 读写功能
-
编写主函数
5.1 按键检测函数
#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;
//使用宏定义独立按键按下的键值
#define KEY1_PRESS 1
#define KEY2_PRESS 2
#define KEY3_PRESS 3
#define KEY4_PRESS 4
#define KEY_UNPRESS 0
unsigned char key_scan(unsigned char mode); // 声明外部函数
#endif
#include "key.h"
// 按键扫描函数
unsigned char key_scan(unsigned char mode)
{
static unsigned key = 1;
if(mode)key = 1; // 连续扫描按键
if(key == 1 && (KEY1 == 0 || KEY2 == 0 || KEY3 == 0 || KEY4 == 0)) // 任意按键按下
{
delay_10us(1000);//消抖
key = 0;
if(KEY1 == 0)
return KEY1_PRESS; // 按键1按下
else if(KEY2 == 0)
return KEY2_PRESS; // 按键2按下
else if(KEY3 == 0)
return KEY3_PRESS; // 按键3按下
else if(KEY4 == 0)
return KEY4_PRESS; // 按键4按下
}
else if(KEY1 == 1 && KEY2 == 1 && KEY3 == 1 && KEY4 == 1) // 无按键按下
{
key = 1;
}
return KEY_UNPRESS;
}
按键检测函数我们在前面的章节就已经讲过,应该不用再重复了,并且我也加了注释
5.2 数码管显示函数
#ifndef _smg_H
#define _smg_H
#include "public.h"
#define SMG_A_DP_PORT P0 //使用宏定义数码管段码口
//定义数码管位选信号控制脚
sbit LSA = P2^2;
sbit LSB = P2^3;
sbit LSC = P2^4;
void smg_display(unsigned char dat[], unsigned char pos);
#endif
#include "smg.h"
//共阴极数码管显示0~F的段码数据
unsigned char gsmg_code[17]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,
0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};
// 数码管显示函数
void smg_display(unsigned char dat[],unsigned char pos)
{
unsigned char i = 0;
unsigned char pos_temp = pos-1;
for(i = pos_temp; i<8; i++)
{
switch(7 - 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;
}
SMG_A_DP_PORT = gsmg_code[dat[i-pos_temp]]; // 传送段选数据
delay_10us(100); // 延时一段时间,等待显示稳定
SMG_A_DP_PORT = 0x00; // 消隐
}
}
数码管显示函数也是老朋友了,段选位选是核心,还有不要忘记消隐。
5.3 I2C读写函数
#include "iic.h"
// i2c产生起始信号
void iic_start(void)
{
IIC_SDA = 1; //如果把该条语句放在SCL后面,第二次读写会出现问题
delay_10us(1);
IIC_SCL = 1;
delay_10us(1);
IIC_SDA = 0; // 当SCL为高电平时,SDA由高变为低
delay_10us(1);
IIC_SCL = 0; //钳住I2C总线,准备发送或接收数据
delay_10us(1);
}
// 产生i2c停止信号
void iic_stop(void)
{
IIC_SDA = 0; // 如果把该条语句放在SCL后面,第二次读写会出现问题
delay_10us(1);
IIC_SCL = 1;
delay_10us(1);
IIC_SDA = 1; // 当SCL为高电平时,SDA由低变为高
delay_10us(1);
}
// 产生ACK应答
void iic_ack(void)
{
IIC_SCL = 0;
IIC_SDA = 0; // SDA为低电平
delay_10us(1);
IIC_SCL = 1;
delay_10us(1);
IIC_SCL = 0;
}
// 产生NACK非应答
void iic_nack(void)
{
IIC_SCL = 0;
IIC_SDA = 1; // SDA为高电平
delay_10us(1);
IIC_SCL = 1;
delay_10us(1);
IIC_SCL = 0;
}
// 等待应答信号到来
unsigned char iic_wait_ack(void)
{
unsigned char time_temp = 0;
IIC_SCL = 1;
delay_10us(1);
while(IIC_SDA) //等待SDA为低电平
{
time_temp++;
if(time_temp>100) // 超时则强制结束IIC通信
{
iic_stop();
return 1;
}
}
IIC_SCL = 0;
return 0;
}
// IIC发送一个字节
void iic_write_byte(unsigned char dat)
{
unsigned char i=0;
IIC_SCL = 0;
for(i = 0; i <8; i++) // 循环8次将一个字节传出,先传高再传低位
{
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);
}
}
// IIC读一个字节
unsigned char iic_read_byte(unsigned char ack)
{
unsigned char i=0,receive=0;
for(i=0;i<8;i++ ) //循环8次将一个字节读出,先读高再传低位
{
IIC_SCL=0;
delay_10us(1);
IIC_SCL=1;
receive<<=1;
if(IIC_SDA)receive++;
delay_10us(1);
}
if (!ack)
iic_nack();
else
iic_ack();
return receive;
}
5.4 AT24C02读写字节函数
#ifndef _24c02_H
#define _24c02_H
#include "public.h"
void at24c02_write_one_byte(unsigned char addr, unsigned char dat); // AT24C02指定地址写数据
unsigned char at24c02_read_one_byte(unsigned char addr); // AT24C02指定地址读数据
#endif
#include "24c02.h"
#include "iic.h"
// 在AT24CXX指定地址写入一个数据
void at24c02_write_one_byte(unsigned char addr,unsigned char dat)
{
iic_start();
iic_write_byte(0XA0);// 发送写命令
iic_wait_ack();
iic_write_byte(addr);// 发送写地址
iic_wait_ack();
iic_write_byte(dat); // 发送字节
iic_wait_ack();
iic_stop(); // 产生一个停止条件
delay_ms(10);
}
// 在AT24CXX指定地址读出一个数据
unsigned char at24c02_read_one_byte(unsigned char addr)
{
unsigned char 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(0);// 读取字节
iic_stop();
return temp; //返回读取的数据
}
5.5 主函数
#include "public.h"
#include "24c02.h"
#include "key.h"
#include "smg.h"
#define EEPROM_ADDRESS 0 // 定义数据存入EEPROM的起始地址
void main()
{
unsigned char key_temp = 0;
unsigned char save_value = 0;
unsigned char save_buf[3];
while(1)
{
key_temp = key_scan(0);
if(key_temp == KEY1_PRESS)
{
at24c02_write_one_byte(EEPROM_ADDRESS, save_value);
}
else if(key_temp == KEY2_PRESS)
{
save_value = at24c02_read_one_byte(EEPROM_ADDRESS);
}
else if(key_temp == KEY3_PRESS)
{
save_value++;
if(save_value == 255)save_value = 255;
}
else if(key_temp == KEY4_PRESS)
{
save_value =0;
}
save_buf[0] = save_value/100;
save_buf[1] = save_value%100/10;
save_buf[2] = save_value%100%10;
smg_display(save_buf,6);
}
}
6. 小结
-
按键检测还有数码管显示函数不必多说,下面我们要重点分析的是i2c配置和AT24C02读写字节函数
-
先了解一下i2c配置函数:
首先就是i2c起始信号了
// iic起始信号
void iic_start(void)
{
IIC_SDA = 1; // 如果把该条语句放在SCL后面,第二次读写会出现问题
delay_10us(1);
IIC_SCL = 1;
delay_10us(1);
IIC_SDA = 0; // 当SCL为高电平时,SDA由高变为低
delay_10us(1);
IIC_SCL = 0; // 钳住I2C总线,准备发送或接收数据
delay_10us(1);
}
无法理解?如果你有数电的基础,看一下下面的时序图就懂了:
当 SCL 线是高电平时 SDA 线从高电平向低电平切换,这个情况表示通讯的起始。
既然有开始就有停止信号:
// iic停止信号
void iic_stop(void)
{
IIC_SDA = 0; // 如果把该条语句放在SCL后面,第二次读写会出现问题
delay_10us(1);
IIC_SCL = 1;
delay_10us(1);
IIC_SDA = 1; // 当SCL为高电平时,SDA由低变为高
delay_10us(1);
}
分析同开始信号一样,不过是先拉低SDA(0)再高电平(1)代表停止发送信号,而开始信息就是1->0啦。(SDA)
那么如何让i2c产生ACK应答呢?我们不妨把产生非应答拉过来一起分析
// iic产生ACK应答
void iic_ack(void)
{
IIC_SCL = 0;
IIC_SDA = 0; // SDA为低电平
delay_10us(1);
IIC_SCL = 1;
delay_10us(1);
IIC_SCL = 0;
}
// 产生NACK非应答
void iic_nack(void)
{
IIC_SCL = 0;
IIC_SDA = 1; // SDA为高电平
delay_10us(1);
IIC_SCL = 1;
delay_10us(1);
IIC_SCL = 0;
}
可以看到区别就是IIC_SDA是否为1即高低电平的区别,SDA即是串行数据线,在我们写的应答函数里,SDA如果为低电平就代表产生ACK应答咯
接着我们再看I2C等待应答
unsigned char iic_wait_ack(void)
{
unsigned char time_temp = 0;
IIC_SCL = 1; // 拉高SCL,准备发送应答-串行时钟线
delay_10us(1);
while(IIC_SDA) // 等待SDA为低电平
{
time_temp++; // 如果SDA不为低电平,计时器加1
if(time_temp > 100) // 超时则强制结束IIC通信
{
iic_stop(); // 调用信号停止函数
return 1; // 超时返回1-异常
}
}
IIC_SCL = 0; // 拉低SCL,准备接收应答
return 0; // 正常返回0
// 此时返回0代表正常,那么可以准备收发数据了
}
最后来到I2C收发字节函数:
// iic发送一个字节
void iic_write_byte(unsigned char dat)
{
unsigned char i = 0;
IIC_SCL = 0; // 将时钟线拉低,准备发送数据
for(i = 0; i < 8; i++) // 循环8次将一个字节传出,先传高位再传低位
{
if((dat & 0x80) > 0) // 检查dat的最高位是否为1
IIC_SDA = 1; // 如果最高位为1,则数据线拉高(发送逻辑1)
else
IIC_SDA = 0; // 如果最高位为0,则数据线拉低(发送逻辑0)
dat <<= 1; // 将数据dat向左移动一位,准备发送下一个位
delay_10us(1); // 稍作延时,保证时序满足要求
IIC_SCL = 1; // 将时钟线拉高,通知接收方可以读取数据
delay_10us(1); // 稍作延时,保证时序满足要求
IIC_SCL = 0; // 将时钟线再次拉低,为发送下一位数据做准备
delay_10us(1); // 稍作延时,保证时序满足要求
}
}
// IIC读一个字节 ack=1时,发送ACK,ack=0,发送nACK
unsigned char iic_read_byte(unsigned char ack)
{
unsigned char i = 0, receive = 0;
for(i =0; i < 8; i++ ) // 循环8次将一个字节读出,先读高再传低位
{
IIC_SCL = 0;
delay_10us(1);
IIC_SCL = 1;
receive <<= 1;
if(IIC_SDA)
receive++;
delay_10us(1);
}
if (!ack)
iic_nack(); // 发送nACK
else
iic_ack(); // 发送ACK
return receive; // 返回读出的字节
}
- 熟悉I2C的配置,接下来AT24C02写/读数据函数就更简单了
// AT24C02的写入数据的函数
void at24c02_write_one_byte(unsigned char addr, unsigned char dat)
{
iic_start(); // iic开始信号
iic_write_byte(0XA0); // 发送写命令 1010 0000
iic_wait_ack(); // iic等待应答
iic_write_byte(addr); // 发送写地址
iic_wait_ack();
iic_write_byte(dat); // 发送字节
iic_wait_ack();
iic_stop(); // 产生一个停止条件
delay_ms(10);
}
这个还是很简单明了,首先利用我们写好的I2C开始信号函数代表I2C已经准备好可以开始发送信号,之后就利用I2C发送字节函数发送写命令,接着就是等待应答咯,要是没有返回错误就继续发送写地址和字节了,最后发送I2C停止信号。
读数据函数大同小异,看看注释得了。
// AT24C02的读取数据的函数
unsigned char at24c02_read_one_byte(unsigned char addr)
{
unsigned char temp = 0; // 定义一个临时变量存储读取的数据
iic_start(); // iic开始信号
iic_write_byte(0XA0); // 发送写命令
iic_wait_ack(); // iic等待应答
iic_write_byte(addr); // 发送写地址
iic_wait_ack();
iic_start();
iic_write_byte(0XA1); // 进入接收模式
iic_wait_ack();
temp = iic_read_byte(0); // 读取字节
iic_stop(); // 产生一个停止条件
return temp; // 返回读取的数据
}
该章重点其实是学会模块化工程,关于I2C配置不会,其实只要拿来用就行了,EEPROM也是一样,如果需要具体学习,后面会有一个扩展章节:关于51单片机配置I2C
2024.7.21 第一次修订
2024.8.22 第二次修订,后期不在维护