ヘルスチェックデバイス:脈拍数+酸素飽和度SpO2+体温測定(FreeRTOS+ESP32)

FreeRTOS

ESP32 FreeRTOS Arduinoサンプルスケッチ

ESP32 FreeRTOS

Task API
https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/freertos.html#freertos

Functions

BaseType_t xTaskCreatePinnedToCore (TaskFunction_t pvTaskCode, const char *const pcName, const uint32_t usStackDepth, void *const pvParameters, UBaseType_t uxPriority, TaskHandle_t *const pvCreatedTask, const BaseType_t xCoreID )

Create a new task with a specified affinity.

This function is similar to xTaskCreate, but allows setting task affinity in SMP system.

Return

pdPASS if the task was successfully created and added to a ready list, otherwise an error code defined in the file projdefs.h

Parameters

  • pvTaskCode: Pointer to the task entry function. Tasks must be implemented to never return (i.e. continuous loop), or should be terminated using vTaskDelete function.
  • pcName: A descriptive name for the task. This is mainly used to facilitate debugging. Max length defined by configMAX_TASK_NAME_LEN - default is 16.
  • usStackDepth: The size of the task stack specified as the number of bytes. Note that this differs from vanilla FreeRTOS.
  • pvParameters: Pointer that will be used as the parameter for the task being created.
  • uxPriority: The priority at which the task should run. Systems that include MPU support can optionally create tasks in a privileged (system) mode by setting bit portPRIVILEGE_BIT of the priority parameter. For example, to create a privileged task at priority 2 the uxPriority parameter should be set to ( 2 | portPRIVILEGE_BIT ).
  • pvCreatedTask: Used to pass back a handle by which the created task can be referenced.
  • xCoreID: If the value is tskNO_AFFINITY, the created task is not pinned to any CPU, and the scheduler can run it on any core available. Values 0 or 1 indicate the index number of the CPU which the task should be pinned to. Specifying values larger than (portNUM_PROCESSORS - 1) will cause the function to fail.

static BaseType_t xTaskCreate (TaskFunction_t pvTaskCode, const char *const pcName, const uint32_t usStackDepth, void *const pvParameters, UBaseType_t uxPriority, TaskHandle_t *const pvCreatedTask )

ESP-IDF FreeRTOS SMP Changes

https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/freertos-smp.html#esp-idf-freertos-smp-changes
ESP32は2コアCPU構成で、この2者間でメモリを共有することにより、同一タスクを割振ることが可能です。

脈拍数・酸素飽和度・体温を測定するヘルスデバイスの作成

  • マイコン:ESP32S
  • パルスオキシメーター: MAX30102
  • 非接触型赤外線温度センサー: GY-906 MLX90614
  • OLED:2色、 128X64 I2C SSD1306

各センサーの制御をFreeRTOSにより管理、MQTTプロトコルにより出力値をホームオートメーションシステムに送信することで、遠隔でのヘルスチェック、異常時の緊急呼び出しなどを実現します。

注) MAX30102では、データ読取りサンプリング中に他のタスクの割り込みが入ると結果の数値が大きく影響を受けるため、RTOSでタスク管理する際は、この点に留意する必要があります。

パルスオキシメータ原理

graph_animation

MAX30100 SpO2測定アルゴリズム

動作確認中(I2C接続)

各デバイスのI2Cアドレス確認

確認用Arduinoスケッチ

/*********
  Rui Santos
  Complete project details at https://randomnerdtutorials.com  
*********/

#include <Wire.h>
 
void setup() {
  Wire.begin();
  Serial.begin(115200);
  Serial.println("\nI2C Scanner");
}
 
void loop() {
  byte error, address;
  int nDevices;
  Serial.println("Scanning...");
  nDevices = 0;
  for(address = 1; address < 127; address++ ) {
    Wire.beginTransmission(address);
    error = Wire.endTransmission();
    if (error == 0) {
      Serial.print("I2C device found at address 0x");
      if (address<16) {
        Serial.print("0");
      }
      Serial.println(address,HEX);
      nDevices++;
    }
    else if (error==4) {
      Serial.print("Unknow error at address 0x");
      if (address<16) {
        Serial.print("0");
      }
      Serial.println(address,HEX);
    }    
  }
  if (nDevices == 0) {
    Serial.println("No I2C devices found\n");
  }
  else {
    Serial.println("done\n");
  }
  delay(5000);          
}

シリアルモニタ

Scanning...
I2C device found at address 0x3C ---> OLED
I2C device found at address 0x57 ---> MAX30102
I2C device found at address 0x5A ---> MLX90614
done

MAX30102モジュール補足

注) 今回購入したMAX30102モジュールの回路図(30100を30102に置換え)は以下の通りです。MAX30100とはADC分解能、レジスタマップが異なるため、MAX30100ライブラリは適用できません。

回路図

Sparkfun Arduinoライブラリ
以下SparkFunのライブラリを採用します。

MAX30100

MAX30102

Sparkfun ArduinoライブラリでRed LEDとIR LEDの設定が逆の可能性

LED1 < — > LED2
sense.red < — > sense.IR

MAX30105.cpp

.....
.....
void MAX30105::setPulseAmplitudeRed(uint8_t amplitude) {
  writeRegister8(_i2caddr, MAX30105_LED2_PULSEAMP, amplitude);
}

