S1-04 OLED和MPU6050

使用U8G2操作OLED

什么是U8G2

u8g2是一个基于monochrome lcd的嵌入式图形库。它可以通过多种接口与mcu交互,支持i2c、spi和并行模式等多种接口,以及若干种字体和绘图函数,可用于创建各种ui界面、菜单、文本等。其具有轻量级,灵活性高,支持多种格式的字体及占用空间小等优点,因此在很多嵌入式产品中得到了广泛的应用。

OLED12860

OLED12864是一种工业级的点阵屏,分辨率是128×64像素,通常使用IIC、串口、SPI接口进行控制。它具有高对比度、宽视角和低功耗等特点,使其在诸如机器人、测量仪器、移动终端等领域得到了广泛应用。它可以显示图像、文字和图形等信息,具有丰富的控制选项和灵活的使用方式,是一款非常实用的显示设备。

IIC通讯

IIC通讯协议(inter-integrated circuit)也叫I2C或I²C协议,是一种串行通信协议。使用该协议可以将一个主设备(即控制器)和多个从设备(即被控制器)连接到一个总线上进行数据传输。IIC通讯协议以同步方式传输数据,最常用的是两根线:SCL(时钟线)和SDA(数据线)。SCL由主设备输出,控制整个传输过程的时序;SDA则用来传输数据。在IIC通讯中,每个从设备都有一个唯一的地址,通过地址来识别从设备并进行通信。通信过程中还包括了读写操作、应答检测等基本动作。IIC通讯协议具有传输速率较慢、可靠性强、接口简单等特点,广泛应用于嵌入式系统、数字信号处理器、LCD驱动器等领域。

ESP32-S3利用GPIO矩阵可以将IIC的SCL和SDA映射到任意引脚上,也就意味着,我们所有的IIC都将采用软件IIC(有部分ESP32和其他芯片中采用的是硬件IIC,有固定的引脚,软IIC和硬IIC通讯原理是一样的,在ESP32系列芯片中不存在快慢问题)。

例程中,我们使用16号引脚充当SCL线,17号引脚充当SDA线,VCC连接开发板的VCC5或者VCC3.3(不同OLED电压不同),GND与开发板GND相连。

原理图

需要补全

我们选购的这款OLED的驱动芯片是SSD1306,大小是128×64,采用软I2C协议,所以屏幕初始化代码是:

U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, /*clock=*/SCL, /*data=*/SDA, /*reset=*/U8X8_PIN_NONE);

我中第一个参数是屏幕的旋转方向,第二个参数是SCL引脚编号,第三个参数是SDA引脚编号。
如果使用ESP32其他带有硬件IIC的开发板,可以使用。
我们用的是SH1106模块

U8G2_SSD1306_128X64_NONAME_F_FW_I2C u8g2(U8G2_R0, /*reset=*/U8X8_PIN_NONE);

初始化代码只需要 u8g2.begin(); 一行即可,在模拟器上运行较慢,但在实体开发板中速度非常快。

如果想在模拟器使用U8G2,则需要再模拟器 Library Manager中添加U8G2的类库。

代码共享位置:https://wokwi.com/projects/362501497983009793

#include <U8g2lib.h>
#include <Wire.h>
#define SCL 16
#define SDA 17
U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, /*clock=*/SCL, /*data=*/SDA, /*reset=*/U8X8_PIN_NONE);
// LED显示任务
void led_task(void *param_t){
  for(;;){
    u8g2.clearBuffer();         // 清理缓冲区
    u8g2.setFont(u8g2_font_ncenB08_tr); // 设置字体
    u8g2.drawStr(30, 10, "Hello ESP32");  // 打印文字
    u8g2.drawRFrame(5,20,118,44,5);
    u8g2.sendBuffer();          // 显示内容
    delay(100);
  }
}
void setup() {
  Serial.begin(115200);
  Serial.println("Hello, ESP32-S3!");
  u8g2.begin();
  printf("初始化完毕\n");
  xTaskCreate(led_task, "LED-DISP", 1024*4, NULL, 1, NULL);
  // xTaskCreatePinnedToCore(led_task, "LED-DISP", 1024*4, NULL, 1, NULL, 1);
}
void loop() {
  delay(100);
}

