文章目录
一、AT24C02介绍
AT24C02是一种可以实现掉电不丢失的存储器,可用于保存单片机运行时想要永久保存的数据信息
•存储介质:EEPROM
•通讯接口:I2C总线
•容量:256字节
二、原理
AT24C02原理图及典型应用电路如图所示

引脚 | 功能 |
---|---|
VCC、GND | 电源(1.8V~5.5V ) |
WP | 写保护(高电平有效) |
SCL、SDA | I2C接口 |
A0、A1、A2 | I2C地址 |
本开发板AT24C02原理图如图所示

I2C地址全接地,即全为0
WE接地,没有写使能
SCL接P21 SDA接P20
没有外接上拉电阻是因为单片机的每个IO口都接了上拉电阻
AT24C02内部结构框图如图所示

简单来说,就是通过SCL和SDA接口获得数据,经过一定的逻辑,数据存储到EEPROM(通过X和Y来控制数据的存储位置),并且可以通过一定的逻辑,将数据输出出来
三、I2C
I2C总线介绍
I2C总线(Inter IC BUS)是由Philips公司开发的一种通用数据总线
•两根通信线:SCL(Serial Clock)、SDA(Serial Data)
•同步(有时钟线SCL)、半双工(只有一根数据线SDA),带数据应答
特点:通用的I2C总线,可以使各种设备的通信标准统一,对于厂家来说,使用成熟的方案可以缩短芯片设计周期、提高稳定性,对于应用者来说,使用通用的通信协议可以避免学习各种各样的自定义协议,降低了学习和应用的难度
I2C电路规范
•所有I2C设备的SCL连在一起,SDA连在一起(不同于USRT的交叉连接)
•设备的SCL和SDA均要配置成开漏输出模式(具体的各种IO的模式可见stm32)
本单片机所有IO口都是弱上拉模式,如图

开关闭合,则输出0
开关打开,则输出1
高电平驱动能力弱,低电平驱动能力强
开漏模式如图,没有接上拉电阻

开关闭合,输出0
开关断开,引脚呈浮空状态(什么都没接),电平不稳定
•SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右(根据原理图可知,本开发板使用的是10KΩ)
因为开漏输出模式无法直接输出1,且所有设备都是开漏输出模式,添加上拉电阻来使得当所有设备都是浮空是,系统可以输出1
•开漏输出和上拉电阻的共同作用实现了“线与”的功能,此设计主要是为了解决多机通信互相干扰的问题(CPU与某个设备进行通信,最好的状态是CPU与其他通信设备断开,以达到不干扰的目的,而开漏输出模式有浮空状态,即断开状态,符合要求)

I2C时序结构
•起始条件:SCL高电平期间,SDA从高电平切换到低电平(下左图)
•终止条件:SCL高电平期间,SDA从低电平切换到高电平(下右图)

•发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位在前),然后拉高SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节

•接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位在前),然后拉高SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA(半双工))

•发送应答:在接收完一个字节之后,主机在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答
•接收应答:在发送完一个字节之后,主机在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)
(与计算机网络中的知识进行类比)

I2C数据帧
•发送一帧数据

开始发送之后,要先发送地址及读/写,其中A6-A3是固定值,AT24C02是1010,A2-A0是与模块原理图的A2-A0相对应
•接收一帧数据

•先发送再接收数据帧(复合格式)

AT24C02数据帧
•字节写:在WORD ADDRESS处写入数据DATA

就是将发送一帧数据的发送第一个字节数据变为了发送字地址
•随机读:读出在WORD ADDRESS处的数据DATA

