目录
本文参考资料:江协科技——[12-1] AT24C02(I2C总线)_bilibili
在使用51的过程中,常会用到诸如OLED、AT24C02(EEPROM)等通讯协议较复杂的外设,据此,Philips公司开发了一种通用数据总线“I2C总线(Inter IC BUS)”,下文将以AT24C02为例,介绍该总线协议的使用方法
一、I2C总线介绍
通信线: SCL (Serial Clock)、SDA (Serial Data);特性:同步、半双工,带数据应答
1. 电路规范
注:
1. 上拉电阻:通过电阻与一个固定的高电平VCC相接,使其电压在空闲状态保持在VCC电平电阻阻值与上下拉能力反相关,故强上拉的电阻阻值较小(强上拉一般在1K~5.6K,弱上拉至少是10K或更大阻值。)
推荐参考文章:最全讲解上下拉电阻 - 知乎
2. SCL和SDA配置为开漏输出模式:使得不必在每个设备都接电源及上拉电阻,进而减小电路干扰;对SCL和SDA各添一个总的上拉电阻,使得挂载在总线上的所有设备都能在同一时间收到相同的信号,因此实现总线上多设备的挂载和使用
2. 时序结构
1). 起始条件:SCL高电平期间,SDA从高电平切换到低电平
2). 终止条件:SCL高电平期间,SDA从低电平切换到高电平
3). 主机发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位在前),然后拉高SCL,从机在SCL高电平期间读取数据位(因此SCL高电平期间SDA不允许有数据变化),依次循环上述过程8次,即可发送一个字节。
4). 主机接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线.上(高位在前),然后拉高SCL,主机将在SCL高电平期间读取数据位(同样SCL高电平期间SDA不允许有数据变化),依次循环上述过程8次,即可接收一个字节。【注意:主机在接收之前,需要释放SDA】
5). 主机发送应答:在接收完一个字节之后,主机在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答。
6). 主机接收应答:在发送完一个字节之后,主机在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答。【注意:主机在接收之前,需要释放SDA】
即:主 ==> 从,从应答;从 ==> 主,主应答(主机接收完字节后,从机释放总线,主机接过总线后发送应答;而当主机发送完字节后,主机先释放总线,从机接过总线,通过总线向主机发送应答)
注意:上文中的 “主机发送/接收应答” 一般简称为:SA(Send Acknowledge 发送应答)、RA(Receive Acknowledge 接收应答),下文统一使用简称
3. I2C数据帧
主机发送数据帧内容:起始信号 → 包含写标志的从机地址 → 接收应答(从机给)→【发送字节→接收应答(从机给)(若发送n字节则循环n次)】→ 终止信号
主机接收数据帧内容:起始信号 → 包含读标志的从机地址→ 接收应答(从机给,表示收到)→【读取字节 → 发送应答(主机发)(若接收n字节则循环n次)】→ 终止信号
二、 AT24C02
1. 芯片介绍
2. 数据帧
芯片手册原图:
三、程序示例
根据I2C时序图,可以知道I2C总线协议需要实现:开始/停止、发送/接收数据、发送/接收应答的功能,据此编写单片机程序,其中为了提高代码可读性,采用 "sbit" 关键字声明位寄存器,用 #define语句对已定义的寄存器再定义。
语法:#define <易懂的名字> <寄存器> 和 sbit <易懂的名字> = <位寄存器>,示例如下:
#define MATRIX LED P0
sbit I2C_SCL=P2^1;
sbit I2C_SDA=P2^0;
sbit和sfr的区别:
- sfr (Special Function Register 特殊功能寄存器声明)
- sbit (Special Bit 特殊位声明)
例:
sfr P0= 0x80; //声明P0口寄存器,物理地址为0x80
sbit P0_1 = 0x81; 或 sbit P0_1 = P0^1; //声明P0寄存器的第1位
1. I2C开始/停止
实现函数
这里的SDA置1/0:保证SCL变化之前SDA的状态
2. 主机发送/接收数据
右图 SDA 置1:主机释放SDA,将控制权交给从机
3. 主机发送/接收应答
利用上述各I2C功能函数,可以实现使用AT24C02芯片存储数据 :
最后,我们在主程序中调用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芯片的最大写入耗时5ms,故此处延时5ms,下同
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," ");
}
}
}
四、关于主函数void main和int main的思考:
上文沿用的,51单片机开发(C51语言)常用的主函数形式:void main() { <函数体> } 并不是标准的C语言语法,但单片机的编译器(Keil)支持这种操作,这种写法也常见于很多资料中,而在C语言中标准的主函数形式 :int main(){ <函数体> } 也同样可用,且具有保证程序在不同编译器上都能够正常工作的优点;个人理解其原因在于:C++Primer第五版中文版(Page28)明确指出,main()函数的返回类型必须是int类型,即整数类型,而有些编译器会对其他语法作出适配,并且对C51语言来说,采用void关键字能更直观地体现其主函数无需返回值的特点,等效于没有return语句的int类型,但仍需明确其与标准语法的区别。