MCU 51-7 I2C Communication EEPROM

学习通信协议: 买一块 逻辑分析仪,示波器(太贵,用公司或实验室)

目前常用的微机与外设之间进行数据传输的串行总线主要有I2C总线、SPI总线和SCI总线。

其中I2C总线以同步串行2线方式进行通信(一条时钟线,一条数据线)。

SPI总线则以同步串行3线方式进行通信(一条时钟线,一条数据输入线,一条数据输出线)。 SCI总线是以异步方式进行通信(一条数据输入线,一条数据输出线)。

1-wire,即单线总线,又叫单总线。

IIC Communication

I2C总线是PHLIPS公司推出的一种串行总线,它只有两根双向信号线。一根是数据线SDA(serial data I/O),另一根是时钟线SCL(serial clock)。

如下图所示,IIC总线上可以挂多个器件,而每个器件都有唯一的地址,这样可以标识通信目标。数据的通信的方式采用主从方式,主机负责主动联系从机,而从机则被动回应数据。

I2C总线通过上拉电阻接正电源。当总线空闲时,两根线均为高电平。连到总线上的任一器件输出的低电平,都将使总线的信号变低,即各器件的SDA及SCL都是线“与”关系。

开发板上的 I2C总线以及总线上的设备AT24C02

A0A1A2: 000 则从机AT24C02的地址:0x00

AT24C02是一个2K位串行CMOS E2PROM, 内部含有256个字节,CATALYST公司的先进CMOS技术实质上减少了器件的功耗。AT24C02有一个8字节页写缓冲器。该器件通过IIC总线接口进行操作,有一个专门的写保护功能。

EEPROM (Electrically Erasable Programmable read only memory)是指带电可擦可编程只读存储器。是一种掉电后数据不丢失的存储芯片。 EEPROM 可以在电脑上或专用设备上擦除已有信息,重新编程。一般用在即插即用。

 

I2C总线传输协议

数据位的有效性规定

SCL为高电平期间,数据线上的数据必须保持稳定,只有SCL信号为低电平期间,SDA状态才允许变化。

I2C的起始和终止信号

SCL线为高电平期间,SDA线由高电平向低电平的变化表示起始信号;SCL线为高电平期间,SDA线由低电平向高电平的变化表示终止信号。

I2C字节的传送与应答

每一个字节必须保证是8位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)。

I2C字节的传送与应答

每一个字节必须保证是8位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)。

应答位的作用

主机在发送数据时,每次发送一字节数据,都需要读取从机应答位,当从机空闲可以接收该字节数据时,从机会发出应答(一帧数据的第9位为“0”),当从机正忙于其他工作的处理来不及接收主机发送的数据时,从机会发出非应答(一帧数据的第9位为“1”)主机则应发出终止信号以结束数据的继续传送,主机通过从机发出的应答位来判断从机是否成功接收数据。

当主机接收数据时,它收到最后一个数据字节后,必须向从机发出一个结束传送的信号。这个信号是由对从机的“非应答”来实现的。然后,从机释放SDA线,以允许主机产生终止信号。

I2C写数据流程

在起始信号后必须传送一个从机的地址(7位)我们开发板上的AT24C02地址为0xa0,第8位是数据的传送方向位(R/T),用“0”表示主机发送数据(T),“1”表示主机接收数据(R)。

I2C读数据流程

在读数据时也要先发送器件地址,读写方向为写,因为我们下一帧需要发送从AT24C02内那个单元开始读,之后需在发一次器件地址这个时候读写方向就为读了,接着我们就可以从总线上读取数据。

软件模拟I2C通信时序

I2C总线的数据传送有严格的时序要求。I2C总线的起始信号、终止信号、发送“0”及发送“1”的模拟时序 :

# include <reg52.h>
# include <intrins.h>

#define uchar unsigned char
#define uint unsigned  int
#define AT24C02_ADDR  0xa0    //AT24C02地址

/*I2C IO口定义*/
sbit SDA = P2^0;
sbit SCL = P2^1;

/*5us延时*/
void delay_5us()  
{
    _nop_();
}

/*1Ms延时*/
void delay(uint z)
{
    uint x,y;
    for(x = z; x > 0; x--)
        for(y = 114; y > 0 ; y--);
}

/*I2C初始化*/
void I2C_init()    
{
    SDA = 1;
    _nop_();//1.08506us
    SCL = 1;
    _nop_();
}

