STC89C52不支持I2C总线,因此这里是使用软件模拟IIC总线

I2C串行总线的组成和工作原理

I2C的组成

image-20220618212427919

I2C总线可以挂载多个器件,每个器件有唯一的地址。

采用主从方式,主机(单片机)负责主动联系从机,从机(其他外设)被动发送数据

通常是单主机系统。如果存在不止一个单片机芯片,就是多主机系统,这种情况下可能会发生冲突,需要I2C仲裁

image-20220618212753801

I2C总线通过上拉电阻连接电源。

当总线空闲时,两根线均为高电平。连到总线上的任一器件输出低电平都会使总线信号为低电平,即各器件的SDA(数据线)和SCL(时钟线)都是线与关系。

SCL始终由单片机控制,SDA则不是

I2C的工作原理

数据位的有效性规定

SCL高电平期间,SDA信号不能发生改变;SCL低电平期间,SDA信号可以变化

I2C的起始和终止信号

SCL高电平期间,SDA由高变低表示起始信号

SCL高电平期间,SDA由低变高表示终止信号

起始和终止信号均有主机发出。起始信号产生后,总线处于占用状态,直至终止信号出现

I2C字节的传送与应答

每个字节必须是8位。先传送最高位,每个被传送的字节后面必须跟随一位应答位(即一帧有9位)

主机在发送数据时,每发送一字节数据,都要读取从机应答位。当从机空闲时,从机会发出应答位(第9位为0),否则发出非应答(应答位为1),此时主机应发出终止信号、停止发送数据。

主机在接收数据时,收到最后一个字节后,必须向从机发出一个非应答信号。之后从机释放SDA,允许主机产生终止信号。

数据帧格式

I2C总线传送的信号既有数据信号,又有地址信号。

在起始信号后必须传送一个从机的地址(7位),第8位是数据的传送方向(R/T),0表示主机发送数据(T),1表示主机接收数据(R)。

每次数据传送均以主机产生的终止信号结束。若主机希望继续进行数据传送,可不产生终止信号,再次发出起始信号和地址信号。

主机发送数据

image-20220618221307782

  • 阴影部分是主机发送,非阴影部分是从机发送
  • S表示起始信号
  • A表示应答
  • $\overline{A}$表示非应答
  • P表示终止信号

主机接收数据

image-20220618224411100

主机发送转接收

image-20220618225028462

总线寻址

image-20220618225256558

D1~D7为从机地址,D0表示传送方向,0表示主机发送数据,1表示主机接收数据

主机发送地址时,每个从机都会将地址码和自己的地址进行比较,若相同则认为自己被选中,并根据最后一位确定自己是否为发送方或接收方。

从机的地址由固定部分和可编程部分组成。固定部分由芯片厂家给出,见于datasheet。可编程部分的位数决定了该I2C总线最多可挂载的相同从机数量,如3位可编程部分则最多挂载$2^3=8$个相同从机

软件模拟I2C通信时序

I2C总线的数据传输有严格的时序要求。

image-20220618230553946

初始信号程序:

void I2cStart(){
    SCL = 1;
    SDA = 1;
    delay5us();
    SDA = 0;
    delay5us();
}

终止信号程序:

void I2cStop(){
    SCL = 0;
    SDA = 0;
    SCL = 1;
    delay5us();
    SDA = 1;
    delay5us();
}

主机读从机应答程序:

bit ReadACK()
{
    SCL = 0;
    SCL = 1;
    delay5us();
    if(SDA)//NOACK
    {
        SCL = 0;
        return(1);
    }else{//ACK
        SCL = 0;
        return(0);
    }
}

主机发送应答:

void SendACK(bit i){
    SCL = 0;
    if(i)
        SDA = 1;
    else
        SDA = 0;
    SCL = 1;
    delay5us();
    SCL = 0;
    SDA = 1;//释放数据线
}

与AT24C02进行通信

下面通过软件模拟IIC、与AT24C02 EEPROM模块进行通信。

以下是原理图

#include "Header/IIC.h"

#define At24c02ADDR 0xA0
#define I2cRead 1
#define I2cWrite 0

sbit dula = P2^6;//段选控制
sbit wela = P2^7;//位选控制
sbit SCL = P2^1;//IIC时钟线
sbit SDA = P2^0;//IIC数据线
unsigned char num = 0;//动态显示的数字,限定为3位
bit At24c02AckFlag;//At24c02应答标志位

//段选编码表
unsigned char code table_dula[]={
0x3f,0x06,0x5b,0x4f,
0x66,0x6d,0x7d,0x07,
0x7f,0x6f,0x77,0x7c,
0x39,0x5e,0x79,0x71
};

//毫秒级延时函数定义
void delay(unsigned int z)
{
    unsigned int x,y;
    for(x = z; x > 0; x--)
            for(y = 114; y > 0 ; y--);                 
}

//位选编码表
unsigned char code table_wela[]={
    0xfe,0xfd,0xfb
};