void MAX30105::setPulseAmplitudeIR(uint8_t amplitude) {
  writeRegister8(_i2caddr, MAX30105_LED1_PULSEAMP, amplitude);
}
.....
.....
.....
while (toGet > 0)
      {
        sense.head++; //Advance the head of the storage struct
        sense.head %= STORAGE_SIZE; //Wrap condition

        byte temp[sizeof(uint32_t)]; //Array of 4 bytes that we will convert into long
        uint32_t tempLong;

        //Burst read three bytes - RED
        temp[3] = 0;
        temp[2] = _i2cPort->read();
        temp[1] = _i2cPort->read();
        temp[0] = _i2cPort->read();

        //Convert array to long
        memcpy(&tempLong, temp, sizeof(tempLong));
		
		tempLong &= 0x3FFFF; //Zero out all but 18 bits

        sense.IR[sense.head] = tempLong; //Store this reading into the sense array

        if (activeLEDs > 1)
        {
          //Burst read three more bytes - IR
          temp[3] = 0;
          temp[2] = _i2cPort->read();
          temp[1] = _i2cPort->read();
          temp[0] = _i2cPort->read();

          //Convert array to long
          memcpy(&tempLong, temp, sizeof(tempLong));

		  tempLong &= 0x3FFFF; //Zero out all but 18 bits
          
		  sense.red[sense.head] = tempLong;
        }
.....

ESP32+FreeRTOS 参考サイト

ESP32: Running code on a specific core

How to use ESP32 Dual Core with Arduino IDE

ESP32はデュアルコア構成。loop(), setup()はデフォルトでCore 1を使用
コアを指定しないタスクCore 0を使用

Multitasking on ESP32 with Arduino and FreeRTOS

FreeRTOS Arduino Tutorials

FreeRTOS Arduino : How to Delete Tasks with vTaskDelete() API

vTaskDelete() サンプルコード

// Include Arduino FreeRTOS library
#include <Arduino_FreeRTOS.h>
#include "task.h"

TaskHandle_t TaskHandle_2; // handler for Task2

void setup() 
{
  Serial.begin(9600); // Enable serial communication library.
  pinMode(4, OUTPUT);  // define LED1 pin as a digital output 
  pinMode(5, OUTPUT);  // define LED2 pin as a digital output

  //Create the first task at priority 1
  // Name of task is "LED1"
  // Stack size is set to 100
  // we do not pass any value to Task1. Hence, third agument is NULL
  // Set the priority of task to one
  // Task1 handler is not used. Therefore set to Null
  xTaskCreate(Task1, "LED1", 100, NULL, 1, NULL);
  // Start FreeRTOS scheduler in Preemptive timing silicing mode
  vTaskStartScheduler();
}

void loop() 
{
// Do nothing as schduler will allocated CPU to Task1 and Task2 automatically
}
/* Task1 with priority 1 */
void Task1(void* pvParameters)
{

  while(1)
  {
    Serial.println("Task1 Running"); // print "Task1 Running" on Arduino Serial Monitor
    digitalWrite(4, HIGH); // sets the digital pin 4 on
    digitalWrite(5, LOW); // sets the digital pin 5 off
    xTaskCreate(Task2, "LED2", 100, NULL, 2, &TaskHandle_2); // create task2 with priority 2
    vTaskDelay( 100 / portTICK_PERIOD_MS ); // wait for one second
  }
}


/* Task2 with priority 2 */
void Task2(void* pvParameters)
{ 
    //digitalWrite(5, HIGH); // sets the digital pin 5 high
    //digitalWrite(4, LOW); // sets the digital pin 4 low
    Serial.println("Task2 is runnig and about to delete itself");
    vTaskDelete(TaskHandle_2);     //Delete own task by passing NULL(TaskHandle_2 can also be used)
}

FreeRTOS Arduino: Changing the Priority of a Task

プログラム実行中のタスクプライオリティの変更

vTaskPrioritySet() サンプルコード

#include <Arduino_FreeRTOS.h>
#include <task.h>
void Task1( void *pvParameters );
void Task2( void *pvParameters );

TaskHandle_t TaskHandle_1; // handler for Task1
TaskHandle_t TaskHandle_2; // handler for Task2


void setup() 
{
  Serial.begin(9600); // Enable serial communication library.

   xTaskCreate(Task1, "LED1", 100, NULL, 3, &TaskHandle_1);
   xTaskCreate(Task2, "LED2", 100, NULL, 2, &TaskHandle_2);
   vTaskStartScheduler();
}

void loop() 
{
  // put your main code here, to run repeatedly:

}

//definition of Task1
void Task1(void* pvParameters)
{
     UBaseType_t uxPriority = uxTaskPriorityGet( NULL );
    while(1)
    {
    Serial.println("Task1 is running and about to raise Task2 Priority");
    vTaskPrioritySet( TaskHandle_2, ( uxPriority + 1 ) );
   
    }
}
void Task2(void* pvParameters)
{
 UBaseType_t   uxPriority = uxTaskPriorityGet( NULL );
     while(1)
    {
    Serial.println("Task2 is running and about to lower Task2 Priority");
    vTaskPrioritySet( TaskHandle_2, ( uxPriority - 2 ) );
    
    }
  
}

FreeRTOS非適用の通常のループ関数によるI2C接続された各デバイスの動作確認用スケッチ。
MAX30102の酸素飽和度の測定精度(データのサンプリング)は、I2C同一ライン上に接続されたOLEDデバイスへの通信負荷により大きく影響を受けるため測定中は非表示とする(電源の問題で表示しても問題ない)。

/*
  Optical SP02 Detection (SPK Algorithm) using the MAX30102 Breakout
  By: Takanobu Fuse @ Ficusonline F9E
  Date: May 17th, 2021

  Hardware Connections (Breakoutboard to Arduino):
  -5V = 5V (3.3V is allowed)
  -GND = GND
  -SDA = A4 (or SDA)
  -SCL = A5 (or SCL)
  -INT = Not connected
 
  The MAX30102 Breakout can handle 5V or 3.3V I2C logic.
*/

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_MLX90614.h>
#include "MAX30105.h"
#include "heartRate.h"
#include "spo2_algorithm.h"

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
#define OLED_RESET     -1 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

MAX30105 particleSensor;

Adafruit_MLX90614 mlx = Adafruit_MLX90614();

#define MAX_BRIGHTNESS 255

bool checkFlag = 0; // Flag for finding error

// Check Presence variables
long unblockedValue; //Average IR at power up

// Heart Rate variables
const byte RATE_SIZE = 4; //Increase this for more averaging. 4 is good.
byte rates[RATE_SIZE]; //Array of heart rates
byte rateSpot = 0;
long lastBeat = 0; //Time at which the last beat occurred

float beatsPerMinute;
int beatAvg;
long redValue;
long delta;

// Spo2 variables
uint32_t irBuffer[100]; //infrared LED sensor data
uint32_t redBuffer[100];  //red LED sensor data
int32_t bufferLength; //data length
int32_t spo2; //SPO2 value
int8_t validSPO2; //indicator to show if the SPO2 calculation is valid
int32_t heartRate; //heart rate value
int8_t validHeartRate; //indicator to show if the heart rate calculation is valid

//MAX30102 Config
byte ledBrightness = 60; //Options: 0=Off to 255=50mA
byte sampleAverage = 4; //Options: 1, 2, 4, 8, 16, 32
byte ledMode = 2; //Options: 1 = Red only, 2 = Red + IR, 3 = Red + IR + Green
byte sampleRate = 1000; //Options: 50, 100, 200, 400, 800, 1000, 1600, 3200
int pulseWidth = 411; //Options: 69, 118, 215, 411
int adcRange = 4096; //Options: 2048, 4096, 8192, 16384

// Led indicators
byte blueLED = 12; //Must be on PWM pin
byte greenLED = 13; //Blinks with each data read

void setup()
{
  Serial.begin(115200); // initialize serial communication at 115200 bits per second:

  pinMode(blueLED, OUTPUT);
  pinMode(greenLED, OUTPUT);

  // Initialize sensor
  if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) //Use default I2C port, 400kHz speed
  {
    Serial.println(F("MAX30105 was not found. Please check wiring/power."));
    while (1);
  }

  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { 
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
  }

  delay(1000);
  display.clearDisplay();
  display.setTextColor(WHITE);
  displayNotice();

  // max30102
  particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange); //Configure sensor with these settings
  // mlx90614
  mlx.begin();
}