/*I2C起始信号*/
void I2C_Start()  
{
    SCL = 1;
    _nop_();
    SDA = 1;
    delay_5us();
    SDA = 0;
    delay_5us();
}

/*I2C终止信号*/
void I2C_Stop()
{
    SDA = 0;
    _nop_();
    SCL = 1;
    delay_5us();
    SDA = 1;
    delay_5us();
}

/*主机发送应答*/
void Master_ACK(bit i)        
{
    SCL = 0; // 拉低时钟总线允许SDA数据总线上的数据变化
    _nop_(); // 让总线稳定
    if (i)     //如果i = 1 那么拉低数据总线 表示主机应答
    {
        SDA = 0;
    }
    else     
    {
        SDA = 1;     //发送非应答
    }
    _nop_();//让总线稳定
    SCL = 1;//拉高时钟总线 让从机从SDA线上读走 主机的应答信号
    delay_5us();
    SCL = 0;//拉低时钟总线, 占用总线继续通信
    _nop_();
    SDA = 1;//释放SDA数据总线。
    _nop_();
}

/*检测从机应答*/
bit Test_ACK()
{
    SCL = 1;//高电平期间才可以读取应答信号
    delay_5us();
    if (SDA)//非应答
    {
        SCL = 0;
        _nop_();
        I2C_Stop();//没有应答, 直接结束通信
        return(0);//跳出这个函数
    }
    else //应答
    {
        SCL = 0;
        _nop_();
        return(1);
    }
}

/*发送一个字节*/
void I2C_send_byte(uchar byte)
{
    uchar i;
    for(i = 0 ; i < 8 ; i++)
    {
        SCL = 0;//低电平时发送数据
        _nop_();
        if (byte & 0x80)//读取最高位数据
        {                
            SDA = 1;    //1
            _nop_();                   
        }                
        else
        {
            SDA = 0;//0
            _nop_();
        }
        SCL = 1; //拉高,开始取数
        _nop_();
        byte <<= 1;    // 0101 0100B 发送下一位 
    }
    SCL = 0;//占用总线,继续通信,以接受应答信号
    _nop_();
    SDA = 1;//释放SDA线,接受应答信号
    _nop_();
}

 
/*I2C 读一字节*/
uchar I2C_read_byte()
{
    uchar dat,i;
    SCL = 0;//先拉低,因为不知道当前电平状态
    _nop_();
    SDA = 1;//释放SDA线
    _nop_();
    for(i = 0 ; i < 8 ; i++)
    {
        SCL = 1;//高电平读取
        _nop_();
        if (SDA)                
        {
             dat |= 0x01; //
        }
        else
        {
            dat &=  0xfe;    //1111 1110
        }
        _nop_();
        SCL = 0 ;//拉低,准备读取下一个数据
        _nop_();
        if(i < 7)
        {
            dat = dat << 1;    
        }
    }
    return(dat);
}

/*I2C发送数据*/
bit I2C_TransmitData(uchar ADDR, DAT)
{
    I2C_Start();
    I2C_send_byte(AT24C02_ADDR+0);
    if (!Test_ACK())
    {
        return(0);
    }
    I2C_send_byte(ADDR); 
    if (!Test_ACK())//无应答,返回
    {
        return(0);
    }
    I2C_send_byte(DAT);
    if (!Test_ACK())//无应答,返回
    {
        return(0);
    }
    I2C_Stop();
    return(1);    
}

/*I2C接收数据*/
uchar I2C_ReceiveData(uchar ADDR)
{
    uchar DAT;
    I2C_Start();
    I2C_send_byte(AT24C02_ADDR+0);
    if (!Test_ACK())
    {
        return(0);
    }
    I2C_send_byte(ADDR);
    Master_ACK(0);
    I2C_Start();
    I2C_send_byte(AT24C02_ADDR+1);
    if (!Test_ACK())
    {
        return(0);
    }
    DAT = I2C_read_byte();
    Master_ACK(0);
    I2C_Stop();
    return(DAT);    
}