U8G2_SSD1306_128X64_NONAME_F_SW_I2C 是用软件IIC方式设置SSD1306芯片OLED,如果使用其他芯片或者是硬件IIC,可以选择其他初始化方式,具体可以参照 U8G的相关文档 U8G2官方文档

u8g2.begin(); 用于初始化U8G2,在初始化过程中需要等待一段时间(实际硬件中不会太长,模拟器性能不行,所以要等很长时间),在初始化未完成之前不要对U8G2进行任何操作。

u8g2.setFont(u8g2_font_ncenB08_tr); 用于设置字体,例程中把设置字体放在大循环中了,其实完全没有必要,设置一次即可,常用的字体如下:

  • u8g2_font_unifont_t_chinese2:一种支持 UTF-8 编码的中文、日文、韩文等字符集的字体。
  • u8g2_font_timB08_tf:8x8 像素的 Times New Roman 标准字体。
  • u8g2_font_ncenB08_tf:8x8 像素的 New Century Schoolbook 标准字体。
  • u8g2_font_profont10_tf:10x6 像素的 ProFont 字体。
  • u8g2_font_6x10_mf:6x10 像素的固定宽度字体。
  • u8g2_font_7x14_mr:7x14 像素的 Monospace Regular 字体。

u8g2.clearBuffer(); 用于清空缓冲区,这里不会对硬件进行操作,缓冲区只是放在内存中的一小块区域,缓冲区的内容最后要通过 u8g2.sendBuffer(); 方式发送到硬件中,这里才对硬件进行操作。
其他的绘制方法可以参考U8G2 API文档
OLED上使用U8g2中文库的指南

在多线程中使用U8G2

代码共享位置:https://wokwi.com/projects/362501497983009793

#include <U8g2lib.h>
#include <Wire.h>
#define SCL 16
#define SDA 17
U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, /*clock=*/SCL, /*data=*/SDA, /*reset=*/U8X8_PIN_NONE);
// LED显示任务
void led_task(void *param_t){
  for(;;){
    u8g2.clearBuffer();         // 清理缓冲区
    u8g2.setFont(u8g2_font_ncenB08_tr); // 设置字体
    u8g2.drawStr(30, 10, "Hello ESP32");  // 打印文字
    u8g2.drawRFrame(5,20,118,44,5);
    u8g2.sendBuffer();          // 显示内容
    delay(100);
  }
}
void setup() {
  Serial.begin(115200);
  Serial.println("Hello, ESP32-S3!");
  u8g2.begin();
  printf("初始化完毕\n");
  // xTaskCreate(led_task, "LED-DISP", 1024*4, NULL, 1, NULL);
  xTaskCreatePinnedToCore(led_task, "LED-DISP", 1024*4, NULL, 1, NULL, 1);
}
void loop() {
  delay(100);
}

在单任务中使用U8G2对OLE进行操作时候,需要注意的是 u8g2.begin 要和使用的其他函数在同意核心中执行。
在ESP32-S3的Arduino环境中,SuperLoop 默认是在核心1中启动的,但如果我们通过 xTaskCreate 创建任务,那新创建的任务运行在哪个CPU是随机的(或是由系统机制决定的),这样做有一半的几率可能会导致OLED无法显示。
所以我们正确的做法是使用 xTaskCreatePinnedToCore 指定OLED的任务必须在核心1上运行。

多任务同时操作OLED

涉及到多任务同时操作OLED(或其他设备)时,一定要对这个设备的所有操作加锁。
但更好的做法是只有一个线程来控制设备,其他线程如果需要使用设备时,通过消息队列、邮件等方式间接操作设备,这个后续的课程中我们会讲到。

