ESP32マウス(shota)マウス自作研修

ESP32マウスPart.42 フラッシュメモリにデータを保存する

esp32_filesystem ESP32マウス(shota)

こんにちは、shotaです。
このシリーズではESP32を搭載したオリジナルマイクロマウスEspecialを製作します。

前々回から、フラッシュメモリを扱うためにESP32の中身を紹介してきました。

今回は本番です。Especialでフラッシュメモリを扱うプログラムを実行します。

ブロクで書いたサンプルコードはGitHubに公開しています。

サンプルプログラムの紹介

ESP32のアドレスマッピングやファイルシステムについては前回までの記事で説明しました。
そのため今回はサンプルプログラムの紹介からスタートします。

プログラムの実行動画

こちらが今回作成したプログラムです。

何をしているかと言うと、

  1. 起動時にフラッシュメモリからLED点灯パターンを読み取り、
  2. LED点灯パターンによって、LED点滅周期を切り替える。
  3. IMUからX軸回りの角速度を読み取り、
  4. 角速度がしきい値を超えたらLED点灯パターンインクリメント/デクリメントし、
  5. 更新されたLED点灯パターンをフラッシュメモリに保存する。

です。

サンプルプログラムの実行方法

今回実行するサンプルプログラムは7_loggerです。

$ get_idf
$ cd ~/esp/especial/examples/7_logger
$ idf.py -p /dev/ttyUSB0 flash monitor

ESP-IDFの環境構築方法は過去の記事を参照してください。

サンプルプログラムの解説

それではプログラムを解説していきます。

GitHubにアップロードしたソースコードも参照してください。

サンプルプログラムのフォルダ構成

サンプルプログラムのフォルダ構成を見てみましょう。

.
├── CMakeLists.txt
├── README.md
├── components
│   └── icm20648
│       ├── CMakeLists.txt
│       ├── icm20648.cpp
│       └── include
│           └── icm20648.h
├── main
│   ├── CMakeLists.txt
│   └── main.cpp
├── partitions_example.csv
└── sdkconfig.defaults

これまでのサンプルプログラムと少し様子が違います。

まず、IMUモジュールICM-20648との通信ライブラリ(icm20648.cpp、icm20648.h)をコンポーネントとして取り込みました。
ライブラリの中身については過去の記事を参照してください。

ESP-IDFはプロジェクトのコンパイル時にcomponentsというフォルダを見に行きます。
componentsフォルダにライブラリを保存しておけば、自動でライブラリを読み込んでくれるので、面倒なパスの設定は不要です。
icm20648.cpp/CMakeLists.txtの中身もシンプルです。)

コンポーネントの詳細については公式ドキュメントを参照してください。

partitions_example.csvも新しく登場したテキストファイルですが、こちらは前回の記事で紹介したWear Levellingサンプルからコピーしたものです。
このファイルにはフラッシュメモリ内のパーティションテーブルが書かれています。

サンプルプログラムのパーティションテーブル

それではpartitions_example.csvを見てみましょう。

# Name,   Type, SubType, Offset,  Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs,        data, nvs,     0x9000,  0x6000,
phy_init,   data, phy,     0xf000,  0x1000,
factory,    app,  factory, 0x10000, 1M,
my_storage, data, fat,     ,        2M, 

FATファイルシステムで使うためのmy_storageというパーティションを追加しました。
その他はWear Levellingサンプルと同じです。

サンプルプログラムのコンフィグファイル

つづいて、コンフィグファイルsdkconfig.defaultsを見てみましょう。

CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_example.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions_example.csv"
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
# CONFIG_FATFS_LFN_NONE is not set
CONFIG_FATFS_LFN_HEAP=y
# CONFIG_FATFS_LFN_STACK is not set
CONFIG_FATFS_MAX_LFN=255
CONFIG_FATFS_API_ENCODING_ANSI_OEM=y
# CONFIG_FATFS_API_ENCODING_UTF_16 is not set
# CONFIG_FATFS_API_ENCODING_UTF_8 is not set

こちらもWear Levellingサンプルから流用したものですが、ロングファイルネーム(Long File Name、LFN)に関する設定を追加しています。

LFNを設定することで、ファイルシステムで扱えるファイル名の長さや文字コード(ANSI、UTF8、UTF16)を変更できます。
LFNを設定しないと、”ABCDEFGHIJKLMN.txt”や、”あいうえおかきくけこ.txt”のような長いファイル名(もしくは日本語等のファイル名)を扱えません。

短いファイル名でも良ければLFNの設定は不要です。

main.cppの説明① 使用するライブラリについて

それではmain.cppを見ていきましょう。
部分的に説明するので、全体像を見たい方はGitHubにアップロードしたファイルを参照してください。

