stm32学习笔记-10 I2C通信

10 I2C通信

注:笔记主要参考B站 江科大自化协 教学视频“STM32入门教程-2023持续更新中”。
注:工程及代码文件放在了本人的Github仓库


10.1 I2C通信

图10-1 采用I2C协议的部分模块实物图

I2C总线(Inter IC BUS) 是由Philips公司开发的一种通用数据总线,应用广泛,下面是一些指标参数:

  • 两根通信线:SCL(Serial Clock,串行时钟线)、SDA(Serial Data,串行数据线)。
  • 同步,半双工。
  • 主从机通信过程中,会带数据应答。
  • 支持总线挂载多设备(一主多从、多主多从)。“多主多从”模式更复杂,下面没有特殊说明,默认都是“一主多从”模式。
  • 高位先行

那同步时序和异步时序的优缺点各是什么呢?

  • 同步时序:优点是对时间要求不严格,可以软件手动翻转电平实现通信,所以可以大幅降低单片机对外围硬件电路的依赖,在一些低端单片机没有硬件资源的情况下,很容易使用软件来模拟时序;缺点就是多一个时钟线。
  • 异步时序:优点是少一根时钟线,节省资源;缺点是对时间要求严格,对硬件电路的依赖严重。

注:至于为什么学完串口,又要学习I2C协议,新手建议听听UP原视频“大公司要给我一千万”的例子——P31的3:00~14:53,引人入胜。

作为一个通信协议,就必须在硬件和软件上都做出规定:

  • 硬件上规定电路如何连接、端口的输入输出模式等
  • 软件上规定通信时序、字节如何传输、高位/低位先行等

下面首先来看I2C的硬件规定:

图10-2 I2C硬件电路示意图——一主多从

硬件电路规定所有I2C设备的SCL连在一起,SDA连在一起。设备的SCL和SDA均要配置成 开漏输出模式。SCL和SDA各添加一个 上拉电阻,阻值一般为4.7KΩ左右。
总结:为防止总线没协调好,导致电源短路,采用 外置弱上拉电阻+开漏输出 的电路结构。

  • CPU:主机,对总线控制权力大。任何时候,都是主机对SCL完全控制。空闲状态下,主机可以主动发起对SDA的控制。
  • 被控ICx:从机,对总线控制权力小。任何时刻,从机都只能被动读取SCL线。只有在从机发送数据、或者从机发送应答位时,从机才短暂的具有对SDA的控制权。
  • 从机设备地址:为了保证通信正常,每个从机设备都具有一个唯一的地址。在I2C协议标准中,从机设备地址分为7位地址、10位地址,其中7位较为简单且应用更广泛。不同的I2C模块出厂时,厂商都会为其分配唯一的7位地址,具体可以在芯片手册中查询,如MPU6050的地址是110_1000,AT24C02的地址是101_0000等,其中器件地址的最后几位是可以在电路中改变的。总线上都是不同模块一般不会有地址冲突,若总线上有相同的模块就需要外界电路来相应的器件地址。
  • 通信引脚结构:输入线都很正常。输出线则采用开漏输出,引脚下拉是“强下拉”,引脚上拉则处于“浮空状态”。于是所有设备都只能输出低电平,为了避免高电平造成的引脚浮空,就需要在总线外面设置上拉电阻(弱上拉)。采用这个模式的好处:
  1. 完全杜绝了电源短路的情况,保证了电路安全。
  2. 避免了引脚模式的频繁切换。开漏+弱上拉的模式,同时兼具了输入和输出的功能。要想输出就直接操纵总线;要想输入就直接输出高电平,然后读取总线数据,无需专门将GPIO切换成输入模式。
  3. 此模式可以实现“线与”功能。只有总线上所有设备都输出高电平,总线才是高电平;否则只要有一个设备输出低电平,总线就是低电平。I2C可以利用这个特征,执行多主机模式下的时钟同步(所以SCL也采用此模式)和总线仲裁。

下面介绍I2C通信的软件规定——时序结构(一主多从):

下面的这些操作的主语都是“主机”。

  • 起始条件:SCL高电平期间,SDA从高电平切换到低电平。
  • 终止条件:SCL高电平期间,SDA从低电平切换到高电平。
  • 发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。
  • 接收一个字节:主机在接收之前释放SDA,只控制SCL变化。SCL低电平期间,从机将数据位依次放到SDA线上(高位先行,且一般贴着SCL下降沿变化);SCL高电平期间,主机读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节。
  • 发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答。若从机没有收到主机的应答,就会完全释放SDA的控制权,回到待机模式。
  • 接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)。

于是下面就可以拼凑出完整的数据帧:

  • 指定地址写:对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)。
  • 当前地址读:对于指定设备(Slave Address),在 当前地址指针 (上电默认0x00)指示的地址下,读取从机数据(Data)。
  • 指定地址读(“哑写”更换地址):对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)。

注:额外还有“连续写”、“连续读”就不多做介绍了。

10.2 MPU6050简介

图10-3 MPU6050示意图

MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角(欧拉角),常应用于平衡车、飞行器等需要检测自身姿态的场景。以飞机为例,欧拉角就是飞机机身相对于初始3个轴的夹角——俯仰(Pitch)、滚转(Roll)、偏航(Yaw)。但是单一的加速度计、陀螺仪、磁力计都不能获得精确且稳定的欧拉角,只有综合多种传感器进行数据融合、取长补短,才能获得精确且稳定的欧拉角。常见的数据融合算法,一般有互补滤波、卡尔曼滤波等(惯性导航领域——姿态解算)。 下面给出MPU6050芯片的一些参数:

  • 3轴加速度计(Accelerometer):测量X、Y、Z轴的加速度。本质上就是弹簧测力计: F = m a F=ma F=ma。测量的值是合加速度,由于系统运动时有运动加速度的影响,所以只有在静态时才表示系统的姿态。也就是说,加速度计静态稳定,而动态不稳定。
  • 3轴陀螺仪传感器(Gyroscope):测量X、Y、Z轴的角速度。若想得到角度,只需要对角速度进行积分即可。陀螺仪的缺点是,物体静止时,会由于噪声导致角速度无法完全归零,于是就会导致积分得到的角度产生缓慢的漂移,积分时间越长漂移越明显,但当物体运动时就可以忽略漂移的大小。也就是说,陀螺仪动态稳定,而静态不稳定。
  • 3轴磁场传感器(选修):测量X、Y、Z轴的磁场强度,某些芯片会有此功能。
  • 气压传感器(选修):测量气压大小,一般气压值反映高度信息(海拔高度),某些芯片会有此功能。

注:于是姿态解算的大题思路就是:将“加速度计”、“陀螺仪”进行互补滤波,融合得到静态和动态都稳定的姿态角。
注:上图所示的机械结构只是便于理解,实际MPU6050芯片中不存在图示的机械结构。

  • 16位ADC采集传感器的模拟信号,量化范围:-32768~32767(有符号数)。
  • 加速度计满量程选择:±2、±4、±8、±16(g,1g=9.8m/s2)。
  • 陀螺仪满量程选择: ±250、±500、±1000、±2000(°/sec)。
  • 可配置的数字低通滤波器,使输出数据更加平缓。
  • 可配置的时钟源、可配置的采样分频。时钟源经过分频后,为内部的ADC提供转换时钟
  • I2C从机地址(手册可以查看到):MPU6050的设备号固定为0x68(某些批次可能为0x98),所以可以用于验证I2C读出协议是否正常。对于出厂地址为0x68的芯片来说,调整AD0引脚电压就可以改变其从机地址:1101000(AD0=0)、1101001(AD0=1)
表10-1 MPU6050模块-引脚描述
引脚功能
VCC、GND电源
SCL、SDAI2C通信引脚
XCL、XDA主机I2C通信引脚
AD0从机地址最低位
INT中断信号输出
图10-4 MPU6050模块-硬件电路
  • SCL、SDA:模块已经内置了上拉电阻,所以可以将端口直接用线连在GPIO口上。
  • XCL、XDA:使I2C可以作为主机,扩展主机功能,使其可以外接磁力计或气压计。因为只根据加速度计和陀螺仪融合出来的姿态角,无法准确计算出绕Z轴的角度(偏航角),因为其漂移无法通过加速度计进行修正,此时就需要外接指南针(磁力计)来提供长时间的稳定偏航角进行参考。另外要进行定高飞行,也需要外接一个气压计来感知海拔高度。然后MPU6050读取到这些扩展模块的数据,就可以通过其内部的DMP单元进行数据融合和姿态解算。当然也可以将磁力计或气压计直接挂载到I2C总线上,由stm32进行解算。
  • AD0引脚:扩展芯片的从机地址。模块电路中将AD0下拉接地,所以不接其他外部信号,从机地址固定为1101000。当然也可以外部强上拉成高电平。
  • INT中断输出引脚:可以配置芯片内部的一些事件,来触发中断引脚的输出,如数据准备好、I2C主机错误、自由落体检测、运动检测、零运动检测等。
    LDO低压差线性稳压器:MPU6050供电范围是2.375~3.46V,为了扩大模块的供电范围,模块设计者添加了LDO电路。其输入电压范围为3.3~5V,经过稳压器,可以输出稳定的3.3V,给模块上的芯片MPU6050供电。当然,如果已经有稳定的3.3V电源,就不再需要这部分电路了。
图10-5 MPU6050-芯片框图
  • 时钟系统:可以选择外部时钟电路。但一般选择内部时钟,所以模块中没有涉及这两个引脚。
  • 传感器部分:包括三个加速度计、三个陀螺仪、一个温度传感器。这些传感器本质上都相当于可变电阻,分压后的模拟电压经过16位ADC转换后,可以将数据输出到数据寄存器存起来。芯片内部的转换+转运都是全自动完成的,并且不存在数据覆盖的问题。只需要配置转换速率即可。
  • 自测单元:用于验证芯片是否正常。启动自测单元后,芯片内部会模拟一个外力,这个外力会导致传感器数据比平常大。于是进行自测的步骤就是:先使能自测读取数据,再失能自测读取数据,两者相减就可以得到“自测响应”。芯片手册中有这个“自测响应”的范围,若不在范围内,那就说明该传感器有问题不宜使用。
  • 电荷泵:电荷泵是一种升压电路。CPOUT引脚需要外接一个电容。通过不断地快速给外接电容并联充电、串联放电,就可以实现升压;再接上一个电压滤波电路,便可以输出一个稳定的升压。注意到这个升压电路是供给陀螺仪传感器使用的。
  • 寄存器和接口部分:
  • 中断状态寄存器:可以控制内部哪些事件到中断引脚的输出。
  • FIFO:先入先出寄存器,可以对数据流进行缓存。本节暂时不用。
  • 配置寄存器:可以对内部的各个电路进行配置。
  • 传感器数据寄存器:存储各个传感器的数据。
  • 工厂校准:意思就是内部的传感器都进行了校准。无需了解。
  • 数字运动处理器:简称DMP。是芯片内部自带的姿态解算的硬件算法,配合官方的DMP库,就可以进行姿态解算。本节暂时不涉及。
  • FSYNC:帧同步。暂时不涉及。
  • 从机I2C/SPI接口:从机的I2C、SPI接口,用于和STM32通信。
  • 主机I2C接口:用于和MPU6050扩展的设备进行通信。
  • 旁路选择器:前面说,MPU6050是stm32的小弟之一,而MPU6050自己也可以接很多小弟。开关拨到上面,小弟合并,导致stm32是所有设备的大哥;开关拨到下面,stm32只是MPU6050的大哥,而MPU6050又是扩展设备的大哥,这两管理系统相互独立、互不干扰。本节不会用到此功能。
  • 供电部分:按照手册要求供电即可。