MPU6050

MPU6050 是一款集成了三轴陀螺仪和三轴加速度计的数字运动处理芯片,由英国飞思卡尔公司研制。它能够通过测量物体在空间中的角度和加速度,实现姿态检测、运动追踪、手势识别等功能。同时,MPU6050 还集成了数模转换器(ADC)和数字信号处理器(DSP),能够将模拟信号转换为数字信号,并进行滤波、数据处理等操作,便于嵌入式系统与其通信。
MPU6050 功能强大,精度高,常被用于无人机、智能手环、智能车、电子游戏、运动健康等领域,是一款十分优秀的传感器芯片。
替换产品还有个经度更高的MPU9250,MPU9250 是一款集成了三轴陀螺仪、三轴加速度计和三轴磁力计于一体的九轴传感器芯片,由英伟达子公司 InvenSense 公司生产。与 MPU6050 相比,MPU9250 采用更先进的 MEMS 技术和信号处理算法,能够实现更高精度的姿态检测和运动跟踪,并且支持更多的数据输出格式和接口类型(如 SPI、I2C和UART等)。此外,MPU9250 还支持外部磁力计和增量式编码器等传感器的接入,为一些特定应用场景提供了更强的扩展性和可靠性。
如果我们需要用到指南针的场景,可以选择MPU9250。
本利中,我们只讲解MPU6050的使用方式,关于MPU9250的应用,大家可以参考MPU9250项目,其中有Arduino和ESP32操作的详细案例。

MPU6050接入方式

我们采用的MPU6050使用IIC接口接入到ESP32上,本次例程中我们使用的是MPU6050的模块,是已经封装好的,但在实际开发中我们用到模块的几率比较小(除非是为了偷懒),正常如果不出意外的话,肯定是要将6050的芯片集成到我们自己画的主板上。
MPU6050的电路图非常简单, 除了自身以外还有一些旁路电容和上拉电阻,对于第三方芯片或模块的使用,我们不用背电路图,在参数文档中全都有,只要照抄就行了,以后我们所有应用到第三方芯片和模块,都去找Datasheet或者是技术参考手册,其中百分百会有原理图,直接腾挪过来即可,这是硬件工程师最正确的做法。切忌:自己装逼只会带来灾难。

插入MPU6050的电路图

对于MPU6050的模块应用就比较简单了,SCL和SDL接入单片机的任意引脚,VCC接入3.3V(数据文档中有,不是所有的期间都是3.3V的),GND共线,因为SCL和SDL属于数据引脚,我们不用三极管做隔离。
插入MPU6050的电路图

代码共享位置:https://wokwi.com/projects/362759032395062273

#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Wire.h>
#define SCL 16
#define SDA 17
Adafruit_MPU6050 mpu;   // 声明设备
volatile bool initialized = false;
void setup() {
  Serial.begin(115200);
  Wire.begin(SDA, SCL);   // 初始化I2C总线
  initialized = mpu.begin();
  if (initialized) {      // 初始化陀螺仪
    Serial.println("MPU6050 初始化成功");
    mpu.setAccelerometerRange(MPU6050_RANGE_8_G);     // 设置重力加速度的测量范围为±8G
    mpu.setGyroRange(MPU6050_RANGE_500_DEG);          // 设置陀螺仪的测量范围为±500度/秒
    mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);       // 设置滤波器带宽
  } else {
    Serial.println("MPU6050 初始化失败");
  }
}
void loop() {
  sensors_event_t accel;
  sensors_event_t gyro;
  sensors_event_t temp;
  while(initialized){
    mpu.getEvent(&accel, &gyro, &temp);
    printf("-----===== MPU6050 =====-----\n");
    printf("Temp   : %.2f°C\n",temp.temperature);
    printf("Gyro-X : %.2f°\n",gyro.gyro.x *180 / PI);
    printf("Gyro-Y : %.2f°\n",gyro.gyro.y *180 / PI);
    printf("Gyro-Z : %.2f°\n",gyro.gyro.z *180 / PI);
    printf("Acce-X : %.2f g\n",accel.acceleration.x / 9.81);
    printf("Acce-Y : %.2f g\n",accel.acceleration.y / 9.81);
    printf("Acce-Z : %.2f g\n",accel.acceleration.z / 9.81);
    printf("==============================\n");
    vTaskDelay(pdMS_TO_TICKS(1000));
  }
  while(1){vTaskDelay(pdMS_TO_TICKS(100));}
}

