ESP32-WROOM-32EのEEPROMを使用する方法

組み込みエンジニア

こんにちは、ENGかぴです。

ESP32-WROOM-32EはArduino環境で開発できるためEEPROMライブラリを使うことで電源をOFFしてもデータ保持ができます。EEPROMのライブラリを使ってデータの読み書きを行う方法と使用例をまとめています。

ESP32シリーズではEEPROMは非推奨(データシートのペリフェラルにも記載なし)でありFLASHを使うことが推奨されています。Arduino環境でEEPROMを使った開発できるため下位互換としてEEPROMライブラリが実装されています。

ESP32-WROOM-32Eの開発環境はArduino IDEを使用しています。またESP32-WROOM-32E開発ボード(秋月電子)及びLCDはAQM1602XA-RN-GBW(秋月電子)を使用しています。LCDの使い方については下記記事にまとめています。

Seeeduino XIAOを使ってBME280のデータをLCDに表示する

ESP32-WROOM-32Eで動作確認したことについてリンクをまとめています。

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

EEPROMの使い方

ESP32 Dev Module用のスケッチ例にEEPROMが準備されているためスケッチ例を流用しながら使い方をまとめています。

EEPROMライブラリではEEPROMクラスが実装されておりユーザーが定義した領域と予め準備されている領域を使ってEEPROMを使用することができます。

EEPROMの初期設定

#include "EEPROM.h"

EEPROMClass  MAIN("eeprom1", 0x80); //ユーザー定義のEEP領域
EEPROMClass  CHK("eeprom2", 0x80); //ユーザー定義のEEP領域

EEPROMライブラリを使用するためにEEPROM.hをインクルードします。ユーザー定義のEEP領域を使用する場合はEEPROMClassの変数を定義します。

例ではMAINとCHKを宣言しています。MAINでは領域名を「eeprom1」としサイズを0x80(128バイト)としています。CHKでは領域名を「eeprom2」としてサイズを0x80(128バイト)としています。

ユーザー定義の領域を使用しない場合は必要ありません。ユーザー定義の領域を使用しない場合は「eeprom」領域(EEPROMの宣言で使用)となります。

EEPROM動作開始

void EepRead(void){
  //ユーザー定義の領域を使用する場合
  if( !MAIN.begin(MAIN.length())){
    Serial.println("Restarting...");
    delay(1000);
    ESP.restart();
  }
  //通常の領域を使用する場合
  if( !EEPROM.begin(0x100)){
    Serial.println("Restarting3...");
    delay(1000);
    ESP.restart();    
  } 
}

ユーザー定義の領域を使用する場合と通常の領域を使用する場合どちらも最初にbegin()メソッドでEEPROMの領域を確定する必要があります。begin()メソッドで使用するサイズを指定しなかった場合はデータの読み書きができません。

begin()メソッドの引数に使用したいサイズを指定します。指定するサイズがEEPROMの領域をオーバーすると動作が不安定になるためbegin()でサイズを指定した戻り値でサイズがオーバーしていないかの確認を行っています。

最大のサイズ長については把握していませんが0x1000を超えたあたりからサイズオーバーになることがあります。今回の例では合計で0x2800程度が限界でした。

サイズオーバーした場合は動作が不安定となる可能性があるためリスタートしています。リスタートしてもサイズオーバーすることは変わりがないため繰り返しリスタートしてしまいますが、通常動作中に暴走するよりかはましです。

EEPROMのデータを読み込む

void EepRead(void){
  uint8_t dat;
  uint8_t chk;

  //begin()メソッド
  MAIN.get(0,dat);
  CHK.get(0,chk);

  if(dat == chk){ //二重化チェック
    if( dat == 0xFF){
      dat = 0;
    }
    btncntInit = dat;
  }
}

get()メソッドを使用すると第2引数で指定した変数の型に応じた読み込みができます。Read()メソッドを使用した場合は1バイトデータの読み込みになります。get()メソッドの第1引数は読み込みをスタートする位置を指定します。

