软件I2C的引脚可以任意更改,而硬件I2C的引脚不能随意更改,要按照引脚使用手册来看。

因为硬件I2C有相关库函数了,就不用像软件I2C一样自己编写底层函数,所以就要把原来软件I2C用到的Myi2c的.c以及.h文件移除,具体做法是现在工程移除,再到文件夹移除,保证工程目录以及文件夹目录的数值是一致的。

步骤:

1.开启I2C以及GPIO引脚时钟

2.把I2C对应的GPIO口配置为复用开漏输出

3.使用结构体,对I2C进行配置

4.I2C_cmd使能I2C

void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);//生成起始条件,也就是把CR寄存器的某一位置1产生起始条件

image.png

void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);//产生停止位

void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);//配置CR寄存器的ACK位

image.png

当他为1的时候给从机应答,0就是不给从机应答, 

void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);//发送数据,是直接写到DR寄存器的

 image.png

uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);//接收数据

 image.png

 void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);//发送地址

因为有的事件比如前面的EV事件会置多个标志位,如果仅仅查看其中的一个标志位就判断事件发生就不严谨,如果调用多次获取标志位的函数,就会不方便,所以引入Using I2C_CheckEvent() function:,判断一个或者多个标志位。

具体在I2C.h文件中。

硬件I2C就是复用开漏,软件i2c就是通用开漏输出,因为硬件I2C要把GPIO控制权交给外设。

ctrl+alt+空格,开启提示模式

速率:0到100khz是标准速率,100到400是快速

void MPU6050_Init(void)

{

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    

    GPIO_InitTypeDef GPIO_InitStructure;

    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;

    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

    GPIO_Init(GPIOB, &GPIO_InitStructure);

    

    I2C_InitTypeDef I2C_InitStructure;

    I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;

    I2C_InitStructure.I2C_ClockSpeed = 50000;

    I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;//占空比,只有快速模式下才有用,也就是100khz以上,小于则是低电平比高电平1比1,16:9就是低电平比高电平16:9,2就是2:1,按道理来说,同步时序无论高低电平持续多久都没问题,但是为了快速传输,所以设计了占空比,标准速度是1:1,SCL以及SDA下降沿是特别快的,但是上升沿确是略带缓慢的,如下图是100khz,原因是强下拉以及弱上拉,强下拉就会非常果决,弱上就会很慢,渐变的过程。下下图示101khz。为什么要有低电平持续的时间长一点呢,因为在低电平的时候数据变化,高电平的时候读取数据,而SDA在数据变化的时候,这个变化是缓慢的,所以需要比较长的时间,而读取是比较快的。要快速写入,就需要给更多的时间适应SDA电平变化,不然快速发送的时候数据变化来不及,高电平读取也没有啥用。

100khz:

image.png

 101khz:

image.png

200khz: 时间轴尺度进一步减小,SDA的变化延缓非常明显,如果没有充足的低电平变化,会来不及变化。

image.png

  400MHz:SCL还没到高电平呢,就一下子被拉下来变成低电平了,这个湾湾限制了最大传输速度,并且数据变化也不是完全贴着下降沿的,有一定的延迟,

image.png

 I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;//这一行跟这个代码的效果是一样的:void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);,都是用来配置ACK位的,实现接收成功后是否给ACK应答

    I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;//STM32作为从机可以相应几位的地址

    I2C_InitStructure.I2C_OwnAddress1 = 0x00;//STM32作为从机的地址,跟上一条函数相呼应,上一条是7位这里就一个字节,上一条函数10位这里就两个字节。

    I2C_Init(I2C2, &I2C_InitStructure);

    

    I2C_Cmd(I2C2, ENABLE);

    

    MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);

    MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);

    MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);

    MPU6050_WriteReg(MPU6050_CONFIG, 0x06);

    MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);

    MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);

}

接下来是写函数的写法:

