【概述】0 什么是智能系统

【阶段一】1 PlatformIO环境配置和GPIO

【阶段一】2 PWM与电机控制

【阶段一】3 外部中断和定时器中断

【阶段一】4 串口与串口中断

【阶段一】5 I2C与MPU6050模块

【阶段一】6 基础小车设计

【阶段二】7 智能系统概述

5.1i2c概述

I2C(Inter-Integrated Circuit)是一种串行通信协议,也称为IIC(Inter-IC)总线,通常用于连接微控制器和各种外部设备,如传感器、存储器、显示屏等。

I2C总线由两根线构成:串行数据线(SDA)和串行时钟线(SCL)。

在I2C总线上,可以同时连接多个设备,每个设备都有一个唯一的7位地址,用于在总线上唯一识别该设备。通过发送起始条件和设备地址,主设备(通常是微控制器)可以选择与特定设备通信。通信过程中,数据通过SDA线传输,时钟信号通过SCL线同步传输。

特点:

两根制总线,有效降低了连接的复杂性。

可变的时钟速率:标准模式100khz,快速模式400khz,高速模式3.4mhz

低功耗

由于存在时钟线,所以单片机随时可以暂停传输去处理中断的事情。

i2c的两种工作模式

一主多从模式:只有一个主设备(通常是微控制器)控制整个通信过程,可以与多个从设备进行通信。主设备负责发送起始条件、设备地址以及读写指令,从而选择特定的从设备进行数据交换。其他从设备则根据其地址是否匹配来决定是否响应主设备的通信请求。

多主多从模式: 在多主多从模式下,除了多个从设备之外,还可以有多个主设备连接到同一条I2C总线上。不同主设备之间会通过仲裁机制来协调总线的访问权,避免通信冲突和数据丢失。当一个主设备想要访问总线上的某个从设备时,它必须首先获得总线的控制权,其他主设备则处于被动监听状态。

此次培训主要探讨一主多从模式。

IIC中,任何时候都是主机(CPU)完全掌控SCL线,在空闲状态下,主机可以主动发起对SDA线的控制,只有在从机发送数据(主机发送读取从机命令之后)和从机应答的时候,主机才会转交SDA的控制权给从机。

5.2,i2c数据传输流程

以单片机向从设备写数据为例:

这是一个标准的写数据帧:

可以看到一个标准的写数据帧由一个start起始信号,从机设备的七位地址,写数据位,从机的应答信号,从机的寄存器八位地址,又有一个 从机的应答信号,八位数据,从机的应答信号和stop停止位组成。

可以理解这一帧的意思为:向地址为1010000的设备写数据,在从设备地址为00000000的寄存器中写入00001111这一数据。

以单片机向从设备读取数据为例

这是一个标准的读数据帧:

可以看到,同样由start信号开始,接着是设备七位地址,第八位为写数据位,应答信号,寄存器地址,应答信号,又有一个start起始信号,又是设备地址,接下来的这一位与刚刚的不同,为1,代表要读取数据,紧跟着的是从设备要接收的数据,应答位,此时5应答位为一,代表从机结束数据读取,最后stop停止。

这一帧可以理解为:单片机读取设备地址为1010000的设备的寄存器里的数据。

但是到底是怎么传输0或者1的呢,又或者读取到0或者1的呢?

这就不得不提及i2c总线上SCL与SDA两条线了。SCL叫做串行时钟线,sda叫做串行数据线。在他俩的共同作用下,实现的。

首先介绍逻辑0与逻辑1的概念。

首先,只看SCL高电平的时候,读取sda的状态,为低电平就代表逻辑0,为高电平就代表逻辑1;因为在SCL为高电平读取的数据才是有效的。

那么,我们来看看下面这张图读取的数据是什么?

只看SCL为高电平的时刻,读取到sda的数据是1010000

5.3,i2c的时序

以下面的一个的i2c时序图为例

步骤1:主设备将产生一个开始信号,向其他设备发出信号,开始监听总线并准备接收数据。当发送启动信号条件时,总线将进入繁忙状态,其中当前数据传输仅限于选定的主设备和从设备。只有在产生停止条件后,总线才会被释放并再次处于空闲模式。

步骤2:主设备向每个设备发送一个7位设备地址加上一位读写数据帧。该位还将指示下一个数据传输的方向。0 = 主设备向从设备写入数据。1 = 主设备读取数据到从设备。

步骤3:每个从机将主机发送的地址与自己的地址进行比较。成功匹配地址的从设备通过拉低 SDA 线返回 ACK 位。

步骤4:当主设备收到从设备的确认信号后,开始发送或接收数据。下图是向指定设备传输数据的过程图。