void loop()
{
  if (preCheck() == true) {
    displayChecking();
    readHR();
    if(checkFlag==0) {
      ESP.restart();
    }
    
    displayChecking();
    readSpo2();
    if(checkFlag==0) {
      ESP.restart();
    }
    
    readTemp();
    
    ESP.restart();
  }
}

bool preCheck() {
  particleSensor.setPulseAmplitudeRed(0x00); //Red Led off

  //Take an average of IR readings at power up
  unblockedValue = 0;
  
  for (byte x = 0 ; x < 32 ; x++)
  {
    unblockedValue += particleSensor.getIR(); //Read the IR value
  }
  unblockedValue /= 32;


  Serial.print("IR ");
  Serial.print(particleSensor.getIR());

  long currentDelta = particleSensor.getIR() - unblockedValue;

  Serial.print(", delta ");
  Serial.println(currentDelta);

  if (currentDelta > (long)500)
  {
    Serial.println(" Something is there!");
    particleSensor.setPulseAmplitudeRed(ledBrightness);
    return true;
  }
  
  displayNotice();
  return false;

}

void readHR() {
  checkFlag=false;
  digitalWrite(blueLED, 1);
  particleSensor.setup(); //Configure sensor with default settings
  particleSensor.setPulseAmplitudeRed(0x0A); //Turn Red LED to low to indicate sensor is running
  particleSensor.setPulseAmplitudeIR(0); //Turn off Green LED
  
  for (int i=0; i<1000; i++) {
    
    digitalWrite(blueLED, !digitalRead(blueLED)); //Blink onboard LED with every data read

    redValue = particleSensor.getRed();
  
    if (checkForBeat(redValue) == true)
    {
      //We sensed a beat!
      delta = millis() - lastBeat;
      lastBeat = millis();
  
      beatsPerMinute = 60 / (delta / 1000.0);
  
      if (beatsPerMinute < 255 && beatsPerMinute > 20)
      {
        rates[rateSpot++] = (byte)beatsPerMinute; //Store this reading in the array
        rateSpot %= RATE_SIZE; //Wrap variable
  
        //Take average of readings
        beatAvg = 0;
        for (byte x = 0 ; x < RATE_SIZE ; x++)
          beatAvg += rates[x];
        beatAvg /= RATE_SIZE;
      }
    }
  
    Serial.print("Red=");
    Serial.print(redValue);
    Serial.print(", BPM=");
    Serial.print(beatsPerMinute);
    Serial.print(", Avg BPM=");
    Serial.print(beatAvg);

    if (redValue < 10000)
      Serial.print(" No finger?");  
  
    Serial.println();
    if((beatAvg >30) && (beatsPerMinute > 0.95*beatAvg && beatsPerMinute < 1.05*beatAvg)) {
      checkFlag=true;
      break;
    }
  }
  
  digitalWrite(blueLED, 0);
  
  if(checkFlag==true) {
    displayHR();
  }else {
    displayError();
  }

}

void readSpo2() {
  checkFlag=false;
  digitalWrite(greenLED, 1);
  
  particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange); //Configure sensor with these settings
  
  bufferLength = 100; //buffer length of 100 stores 4 seconds of samples running at 25sps

  //read the first 100 samples, and determine the signal range
  for (byte i = 0 ; i < bufferLength ; i++)
  {
    while (particleSensor.available() == false) //do we have new data?
      particleSensor.check(); //Check the sensor for new data

    redBuffer[i] = particleSensor.getRed();
    irBuffer[i] = particleSensor.getIR();
    particleSensor.nextSample(); //We're finished with this sample so move to next sample

    Serial.print(F("red="));
    Serial.print(redBuffer[i], DEC);
    Serial.print(F(", ir="));
    Serial.println(irBuffer[i], DEC);
  }

  //calculate heart rate and SpO2 after first 100 samples (first 4 seconds of samples)
  maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);

  //Continuously taking samples from MAX30102.  Heart rate and SpO2 are calculated every 1 second
  for (int x=0; x<20; x++)
  //while(1)
  {
    //dumping the first 25 sets of samples in the memory and shift the last 75 sets of samples to the top
    for (byte i = 25; i < 100; i++)
    {
      redBuffer[i - 25] = redBuffer[i];
      irBuffer[i - 25] = irBuffer[i];
    }

    //take 25 sets of samples before calculating the heart rate.
    for (byte i = 75; i < 100; i++)
    {   
      while (particleSensor.available() == false) //do we have new data?
        particleSensor.check(); //Check the sensor for new data

      digitalWrite(greenLED, !digitalRead(greenLED)); //Blink onboard LED with every data read  

      redBuffer[i] = particleSensor.getRed();
      irBuffer[i] = particleSensor.getIR();
      particleSensor.nextSample(); //We're finished with this sample so move to next sample

      //send samples and calculation result to terminal program through UART
      Serial.print(F("red="));
      Serial.print(redBuffer[i], DEC);
      Serial.print(F(", ir="));
      Serial.print(irBuffer[i], DEC);

      Serial.print(F(", HR="));
      Serial.print(heartRate, DEC);

      Serial.print(F(", HRvalid="));
      Serial.print(validHeartRate, DEC);

      Serial.print(F(", SPO2="));
      Serial.print(spo2, DEC);

      Serial.print(F(", SPO2Valid="));
      Serial.println(validSPO2, DEC);

    }
    
    //After gathering 25 new samples recalculate HR and SP02
    maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);
    
    if((heartRate > 0.8*beatAvg && heartRate < 1.2*beatAvg) && (validHeartRate==1 && validSPO2==1)) {
      checkFlag=true;
      break;
    }
    //if(checkFlag==true) {
    //  break;
    //}
  }
  
  digitalWrite(greenLED, 0);
  
  if(checkFlag==true) {
    displaySpo2();
  } else {
    displayError();
  }
  
}