10.3 实验:软件读写MPU6050

需求:通过I2C通信协议,对MPU6050内部的寄存器进行读写。写入到配置寄存器,就可以对这个外挂模块进行配置。读出数据寄存器,就可以获取外挂模块的数据。最终将读出的数据显示在OLED屏幕上。要求显示:设备的ID号、加速度传感器的三个输出数据、陀螺仪传感器的三个输出数据(角速度)。

注:串口是异步通信,时序严格,所以一般不用软件模拟;I2C等同步通信,对时序要求不严格,可以很方便的进行软件模拟。
注:软件模拟时,SCL和SDA都是普通的GPIO口,所以可以随便接。
注:SCL和SDA都应该有一个上拉电阻,在模块的电路中已经设计了这个上拉电阻,所以可以跳线直连GPIO口。

图10-6 软件读写MPU6050-接线图
图10-7 软件读写MPU6050-代码调用(非库函数)

下面是代码展示:Delay、OLED相关代码省略。
- main.c

#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "MPU6050.h"

int main(void){
    MPU6050_DataStruct SensorData;//存放6轴传感器数据
        
    OLED_Init();   //OLED初始化 
    MPU6050_Init();//MPU6050初始化
    
    //显示一些基本信息
    OLED_ShowString(1,1,"ID:");
    OLED_ShowHexNum(1,4,MPU6050_GetID(),2);
//    OLED_ShowString(1,1,"Acce:  | Gyro:");
    OLED_ShowString(2,1,"       |");
    OLED_ShowString(3,1,"       |");
    OLED_ShowString(4,1,"       |");
    //不断获取传感器值并显示
    while(1){
        MPU6050_GetData(&SensorData);
        //原始数值
//        OLED_ShowSignedNum(2,1,SensorData.Acce_X,5);
//        OLED_ShowSignedNum(3,1,SensorData.Acce_Y,5);
//        OLED_ShowSignedNum(4,1,SensorData.Acce_Z,5);
//        OLED_ShowSignedNum(2,9,SensorData.Gyro_X,5);
//        OLED_ShowSignedNum(3,9,SensorData.Gyro_Y,5);
//        OLED_ShowSignedNum(4,9,SensorData.Gyro_Z,5);
        //转换成小数的数值
        OLED_ShowFloat(2,1,(float)SensorData.Acce_X/32768*20,3,1);
        OLED_ShowFloat(3,1,(float)SensorData.Acce_Y/32768*20,3,1);
        OLED_ShowFloat(4,1,(float)SensorData.Acce_Z/32768*20,3,1);
        OLED_ShowFloat(2,9,(float)SensorData.Gyro_X/32768*500,3,1);
        OLED_ShowFloat(3,9,(float)SensorData.Gyro_Y/32768*500,3,1);
        OLED_ShowFloat(4,9,(float)SensorData.Gyro_Z/32768*500,3,1);
    };
}

- I2C_User.h

#ifndef __I2C_USER_H
#define __I2C_USER_H

void  I2C_User_Init(void);
void I2C_User_Start(void);
void I2C_User_Stop(void);
void I2C_User_SendByte(uint8_t send_byte);
uint8_t I2C_User_RecvByte(void);
void I2C_User_SendAck(uint8_t AckBit);
uint8_t I2C_User_RecvAck(void);

#endif

- I2C_User.c

//本文件 定义I2C的6个基本的时序单元,供其他模块调用
#include "stm32f10x.h"                  // Device header
#include "Delay.h"

//引脚操作的封装和改名,方便移植——移植时仅需修改本部分即可
//
//定义I2C通信的两个引脚-PB10为SCL、PB11为SDA
#define I2C_User_SCL_Port GPIOB
#define I2C_User_SDA_Port GPIOB
#define I2C_User_SCL GPIO_Pin_10
#define I2C_User_SDA GPIO_Pin_11
//#define I2C_User_SCL_High GPIO_SetBits  (I2C_User_SCL_Port, I2C_User_SCL)
//#define I2C_User_SCL_Low  GPIO_ResetBits(I2C_User_SCL_Port, I2C_User_SCL)
//#define I2C_User_SDA_High GPIO_SetBits  (I2C_User_SDA_Port, I2C_User_SDA)
//#define I2C_User_SDA_Low  GPIO_ResetBits(I2C_User_SDA_Port, I2C_User_SDA)
//写SCL的操作
void I2C_User_W_SCL(uint8_t BitValue){
    GPIO_WriteBit(I2C_User_SCL_Port,I2C_User_SCL,(BitAction)BitValue);
    Delay_us(3);// 延迟一位(I2C最大通信速率400kHz-2.5us)
}

//写SDA的操作
void I2C_User_W_SDA(uint8_t BitValue){
    GPIO_WriteBit(I2C_User_SDA_Port,I2C_User_SDA,(BitAction)BitValue);
    Delay_us(3);// 延迟一位(I2C最大通信速率400kHz-2.5us)
}