在进行软件I2C的时候,设定基本函数的时候,内部是添加了延迟函数的,整体调用出来属于非阻塞式,但是硬件I2C的这些函数,是属于非阻塞式的函数。阻塞式的函数在执行完成之后,数据一定已经发送完成,所以上一个函数可以紧跟着下一个函数,但是非阻塞式的函数却不一定,所以我们需要测试上一条函数是否已经执行结束,也就是测试EV事件有没有发生。检测的话就是用状态检测函数。

I2C_EVENT_MASTER_MODE_SELECT:这是主机模式已选择事件,STM32初始是从机,发送起始条件之后,就变成了主机,这也是EV5的别名。

void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)

{

    I2C_GenerateSTART(I2C2, ENABLE);

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);

    

    I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);//发送字节函数自带了应答的功能,不需要调用函数实现,如果应答失败,会置标志位提醒,发送地址用普通发送字节的函数或者这个专用地址发送函数都能实现,里面的参数分别分为发送模式已选择以及接收模式已选择,注意甄别。

接下来并不需要理会那个EV8_1的事件,想理会也没有这个函数,直接进行下一步即可。

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);

    

    I2C_SendData(I2C2, RegAddress);

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);

    

    I2C_SendData(I2C2, Data);//发送的最后育德数据甄别的是EV8_2并非EV8_1,因为要确保移位寄存器以及数据寄存器都已经是空的了,也就是把两级缓存中的数据消化掉才进行结束条件。

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);

    

    I2C_GenerateSTOP(I2C2, ENABLE);

}

/**************************复合格式指定地址读***********************************/

uint8_t MPU6050_ReadReg(uint8_t RegAddress)

{

    uint8_t Data;

    

    I2C_GenerateSTART(I2C2, ENABLE);

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);

    

    I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);//指定地址,是写的

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);

    

    I2C_SendData(I2C2, RegAddress);

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);

    

    I2C_GenerateSTART(I2C2, ENABLE);

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);

    

    I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);//指定地址,不过是读的,内部会自动把最后的读写位写上,不用手动写。

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);//注意EV6事件的接收以及发送两者是不一样的,跳转到定义仔细观看。

    

    I2C_AcknowledgeConfig(I2C2, DISABLE);

    I2C_GenerateSTOP(I2C2, ENABLE);

    

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);//EV7在接收一个字节之后就会产生,所以在接收到一个字节之前就把ACK以及停止位搞好就行。在这之后,一个字节已经在DR寄存器中了,

    Data = I2C_ReceiveData(I2C2);//读取DR寄存器,就可以拿出这个值,

    //想要读取多个字节,就把红色部分放进FOR循环中,接收前面字节的时候,只执行后面两行,最后一个字节的时候,就四行一起执行。

    I2C_AcknowledgeConfig(I2C2, ENABLE);//最后把应答置1,回复默认的值,方便后续的运用

    

    return Data;

}

 

/************************************************************************/

 image.png

 在接收多个字节的时候,每接收一个字节的时候,等待EV7事件的产生就行了,但是在最后一个字节的时候,就要在EV6之后的时候,把ACK置0,然后进行STOP请求,ACK置0就会让最后一个字节不进行应答,从而结束,STOP函数在这时候启动,并不会马上结束,会等到这个字节发送完成的时候再进行结束函数。否则时序会多出来一个。

EV7在接收一个字节之后就会产生,所以在接收到一个字节之前就把ACK以及停止位搞好就行。

 原本用等待的时候是用了大量的死循环,while,但是容易造成总线出错程序卡死,所以我们需要一个超市退出机制,就是在while

循环这里加上一个数值,没执行一次while就减1,if多少就break。

下面是封装好的函数:

 

void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)

{

    uint32_t Timeout;

    Timeout = 10000;

    while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)

    {

        Timeout –;

        if (Timeout == 0)

        {

            break;

        }

    }

}

 软件I2C的引脚可以任意更改,而硬件I2C的引脚不能随意更改,要按照引脚使用手册来看。

