P12 AT24C02'I2C'总线
前言
上一节我学习了蜂鸣器的知识,这节学习AT24C02的内容,并以此学习I2C通信以及定时器扫描按键和数码管的知识,实现数据存储和秒表计时两个功能。
存储器介绍
- RAM(random access memory):存储速度快,但掉电丢失。运行程序一般在RAM中。
- SRAM:内部就是一个锁存器,存储速度最快,一般用于CPU。
- DRAM:利用电容存储数据,需要动态刷新,一般用于内存条。
- ROM(read only memory):掉电不丢失,但存储速度慢。
- Mask ROM:仅仅依靠电路存储数据。
- PROM:只能写入一次数据。
- EPROM:需要紫外线照射才能重新编写。
- EEPROM:5v低压电可编程编写。
- Flash:现在应用最广泛的存储器。
存储器像是一个网格,左边地址总线一次只能选中一行,然后选择连接的结点(右边的就是不同ROM连接结点的方式),数据总线输出数据。
AT24C02介绍
I2C介绍
开漏输出模式如下图所示:
- 开漏输出保证在开关闭合的时候,输出低电平;而开关断开时,受到上拉电阻的作用,断开的端口被拉至高电平。
- 那为什么要把上拉电阻放到外面呢,如果直接在支路接上拉电阻,可能会被其它支路的低电平拉至低电位。
- I2C总线的设计通过开漏输出和上拉电阻的组合,实现了一种选择机制。在需要通信时,设备主动拉低总线(输出0);在不需要通信时,上拉电阻会将总线拉高(输出1)这种机制确保了多设备连接在同一总线上时稳定通信。
I2C时序结构
起始和终止
发送字节
- 发送字节时,SLC高电平时,SDA不能有点位变化
- SLC高电平时,若SDA是高电平则表示1,若SDA低电平则表示0
- 这是为了防止把发送字节误认为起始信号或终止信号
- 意思就是在SCL处于低电平时改变SDA让SDA变成你要发送的数据0或1,然后拉高SCL,SCL处于高电平时读取数据0或1。
接收字节
- 当主机发送时,在开始序列中,将SDA=0,此时控制权在主机手上,那么开始发送后,从机默认读取。
- 当从机发送时,在开始序列中,将SDA=0,此时控制权依然在主机手上,那么开始发送后,为了使从机发送,需要将SDA=1(让杆子默认为1,允许从机向下拉动),那么此时从机才获得控制权,主机默认读取。
- 谁控制sda,谁就发送,反之接收
带数据应答
同理,主机在发送完一个字节之后,为了判断从机是否接受应答,先释放掉管理权,即SDA置1,如果从机接收到了,那么就会拉一下杆子表示自己在听,此时SDA变为0,于是主机就接收到了应答。
I2C数据帧
I2C数据帧有三种
- 发送一帧数据,S起始。
- SLAVE ADDRESS + W,八个字节,前四位固定1010,后三位可配置,最后一位选择W发送。
- RA:0 从机接收应答。
- S:发送一个字节。 RA:0 接收。
以此往复。 - P终止。
- 接收一帧数据,S起始。
- SLAVE ADDRESS + R,八个字节,前四位固定1010,后三位可配置,最后一位选择R接收。
- RA:0 从机发送应答。
- R:主机读一个字节。 SA:0 主机应答。
以此往复。 - SA:1 结束应答 P终止。
- 先发送再接收
- 结合图片内容,形象形容一下:老师(主机)、小明(从机)
- 老师说我要开始提问了(S),小明你听着(S:SLAVE ADDRESS),问题我写在黑板上( + W),小明收到( RA:0 ),问题1(S:BYTE1),小明收到( RA:0 )。
- 老师说开始回答(S),小明你看着黑板的问题来回答(S:SLAVE ADDRESS + R),小明收到( RA:0 ),小明说问题1的答案是(R:BYTE1),老师说收到( SA:0 ),最后老师说够了(SA:1),停止回答(P)。
AT24C02数据帧
- 字节写类比刚刚I2C的发送一帧字节。
- 随机读类比复合格式。
以上这些知识听的我还是有些迷茫,希望通过写程序来加深我的理解。
12-1 AT24C02数据存储
这部分程序的大体思路分为两个模块:
- I2C.c包含上面讲的六部分
- AT.c包含写和读部分·
在主函数调用这两模块实现我们的功能。主要就是根据咱们各部分的时序图来设置SDA和SCL。
I2C模块
先来编写I2C模块(这里的发送和接收的主体都是主机视角)
#include <REGX52.H>
sbit I2C_SCL=P2^1;
sbit I2C_SDA=P2^0;
/**
* @brief I2C开始
* @param 无
* @retval 无
*/
void I2C_Start(void)
{
I2C_SDA=1;
I2C_SCL=1;
I2C_SDA=0;
I2C_SCL=0;
}
/**
* @brief I2C停止
* @param 无
* @retval 无
*/
void I2C_Stop(void)
{
I2C_SDA=0;
I2C_SCL=1;
I2C_SDA=1;
}
/**
* @brief I2C发送一个字节
* @param Byte 要发送的字节
* @retval 无
*/
void I2C_SendByte(unsigned char Byte)
{
unsigned char i;
for(i=0;i<8;i++)
{
I2C_SDA=Byte&(0x80>>i);
I2C_SCL=1;
I2C_SCL=0;
}
}
/**
* @brief I2C接收一个字节
* @param 无
* @retval 接收到的一个字节数据
*/
unsigned char I2C_ReceiveByte(void)
{
unsigned char i,Byte=0x00;
I2C_SDA=1;
for(i=0;i<8;i++)
{
I2C_SCL=1;
if(I2C_SDA){Byte|=(0x80>>i);}
I2C_SCL=0;
}
return Byte;
}
/**
* @brief I2C发送应答
* @param AckBit 应答位,0为应答,1为非应答
* @retval 无
*/
void I2C_SendAck(unsigned char AckBit)
{
I2C_SDA=AckBit;
I2C_SCL=1;
I2C_SCL=0;
}
/**
* @brief I2C接收应答位
* @param 无
* @retval 接收到的应答位,0为应答,1为非应答
*/
unsigned char I2C_ReceiveAck(void)
{
unsigned char AckBit;
I2C_SDA=1;
I2C_SCL=1;
AckBit=I2C_SDA;
I2C_SCL=0;
return AckBit;
}
- 起始S:由于起始时(S)SDA和SCL为高电平,为保证什么时候都能调用S,所以SDA和SCL先置1再拉低0。
- 终止P:SDA先拉低后,拉高SCL,再拉高SDA。
- 发送 S:(Byte):主机发送数据时,SDA=Byte&(0x80>>i),循环八次:把数据放在SDA,再将SCL拉高,再拉低。
- 接收R:(Byte):主机接收数据时,先释放SDA(SDA=1),if(SDA=1) { byte |= 0x80>>i; };就是SCL高电平读取SDA数据的时候,若SDA上数据为1,则将data上的那一位置1,若SDA为0,不执行if语句,因为byte=0x00,这个if作用是用于接收数据的,因为SDA现在控制权在从机,上面的SDA=1是主机释放,如果从机发送1,那我就用if接收。
- 发送应答S:(1/0):因为这个是主机的代码,主机先给总线ackbit(SDA),再发信号(SCL)让从机接受信号
- 接收应答R:(1/0):SDA=1是主机释放控制权,也就是说此时从机可以获得控制权可以将SDA=0或者1,至于等于多少是从机自己内部处理,不在代码上体现,代码只要让scl来个上升沿然后接收这个应答就行了。
AT24C02模块
根据我们AT24C02数据帧部分的PPT编写程序
#include <REGX52.H>
#include "I2C.h"
#define AT24C02_ADDRESS 0xA0
/**
* @brief AT24C02写入一个字节
* @param WordAddress 要写入字节的地址
* @param Data 要写入的数据
* @retval 无
*/
void AT24C02_WriteByte(unsigned char WordAddress,Data)
{
I2C_Start();
I2C_SendByte(AT24C02_ADDRESS);
I2C_ReceiveAck();
I2C_SendByte(WordAddress);
I2C_ReceiveAck();
I2C_SendByte(Data);
I2C_ReceiveAck();
I2C_Stop();
}
/**
* @brief AT24C02读取一个字节
* @param 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;
}
- 结合图片内容,形象形容一下:老师(主机)、小明(从机)
- 老师说我要开始提问了(S),小明你听着(S:SLAVE ADDRESS),问题我写在黑板上( + W),小明收到( RA:0 ),问题1(S:BYTE1),小明收到( RA:0 )。
- 老师说开始回答(S),小明你看着黑板的问题来回答(S:SLAVE ADDRESS + R),小明收到( RA:0 ),小明说问题1的答案是(R:BYTE1),老师说收到( SA:0 ),最后老师说够了(SA:1),停止回答(P)。
测试下我们写的功能
#include <REGX52.H>
#include "Key.h"
#include "LCD1602.h"
#include "AT24C02.h"
#include "DELAY.h"
unsigned char Data;
int main()
{
LCD_Init();
LCD_ShowString(1,1,"Hello");
AT24C02_WriteByte(1,66);
Delay(5);
Data=AT24C02_ReadByte(1);
LCD_ShowNum(2,1,Data,3);
while(1)
{
}
}
写周期需要5ms的写入时间,所以每次写完后都要Delay5ms
成功实现在1地址写入66,读取66。
完成AT24C02数据存储
主函数代码:
#include <REGX52.H>
#include "LCD1602.h"
#include "Key.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=Key();
if(KeyNum==1) //K1按键,Num自增
{
Num++;
LCD_ShowNum(1,1,Num,5);
}
if(KeyNum==2) //K2按键,Num自减
{
Num--;
LCD_ShowNum(1,1,Num,5);
}
if(KeyNum==3) //K3按键,向AT24C02写入数据
{
AT24C02_WriteByte(0,Num%256);
Delay(5);
AT24C02_WriteByte(1,Num/256);
Delay(5);
LCD_ShowString(2,1,"Write OK");
Delay(1000);
LCD_ShowString(2,1," ");
}
if(KeyNum==4) //K4按键,从AT24C02读取数据
{
Num=AT24C02_ReadByte(0);
Num|=AT24C02_ReadByte(1)<<8;
LCD_ShowNum(1,1,Num,5);
LCD_ShowString(2,1,"Read OK ");
Delay(1000);
LCD_ShowString(2,1," ");
}
}
}
12-2 秒表(定时器扫描按键数码管)
这部分运用了一个新的编程方法:通过一个中断函数的功能实现两个外部函数的delay嘛定时器分别提供给这两个函数做延时作用。
之前我们的按键模块和数码管模块都是采用延时加死循环的方式来实现功能,这样会产生我们在按键按下的时候,主循环的其他程序就不能运行的问题,因此我们对按键和数码管模块也进行了改进。
完整代码
按键模块:
#include <REGX52.H>
#include "Delay.h"
unsigned char Key_KeyNumber;
/**
* @brief 获取按键键码
* @param 无
* @retval 按下按键的键码,范围:0,1~4,0表示无按键按下
*/
unsigned char Key(void)
{
unsigned char Temp=0;
Temp=Key_KeyNumber;
Key_KeyNumber=0;
return Temp;
}
/**
* @brief 获取当前按键的状态,无消抖及松手检测
* @param 无
* @retval 按下按键的键码,范围:0,1~4,0表示无按键按下
*/
unsigned char Key_GetState()
{
unsigned char KeyNumber=0;
if(P3_1==0){KeyNumber=1;}
if(P3_0==0){KeyNumber=2;}
if(P3_2==0){KeyNumber=3;}
if(P3_3==0){KeyNumber=4;}
return KeyNumber;
}
/**
* @brief 按键驱动函数,在中断中调用
* @param 无
* @retval 无
*/
void Key_Loop(void)
{
static unsigned char NowState,LastState;
LastState=NowState; //按键状态更新
NowState=Key_GetState(); //获取当前按键状态
//如果上个时间点按键按下,这个时间点未按下,则是松手瞬间,以此避免消抖和松手检测
if(LastState==1 && NowState==0)
{
Key_KeyNumber=1;
}
if(LastState==2 && NowState==0)
{
Key_KeyNumber=2;
}
if(LastState==3 && NowState==0)
{
Key_KeyNumber=3;
}
if(LastState==4 && NowState==0)
{
Key_KeyNumber=4;
}
}
数码管模块:
#include <REGX52.H>
#include "Delay.h"
//数码管显示缓存区
unsigned char Nixie_Buf[9]={0,10,10,10,10,10,10,10,10};
//数码管段码表
unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x00,0x40};
/**
* @brief 设置显示缓存区
* @param Location 要设置的位置,范围:1~8
* @param Number 要设置的数字,范围:段码表索引范围
* @retval 无
*/
void Nixie_SetBuf(unsigned char Location,Number)
{
Nixie_Buf[Location]=Number;
}
/**
* @brief 数码管扫描显示
* @param Location 要显示的位置,范围:1~8
* @param Number 要显示的数字,范围:段码表索引范围
* @retval 无
*/
void Nixie_Scan(unsigned char Location,Number)
{
P0=0x00; //段码清0,消影
switch(Location) //位码输出
{
case 1:P2_4=1;P2_3=1;P2_2=1;break;
case 2:P2_4=1;P2_3=1;P2_2=0;break;
case 3:P2_4=1;P2_3=0;P2_2=1;break;
case 4:P2_4=1;P2_3=0;P2_2=0;break;
case 5:P2_4=0;P2_3=1;P2_2=1;break;
case 6:P2_4=0;P2_3=1;P2_2=0;break;
case 7:P2_4=0;P2_3=0;P2_2=1;break;
case 8:P2_4=0;P2_3=0;P2_2=0;break;
}
P0=NixieTable[Number]; //段码输出
}
/**
* @brief 数码管驱动函数,在中断中调用
* @param 无
* @retval 无
*/
void Nixie_Loop(void)
{
static unsigned char i=1;
Nixie_Scan(i,Nixie_Buf[i]);
i++;
if(i>=9){i=1;}
}
主函数main.c
#include <REGX52.H>
#include "Timer0.h"
#include "Key.h"
#include "Nixie.h"
#include "Delay.h"
#include "AT24C02.h"
unsigned char KeyNum;
unsigned char Min,Sec,MiniSec;
unsigned char RunFlag;
void main()
{
Timer0_Init();
while(1)
{
KeyNum=Key();
if(KeyNum==1) //K1按键按下
{
RunFlag=!RunFlag; //启动标志位翻转
}
if(KeyNum==2) //K2按键按下
{
Min=0; //分秒清0
Sec=0;
MiniSec=0;
}
if(KeyNum==3) //K3按键按下
{
AT24C02_WriteByte(0,Min); //将分秒写入AT24C02
Delay(5);
AT24C02_WriteByte(1,Sec);
Delay(5);
AT24C02_WriteByte(2,MiniSec);
Delay(5);
}
if(KeyNum==4) //K4按键按下
{
Min=AT24C02_ReadByte(0); //读出AT24C02数据
Sec=AT24C02_ReadByte(1);
MiniSec=AT24C02_ReadByte(2);
}
Nixie_SetBuf(1,Min/10); //设置显示缓存,显示数据
Nixie_SetBuf(2,Min%10);
Nixie_SetBuf(3,11);
Nixie_SetBuf(4,Sec/10);
Nixie_SetBuf(5,Sec%10);
Nixie_SetBuf(6,11);
Nixie_SetBuf(7,MiniSec/10);
Nixie_SetBuf(8,MiniSec%10);
}
}
/**
* @brief 秒表驱动函数,在中断中调用
* @param 无
* @retval 无
*/
void Sec_Loop(void)
{
if(RunFlag)
{
MiniSec++;
if(MiniSec>=100)
{
MiniSec=0;
Sec++;
if(Sec>=60)
{
Sec=0;
Min++;
if(Min>=60)
{
Min=0;
}
}
}
}
}
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count1,T0Count2,T0Count3;
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count1++;
if(T0Count1>=20)
{
T0Count1=0;
Key_Loop(); //20ms调用一次按键驱动函数
}
T0Count2++;
if(T0Count2>=2)
{
T0Count2=0;
Nixie_Loop();//2ms调用一次数码管驱动函数
}
T0Count3++;
if(T0Count3>=10)
{
T0Count3=0;
Sec_Loop(); //10ms调用一次数秒表驱动函数
}
}
静态变量:是C语言变量中的一种特殊类型,此变量会保存在静态数据区而不是栈或堆中,静态变量使用范围为全局而不像普通变量一样只在本函数使用。