void readTemp() {
  display.clearDisplay();
  // display temperature of object
  display.setTextSize(2);
  display.setCursor(0,0);
  display.print(mlx.readObjectTempC());
  display.print(" ");
  display.setTextSize(1);
  display.cp437(true);
  display.write(167);
  display.setTextSize(2);
  display.print("C");
  display.setTextSize(1);
  display.setCursor(0,18);
  display.print("TEMPERATURE: ");
  
  // display temperature of ambient
  display.setTextSize(2);
  display.setCursor(0, 34);
  display.print(mlx.readAmbientTempC());
  display.print(" ");
  display.setTextSize(1);
  display.cp437(true);
  display.write(167);
  display.setTextSize(2);
  display.print("C");
  display.setTextSize(1);
  display.setCursor(0, 52);
  display.print("AMBIENT: ");
  
  delay(10);
  yield();
  display.display();
  delay(3000);
}

void displayNotice() {
  display.clearDisplay();
  display.setTextSize(2);
  display.setCursor(0,0);
  display.print("Start >>>");
  display.setTextSize(1);
  display.setCursor(0,30);
  display.print("Put Your Finger on");
  display.display();
  delay(2000);
  display.clearDisplay();
  display.display();
  delay(500);
}

void displayChecking() {
  display.clearDisplay();
  display.setTextSize(2);
  display.setCursor(0,0);
  display.print("Checking..");
  display.setTextSize(1);
  display.setCursor(0,30);
  display.print("Keep Your Finger");
  display.setCursor(0,40);
  display.print("putting on");
  display.display();
  delay(2000);
  display.clearDisplay();
  display.display();
}

void displayError() {
  display.clearDisplay();
  display.setTextSize(1);
  display.setCursor(20,30);
  display.print(F("Read Error..."));
  display.setCursor(20,40);
  display.print(F("Try Again!"));
  
  delay(10);
  yield();
  display.display();
  delay(3000);
}

void displayHR() {
  display.clearDisplay();

  if(redValue > 5000) {
    display.setTextSize(2);
    display.setCursor(0,0); 
    display.print(beatAvg);
    
    display.setTextSize(1);
    display.setCursor(0,18);
    display.print(F("Average BPM"));
    
    display.setTextSize(2);
    display.setCursor(0,34); 
    display.print(beatsPerMinute);
    
    display.setTextSize(1);
    display.setCursor(0,52);
    display.print(F("BPM"));

  } else {
    display.setTextSize(1);
    display.setCursor(40,32);
    display.print(F("HR Not Valid"));
  }
  delay(10);
  yield();
  display.display();
  delay(3000);
}

void displaySpo2() {
  display.clearDisplay();
  display.setTextSize(2);
  display.setCursor(0,0); 
  display.print(spo2, DEC);
  display.print(" ");
  display.print("%");
  
  display.setTextSize(1);
  display.setCursor(0,18);
  display.print(F("SpO2"));
  
  display.setTextSize(2);
  display.setCursor(0,34); 
  display.print(heartRate, DEC);
  
  display.setTextSize(1);
  display.setCursor(0,52);
  display.print(F("Heart Rate(BPM) "));
  
  delay(10);
  yield();
  display.display();
  delay(3000);
}

このスケッチをベースにFreeRTOSを適用したスケッチを作成中。
完成後、心拍数・酸素飽和度・体温の各値をJSONフォーマットにより1セットにし、ホームオートメーションシステムへMQTTプロトコルによりパブリッシュ。

YouTube動画

起動画面

測定モード

心拍数測定

酸素飽和度測定

体温測定
注)ケースデザイン未定のため、MLX90614に指を当てていません。

Arduinoのボードマネージャにより追加できるESP32のライブラリにはFreeRTOSが標準装備されています。そのため、以下の各サンプルスケッチでヘッダーファイルは省略可。

I2Cで接続された各デバイスをRTOSにより管理するため、以下のSemaphore、Mutexを導入します。

ローカル路線でよく見かける単線を上下線で共有する場合、中間地点に設けられた複線区間で上下線のどちらかが待つことで運行している状態がSemaphoreであり(単線上に同時に列車が走行)、単線を上下線のどちらかしか走行できない状態がMutex
Semaphoreでは、複線区間の長さと車列の長さのバランスが崩れると上下線が衝突しますが、Mutexでは衝突は起こりません。効率はSemaphoreが優位です。