因为硬件I2C有相关库函数了,就不用像软件I2C一样自己编写底层函数,所以就要把原来软件I2C用到的Myi2c的.c以及.h文件移除,具体做法是现在工程移除,再到文件夹移除,保证工程目录以及文件夹目录的数值是一致的。

步骤:

1.开启I2C以及GPIO引脚时钟

2.把I2C对应的GPIO口配置为复用开漏输出

3.使用结构体,对I2C进行配置

4.I2C_cmd使能I2C

void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);//生成起始条件,也就是把CR寄存器的某一位置1产生起始条件

image.png

void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);//产生停止位

void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);//配置CR寄存器的ACK位

image.png

当他为1的时候给从机应答,0就是不给从机应答, 

void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);//发送数据,是直接写到DR寄存器的

 image.png

uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);//接收数据

image.png

 

 void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);//发送地址

因为有的事件比如前面的EV事件会置多个标志位,如果仅仅查看其中的一个标志位就判断事件发生就不严谨,如果调用多次获取标志位的函数,就会不方便,所以引入Using I2C_CheckEvent() function:,判断一个或者多个标志位。

具体在I2C.h文件中。

硬件I2C就是复用开漏,软件i2c就是通用开漏输出,因为硬件I2C要把GPIO控制权交给外设。

ctrl+alt+空格,开启提示模式

速率:0到100khz是标准速率,100到400是快速

void MPU6050_Init(void)

{

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    

    GPIO_InitTypeDef GPIO_InitStructure;

    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;

    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

    GPIO_Init(GPIOB, &GPIO_InitStructure);

    

    I2C_InitTypeDef I2C_InitStructure;

    I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;

    I2C_InitStructure.I2C_ClockSpeed = 50000;

    I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;//占空比,只有快速模式下才有用,也就是100khz以上,小于则是低电平比高电平1比1,16:9就是低电平比高电平16:9,2就是2:1,按道理来说,同步时序无论高低电平持续多久都没问题,但是为了快速传输,所以设计了占空比,标准速度是1:1,SCL以及SDA下降沿是特别快的,但是上升沿确是略带缓慢的,如下图是100khz,原因是强下拉以及弱上拉,强下拉就会非常果决,弱上就会很慢,渐变的过程。下下图示101khz。为什么要有低电平持续的时间长一点呢,因为在低电平的时候数据变化,高电平的时候读取数据,而SDA在数据变化的时候,这个变化是缓慢的,所以需要比较长的时间,而读取是比较快的。要快速写入,就需要给更多的时间适应SDA电平变化,不然快速发送的时候数据变化来不及,高电平读取也没有啥用。

100khz:

image.png

 101khz:

image.png

200khz: 时间轴尺度进一步减小,SDA的变化延缓非常明显,如果没有充足的低电平变化,会来不及变化。

image.png

  400MHz:SCL还没到高电平呢,就一下子被拉下来变成低电平了,这个湾湾限制了最大传输速度,并且数据变化也不是完全贴着下降沿的,有一定的延迟,

image.png

 I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;//这一行跟这个代码的效果是一样的:void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);,都是用来配置ACK位的,实现接收成功后是否给ACK应答

    I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;//STM32作为从机可以相应几位的地址

    I2C_InitStructure.I2C_OwnAddress1 = 0x00;//STM32作为从机的地址,跟上一条函数相呼应,上一条是7位这里就一个字节,上一条函数10位这里就两个字节。

    I2C_Init(I2C2, &I2C_InitStructure);

    

    I2C_Cmd(I2C2, ENABLE);

    

    MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);

    MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);

    MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);

    MPU6050_WriteReg(MPU6050_CONFIG, 0x06);

    MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);

    MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);

}

接下来是写函数的写法:

