打印
[应用相关]

STM32_IIC

[复制链接]
2756|1
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
tpgf|  楼主 | 2024-6-7 08:37 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
1、IIC简介
        I2C,即Inter IC Bus。是由Philips公司开发的一种串行通用数据总线,主要用于近距离、低速的芯片之间的通信;有两根通信线:SCL(Serial Clock)用于通信双方时钟的同步、SDA(Serial Data)用于收发数据;具有同步,半双工,带数据应答,支持总线挂载多设备(一主多从、多主多从)等特点。

        IIC总线是一种多主机总线,连接在IIC总线上的器件分为主机和从机,主机有权发起和结束一次通信,而从机只能被主机呼叫;当总线上有多个主机同时启用总线时,IIC也具备冲突检测和仲裁的功能来防止错误产生;每个连接到IIC总线上的器件都有一个唯一的地址(一般是7bit),且每个器件都可以作为主机也可以作为从机(同一时刻只能有一个主机),总线上的器件增加和删除不影响其它器件正常工作;IIC总线在通信时,总线上发送数据的器件为发送器,接收数据的器件为接收器。



        所有I2C设备的SCL连在一起,SDA连在一起;设备的SCL和SDA均要配置成开漏输出模式;SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右。

        在STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担;支持多主机模型;支持7位/10位地址模式;支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz);支持DMA;兼容SMBus协议。

        STM32F103C8T6 硬件I2C资源:I2C1、I2C2

        对于串口这样的异步时序来说,软件实现非常麻烦,硬件实现非常简单,所以串口的实现基本全都倒向硬件了;而对IIC这样的同步时序来说,软件实现反而简单灵活,硬件实现,相比之下,不能完全让人省心,所以IIC的实现,软件模拟的情况还是比较多的。

        考虑到硬件IIC也有很多独有的优势,比如执行效率比较高,可以节省软件资源,功能比较强大,可以实现完整的多主机通信模型,时序波形规整,通信速率快等,所以硬件IIC也是有相应的应用场景的。

2、IIC结构图
        以下结构图基于STM32F103xxx



         这里的数据收发的核心部分是数据寄存器和数据移位寄存器,当我们需要发送数据时,可以把一个字节的数据写到数据寄存器DR,当移位寄存器没有数据移位时,这个数据寄存器的值就是进一步转到移位寄存器这里,在移位的过程中,我们就可以直接把下一个数据放在数据寄存器里等着了,一旦数据发送完成,下一个数据就可以无缝连接,继续发送。当数据由数据寄存器转到移位寄存器时,就会置状态寄存器的值TXE位为1,表示发送寄存器为空。

        在接收时,也是这一路,输入的数据,一位一位的从引脚移入到移位寄存器里,当一个字节的数据收齐之后,数据就整体从移位寄存器转到数据寄存器,同时置标志位RXNE,表示接收寄存器非空,这时就可以把数据从数据寄存器读出来了。

        基本框图



3、IIC时序
3.1 IIC时序基本单元
        起始条件:SCL高电平期间,SDA从高电平切换到低电平

        终止条件:SCL高电平期间,SDA从低电平切换到高电平



        发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节 。



        接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)



        发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答

        接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)



3.2 IIC时序
3.2.1 指定地址写
        对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)

        这里这个指定设备,通过从机地址来确定,这里这个指定地址就是某个设备内部的寄存器地址。



3.2.2  当前地址读
        对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)

        在这个时序图中,主机发送第一个字节,指定读之后,第2个字节读写的方向就要反过来了,控制权交给从机,由从机来发送数据,这时主机无法去指定是由从机哪个寄存器发出的数据,那么这里这个当前地址指针指示的地址就很重要了。在从机中,所有的寄存器都被分配到了一个线性区域中,并且会有一个单独的指针变量指示着其中一个寄存器,这个指针上电一般默认0地址,并且每写入和读出一个字节后,这个指针就会自动自增一次,移动到下一个位置,那么在调用当前地址读的时序时,主机没有指定要读哪个地址,从机就会返回当前指针指向的寄存器的值。



3.2.3 指定地址读
        对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)

        这里先指定从机地址是1101000,读写标志位是0,代表我要进行写的操作。经常从机应答之后,再发送一个字节,第二个字节用来指定地址,这个数据就写入到了从机的地址指针中了,也就是从机接受到这个数据之后,他的寄存器指针就指向了0x19这个位置,之后再重复一个起始条件,因为指定读写标志位只能是跟着起始条件的第一个字节,如果想要切换读写方向,只能再来个起始条件,然后起始条件后,重新寻址并且指定读写标志位,此时读写标志位是1,代表我要开始读了,这时候接收到的就是0x19下的数据。

        写入的地址会存在地址指针里面,所以这个地址并不会因为时序的停止而消失。



