• IIC的使用


    借鉴于:蓝桥杯嵌入式快速通关篇,IIC通讯及EEPROM_穿上我的格子衫的博客-CSDN博客

                    IIC原理超详细讲解---值得一看_Z小旋的博客-CSDN博客_iic

    总线空闲:SDA  = 1,SCL = 1;
    启动信号:SCL = 1,SDA 1 -> 0;
    停止信号:SCL = 1,SDA 0 -> 1;

    数据传输:SCL为1时,SDA必须保持稳定,即为0或1(停止时相反,所以在停止前要改变SDA都要先把SCL置为0);
             SCL为0时,才允许改变SDA状态;
             SCL在上升沿时写入数据,在下降沿时读出数据;
             
    应答信号:为0时,是有效应答,为1时,是无效应答;
             传输到最后一个字节后,要发送一个无效应答,再发送停止信号;

    不管什么信号,其线的变化都要严格与对应时序吻合(包括起始,变化过程,结果)

    总线封锁:将SCL锁定为0即可;

    多次数据通信,采用复启动信号,即写一个停止信号,再接上一个启动信号;

    单片机为主机,24C02为从机;
    SCL时钟由主机控制;

    初始化时仅需要设置端口为最基本的推挽模式(因为仅需要gpio输出最基本的高低电平)
    写时应答信号从机给出;
    读时由主机给出;

    24C02器件地址:前四位默认为1010,后四位的前三位为A0,A1,A2,均已接地,即为0,
                  最后一位为读写保护位,写时为0(0xA0),读时为1(0xA1).

    在这里插入图片描述

    Start: IIC开始信号,表示开始传输。
    DEVICE_ADDRESS:: 从设备地址,就是7位从机地址
    R/W: W(write)为写,R(read)为读
    ACK: 应答信号
    WORD_ADDRESS : 从机中对应的寄存器地址 比方说访问 OLED中的 某个寄存器
    DATA发送的数据
    STOP: 停止信号。结束IIC

    首先要知道的是读写分为总体部分与发送接收的单字节部分

    IIC数据传送

    数据传送格式

    SDA线上的数据在SCL时钟“高”期间必须是稳定的,只有当SCL线上的时钟信号为低时,数据线上的“高”或“低”状态才可以改变。输出到SDA线上的每个字节必须是8位,数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)。

    当一个字节按数据位从高位到低位的顺序传输完后,紧接着从设备将拉低SDA线,回传给主设备一个应答位ACK, 此时才认为一个字节真正的被传输完成 ,如果一段时间内没有收到从机的应答信号,则自动认为从机已正确接收到数据。

    在功能函数结束后不要将线的电平恢复(不要越界处理别的函数要做的事)

    img

    //void SDA_Output( uint16_t val )  val为1或0
    //{
    //    if ( val ) {
    //        GPIO_SetBits(I2C_PORT,SDA_Pin);
    //    } else {
    //        GPIO_ResetBits(I2C_PORT,SDA_Pin);
    //    }

    每次设置线上的电平高低后都要进行延时

    IIC写数据(要注意的是读写模式不同,注意切换SDA线的GPIO模式):

    //uint8_t SDA_Input()
    //{
    //    return GPIO_ReadInputDataBit( I2C_PORT, SDA_Pin);
    //}

    img

    从这两个图中我们可以看出在结束时SDA与SCL都为低电平恢复为高电平,这就是结束条件!

    多数从设备的地址为7位或者10位,一般都用七位。
    八位设备地址=7位从机地址+读/写地址,另加一个ack应答地址

    再给地址添加一个方向位位用来表示接下来数据传输的方向,

    • 0表示主设备向从设备(write)写数据,

    • 1表示主设备向从设备(read)读数据

    IIC的每一帧数据由9bit组成,其中数据有两种

    如果是发送数据,则包含 8bit数据+1bit ACK,

    如果是设备地址数据,则8bit包含7bit设备地址 1bit方向
    在这里插入图片描述

    在起始信号后必须传送一个从机的地址(7位) 1~7位为7位接收器件地址,第8位为读写位,用“0”表示主机发送数据(W),“1”表示主机接收数据 (R), 第9位为ACK应答位,紧接着的为第一个数据字节,然后是一位应答位,后面继续第2个数据字节。

    IIC发送一个字节数据(8位)

    //IIC发送一个字节
    //返回从机有无应答
    //1,有应答
    //0,无应答            

    //IIC_SCL=0;
    //在SCL上升沿时准备好数据,进行传送数据时,拉高拉低SDA,因为传输一个字节,一个SCL脉冲里传输一个位。
    //数据传输过程中,数据传输保持稳定(在SCL高电平期间,SDA一直保持稳定,没有跳变)
    //只有当SCL被拉低后,SDA才能被改变
    //总结:在SCL为高电平期间,发送数据,发送8次数据,数据为1,SDA被拉高,数据为0,SDA被拉低。
    //传输期间保持传输稳定,所以数据线仅可以在时钟SCL为低电平时改变。
    void IIC_Send_Byte(u8 txd)
    {                        
        u8 t;   
        SDA_OUT();         //设置为输出模式(PP模式)
        IIC_SCL=0;//拉低时钟开始数据传输
        for(t=0;t<8;t++)
        {              
            //IIC_SDA=txd&0x80;   //获取最高位
            //获取数据的最高位,然后数据左移一位
            //如果某位为1,则SDA为1,否则相反
            if(txd&0x80)  //如果最高位(第八位)为一,则置SDA为高电平
                IIC_SDA=1;
            else
                IIC_SDA=0;
            txd<<=1;       
            delay_us(2);   
            IIC_SCL=1;
            delay_us(2); 
            IIC_SCL=0;    
            delay_us(2);
        }     
    }       
    或者:
            //IIC_SDA=txd&0x80;   //获取最高位
            //获取数据的最高位,然后右移7位,假设为 1000 0000 右移7位为 0000 0001 
            // 假设为 0000 0000 右移7位为 0000 0000 
            //如果某位为1,则SDA为1,否则相反
            IIC_SDA=((txd&0x80)>>7);
            txd<<=1;

    或者这么写:

    //void I2CSendByte(unsigned char cSendByte)
    //{
    //    unsigned char  i = 8;
    //    while (i--)
    //    {
    //        SCL_Output(0);delay1(500); 
    //        SDA_Output(cSendByte & 0x80); delay1(500);
    //        cSendByte += cSendByte;        //二进制自加相当于左移一位
    //        delay1(500); 
    //        SCL_Output(1);delay1(500); 
    //    }
    //    SCL_Output(0);delay1(500); 
    //}


    IIC读取一个字节数据:


    //读1个字节,ack=1时,发送ACK,ack=0,发送nACK   
    u8 IIC_Read_Byte(unsigned char ack)
    {
        unsigned char i,receive=0;
        SDA_IN();        //SDA设置为输入   (其实就是IPD模式
        for(i=0;i<8;i++ )
        {
            IIC_SCL=0; 
            delay_us(2);
            IIC_SCL=1;
            receive<<=1;                        //左移一位
            if(READ_SDA)receive++;   //使最低位为一
            delay_us(1); 
        }                     
        if (!ack)
            IIC_NAck();        //发送nACK
        else
            IIC_Ack();         //发送ACK   
        return receive;
    }

    1. 主机首先产生START信号
    2. 然后紧跟着发送一个从机地址,这个地址共有7位,紧接着的第8位是数据方 向位(R/W),0表示主机发送数据(写),1表示主机接收数据(读)
    3. 主机发送地址时,总线上的每个从机都将这7位地址码与自己的地址进行比较,若相同,则认为自己正在被主机寻址,根据R/T位将自己确定为发送器和接收器
    4. 这时候主机等待从机的应答信号(A)
    5. 当主机收到应答信号时,发送要访问从机的那个地址, 继续等待从机的应答信号
    6. 当主机收到应答信号时,发送N个字节的数据,继续等待从机的N次应答信号,
    7. 主机产生停止信号,结束传送过程。

    具体表现为:

    void i2c_write(u8 add ,u8 reg ,u8 data)

    { I2CStart();

    I2CSendByte(add);

    I2CWaitAck();

    I2CSendByte(reg);

    I2CWaitAck();

    I2CSendByte(data);

    I2CWaitAck();

    I2CStop();

    }

    函数void i2c_write(u8 add ,u8 reg ,u8 data);
    是iic通讯的写函数,第一个 add是设备的地址,如0xa0是EEPROM的地址,0x38 是扩展板上的三轴加速度计的通讯地址。
    第二个形参是寄存器的地址,需要注意的是AT24C02的地址是从0x00~0xff,如果超出该地址值会出现错误。地址和写入或读取的数据的类型都是无符号字符型,其他类型的数据有可能出现错误。就是说,但个地址存入读出的值不会超过255,即存取的的数据范围是0 ~255

    主机要从从机读数据时

    1. 主机首先产生START信号
    2. 然后紧跟着发送一个从机地址,注意此时该地址的第8位为0,表明是向从机写命令,
    3. 这时候主机等待从机的应答信号(ACK)
    4. 当主机收到应答信号时,发送要访问的地址,继续等待从机的应答信号,
    5. 当主机收到应答信号后,主机要改变通信模式(主机将由发送变为接收,从机将由接收变为发送)所以主机重新发送一个开始start信号,然后紧跟着发送一个从机地址,注意此时该地址的第8位为1,表明将主机设 置成接收模式开始读取数据,
    6. 这时候主机等待从机的应答信号,当主机收到应答信号时,就可以接收1个字节的数据,当接收完成后,主机发送非应答信号,表示不在接收数据
    7. 主机进而产生停止信号,结束传送过程。

    u8 i2c_read(u8 add ,u8 reg){
        u8 data ;
        I2CStart();
        I2CSendByte(add);
        I2CWaitAck();
        
        I2CSendByte(reg);
        I2CWaitAck();
        I2CStart();
        I2CSendByte(add+1);
        I2CWaitAck();
        data =I2CReceiveByte();
        I2CWaitAck();

        I2CStop();
        return data;
        
        
    }
        

    以AT24C02为例子(与连接芯片的差别为寄存器地址改为了具体储存地址

    24C02是一个2K Bit的串行EEPROM存储器(掉电不丢失),内部含有256个字节。在24C02里面有一个8字节的页写缓冲器。
    在这里插入图片描述
    A0,A1,A2:硬件地址引脚
    WP:写保护引脚,接高电平只读,接地允许读和写
    SCL和SDA:IIC总线

    以看出对于不同大小的24Cxx,具有不同的从器件地址。由于24C02为2k容量,也就是说只需要参考图中第一行的内容:
    在这里插入图片描述
    芯片的寻址:
    AT24C设备地址为如下,前四位固定为1010,A2~A0为由管脚电平。AT24CXX EEPROM Board模块中默认为接地。A2~A0为000,最后一位表示读写操作。所以AT24Cxx的读地址为0xA1,写地址为0xA0。

    • 一个EEPROM空间(地址)占一个字节,只能存储0-255,就是一个byte值的范围。
    • EEPROM可操作的地址为0~4095。

    常见的EEPROM如24C02的大小为2Kbit(有的也称2KB)

    2KB中,B表示单位bit,K表示1024。

    单片机编程中常用的数据类型为unsigned char(u8)的变量的大小为1字节。

    1字节=8bit

    因此

    2KB = 2*1024/8 = 256字节

    也就是说,单片机编程中,一片2KB的EEPROM最多存储256(2的8次方,也就是八位)个u8(unsigned char)类型的数据

    AT24C设备地址,前四位固定为1010,而储存不受此限制,可以从0x00开始储存

    也就是说如果是
    写24C02的时候从器件地址为10100000(0xA0);
    读24C02的时候,从器件地址为10100001(0xA1)。

    片内地址寻址:

    芯片寻址可对内部256B中的任一个进行读/写操作,其寻址范围为00~FF,共256个寻址单位。

    对应的修改 A2A1A0 三位数据即可
    在这里插入图片描述

    向AT24C02中写数据
    在这里插入图片描述
    操作时序:

    1. MCU先发送一个开始信号(START)启动总线
    2. 接着跟上首字节,发送器件写操作地址(DEVICE ADDRESS)+写数据(0xA0)
    3. 等待应答信号(ACK)
    4. 发送数据的存储地址。24C02一共有256个字节的存储空间,地址从0x00~0xFF,想把数据存储>在哪个位置,此刻写的就是哪个地址。
    5. 发送要存储的数据第一字节、第二字节、…注意在写数据的过程中,E2PROM每个字节都会>回应一个“应答位0”,老告诉我们写E2PROM数据成功,如果没有回应答位,说明写入不成功。
    6. 发送结束信号(STOP)停止总线

    注意:
    在写数据的过程中,每成功写入一个字节,E2PROM存储空间的地址就会自动加1,当加到0xFF后,再写一个字节,地址就会溢出又变成0x00。

    写数据的时候需要注意,E2PROM是先写到缓冲区,然后再“搬运到”到掉电非易失区。所以这个过程需要一定的时间,AT24C02这个过程是不超过5ms!
    所以,当我们在写多个字节时,写入一个字节之后,再写入下一个字节之前,必须延时5ms才可以



     

    从AT24C02中读数据

    读当前地址的数据
    在这里插入图片描述
    2、读随机地址的数据
    在这里插入图片描述

    1. MCU先发送一个开始信号(START)启动总线
    2. 接着跟上首字节,发送器件写操作地址(DEVICE ADDRESS)+写数据(0xA0)
      注意:这里写操作是为了要把所要读的数据的存储地址先写进去,告诉E2PROM要读取哪个地址的数据。
    3. 发送要读取内存的地址(WORD ADDRESS),通知E2PROM读取要哪个地址的信息。
    4. 重新发送开始信号(START)
    5. 发送设备读操作地址(DEVICE ADDRESS)对E2PROM进行读操作 (0xA1)这里发送读操作地址是为了让从设备知道主设备要读了,为什么不在第二部就发的原因是要先联系上从设备。
    6. E2PROM会自动向主机发送数据,主机读取从器件发回的数据,在读一个字节后,MCU会回应一个应答信号(ACK)后,E2PROM会继续传输下一个地址的数据,MCU不断回应应答信号可以不断读取内存的数据
    7. 如果不想读了,告诉E2PROM不想要数据了,就发送一个“非应答位NAK(1)”。发送结束信号(STOP)停止总线

    应答信号(ack

    每当主机向从机发送完一个字节的数据,主机总是需要等待从机给出一个应答信号,以确认从机是否成功接收到了数据,

    应答信号:主机SCL拉高,读取从机SDA的电平,为低电平表示产生应答

    • 应答信号为低电平时,规定为有效应答位(ACK,简称应答位),表示接收器已经成功地接收了该字节;
    • 应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。

    在这里插入图片描述

    **每发送一个字节(8个bit)**在一个字节传输的8个时钟后的第九个时钟期间,接收器接收数据后必须回一个ACK应答信号给发送器,这样才能进行数据传输。

    å¨è¿éæå¥å¾çæè¿°

    应答出现在每一次主机完成8个数据位传输后紧跟着的时钟周期,低电平0表示应答,1表示非应答,

    //主机产生(发送ack)应答信号ACK
    //1.先拉低SCL,再拉低SDA
    //2.拉高SCL
    //3.拉低SCL
    ## 标题
    void I2C_Ack(void)
    {
       IIC_SCL=0;   //先拉低SCL,使得SDA数据可以发生改变
       IIC_SDA=0;   
       delay_us(2);
       IIC_SCL=1;
       delay_us(5);
       IIC_SCL=0;
    }

    标准写法:

    unsigned char I2CWaitAck(void)
    {
        unsigned short cErrTime = 5;
        SDA_Input_Mode();                 //设为IPU输入模式
        delay1(500);
        SCL_Output(1);delay1(500);
        while(SDA_Input())            //如果一直读端口为1(GPIO_ReadInputDataBit( I2C_PORT, SDA_Pin)),表示没有接受到应答,返回失败
        {
            cErrTime--;
            delay1(500);
            if (0 == cErrTime)
            {
                SDA_Output_Mode();        //实际上是不必要的
                I2CStop();
                return FAILURE;
            }
        }
        SDA_Output_Mode();                //实际上是不必要的
        SCL_Output(0);delay1(500);         
        return SUCCESS;        
    }


    //主机不产生应答信号,也就是NACK
    //1.先拉低SCL,再拉高SDA
    //2.拉高SCL
    //3.拉低SCL
    void I2C_NAck(void)
    {
       IIC_SCL=0;   //先拉低SCL,使得SDA数据可以发生改变
       IIC_SDA=1;   //拉高SDA,不产生应答信号
       delay_us(2);
       IIC_SCL=1;
       delay_us(5);
       IIC_SCL=0;
    }

    3、连续读数据
    在这里插入图片描述
    E2PROM支持连续写操作,操作和单个字节类似,先发送设备写操作地址(DEVICE ADDRESS),然后发送内存起始地址(WORD ADDRESS),MCU会回应一个应答信号(ACK)后,E2PROM会继续传输下一个地址的数据,MCU不断回应应答信号可以不断读取内存的数据。E2PROM的地址指针会自动递增,数据会依次保存在内存中。不应答发送结束信号后终止传输。

    软件IIC和硬件IIC

    IIC分为软件IIC和硬件IIC

    软件IIC:软件IIC通信指的是用单片机的两个I/O端口模拟出来的IIC,用软件控制管脚状态以模拟I2C通信波形,软件模拟寄存器的工作方式。

    硬件IIC:一块硬件电路,硬件I2C对应芯片上的I2C外设,有相应I2C驱动电路,其所使用的I2C管脚也是专用的,硬件(固件)I2C是直接调用内部寄存器进行配置。

    硬件I2C的效率要远高于软件的,而软件I2C由于不受管脚限制,接口比较灵活。

    代码需要注意的点(出现问题也可依次检查)

    读取的操作结束后,都建议延迟几毫秒,实测如果不延迟数据极有可能会出错,延迟操作可以加到函数末尾,也可在函数调用后。

    调用函数之前,要先使用i2c.c文件中已有的函数i2c_init()来初始化iic要使用到的时钟,引脚等。

    读写函数并不能直接存取超过范围的数,如果存入的数不在0到255之间,肯定会出错,地址也一样,要注意范围。

    在读取iic的值时,需要再进行一次I2CStart(),不然读取的值也可能会出错!
    在这里插入图片描述

  • 相关阅读:
    滑动窗口内最大值和最小值的更新结构
    用灵活的依赖排除策略来规避不必要的依赖关系
    C++中extern的使用
    Linux--VMware的安装和Centos
    02_udp编程
    Tomcat调优
    upload-labs1-21关文件上传通关手册
    Visual Studio 2022下载、安装与运行使用方法
    AutoJs7打包薅羊毛时间版
    什么是闭包?闭包详解
  • 原文地址:https://blog.csdn.net/qq_54215896/article/details/127986444