• I2C的硬件实现


    因为I2C是同步的,所以相对来说I2C更好用软件来实现,硬件却相对来说没这么好,但是硬件I2C通信也是有其优点的

    我们是通过软件写入控制寄存器CR和数据寄存器DR,读取状态寄存器SR来了解外设电路当前处于什么状态,来实现I2C通信的,而我们通过STM32的库函数来实现配置这些寄存器,这些操作就变得更简单了;有了I2C硬件外设的存在,硬件自动实现时序,就可以减轻CPU的负担,节省软件资源,由硬件来做这件事,可以更加专注,时序生成的性能、效率也会更高,这就是I2C外设存在的意义。

    I2C外设简介

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

    多主机模型:

    1、固定多主机:固定多台机器为主机,只有其中一台主机才可以通过主线控制所有从机,当多个主机控制主线时,会出现主线冲突,这个时候就会进行总线总裁,仲裁失败的一方会让出总线控制权;

    2、可变多主机:默认情况下,全部机器都是从机,当某台机器需要控制权时,就会跳出来变成主机,控制主线,多个从机跳出来时,就会进行总线仲裁,仲裁成功的获得总线控制权。STM32的I2C就是可变多主机的模型

    I2C框图

    引脚定义

     

    I2C基本结构

    发送:移位寄存器中的数据由高位到低位先移出去,移8次就可以移一个字节,有高位到低位依次放到SDA线上;

    接收:数据从GPIO口从右边依次移进来,最终移8次,一个字节就接收完成了。

    配置问题:两个GPIO口都需要配置成复用开漏输出模式,复用——GPIO的状态是交由片上外设来控制的,开漏输出——I2C协议要求的端口配置(在这个模式下依然可以通过GPIO口进行输入)

    主机发送

    EV-标志位(多个事件发生的标志位)

    对应数据手册24.6.7

    EV5:(不需要手动清除的) 

     主机接收

     代码实操

     硬件IIC的缺点就是引脚固定

    相关函数介绍

    1. //老朋友
    2. void I2C_DeInit(I2C_TypeDef* I2Cx);
    3. void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);
    4. void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);
    5. void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
    1. //生成起始条件
    2. void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);
    3. //生成结束条件
    4. void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);

     生成起始条件的函数——通过配置IIC中的CR1寄存器的值来决定是否生成起始条件

    想要更深入了解I2C还是得需要阅读数据手册 

    可以看到这是配置ACK应答使能的函数 

    1. //接收应答
    2. void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);

     

     接收数据

    void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);
    

    查看其函数定义可以发现是配置DR数据寄存器的

     

    1. //接收数据——读取DR寄存器
    2. uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);
    1. //发送七位地址
    2. void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);

    这个函数还可以通过参数判断是接收还是发送模式来写入最低位(R/W)当然也可以自己通过SendData来操作 

     I2C状态监控函数

    这是库函数给我们设计的多种监控状态方案

    1、基本状态监控

    同时判断一个或者多个标志位,来确定正处于哪一个状态

    ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT);
    

    2、高级状态监控(不需要掌握)

    实际上并不高级

    3、基于状态标志位的监控

    可以判断某一个标志位是否置1

    FlagStatus I2C_GetFlagStatus(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);
    
    1. void I2C_ClearFlag(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);
    2. ITStatus I2C_GetITStatus(I2C_TypeDef* I2Cx, uint32_t I2C_IT);
    3. void I2C_ClearITPendingBit(I2C_TypeDef* I2Cx, uint32_t I2C_IT);

     实现流程

    我们这节内容和上一节的软件读取IIC类似,所以复制上一个过程文件,把工程中有关IIC的代码删去

    第一步:配置IIC外设

    A、开启IIC外设的时钟和对应GPIO口的时钟
    B、配置GPIO口为复用开漏模式
    1. RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
    2. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    3. GPIO_InitTypeDef GPIO_InitStructure;
    4. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
    5. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
    6. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    7. GPIO_Init(GPIOB, &GPIO_InitStructure);
    C、通过结构体配置相关参数

    特别说明:为什么要设置一个控制占空比的结构体成员呢?

    占空比的设置是为了快速传输而设置的

    50kHz的情况下

    400kHz的情况下

    因为时钟信号是强下拉,弱上拉,这就会导致在产生下降沿的时候速度非常快,而产生上升沿的时候电平却变化得比较慢,是慢慢回弹回去的(类似于弹簧模型,强下拉就像强行把弹簧按下去,而弱上拉却是弹簧自己慢慢弹上去)

    数据变化需要一定时间来产生波形,由其是上升沿,变化比较慢,而数据的采集却会很快(在上升沿时采集SDA上的电平)所以我们需要给产生波形多一点时间,即让SCL时钟信号的低电平时间(写入时间)变得更长一点,所以采取设置其占空比来实现该目的

    D、开关使能

    整体:

    1. void HI2C_Init(void)
    2. {
    3. RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
    4. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    5. GPIO_InitTypeDef GPIO_InitStructure;
    6. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
    7. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
    8. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    9. GPIO_Init(GPIOB, &GPIO_InitStructure);
    10. I2C_InitTypeDef I2C_InitStructure;
    11. //I2C模式
    12. I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
    13. //I2C时钟速度-0~100KHZ(标准速度) 100KHz~400KHz(快速)
    14. I2C_InitStructure.I2C_ClockSpeed = 50000;
    15. //时钟占空比-标准速度(只能为1:1) 快速模式下可以为16:9或者2:1
    16. I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
    17. //应答位(默认为Enable)
    18. I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
    19. //Stm32作为从机的地址位数
    20. I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
    21. //给STM32指定一个从机地址
    22. I2C_InitStructure.I2C_OwnAddress1 = 0x00;
    23. I2C_Init(I2C2, &I2C_InitStructure);
    24. I2C_Cmd(I2C2, ENABLE);
    25. }

    第二步:实现指定地址写的时序

    对照之前写的I2C的软件实现即可

    需要注意的是软件I2C的这些函数内部都添加了Delay函数来等待数据的接收完成,是一种阻塞式的流程,也就是上一个函数完成后,数据肯定已经完成接收或者发送了,但是硬件I2C函数不一样,都不是阻塞式的,函数结束后是将对应的标志位置1,所以要确保数据完整,就必须在函数后加上判断相应标志位的函数,如下图所示

    举个栗子:在发送完起始位后需要判断EV5事件,这样我们就需要用上之前的监控函数了

    ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT);
    

    第二个参数: 

    返回值 

    所以要判断是否完成就需要套用一个while循环

    1. //对照上面的软件I2C协议写
    2. I2C_GenerateSTART(I2C2, ENABLE);
    3. //EV5事件的检测
    4. while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);

    总体

    1. void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
    2. {
    3. // MyI2C_Start();
    4. // MyI2C_SendByte(MPU6050_ADDRESS);
    5. // //可以验证是否收到数据,具体怎么处理就不加上了
    6. // MyI2C_ReceiveAck();
    7. // //发送寄存器的地址
    8. // MyI2C_SendByte(RegAddress);
    9. // MyI2C_ReceiveAck();
    10. // //接收一个字节
    11. // MyI2C_SendByte(Data);
    12. // MyI2C_ReceiveAck();
    13. // MyI2C_Stop();
    14. //对照上面的软件I2C协议写
    15. I2C_GenerateSTART(I2C2, ENABLE);
    16. //EV5事件的检测
    17. while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
    18. //发送从机地址(注意这个地址不包含读写位)
    19. //发送地址函数中已经自带了接收应答的过程,则不用再调用函数来应答了
    20. //同样的,如果发送错误,硬件会通过置标志位或者中断来提醒我们
    21. I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
    22. while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);
    23. I2C_SendData(I2C2, RegAddress);
    24. while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);
    25. I2C_SendData(I2C2, Data);
    26. while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);
    27. I2C_GenerateSTOP(I2C2, ENABLE);
    28. }

    第三步:实现指定地址读的时序

     注意:在接收最后一个字节前,必须要提前把ACK置0和设置STOP请求(STOP不会打断字节的接收),如果只接收一个字节,就可以直接在接收数据1之前就把ACK置0和发送STOP请求。

    1. //指定地址读
    2. uint8_t MPU6050_ReadtheReg(uint8_t RegAddress)
    3. {
    4. uint8_t Data;
    5. // MyI2C_Start();
    6. // MyI2C_SendByte(MPU6050_ADDRESS);
    7. // MyI2C_ReceiveAck();
    8. // MyI2C_SendByte(RegAddress);
    9. // MyI2C_ReceiveAck();
    10. //
    11. // MyI2C_Start();
    12. // MyI2C_SendByte(MPU6050_ADDRESS | 0x01);
    13. // MyI2C_ReceiveAck();
    14. // Data = MyI2C_ReceiveByte();
    15. // //发送主机应答,已经接收到数据了
    16. // MyI2C_SendAck(1);
    17. // MyI2C_Stop();
    18. //与发送一个字节的前半部分一样
    19. I2C_GenerateSTART(I2C2, ENABLE);
    20. while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
    21. I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
    22. while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);
    23. I2C_SendData(I2C2, RegAddress);
    24. //注意这里不是硬抄,检测的标志位改为了ED结尾(其实ing还是ED结尾都没有区别
    25. //都会等到发送数据完全结束后才会进行下一步操作
    26. while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);
    27. //开始接收字节
    28. I2C_GenerateSTART(I2C2, ENABLE);
    29. while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
    30. //发送寄存器地址
    31. I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);
    32. while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS);
    33. //提前ACK置0和发送STOP请求
    34. I2C_AcknowledgeConfig(I2C2, DISABLE);
    35. I2C_GenerateSTOP(I2C2, ENABLE);
    36. while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);
    37. Data = I2C_ReceiveData(I2C2);
    38. I2C_AcknowledgeConfig(I2C2, ENABLE);
    39. return Data;
    40. }

     整体:记得在初始化函数中引用HI2C的初始化函数

    1. #include "stm32f10x.h" // Device header
    2. #include "MPU6050_Reg.h"
    3. #include "Hard-I2C.h"
    4. #define MPU6050_ADDRESS 0xD0
    5. //指定地址写
    6. void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
    7. {
    8. // MyI2C_Start();
    9. // MyI2C_SendByte(MPU6050_ADDRESS);
    10. // //可以验证是否收到数据,具体怎么处理就不加上了
    11. // MyI2C_ReceiveAck();
    12. // //发送寄存器的地址
    13. // MyI2C_SendByte(RegAddress);
    14. // MyI2C_ReceiveAck();
    15. // //接收一个字节
    16. // MyI2C_SendByte(Data);
    17. // MyI2C_ReceiveAck();
    18. // MyI2C_Stop();
    19. //对照上面的软件I2C协议写
    20. I2C_GenerateSTART(I2C2, ENABLE);
    21. //EV5事件的检测
    22. while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
    23. //发送从机地址(注意这个地址不包含读写位)
    24. //发送地址函数中已经自带了接收应答的过程,则不用再调用函数来应答了
    25. //同样的,如果发送错误,硬件会通过置标志位或者中断来提醒我们
    26. I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
    27. while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);
    28. I2C_SendData(I2C2, RegAddress);
    29. while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);
    30. I2C_SendData(I2C2, Data);
    31. while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);
    32. I2C_GenerateSTOP(I2C2, ENABLE);
    33. }
    34. //指定地址读
    35. uint8_t MPU6050_ReadtheReg(uint8_t RegAddress)
    36. {
    37. uint8_t Data;
    38. // MyI2C_Start();
    39. // MyI2C_SendByte(MPU6050_ADDRESS);
    40. // MyI2C_ReceiveAck();
    41. // MyI2C_SendByte(RegAddress);
    42. // MyI2C_ReceiveAck();
    43. //
    44. // MyI2C_Start();
    45. // MyI2C_SendByte(MPU6050_ADDRESS | 0x01);
    46. // MyI2C_ReceiveAck();
    47. // Data = MyI2C_ReceiveByte();
    48. // //发送主机应答,已经接收到数据了
    49. // MyI2C_SendAck(1);
    50. // MyI2C_Stop();
    51. //与发送一个字节的前半部分一样
    52. I2C_GenerateSTART(I2C2, ENABLE);
    53. while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
    54. I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
    55. while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);
    56. I2C_SendData(I2C2, RegAddress);
    57. //注意这里不是硬抄,检测的标志位改为了ED结尾(其实ing还是ED结尾都没有区别
    58. //都会等到发送数据完全结束后才会进行下一步操作
    59. while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);
    60. //开始接收字节
    61. I2C_GenerateSTART(I2C2, ENABLE);
    62. while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
    63. //发送寄存器地址
    64. I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);
    65. while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS);
    66. //提前ACK置0和发送STOP请求
    67. I2C_AcknowledgeConfig(I2C2, DISABLE);
    68. I2C_GenerateSTOP(I2C2, ENABLE);
    69. while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);
    70. Data = I2C_ReceiveData(I2C2);
    71. I2C_AcknowledgeConfig(I2C2, ENABLE);
    72. return Data;
    73. }
    74. void MPU6050_Init(void)
    75. {
    76. HI2C_Init();
    77. //电源管理寄存器1
    78. //设备复位 睡眠模式 循环模式 无关位 温度传感器失能 选择时钟(后三位)
    79. //0(不复位) 0(解除睡眠) 0(不需要) 0 0(不失能) 000(内部时钟)(或者001-陀螺仪时钟)
    80. MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
    81. //电源管理寄存器2
    82. //循环模式唤醒频率(不需要-00)
    83. //后6位每个轴的待机位-(000000-不待机)
    84. MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);
    85. //采样率分频(值越小,数据输出越快)(采样分频为10)
    86. MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);
    87. //配置寄存器
    88. //外部同步-000000 数字低通滤波-110
    89. MPU6050_WriteReg(MPU6050_CONFIG, 0x06);
    90. //陀螺仪配置寄存器
    91. //自测使能-000 满量程选择-11 无关位-000
    92. MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);
    93. //加速度计配置寄存器
    94. //自测使能-000 满量程选择-11 高通滤波器-000
    95. MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);
    96. }
    97. //读取寄存器
    98. void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
    99. int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
    100. {
    101. int16_t DataH, DataL;
    102. DataH = MPU6050_ReadtheReg(MPU6050_ACCEL_XOUT_H);
    103. DataL = MPU6050_ReadtheReg(MPU6050_ACCEL_XOUT_L);
    104. *AccX = (DataH << 8) | DataL;
    105. DataH = MPU6050_ReadtheReg(MPU6050_ACCEL_YOUT_H);
    106. DataL = MPU6050_ReadtheReg(MPU6050_ACCEL_YOUT_L);
    107. *AccY = (DataH << 8) | DataL;
    108. DataH = MPU6050_ReadtheReg(MPU6050_ACCEL_ZOUT_H);
    109. DataL = MPU6050_ReadtheReg(MPU6050_ACCEL_ZOUT_L);
    110. *AccZ = (DataH << 8) | DataL;
    111. DataH = MPU6050_ReadtheReg(MPU6050_GYRO_XOUT_H);
    112. DataL = MPU6050_ReadtheReg(MPU6050_GYRO_XOUT_L);
    113. *GyroX = (DataH << 8) | DataL;
    114. DataH = MPU6050_ReadtheReg(MPU6050_GYRO_YOUT_H);
    115. DataL = MPU6050_ReadtheReg(MPU6050_GYRO_YOUT_L);
    116. *GyroY = (DataH << 8) | DataL;
    117. DataH = MPU6050_ReadtheReg(MPU6050_GYRO_ZOUT_H);
    118. DataL = MPU6050_ReadtheReg(MPU6050_GYRO_ZOUT_L);
    119. *GyroZ = (DataH << 8) | DataL;
    120. }
    121. uint8_t MPU6050_GetID(void)
    122. {
    123. return MPU6050_ReadtheReg(MPU6050_WHO_AM_I);
    124. }

    其他的没改变

    主函数

    1. #include "stm32f10x.h" // Device header
    2. #include "Delay.h"
    3. #include "OLED.h"
    4. #include "MPU6050.h"
    5. uint8_t ID;
    6. int16_t AX, AY, AZ, GX, GY, GZ;
    7. int main(void)
    8. {
    9. OLED_Init();
    10. MPU6050_Init();
    11. OLED_ShowString(1, 1, "ID:");
    12. ID = MPU6050_GetID();
    13. OLED_ShowHexNum(1, 4, ID, 2);
    14. while(1)
    15. {
    16. MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);
    17. OLED_ShowSignedNum(2, 1, AX, 5);
    18. OLED_ShowSignedNum(3, 1, AY, 5);
    19. OLED_ShowSignedNum(4, 1, AZ, 5);
    20. OLED_ShowSignedNum(2, 8, GX, 5);
    21. OLED_ShowSignedNum(3, 8, GY, 5);
    22. OLED_ShowSignedNum(4, 8, GZ, 5);
    23. }
    24. }

    这样就可以实现相关功能了

    优化:

    我们是用死循环来等待标志位结束的,但凡有一个没结束都会导致整个函数停止

    我们可以封装一个函数来实现计时功能,计数指定时间后退出函数

    可以防止卡死错误

    1. void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
    2. {
    3. uint32_t Timeout;
    4. Timeout = 10000;
    5. while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)
    6. {
    7. Timeout --;
    8. if (Timeout == 0)
    9. {
    10. break;
    11. }
    12. }
    13. }

    把函数中的每个while循环改为此函数即可

  • 相关阅读:
    《七月集训》第二十八日——动态规划
    android 自定义View 视差动画
    react中类组件的相关问题及优化
    拼图小游戏
    SQL Server基础指令(创建与检索)
    【无标题】
    2023全新小程序广告流量主奖励发放系统源码 流量变现系统
    nginx升级
    C语言函数
    数据结构(1)线性结构——数组、链表、堆栈、队列(介绍和JAVA代码实现)
  • 原文地址:https://blog.csdn.net/m0_74460550/article/details/133758478