Backtrace: 0x40081444:0x3ffb7ac0 0x400814ce:0x3ffb7ae0 0x400831d1:0x3ffb7b00 0x4000beaf:0x3ffb7b20 0x400859b6:0x3ffb7b40 0x40085c04:0x3ffb7b60 0x40083292:0x3ffb7b80 0x400832bd:0x3ffb7ba0 0x40083411:0x3ffb7bd0 0x400d732f:0x3ffb7bf0 0x400d3849:0x3ffb7eb0 0x400d37e4:0x3ffb7f0p⸮R⸮⸮fgFjRGuru Meditation Error: Core  0 panic'ed (LoadProhibited). Exception was unhandled.
Core 0 register dump:
PC      : 0x40081444  PS      : 0x00060c33  A0      : 0x800814d1  A1      : 0x3ffb7ac0  
A2      : 0x00000054  A3      : 0x00001800  A4      : 0x00000001  A5      : 0x00000000  
A6      : 0x3ffb83a0  A7      : 0x00000008  A8      : 0x00000001  A9      : 0x3ffb7ab0  
A10     : 0x00000001  A11     : 0x00060c23  A12     : 0x00060c23  A13     : 0x00000000  
A14     : 0x00000000  A15     : 0x00000000  SAR     : 0x00000000  EXCCAUSE: 0x0000001c  
EXCVADDR: 0x0000001d  LBEG    : 0x00000000  LEND    : 0x00000000  LCOUNT  : 0x00000000  

ELF file SHA256: 0000000000000000

FreeRTOS Binary Semaphore

FreeRTOS Counting Semaphores

シリアル通信を一つの共有リソースとして、2つのタスクで割り振って使用。

Semaphore

手旗信号; (コンピュータ) 使用中のリソースをロックし他のプログラムによるアクセスを妨げることで共有リソースへのアクセスを制限するために使われるステータス旗(スイッチ)

注) 文字数、遅いシリアルレートと小さなディレイの組合せにより、エラーが発生します。
以下ESP32で動作確認済。スタックサイズ、レート、ディレイ変更。

SemaphoreHandle_t xCountingSemaphore;
byte LED_BUILTIN = 12;
void setup() 
{
  Serial.begin(115200); // Enable serial communication library.
  pinMode(LED_BUILTIN, OUTPUT);

 // Create task for Arduino led 
  xTaskCreate(Task1, // Task function
              "Ledon", // Task name
              1000, // Stack size 
              NULL, 
              0 ,// Priority
              NULL );
   xTaskCreate(Task2, // Task function
              "Ledoff", // Task name
              1000, // Stack size 
              NULL, 
              0, // Priority
              NULL );
   xCountingSemaphore = xSemaphoreCreateCounting(1,1);
   xSemaphoreGive(xCountingSemaphore);

}

void loop() {}



void Task1(void *pvParameters)
{
  (void) pvParameters;

  for (;;) 
    {
      xSemaphoreTake(xCountingSemaphore, portMAX_DELAY); 
      Serial.println("Inside Task1 and Serial monitor Resource Taken");
      digitalWrite(LED_BUILTIN, HIGH);
      xSemaphoreGive(xCountingSemaphore);
      vTaskDelay(5);
    }
  }

void Task2(void *pvParameters)
{
  (void) pvParameters;
  for (;;) 
    {
      xSemaphoreTake(xCountingSemaphore, portMAX_DELAY);
      Serial.println("Inside Task2 and Serial monitor Resource Taken");
      digitalWrite(LED_BUILTIN, LOW);
      xSemaphoreGive(xCountingSemaphore);
      vTaskDelay(5);

  }
}

FreeRTOS Mutex

FreeRTOS-mutex-example-with-shared-resource

Mutex

共通情報源への同期多重アクセス(一度に一つのプログラムへのアクセスと他の全てを遮断するロック - アンロック スイッチを使う)

注) 文字数、シリアルレートに関わらず動作。
以下同一タスクのインスタンスを2つ用意し1つのリソースを共有。
ESP32で動作確認済。スタックサイズのみ変更。

//create handle for the mutex. It will be used to reference mutex
SemaphoreHandle_t  xMutex;

void setup()
{
  // Enable serial module of Arduino with 9600 baud rate
  Serial.begin(9600);
// create mutex and assign it a already create handler 
  xMutex = xSemaphoreCreateMutex();
// create two instances of task "OutputTask" which are used to display string on 
// arduino serial monitor. We passed strings as a paramter to these tasks such as ""Task 1 //#####################Task1" and "Task 2 ---------------------Task2". Priority of one //instance is higher than the other
  xTaskCreate(OutputTask,"Printer Task 1", 1000,"Task 1 #####################Task1 \r\n",1,NULL);
  xTaskCreate(OutputTask,"Printer Task 2", 1000,"Task 2 ---------------------Task2 \r\n",2,NULL);    
}

// this is a definition of tasks 
void OutputTask(void *pvParameters)
{
  char *pcStringToPrint;
  pcStringToPrint = (char *)pvParameters;
  while(1)
  {
    printer(pcStringToPrint);
    vTaskDelay(pdMS_TO_TICKS(100));
  }
}
// this printer task send data to arduino serial monitor
//aslo it is shared resource between both instances of the tasks
void printer(const char* pcString)
{
  // take mutex
  xSemaphoreTake(xMutex, portMAX_DELAY);
  Serial.println(pcString); // send string to serial monitor
  xSemaphoreGive(xMutex); // release mutex
}
void loop(){}

FreeRTOS Gatekeeper

MutexによるDeadlock回避のためのGatekeeper

static const char *pcStringToPrint[] =
{
  "Task 1 ############################## Task1 \r\n",
  "Task 2 ------------------------------ Task2 \r\n",  
};

QueueHandle_t xPrintQueue;
void setup()
{
   Serial.begin(9600);
  xPrintQueue = xQueueCreate(5,sizeof(char *));
  xTaskCreate(SenderTask,"Printer Task1", 1000,(void *)0,1,NULL );
  xTaskCreate(SenderTask,"Printer Task2", 1000,(void *)1,2,NULL );
  xTaskCreate(GateKeeperTask, "GateKeeper", 1000,NULL,0,NULL);
  
}
void SenderTask(void *pvParameters)
{
  int indexToString ;
  indexToString = (int)pvParameters;

  while(1)
  {
   xQueueSend(xPrintQueue,&(pcStringToPrint[indexToString]),portMAX_DELAY); 
   vTaskDelay(pdMS_TO_TICKS(100));

  }
  
}

void GateKeeperTask(void *pvParameters)
{
  char *pcMessageToPrint;
  while(1)
  {
   xQueueReceive(xPrintQueue,&pcMessageToPrint,portMAX_DELAY);
   Serial.println(pcMessageToPrint); 
  }
}