MAINは「eeprom1」・CHK「eeprom2」の領域に同じデータを書き込んでいます。EEPROMのデータを読み込んだ時データの健全性を確認するために2つの領域のデータを比較しています。

2つのデータが一致する場合はEEPROMのデータが健全であるとみなして変数に格納します。EEPROMのデータはイレースした場合や初期状態では0xFFとなるため0xFFが採用されないようにしています。

2つのデータを比較して健全性を確認する方法以外にも読み込んだデータの範囲チェックを行って健全性を確認する方法もあります。データが健全でない場合にシステムが異常動作しないようにデフォルト値を決めておくなど工夫が必要な場合もあります。

EEPROMへデータを書き込む

void mainApp(void){

  if( btncnt != btncntInit){ //データに変更があったか
    MAIN.put(0,btncnt); //EEP書き込み準備
    MAIN.commit(); //書き込み確定
    CHK.put(0,btncnt);
    CHK.commit();
    //EEPROM.put(0,btncnt);
    //EEPROM.commit();
    btncntInit = btncnt;
  }
}

EEPROMへの書き込みの回数を限定するためデータに変化があった場合のみ書き込み処理に遷移するようにしています。

put()メソッドを使用すると第2引数の型に応じた書き込みができます。Write()メソッドを使用した場合は第1引数に指定したアドレスから第2引数に指定したデータを1バイトずつ書き込みます。

put()メソッドの第1引数は書き込みを開始する位置を指定します。put()やWrite()は書き込み開始ではなく書き込みの準備となるためcommit()メソッドによってデータを書き込みます。

私の場合は2つのデータでチェックを行った後に範囲チェックする方法でソフト開発を行うことが多いのですがEEPROMがハード的に問題となったことが一度もないため範囲チェックのみ十分だと感じています。

EEPROMの書き込み回数が有限であるため可能な限り不要な書き込みを防ぐことが推奨されます。上の例では2つの変数が不一致であった場合のみEEPROMのデータの書き込みを行うようにしています。

動作確認

動作確認用の回路図
動作確認用の回路図

ESP32-WROOM-32EのWire(I2C)をLCDに接続しSW1をDIに接続します。SW1を押した回数をLCD上に表示します。SW1を2秒間長押しすると現在のボタンを押した回数をEEPROMに書き込みます。長押しする際もSW1を押した回数がカウントされるためEEPROMに書き込む前にカウント値を-1するようにしています。

EEPROMへの書き込みが終わる(カウントが-1された後)と電源をOFF(モジュール上のリセットボタンでもよい)します。EEPROMに保存されているデータは電源をOFFしても保持されるため次に電源をONしたときにEEPROMのデータの確認を行いデータが消えていないことを確認します。

EEPROMのデータを読み込んでLCDに表示
EEPROMのデータを読み込んでLCDに表示

電源をONすると初期画面が2秒表示されます。メイン処理に遷移すると初期化時に読み込んだEEPROMのデータを表示します。LCDの1段目に「EEP DATA READ:15」と表示されているためEEPROMのデータが読み出せていることが分かります。

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。チェックはしておりますがコメントなど細かな部分で間違っている可能性があります。参考としてお使いいただければと思います。

#include <Wire.h>
#include "EEPROM.h"

#define SLAVE_ADRS 0x3E
#define LINE1_ADRS 0x40
#define LINE2_TOP (0x40 +0x80)
#define FUNC1_SET 0x38
#define FUNC2_SET 0x39
#define INT_OSC 0x14
#define CONST_SET 0x70
#define PWR_ICON_SET 0x56
#define FOLLOWER_SET 0x6B
#define CLR_DISP 0x01
#define DISP_ONOFF_SET 0x0C

#define PIN_DI1 33
#define PIN_DO1 25

#define TIME_OFF -1     //タイマーを使用しない場合
#define TIME_UP 0       //タイムアップ
#define DI_NUM 4        //DIフィルタのサンプリング数
#define	BASE_CNT 10     //10ms×10 = 100ms
#define TIME_FILTER_MAX 1
#define EEP_MAX 200
#define LCD_TIM_MAX 10
#define LED_TIM_MAX 50