このサンプルでは、ESP32のファイルシステムを扱うため、 esp_vfs.hesp_vfs_fat.hesp_system.hをインクルードしています。
また、C++でファイル操作をするためにfstreamiostreamstringという標準ライブラリをインクルードしています。
さらに、LEDを点灯するためdriver/gpio.hを、IMUと通信するため自作ライブラリicm20648.hをインクルードしています。

#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <fstream>
#include <iostream>
#include <string>

#include "esp_vfs.h"
#include "esp_vfs_fat.h"
#include "esp_system.h"
#include "driver/gpio.h"
#include "icm20648.h"

main.cppの説明② LED制御タスク

続いてLED制御タスク関数TaskLEDについて説明します。

このTaskLED関数の仕事はLEDを点滅させることです。
グローバル変数led_pattern_の値に合わせて点滅周期を変更します。

ESP32のLED制御については過去の記事でも扱いましたが、そのときよりシンプルなコードになりました。
よかったら見比べてみてください。

static int led_pattern_ = 0;

static void TaskLED(void *arg){
    const gpio_num_t GPIO_LED_0 = GPIO_NUM_22;
    const gpio_num_t GPIO_LED_1 = GPIO_NUM_23;

    gpio_pad_select_gpio(GPIO_LED_0);  // パッドをGPIOとして扱う
    gpio_pad_select_gpio(GPIO_LED_1);  // パッドをGPIOとして扱う
    gpio_set_direction(GPIO_LED_0, GPIO_MODE_OUTPUT);  // GPIOを出力に設定
    gpio_set_direction(GPIO_LED_1, GPIO_MODE_OUTPUT);  // GPIOを出力に設定

    while(1){
        double delay_time_ms = 100 * (led_pattern_ + 1);

        gpio_set_level(GPIO_LED_0, 1);
        gpio_set_level(GPIO_LED_1, 1);
        vTaskDelay(delay_time_ms / portTICK_PERIOD_MS);

        gpio_set_level(GPIO_LED_0, 0);
        gpio_set_level(GPIO_LED_1, 0);
        vTaskDelay(delay_time_ms / portTICK_PERIOD_MS);
    }
}

main.cppの説明③ IMUセンサ値処理タスク

つづいて、IMUのセンサ値を処理するタスク関数TaskIMUについて説明します。

この関数では、IMUからX軸回りの角速度(ジャイロX、gyro_x)を取得し、
ジャイロXがしきい値(±800 dps)を超えたら、グローバル変数led_pattern_の値を更新します。

led_pattern_が更新された時、グローバル変数save_led_pattern_をTrueにします。
この変数がTrueになると、別の関数でled_pattern_の値がフラッシュメモリに書き込まれます

static bool save_led_pattern_ = false;

static void TaskIMU(void *arg){
    const gpio_num_t GPIO_MOSI = GPIO_NUM_19;
    const gpio_num_t GPIO_MISO = GPIO_NUM_21;
    const gpio_num_t GPIO_SCLK = GPIO_NUM_18;
    const gpio_num_t GPIO_CS = GPIO_NUM_5;
    const unsigned int ACCEL_FSSEL = 1;  // 0:2g, 1:4g, 2:8g, 3:16g
    const unsigned int GYRO_FSSEL = 2;  // 0:250dps, 1:500dps, 2:1000dps, 3:2000dps

    // LED点灯用のパラメータ
    const int MAX_LED_PATTERN = 3;
    const int MIN_LED_PATTERN = 0;
    const double GYRO_THRESHOLD = 800;  // degrees per sec

    icm20648 imu_driver(GPIO_MOSI, GPIO_MISO, GPIO_SCLK, GPIO_CS,
            ACCEL_FSSEL, GYRO_FSSEL);

    while(1){
        double gyro_x = imu_driver.getGyroX();

        // ジャイロの値がしきい値を超えたら、LEDパターンを更新する
        if(std::abs(gyro_x) > GYRO_THRESHOLD){
            if(gyro_x > GYRO_THRESHOLD){
                led_pattern_++;
            }else{
                led_pattern_--;
            }

            // オーバーフロー、アンダーフローの処理
            if(led_pattern_ > MAX_LED_PATTERN){
                led_pattern_ = MIN_LED_PATTERN;
            }else if(led_pattern_ < MIN_LED_PATTERN){
                led_pattern_ = MAX_LED_PATTERN;
            }

            // LEDパターンを保存するためにフラグを立てる
            save_led_pattern_ = true;
            vTaskDelay(100 / portTICK_PERIOD_MS);
        }

        vTaskDelay(10 / portTICK_PERIOD_MS);
    }
}

main.cppの説明④ main関数前半

main関数の前半ではファイルシステムの初期設定と、LED点灯パターンの読み取りを行います。

このコードでは“my_storage”というパーティションに、“/my_log”というディレクトリ(フォルダ)を作成し、
その中の“/my_log/LED点灯パターン.txt”というテキストファイルを操作します。