void main()
{
bit ACK_flag =0; I2C_init();
//I2C初始化
I2C_Start();
I2C_send_byte(AT24C02_ADDR+0);//发送从机地址
if(!Test_ACK())
{
ACK_flag =1;//无应答
}
I2C_send_byte(8);//写地址
if(!Test_ACK())
{
ACK_flag =1;
}
I2C_send_byte(0xfe);//在地址0x08写数据
if(!Test_ACK())
{
ACK_flag =1;
}
I2C_Stop();//发完数据
delay(5);//延时1ms,让从机缓一缓

//开始将刚才写进去的数据读取出来
I2C_Start();
I2C_send_byte(AT24C02_ADDR+1);//发送地址
if(!Test_ACK())
{
ACK_flag =1;
}
P1=I2C_read_byte();//主机读从机该寄存器地址的值,点亮LED1
Master_ACK(0);//主机发送非应答,停止传输数据
I2C_Stop();
if(ACK_flag)
{
P1=0x00;
}
     while(1);     
}
 
# include <reg52.h>
# include <intrins.h>

#define uchar unsigned char
#define uint unsigned  int
#define AT24C02_ADDR  0xa0    //AT24C02地址

/*I2C IO口定义*/
sbit SDA = P2^0;
sbit SCL = P2^1;

/*5us延时*/
void delay_5us()  
{
    _nop_();
}

/*1Ms延时*/
void delay(uint z)
{
    uint x,y;
    for(x = z; x > 0; x--)
        for(y = 114; y > 0 ; y--);
}

/*I2C初始化*/
void I2C_init()    
{
    SDA = 1;
    _nop_();//1.08506us
    SCL = 1;
    _nop_();
}

/*I2C起始信号*/
void I2C_Start()  
{
    SCL = 1;
    _nop_();
    SDA = 1;
    delay_5us();
    SDA = 0;
    delay_5us();
}

/*I2C终止信号*/
void I2C_Stop()
{
    SDA = 0;
    _nop_();
    SCL = 1;
    delay_5us();
    SDA = 1;
    delay_5us();
}

/*主机发送应答*/
void Master_ACK(bit i)        
{
    SCL = 0; // 拉低时钟总线允许SDA数据总线上的数据变化
    _nop_(); // 让总线稳定
    if (i)     //如果i = 1 那么拉低数据总线 表示主机应答
    {
        SDA = 0;
    }
    else     
    {
        SDA = 1;     //发送非应答
    }
    _nop_();//让总线稳定
    SCL = 1;//拉高时钟总线 让从机从SDA线上读走 主机的应答信号
    delay_5us();
    SCL = 0;//拉低时钟总线, 占用总线继续通信
    _nop_();
    SDA = 1;//释放SDA数据总线。
    _nop_();
}

/*检测从机应答*/
bit Test_ACK()
{
    SCL = 1;//高电平期间才可以读取应答信号
    delay_5us();
    if (SDA)//非应答
    {
        SCL = 0;
        _nop_();
        I2C_Stop();//没有应答, 直接结束通信
        return(0);
    }
    else //应答
    {
        SCL = 0;
        _nop_();
        return(1);
    }
}

/*发送一个字节*/
void I2C_send_byte(uchar byte)
{
    uchar i;
    for(i = 0 ; i < 8 ; i++)
    {
        SCL = 0;//低电平时发送数据
        _nop_();
        if (byte & 0x80)//读取最高位数据
        {                
            SDA = 1;    //1
            _nop_();                   
        }                
        else
        {
            SDA = 0;//0
            _nop_();
        }
        SCL = 1; //拉高,开始取数
        _nop_();
        byte <<= 1;    // 0101 0100B 发送下一位 
    }
    SCL = 0;//占用总线,继续通信
    _nop_();
    SDA = 1;//释放SDA线
    _nop_();
}


/*I2C 读一字节*/
uchar I2C_read_byte()
{
    uchar dat,i;
    SCL = 0;//先拉低,因为不知道当前电平状态
    _nop_();
    SDA = 1;//释放SDA线
    _nop_();
    for(i = 0 ; i < 8 ; i++)
    {
        SCL = 1;//高电平读取
        _nop_();
        if (SDA)                
        {
             dat |= 0x01; //
        }
        else
        {
            dat &=  0xfe;    //1111 1110
        }
        _nop_();
        SCL = 0 ;//拉低,准备读取下一个数据
        _nop_();
        if(i < 7)
        {
            dat = dat << 1;    
        }
    }
    return(dat);
}