//读SDA的操作
uint8_t I2C_User_R_SDA(void){
    uint8_t BitValue=0;
    BitValue = GPIO_ReadInputDataBit(I2C_User_SDA_Port,I2C_User_SDA);
    Delay_us(3);// 延迟一位(I2C最大通信速率400kHz-2.5us)
    return BitValue;
}

//初始化两个GPIO口
void  I2C_User_Init(void){
    // 开启APB2-GPIOB的外设时钟RCC
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    // 初始化PA的输出端口:定义结构体及参数
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = I2C_User_SCL | I2C_User_SDA;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;//开漏输出
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    // 默认输出为高电平-释放总线
    GPIO_SetBits(GPIOB, I2C_User_SCL | I2C_User_SDA);
}
//

// 下面是I2C的六个基本时序-移植时无需修改
// 注:除了终止条件,SCL以高电平结束;其他时序都以SCL低电平结束
//
//1. 发送起始位
void I2C_User_Start(void){
    // 调整SCL、SDA输出均为高电平-保险起见,先将SDA拉高
    I2C_User_W_SDA(1);
    I2C_User_W_SCL(1);
    // SDA输出下降沿
    I2C_User_W_SDA(0);
    // 拉低SCL
    I2C_User_W_SCL(0);
}

//2. 发送终止位
void I2C_User_Stop(void){
    // 调整SCL、SDA输出均为低电平
    I2C_User_W_SCL(0);
    I2C_User_W_SDA(0);
    // 拉高SCL
    I2C_User_W_SCL(1);
    // 拉高SDA
    I2C_User_W_SDA(1);  
}
    
//3. 发送一个字节
void I2C_User_SendByte(uint8_t send_byte){
    uint8_t i=0;
    for(i=0;i<8;i++){
        //改变SDA
        I2C_User_W_SDA((0x80>>i) & send_byte);
        //拉高SCL
        I2C_User_W_SCL(1);
        //拉低SCL
        I2C_User_W_SCL(0);
    }
}

//4. 接收一个字节
uint8_t I2C_User_RecvByte(void){
    uint8_t recv_byte=0x00;
    uint8_t i=0;
    //主机释放总线
    I2C_User_W_SDA(1);
    //接收数据
    for(i=0;i<8;i++){
        // 拉高SCL
        I2C_User_W_SCL(1);
        // 读取数据
        if(I2C_User_R_SDA()==1){
            recv_byte |= (0x80>>i);
        }
        // 拉低SCL
        I2C_User_W_SCL(0);
    }
    return recv_byte;
}

//5. 发送应答位
void I2C_User_SendAck(uint8_t AckBit){
    // 将应答位放在SDA上
    I2C_User_W_SDA(AckBit);
    // 拉高SCL
    I2C_User_W_SCL(1);
    // 拉低SCL
    I2C_User_W_SCL(0);
}

//6. 接收应答位
uint8_t I2C_User_RecvAck(void){
    uint8_t AckBit=0;
    //主机释放总线
    I2C_User_W_SDA(1);
    // 拉高SCL
    I2C_User_W_SCL(1);
    // 读取应答信号
    AckBit = I2C_User_R_SDA();
    // 拉低SCL
    I2C_User_W_SCL(0);
    return AckBit;
}
//

- MPU6050.h

#ifndef __MPU6050_H
#define __MPU6050_H

//定义MPU6050的6轴数据结构体
typedef struct{
    int16_t Gyro_X;
    int16_t Gyro_Y;
    int16_t Gyro_Z;
    int16_t Acce_X;
    int16_t Acce_Y;
    int16_t Acce_Z;
    int16_t Temp;
}MPU6050_DataStruct;

//外部可调用函数
void MPU6050_WriteReg(uint8_t RegAddr, uint8_t wData);
uint8_t MPU6050_ReadReg(uint8_t RegAddr);
void MPU6050_Init(void);
uint8_t MPU6050_GetID(void);
void MPU6050_GetData(MPU6050_DataStruct* MPU6050_Data);

#endif

- MPU6050.c

#include "stm32f10x.h"                  // Device header
#include "I2C_User.h"
#include "MPU6050.h"

//定义从机地址、寄存器地址
#define MPU6050_ADDRESS           0xD0
#define MPU6050_REG_SMPLRT_DIV    0x19
#define MPU6050_REG_CONFIG        0x1A
#define MPU6050_REG_GYRO_CONFIG   0x1B
#define MPU6050_REG_ACCEL_CONFIG  0x1C

#define MPU6050_REG_ACCEL_XOUT_H  0x3B
#define MPU6050_REG_ACCEL_XOUT_L  0x3C
#define MPU6050_REG_ACCEL_YOUT_H  0x3D
#define MPU6050_REG_ACCEL_YOUT_L  0x3E
#define MPU6050_REG_ACCEL_ZOUT_H  0x3F
#define MPU6050_REG_ACCEL_ZOUT_L  0x40
#define MPU6050_REG_TEMP_OUT_H    0x41
#define MPU6050_REG_TEMP_OUT_L    0x42
#define MPU6050_REG_GYRO_XOUT_H   0x43
#define MPU6050_REG_GYRO_XOUT_L   0x44
#define MPU6050_REG_GYRO_YOUT_H   0x45
#define MPU6050_REG_GYRO_YOUT_L   0x46
#define MPU6050_REG_GYRO_ZOUT_H   0x47
#define MPU6050_REG_GYRO_ZOUT_L   0x48

#define MPU6050_REG_PWR_MGMT_1    0x6B
#define MPU6050_REG_PWR_MGMT_2    0x6C
#define MPU6050_REG_WHO_AM_I      0x75