ファイルシステムの初期設定はWear Levellingサンプルとほぼ同じです。

ファイルの読み取りではC++の標準ライブラリifstreamを使用しています。
ファイルの読み取りに成功したら、格納されている値をグローバル変数led_pattern_に代入します。

    const char *TAG = "logger_example";
    const char *PARTITION_LABEL = "my_storage";
    const std::string BASE_PATH = "/my_log";
    const std::string FILE_NAME = BASE_PATH + "/LED点灯パターン.txt";
    const esp_vfs_fat_mount_config_t MOUNT_CONFIG = {
            .format_if_mount_failed = true,
            .max_files = 4,
            .allocation_unit_size = CONFIG_WL_SECTOR_SIZE
    };

    wl_handle_t s_wl_handle = WL_INVALID_HANDLE;

    // FATファイルシステムを初期化してバーチャルファイルシステムに登録する
    esp_err_t err = esp_vfs_fat_spiflash_mount(
            BASE_PATH.c_str(), PARTITION_LABEL, &MOUNT_CONFIG, &s_wl_handle);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Failed to mount FATFS (%s)", esp_err_to_name(err));
        return;
    }

    ESP_LOGI(TAG, "LED点灯パターンを読み出します");
    std::ifstream ifs(FILE_NAME, std::ios::in | std::ios::binary);
    if(ifs.fail()){
        ESP_LOGE(TAG, "Failed to open %s for reading.", FILE_NAME.c_str());
    }else{
        std::string line;
        getline(ifs, line);
        led_pattern_ = std::stoi(line);
        ESP_LOGI(TAG, "LED点灯パターン[%d]を読み出しました", led_pattern_);
    }
    ifs.close();

main.cppの説明⑤ main関数後半

main関数の後半では、タスクの作成(登録)と、ファイルへの書き込みを行います。

まず、xTaskCreateという関数で、先ほど紹介したタスク関数TaskLEDTaskIMUFreeRTOSのシステムへ登録します。

その後ろにあるwhile文では、グローバル変数save_led_pattern_を監視し、変数がTrueになったらテキストファイルへled_pattern_の値を書き込む、という処理を繰り返します。

    xTaskCreate(TaskLED, "TaskLED", 4096, NULL, 5, NULL);
    xTaskCreate(TaskIMU, "TaskIMU", 4096, NULL, 5, NULL);

    while(1){
        if(save_led_pattern_){
            ESP_LOGI(TAG, "LED点灯パターン[%d]を保存します", led_pattern_);
            std::ofstream ofs(FILE_NAME, std::ios::out | std::ios::binary);
            if(ofs.fail()){
                ESP_LOGE(TAG, "Failed to open %s for writing.", FILE_NAME.c_str());
            }else{
                ofs << led_pattern_ << std::endl;
                ofs.close();
                ESP_LOGI(TAG, "保存しました");
            }

            save_led_pattern_ = false;
        }

        vTaskDelay(10 / portTICK_PERIOD_MS);
    }

    // FATファイルシステムのアンマウント
    esp_vfs_fat_spiflash_unmount(BASE_PATH.c_str(), s_wl_handle);
}

FreeRTOSについて

タスクという概念はこのブログシリーズで初めて紹介する機能です。
ESP32ではFreeRTOSというリアルタイムOSを実行しています。
FreeRTOS(あるいはリアルタイムOS)にはシステムの処理を分離してほぼ並列で実行できるという特徴があり、
この分離された処理をタスクと呼んでいます。

例えば今回のサンプルでは、「IMU信号に合わせてLEDを点滅パターンを変え、ファイルに保存する」というシステムを、
ファイルの読み書き処理(main)と、LEDの点灯処理(TaskLED)と、IMUのセンサ値処理(TaskIMU)という3つのタスクに分割しています。

分割することで、タスクごとにメモリ使用量、実行時間、実行優先度等を管理できたり、
タスクを別のシステムに再利用できる、という恩恵を受けられます。

FreeRTOSの詳細についてはこのブログ記事で説明しきれないので、
ESP-IDFのドキュメントや、FreeRTOS.orgのドキュメントを参照してください。

まとめ

今回は、私のオリジナルマウスEspecialファイルシステムを使いました。

ファイルシステムを使うことで、例えばマイクロマウスの迷路情報を保存したり、走行調整後の制御パラメータを保存したりと、様々なデータをプログラム実行中に保存できるようになり、マイクロマウスのデバッグ速度が向上します。
「マウスを調整するたびに、マウスをPCに接続しプログラムを書き換える」という面倒な作業から卒業できます。

次回はESP32マウスシリーズの最終回です。
昨年のマイクロマウス大会で使用したプログラムの紹介と、ESP32マウス開発の振り返りを行います。

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