typedef struct{
  uint8_t    wp;
  uint8_t    buf[ DI_NUM ];
  uint8_t    di;
}FILT_DATA;

EEPROMClass  MAIN("eeprom1", 0x80); //ユーザー定義のEEPエリア
EEPROMClass  CHK("eeprom2", 0x80); //ユーザー定義のEEPエリア

/* 変数の宣言 */
FILT_DATA DiData;
uint8_t initmoji[2][16] ={"ESP32-EEP-Test ","        Ver0.01"};
uint8_t CntInit;	//初期化時のみ使用
int8_t timfilter;   //DIフィルタ起動
int16_t timlcd;
int16_t timled;
int16_t timbtnwait = TIME_OFF;
uint32_t beforetimCnt = millis();
uint8_t btncnt;
uint8_t btncntInit;
uint8_t ledcnt;
bool btn1hold;

/* プロトタイプ宣言 */
void LcdInit(void);
void LcdComand(uint8_t cmd);
void LcdWriteData(uint8_t *data, uint8_t sz);
void DspLine2Top(void);
void DspClear(void);
void ShowData(void);
void mainTimer(void);
void mainApp(void);
void DiFilterInit(void);
void DiFilter(void);

void setup() {
  
  pinMode(PIN_DI1,INPUT_PULLUP);
  pinMode(PIN_DO1,OUTPUT);
  Wire.begin();
  Serial.begin(115200);
  EepRead();  //EEPのデータを読み出す
  DiFilterInit();
  LcdInit();
  Serial.println("start");
}