4、操作流程
4.1 主机发送



        指定地址写:首先初始化之后,总线默认空闲状态,STM32默认是从模式,为了产生一个起始条件,STM32需要写入控制寄存器(这个要看一下手册的寄存器描述),之后STM32由从模式转为主模式,控制完硬件电路之后,要检查标志位,来看看硬件有没有达到我们想要的状态,在这里起始条件之后会发生EV5事件,这个EV5事件就可以把它当成标志位(这里使用EV几事件,而不写具体标志位,是因为有的事件会产生多个标志位,这里的EV几事件就是包含了多个标志位的大标志位,在库函数中也会有对应),检查到起始条件已发送的情况下就可以发送一个字节的从机地址了,从机地址需要写到数据寄存器DR中,写入DR后,硬件电路会把这个字节发送到移位寄存器中,再把这一个字节发送到IIC总线上,之后硬件会自动接收应答并判断,如果没有应答,硬件会置应答失败的标志位,然后这个标志位可以申请中断来提醒我们,在寻址完成之后,会发生EV6事件(代表主模式下地址发送结束),EV6事件结束之后是EV8_1事件(TXE标志位=1,移位寄存器空,数据寄存器空),这时需要我们写入数据寄存器DR进行数据发送了,一旦写入数据寄存器之后,因为移位寄存器也是空,所以DR会立刻转到移位寄存器进行发送,这时就是EV8事件(移位寄存器非空,数据寄存器空),这时就是移位寄存器正在发数据的状态,所以流程这里,数据1的时序就发生了,之后应该是写入了下一个数据,数据2此刻应该被写入到数据寄存器里等着了,然后接收应答位之后,数据2就转入移位寄存器进行发送,此时的状态是移位寄存器非空,数据寄存器空,所以这是EV8事件又发生了,之后重复该过程,一旦我们检测要EEV8事件,就可以写入下一个数据了,最后当我们想要发送的数据写完之后,这时就没有新的数据写入数据寄存器了,当移位寄存器当前的数据移位完成时,此时就是移位寄存器空,数据寄存器也空的状态,这个事件就是这里的EV8_2事件,当检测到EV8_2时,就可以产生终止条件了,产生终止条件在控制寄存器中有相应的位可以控制,到这里,一个完整的时序就发送完成了。

4.2 主机接收



        从七位主接收来看,起始,从机地址+读,接收应答,然后就是,接收数据,发送应答,最后一个数据给非应答,之后终止。从这个时序看,这是当前地址读的一个时序。

5、示例代码
5.1 软件读写IIC
#include "stm32f10x.h"                  // Device header
#include "Delay.h"     

//#define SCL_PORT                GPIOB
//#define SCL_PIN                GPIO_Pin_10

//对端口和引脚的封装,方便后续修改和移植
void MyI2C_W_SCL(uint8_t BitValue)
{
        GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
        //I2C时序可以稍微慢一点,但是如果快了,那就要看一下手册对时序时间的要求
        Delay_us(10);
       
}

void MyI2C_W_SDA(uint8_t BitValue)
{
        GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
        //I2C时序可以稍微慢一点,但是如果快了,那就要看一下手册对时序时间的要求
        Delay_us(10);
       
}

uint8_t MyI2C_R_SDA(void)
{
        uint8_t BitValue;
        BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
        Delay_us(10);
        return BitValue;
}

void MyI2C_Init(void)
{
        //软件读取I2C只要gpio的库函数就可以了,I2C的库函数就不用看了
       
        //任务一,将SCL和SDA都初始化为开漏输出模式
        //任务二,将SCL和SDA都置高电平
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
       
        //配置端口
        //先定义一个结构体变量
        GPIO_InitTypeDef GPIO_InitStructure;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;        //开漏输出,开漏输出模式仍然可以输入
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;       
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;        //速度50MHz
        GPIO_Init(GPIOB, &GPIO_InitStructure);
       
        //释放总线,SCL和SDA处于高电平,此时I2C总线处于空闲状态
        GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);
       
       
}

void MyI2C_Start(void)
{
        //根据I2C时序要求,这里兼顾了开始时序和Sr期间时序
        MyI2C_W_SCL(1);
        MyI2C_W_SDA(1);
       
        MyI2C_W_SDA(0);
        MyI2C_W_SCL(0);
}       

void MyI2C_Stop(void)
{
        MyI2C_W_SDA(0);
        MyI2C_W_SCL(1);
        MyI2C_W_SDA(1);
}