void loop(){}
void vATaskFunction( void *pvParameters )
    {
        for( ;; )
        {
            -- Task application code here. --
        }

        /* Tasks must not attempt to return from their implementing
        function or otherwise exit.  In newer FreeRTOS port
        attempting to do so will result in an configASSERT() being
        called if it is defined.  If it is necessary for a task to
        exit then have the task call vTaskDelete( NULL ) to ensure
        its exit is clean. */
        vTaskDelete( NULL );
    }

FreeRTOS導入スケッチ

Mutexでマルチタスクを実行。動作は時系列によるループ関数と同等だが、タスクの管理・増減・優先度の変更などを容易にするメリットがあります。

/*
  Optical SP02 Detection (SPK Algorithm) using the MAX30102 Breakout
  Mesuring Temperatures of Object and Ambient by MLX90614
  on ESP32
  By: Takanobu Fuse @ FICUSONLINE(F9E) https://ficus.myvnc.com
  Date: June 2nd, 2021

  Hardware Connections (Breakoutboard to Arduino):
  -5V = 5V (3.3V is allowed)
  -GND = GND
  -SDA = A4 (or SDA)
  -SCL = A5 (or SCL)
  -INT = Not connected
 
  The MAX30102 Breakout can handle 5V or 3.3V I2C logic.
*/

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_MLX90614.h>
#include "MAX30105.h"
#include "heartRate.h"
#include "spo2_algorithm.h"

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
#define OLED_RESET     -1 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

MAX30105 particleSensor;

Adafruit_MLX90614 mlx = Adafruit_MLX90614();

#define MAX_BRIGHTNESS 255

// MAX30102 Check Presence variables
bool checkFlag = 0; // Flag for finding error
long unblockedValue; //Average IR at power up

// MAX30102 Heart Rate variables
uint32_t chkCount;
const byte RATE_SIZE = 4; //Increase this for more averaging. 4 is good.
byte rates[RATE_SIZE]; //Array of heart rates
byte rateSpot = 0;
long lastBeat = 0; //Time at which the last beat occurred

int32_t beatsPerMinute;
int32_t beatAvg;
long redValue;
long delta;

// MAX30102 Spo2 variables
uint32_t irBuffer[100]; //infrared LED sensor data
uint32_t redBuffer[100];  //red LED sensor data
int32_t bufferLength; //data length
int32_t spo2; //SPO2 value
int8_t validSPO2; //indicator to show if the SPO2 calculation is valid
int32_t heartRate; //heart rate value
int8_t validHeartRate; //indicator to show if the heart rate calculation is valid

// MAX30102 Config
byte ledBrightness = 0x1F; //Options: 0=Off to 255=50mA
byte sampleAverage = 1; //Options: 1, 2, 4, 8, 16, 32
byte ledMode = 2; //Options: 1 = Red only, 2 = Red + IR, 3 = Red + IR + Green
byte sampleRate = 100; //Options: 50, 100, 200, 400, 800, 1000, 1600, 3200
int pulseWidth = 411; //Options: 69, 118, 215, 411
int adcRange = 4096; //Options: 2048, 4096, 8192, 16384

// Led indicators
byte blueLED = 12; //Must be on PWM pin
byte greenLED = 13; //Blinks with each data read

// Final Outputs
typedef struct {
  char *title1;
  char *title2;
  char *value1;
  char *value2;
  char *base_unit1;
  char *base_unit2;
} finalData;

int str_len;
char char_array1[10];
char char_array2[10];

SemaphoreHandle_t  xMutex;