步骤5:接收设备发送完每个数据帧后,向发送方返回另一个ACK位,以确认该帧已成功接收,然后发送方继续发送该数据帧,以此类推。

步骤6:当数据传输完成后,主设备会向其他设备发出停止信号,释放总线,总线进入空闲状态。

包括了起始终止信号、发送与接收、应答等,把他们拆开就分别有了i2c的基本时序单元。

1.起始条件与终止条件:

起始条件

刚开始,两条总线都处于高电平,当主机进行收发时,先让SDA产生一个下降沿,当从机捕获到SCL高电平,SDA下降沿时,会进行自身的复位,等待主机召唤。

在SDA下降沿之后,主机会让SCL变成低电平,一方面是占用这个总线,另一方面是为了进行拼接。因为除了起始和终止条件,每个时序单元的SCL都是低电平开始低电平结束。

终止条件:

相反的,SCL处于高电平期间,SDA产生上升沿,从低电平转变为高电平,代表终止信号。

2.发送与接收一个字节:

只在SCL低电平期间,允许改变SDA的电平。

发送:

序号1:

起始条件之后第一个字节必须由主机发送,在SCL低电平时,主机如果想发送0就把SDA电平拉低,想发送1就是拉高SDA电平。在这一数据位放好时,主机松手时钟线SCL,SCL回归高电平。当SCL为高电平时,从机开始读取SDA上的数据,注意此时的SDA电平信号不能改变,否则就变为了起止条件。

序号2:

SCL处于高电平后,从机要尽快读取SDA上的数据,一般在SCL上升沿时刻,从机已经完成了SDA的读取,因为SCL的控制完全由主机主导控制,从机并不知道SCL会持续多久,所以尽快读取。

之后主机拉低SCL,在下降沿期间,把下一位数据放在SDA线上,不过主机完全主导SCL,不需要着急将数据写入,只需要在SCL低电平期间完成传输。

于时钟线进行同步,所以如果主机一个字节发送就突然进入中断,那么SCL低电平就会被不断拉长,SDA与SCL的电平暂停变化,等待处理结束继续操作。

接收:

虚线代表从机电平的变化。

原理与发送一致,都是高位先行,只是低电平变成从机发送数据,高电平主机读数据。

主机在接收之前,需要释放SDA。可以理解为所有设备包括主机都处于输入模式,谁需要输出就将SDA线电平拉低。

那么如何确保数据的传送与接收正常运行了呢?

这样就引入了I2C的发送应答和接收应答机制了

3.发送应答和接收应答:

I2C通讯的发送和应答机制是主从设备之间通过发送数据和应答信号来确认通讯的进行,确保数据的正确传输。

发送和应答机制和发送与接收字节的原理一样,只是变成了发送一位数据。

如接收应答所示,如果主机发送完字节后,要确定从机是否接收到数据,那么就要将主机切换为输入模式。此时主机立即释放SDA的控制权,从机立刻拉高SDA电平获得控制权,所以主机收到的是从拉低后的电平,这就是数据0表示应答的原因。

发送应答也是同理,如果主机非应答说明主机不想要这个数据,从机就要交出SDA控制权。

在时序图上,主机拉高SDA电平的瞬间就被从机拉低,所以这个过程呈现出一直是低电平的现象。

在了解了i2c原理之后,我们拿出这节课第二个重要的器件——MPU6050

5.4:MPU6050——简单介绍

MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景

3轴加速度计(Accelerometer):测量X、Y、Z轴的加速度

3轴陀螺仪传感器(Gyroscope):测量X、Y、Z轴的角速度

  • Yaw(偏航):在MPU6050中是绕Z轴旋转
  • Pitch(俯仰):在MPU6050中是绕Y轴旋转
  • Roll(翻滚): 在MPU6050中是绕X轴旋转

MPU6050的I2C七位从机地址为:1101000(0x68)

MPU6050寄存器介绍:

0x19:采样频率分频器寄存器:相当于刷新率,越高采集速度越快

0x1A:配置寄存器:DLPF低通滤波器,让输出数据更加平滑

0x1B:陀螺仪配置寄存器:高三位是自测使能位,后两位为最大量程选择位

0x1C: 加速度配置寄存器:高三位是自测使能位,后两位为最大量程选择位

0x3B—0x48:加速度寄存器、温度寄存器、陀螺仪寄存器

0x6B: 电源管理寄存器:第六位sleep上电默认是1睡眠模式,我们需要写0,使之工作

0x75:器件地址寄存器

5.5使esp32与MPU6050进行通讯

1.完成基本时序单元的代码

#include <Arduino.h>
#include <U8g2lib.h>