void MyI2C_SendByte(uint8_t Byte)
{
//        MyI2C_W_SDA(Byte & 0x80);        //取出数据的最高位,SDA是高位先行
//        //释放SCL,读走放在SDA的数据
//        MyI2C_W_SCL(1);
//        //再拉低SCL,就可以放下一位数据了
//        MyI2C_W_SCL(0);
       
        uint8_t i;
        for (i = 0; i < 8; i ++)
        {
                MyI2C_W_SDA(Byte & (0x80 >> i));        //0x80 >> i,表示0x80右移i位
                MyI2C_W_SCL(1);
                MyI2C_W_SCL(0);
        }
}

uint8_t MyI2C_ReceiveByte(void)
{
        uint8_t i, Byte = 0x00;
       
        //主机释放SDA,从机把数据放到SDA
        MyI2C_W_SDA(1);

        for (i = 0; i < 8; i ++)
        {
                //主机释放SCL,SCL高电平,主机就能读取数据了
                MyI2C_W_SCL(1);
                if (MyI2C_R_SDA() == 1)
                {
                        Byte |= (0x80 >> i);
                }
                //再次拉低SCL,这时从机就会把数据放在SDA上
                MyI2C_W_SCL(0);
        }
        return Byte;
       
}

void MyI2C_SendAck(uint8_t AckBit)
{
        //函数进来时,SCL低电平,主机把AckBit放到SDA上
        MyI2C_W_SDA(AckBit);       
        //SCL高电平,从机读取应答
        MyI2C_W_SCL(1);
        //SCL低电平,进入下一个时序单元
        MyI2C_W_SCL(0);
}

uint8_t MyI2C_ReceiveAck(void)
{
        uint8_t AckBit;
        //函数进来时,SCl低电平
        //主机释放SDA,防止从机干扰,同时从机应答位放到SDA
        MyI2C_W_SDA(1);
        //SCL高电平,主机读取应答位
        MyI2C_W_SCL(1);
        AckBit = MyI2C_R_SDA();
        //SCL低电平,进入下一个时序单元
        MyI2C_W_SCL(0);
       
        return AckBit;
       
}

5.2 硬件读写IIC
//MyI2C_Init();
        //用硬件来配置I2C外设,对I2C2外设进行初始化,来替换之前用软件实现的MyI2C_Init();       
        //第一步,开启I2C外设和对应GPIO口的时钟
        RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
       
        //第二步,把I2C外设对应的GPIO口初始化为复用开漏模式
        GPIO_InitTypeDef GPIO_InitStructure;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;        //复用开漏输出,开漏是I2C硬件要求,复用就是GPIO的控制权要交给硬件外设
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;       
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;        //速度50MHz
        GPIO_Init(GPIOB, &GPIO_InitStructure);
       
       
        //第三步,使用结构体,对整个I2C进行配置
        I2C_InitTypeDef I2C_InitStructure;
        I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;        //I2C的模式,这里选择是I2C
        I2C_InitStructure.I2C_ClockSpeed = 50000;        //配置SCL的时钟频率,数值越大,SCL频率越高,数据传输就越快,这里写的是50kHz
        I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;        //时钟占空比,只有在时钟频率大于100kHz,也就是进入到快速状态时才有用,在小雨100kHz的标准速度下,占空比是标准的1:1
        I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;        //应答位配置,这里给enable,默认是给应答的
        I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;        //这里是指定STM32作为从机,可以响应几位的地址,这里选择7位地址
        I2C_InitStructure.I2C_OwnAddress1 = 0x00;        //自身地址1,这个也是stm32作为从机使用的,用于指定stm32的自身地址,方便别的主机呼叫它,这里暂时不需要做从机被别人使唤,随便给一个,只要不和总线上其它设备的地址重复就可以了
        I2C_Init(I2C2, &I2C_InitStructure);
        //第四步,I2C_Cmd,使能I2C
        I2C_Cmd(I2C2, ENABLE);

//封装指定地址写和指定地址读的时序
//指定地址写寄存器
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
//        MyI2C_Start();
//        MyI2C_SendByte(MPU6050_ADDRESS);        //从机地址+读写位
//        MyI2C_ReceiveAck();
//        //发送指定寄存器地址
//        MyI2C_SendByte(RegAddress);
//        MyI2C_ReceiveAck();
//        //发送指定要写入指定寄存器地址下的数据
//        MyI2C_SendByte(Data);
//        MyI2C_ReceiveAck();
//        //终止时序
//        MyI2C_Stop();
       
        uint32_t Timeout;
       
        //控制外设电路,实现指定地址写的时序,来替换上面的WriteReg
        I2C_GenerateSTART(I2C2, ENABLE);        //生成起始条件
        //对于非阻塞的程序,在函数结束之后,都要等待相应的标志位,来确保这个函数的操作执行到位了
        //对照PPT流程图,等待EV5的到来,stm32默认为从机,发送起始条件后变为主机
        Timeout = 10000;
        while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS)        //监测EV5事件是否发生了
        //在程序中如果while死循环等待用多了,一旦总线出问题了,就很容易造成整个程序卡死,还要设计一个超时退出的机制
        {
                Timeout --;
                if (Timeout == 0)
                {
                        break;        //使用break跳出这个循环,使用return跳出整个函数
                        //在实际项目中,如果想让代码更加完善,这里不能只是简单的break了
                        //这里还应该做一些相应的错误处理操作,比如说打印错误日志、进行系统复位
                        //或者说,如果项目设计危险的机械结构,就要评估一下,是不是应该进行紧急停机的操作
                }
        }