/*I2C发送数据*/
bit I2C_TransmitData(uchar ADDR, DAT)
{
    I2C_Start();
    I2C_send_byte(AT24C02_ADDR+0);
    if (!Test_ACK())
    {
        return(0);
    }
    I2C_send_byte(ADDR); 
    if (!Test_ACK())//无应答,返回
    {
        return(0);
    }
    I2C_send_byte(DAT);
    if (!Test_ACK())//无应答,返回
    {
        return(0);
    }
    I2C_Stop();
    return(1);    
}

/*I2C接收数据*/
uchar I2C_ReceiveData(uchar ADDR)
{
    uchar DAT;
    I2C_Start();
    I2C_send_byte(AT24C02_ADDR+0);
    if (!Test_ACK())
    {
        return(0);
    }
    I2C_send_byte(ADDR);
    Master_ACK(0);
    I2C_Start();
    I2C_send_byte(AT24C02_ADDR+1);
    if (!Test_ACK())
    {
        return(0);
    }
    DAT = I2C_read_byte();
    Master_ACK(0);
    I2C_Stop();
    return(DAT);    
}

void main()
{
    I2C_init();//I2C初始化
    if(!I2C_TransmitData(255,0xf0));    //往AT24C02第255个单元中写入数据0XF0
    {
        P1 = 0;
    }
    delay(5);//一定延时一定时间后再读取数据
    /**/
    P1 = I2C_ReceiveData(255);//从AT24C02第255个单元中读取数据
    while(1);     
}

往QX-MCS51开发板的AT24C02内任意一个单元写数据,开发板上电后首先读取此单元的原有的数据,然后赋给程序中的计数变量,计数变量再以5秒的速度+1并且写入AT24C02,当计数大于99时清零再写,用数码管显示。

# include <reg52.h>
# include <intrins.h>

#define uchar unsigned char
#define uint unsigned  int
#define AT24C02_ADDR  0xa0

sbit LED1 = P1^0;
sbit SDA = P2^0;
sbit SCL = P2^1;
sbit we  = P2^7;
sbit du  = P2^6;

uchar EEPROM_DATA;    //存放从AT24C02单元读取到的数据
uchar code leddata[]={ 
 
                0x3F,  //"0"
                0x06,  //"1"
                0x5B,  //"2"
                0x4F,  //"3"
                0x66,  //"4"
                0x6D,  //"5"
                0x7D,  //"6"
                0x07,  //"7"
                0x7F,  //"8"
                0x6F,  //"9"
                0x77,  //"A"
                0x7C,  //"B"
                0x39,  //"C"
                0x5E,  //"D"
                0x79,  //"E"
                0x71,  //"F"
                0x76,  //"H"
                0x38,  //"L"
                0x37,  //"n"
                0x3E,  //"u"
                0x73,  //"P"
                0x5C,  //"o"
                0x40,  //"-"
                0x00,  //熄灭
                0x00  //自定义
 
                         };
void delay_5us()  //5us
{
    _nop_();
}

/*1毫秒延时函数*/
void delay(uint z)    
{
    uint x,y;
    for(x = z; x > 0; x--)
        for(y = 114; y > 0 ; y--);
}

void display(uchar i)  //数码管显示函数
{
    uchar shi,ge;
    shi = i / 10;     //求模
    ge  = i % 10;     //求余
    
    P0 = 0xff;         //清除段码
    we = 1;
    P0 = 0xfe;         //点亮第一位数码管
    we = 0;

    du = 1;
    P0 = leddata[shi];
    du = 0;
    delay(1);

    P0 = 0xff;         //清除段码
    we = 1;
    P0 = 0xfd;        //点亮第二位数码管
    we = 0;

    du = 1;
    P0 = leddata[ge];
    du = 0;
    delay(1);    
}

void I2C_init()    //I2C初始化
{
    SDA = 1;
    _nop_();
    SCL = 1;
    _nop_();
}

/*定时器0初始化*/    
void Timer0_init()
{
    
    TMOD |= 0x01;          //定时器T0 16为计数工作模式
    TH0 = 0x4b;
    TL0 = 0xfe;         //T0 定时50ms
    ET0 = 1;             //T0中断
    TR0 = 1;            //启动T0
    EA = 1;                //开总中断
}

void I2C_Start()  //I2C起始信号
{
    SCL = 1;
    _nop_();
    SDA = 1;
    delay_5us();
    SDA = 0;
    delay_5us();
}