同样的原因,因为ESP32-S3使用的是GPIO矩阵,所以我们任意Pin都可以用作IIC,这里我们依然选择使用16作为SCL,17作为SDL。
MPU6050配置之前,先使用 Wire.begin(SDA, SCL) 对IIC进行初始化,在其他的IIC操作中,如果使用的是Wire操作,都可以使用这个方法对软件IIC进行设置。
IIC总线初始化完毕后,即可通过 mpu.begin() 对MPU6050进行初始化,这一步会调用IIC对MPU6050进行一次通讯,通过返回值确定是否启动成功,如果返回是true表示初始化成功,我们可以继续往下进行。
接下来的三句代码是对陀螺仪的经度进行设置。

mpu.setAccelerometerRange(MPU6050_RANGE_8_G) : 对加速度计的量程范围进行设置,参数可选择如下:

  • MPU6050_RANGE_2G :量程为±2G之间;
  • MPU6050_RANGE_4G :量程为±4G之间;
  • MPU6050_RANGE_8G :量程为±8G之间;
  • MPU6050_RANGE_16G :量程为±16G之间。

mpu.setGyroRange(MPU6050_RANGE_500_DEG) : 对陀螺仪的量程进行设置,表示用户可以测量到的最大偏转角度,单位为弧度/秒,可选参数如下:

  • MPU6050_RANGE_250_DEG,表示量程为±250度每秒;
  • MPU6050_RANGE_500_DEG,表示量程为±500度每秒;
  • MPU6050_RANGE_1000_DEG,表示量程为±1000度每秒;
  • MPU6050_RANGE_2000_DEG,表示量程为±2000度每秒。

mpu.setFilterBandwidth(MPU6050_BAND_21_HZ) : 用来设置低通滤波器的截止频率,
以平滑读数并去除高频噪声,可选参数有:
MPU6050_BAND_260_HZ,表示截止频率为260Hz;
MPU6050_BAND_184_HZ,表示截止频率为184Hz;
MPU6050_BAND_94_HZ,表示截止频率为94Hz;
MPU6050_BAND_44_HZ,表示截止频率为44Hz;
MPU6050_BAND_21_HZ,表示截止频率为21Hz;
MPU6050_BAND_10_HZ,表示截止频率为10Hz;
MPU6050_BAND_5_HZ,表示截止频率为5Hz。

数字低通滤波器的截止频率是指滤波器开始对输入信号进行滤波降频的频率点。在数字低通滤波器中,只有低于截止频率的信号分量可以通过滤波器,而高于截止频率的信号分量则被过滤掉。因此,数字低通滤波器用于去除高频噪声,平滑数据的变化。对于一个数字低通滤波器,其截止频率一般通过设置低通滤波器的模拟升频截止频率来实现。通过一个数字低通滤波器进行信号处理时,需要根据实际的应用需求来选择合适的截止频率。如果数字低通滤波器的截止频率太低,则会导致滤波之后的信号延迟过大,可能会导致时间上的不精确,并且不能及时反映数据的变化。而如果数字低通滤波器的截止频率太高,则不能有效地滤除高频噪声,从而降低滤波效果。因此,在选择数字低通滤波器截止频率时,需要根据具体的应用场景和要求来进行权衡和选择。

数据采集