在进行软件I2C的时候,设定基本函数的时候,内部是添加了延迟函数的,整体调用出来属于非阻塞式,但是硬件I2C的这些函数,是属于非阻塞式的函数。阻塞式的函数在执行完成之后,数据一定已经发送完成,所以上一个函数可以紧跟着下一个函数,但是非阻塞式的函数却不一定,所以我们需要测试上一条函数是否已经执行结束,也就是测试EV事件有没有发生。检测的话就是用状态检测函数。

I2C_EVENT_MASTER_MODE_SELECT:这是主机模式已选择事件,STM32初始是从机,发送起始条件之后,就变成了主机,这也是EV5的别名。

void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)

{

    I2C_GenerateSTART(I2C2, ENABLE);

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);

    

    I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);//发送字节函数自带了应答的功能,不需要调用函数实现,如果应答失败,会置标志位提醒,发送地址用普通发送字节的函数或者这个专用地址发送函数都能实现,里面的参数分别分为发送模式已选择以及接收模式已选择,注意甄别。

接下来并不需要理会那个EV8_1的事件,想理会也没有这个函数,直接进行下一步即可。

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);

    

    I2C_SendData(I2C2, RegAddress);

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);

    

    I2C_SendData(I2C2, Data);//发送的最后育德数据甄别的是EV8_2并非EV8_1,因为要确保移位寄存器以及数据寄存器都已经是空的了,也就是把两级缓存中的数据消化掉才进行结束条件。

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);

    

    I2C_GenerateSTOP(I2C2, ENABLE);

}

/**************************复合格式指定地址读***********************************/

uint8_t MPU6050_ReadReg(uint8_t RegAddress)

{

    uint8_t Data;

    

    I2C_GenerateSTART(I2C2, ENABLE);

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);

    

    I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);//指定地址,是写的

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);

    

    I2C_SendData(I2C2, RegAddress);

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);

    

    I2C_GenerateSTART(I2C2, ENABLE);

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);

    

    I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);//指定地址,不过是读的,内部会自动把最后的读写位写上,不用手动写。

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);//注意EV6事件的接收以及发送两者是不一样的,跳转到定义仔细观看。

    

    I2C_AcknowledgeConfig(I2C2, DISABLE);

    I2C_GenerateSTOP(I2C2, ENABLE);

    

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);//EV7在接收一个字节之后就会产生,所以在接收到一个字节之前就把ACK以及停止位搞好就行。在这之后,一个字节已经在DR寄存器中了,

    Data = I2C_ReceiveData(I2C2);//读取DR寄存器,就可以拿出这个值,

    //想要读取多个字节,就把红色部分放进FOR循环中,接收前面字节的时候,只执行后面两行,最后一个字节的时候,就四行一起执行。

    I2C_AcknowledgeConfig(I2C2, ENABLE);//最后把应答置1,回复默认的值,方便后续的运用

    

    return Data;

}

 

/************************************************************************/

 image.png

 在接收多个字节的时候,每接收一个字节的时候,等待EV7事件的产生就行了,但是在最后一个字节的时候,就要在EV6之后的时候,把ACK置0,然后进行STOP请求,ACK置0就会让最后一个字节不进行应答,从而结束,STOP函数在这时候启动,并不会马上结束,会等到这个字节发送完成的时候再进行结束函数。否则时序会多出来一个。

EV7在接收一个字节之后就会产生,所以在接收到一个字节之前就把ACK以及停止位搞好就行。

 原本用等待的时候是用了大量的死循环,while,但是容易造成总线出错程序卡死,所以我们需要一个超市退出机制,就是在while

循环这里加上一个数值,没执行一次while就减1,if多少就break。

下面是封装好的函数:

 

void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)

{

    uint32_t Timeout;

    Timeout = 10000;

    while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)

    {

        Timeout –;

        if (Timeout == 0)

        {

            break;

        }

    }

}

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

版权声明:本文为CSDN博主「笔下觅封侯」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/m0_63148816/article/details/131055970