//MPU6050指定地址写寄存器
void MPU6050_WriteReg(uint8_t RegAddr, uint8_t wData){
    I2C_User_Start();
    I2C_User_SendByte(MPU6050_ADDRESS);
    I2C_User_RecvAck();//暂时不对应答位进行处理
    I2C_User_SendByte(RegAddr);
    I2C_User_RecvAck();
    I2C_User_SendByte(wData);
    I2C_User_RecvAck();
    I2C_User_Stop();
}

//MPU6050指定地址读寄存器
uint8_t MPU6050_ReadReg(uint8_t RegAddr){
    uint8_t rData=0x00;
    
    I2C_User_Start();
    I2C_User_SendByte(MPU6050_ADDRESS);
    I2C_User_RecvAck();//暂时不对应答位进行处理
    I2C_User_SendByte(RegAddr);
    I2C_User_RecvAck();
    
    I2C_User_Start();
    I2C_User_SendByte(MPU6050_ADDRESS | 0x01);
    I2C_User_RecvAck();//暂时不对应答位进行处理
    rData = I2C_User_RecvByte();
    I2C_User_SendAck(1);
    I2C_User_Stop();
    
    return rData;
}

//MPU6050初始化
void MPU6050_Init(void){
    //I2C初始化
    I2C_User_Init();
    //不复位、解除睡眠、不开启循环模式、温度传感器失能、选择陀螺仪x轴时钟
    MPU6050_WriteReg(MPU6050_REG_PWR_MGMT_1,0x01);
    //没有开启循环模式
    MPU6050_WriteReg(MPU6050_REG_PWR_MGMT_2,0x00);
    //采样率10分频
    MPU6050_WriteReg(MPU6050_REG_SMPLRT_DIV,0x09);
    //不使用外部同步、DLPF设置等级6
    MPU6050_WriteReg(MPU6050_REG_CONFIG,0x06);
    //陀螺仪:自测失能、满量程±500°/s-000_01_000
    MPU6050_WriteReg(MPU6050_REG_GYRO_CONFIG,0x08);
    //加速度计:自测失能、满量程±2g、失能运动检测-000_00_000
    MPU6050_WriteReg(MPU6050_REG_ACCEL_CONFIG,0x00);
}

//获取MPU6050的ID号
uint8_t MPU6050_GetID(void){
    return MPU6050_ReadReg(MPU6050_REG_WHO_AM_I);
}

//获取MPU6050的传感器数据
void MPU6050_GetData(MPU6050_DataStruct* MPU6050_Data){
    uint16_t sensor_byte_L, sensor_byte_H;
    //获取加速度计数据
    sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_ACCEL_XOUT_H);
    sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_ACCEL_XOUT_L);
    MPU6050_Data->Acce_X = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);
    sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_ACCEL_YOUT_H);
    sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_ACCEL_YOUT_L);
    MPU6050_Data->Acce_Y = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);
    sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_ACCEL_ZOUT_H);
    sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_ACCEL_ZOUT_L);
    MPU6050_Data->Acce_Z = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);
    //获取陀螺仪数据
    sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_GYRO_XOUT_H);
    sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_GYRO_XOUT_L);
    MPU6050_Data->Gyro_X = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);
    sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_GYRO_YOUT_H);
    sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_GYRO_YOUT_L);
    MPU6050_Data->Gyro_Y = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);
    sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_GYRO_ZOUT_H);
    sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_GYRO_ZOUT_L);
    MPU6050_Data->Gyro_Z = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);
    //获取温度传感器数据
    sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_TEMP_OUT_H);
    sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_TEMP_OUT_L);
    MPU6050_Data->Temp = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);
}

编程感想:

  1. 扫描总线上所有设备的地址。进行“发送从机地址+0、接收应答、发送停止位”的操作,就可以通过应答位来判断当前地址的从机是否挂在总线上。若遍历所有的从机地址,那边可以实现扫描总线设备的操作。不过要注意遍历时地址只是前7位,第8位的R/W保持为0(写操作)不变,否则一旦交出总线控制权,从机可能会影响后续的操作。
  2. 更改MPU6050模块地址。飞线接AD0引脚,即可改变从机地址的最低位(默认为0)。
  3. C语言函数体返回多个参数。由于C语言函数体只能返回一个参数,所以要想返回多个参数,有以下三种方法:
  1. 定义外部数组变量。
  2. 返回数组指针。
  3. 定义结构体。这个看起来最简洁,所以本文定义结构体来存储数据。

10.4 I2C外设简介

总的来说,由于串口是异步时序,对时序要求极其严格,所以几乎不会软件模拟串口,基本上一边倒的采用硬件实现串口通信。而对于I2C通信来说,尽管用硬件电路可以减轻CPU负担,但是硬件I2C总线数量有限、引脚固定等,也有很多限制;而由于I2C是同步时序,软件模拟I2C足够简单且灵活,所以还是有许多场合都使用软件I2C进行通信。若只是简单应用I2C读写数据,使用软件模拟更加灵活;若对时序要求高,可以使用硬件I2C。本节介绍硬件实现I2C的原理。

STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担,还兼具可以实现完整的多主机通信、时序波形规整、通信速率快等优点,功能非常强大。具体参数如下:

  • 支持多主机模型(固定多主机/可变多主机)。stm32是按照“可变多主机”的模式设置硬件电路的,也就是说,需要自己声明自己是主机。
  • 支持7位/10位地址模式。10位地址发送两字节寻址,高5位固定为11110(这种开头不会在7位地址中出现),低10位作为地址(第一个字节的最低位还是R/W)。
  • 支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)。只要不超过最大频率,多少都可以。
  • 支持DMA。多字节传输时可提高传输效率,如指定地址 读/写 多字节。若只有几个字节也没必要配置DMA。
  • 兼容SMBus(System Management Bus)协议。SMBus是基于I2C总线改进而来的,主要用于电源管理系统中。本节用不到。
  • STM32F103C8T6 硬件I2C资源:I2C1、I2C2
表10-1 I2C引脚复用表
引脚I2C1I2C2
SCLPB6/PB8PB10
SDAPB7/PB9PB11
SMBAIPB5PB12

注:斜杠后引脚定义表示引脚重映射

图10-8 I2C功能框图

SDA部分:

  • 发送数据过程:首先将数据写入“数据寄存器DR”,当没有移位时,DR中的数据就被转运到“数据移位寄存器”中,并同时置状态寄存器的TXE位为1(发送寄存器空)。此时就可以继续向DR中写入数据。

  • 接收数据流程:将数据一位一位的写入“数据移位寄存器”,当一个字节的数据收齐后,将会将数据整体从“数据移位寄存器”转运到“数据寄存器DR”,并同时置标志位RXNE(接收寄存器非空)。此时就可以将数据从DR中读出。

  • 比较器和地址寄存器(用不到):从机模式使用。由于stm32的I2C是基于可变多主机模型设计的,不进行通信时默认为“从机”,于是“自身地址寄存器”就用于存放从机地址,可以由用户指定。“双地址寄存器”存储的也是从机地址,于是stm32就可以同时响应两个从机地址。

  • 数据校验模块(用不到):当发送一个多字节的数据帧时,“帧错误校验计算”可以硬件自动执行CRC校验计算,得到一个字节的校验位附加在数据帧的最后。同样的,当接收的校验字节和CRC计算结果不匹配时,就会置校验错误标志位。

评价:整个数据收发流程类似于“串口通信”,只不过串口是全双工通信,收发电路独立;I2C是半双工通信,收发共用一个电路。

SCL部分:

  • 时钟控制:控制SCL线,具体细节无需了解。
  • 时钟控制寄存器CCR:控制“时钟控制”电路执行相应的功能。
  • 控制逻辑电路:写入“控制寄存器”,可以对整个电路进行控制;读取“状态寄存器”,可以得知整个电路的工作状态。
  • 中断:内部某些事情比较紧急时,可以申请中断。
  • DMA请求与响应:在进行很多字节的收发时,可以配合DMA来提高效率。
图10-9 I2C基本结构图

初始化I2C外设时注意四部分:

  1. RCC时钟:现实需要初始化GPIO、I2C两个外设的时钟。
  2. GPIO外设:都要配置成复用开漏输出模式。“复用”是交给片上外设来控制,“开漏输出”是I2C协议要求的端口配置。
  3. I2C外设:老套路了,先定义结构体,再调用一个I2C_Init即可。
  4. 开关控制:使能I2C外设。

注:上图简化成“一主多从模型”,所以SCL只有时钟输出。“多主机模型”时,SCL也会有输入。

I2C外设初始化完成以后,还要考虑引脚的时序如何变化,才能使得硬件I2C真正的能收发数据。下面介绍硬件I2C的操作流程,编写I2C的读写时序需要参考下面由st公司给出的时序图
与软件I2C中CPU可以实时控制引脚变化不同,硬件I2C使用片上外设I2C来控制引脚变化,CPU不具有引脚的直接控制权,所以只能靠读取标志位来判断当前时序进行到哪一步了。参考手册中给出了“从机发送”、“从机接收”、“主机发送”、“主机接收”。由于本节是“一主多从”模式,所以只介绍“主机发送”、“主机接收”:

图10-10 “主机发送”时序

首先需要说明两点:

  1. “EVx”事件:刚才提到CPU靠读取标志位来获取当前的时序状态,但有的状态会产生多个标志位,所以EVx事件就是组合了多个标志位的一个大标志位。
  2. 关于应答位:I2C库函数发送数据自带接收应答、接收数据自带发送应答,所以用户想要发送应答就需要在发送数据时同时配置是否发送应答(有专门的库函数控制应答使能),而想要接收应答就直接读取相应的EVx事件(有专门的库函数)即可。

下面介绍发送时序:

  • 起始位、终止位:有相应的库函数可以直接调用。
  • EV5:起始位发送完毕,可以写入从机地址。
  • 地址:检测到EV5后,就将数据放到DR上(SB自动清零),发送完成后硬件自动检测应答,无应答则置应答失败的标志位,此时可以用中断来提醒。
  • EV6:从机地址发送完毕,可以写入新的数据到DR寄存器中。
  • EV8_1:只是表明了存在这么一个过渡状态,实际上用不到这个事件。
  • EV8:连续发送数据时,一旦检测到EV8事件,就可以再次写入数据。
  • EV8_2:发送最后一个字节数据之后,就等这个事件发生,就可以产生停止位了。
图10-11 “主机接收”时序

注意没有给出“指定地址读”的时序,需要用户自行组合,也就是先用“主机发送”时序来一段“哑写”,然后再接着进行上图所示的“主机接收”时序。

  • “哑写”时序:按照“主机发送”时序,发送完寄存器地址后,直接等待EV8_2事件发生,然后直接产生“起始位”,接上下面的时序。
  • 起始位、终止位:有相应的库函数可以直接调用。
  • EV5:起始位发送完毕,可以写入从机地址。
  • EV6:注意检测到这个事件后,要配置是否发送应答,因为这决定了是否是连续接收数据。若不发送应答,还要直接产生停止条件(硬件I2C不会立即产生);若发送应答,则需等待EV7。
  • EV7事件:表示当前可以读取数据。若上述没有发送应答,那么到这里时序结束。