void setup() {
  
  // initialize serial communication at 115200 bits per second:
  Serial.begin(9600);
  // create mutex and assign it a already create handler 
  xMutex = xSemaphoreCreateMutex();
  
  pinMode(blueLED, OUTPUT);
  pinMode(greenLED, OUTPUT);

  // Initialize sensor
  if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) //Use default I2C port, 400kHz speed
  {
    Serial.println(F("MAX30105 was not found. Please check wiring/power."));
    while (1);
  }

  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { 
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
  }

  delay(1000);
  display.clearDisplay();
  display.setTextColor(WHITE);

  // MAX30102
  particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange); //Configure sensor with these settings
  // MLX90614
  mlx.begin();
 
  // set up 4 tasks to run independently.
  xTaskCreate(
    preCheck
    ,  "preCheck"   // A name just for humans
    ,  2000  // This stack size can be checked & adjusted by reading the Stack Highwater
    ,  NULL
    ,  3 // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,  NULL);
    
  xTaskCreate(
    hrMax30102
    ,  "hrMax30102"   // A name just for humans
    ,  3000  // This stack size can be checked & adjusted by reading the Stack Highwater
    ,  NULL
    ,  2 // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,  NULL); 
  
  xTaskCreate(
    spo2Max30102
    ,  "spo2Max30102"   // A name just for humans
    ,  4000  // This stack size can be checked & adjusted by reading the Stack Highwater
    ,  NULL
    ,  1  // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,  NULL); 

  xTaskCreate(
    tempMlx90614
    ,  "tempMlx90614"   // A name just for humans
    ,  3000  // This stack size can be checked & adjusted by reading the Stack Highwater
    ,  NULL
    ,  0  // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,  NULL); 
    
}

void loop() {
  
}

//**************************TASKS***********************************
//******************************************************************

// MAX30102:Checking Presence
void preCheck(void *pvParameters)
{
  (void) pvParameters;

  xSemaphoreTake(xMutex, portMAX_DELAY);
  
  for (;;) 
  {
    particleSensor.setPulseAmplitudeRed(0x00); //Red Led off
  
    //Take an average of IR readings at power up
    unblockedValue = 0;
    
    for (byte x = 0 ; x < 32 ; x++)
    {
      unblockedValue += particleSensor.getIR(); //Read the IR value
    }
    unblockedValue /= 32;
  
  
    Serial.print("IR ");
    Serial.print(particleSensor.getIR());
  
    long currentDelta = particleSensor.getIR() - unblockedValue;
  
    Serial.print(", delta ");
    Serial.println(currentDelta);
  
    if (currentDelta > (long)500)
    {
      Serial.println(" Something is there!");
      particleSensor.setPulseAmplitudeRed(ledBrightness);
      
      xSemaphoreGive(xMutex); // release mutex
      vTaskDelay(pdMS_TO_TICKS(100));
      vTaskDelete(NULL);
    }
    displayNotice();
  }
}

// MAX30102:Mesuring Heart Beat
void hrMax30102(void *pvParameters)
{
  (void) pvParameters;

  xSemaphoreTake(xMutex, portMAX_DELAY);
  
  checkFlag=false;
  digitalWrite(blueLED, 1);
  displayChecking();
  
  particleSensor.setup(); //Configure sensor with default settings
  particleSensor.setPulseAmplitudeRed(0x0A); //Turn Red LED to low to indicate sensor is running
  particleSensor.setPulseAmplitudeIR(0); //Turn off IR LED
  
  for(int i=0; i<1500; i++) {
    
    digitalWrite(blueLED, !digitalRead(blueLED)); //Blink onboard LED with every data read

    redValue = particleSensor.getRed();
  
    if (checkForBeat(redValue) == true)
    {
      //We sensed a beat!
      delta = millis() - lastBeat;
      lastBeat = millis();
  
      beatsPerMinute = 60 / (delta / 1000.0);
  
      if (beatsPerMinute < 255 && beatsPerMinute > 20)
      {
        rates[rateSpot++] = (byte)beatsPerMinute; //Store this reading in the array
        rateSpot %= RATE_SIZE; //Wrap variable
  
        //Take average of readings
        beatAvg = 0;
        for (byte x = 0 ; x < RATE_SIZE ; x++)
          beatAvg += rates[x];
        beatAvg /= RATE_SIZE;
      }
    }
  
    Serial.print("Red=");
    Serial.print(redValue);
    Serial.print(", BPM=");
    Serial.print(beatsPerMinute);
    Serial.print(", Avg BPM=");
    Serial.print(beatAvg);

    if (redValue < 10000) {
      chkCount++;
      Serial.print(" No finger?");
      if (chkCount > 30) {
        break;
      }
    }  
  
    Serial.println();
    if(i > 500) {
      if((beatAvg >30) && (beatsPerMinute > 0.95*beatAvg && beatsPerMinute < 1.05*beatAvg)) {
        checkFlag=true;
        break;
      }
    }
  }

  
  digitalWrite(blueLED, 0);
  
  if(checkFlag==true) {
    finalData *p = (finalData *)malloc(sizeof(finalData));
    
    p->title1 = "Average BPM";
    p->title2 = "BPM";

    p->value1 = strToChar1(String(beatAvg));
    p->value2 = strToChar2(String(beatsPerMinute));
       
    p->base_unit1 = "bpm";
    p->base_unit2 = "bpm";
    
    displayFinal(p);
    
  }else {
    displayError();
  }
  
  vTaskDelay(pdMS_TO_TICKS(100));
  vTaskDelete(NULL);
}

// MAX30102:Mesuring SpO2 Pulse Oximeter
void spo2Max30102(void *pvParameters)
{
  (void) pvParameters;

  xSemaphoreTake(xMutex, portMAX_DELAY);

  checkFlag=false;
  digitalWrite(greenLED, 1);
  displayChecking();

  // max30102
  particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange); //Configure sensor with these settings

  bufferLength = 100; //buffer length of 100 stores 4 seconds of samples running at 25sps

  //read the first 100 samples, and determine the signal range
  for (byte i = 0 ; i < bufferLength ; i++)
  {
    while (particleSensor.available() == false) //do we have new data?
      particleSensor.check(); //Check the sensor for new data

    redBuffer[i] = particleSensor.getRed();
    irBuffer[i] = particleSensor.getIR();
    particleSensor.nextSample(); //We're finished with this sample so move to next sample

    Serial.print(F("red="));
    Serial.print(redBuffer[i], DEC);
    Serial.print(F(", ir="));
    Serial.println(irBuffer[i], DEC);
  }

  //calculate heart rate and SpO2 after first 100 samples (first 4 seconds of samples)
  maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);

  //Continuously taking samples from MAX30102.  Heart rate and SpO2 are calculated every 1 second
  for (int x=0; x< 25; x++)
  {
    //dumping the first 25 sets of samples in the memory and shift the last 75 sets of samples to the top
    for (byte i = 25; i < 100; i++)
    {
      redBuffer[i - 25] = redBuffer[i];
      irBuffer[i - 25] = irBuffer[i];
    }

    //take 25 sets of samples before calculating the heart rate.
    for (byte i = 75; i < 100; i++)
    {
      while (particleSensor.available() == false) //do we have new data?
        particleSensor.check(); //Check the sensor for new data

      digitalWrite(greenLED, !digitalRead(greenLED)); //Blink onboard LED with every data read

      redBuffer[i] = particleSensor.getRed();
      irBuffer[i] = particleSensor.getIR();
      particleSensor.nextSample(); //We're finished with this sample so move to next sample

      //send samples and calculation result to terminal program through UART

      Serial.print(F("red="));
      Serial.print(redBuffer[i], DEC);
      Serial.print(F(", ir="));
      Serial.print(irBuffer[i], DEC);

      Serial.print(F(", HR="));
      Serial.print(heartRate, DEC);

      Serial.print(F(", HRvalid="));
      Serial.print(validHeartRate, DEC);

      Serial.print(F(", SPO2="));
      Serial.print(spo2, DEC);

      Serial.print(F(", SPO2Valid="));
      Serial.println(validSPO2, DEC);
    }

    //After gathering 25 new samples recalculate HR and SP02
    maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);

    if((heartRate > 0.8*beatAvg && heartRate < 1.2*beatAvg) && (validHeartRate==1 && validSPO2==1)) {
      checkFlag=true;
      break;
    }
  }

  digitalWrite(greenLED, 0);
  
  if(checkFlag==true) {
    finalData *p = (finalData *)malloc(sizeof(finalData));
    
    p->title1 = "SpO2";
    p->title2 = "Heart Rate(BPM)";

    p->value1 = strToChar1(String(spo2));
    p->value2 = strToChar2(String(heartRate));
    
    p->base_unit1 = "%";
    p->base_unit2 = "bpm";
    
    displayFinal(p);
    
  } else {
    displayError();
  }
  
  vTaskDelay(pdMS_TO_TICKS(100));
  vTaskDelete(NULL);
}