//动态显示3位数字
void display(unsigned char num){
    static unsigned char i;//位选控制位
    if(i > 2)//重置位选控制位
        i = 0;
    //位选
    wela = 1;
    P0 = table_wela[i];
    wela = 0;
    //段选
    switch(i){
        case 0:dula = 1; P0 = table_dula[num/100]; dula = 0; break;//百位
        case 1:dula = 1; P0 = table_dula[(num % 100)/10]; dula = 0; break;//十位
        case 2:dula = 1; P0 = table_dula[num%10]; dula = 0; break;//个位
    }
    i++;
}

//初始化定时器T0,以进行动态显示
void timing0_Init(){

    EA = 1;//开总中断
    ET0 = 1;//允许计时器中断
    TR0 = 1;//启动计时
    TMOD = 0x01;//设置定时器0为工作方式1:16位
    //设置计数初值
    TH0 = 0xED;
    TL0 = 0xff;//定时5ms
}

//T0中断函数
void timing0() interrupt 1
{
    //重置计数初值
    TH0 = 0xED;
    TL0 = 0xff;
    display(num);
}

//延时5us
void delay5us()
{
    _nop_();//执行一次_nop_可延时1us,进入该函数需要3点几机器周期,因此整体延时5us左右
}

//IIC起始信号
void I2cStart()
{
    SCL = 1;
    SDA = 1;
    delay5us();
    SDA = 0;
    delay5us();
}

//IIC终止信号
void I2cStop()
{
    SCL = 0;
    SDA = 0;
    SCL = 1;
    delay5us();
    SDA = 1;
    delay5us();
}

//主机读从机应答
bit ReadACK()
{
    SCL = 0;
    SCL = 1;
    delay5us();
    if(SDA)//NOACK
    {
        SCL = 0;
        return(1);
    }else{//ACK
        SCL = 0;
        return(0);
    }
}

//主机发送应答
void SendACK(bit i){
    SCL = 0;
    if(i)
        SDA = 1;
    else
        SDA = 0;
    SCL = 1;
    delay5us();
    SCL = 0;
    SDA = 1;//释放数据线
}

//发送一个字节
void I2cSendByte(unsigned char dat)
{
    unsigned char i;
    for(i = 0;i < 8;i++)
    {
        SCL = 0;
        if(dat & 0x80)//判断最高位是否为1
            SDA = 1;
        else
            SDA = 0;
        SCL = 1;
        dat <<= 1;//左移一位
    }
    SCL = 0;
    SDA = 1;//释放数据总线
}


//接收一个字节
unsigned char I2cReadByte()
{
    unsigned char i,dat;
    for(i = 0;i < 8;i++)
    {
        dat <<= 1;//左移一位
        SCL = 0;//从机将一位数据发到SDA上
        SCL = 1;//保持
        if(SDA)
            dat |= 0x01;
    }
    return dat;
}

//向AT24C02写入数据
void At24c02Write(unsigned char addr,unsigned char dat)
{
    I2cStart();//起始信号

    I2cSendByte(At24c02ADDR + I2cWrite);//找到At24c02
    if(ReadACK())//At24c02是否应答
        At24c02AckFlag = 1;//应答
    else
        At24c02AckFlag = 0;//无应答

    I2cSendByte(addr);//找到存储单元
    if(ReadACK())//At24c02是否应答
        At24c02AckFlag = 1;//应答
    else
        At24c02AckFlag = 0;//无应答

    I2cSendByte(dat);//发送数据
    if(ReadACK())//At24c02是否应答
        At24c02AckFlag = 1;//应答
    else
        At24c02AckFlag = 0;//无应答

    I2cStop();
}

//读AT24C02中的数据
unsigned char At24c02Read(unsigned char addr)
{
    unsigned char dat;//读出的数据
    I2cStart();//起始信号

    I2cSendByte(At24c02ADDR + I2cWrite);//找到At24c02
    if(ReadACK())//At24c02是否应答
        At24c02AckFlag = 1;//应答
    else
        At24c02AckFlag = 0;//无应答

    I2cSendByte(addr);//找到存储单元
    ReadACK();
    I2cStart();
    I2cSendByte(At24c02ADDR + I2cRead);//改变方向
    if(ReadACK())//At24c02是否应答
        At24c02AckFlag = 1;//应答
    else
        At24c02AckFlag = 0;//无应答
    dat = I2cReadByte();
    SendACK(1);//发送非应答
    I2cStop();
    return dat;
}

//测试
void test()
{
    timing0_Init();
    EA = 0;//屏蔽中断
    At24c02Write(2,9);//向第二个存储单元写入9
    delay(1);//延时1ms,等待At24c02处理完成
    num = At24c02Read(2);//读出数据并在数码管上显示
    EA = 1;
    while(1);
}
最后修改:2022 年 08 月 10 日
如果觉得我的文章对你有用,请随意赞赏