史上最详细IIC教程
基础知识介绍
时序图
本文所有时序图均来自AT24C02的芯片手册
IIC
I2C 总线是一种串行数据总线,只有二根信号线,一根是双向的数据线SDA,另一根是时钟线SCL,两条线可以挂多个设备。 IIC设备(绝大多数)里有个固化的地址,只有在两条线上传输的值等于IIC设备的固化地址时,其才会作出响应。通常我们为了方便把IIC设备分为主设备和从设备,基本上谁控制时钟线(即控制SCL的电平高低变换)谁就是主设备。
AT24C02
AT24C02是一个2K位串行CMOS E2PROM,有一个16字节页写缓冲器。该器件通过IIC总线接口进行操作,有一个专门的写保护功能, 如果WP管脚连接到Vcc,所有的内容都被写保护只能读。当WP管脚连接到Vss or GND 或悬空允许器件进行正常的读/写操作。
AT24C02的存储容量为2K bit,内容分成32页,每页8Byte,共256Byte,操作时有两种寻址方式:芯片寻址和片内子地址寻址。
(1)芯片寻址:AT24C02的芯片地址为1010,其地址控制字格式为1010A2A1A0R/W。其中A2,A1,A0可编程地址选择位,A2,A1,A0引脚接高、低电平后得到确定的三位编码,与1010形成7位编码,即为该器件的地址码。A0、A1、A2引脚是器件地址输入端,这些输入脚用于多个器件级联时设置器件地址,当这些脚悬空时默认值为0,通过改变A2,A1,A0的引脚状态,共有8种组合,所以一个IIC总线最大可级联8个器件。R/W为芯片读写控制位,该位为0,表示芯片进行写操作。在下面的分析及程序中,只有一个AT24C02器件,A2,A1,A0全部悬空,即全部为0,地址控制字为1010000*,*为R/W位。
(2)片内子地址寻址:芯片寻址可对内部256B中的任一个进行读/写操作,其寻址范围为00~FF,共256个寻址单位。
主要C语言程序声明
sbit sda = P2^0; // I2C接口SDA定义
sbit scl = P2^1; // I2C接口SCL定义
void flash() // 延时子函数
{
; ; ;
}
void delay(unsigned char i) // 延时程序
{
for( j=i ; j>0 ; j-- )
for( k=125; k>0; k-- );
}
IIC通信时序图及子程序讲解
初始化、起始和停止
初始化:IIC的初始化为SDA和SCL均为高。
开始信号:处理器让SCL时钟保持高电平,然后让SDA数据信号由高变低就表示一个开始信号。同时IIC总线上的设备检测到这个开始信号它就知道处理器要发送数据了。
停止信号:处理器让SCL时钟保持高电平,然后让SDA数据信号由低变高就表示一个停止信号。同时IIC总线上的设备检测到这个停止信号它就知道处理器已经结束了数据传输,我们就可以各忙各个的了,如休眠等。
void x24c08_init()// 2402初始化子程序
{
scl = 1;//首先把时钟线拉高
flash();//延时函数,后面不再注释
sda = 1;//在SCL为高的情况下把SDA拉高,实际上是在IIC总线上发送了一个停止信号
flash();
}
void start()// 启动I2C总线
{
sda = 1;//确保SDA线为高电平
flash();
scl = 1; //确保SCL高电平
flash();
sda = 0;//在SCL为高时拉低SDA线,即为起始信号
flash();
scl = 0;//将SCL线拉低,为后面SDA的电平变化做准备
flash();
}
void stop()// 停止I2C总线
{
sda = 0;//确保SDA线为低电平
flash();
scl = 1;//确保SCL高电平
flash();
sda = 1;//在SCL为高电平时拉高SDA线,即为停止信号,此时SDA和SCL均为高,同时完成IIC初始化过程
flash();
}
数据传输和响应信号
数据传输:SDA上的数据只能在SCL为低电平期间翻转变化,在SCL为高电平期间必须保持稳定,IIC设备只在SCL为高电平期间采集SDA数据。
响应信号(ACK):单片机发完8bit数据后就不再驱动总线了(SDA引脚变输入),而SDA和SDL硬件设计时都有上拉电阻,所以这时候SDA变成高电平。那么在第8个数据位,如果外接IIC设备能收到信号的话接着在第9个周期把SDA拉低,那么处理器检测到SDA拉低就能知道外接IIC设备数据已经收到。IIC数据从最高位开始传输(小端传输)。
void writex(unsigned char j)// 向IIC总线上写8位
{
unsigned char i,temp;
temp = j;
for(i=0; i<8; i++)//循环8次,每次发送一位数据
{
temp = temp<<1;//将temp左移一位,如果temp最高位是1,左移后将产生进位,进位标志CY=1,如果temp最高位是0,左移后不会产生进位,进位标志CY=0
scl = 0;//将SCL总线拉低,为SDA电平变化做准备
flash();
sda = CY;//根据进位标志CY更改SDA的电平
flash();
scl = 1;//将SCL拉高,告诉IIC器件现在可以检测SDA的电平了
flash();
}
scl = 0;//8位数据传输完成后,将SCL拉低
flash();
sda = 1;//将SDL拉高,开始为IIC设备的应答ACK做准备
flash();
}
void clock()// I2C总线应答子函数
{
unsigned char i = 0;
scl = 1;//将SCL拉高
flash();
while((sda==1)&&(i<255)) i++;//在这里检测SDA的电平变化,若SDA被拉低,即收到应答信号,则退出while循环,若i=255,则按超时处理,同样退出循环
scl = 0;//将SCL拉低,为后续SDA电平变化做准备
flash();
}
unsigned char readx()//从IIC总线读8位
{
unsigned char i,j,k=0;
scl = 0;//确保SCL为低,为后续SDA电平变化做准备
flash();
sda = 1;//从单片机角度释放SDA总线,为IIC设备操作SDA线做准备
for(i=0; i<8; i++)//循环8次,读取8位SDA值
{
flash();
scl = 1;//将SCL拉高,此时IIC设备的SDA电平不再变化,为单片机读SDA做准备
flash();
if(sda==1) j = 1;
else j = 0;//将SDA电平赋值给j
k = (k<<1)|j;//将j左移赋值给k,小端传输
scl = 0;//将SCL拉低,告诉IIC设备现在可以改变SDA的值了,进行下一位的传输
}
flash();
return(k);//将8位数据返回,完成一个数据读取过程
}
AT24C02通信时序图及程序讲解
单片机从AT24C02读数据的时序图及程序
先对照AT24C02读数据时序图把程序看懂,再结合程序把AT24C02读数据时序图看懂。
单片机读操作的大致流程:首先单片机发送起始信号,接着使用写操作进行芯片寻址0xa0,接着定位片内子地址address,因为我们要读数据,但是刚才使用的是写操作进行芯片寻址,我们要把R/W位设置成读操作才可以对address处数据进行读操作,所以接下来需要重新发送一个起始位,接着使用读操作进行芯片寻址0xa1,接着单片机从IIC总线中读取8位数据保存到变量中,最后单片机发送一个结束信号完成整个读过程,其中中间夹杂着应答过程。
unsigned char x24c08_read(unsigned char address) // 从24c02的地址address中读取一个字节数据
{
unsigned char i;
start();//向IIC总线发送一个起始位
writex(0xa0);// 芯片寻址,向IIC总线写入0xa0,即10100000,R/W位是0,代表写操作
clock();//等待AT24C02应答
writex(address);// 片内子地址寻址,将address写入IIC总线,AT24C02下次将操作此地址处的数据
clock();//等待AT24C02应答
start();//向IIC总线发送一个起始位
writex(0xa1); //芯片寻址,向IIC总线写入0xa0,即10100001,R/W位是1,代表读操作
clock();//等待AT24C02应答
i = readx();单片机从IIC总线中读取8位数据放入i中
stop();//向IIC总线发送一个停止位
delay(10);//此处延时是为了系统稳定
return(i);//将读到的数值返回
}
单片机从AT24C02写数据的时序图及程序
先对照AT24C02写数据时序图把程序看懂,再结合程序把AT24C02写数据时序图看懂。
单片机写数据的大致流程:首先发送起始信号,接着使用写操作芯片寻址0xa0,接着进行片内子地址寻址address,接着向address写入info,最后发送停止信号,其中中间夹杂着应答的过程。
void x24c08_write(unsigned char address,unsigned char info)// 向2402的address地址中写入一个字节数据
{
start();//向IIC总线发送一个起始位
writex(0xa0); // 芯片寻址,向IIC总线写入0xa0,即10100000,R/W位是0,代表写操作
clock();//等待AT24C02应答
writex(address); // 片内子地址寻址,将address写入IIC总线,AT24C02下次将操作此地址处的数据
clock();//等待AT24C02应答
writex(info);//将info写入AT24C02的address处
clock();//等待AT24C02应答
stop();//向IIC总线发送一个停止位
delay(10); //此处延时是为了系统稳定
}
附51单片机读取AT24C02的完整代码
实现功能:0-99秒的自动计时器,随机关断电源,在通电以后计时器接着断电时的状态继续计时
#include<reg51.h>
#include<intrins.h>
#define uint unsigned int
#define uchar unsigned char
unsigned char sec; // 定义计数值,每过1s,sec加1
unsigned int tcnt; // 定时中断次数
bit write = 0; // 写2408的标志
sbit sda = P2^0; // I2C接口SDA定义
sbit scl = P2^1; // I2C接口SCL定义
sbit dula = P2^6;
sbit wela = P2^7;
unsigned char j,k;
void delay(unsigned char i) // 延时程序
{
for( j=i ; j>0 ; j-- )
for( k=125; k>0; k-- );
}
uchar code table[] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71}; // 数码管编码
void display(uchar bai_c,uchar sh_c) // 显示程序
{
dula = 0;
P0 = table[bai_c];// 显示百位
dula = 1;
dula = 0;
wela = 0;
P0 = 0x7e;
wela = 1;
wela = 0;
delay(5);
dula = 0;
P0 = table[sh_c];
dula = 1;
dula = 0;
wela = 0;
P0 = 0x7d;
wela = 1;
wela = 0;
delay(5);
}
// 24c02读写驱动程序
void delay1(unsigned char x)
{
unsigned int i ;
for( i=0; i<x; i++);
}
void flash() // 延时子函数
{
; ; ;
}
void x24c08_init()// 2402初始化子程序
{
scl = 1;
flash();
sda = 1;
flash();
}
void start()// 启动I2C总线
{
sda = 1;
flash();
scl = 1;
flash();
sda = 0;
flash();
scl = 0;
flash();
}
void stop()// 停止I2C总线
{
sda = 0;
flash();
scl = 1;
flash();
sda = 1;
flash();
}
void writex(unsigned char j)// 写一个字节
{
unsigned char i,temp;
temp = j;
for(i=0; i<8; i++)
{
temp = temp<<1;
scl = 0;
flash();
sda = CY;
flash();
scl = 1;
flash();
}
scl = 0;
flash();
sda = 1;
flash();
}
unsigned char readx()// 读一个字节
{
unsigned char i,j,k=0;
scl = 0;
flash();
sda = 1;
for(i=0; i<8; i++)
{
flash();
scl = 1;
flash();
if(sda==1) j = 1;
else j = 0;
k = (k<<1)|j;
scl = 0;
}
flash();
return(k);
}
void clock()// I2C总线应答子函数
{
unsigned char i = 0;
scl = 1;
flash();
while((sda==1)&&(i<255)) i++;
scl = 0;
flash();
}
unsigned char x24c08_read(unsigned char address) // 从24c02的地址address中读取一个字节数据
{
unsigned char i;
start();
writex(0xa0);
clock();
writex(address);
clock();
start();
writex(0xa1);
clock();
i = readx();
stop();
delay(10);
return(i);
}
void x24c08_write(unsigned char address,unsigned char info)// 向2402的address地址中写入一个字节数据
{
EA = 0;
start();
writex(0xa0);
clock();
writex(address);
clock();
writex(info);
clock();
stop();
EA = 1;
delay1(50);
}
void t0(void) interrupt 1 using 0 // 定时中断服务函数
{
TH0 = (65536-50000)/256; // 对TH0,TL0赋值
TL0 = (65536-50000)%256;
tcnt++; // 每过250us tcnt加1
if(tcnt == 20)
{
tcnt = 0; // 重新再计
sec++;
write = 1; // 1s写一次24c02
if(sec == 100)
{
sec = 0; // 定时100s,从0开始计时
}
}
}
void main() // 主函数
{
unsigned char i;
TMOD = 0x01; // 定时器工作在方式1
ET0 = 1;
EA = 1; // 开中断
x24c08_init();// 初始化24c08
sec = x24c08_read(2); // 读出保存的数据赋予sec
TH0 = (65536-50000)/256;
TL0 = (65534-50000)%256 ; // 使定时器0.05s中断一次
TR0 = 1;// 启动定时器
while(1)
{
i = 10;
while(i--)
{
display(sec/10,sec%10);
}
if(write = 1) // 判断计时器是否计时1s
{
write = 0; // 清0
x24c08_write(2,sec); // 在24c02的地址中写入数据sec
}
}
}