【51单片机】I2C总线详解 + AT24C02
学习使用的开发板:STC89C52RC/LE52RC
编程软件:Keil5
烧录软件:stc-isp
开发板实图:
文章目录
- AT24C02介绍
- 存储器
- I2C总线介绍
- I2C时序结构
- 数据帧
- AT24C02数据帧
- 编程实例 —— 按键控制数据大小&存储器写入读出
AT24C02介绍
- AT24C02 是一种可以实现掉电不丢失的存储器,可用于保存单片机运行时想永久保存的数据信息
- 存储介质:
E2PROM
- 通讯接口:
I2C总线
- 容量:256字节
引脚及应用电路如下:
详细结合I2C通信协议讲解
存储器
存储器大致分为两类:
RAM(Random Access Memory,随机存取存储器),掉电丢失
ROM(Read Only Memory,只读存储器),掉电不丢失
RAM
- SRAM(静态随机存储器):访问时间短,速度快,用于CPU高速缓存(Cache)。
- DRAM(动态随机存储器):访问时间长,速度较慢,用于主存,也就是运行内存。
ROM
- Mask ROM:在制作过程写入,写入后不可更改,类似磁带,只用于读取
- PROM:可改写一次的只读存储器。PROM在出厂时,存储的内容全为1,用户可以根据需要将其中的某些单元写入数据0(部分的PROM相反,出场全为0), 以实现对其“编程”的目的
- EPROM(可擦除可编程ROM):具有可擦除功能,擦除后即可进行再编程,但是缺点是擦除需要使用紫外线照射一定的时间,且时间较长
- E2PROM/E2PROM/EEPROM(电可擦除):可直接用电信号擦除,也可用电信号写入,写入时间为ms级别
- Flash(闪存):属于E2PROM的改进产品。最大特点是必须按块(Block)擦除(每个区块的大小不定,不同厂家的产品有不同的规格), 而EEPROM则可以一次只擦除一个字节(Byte)。
- 硬盘、软盘、光盘:相应控制器 + Flash闪存芯片
I2C总线介绍
单片机常用的通信方式有:UART(串口)、SPI、I2C、DS232
UART常用于两个单片机进行通信,详情参看【51单片机】串口通信
UART有三个缺陷:不能远距离传输、传输速度慢、不能一对多通信。针对这三个缺陷,衍生出了其他的通信方式:
远距离传输:RS232/RS485
传输速度:SPI(加了个clock线提供时钟信号),常用于SD卡,屏幕这类对通信速度要求高的外设
一对多通信:I2C —— 常用于开发板中,单片机与其他外设进行通信
- I2C总线(Inter IC BUS) 是由 Philips 公司开发的一种通用数据总线
- 同步、半双工、带数据应答
- 有两根通信线:SCL(Serial Clock)、SDA(Serial Data)
所有I2C设备的 SCL 连接在一起,SDA 连在一起
设备的 SCL 和 SDA 均要配置成开漏输出模式
开漏输出可参看B站up主工科男孙老师的《推挽 开漏 高阻 这都是谁想出来的词??》
开漏输出可用于适配不同电压的芯片;在总线通信时避免不同设备输出高低电平导致设备烧毁。通过搭配外接上拉电阻输出高低电平
当 SCLKN1OUT 关闭,SCLKIN 为高电平;当 SCLK1OUT 打开,SCLKIN 为低电平
SCL 和 SDA 各添加一个上拉电阻,阻止一般为4.7KΩ左右
上拉电阻可参看B站up主工科男孙老师的《单片机的上拉电阻 到底在拉什么?》
上拉电阻阻值过小,会漏电;阻值过大,驱动能力(将低电平拉高的速度)下降。
当总线挂载设备增多时,可适当降低上拉电阻阻值
开漏输出 和 上拉电阻 的共同作用实现了“线与”的功能,解决多机通信互相干扰的问题
当总线设备较多且通信速度要求高,可以适当降低上拉电阻的阻值
详情参看B站up主工科男孙老师的《单片机I2C通信入门(上):硬件部分有哪些注意点?》
I2C时序结构
因为总线上挂载多个设备,需要约定通信协议才能让通信正确无误,畅通无阻
例如一次读数据通信,需要知道什么时候发起,向谁读数据,对方是否存在,读数据,停止读数据。
I2C通信协议将上述操作都抽象成了一个个时序,最终拼接成数据帧
总共有六个时序结构,如下:
起始条件
—— 数据帧的起始,表示开始通信
- SCL 高电平期间,SDA 从高电平切换到低电平
终止条件
—— 数据帧的结尾,表示通信结束
- SCL 高电平期间,SDA 从低电平切换到高电平
发送一个字节
—— 可发送数据,I2C地址,字地址…
- SCL 低电平期间,主机将数据位依次放到SDA线上(高位在前),然后拉高 SCL,从机(对方设备)将在 SCL 高电平期间读取数据位。SCL 高电平期间 SDA 不允许有数据变化,依次循环上述过程8次,即可发送一个字节数据
注意:先拉低 SCL,再将数据放到 SDA,然后拉高 SCL,此时从机会读取 SDA 数据
接收一个字节
—— 主机(MCU) 接收从机数据
- SCL 低电平期间,从机将数据依次放到 SDA 线上(高位在前),然后拉高 SCL,主机将在 SCL 高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)
注意:先拉高 SDA,释放总线给从机。从机在 SCL 低电平时将数据放到 SDA ,主机拉高 SCL 读取数据,再拉低 SCL 让从机放数据
发送应答
—— 接收从机数据后,告诉从机无误或有误
- 在接收完一个字节之后,主机在下一个时钟发送一位数据、数据0表示应答,数据1表示非应答。发送流程同
发送一位数据
接收应答
—— 主机发送一字节数据后,接收从机的接收应答
- 在主机发送完一个字节之后,主机在下一个时钟接收一位数据,判断从机是否应答,数据0 表示应答,数据1 表示非应答(主机在接收之前,需要释放SDA)
数据帧
I2C数据帧描述具体的一次通信,由上述时序结构拼接而成。
发送一帧数据
—— 向谁发什么
协议格式中第一个字节(为slave address)由7位地址和一位R/W读写位组成的,这字节是个器件地址。
I2C 挂载多个设备,通过从机地址(slave address)标识对哪个设备进行读/写操作
常用I2C接口通用器件的器件地址是由种类型号,及寻址码组成的,共7位。
如格式如下:
-
器件类型:A6 - A3 共4位决定的。这是由半导公司生产时就已固定此类型的了,也就是说这4位已是固定的。本开发板为
1010
-
用户自定义地址码:A2 - A0 共3位。这是由用户自己设置的,通常的作法如EEPROM这些器件是由外部IC的3个引脚所组合电平决定的(用常用的名字如A0,A1,A2)。这也就是寻址码。
AT24C02 的寻址码为 000
-
最低一位就是R/W位,,“0”表示写,“1”表示读(通常读写信号中写上面有一横线,表示低电平)。所以I2C设备通常有两个地址,即读地址和写地址
综上,AT24C02 的写地址为0xA0
,读地址为0xA1
回到数据帧,发送一帧数据
流程如下
S(起始位) -> 发送从机地址 + W -> 从机返回接收应答 -> 发送一字节数据 -> 从机返回接收应答 … -> P(终止位)
接收一帧数据
S(起始位) -> 发送从机地址 + W -> 从机返回接收应答 -> 接收一字节数据 -> 主机发送应答 -> … -> P(终止位)
复合格式
—— 先发送再接收
AT24C02数据帧
AT24C02是一个存储器,本质就是写入数据和读取数据,其数据帧分为字节写
和 随机读
字节写
存储器内部还有字地址,需要指明将数据写到存储器的哪个位置 —— word address
随机读
需要先告诉 AT24C02 要读哪个地址的数据,再进行读字节数据帧
编程实例 —— 按键控制数据大小&存储器写入读出
按键和数码管部分参看【51单片机】独立按键 和 【51单片机】数码管
首先对I2C的时序结构进行封装,读者可以对照上述时序图,代码如下:
#include <REGX52.h>
#include "Delay.h"
sbit I2C_SDA = P2^0;//数据线
sbit I2C_SCL = P2^1;//Clock线
/**
* @brief I2C开始标志
* @parm 无
* @retval 无
*/
void I2C_Start()
{
//起始条件:SCL高电平期间,SDA从高电平切换到低电平
//先置SDA = 1,确保从高电平切到低电平
I2C_SDA = 1;
//SCL置高电平
I2C_SCL = 1;
//SDA由高电平切换为低电平
I2C_SDA = 0;
//SCL恢复低电平
I2C_SCL = 0;
}
/**
* @brief I2C停止标志
* @parm 无
* @retval 无
*/
void I2C_Stop()
{
//终止条件:SCL高电平期间,SDA从低电平切换到高电平
//先置SDA = 0,确保从低电平切到高电平
I2C_SDA = 0;
//SCL置高电平
I2C_SCL = 1;
//SDA由低电平切换为高电平
I2C_SDA = 1;
}
/**
* @brief I2C发送字节数据
* @parm Byte:要发送的数据,范围: 0 ~ 255
* @retval 无
*/
void I2C_SendByte(unsigned char Byte)
{
//发送数据:从高位开始
//SCL低电平期间,SDA写数据,SCL拉高后,从机读取数据
unsigned char i = 0;
for(i = 0; i < 8; ++i)
{
I2C_SDA = Byte & (0x80 >> i);
I2C_SCL = 1;//此处要注意若机器频率过快,可能导致从机还没读到数据,SCL就被拉低了
I2C_SCL = 0;//需要查看手册。
}
}
/**
* @brief I2C读取一字节数据
* @parm 无
* @retval 读取到的数据
*/
unsigned char I2C_ReceiveByte()
{
//接收数据,从高位开始
//SCL高电平时,从SDA读数据
unsigned char Byte = 0, i;
//将SDA置高电平,释放SDA给从机
I2C_SDA = 1;
for(i = 0; i < 8; ++i)
{
I2C_SCL = 1;
if(I2C_SDA) Byte |= (0x80 >> i);
I2C_SCL = 0;
}
return Byte;
}
/**
* @brief 发送应答
* @parm AckBit为0表示应答,为1表示非应答
* @retval 无
*/
void I2C_SendAck(unsigned char AckBit)
{
//在读完一个字节后,主机在下一个时钟发送一位数据
//数据0表示应答,数据1表示非应答
I2C_SDA = AckBit;
I2C_SCL = 1;
I2C_SCL = 0;
}
/**
* @brief 接收应答
* @parm 无
* @retval Ack为0表示应答,为1表示非应答
*/
unsigned char I2C_ReceiveAck()
{
//在发完一个字节后,主机在下一个时钟接收一位数据,判断从机是否应答
//数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)
unsigned char Ack;
//释放SDA
I2C_SDA = 1;
//SCL置高电平
I2C_SCL = 1;
//读响应
Ack = I2C_SDA;
//SCL置低电平
I2C_SCL = 0;
return Ack;
}
然后封装 AT24C02 的两个数据帧,对 I2C 的时序结构进行拼接
#include <REGX52.h>
#include "I2C.h"
//AT24C02的从机地址写,从机地址读需 |= 0x01
#define AT24C02_ADDRESS 0xA0
/**
* @brief 向AT24C02暂存器写入数据
* @parm WordAddress:写到暂存器的什么地址
* @parm Data:要写入的数据
* @retval 无
*/
void AT24C02_WriteByte(unsigned char WordAddress, unsigned char Data)
{
I2C_Start();
I2C_SendByte(AT24C02_ADDRESS);
I2C_ReceiveAck();
I2C_SendByte(WordAddress);
I2C_ReceiveAck();
I2C_SendByte(Data);
I2C_ReceiveAck();
I2C_Stop();
}
/**
* @brief 从AT24C02读取数据
* @parm WordAddress:读什么地址的数据
* @retval 读出的数据
*/
unsigned char AT24C02_ReadByte(unsigned char WordAddress)
{
unsigned char Data;
I2C_Start();
I2C_SendByte(AT24C02_ADDRESS);
I2C_ReceiveAck();
I2C_SendByte(WordAddress);
I2C_ReceiveAck();
I2C_Start();
I2C_SendByte(AT24C02_ADDRESS | 0x01);//此处是读
I2C_ReceiveAck();
Data = I2C_ReceiveByte();
I2C_SendAck(1);
I2C_Stop();
return Data;
}
主程序 —— 监控按键按下,并执行相应操作
- 按键一:变量++
- 按键二:变量–
- 按键三:将变量写入 AT24C02
- 按键四:将数据读出 At24C02
#include <REGX52.h>
#include "AT24C02.h"
#include "SoleKey.h"
#include "LCD1602.h"
#include "Delay.h"
void main()
{
unsigned char Key = 0;
unsigned int Count = 0;
LCD_Init();//LCD1602初始化
LCD_ShowNum(1, 1, Count, 5);
while(1)
{
Key = SoleKey();//获取哪个按键按下
if(Key)
{
if(Key == 1)//1按键按下,Count++
{
Count++;
LCD_ShowNum(1, 1, Count, 5);
}
else if(Key == 2)//2按键按下,Count--
{
Count--;
LCD_ShowNum(1, 1, Count, 5);
}
else if(Key == 3)//3按键按下,存储数据
{
AT24C02_WriteByte(0x00, Count / 256);
Delayms(5);//因为写速度较慢,不能连续写,会导致数据错误
AT24C02_WriteByte(0x01, Count % 256);
LCD_ShowNum(1, 1, Count, 5);
//显示写入成功
LCD_ShowString(2, 1, "Write OK!");
Delayms(1000);
LCD_ShowString(2, 1, " ");
}
else if(Key == 4)//4按键按下,读出数据
{
Count = AT24C02_ReadByte(0x00) * 256 + AT24C02_ReadByte(0x01);
LCD_ShowNum(1, 1, Count, 5);
//显示读出成功
LCD_ShowString(2, 1, "Read OK! ");
Delayms(1000);
LCD_ShowString(2, 1, " ");
}
}
}
}
效果如下:
【51单片机AT24C02 & I2C通信】
完整项目链接:
AT24C02 存储数据 & I2C通信
以上就是本篇博客的所有内容,感谢你的阅读
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。