void loop() {
  mainTimer();
  DiFilter();
  mainApp();
}
/* メインアプリ処理 */
void mainApp(void){

  if( DiData.di == 0){
    if( btn1hold == false ){
      btn1hold = true;
      if( timbtnwait == TIME_OFF ){
        timbtnwait = EEP_MAX;
      }
      if(++btncnt >= 100 ){
        btncnt = 0;
      }
    }
  }
  else{
    timbtnwait = TIME_OFF;
    btn1hold = false;
  }    
 
  if( timlcd == TIME_UP ){
    timlcd = LCD_TIM_MAX;
    ShowData(); //LCD表示部分をセット
  }

  if( timled == TIME_UP ){
    timled = LED_TIM_MAX;

    if( digitalRead(PIN_DO1)){
      digitalWrite(PIN_DO1,LOW);
    }
    else{
      digitalWrite(PIN_DO1,HIGH);
    }

    if( ++ledcnt >= btncntInit*2){
      timled = TIME_OFF;
    }
  }
    
  if( timbtnwait == TIME_UP){
    timbtnwait = TIME_OFF;
        
    if(btncnt != 0x00){
       --btncnt;
    }
        
    if( btncnt != btncntInit){
      MAIN.put(0,btncnt); //EEP書き込み準備
      MAIN.commit(); //書き込み確定
      CHK.put(0,btncnt);
      CHK.commit();
      //EEPROM.put(0,btncnt);
      //EEPROM.commit();
      Serial.println("eep-ok");
      btncntInit = btncnt;
    }
  }
}
/* EEPデータ読み込み処理 */
void EepRead(void){
  uint8_t dat;
  uint8_t chk;

  if( !MAIN.begin(MAIN.length())){
    Serial.println("Restarting...");
    delay(1000);
    ESP.restart();
  }

  if( !CHK.begin(CHK.length())){
    Serial.println("Restarting2...");
    delay(1000);
    ESP.restart();    
  }

// if( !EEPROM.begin(0x1000)){
// Serial.println("Restarting3...");
//  delay(1000);
//  ESP.restart();    
} 

  MAIN.get(0,dat);
  CHK.get(0,chk);
  //EEPROM.get(0,dat);
  //EEPROM.get(0x80,chk);
  
  Serial.print("dat: ");   Serial.println(dat);
  Serial.print("chk: "); Serial.println(chk);  
  Serial.print("dat2: ");   Serial.println(dat2);
  Serial.print("chk2: "); Serial.println(chk2);  
    
  if(dat == chk){ //二重化チェック
    if( dat == 0xFF){
      dat = 0;
    }
    btncntInit = dat;
  }
}
/* LCD初期化処理 */
void LcdInit(void){
 
  LcdComand(FUNC1_SET); //8ビットバス・2LINE表示
  LcdComand(FUNC2_SET); //拡張コマンド
  LcdComand(INT_OSC); //内部周波数調整
  LcdComand(CONST_SET); //コントラスト1
  LcdComand(PWR_ICON_SET); //コントラスト2
  LcdComand(FOLLOWER_SET); //フォロワー制御
  LcdComand(FUNC1_SET); //拡張コマンドをオフ
  DspClear();
  LcdComand(DISP_ONOFF_SET);
  LcdWriteData( &initmoji[0][0], sizeof(initmoji[0])); //1段目の表示
  DspLine2Top(); //カーソル移動
  LcdWriteData( &initmoji[1][0], sizeof(initmoji[1])); //2段目の表示
  delay(2000);
}
/* LCDへのコマンド処理 */
void LcdComand(uint8_t cmd){

  Wire.beginTransmission(SLAVE_ADRS);
  Wire.write(0x00);
  Wire.write(cmd);
  Wire.endTransmission();
  delay(1);
}
/* LCDに文字を表示 */
void LcdWriteData(uint8_t *data, uint8_t sz){

  Wire.beginTransmission(SLAVE_ADRS);
  Wire.write(0x40);
  Wire.write(data, sz);
  Wire.endTransmission();
}
/* 2段目にカーソル移動 */
void DspLine2Top(void){
    
  LcdComand(LINE2_TOP);
  delayMicroseconds(40);
}
/* ディスプレイクリア */
void DspClear(void){
  
  LcdComand(CLR_DISP);
  delayMicroseconds(40);
}
/* ディスプレイ表示 */
void ShowData(void){
  char line1[16];
  char line2[16];

  DspClear();
  sprintf(line1, "EEP DATA READ:%u", btncntInit);
  sprintf(line2, "btncnt:%u", btncnt);

  LcdWriteData( (uint8_t*)&line1, sizeof(line1)); //1段目の表示
  DspLine2Top(); //2段目にカーソル移動
  LcdWriteData( (uint8_t*)&line2, sizeof(line2)); //2段目の表示
}
//--------SWのDIフィルタ----------------------------
void DiFilter(void){
  uint8_t i;
  bool    boo = true;

  if( timfilter == TIME_UP ){
    timfilter = TIME_FILTER_MAX;
    DiData.buf[ DiData.wp ] = digitalRead(PIN_DI1);

    for( i = 1; i < DI_NUM; i++){
      if( DiData.buf[i-1] != DiData.buf[i] ){
        boo = false;
        break;
      }
    }

    if( boo ){ ////比較して一致したら値を採用
       DiData.di = DiData.buf[ 0 ]; 
    }

    if( ++DiData.wp >= DI_NUM){
      DiData.wp = 0;
    }
  }
}
// DIフィルタの初期化 //
void DiFilterInit(void){
	
  CntInit = BASE_CNT;
  while(CntInit > 0){  //0になるまでフィルタを実施
    DiFilter();         //DIフィルタ処理
    delay(10);     //10ms遅延させてDIフィルタ処理
    CntInit--;
    timfilter = TIME_UP;
  }    
}
/* タイマ管理関数 */
void mainTimer(void){

  if ( millis() - beforetimCnt > BASE_CNT ){
    beforetimCnt = millis();
            
    if( timlcd > TIME_UP ){
        --timlcd;
    }
    if( timfilter > TIME_UP ){
        --timfilter;
    }
    if( timbtnwait > TIME_UP ){
        --timbtnwait;
    }
    if( timled > TIME_UP ){
        --timled;
    }
  }
}

関連リンク

Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

RUNTEQ-プログラミングで自由を手に入れる

最後まで、読んでいただきありがとうございました。

タイトルとURLをコピーしました