/*i2c初始化
配置引脚8、9为开漏输出模式,默认给8,9引脚高电平*/
void myi2c_Init(void){
    pinMode(8,OUTPUT_OPEN_DRAIN);
    pinMode(9,OUTPUT_OPEN_DRAIN);
    digitalWrite(8,HIGH);
    digitalWrite(9,HIGH);
}

/*配置8号引脚为scl时钟线,传输bitvalue对其进行电平控制*/
void myi2c_w_scl(uint8_t bitvalue){
    digitalWrite(8,bitvalue);
}

/*配置9号引脚为sda数据线,传输bitvalue对其进行电平控制*/
void myi2c_w_sda(uint8_t bitvalue){
    digitalWrite(9,bitvalue);
}

/*读取sda引脚电平函数,
返回值为sda的电平,0-1*/
uint8_t myi2c_r_sda(void){
    uint8_t bitvalue;
    bitvalue = digitalRead(9);
    return bitvalue;
}

/*i2c起始*/
void myi2c_start(void){
    myi2c_w_sda(1);
    myi2c_w_scl(1);
    myi2c_w_sda(0);
    myi2c_w_scl(0);
}

/*i2c停止*/
void myi2c_stop(void){
    myi2c_w_sda(0);
    myi2c_w_scl(1);
    myi2c_w_sda(1);
}

/*i2c发送一个字节
byte;要发送的一个字节的数据,范围:0x00-0xFF*/
void my_i2c_sendbyte(uint8_t byte){
    uint8_t i;
    for(i = 0;i<8;i++){
        myi2c_w_sda(byte & (0x80>>i));
        myi2c_w_scl(1);
        myi2c_w_scl(0);
    }
}

/*i2c接收一个字节
返回值:接收到的一个字节数据,范围:0x00-0xFF*/
uint8_t myi2c_receivebyte(void){
    uint8_t i,byte = 0x00;
    myi2c_w_sda(1);
    for(i = 0;i<8 ;i++){
        myi2c_w_scl(1);
        if (myi2c_r_sda() == 1){
            byte |=(0x80>>i);
        }
        myi2c_w_scl(0);
    }
    return byte;
}

/*i2c发送应答信号
byte:要发送的应答位,范围:0-1,0表示应答,1表示非应答*/
void myi2c_sendack(uint8_t ackbit){
    myi2c_w_sda(ackbit);
    myi2c_w_scl(1);
    myi2c_w_scl(0);
}

/*i2c接收应答
返回值ackbit,接收到的应答位,范围0-1,0表示应答,1表示非应答*/
uint8_t myi2c_receiveack(void){
    uint8_t ackbit;
    myi2c_w_sda(1);
    myi2c_w_scl(1);
    ackbit = myi2c_r_sda();
    myi2c_w_scl(0);
    return ackbit;
}

接下来我们先用串口监视器验证一下是否正确:

创建一下setup函数:

uint8_t ack=1;    //ack应答位为0,所以为了验证是否回应,将ack默认为1.
void setup(void){
    Serial.begin(115200);
    myi2c_Init();
    myi2c_start();
    myi2c_sendbyte(0xD0);//MPU6050的七位地址为0x68,加上读写位0,合在一块就是0xD0
    ack = myi2c_receiveack();
    myi2c_stop();
    Serial.print(ack);  
}

可以看到串口接收到的数据为0,代表esp32已经发送并且mpu6050已经接收单片机发送的数据了。

好,我们再从反方向验证一下,就是把要发送的从机地址改为0xA0,看看串口输出的数是几。

可以看到,因为我们把从机地址改了,没有一个从机与之匹配。所以输出的是1。

接下来,建立在myi2c时序之上的MPU6050模块

#include <Arduino.h>
#include <myi2c.h>
#define MPU6050_Address 0xD0
void MPU6050_Init(void){
    myi2c_Init();/*因为建立在myi2c时序之上,所以可以直接初始化,以避免在
    调用此模块时,还要单独再对i2c初始化*/
}
/*指定寄存器地址写数据
RegAddress:寄存器地址、data:要写入的数据*/
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();
}
/*指定寄存器地址读数据
RegAddress:寄存器地址
返回值data:读出的数据*/
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);
    myi2c_receiveack();
    data =  myi2c_receivebyte();
    myi2c_sendack(1);
    myi2c_stop(); 
     
    return data;  
}

接下来我们就试着读出MPU6050的其中的一个寄存器数据,查看我们的芯片手册,可以看到“WHO_AM_I”0x75寄存器存储的是代表着MPU6050的地址,对它进行读取,下面是主函数的部分:

#include <Arduino.h>
#include <U8g2lib.h>
#include <MPU6050.h>
void setup(void){
    uint8_t ack = 0;  //定义变量储存读出的数据
    Serial.begin(115200);//串口波特率设置
    MPU6050_Init();
    ack = MPU6050_ReadReg(0x75);    //读取0x75寄存器数据
    Serial.print(ack,HEX);        //使用串口打印出16进制数据
    
}
void loop(void){

}

既然没问题了,那么就可以对MPU6050的其他寄存器的数据进行读取:

由于我们对MPU6050所需要读取的数据有14个,分别是xyz轴加速度,温度,xyz轴角速度数据。

用来对其配置的寄存器有4个,分频器,配置寄存器(低通滤波器),陀螺仪配置寄存器,加速度配置寄存器。

还有电源管理寄存器两个,地址寄存器。

总共21个寄存器地址,我们在编写程序时,肯定不会一个地址一个地址的输入,那样做极容易输入错误,所以我们可以用一个.h文件对所有需要的寄存器地址进行宏定义,方便我们理解。

下图是我对MPU6050我们所需要用的寄存器进行的宏定义,

我们刚刚在MPU6050_Init();函数中只写了myi2c_Init();但是我们对MPU6050的初始化还没有配置,所以继续在函数中配置MPU6050的寄存器。

有分频器,配置寄存器(低通滤波器),陀螺仪配置寄存器,加速度配置寄存器。还有电源管理寄存器两个(用于解除MPU6050的睡眠模式)

void MPU6050_Init(void){
    myi2c_Init();
    /*MPU6050寄存器初始化,需要对照MPU6050手册的寄存器描述配置,此处仅配置了部分重要的寄存器*/
    MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);     //电源管理寄存器1,取消睡眠模式,选择时钟源为X轴陀螺仪
    MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);     //电源管理寄存器2,保持默认值0,所有轴均不待机
    MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);     //采样率分频寄存器,配置采样率
    MPU6050_WriteReg(MPU6050_CONFIG, 0x06);         //配置寄存器,配置DLPF
    MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);    //陀螺仪配置寄存器,选择满量程为±2000°/s
    MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);   //加速度计配置寄存器,选择满量程为±16g
}

OK,接下来我们来写获取xyz轴的加速度、温度与xyz轴角速度的数据读取函数:

void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, 
                                                int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
        uint8_t DataH, DataL;                               //定义数据高8位和低8位的变量
        
        DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);      //读取加速度计X轴的高8位数据
        DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);      //读取加速度计X轴的低8位数据
        *AccX = (DataH << 8) | DataL;                       //数据拼接,通过输出参数返回
        
        DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);      //读取加速度计Y轴的高8位数据
        DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);      //读取加速度计Y轴的低8位数据
        *AccY = (DataH << 8) | DataL;                       //数据拼接,通过输出参数返回
        
        DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);      //读取加速度计Z轴的高8位数据
        DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);      //读取加速度计Z轴的低8位数据
        *AccZ = (DataH << 8) | DataL;                       //数据拼接,通过输出参数返回
        
        DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);       //读取陀螺仪X轴的高8位数据
        DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);       //读取陀螺仪X轴的低8位数据
        *GyroX = (DataH << 8) | DataL;                      //数据拼接,通过输出参数返回
        
        DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);       //读取陀螺仪Y轴的高8位数据
        DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);       //读取陀螺仪Y轴的低8位数据
        *GyroY = (DataH << 8) | DataL;                      //数据拼接,通过输出参数返回
        
        DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);       //读取陀螺仪Z轴的高8位数据
        DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);       //读取陀螺仪Z轴的低8位数据
        *GyroZ = (DataH << 8) | DataL;                      //数据拼接,通过输出参数返回
}

回到主函数

#include <Arduino.h>
#include <U8g2lib.h>
#include <MPU6050.h>
int16_t AX, AY, AZ, GX, GY, GZ; 
void setup(void){
    Serial.begin(115200);
    MPU6050_Init();
}
void loop(void){
    MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);      //获取MPU6050的数据
    Serial.print(AX);
    Serial.print(",");
    Serial.print(AY);
    Serial.print(",");
    Serial.println(AZ);
    Serial.print(GX);
    Serial.print(",");
    Serial.print(GY);
    Serial.print(",");
    Serial.println(GZ);
    delay(500);
}

打开串口监视器,就能够看到数据在不断地变化了。

我们来计算一下,其中第一行第三个数为z轴加速度数据,怎么把它转化为我们能看懂的G值呢?

因为我们配置寄存器时,选择了满量程为16G,所以用当前数值除以满数值也就是32768(2的15次方)然后乘以最大量程(这里时加速度值所以对应16G),大家可以用这个计算公式算一下你当地的重力加速度是多少呢?

那么同样,角速度的计算同理也可以使用这个公式。

Avatar photo

作者 Gd

发表回复