AT24C02的固定地址为1010,可配置地址本开发板上为000。所以SLAVE ADDRESS+W为0xA0,SLAVE ADDRESS+R为0xA1
四、实验1:数据存储
1、创建与添加文件
创建AT24C02.c
及其头文件
添加LCD1602、Delay、Key相关文件
2、进行位声明
sbit I2C_SCL = P2^1;
sbit I2C_SDA = P2^0;
3、编写起始和终止函数
void I2C_Start()
{
//保证开始之前SCL和SDA是高电平的
I2C_SCL = 1;
I2C_SDA = 1;
I2C_SDA = 0;
I2C_SCL = 0;
}
void I2C_Stop()
{
//保证结束之前SDA是低电平的 不需要设置SCL为低电平,因为在整个数据传输过程中SCL都为0
I2C_SDA = 0;
I2C_SCL = 1;
I2C_SDA = 1;
}
4、编写发送数据和接收数据函数
void I2C_SendByte(unsigned char byte)
{
unsigned char i;
for(i=0;i<8;i++)
{
//从高到低依次获得该位的数据
I2C_SDA = byte&(0x80>>i);
//原来SCL为0,先设置为1,再设置为0,就会产生一个一定宽度高电平脉冲
I2C_SCL = 1;
I2C_SCL = 0;
}
}
unsigned char I2C_ReceiveByte()
{
unsigned char i, byte = 0x00;
//先释放SDA
I2C_SDA = 1;
for(i=0;i<8;i++)
{
//原来SCL为0,先设置为1,再设置为0,就会产生一个一定宽度高电平脉冲
I2C_SCL = 1;
if(I2C_SDA)//因为byte初始化为0,当SDA为0,不执行,则该位为0,当SDA为1时,该位为1
{
byte |= (0x80>>i);
}
I2C_SCL = 0;
}
return byte;
}
5、编写发送应答和接收应答函数
void I2C_SendAck(unsigned char ackBit)
{//ackBit也可以定义为bit类型
I2C_SDA = ackBit;//非0即1
I2C_SCL = 1;
I2C_SCL = 0;
}
unsigned char I2C_ReceiveAck()
{
unsigned char ackBit;
//先释放SDA
I2C_SDA = 1;
I2C_SCL = 1;
ackBit = I2C_SDA;//非0即1
I2C_SCL = 0;
return ackBit;
}
6、编写AT24C02的读写数据的函数
#define AT24C02_ADDRESS 0xA0
void AT24C02_WriteByte(unsigned char wordAddress, myData)
{
I2C_Start();
I2C_SendByte(AT24C02_ADDRESS);
I2C_ReceiveAck();
I2C_SendByte(wordAddress);
I2C_ReceiveAck();
I2C_SendByte(myData);
I2C_ReceiveAck();
I2C_Stop();
}
unsigned char AT24C02_ReadByte(unsigned char wordAddress)
{
unsigned char myData;
I2C_Start();
I2C_SendByte(AT24C02_ADDRESS);
I2C_ReceiveAck();
I2C_SendByte(wordAddress);
I2C_ReceiveAck();
I2C_Start();
I2C_SendByte(AT24C02_ADDRESS|0x01);//写地址变为读地址
I2C_ReceiveAck();
myData = I2C_ReceiveByte();
I2C_SendAck(1);
I2C_Stop();
return myData;
}
7、测试AT24C02的读写数据函数功能
#include <REGX52.H>
#include "Key.h"
#include "LCD1602.h"
#include "AT24C02.h"
#include "Delay.h"
void main()
{
unsigned char mydata;
LCD_Init();
AT24C02_WriteByte(1,66);
//写数据完后立马读出数据,读不出来
//根据芯片数据手册,写周期最长为5ms(写周期时间是指从一个写序列的有效停止条件开始至内部写周期结束的时间)
Delayms(5);
mydata = AT24C02_ReadByte(1);
LCD_ShowNum(1,1,mydata,3);
while(1)
{
}
}
8、最终主函数代码的编写
#include <REGX52.H>
#include "Key.h"
#include "LCD1602.h"
#include "AT24C02.h"
#include "Delay.h"
unsigned char keyNum;
unsigned int num;
void main()
{
LCD_Init();
LCD_ShowNum(1,1,num,5);
while(1)
{
keyNum = GetKeyNum();
if(keyNum == 1)
{
num++;
LCD_ShowNum(1,1,num,5);
}
if(keyNum == 2)
{
num--;
LCD_ShowNum(1,1,num,5);
}
if(keyNum == 3)
{
//写入num的低8位
AT24C02_WriteByte(0,num%256);
Delayms(5);
//写入num的高8位
AT24C02_WriteByte(1,num/256);
Delayms(5);
LCD_ShowString(2,1,"Write OK");
Delayms(1000);
LCD_ShowString(2,1," ");
}
if(keyNum == 4)
{
//取出低8位
num = AT24C02_ReadByte(0);
//取出高8位,左移8位,再或运算,即可得出16位的num
num |= AT24C02_ReadByte(1)<<8;
LCD_ShowNum(1,1,num,5);
LCD_ShowString(2,1,"Read OK");
Delayms(1000);
LCD_ShowString(2,1," ");
}
}
}
五、实验2:秒表
1、添加文件
将相关文件添加到项目中
2、修改key.c和key.h
之前的判断按键按下的逻辑是,当判断到按键按下时(对应的IO口为0),延迟20ms(消抖),如果IO口还是为0,则会死循环一直等待知道IO口为1为止,然后再延迟20ms进行消抖。这样会有弊端,即如果一直按着按键,则系统不能执行其它程序。
现在,通过使用定时器来优化按键逻辑。定时器每个一段时间(设定为20ms)扫描按键状态。若上次按键状态为1,本次按键状态为0,则说明按键按下;若两次按键状态都为1,则按键没有被按下。
修改Key.c和Key.h
Key.c
#include <REGX52.H>
#include "Delay.h"
unsigned char Key_KeyNumber;
/**
* @brief 获取按键键码
* @param 无
* @retval 按下按键的键码,范围:0,1~4, 0表示无按键按下
*/
unsigned char Key()
{
//将Key_KeyNumber清零,Key_Loop函数没有将Key_KeyNumber清零,这里清零,防止影响下次判断
unsigned char temp = Key_KeyNumber;
Key_KeyNumber = 0;
return temp;
}
/**
* @brief 获取当前按键的状态,无消抖及松手检测
* @param 无
* @retval 按下按键的键码,范围:0,1~4,0表示无按键按下
*/
unsigned char Key_GetState()
{
unsigned char keyNum = 0;
if(P3_1 == 0){keyNum = 1;}
if(P3_0 == 0){keyNum = 2;}
if(P3_2 == 0){keyNum = 3;}
if(P3_3 == 0){keyNum = 4;}
return keyNum;
}
/**
* @brief 按键驱动函数,在中断中调用
* @param 无
* @retval 无
*/
void Key_Loop()
{
static unsigned char nowState, lastState;
//状态更新
lastState = nowState;
nowState = Key_GetState();
if(lastState == 1 && nowState == 0)
{
Key_KeyNumber = 1;
}
else if(lastState == 2 && nowState == 0)
{
Key_KeyNumber = 2;
}
else if(lastState == 3 && nowState == 0)
{
Key_KeyNumber = 3;
}
else if(lastState == 4 && nowState == 0)
{
Key_KeyNumber = 4;
}
}
3、修改Nixie.c和Nixie.h
与按键同理
Nixie.c
#include <REGX52.H>
#include "Delay.h"
//用于数据缓存,第0位不用
unsigned char Nixie_Buf[9] = {0,10,10,10,10,10,10,10,10};
//数码管段码表,添加一个0x00,用于啥都不显示 添加一个0x40,用于显示横杠(秒表的分钟与秒中间)
unsigned char NixieTable[] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x00,0x40};
/**
* @brief 设置显示缓存区
* @param location 要设置的位置,范围:1~8
* @param num 要设置的数字,范围:段码表索引范围
* @retval 无
*/
void Nixie_SetBuf(unsigned char location, num)
{
Nixie_Buf[location] = num;
}
/**
* @brief 数码管扫描显示
* @param location 要显示的位置,范围:1~8
* @param num 要显示的数字,范围:段码表索引范围
* @retval 无
*/
void Nixie_Scan(unsigned char location, num)
{
P0 = 0x00;//清零,消影
switch(location)
{
case 1:P2_4 = 0;P2_3 = 0;P2_2 = 0;break;
case 2:P2_4 = 0;P2_3 = 0;P2_2 = 1;break;
case 3:P2_4 = 0;P2_3 = 1;P2_2 = 0;break;
case 4:P2_4 = 0;P2_3 = 1;P2_2 = 1;break;
case 5:P2_4 = 1;P2_3 = 0;P2_2 = 0;break;
case 6:P2_4 = 1;P2_3 = 0;P2_2 = 1;break;
case 7:P2_4 = 1;P2_3 = 1;P2_2 = 0;break;
case 8:P2_4 = 1;P2_3 = 1;P2_2 = 1;break;
}
P0 = NixieTable[num];
}
/**
* @brief 数码管驱动函数,在中断中调用
* @param 无
* @retval 无
*/
void Nixie_Loop()
{
//i用于循环扫描数码管,使得可以实现同时显示多个数码管
static unsigned char i = 1;
Nixie_Scan(i,Nixie_Buf[i]);
i++;
if(i >= 9)
{
i = 1;
}
}
4、撰写秒表逻辑
#include <REGX52.H>
#include "Key.h"
#include "LCD1602.h"
#include "AT24C02.h"
#include "Delay.h"
#include "Nixie.h"
#include "Timer.h"
unsigned char keyNum;
//分 秒
unsigned char Min,Sec,MiniSec;
//秒表运行标志
unsigned char RunFlag;
void main()
{
Timer0Init();
while(1)
{
keyNum = Key();
if(keyNum == 1)
{
RunFlag = !RunFlag;
}
if(keyNum == 2)
{//清零
Min = Sec = MiniSec = 0;
}
if(keyNum == 3)
{//秒表值存入AT24C02
AT24C02_WriteByte(0,Min);
Delayms(5);
AT24C02_WriteByte(1,Sec);
Delayms(5);
AT24C02_WriteByte(2,MiniSec);
Delayms(5);
}
if(keyNum == 4)
{//从AT24C02中读取数据
Min = AT24C02_ReadByte(0);
Sec = AT24C02_ReadByte(1);
MiniSec = AT24C02_ReadByte(2);
}
//显示数据
//分
Nixie_SetBuf(8,Min/10);
Nixie_SetBuf(7,Min%10);
//显示横杠
Nixie_SetBuf(6,11);
//秒
Nixie_SetBuf(5,Sec/10);
Nixie_SetBuf(4,Sec%10);
Nixie_SetBuf(3,11);
Nixie_SetBuf(2,MiniSec/10);
Nixie_SetBuf(1,MiniSec%10);
}
}
/**
* @brief 秒表驱动函数,在中断中调用
* @param 无
* @retval 无
*/
void Sec_Loop()
{//与之前LCD1602的计时逻辑一致
if(RunFlag)
{
MiniSec++;
if(MiniSec>=100)
{
MiniSec=0;
Sec++;
if(Sec>=60)
{
Sec=0;
Min++;
if(Min>=60)
{
Min=0;
}
}
}
}
}
//定时器0的中断处理函数
void Timer0_Routine() interrupt 1
{
static unsigned int count1, count2, count3;
//重新赋值计数器,因为计数器溢出后会变为0,要1ms产生一次中断,需要重新给计数器赋值为64535
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
//1ms产生一次中断,每次中断count+1,1000次后再进行处理,此时处理的时间间隔就为1s
count1++;
count2++;
count3++;
if(count1 >= 20)
{
count1 = 0;
Key_Loop();
}
if(count2 >= 2)
{
count2 = 0;
Nixie_Loop();
}
if(count3 >= 10)
{
count3 = 0;
Sec_Loop();
}
}
更多51单片机笔记见主页