图10-12 软件/硬件I2C波形对比

上图给出了使用 软件I2C/硬件I2C 的波形的不同:

  1. 引脚电平变化:两者相同,对应的数据也相同。
  2. 时钟线的规整程度:硬件I2C更加规整,每个时钟的周期、占空比都非常一致;软件I2C由于是在操作引脚之后,才添加了软件延迟(有可能多,有可能少),所以软件时序的时钟周期、占空比可能不规整(但是同步时序,不规整也无所谓)。
  3. SDA数据变化时机:协议规定SCL低电平写、SCL高电平读,但是一般要求保证尽早的原则,所以理想状态下,可以直接认为是SCL下降沿写、SCL上升沿读。软件I2C在下降沿后,因为操作端口之后有一些延时,所以等了一会才进行写入操作;而硬件I2C中,数据写入都是紧贴下降沿、数据读出都是紧贴上升沿。这一点,在图中红色箭头处体现的非常明显。
  4. 关于SCL占空比:ST公司规定时钟速率<=100kHz为标准通信,100kHz<=通信速率<=400kHz为高速通信。标准通信时,硬件I2C占空比固定为1:1;高速通信时,占空比可以选择SCL 低电平:高电平 = 16:9/2:1。之所以SCL低电平时间更长,是因为SCL、SDA引脚为都开漏输出,上拉为弱上拉,上升沿平缓时间长;下拉为强下拉,下降沿陡峭时间短。为了保证SCL低电平时,SDA有足够的时间改变数据,所以设置SCL低电平占空比更大。具体的高速波形,可以参考UP主讲解:“P35[10-5]硬件I2C读写MPU6050,20:10~25:16”。

10.5 实验:硬件I2C读写MPU6050

需求:与软件I2C读写MOU6050相同,读出MPU6050数据并显示在OLED上。

注:根据引脚定义表,选择I2C2的PB10、PB11进行通信,于是引脚选择也就和软件I2C相同。

接线图与上一个实验——“软件I2C读写MOU6050”一致,但最底层的代码是直接调用的I2C库函数,所以代码调用稍有不同:

图10-13 软件读写MPU6050-代码调用(非库函数)

下面是代码展示main.cMPU6050.h与源文件相同,MPU6050.c中只是将涉及到I2C引脚变化的操作都替换成库函数了,具体的变化过程参考图10-10“主机发送”时序、图10-11“主机接收”时序。
- MPU6050.c

#include "stm32f10x.h"                  // Device header
#include "MPU6050.h"

//定义从机地址、寄存器地址
#define MPU6050_ADDRESS           0xD0
#define MPU6050_REG_SMPLRT_DIV    0x19
#define MPU6050_REG_CONFIG        0x1A
#define MPU6050_REG_GYRO_CONFIG   0x1B
#define MPU6050_REG_ACCEL_CONFIG  0x1C

#define MPU6050_REG_ACCEL_XOUT_H  0x3B
#define MPU6050_REG_ACCEL_XOUT_L  0x3C
#define MPU6050_REG_ACCEL_YOUT_H  0x3D
#define MPU6050_REG_ACCEL_YOUT_L  0x3E
#define MPU6050_REG_ACCEL_ZOUT_H  0x3F
#define MPU6050_REG_ACCEL_ZOUT_L  0x40
#define MPU6050_REG_TEMP_OUT_H    0x41
#define MPU6050_REG_TEMP_OUT_L    0x42
#define MPU6050_REG_GYRO_XOUT_H   0x43
#define MPU6050_REG_GYRO_XOUT_L   0x44
#define MPU6050_REG_GYRO_YOUT_H   0x45
#define MPU6050_REG_GYRO_YOUT_L   0x46
#define MPU6050_REG_GYRO_ZOUT_H   0x47
#define MPU6050_REG_GYRO_ZOUT_L   0x48

#define MPU6050_REG_PWR_MGMT_1    0x6B
#define MPU6050_REG_PWR_MGMT_2    0x6C
#define MPU6050_REG_WHO_AM_I      0x75

//将I2C_CheckEvent封装成具有超时退出机制的WaitEvent函数
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT){
    uint32_t Timeout;
    Timeout = 100000;
    while(I2C_CheckEvent(I2Cx, I2C_EVENT)==ERROR){
        Timeout--;
        if(Timeout==0){
            //超时退出操作,如打印错误日志、进行系统复位、紧急停机等
            break;
        }
    }
}

//MPU6050指定地址写寄存器
void MPU6050_WriteReg(uint8_t RegAddr, uint8_t wData){
    //1.发送起始位
    I2C_GenerateSTART(I2C2, ENABLE);
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);//等待EV5事件发生
    //2.发送从机地址
    I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
    //不需要接收应答,库函数发送数据自带接收应答、接收数据自带发送应答
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);//等待EV6事件发生
    //3.发送寄存器地址
    I2C_SendData(I2C2, RegAddr);//这一步无需等到EV8_1事件发生
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);//等待EV8事件发生
    //4.发送数据
    I2C_SendData(I2C2, wData);
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);//等待EV8_2事件发生
    //5.发送停止位
    I2C_GenerateSTOP(I2C2, ENABLE);
    
}