完成 之后进入大循环,开始不断采集信号。
首先通过 sensors_event_t 声明了一系列的结构体,用于存储MPU6050传回的数据
然后调用 mpu.getEvent(&accel, &gyro, &temp) 将这几个结构体的指针传入,每调用一次getEvent即可获得一次当前的数据。
sensors_event_t 结构体是通用的,每个里面都保存了温度、角速度、加速度三组变量,所以需要通过不同的成员变量取得。
sensors_event_t.temperature :用于获得温度信息,返回一个float型数值,单位为摄氏度(°C)
sensors_event_t.gyro :用于获得XYZ三周的旋转加角速度,也就是我们在不同轴上旋转的数值变化,单位是 弧度/秒,可以通过数学公式:red*180/PI 得到旋转的角度(角度/秒)
sensors_event_t.acceleration :用于获得三个轴的重力加速度,即我们在每个轴上移动的速度,返回的单位为m/s²,除以9.81可得到 以重力加速度g为单位的数据。
实际开发中我们不需要做转换,直接用原始数据操作即可,正现实中为了更直观,我们做了数学转换。

在实际开发中,因为sensors_event_t有些浪费空间,我们可以换成两外三个函数分别获得温度、重力加速度和角速度:
mpu.getTemperature() :没有参数,返回一个float用于表示温度
mpu.getAcceleration(&ax, &ay, &az) :传入三个指针,会得到三轴的重力加速度
mpu.getRotation(&gx, &gy, &gz) :传入三个指针,会得到三轴的角速度

模拟器运行后,我们可以通过点选MPU6050设备打开参数调整开关:
在这里插入图片描述

多任务多IIC设备的综合运用

IIC总线挂在多设备

IIC总线中可以挂在多个设备,主机和设备之间通过地址方式通讯,首先主机发送一个START信号(开始信号),选好传输的开始,紧接着主机会发送设备ID地址(7位或10位)给所有设备,当设备发现地址属于自己的时候,选择激活,等待与主机通讯;之后主机发送读写控制位、数据位、STOP信号(停止信号)释放总线。这样就完成了一次通讯,总线进入静默模式,等待下一次通讯。
由此可见,在IIC总线通讯中,我们可以将很多个设备挂在总线中(理论最多可挂在112个设备),而这些设备进行复杂通讯也仅仅占用了SCL和SDA两个引脚,非常的经济实惠。
在本案例中,我们将MPU6050和OLED共同连接在一个IIC总线中,实现多任务数据通讯。

硬件设计

硬件电路图

软件设计

在本案例中,一共运行三个线程,分别是:

  1. MPU6050数据采集线程,用于对MPU6050硬件进行操作,其中使用IIC通讯获取数据,并放入一个全局通用的数据结构体中;
  2. Print线程,对全局数据结构体内的数据进行控制台输出;
  3. OLED线程,使用IIC与硬件LED进行通讯,同时将全局结构体数据显示在屏幕上。
    其中,MPU6050线程和Print线程都要对全局结构体进行数据操作,为了不引起数据混乱,所以要使用互斥信号量对这个数据结构体进行加锁操作。
    而MPU6050和OLED又同时对IIC总线进行操作,为了防止总线数据混乱,所以需要对IIC总线及一把同步锁,在所有使用到IIC总线的情况下进行互斥操作,而没有用到总线的情况下则不需要加锁。
    最后需要注意,IIC总线的初始化和应用都要在同一核心下进行,否则会引起设备混乱。

代码共享地址:https://wokwi.com/projects/362704315801904129