//        MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);        //这一句就等同与上面的等待事件和超时退出的结合
       
        I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);        //发送从机地址,第三个参数是方向,也就是从机地址的最低位,读写位
        //在这个库函数中,发送数据都自带了接收应答的过程,同样,接收数据也自带了发送应答的过程,如果应答错误,硬件会通过中断和标志位来提示我们,所以这里发送地址后,应答位就不需要处理了
        //等待EV6事件
        while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);
        //写入DR,发送数据
        I2C_SendData(I2C2, RegAddress);
        //等待EV8事件
        while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);
        //发送数据
        I2C_SendData(I2C2, Data);
        //等待事件,这里这个是最后一个字节,要等待EV8_2事件
        while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);
       
        I2C_GenerateSTOP(I2C2, ENABLE);
}

//指定地址读
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
        uint8_t Data;
//        MyI2C_Start();
//        MyI2C_SendByte(MPU6050_ADDRESS);        //从机地址+读写位
//        MyI2C_ReceiveAck();
//        //发送指定寄存器地址
//        MyI2C_SendByte(RegAddress);
//        MyI2C_ReceiveAck();
//       
//        //转入读的时序,就必须重新指定读写位,就必须重新起始
//        MyI2C_Start();
//        MyI2C_SendByte(MPU6050_ADDRESS | 0x01);                //原从机地址,读写位为1
//        MyI2C_ReceiveAck();                //接收应答后,总线控制权就正式交给从机了
//        Data = MyI2C_ReceiveByte();
//        //主机接收后,要给从机发送一个应答
//        //参数给0,就是给从机应答,给1,就是不给从机应答;想继续读多个字节,那就要给应答,从机收到应答后就会继续发送数据
//        MyI2C_SendAck(1);
//        MyI2C_Stop();
       
        //控制外设电路,来实现指定地址读的时序,来替换上面的ReadReg
        I2C_GenerateSTART(I2C2, ENABLE);        //生成起始条件
        while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);        //监测EV5事件是否发生了
        //在程序中如果while死循环等待用多了,一旦总线出问题了,就很容易造成整个程序卡死,还要设计一个超时退出的机制
       
        I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);        //发送从机地址,第三个参数是方向,也就是从机地址的最低位,读写位
        while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);

        //写入DR,发送数据
        I2C_SendData(I2C2, RegAddress);
        while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);
       
        I2C_GenerateSTART(I2C2, ENABLE);        //重复生成起始条件
        while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);        //监测EV5事件是否发生了
       
        I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);        //第三个参数改为Receiver之后,函数内部就会自动把MPU6050_ADDRESS这个地址的最低位置1了,就不需要手动来改了
        while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS);       
        //在接收最后一个字节之前,也就是EV7_1事件那里,需要提前把ACK置0,STOP置1,如果只需要读取一个字节,那在EV6事件之后就要立刻ACK置0,STOP置1,要是设置晚了,时序上就会多一个字节出来
        I2C_AcknowledgeConfig(I2C2, DISABLE);
        I2C_GenerateSTOP(I2C2, ENABLE);
       
        //等待EV7事件,等EV7事件产生后,一个字节的数据就已经在DR里面了,我们读取DR即可拿出这一个字节
        while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);
        Data = I2C_ReceiveData(I2C2);
       
        //ack再次置1,我们的想法是,默认状态下ACK就是1,给从机应答,在接收最后一个字节之前,临时把ACK置0,给非应答。
        //所以在接收函数的最后,要回复默认的ACk = 1,这个流程是为了方便指定地址收多个字节
        I2C_AcknowledgeConfig(I2C2, ENABLE);
       
        return Data;
}

————————————————

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/m0_57368670/article/details/139289481

使用特权

评论回复
沙发
xuanhuanzi| | 2024-6-9 18:18 | 只看该作者
I2C的HAL库非常好用。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

2028

主题

15904

帖子

14

粉丝