目录
2.1.1、AT24C02介绍:
AT24C02是一种可以实现掉电不丢失的存储器,可用于保存单片机运行时想要永久保存的数据信息,工作电压在1.8V~5.5V之间,输入引脚通过施密特触发器滤波抑制噪声,存储介质是EEPROM,通讯接口:I2C总线,容量:256字节,支持硬件写保护 且有高可靠性:读写次数:1,000,000 次 ,数据可保存100 年之久,该芯片被广泛应用于低电压及低功耗的工商业领域,而不同的AT24CX产品其容量也不同,下面给大家具体列出各种AT24CX产品的型号:
引脚定义图及说明:
应用电路如下图所示:
具体电子器件作用:
施密特触发器:它是一种常见的数字电路元件,用于抑制输入信号中的噪声和抖动,它是通过在输入信号的上升沿和下降沿之间引入一个滞后阈值来实现。具体工作流程是由电路元件的特性组成:施密特触发器具有两个阈值电压:上阈值电压(Vth_up)和下阈值电压(Vth_down)。当输入信号的电压高于上阈值电压时,输出保持为高电平;当输入信号的电压低于下阈值电压时,输出保持为低电平。只有当输入信号的电压跨过上阈值电压或下阈值电压时,输出才会发生翻转。
具体的工作流程示意图如下所示:
黑色部分代表具体的输入电压,而红色部分代表经过施密特触发器处理后的电压只有当输入电压达到高电平的阈值上限后才会变成高电平,当输入电压达到低电平的阈值下限后才会变为低电平。
EEPROM:AT24C02之所以可以实现掉电不丢失,就是因为他的存储介质是EEPROM(电可擦除和编程只读存储器),它是一种非易失性存储器,即在断电后,存储在其中的数据不会消失。并且EEPROM具有可编程性、存储密度高、低功耗和可靠性等优点,非常适合用于需要持久存储数据的应用场景。而AT24C02主要被用于于低电压及低功耗的工商业领域,这也是由该存储介质决定的,同时EEPROM可以实现单字节的擦除与编写,对于使用者来说比其他类型的可擦除和编程只读存储器(如闪存(W25Q64)只能实现扇区擦除与编写)更灵活。
2.1.2AT24C02内部框图学习:
其内部框图如下:
这里我们分别通过对AT24C02的两种操作(写操作和读操作)来讲解AT24C02是如何运作的:
字节写:
- 通信开始:主设备在SDA线上产生一个从高电平转为低电平的信号,同时SCL线处于高电平状态。这个信号被AT24C02的控制逻辑识别为起始条件。
- 写入设备地址/写入写标志,等待从机应答:(设备地址在点对点通信时不需要写入)主设备在SDA线上发送设备地址和写标志。这些信息在每个SCL的高电平到低电平的过渡时被串行数据选择器读取并传递到控制逻辑进行解析。
- 写入存储器地址,等待从机应答:主设备继续在SDA线上发送要写入的存储器地址。这个地址也在每个SCL的高电平到低电平的过渡时被串行数据选择器读取并传递到控制逻辑进行解析。
- 数据写入,等待从机应答:主设备在SDA线上发送要写入的数据。这些数据在每个SCL的高电平到低电平的过渡时被串行数据选择器读取并传递到数据缓冲器进行暂存,然后再写入到存储器阵列
- 通信结束:主设备在SDA线上产生一个从低电平转为高电平的信号,同时SCL线处于高电平状态。这个信号被AT24C02的控制逻辑识别为停止条件,表示写操作结束。
字节读:
- 通信开始:主设备在SDA线上产生一个从高电平转为低电平的信号,同时SCL线处于高电平状态。这个信号被AT24C02的控制逻辑识别为起始条件。
- 开始写设备地址/写入写标志,等待应答:(设备地址在点对点通信时不需要写入)发送设备地址和写标志。这些信息被AT24C02的控制逻辑和串行数据选择器处理。
- 写入读取的存储器地址,等待应答:主设备发送要读取的存储器地址,这个地址被串行数据选择器读取并传递到控制逻辑进行解析。
- 再次开始通信并写入读标志,等待应答:主设备产生另一个起始条件并发送设备地址,这次是读标志。这些信息再次被AT24C02的控制逻辑和串行数据选择器处理。
- 数据读取读完一个字节后,发送应答停止读取:AT24C02开始从指定的地址读取数据。数据从存储器阵列读取到数据缓冲器,然后在每个SCL的低电平到高电平的过渡时,通过串行数据选择器发送到SDA线上。
- 通信停止:主设备产生停止条件,表示读操作结束。
以上是我们结合AT24C02的具体操作来描述内部框图的具体工作流程,需要注意的是AT24C02支持随机读取和顺序读取两种模式。在随机读取模式中,可以直接读取任何地址的数据。在顺序读取模式中,可以从一个指定的地址开始,连续读取多个字节的数据。而我们学习完如何进行读写操作后,而这里描述的起始条件数据写入,数据读取,数据应答,和停止条件是怎么做到的,我们就要学习I2C通信的具体时序(同样I2C的驱动代码主办方也会给出)。
2.2、IIC通信协议:
2.2.1 I2C通信协议介绍
I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线,I2C协议使用两根信号线,分别是串行数据线(SDA)和串行时钟线(SCL)。这两根线共享总线,并且可以连接多个设备。其中,SDA线用于传输数据,SCL线用于同步数据传输的时钟信号。
在I2C通信中,设备之间分为两种角色:主机(Master)和从机(Slave)。主设备负责发起和控制通信过程,而从设备则响应主设备的指令和传输数据。
I2C通信特点:
- 速度可变:I2C通信的速度可以根据需求进行调整,常见的速度有100 kbps、400 kbps和1 Mbps等。
- 双向通信:I2C协议支持双向数据传输,主设备可以向从设备发送数据,也可以从从设备读取数据。
- 硬件简单:I2C协议只需要两根信号线,相对于其他通信协议来说,硬件实现相对简单。
- 注意:虽然I2C通信实现有硬件和软件两种,但是我们目前8位单片机没有搭载硬件IIC的实现,并且8位单片机的资源是有限的,所以,通过IO口模拟IIC的时序来实现软件模拟IIC通信会更好。
实现软件I2C是通过直接控制8位单片机的两个 IO 引脚,分别用作 SCL 及 SDA,按照规定的时序要求,直接像控制 LED 灯那样控制引脚的输出 (若是接收数据时则读取 SDA 电平),就可以实现 I2C 通讯。
I2C设备的连接方式:
如图所有I2C设备的SCL连在一起,SDA连在一起,设备的SCL和SDA均要配置成开漏输出模式,SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右。(为了避免总线调治不协调,I2C规定禁止所有的设备输出强上拉的高电平,采用弱上拉的电阻加开漏输出的结构。)具体电路元件如下:
I2C时序信号:
起始信号与中止信号:
起始条件:SCL高电平期间,SDA从高电平切换到低电平
停止条件:SCL高电平期间,SDA从低电平切换到高电平
代码如下:(附详细解释)
void I2C_Start()//起始条件
{
I2C_SDA=1;
I2C_SCL=1;//给SCL与SDA同时置高电平
I2C_SDA=0;//在SCL为高电平期间将SDA拉低
I2C_SCL=0;//SCL拉低为写入(读出)数据做准备
}
void I2C_Stop()//停止条件
{
I2C_SDA=0;//将SDA置高电平
I2C_SCL=1;//拉高SCL
I2C_SDA=1;//在SCL高电平期间将SDA从低电平变换为高电平
}
发送一个字节:
SCL低电平期间(SCL=0),主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。
代码部分(附详细解释):
//在第一次写入数据时由于在起始条件的时候已经将SCL拉低
//所以下列代码并没有将SCL置0的过程,直接将数据写入SDA即可
void I2C_SendBit(bit dat)//发送一位数据
{
I2C_SDA = dat;//在SCL低电平期间将发送的数据写入SDA
I2C_SCL = 1;//在SCL高电平期间从机读取要发送的数据
I2C_SCl = 0;//将SCL拉低为下一次发送数据做准备
}
//发送一个字节的过程其实就是把发送一位的过程连续执行八次
void I2C_SendByte(unsigned char Byte)//发送一个字节
{ unsigned char i;
for(i=0;i<8;i++)
{
//I2C数据写入的时候时从高位往低位写所以,这里要从(最高位开始与)
I2C_SDA=Byte&(0x80>>i);
I2C_SCL=1;
I2C_SCL=0;
}
}
接收一个字节:
SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)(主机失去对SDA的控制由从机控制SDA给主机发送数据)
代码部分(附详细解释)
unsigned char I2C_ReceiveByte(void)//主机接收一位数据
{
unsigned char i,Byte=0x00;//将参数初始化
I2C_SDA=1;//释放I2C总线,此时由从机控制通信
for(i=0;i<8;i++)
{
I2C_SCL=1;//将SCL拉高主机准备读取从机发送的数据
if(I2C_SDA){Byte|=(0x80>>i);} //主机读取从机发送的数据
//(读取数据时也是从高位往低位读)
I2C_SCL=0;//将SCL拉低准备下一次的读取
//.....这里执行的是从机将主机要读取的数据放在SDA数据线上
//虽然代码不体现,但我们要了解这个过程
}
return Byte;//将接收的数据作为参数返回
}
发送应答与接收应答:
发送应答:主机在接收完一个字节之后,在下一个时钟发送(与发送字节里单独发送一位的操作一致)一位数据,数据0表示应答,数据1表示非应答
代码部分:
//发送一个应答其实就是发送一位数据,其通信过程与发送一位数据基本完全一致
void I2C_SendAck(unsigned char AckBit)//发送一个应答
{//在数据写入完成之后已经将SCL拉低具体可以看发送一个字节的代码,所以这里不用再将SCL拉低
I2C_SDA=AckBit;//在SCL低电平期间将要发送的应答位写入SDA
I2C_SCL=1;//SCL电平拉高读取主机发送的应答
I2C_SCL=0;//SCL拉低为后续操作做准备
}
接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)
代码部分:
unsigned char I2C_ReceiveAck(void)
{
unsigned char AckBit;
I2C_SDA=1;//释放I2C总线由从机控制通信
I2C_SCL=1;//拉高SCL为主机接收从机发送的应答做准备
AckBit=I2C_SDA;//将SDA数据线中的应答复制给变量
I2C_SCL=0;//拉低SCL,为后续操作做准备
return AckBit;
}
I2C组合六种时序的具体使用方法:
写一位数据:
读以为数据:
S:起始位
SLAVE ADDRESS:代表从机地址,表示主机要向哪个从机发送数据
RW:读还是写,0表示主机发送数据,1表示主机接收数据
WORD ADDRESS: 代表存储器地址
RA:应答位,主机向从机发送完从机地址后,被选中的从机会向主机发送一个应答位,表示接收到了主机发送的数据
DATA:数据,一次发送八位二进制数据,从机在接收到数据后会发送一个应答位,
P:是停止位
注意:
- I2C 使用 SDA 信号线来传输数据,使用 SCL 信号线进行数据同步 。
- SDA 数据线在 SCL 的每个时钟周期传输一位数据。
- 传输时,SCL 为高电平的时候 SDA 表示的数据有效,即此时的 SDA 为高电平时表示数据“1”,为低电平时表示数据“0”。当 SCL为低电平时,SDA的数据无效,一般在这个时候 SDA 进行电平切换,为下一次表示数据做好准备。
- I2C总线上每个设备都有自己的独立地址,主机发起通讯时通过SDA发送设备地址(SLAVE ADDRESS)来查找从机,I2C协议规定从机地址可以是7位也可以是10位,设备地址后一位是数据方向位,第八位或者第十一位,0表示主机发送,1表示主机接收。
- I2C 的数据和地址传输都带响应。响应包括“应答 (ACK)”和“非应答 (NACK)”两种信号。作为数据接收端时,当设备 (无论主从机) 接收到 I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答 (ACK)”信号,发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答 (NACK)”信号,发送方接收到该信号后会产生一个停止信号,结束信号传输。
2.3、基础训练内容:
- 手写I2C协议(也可用底层驱动代码,主要是为了学习)。
- 将J5的23脚短接,把S4、S5和S6设置为独立按键。
- 用24C02存储器的0x00、0x01和0x02这个三个地址单 元分别存储S4、S5和S6的按下次数。
- 系统上电后,先从24C04存储器的0x00、0x01和0x02 这三个地址单元读取数据,接着判断读出的数据,如果大于13, 则复位清0,然后从左到右依次显示在数码管上,各个数字之间用“-”分隔。
- S4、S5 和S6按键每按下一次,就在对应读出的历史按下次数基础上进行加1累计, 当累计值大于13时,复位清0。
- 将按键按下的最新次数写入24C02的对应单元,并在数码管上刷新显示,并且要保证单片机掉电后重新上电之后,按键按下次数可以保留
2.4代码实现:
main.c
#include "System.h"
void main(){
Init_System();//系统初始化
Timer0_Init();//定时器初始化
while(1){
Key_Statistics();//按键次数计数
Display_SMG(); //显示按键次数
}
}
I2C.c
#include "I2C.h"
void I2C_Start(void){//起始信号
SDA = 1;
SCL = 1;
SDA = 0;
SCL = 0;
}
void I2C_Stop(void){//停止信号
SDA = 0;
SCL = 1;
SDA = 1;
}
void I2C_SendByte(unsigned char Byte){//发送一位数据
unsigned char i;
for(i = 0; i < 8; i++){
SDA = Byte&(0x80>>i);
SCL = 1;
SCL = 0;
}
}
unsigned char I2C_ReceiveByte(void){//接收一位数据
unsigned char i,Byte=0x00;
SDA = 1;
for(i = 0; i < 8; i++){
SCL = 1;
if(SDA){Byte |= (0x80>>i);}
SCL = 0;
}
return Byte;
}
void I2C_SendACK(unsigned char ACK){//发送应答
SDA = ACK;
SCL = 1;
SCL = 0;
}
unsigned char I2C_waitACK(void){//等待应答
unsigned char ACK;
SDA = 1;
SCL = 1;
ACK = SDA;
SCL = 0;
return ACK;
}
AT24C.c
#include "AT24C02.h"
unsigned char AT24C02_ReadByte(unsigned char address){
unsigned char Byte= 0;
I2C_Start();//开始通信
I2C_SendByte(SlaveAddrW);//写入写标志
I2C_waitACK();//等待应答
I2C_SendByte(address);//写入要写的地址
I2C_waitACK();//等待应答
I2C_Start();//重启开始通信
I2C_SendByte(SlaveAddrR);//写入读标志
I2C_waitACK();//等待应答
Byte = I2C_ReceiveByte();//读取内容
I2C_SendACK(1);//发送停止信号
I2C_Stop();//结束通信
return Byte;
}
void AT24C02_WriteByte(unsigned char address,unsigned char Byte){
I2C_Start();//开始通信
I2C_SendByte(SlaveAddrW);//发送写地址
I2C_waitACK();//等待应答
I2C_SendByte(address);//发送数据要写入的地址
I2C_waitACK();//等待应答
I2C_SendByte(Byte);//在指定的地址中写入数据
I2C_waitACK();//等待应答
I2C_Stop();//结束通信
}
Key.c(按键扫描用定时器做)
#include "key.h"
void Timer0_Init(){//定时器初始化
TMOD = 0xF0;
TL0 = 0x18;
TH0 = 0x66;
TR0 = 1;
EA = 1;
ET0 = 1;
PT0 = 0;
}
unsigned char Key_Scan(void){//读取按键键值
uchar KeyNumber = 0;
if(Key6==0){ if(Key6==0)KeyNumber = 6;}
if(Key5==0){ if(Key5==0)KeyNumber = 5;}
if(Key4==0){ if(Key4==0)KeyNumber = 4;}
return KeyNumber;
}
unsigned char Key_Read(void){//获取按键键值
static uchar Laststate,nowstate;
uchar Keynum;
Laststate = nowstate;
nowstate = Key_Scan();
if(Laststate == 6 && nowstate == 0) Keynum = 6;
if(Laststate == 5 && nowstate == 0) Keynum = 5;
if(Laststate == 4 && nowstate == 0) Keynum = 4;
return Keynum;
}
void Timer0_handel(void) interrupt 1{
static uint T0Count = 0;
T0Count++;
if(T0Count>=20){
T0Count = 0;
Key_Read();//用定时器扫描按键
}
}
System.c
#include "System.h"
uchar S6=0;
uchar S5=0;
uchar S4=0;
uchar nowkey;
uchar lastkey;
/*0 1 2 3 4 5 6 7 8 9 */
uchar code NixieTube_nodot[]={0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,0x80,0x90,
/* A B C D E F H L N P */
0x88,0x83,0xC6,0xA1,0x86,0x8E,0x89,0xC7,0xC8,0x8C,
/* U - ' ' */
0xC1,0xBF,0xFF};//无小数点
void HC138_Select(uchar choice){//P0IO口工作模式选择选择
switch(choice){
case 0: P2 =( P2 & 0x1f) | 0x00; break;
case 4: P2 =( P2 & 0x1f) | 0x80; break;
case 5: P2 =( P2 & 0x1f) | 0xa0; break;
case 6: P2 =( P2 & 0x1f) | 0xc0; break;
case 7: P2 =( P2 & 0x1f) | 0xe0; break;
}
}
void Init_System(){//对系统初始化
HC138_Select(4);
P0 = 0xFF;
HC138_Select(5);
P0 = 0x00;
HC138_Select(6);
P0 = 0xFF;
HC138_Select(7);
P0 = 0xFF;
}
void Key_Statistics(void){//按键按下次数计数
lastkey = nowkey;//获取按键键值
nowkey = Key_Read();
if(lastkey == 6){
if(lastkey == 6){
S6++;
if(S6>13)//如果大于13计数清零
{
S6 = 0;
AT24C02_WriteByte(0x00,0x00);//将读取的按键次数写入AT24C02中
}
else{
AT24C02_WriteByte(0x00,S6);
}
}
}
if(lastkey == 5){
if(lastkey == 5){
S5++;
if(S5>13)
{
S5 = 0;
AT24C02_WriteByte(0x01,0x00);
}
else{
AT24C02_WriteByte(0x01,S5);
}
}
}
if(lastkey == 4){
if(lastkey == 4){
S4++;
if(S4>13)
{
S4 = 0;
AT24C02_WriteByte(0x02,0x00);
}
else{
AT24C02_WriteByte(0x02,S4);
}
}
}
}
void Delay(uchar xms) //@11.0592MHz
{
unsigned char data i, j;
while(xms--){
i = 12;
j = 190;
do
{
while (--j);
} while (--i);
}
}
void SMG_TranslateBit(uchar pos,uchar value){
HC138_Select(7);
P0 = 0xFF;//消隐
HC138_Select(6);
P0 = 0x01<<pos;
HC138_Select(7);
P0 = value;
Delay(2);
}
void Display_SMG(void){
//读取AT24C02
S6 = AT24C02_ReadByte(0x00);//读取保存至AT24C02中的按键次数
S5 = AT24C02_ReadByte(0x01);
S4 = AT24C02_ReadByte(0x02);
//按键次数显示函数
SMG_TranslateBit(0,NixieTube_nodot[S6/10]);
SMG_TranslateBit(1,NixieTube_nodot[S6%10]);
SMG_TranslateBit(2,NixieTube_nodot[21]);
SMG_TranslateBit(3,NixieTube_nodot[S5/10]);
SMG_TranslateBit(4,NixieTube_nodot[S5%10]);
SMG_TranslateBit(5,NixieTube_nodot[21]);
SMG_TranslateBit(6,NixieTube_nodot[S4/10]);
SMG_TranslateBit(7,NixieTube_nodot[S4%10]);
}