#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Wire.h>
#include <U8g2lib.h>
#define SCL 16
#define SDA 17
#define DISP_OLED 1
#define DISP_CONSOLE 1
typedef struct{
  // 温度
  float temp;
  // 加速度
  float acce_x;
  float acce_y;
  float acce_z;
  // 姿态
  float gyro_x;
  float gyro_y;
  float gyro_z;
}MPU6050;
MPU6050 mpu6050;
SemaphoreHandle_t xMutexMPU6050 = NULL;   // 同步锁
SemaphoreHandle_t xMutexIIC = NULL;            // IIC设备同步
Adafruit_MPU6050 mpu;   // 声明设备
// 陀螺仪任务
void mpu_task(void* param_t){
  if (xSemaphoreTake(xMutexIIC, 1000) == pdPASS) {
    mpu.setAccelerometerRange(MPU6050_RANGE_8_G);     // 设置重力加速度的测量范围为±8G
    mpu.setGyroRange(MPU6050_RANGE_500_DEG);          // 设置陀螺仪的测量范围为±500度/秒
    mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);       // 设置滤波器带宽
    xSemaphoreGive(xMutexIIC);    // 释放同步锁
  }
 
  sensors_event_t accel;
  sensors_event_t gyro;
  sensors_event_t temp;
 
  for(;;){
    // 循环读取更新陀螺仪数据
    if (xSemaphoreTake(xMutexIIC, 1000) == pdPASS) {
      mpu.getEvent(&accel, &gyro, &temp);             // 获取数据
      xSemaphoreGive(xMutexIIC);    // 释放同步锁
    }
    if (xSemaphoreTake(xMutexMPU6050, 1000) == pdPASS) {
      // printf("获得钥匙,赶紧更新数据...\n");
      // 读出温度
      mpu6050.temp = temp.temperature;
      // 读出加速度
      mpu6050.acce_x = accel.acceleration.x / 9.81; // 得到的是重力, / 9.81得到的是牛
      mpu6050.acce_y = accel.acceleration.y / 9.81;
      mpu6050.acce_z = accel.acceleration.z / 9.81;
      // 读出姿态
      mpu6050.gyro_x = gyro.gyro.x *180 / PI;     // 得到的是弧度,转换为角度
      mpu6050.gyro_y = gyro.gyro.y *180 / PI;
      mpu6050.gyro_z = gyro.gyro.z *180 / PI;
      xSemaphoreGive(xMutexMPU6050);    // 释放同步锁
      // printf("数据更新完毕,把钥匙还回去了!\n");
    }else{
      printf("没人没送回钥匙来,我不等了~~\n");
    }
    vTaskDelay(pdMS_TO_TICKS(100));
  }
}
#if defined(DISP_CONSOLE) && DISP_CONSOLE == 1
// 打印输出线程
void print_task(void *param_t){
  for(;;){
    if (xSemaphoreTake(xMutexMPU6050, 1000) == pdPASS) {
      printf("-----===== MPU6050 =====-----\n");
      printf("Temp   : %.2f°C\n",mpu6050.temp);
      printf("Gyro-X : %.2f°\n",mpu6050.gyro_x);
      printf("Gyro-Y : %.2f°\n",mpu6050.gyro_y);
      printf("Gyro-Z : %.2f°\n",mpu6050.gyro_z);
      printf("Acce-X : %.2f g\n",mpu6050.acce_x);
      printf("Acce-Y : %.2f g\n",mpu6050.acce_y);
      printf("Acce-Z : %.2f g\n",mpu6050.acce_z);
      printf("==============================\n");
      xSemaphoreGive(xMutexMPU6050);
    }
    vTaskDelay(pdMS_TO_TICKS(1000));
  }
}
#endif
#if defined(DISP_OLED) && DISP_OLED == 1
U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, /*clock=*/SCL, /*data=*/SDA, /*reset=*/U8X8_PIN_NONE);
// OLED显示线程
void led_task(void *param_t){
  if (xSemaphoreTake(xMutexIIC, 1000) == pdPASS) {
    u8g2.begin();
    // u8g2.setFont(u8g2_font_ncenB08_tr);  // 设置字体
    u8g2.setFont(u8g2_font_7x14_mr);
    xSemaphoreGive(xMutexIIC);       // 释放同步锁
  }
  char buf[24];   // 打印缓冲区
  for(;;){
    u8g2.clearBuffer();         // 清理缓冲区
    if (xSemaphoreTake(xMutexMPU6050, 1000) == pdPASS) {
      sprintf(buf,"Temp : %.2f°C\n",mpu6050.temp);
      u8g2.drawStr(0,10,buf);
      sprintf(buf,"GX:%3.2f° AX:%3.2fg\n",mpu6050.gyro_x, mpu6050.acce_x);
      u8g2.drawStr(0,25,buf);
      sprintf(buf,"GY:%3.2f° AY:%3.2fg\n",mpu6050.gyro_y, mpu6050.acce_y);
      u8g2.drawStr(0,40,buf);
      sprintf(buf,"GZ:%3.2f° AZ:%3.2fg\n",mpu6050.gyro_z, mpu6050.acce_z);
      u8g2.drawStr(0,55,buf);
      xSemaphoreGive(xMutexMPU6050);
    }
    if (xSemaphoreTake(xMutexIIC, 1000) == pdPASS) {
      u8g2.sendBuffer();              // 显示内容
      xSemaphoreGive(xMutexIIC);       // 释放同步锁
    }
    vTaskDelay(pdMS_TO_TICKS(1000));
  }
}
#endif
void setup() {
  Serial.begin(115200);
  xMutexMPU6050 = xSemaphoreCreateMutex();    // 创建数据同步锁
  xMutexIIC = xSemaphoreCreateMutex();           // 创建IIC同步锁
  Wire.begin(SDA, SCL);   // 初始化I2C总线
  if (mpu.begin()) {      // 初始化陀螺仪
    Serial.println("MPU6050 初始化成功");
    xTaskCreatePinnedToCore(mpu_task, "MPU6050", 1024*4, NULL, 1, NULL, 1);  
#if defined(DISP_CONSOLE) && DISP_CONSOLE == 1
    xTaskCreate(print_task, "Print", 1024*4, NULL, 1, NULL);
#endif
#if defined(DISP_OLED) && DISP_OLED == 1
    xTaskCreatePinnedToCore(led_task, "OLED", 1024*4, NULL, 1, NULL,1);
#endif
  } else {
    Serial.println("MPU6050 初始化失败");
  }
}
void loop() {
  delay(10);
}