void I2C_Stop()
{
    SDA = 0;
    _nop_();
    SCL = 1;
    delay_5us();
    SDA = 1;
    delay_5us();
}

void Master_ACK(bit i)        // 主机发送应答
{
    SCL = 0; // 拉低时钟总线允许SDA数据总线上的数据变化
    _nop_(); // 让总线稳定
    if (i)     //如果i = 1 那么拉低数据总线 表示主机应答
    {
        SDA = 0;
    }
    else     
    {
        SDA = 1;     //发送非应答
    }
    _nop_();//让总线稳定
    SCL = 1;//拉高时钟总线 让从机从SDA线上读走 主机的应答信号
    _nop_();
    SCL = 0;//拉低时钟总线, 占用总线继续通信
    _nop_();
    SDA = 1;//释放SDA数据总线。
    _nop_();
}

bit Test_ACK()     // 检测从机应答
{
    SCL = 1;//时钟总线为高电平期间可以读取从机应答信号
    delay_5us();
    if (SDA)
    {
        SCL = 0;
        I2C_Stop();
        return(0);
    }
    else
    {
        SCL = 0;
        return(1);
    }
}

void I2C_send_byte(uchar byte)     //发送一个字节
{
    uchar i;
    for(i = 0 ; i < 8 ; i++)
    {
        SCL = 0;
        _nop_();
        if (byte & 0x80)    //
        {
            SDA = 1;
            _nop_();
        }
        else
        {
            SDA = 0;
            _nop_();
        }
        SCL = 1;
        _nop_();
        byte <<= 1;
    }
    SCL = 0;
    _nop_();
    SDA = 1;
    _nop_();    
}

uchar I2C_read_byte()  //读一个字节
{
    uchar i, dat;
    SCL = 0 ;
    _nop_();
    SDA = 1;
    _nop_();
    for(i = 0 ; i < 8 ; i++)
    {
        SCL = 1;
        _nop_();
        dat <<= 1;      
        if (SDA)
        {
            dat |= 0x01;  
        }
        _nop_();
        SCL = 0;
        _nop_();
    }
    return(dat);
}

bit I2C_WriteData(uchar ADDR,uchar DAT)     //I2C写数据
{
    I2C_Start();
    I2C_send_byte(AT24C02_ADDR+0);
    if (!Test_ACK())
    {
        return(0);    
    }
    I2C_send_byte(ADDR);
    if (!Test_ACK())
    {
        return(0);    
    }
    I2C_send_byte(DAT);
    if (!Test_ACK())
    {
        return(0);    
    }
    I2C_Stop();
    return(1);    
}

uchar I2C_ReadData(uchar ADDR)     //I2C读数据
{
    uchar dat;
    I2C_Start();
    I2C_send_byte(AT24C02_ADDR+0);
    if (!Test_ACK())
    {
        return(0);    
    }
    I2C_send_byte(ADDR);
    if (!Test_ACK())
    {
        return(0);    
    }
    Master_ACK(0);
    I2C_Start();
    I2C_send_byte(AT24C02_ADDR+1);
    if (!Test_ACK())
    {
        return(0);    
    }
    dat = I2C_read_byte();
    Master_ACK(0);
    I2C_Stop();
    return(dat);
}

void main()
{
    I2C_init();
    Timer0_init();
    EEPROM_DATA = I2C_ReadData(255);    //上电后首先读出24C02第255单元的值
    while(1)
    {

        display(EEPROM_DATA);//数码管显示
            
    }
}

/*定时器0中断服务程序*/
void timer0() interrupt 1    //T0内部查询顺序1
{
    uchar i;      
    TH0 = 0x4b;
    TL0 = 0xfe;         //T0 定时50ms
    i++;
    if (i == 100)      //5秒时间到
    {
        i = 0;          //计数清零
        if (EEPROM_DATA < 99) //判断待写数据值
        {
            EEPROM_DATA++;
        }
        else
        {
            EEPROM_DATA = 0;    
        }
        if(!I2C_WriteData(255,EEPROM_DATA))    //写入待写数据并判断是否成功写入
        {
            LED1 = 0;//如果写失败  让LED1小灯点亮
        }
        else
        {
            LED1 = 1;    
        }        
    }
}      
原文地址:https://www.cnblogs.com/darren-pty/p/13288680.html