// MLX90614:Measuring Object Temp and Ambient Temp
void tempMlx90614(void *pvParameters)
{
  (void) pvParameters;
  
  xSemaphoreTake(xMutex, portMAX_DELAY);
  
  finalData *p = (finalData *)malloc(sizeof(finalData));
  
  p->title1 = "TEMPERATURE:";
  p->title2 = "AMBIENT:";

  p->value1 = strToChar1(String(mlx.readObjectTempC()));
  p->value2 = strToChar2(String(mlx.readAmbientTempC()));

  p->base_unit1 = "`C";
  p->base_unit2 = "`C";
  
  displayFinal(p); 

  ESP.restart();
}

// Output data will be transformed to char from string
char *strToChar1(String str) {
  str_len=str.length()+1;
  str.toCharArray(char_array1, str_len);
  return char_array1;
}
char *strToChar2(String str) {
  str_len=str.length()+1;
  str.toCharArray(char_array2, str_len);
  return char_array2;
}

//************************OLED Display Functions*********************************
//*****************************************************************************

void displayNotice() {
  display.clearDisplay();
  display.setTextSize(2);
  display.setCursor(0,0);
  display.print("Start >>>");
  display.setTextSize(1);
  display.setCursor(0,30);
  display.print("Put Your Finger on");
  display.display();
  delay(2000);
  display.clearDisplay();
  display.display();
  delay(500);
}

void displayFinal(finalData *p) {
  display.clearDisplay();
  
  display.setTextSize(2);
  display.setCursor(0,0); 
  display.print(p->value1);
  display.print(" ");
  display.print(p->base_unit1);
  
  display.setTextSize(1);
  display.setCursor(0,18);
  display.print(p->title1);
  
  display.setTextSize(2);
  display.setCursor(0,34); 
  display.print(p->value2);
  display.print(" ");
  display.print(p->base_unit2);
  
  display.setTextSize(1);
  display.setCursor(0,52);
  display.print(p->title2);
  
  delay(10);
  yield();
  display.display();
  delay(3000);  
  
  xSemaphoreGive(xMutex); // release mutex
}

void displayError() {
  display.clearDisplay();
  display.setTextSize(1);
  display.setCursor(20,30);
  display.print(F("Read Error..."));
  display.setCursor(20,40);
  display.print(F("Try Again!"));
  
  delay(10);
  yield();
  display.display();
  delay(3000);
  
  xSemaphoreGive(xMutex); // release mutex
  tempMlx90614(NULL);
}

void displayChecking() {
  delay(500);
  display.clearDisplay();
  display.setTextSize(2);
  display.setCursor(0,0);
  display.print("Checking..");
  display.setTextSize(1);
  display.setCursor(0,30);
  display.print("Keep Your Finger");
  display.setCursor(0,40);
  display.print("putting on");
  display.display();
}

ホストネームの変更とその呼出

デフォルトのホストネームは "espressif"
以下 "ESP32"へ変更する場合

#define HOSTNAME "ESP32"

void setup(void){
 ....
 .....
    WiFi.setHostname(HOSTNAME); 
.....
.....
    Serial.println(WiFi.getHostname());
.....
.....

Arduino IDEからVS Code+PlatformIOへ移行

C(.ini)またはC++(.cpp)適用ルールの相違に注意

Arduinoライブラリを直接ダウンロードして platformio.ini で指定

PlatformIO ESP32 FreeRTOS 設定ファイル

.platformio/packages/framework-arduinoespressif32/tools/sdk/include/freertos/freertos/FreeRTOSConfig.h

https://github.com/espressif/arduino-esp32/blob/master/tools/sdk/esp32/include/freertos/include/freertos/FreeRTOSConfig.h

Arduino IDE —> C
PlatformIO —> C++

以下PlatformIOでもビルド・コンパイルできるが、警告がでるので

char* p = "abc"; // valid in C, invalid in C++

C++でも適合するように変更

char const *p = "abc"; // valid and safe in either C or C++.

上記と同義

const char *p = "abc"; // valid and safe in either C or C++.

MQTTサーバへデータをパブリッシュする機能追加

以下MQTTクライアントによるデータパブリッシュ機能を追加した最終スケッチをアップロード。

このデータをホームオートメーションシステムにより条件処理し、IP電話により異常を知らせます。


APモードによる設定画面は以下の通り。


WiFiマネージャーメニュー

OLED表示部回転

筐体内でのOLED表示部のレイアウト上、表示を90度または180度回転させる必要がある場合にAdafruit GFX Graphics Libraryvoid setRotation(uint8_t rotation);を利用します。

setup()内で以下のように指定

.....
.....
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
#define OLED_RESET     -1 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
.....
.....
setup() {
.....
.....
display.clearDisplay();
display.setTextColor(WHITE);
display.setRotation(2);
.....
.....
}
  • setRotation(0) ---> 0 deg
  • setRotation(1) ---> 90 deg
  • setRotation(1) ---> 180 deg
  • setRotation(3) ---> 270 deg

OLED表示エラー対応

OLEDの表示が大きく崩れる現象。クロック周波数を下げることで対応。
https://esp32.com/viewtopic.php?t=8674

Projects/esp32_max30102_mlx90614_wifi/.pio/libdeps/esp32dev/Adafruit SSD1306/Adafruit_SSD1306.h

class Adafruit_SSD1306 : public Adafruit_GFX {
public:
  // NEW CONSTRUCTORS -- recommended for new projects
  Adafruit_SSD1306(uint8_t w, uint8_t h, TwoWire *twi = &Wire,
                   int8_t rst_pin = -1, uint32_t clkDuring = 400000UL,
                   uint32_t clkAfter = 100000UL);

上記クラスからクロック周波数を再定義

#define OLED_RESET     -1 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET, 100000UL, 100000UL);

動作デモ動画