赠品

/*******************************************************************************
 * 用陀螺仪控制小球移动
 * 本例中使用了OLED12864 和 MPU6050两个组件,但是OLED手里之后恶心的 SH1106
 * 这货跟 MPU6050 或者其他的 IIC 设备不太兼容,同在一个总线容易出问题,
 * 所以大家选择OLED的时候尽量用0.9寸的SSD1306。
 *
 * 还是已恶心的 SH1106为例,必须使用第二条IIC总线,所以这里扩展用到了TwoWire
 * ball_task 线程中负责SH1106的刷新,这个芯片刷新OLED非常慢,大概1帧/秒
 * mpu_task 负责采集陀螺仪的偏转角度,根据偏转角度大小设置小球每帧滑动步长
 ******************************************************************************/
#include <Adafruit_MPU6050.h>
#include <U8g2lib.h>
#include <Wire.h>
#define OLED_SCL 16   // OLED 的IIC_SCL引脚
#define OLED_SDA 17   // OLED 的IIC_SDA引脚
#define MPU_SDA  41   // MPU6050 的IIC_SDA引脚
#define MPU_SCL  42   // MPU6050 的IIC_SCL引脚
#define BALL_SIZE 5   // 球的大小
float x=64 ,y=32;     // 球的中心点坐标
float xa=0, ya=0;     // XY 轴的滑动速度
float xc=0, yc=0;     // 校准后的X轴和Y轴,上电后第一件事就是校准
/*******************************************************************************
 * LED 显示任务
 * 屏幕用的是 SH1106,使用的是IIC_1 ,也就是默认的IIC,
 * 因为在ESP32中使用了GPIO矩阵设置IIC,所有理论上所有的IIC引脚都需要使用Wire.begin
 * 做初始化。OLED的SDC和SCL分别是16和17引脚
 ******************************************************************************/
