目录
AT24C02数据存储
准备工作
代码讲解
I2C.c
模拟起始位置的时序
模拟发送一个字节的时序
模拟接收应答的时序
模拟接收一个字节的时序
模拟发送应答的时序
模拟结束位置的时序
I2C.h
AT24C02.c
字节写:在WORD ADDRESS(字地址)处写入数据DATA
随机读:读出在WORD ADDRESS处的数据DATA
AT24C02.h
main.c
上一节讲了AT24C02和I2C相关的工作原理,这一节开始代码演示!
准备工作
新创建一个工程:AT24C02数据存储
把要用到的程序模块添加进来,这些程序模块都是我前面的博客里演示过的了
然后新建文件main.c, AT24C02.c, AT24C02.h, I2C.c, I2C.h
代码讲解
接下来就开始代码讲解:
I2C.c
首先我们先写I2C.c
在这个文件里面我们是按照上一篇博客所讲的各部分时序来逐个定义函数,函数体的内容就是模拟每一部分时序写的。
首先我们得根据原理图重新定义一下引脚
#include <REGX52.H>
sbit I2C_SCL=P2^1;//将P2^1重命名为I2C_SCL
sbit I2C_SDA=P2^0;//将P2^0重命名为I2C_SDA
模拟起始位置的时序
void I2C_Start(void)
{
I2C_SDA=1;
I2C_SCL=1;
I2C_SDA=0;//SCL高电平期间,SDA从高电平切换到低电平
I2C_SCL=0;
}
模拟发送一个字节的时序
void I2C_SendByte(unsigned char Byte)
{
unsigned char i;
for(i=0;i<8;i++)//一个字节循环8次发送8位数据
{
//SCL低电平期间,主机将数据位依次放到SDA线上(高位在前)
I2C_SDA=Byte&(0x80>>i);//从最高位开始取出,依次右移一位,直到取到最低位
I2C_SCL=1;//然后拉高SCL,从机将在SCL高电平期间读取数据位
I2C_SCL=0;//发送完一个字节后拉低SCL(下降沿)
}
}
注意:SCL当VCC等于5V的情况下是1000kHz=1MHz,而我们单片机的IO口翻转一次最快也就1微秒(大于0.4微秒),就是500Hz,由此可见它的频率比IO口翻转一次的频率还要快,所以即使我们拉高SCL立马又拉低也不会影响它的最大时钟,它也能很快读取到数据。
模拟接收应答的时序
unsigned char I2C_ReceiveAck(void)
{
unsigned char AckBit;
I2C_SDA=1;//主机接收应答之前先把SDA拉高,
I2C_SCL=1;//在SCL位高电平时,主机检测从机是否应答
//接下来我们不管从机的时序是怎么变化的
//所以这里我们没有在代码中体现从机是拉低了SDA还是默认SDA就是高电平
//我们的单片机是主机,24C02是从机。
//主机和从机的程序是不一样的,我们只写主机,从机是主动检测的,
//从机读取数据的时候是程序自动完成的,
//我们只需要把主机的时序模拟出来就好了。
AckBit=I2C_SDA;//主机接收从机的应答
//如果从机不想应答或者从机不存在就默认SDA还是高电平
//从机应答的话就拉低了SDA赋值给AckBit
I2C_SCL=0;
return AckBit;
}
模拟接收一个字节的时序
上图SDA紫色部分就是从机控制总线的时候
unsigned char I2C_ReceiveByte(void)
{
unsigned char i,Byte=0x00;
I2C_SDA=1;//接收之前把SDA释放
for(i=0;i<8;i++)//一个字节循环8次读取8位数据
{
//SCL低电平期间,从机将数据位依次放到SDA线上(高位在前)
I2C_SCL=1;//然后拉高SCL,主机将在SCL高电平期间读取数据位
if(I2C_SDA){Byte|=(0x80>>i);}//从最高位开始读,依次右移一位,直到读到最低位
I2C_SCL=0;
}
return Byte;
}
模拟发送应答的时序
void I2C_SendAck(unsigned char AckBit)
{
I2C_SDA=AckBit;//主机发送应答给从机
I2C_SCL=1;//SCL高电平期间,从机检测主机是否应答
I2C_SCL=0;
}
模拟结束位置的时序
void I2C_Stop(void)
{
I2C_SDA=0;//不管主机/从机是否应答,都要拉低SDA。
I2C_SCL=1;
I2C_SDA=1;//SCL高电平期间,SDA从低电平切换到高电平
}
I2C.h
最后在I2C.h文件中声明一下这六个函数:
#ifndef __I2C_H__
#define __I2C_H__
void I2C_Start(void);
void I2C_Stop(void);
void I2C_SendByte(unsigned char Byte);
unsigned char I2C_ReceiveByte(void);
void I2C_SendAck(unsigned char AckBit);
unsigned char I2C_ReceiveAck(void);
#endif
AT24C02.c
接下来写AT24C02.c
AT24C02.c的内容主要是按我上一篇博客讲过的这个流程图来逐个调用I2C.c中的六个函数:
字节写:在WORD ADDRESS(字地址)处写入数据DATA
随机读:读出在WORD ADDRESS处的数据DATA(这其实是一种复合格式)
上一篇博客我写过AT24C02的固定地址为1010,可配置地址本开发板上为000,所以从机的写地址SLAVE ADDRESS+W为0xA0,从机的读地址SLAVE ADDRESS+R为0xA1
所以我们可以先重定义从机的写地址,将从机的写地址重定义为AT24C02_ADDRESS,然后从机的读地址我们到时候直接给字节的最低位置1就可以了:
从机的写地址=0xA0=AT24C02_ADDRESS=1010 0000
从机的读地址=AT24C02_ADDRESS|0x01=1010 0000|0000 0001=1010 0001=0xA1
#include <REGX52.H>
#include "I2C.h"
#define AT24C02_ADDRESS 0xA0 //将从机的写地址重定义为AT24C02_ADDRESS
字节写:在WORD ADDRESS(字地址)处写入数据DATA
按照字节写的流程图写函数体:
void AT24C02_WriteByte(unsigned char WordAddress,Data)//Data的类型和wordAddress一样
{
I2C_Start();//起始位置
I2C_SendByte(AT24C02_ADDRESS);//发送从机地址
I2C_ReceiveAck();//接收应答
I2C_SendByte(WordAddress);//发送字节地址
I2C_ReceiveAck();//接收应答
I2C_SendByte(Data);//发送数据
I2C_ReceiveAck();//接收应答
I2C_Stop();//结束位置
}
随机读:读出在WORD ADDRESS处的数据DATA
按照字节写的流程图写函数体:
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);//发送从机地址,将从机的写地址的最低位置1
I2C_ReceiveAck();//接收应答
//前面说过从机接收了什么数据怎么接收的数据我们在代码中不体现出来,
//我们只写主机的程序
Data=I2C_ReceiveByte();//我们只要把从机里面那个指定的字节地址处的数据读出来赋值给Data
I2C_SendAck(1);//读取完一个字节可以不用再应答从机
I2C_Stop();//结束位置
return Data;//返回读出来的数据
}
AT24C02.h
声明一下这两个函数
#ifndef __AT24C02_H__
#define __AT24C02_H__
void AT24C02_WriteByte(unsigned char WordAddress,Data);
unsigned char AT24C02_ReadByte(unsigned char WordAddress);
#endif
main.c
接下来我们在主程序里实现在WORD ADDRESS(字地址)处写入数据DATA,然后读出在WORD ADDRESS处的数据DATA,最后在液晶屏上显示我们写入并读出来的数据,结合独立按键的功能完成这个效果
先定义两个变量:
#include <REGX52.H>
#include "LCD1602.h"
#include "Key.h"
#include "AT24C02.h"
#include "Delay.h"
unsigned char KeyNum;//键码
unsigned int Num;//初值,16位数据,范围是0~65535
主程序(请认真结合注释理清每一句代码的逻辑意思)
void main()
{
LCD_Init();
LCD_ShowNum(1,1,Num,5);
while(1)
{
KeyNum=Key();
if(KeyNum==1) //K1按键,Num自增
{
Num++;//第一次就按K1的时候,由0变成1
LCD_ShowNum(1,1,Num,5);
}
if(KeyNum==2) //K2按键,Num自减
{
Num--;//第一次就按K2的时候,由0变成65535
LCD_ShowNum(1,1,Num,5);
}
if(KeyNum==3) //K3按键,向AT24C02写入数据
{
AT24C02_WriteByte(0,Num%256);//在字地址0处,写入Num的低八位
//%256是16进制取低8位的方法
//因为Num是unsigned int型占2个字节即16位数据
//所以把Num的低8位取出来写入
Delay(5);
//写进去不能立马读出来,ROM一般要2~3ms才能写完
//手册上的写周期是5ms,意味着我们每次写入之后需要Delay 5ms
AT24C02_WriteByte(1,Num/256);在字地址1处,写入Num的高八位
// 或256是16进制取高8位的方法
Delay(5);
LCD_ShowString(2,1,"Write OK");
Delay(1000);//延时1000ms=1s
LCD_ShowString(2,1," ");//第2行第1列清屏
}
if(KeyNum==4) //K4按键,从AT24C02读取数据
{
Num=AT24C02_ReadByte(0);//低八位的字地址是0,把低八位数据读出来赋值给Num
Num|=AT24C02_ReadByte(1)<<8;//高八位的字地址1,把八位数据读出来每个左移8就是高八位
LCD_ShowNum(1,1,Num,5);
LCD_ShowString(2,1,"Read OK ");
Delay(1000);
LCD_ShowString(2,1," ");
}
}
}
注意:
写进去立马读出来能读到吗?
不能!
为什么不能?
我们看一下手册上的写周期是5ms
这个写周期意味着我们每次写入之后需要Delay 5ms,写的数据帧stop一旦结束,它内部要执行一些操作,把数据写出去。所以ROM要比RAM慢一些,因为ROM有个写入时间,它这个是最长5ms,经过实测写个数据大概两三毫秒就能写完,所以我们每次写入之后需要Delay 5ms。
效果展示
效果请看视频:
AT24C02数据存储
以上就是本篇内容!
之后有时间还会补充一个“秒表(定时器扫描按键数码管)”的示例代码,敬请关注!
源码会放在评论区,自取!如有问题可评论区留言。