//MPU6050指定地址读寄存器
uint8_t MPU6050_ReadReg(uint8_t RegAddr){
    uint8_t rData=0x00;
    
    //一、先进行一段“哑写”,指定要读取的地址
    //1.发送起始位
    I2C_GenerateSTART(I2C2, ENABLE);
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);//等待EV5事件发生
    //2.发送从机地址
    I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);//等待EV6事件发生
    //3.发送寄存器地址
    I2C_SendData(I2C2, RegAddr);//这一步无需等到EV8_1事件发生
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);//等待EV8_2事件发生    
    
    //二、再按照主机接收(当前地址读)的流程来
    //4.再次发送起始位
    I2C_GenerateSTART(I2C2, ENABLE);
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);//等待EV5事件发生
    //5.发送从机地址
    I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);//等待EV6事件发生
    //6.按照手册,需要首先设置ACK=0、并且产生停止条件
    I2C_AcknowledgeConfig(I2C2, DISABLE);    
    I2C_GenerateSTOP(I2C2, ENABLE);
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);//等待EV7事件发生
    //7.接收数据
    rData = I2C_ReceiveData(I2C2);
    I2C_AcknowledgeConfig(I2C2, ENABLE);//将ACK改回默认的使能配置,方便指定地址收多个字节
    
    return rData;
}

//MPU6050初始化
void MPU6050_Init(void){
    //配置I2C外设
    // 1.开启RCC时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    // 2.GPIO口初始化位复用开漏输出
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;//复用开漏输出
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    // 3.使用结构体对整个I2C进行配置
    I2C_InitTypeDef I2C_InitStructure;
    I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
    I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;//默认给应答,后续可以单独修改
    I2C_InitStructure.I2C_ClockSpeed = 200000;//200kHz
    //时钟占空比只有在频率大于100kHz(快速模式)才起作用,标准模式固定为1:1
    //因为弱上拉导致上升沿时间比时钟下降沿更长,为了在快速模式下也准确传输数据,就需要给数据变化的区间(SCL低电平)更长的时间
    I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;//选择SCL低电平:高电平=2:1
    I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;//stm32作为从机,有几位地址
    I2C_InitStructure.I2C_OwnAddress1 = 0x00;//stm32作为从机,从机地址是多少
    I2C_Init(I2C2, &I2C_InitStructure);
    // 4.使能I2C
    I2C_Cmd(I2C2, ENABLE);
    
    //不复位、解除睡眠、不开启循环模式、温度传感器失能、选择陀螺仪x轴时钟
    MPU6050_WriteReg(MPU6050_REG_PWR_MGMT_1,0x01);
    //没有开启循环模式
    MPU6050_WriteReg(MPU6050_REG_PWR_MGMT_2,0x00);
    //采样率10分频
    MPU6050_WriteReg(MPU6050_REG_SMPLRT_DIV,0x09);
    //不使用外部同步、DLPF设置等级6
    MPU6050_WriteReg(MPU6050_REG_CONFIG,0x06);
    //陀螺仪:自测失能、满量程±500°/s-000_01_000
    MPU6050_WriteReg(MPU6050_REG_GYRO_CONFIG,0x08);
    //加速度计:自测失能、满量程±2g、失能运动检测-000_00_000
    MPU6050_WriteReg(MPU6050_REG_ACCEL_CONFIG,0x00);
}

//获取MPU6050的ID号
uint8_t MPU6050_GetID(void){
    return MPU6050_ReadReg(MPU6050_REG_WHO_AM_I);
}

//获取MPU6050的传感器数据
void MPU6050_GetData(MPU6050_DataStruct* MPU6050_Data){
    uint16_t sensor_byte_L, sensor_byte_H;
    //获取加速度计数据
    sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_ACCEL_XOUT_H);
    sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_ACCEL_XOUT_L);
    MPU6050_Data->Acce_X = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);
    sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_ACCEL_YOUT_H);
    sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_ACCEL_YOUT_L);
    MPU6050_Data->Acce_Y = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);
    sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_ACCEL_ZOUT_H);
    sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_ACCEL_ZOUT_L);
    MPU6050_Data->Acce_Z = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);
    //获取陀螺仪数据
    sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_GYRO_XOUT_H);
    sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_GYRO_XOUT_L);
    MPU6050_Data->Gyro_X = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);
    sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_GYRO_YOUT_H);
    sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_GYRO_YOUT_L);
    MPU6050_Data->Gyro_Y = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);
    sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_GYRO_ZOUT_H);
    sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_GYRO_ZOUT_L);
    MPU6050_Data->Gyro_Z = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);
    //获取温度传感器数据
    sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_TEMP_OUT_H);
    sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_TEMP_OUT_L);
    MPU6050_Data->Temp = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);
}

编程感想:

  1. 关于应答位。I2C库函数发送数据自带接收应答、接收数据自带发送应答,所以用户想要发送应答就需要提前配置,想要接收应答也就是读取相应的标志位。
  2. 关于起始条件I2C_GenerateSTART。这个起始条件会等待前一个字节发送完毕后才产生,并不会直接截断传送。所以发送数据后,即使等待EV8事件发生,就发送起始条件,也没问题。但是保险起见,还是等待EV8_2事件,再发送起始条件。
  3. 超时退出机制。程序中有大量的等待事件的while循环,万一有某个事件没有产生,程序就会卡死,非常危险,所以要设置超时退出机制。也不用很高大上,就是一个简单的计数退出就行。
  4. 奇怪的bug。前天晚上写好了代码,结果下载以后一看,陀螺仪、加速度计的XYZ分别都是相同的值,但是不知道bug在哪,再加上还有别的事的就先走了,打算今早再看,结果代码没变,一下载显示正常!啊这…一观测就消失,量子bug是吧😂。
  • 7
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

虎慕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值