void ball_task(void *param_t){
  // 初始化U8G2库和OLED
  U8G2_SH1106_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, /*clock=*/OLED_SCL, /*data=*/OLED_SDA, /*reset=*/U8X8_PIN_NONE);
  Wire.begin(OLED_SDA, OLED_SCL);   // 初始化 IIC 总线
  u8g2.begin();                     // 初始化OLED
  for(;;){
    u8g2.clearBuffer();             // 每帧开始先清空缓冲区
    u8g2.drawFrame(0,0,u8g2.getWidth(), u8g2.getHeight());      // 画一个边框,已确定小球滚动的范围
    x-=xa; y-=ya;                   // 根据偏转角度计算本帧的移动步长
    // 以下四句将小球控制在横轴0~127,纵轴0~63之间,也就是屏幕的大小
    x = x<BALL_SIZE?BALL_SIZE:x;
    x = x>127-BALL_SIZE?127-BALL_SIZE:x;
    y = y<BALL_SIZE?BALL_SIZE:y;
    y = y>63-BALL_SIZE?63-BALL_SIZE:y;
    u8g2.drawDisc((int)x, (int)y, BALL_SIZE, U8G2_DRAW_ALL);    // 画一个实体小球
    u8g2.sendBuffer();                                          // 显示内容,将缓冲区内容发送到OLED,这一步巨慢无比
    delay(1);                                                   // 让出CPU
  }
}
/*******************************************************************************
 * MPU6050 偏转角度获取
 * 为了给恶心的 SH1106 让路,可怜的 MPU6050 只能委屈使用IIC_1
 *
 ******************************************************************************/
void mpu_task(void* param_t){
  // 初始化MPU6050
  Adafruit_MPU6050 mpu;
  TwoWire wire = TwoWire(1);        // 使用第二路IIC,第一路索引为0, 第二路索引为1 ,如果存在第三路,则索引为2
  wire.begin(MPU_SDA,MPU_SCL);      // 使用41和42引脚初始化第二路IIC
 
  // Wire.begin(MPU_SDA, MPU_SCL);
  if (mpu.begin(MPU6050_I2CADDR_DEFAULT,&wire)){      // 使用第二路IIC初始化 MPU6050
  // if (mpu.begin()){
    mpu.setAccelerometerRange(MPU6050_RANGE_8_G);     // 设置重力加速度的测量范围为±8G
    mpu.setGyroRange(MPU6050_RANGE_500_DEG);          // 设置陀螺仪的测量范围为±500度/秒
    mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);       // 设置滤波器带宽
    }else{
      printf("MPU6050 initialization failed!\n");
      vTaskDelete(NULL);
    }
  sensors_event_t accel;
  sensors_event_t gyro;
  sensors_event_t temp;
  /* 先校准
   * 第一次获取MPU6050数据是为了校准用,将开机时候的角度作为默认角度
   * 后面发生角度偏转的时候都已初始角度为参考
   * 因为MPU6050的安装是XY相反的,所以需要对调以下
   */
  mpu.getEvent(&accel, &gyro, &temp);     // 获得偏转角度,加速度和温度
  xc = accel.acceleration.y;
  yc = accel.acceleration.x;
  for(;;){
    /* 开始循环获取角度偏转信息,偏转角度越大小球滑动越快
     * 获得偏转角度后需要和初始化的角度做补偿运算
     */
    mpu.getEvent(&accel, &gyro, &temp);
    xa = accel.acceleration.y - xc;
    ya = accel.acceleration.x - yc;
    delay(100);
  }
}
void setup() {
  Serial.begin(115200);
  Serial.println("Hello, ESP32-S3!");
  xTaskCreate(mpu_task, "MPU6050", 10240, NULL, 1, NULL);
  xTaskCreate(ball_task, "BALL", 10240, NULL, 1, NULL);
  vTaskDelete(NULL);    // 自宫
}
void loop() {
}
  • 16
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值