マイク比較 M5StickC、M5Stack Fire、ESP-EYE その2 録音の準備 ESP-EYE

概要

前回はなんとなくの波形だけ表示してみました。今回は実際に録音をして波形を確認してみたいと思いますが、まずはどうすれば比較できるかを準備します。

実験方法

前回なんとなく波形がでたパラメーターをベースに音声データを保存して、シリアル出力に16進数で吐き出す。

その後にHEX変換ツールを使って、バイナリデータに変換後に音声編集アプリで波形を確認してみました。

利用アプリ

無料で使えるオープンソースの音声編集ソフトを使いました。

スケッチ

#include <driver/i2s.h>

#define I2S_NUM             I2S_NUM_0           // 0 or 1

#define I2S_SAMPLE_RATE     16000
#define I2S_SAMPLE_SIZE     512
#define I2S_BUFFER_SIZE     (10*1024)

#define I2S_PIN_CLK         26
#define I2S_PIN_WS          32
#define I2S_PIN_DOUT        I2S_PIN_NO_CHANGE
#define I2S_PIN_DIN         33

byte i2s_samples[I2S_BUFFER_SIZE];
int i2s_samplesCount;

void i2sMicInit() {
  i2s_config_t i2s_config = {
    .mode                 = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate          = I2S_SAMPLE_RATE,
    .bits_per_sample      = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format       = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_I2S,
    .intr_alloc_flags     = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count        = 2,
    .dma_buf_len          = 512,
    .use_apll             = false,
    .tx_desc_auto_clear   = false,
    .fixed_mclk           = 0
  };
  i2s_pin_config_t pin_config = {
    .bck_io_num           = I2S_PIN_CLK,
    .ws_io_num            = I2S_PIN_WS,
    .data_out_num         = I2S_PIN_DOUT,
    .data_in_num          = I2S_PIN_DIN,
  };

  i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM, &pin_config);
  //i2s_set_clk(I2S_NUM, I2S_SAMPLE_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);

  i2s_samplesCount = 0;
}

void setup() {
  Serial.begin(115200);
  delay(100);

  i2sMicInit();
}

void loop() {
  size_t bytes_read;
  i2s_read(I2S_NUM, (void *)&i2s_samples[i2s_samplesCount], 512, &bytes_read, portMAX_DELAY);
  i2s_samplesCount += bytes_read;

  if (I2S_BUFFER_SIZE <= i2s_samplesCount) {
    for (int i = 0; i < I2S_BUFFER_SIZE; i++) {
      Serial.printf("%02X", i2s_samples[i]);
    }
    Serial.println();
    i2s_samplesCount = 0;
  }
}

操作手順

Arduino側

後ほどコードを出しますが、録音したデータをSerial.printf(“%02X”,i2s_samples[i])で表示します。

テスト音声再生

Audacityのジェネレーターからトーンを選択して、440Hzの音声データを作成して、この音を再生しながら録音してみます。

シリアルモニタからコピー

Arduinoから吐き出した16進数の文字列をコピーしてきます。テストの音声を再生させながら、1行分コピーするとちょうどいいです。

バイナリファイルへ変換

上記のWebツールを使わせていただきました。

左上に16進数文字列をペーストして、入力形式をHEX(16進数)にセットして、出力形式をファイルダウンロードにしてから変換実行をすると、ファイルがダウンロードされます。

Audacityでインポート

メニューから「取り込み」→「ロー(Raw)データの取り込み」を選択します。

取り込み設定は16ビットのリトルエンディアンで、モノラルにして16000Hzを選択します。

無音に見えます。。。念の為再生してみまと、ちゃんと440Hzぐらいの音がします。録音はされていますが、音が小さいのですね。とりあえず入力側は変更せずにAudacityで拡大してみます。

音声増幅

Audacityで音声を全選択してから、エフェクトの増幅を選びます。

デフォルトで劣化しない最大音量に増幅する設定になっているのでそのままOKで増幅させます。

波形が見えました。思ったより波形が波打っていますね、、、

Arduinoにクロック設定を追加

ちょっと気になっていた設定がありまして、クロック設定を追加してみました。

  i2s_set_clk(I2S_NUM, I2S_SAMPLE_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);

この状態で録音して、取り込んでみます。

あれ、増幅しなくても波形が見える大きさです。そして波形ががびがびしていますね。ここまで変わるのがちょっと意外です。。。

APIリファレンスを読む

上記のI2Sのリファレンスを読み直します。

#include "driver/i2s.h"
#include "freertos/queue.h"

static const int i2s_num = 0; // i2s port number

static const i2s_config_t i2s_config = {
    .mode = I2S_MODE_MASTER | I2S_MODE_TX,
    .sample_rate = 44100,
    .bits_per_sample = 16,
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB,
    .intr_alloc_flags = 0, // default interrupt priority
    .dma_buf_count = 8,
    .dma_buf_len = 64,
    .use_apll = false
};

static const i2s_pin_config_t pin_config = {
    .bck_io_num = 26,
    .ws_io_num = 25,
    .data_out_num = 22,
    .data_in_num = I2S_PIN_NO_CHANGE
};

...

    i2s_driver_install(i2s_num, &i2s_config, 0, NULL);   //install and start i2s driver
    i2s_set_pin(i2s_num, &pin_config);
    i2s_set_sample_rates(i2s_num, 22050); //set sample rates
    i2s_driver_uninstall(i2s_num); //stop & destroy i2s driver

送信用ですが、設定例があります。i2s_configはサンプリングレートが44100ですが、i2s_set_sample_rates()は22050です。なぜ、、、

スケッチ例を探す

void i2sInit()
{
   i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN),
    .sample_rate =  I2S_SAMPLE_RATE,              // The format of the signal using ADC_BUILT_IN
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // is fixed at 12bit, stereo, MSB
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = I2S_COMM_FORMAT_I2S_MSB,
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count = 4,
    .dma_buf_len = 8,
    .use_apll = false,
    .tx_desc_auto_clear = false,
    .fixed_mclk = 0
   };
   i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
   i2s_set_adc_mode(ADC_UNIT_1, ADC_INPUT);
   i2s_adc_enable(I2S_NUM_0);
}

i2s_driver_install()で検索するとHiFreq_ADC.inoのスケッチ例がありました。こちらではi2s_set_sample_rates()は呼び出していませんね、、、

ESP-IDFでも探す

  • \bluetooth\a2dp_gatts_coex\main\main.c
  • \bluetooth\a2dp_sink\main\main.c
  • \peripherals\i2s\main\i2s_example_main.c
  • \peripherals\i2s_adc_dac\main\app_main.c

上記4つサンプルがありましたが、全部i2s_set_sample_rates()系は呼び出していませんでした。そして送信ばかりで受信のサンプルが少ないですね。

バッファサイズと数も調査する

dma_buf_countとdma_buf_lenにいくつを設定すればいいのかわからないので、サンプルの値を確認してみます。

出典サンプルdma buf countdma buf lencount * len
Arduino\I2S\HiFreq_ADC\HiFreq_ADC.ino4832
ESP-IDF\bluetooth\a2dp_gatts_coex\main\main.c660360
ESP-IDF\bluetooth\a2dp_sink\main\main.c660360
ESP-IDF\peripherals\i2s\main\i2s_example_main.c660360
ESP-IDF\peripherals\i2s_adc_dac\main\app_main.c210242048
API リファレンス/esp32/api-reference/peripherals/i2s.html864512

んー、一貫性がありません。ESP-IDFのlenが60ってのが8の倍数じゃないんですよね。Arduinoだと動かなかったような?

フォーラムなどでもたまに質問がありましたが、明確な答えがでていませんでした。送信などでは一度に送る量などによって細かいDMAをたくさんの方が良さそうですが、受信だとリアルタイム性を求めなければバッファが溢れない量あればよさそうな感じです。

GitHubを探す

espressifのリポジトリでi2s_driver_installを含むコードを見ていきます。

送信

非常にシンプルな送信例です。データを作成して送信前にi2s_set_clk()を呼び出しています。

受信

シンプルな受信例を発見しました。DMAバッファは300を3個と中途半端な数ですね。。。そして、intr_alloc_flagsにESP_INTR_FLAG_LEVEL2を使っています。割り込みレベルを少し上げているのかな?

初期化はi2s_driver_install()のあとにi2s_set_pin()とi2s_zero_dma_buffer(1)とシンプルです。受信時にはi2s_zero_dma_buffer()で受信バッファをすべてクリアしないと確かに雑音が入っている気がします。。。

クロック系の関数はありませんでした。

ADC受信

ADCのみはESP-IDFではなくArduinoのスケッチ例しかありませんでした。i2s_set_pin()のかわりにADCの設定が追加されていますね。こちらも本当はi2s_zero_dma_buffer()を呼び出したほうがよさそうなサンプルですね。

こちらもクロック系の関数はなかったです。

DAC送信

ADC受信と使い方は似ていますね。送信だけれどクロック系関数は呼び出していません。

ADC受信&DAC送信

いろいろやっていますが、特に上のサンプルと変わっていませんでした。

Bluetooth送信

両方A2DPでのオーディオ送信のサンプルかな?

テクニカルマニュアルを読む

迷子になったので、テクニカルマニュアルを読みます。しかし、直接的な記述なし、、、

受信サンプルの設定にしてみる

    //.use_apll             = false,
    //.tx_desc_auto_clear   = false,
    //.fixed_mclk           = 0

上記の謎設定をコメントアウトしてみました。

ちなみにdma_buf_countとdma_buf_len、intr_alloc_flagsをESP_INTR_FLAG_LEVEL2にしても波形は変わりませんでした、、、

サンプリングレートを疑う

16000Hzサンプリングレートで440Hzのトーンを作成したのが一番上の波形です。真ん中が謎のパラメーターを消したもので、きれいな波形に見えていましたが、サンプリングレートがおかしいですね。

下が最初に取得した波形です。こっちは波形は汚いですがサンプリングレートはただしそうです。

そして、マイクで録音した波形は2ポイント同じデータが続いていますね。実質的なサンプリングレートが半分に見えます。

謎のパラメータを調べる

use_apll

高精度クロックを使うかのフラグですね。詳細不明。

tx_desc_auto_clear

TXなので送信用パラメーターです。送信用バッファをクリアするかどうかの設定なので受信には関係なさそうですね。どうやら送信するものがなくなった場合にデフォルトだと前回のデータを送信しちゃうみたいなので、この設定ができたみたいです。

fixed_mclk

MCLKの指定をするかのフラグですね。

まとめ

よくよく考えると、構造体の初期化をしないと変な値がはいっていますので絶対に指定しないとだめですね、、、

再度検証

#include <driver/i2s.h>

#define I2S_NUM             I2S_NUM_0           // 0 or 1

#define I2S_SAMPLE_RATE     16000
#define I2S_SAMPLE_SIZE     512
#define I2S_BUFFER_SIZE     (5*1024)

#define I2S_PIN_CLK         26
#define I2S_PIN_WS          32
#define I2S_PIN_DOUT        I2S_PIN_NO_CHANGE
#define I2S_PIN_DIN         33

byte i2s_samples[I2S_BUFFER_SIZE];
int i2s_samplesCount;

void i2sMicInit() {
  i2s_config_t i2s_config = {
    .mode                 = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate          = I2S_SAMPLE_RATE,
    .bits_per_sample      = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format       = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_I2S,
    .intr_alloc_flags     = ESP_INTR_FLAG_LEVEL2,
    .dma_buf_count        = 2,
    .dma_buf_len          = 512,
    .use_apll             = false,
    .tx_desc_auto_clear   = false,
    .fixed_mclk           = 0
  };
  i2s_pin_config_t pin_config = {
    .bck_io_num           = I2S_PIN_CLK,
    .ws_io_num            = I2S_PIN_WS,
    .data_out_num         = I2S_PIN_DOUT,
    .data_in_num          = I2S_PIN_DIN,
  };

  i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM, &pin_config);
  //  i2s_set_clk(I2S_NUM, I2S_SAMPLE_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);
  i2s_zero_dma_buffer(I2S_NUM);

  i2s_samplesCount = 0;
}

void setup() {
  Serial.begin(115200);
  delay(100);

  i2sMicInit();
}

void loop() {
  size_t bytes_read;
  i2s_read(I2S_NUM, (void *)&i2s_samples[i2s_samplesCount], 512, &bytes_read, portMAX_DELAY);
  i2s_samplesCount += bytes_read;

  if (I2S_BUFFER_SIZE <= i2s_samplesCount) {
    for (int i = 0; i < I2S_BUFFER_SIZE; i++) {
      Serial.printf("%02X", i2s_samples[i]);
    }
    Serial.println();
    i2s_samplesCount = 0;
  }
}

上記をベースに検証し直します、、、

バッファが長いと取得が面倒なので、すこし短い時間に変更しました。

パターン1 ベースのまま

上が録音データで、下が再生データです。位相はそろえてあります。サンプリングレートはあっていますが、上の波形がずれていますね。2ポイント単位でデータを取得しています。

パターン2 クロック指定

  i2s_set_clk(I2S_NUM, I2S_SAMPLE_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);

上記でクロック指定を追加。

周波数はあっていますが、データがバラけています。

パターン3 周波数を倍にする

    .sample_rate          = I2S_SAMPLE_RATE*2,

変になるのわかっていますが検証。

32000Hzで取り込み。周波数が倍になっていますが、山がきれいですね。。。ただよく考えるとポイントが増えるはずで、周波数は変わらないはずです。

16000Hzで取り込んで見ました、、、あれ?

きれいじゃない?

2ポイント単位で処理されていますがちゃんと山ができています。

パターン4 周波数を倍+クロック指定

    .sample_rate          = I2S_SAMPLE_RATE*2,
  i2s_set_clk(I2S_NUM, I2S_SAMPLE_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);

これはだめと。

パターン5 周波数を倍+クロック倍指定

    .sample_rate          = I2S_SAMPLE_RATE*2,
  i2s_set_clk(I2S_NUM, I2S_SAMPLE_RATE*2, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);

アナログの測定誤差っぽくみえますが、交互にずれているのでおそらく位相の違う波形がずれている感じですね、、、

パターン6 周波数を倍+use_apll

    .sample_rate          = I2S_SAMPLE_RATE*2,
    .use_apll             = true,

パターン3とあまりかわらないですが、若干山がなまっているきがします。

パターン7 周波数を倍+use_apll+fixed_mclkを倍

    .sample_rate          = I2S_SAMPLE_RATE*2,
    .use_apll             = true,
    .fixed_mclk           = I2S_SAMPLE_RATE*2

違いがわからず。

パターン8 周波数を倍+use_apll+fixed_mclkを4倍

    .sample_rate          = I2S_SAMPLE_RATE*2,
    .use_apll             = true,
    .fixed_mclk           = I2S_SAMPLE_RATE*2

無茶な設定をいれてみる。

波形が崩れたので、このパラメーターは影響をあたえているのがわかりました。

パターン9 周波数を倍+use_apll+fixed_mclk

    .sample_rate          = I2S_SAMPLE_RATE*2,
    .use_apll             = true,
    .fixed_mclk           = I2S_SAMPLE_RATE

んー、とくにかわらない。

考える

パターン3が一番キレイかな?

ここまでESP-EYEに搭載しているI2Sマイクのデータシートをみていないのが敗因なきがします。。。

データシートを探す

何故かESP-EYEの商品ページには搭載しているマイクの情報がありません。

上記に回路図があるので展開して確認します。

おそらくこのマイクです。検索するとM5StickVに採用されたマイクですね(微妙な表現)。

えーっとデータシートを読むんですがタイミング系のことばかりで、よくわからないですね、、、

あっ、12ビット精度のマイクだと思っていたら24ビット精度(32ビット保存)だった、、、なので1サンプリング2バイトじゃなくて、4バイトみたいです。ということでデータが変だったのは受信単位がおかしかったからでした。

再々検証

パターン10 周波数を倍+32ビット受信

    .sample_rate          = I2S_SAMPLE_RATE*2,
    .bits_per_sample      = I2S_BITS_PER_SAMPLE_32BIT,

おー、ちゃんと1ポイント単位でデータが取れています。周波数倍のままだったので16000に戻してみます。

パターン11 32ビット受信

    .bits_per_sample      = I2S_BITS_PER_SAMPLE_32BIT,

あれ、周波数は倍で良かったみたいです。

パターン12 32ビット受信+MSB

データシートにMSBとあったので指定してみました。

    .bits_per_sample      = I2S_BITS_PER_SAMPLE_32BIT,
    .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),

おー、これこれまでで一番きれいですね。32ビット精度だと使いにくいので16ビット精度に落としたいと思います。

パターン13 32ビット受信+MSB+16ビット保存

#include <driver/i2s.h>

#define I2S_NUM             I2S_NUM_0           // 0 or 1

#define I2S_SAMPLE_RATE     16000
#define I2S_SAMPLE_SIZE     512
#define I2S_BUFFER_SIZE     (5*1024)

#define I2S_PIN_CLK         26
#define I2S_PIN_WS          32
#define I2S_PIN_DOUT        I2S_PIN_NO_CHANGE
#define I2S_PIN_DIN         33

byte i2s_samples[I2S_BUFFER_SIZE];
int i2s_samplesCount;

void i2sMicInit() {
  i2s_config_t i2s_config = {
    .mode                 = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate          = I2S_SAMPLE_RATE * 2,
    .bits_per_sample      = I2S_BITS_PER_SAMPLE_32BIT,
    .channel_format       = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),
    .intr_alloc_flags     = ESP_INTR_FLAG_LEVEL2,
    .dma_buf_count        = 2,
    .dma_buf_len          = 512,
    .use_apll             = false,
    .tx_desc_auto_clear   = false,
    .fixed_mclk           = 0
  };
  i2s_pin_config_t pin_config = {
    .bck_io_num           = I2S_PIN_CLK,
    .ws_io_num            = I2S_PIN_WS,
    .data_out_num         = I2S_PIN_DOUT,
    .data_in_num          = I2S_PIN_DIN,
  };

  i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM, &pin_config);
  i2s_zero_dma_buffer(I2S_NUM);

  i2s_samplesCount = 0;
}

void setup() {
  Serial.begin(115200);
  delay(100);

  i2sMicInit();
}

void loop() {
  size_t bytes_read;
  int32_t readBuff[512 / 4];
  i2s_read(I2S_NUM, (void *)readBuff, 512, &bytes_read, portMAX_DELAY);

  for (int i = 0; i < bytes_read / 4; i++) {
    int16_t *p = (int16_t*)&i2s_samples[i2s_samplesCount];
    *p = (readBuff[i] / 256);
    i2s_samplesCount += 2;
  }

  if (I2S_BUFFER_SIZE <= i2s_samplesCount) {
    for (int i = 0; i < I2S_BUFFER_SIZE; i++) {
      Serial.printf("%02X", i2s_samples[i]);
    }
    Serial.println();
    i2s_samplesCount = 0;
  }
}

32ビットデータがありますが、実際には24ビット精度なので、さらに8ビット分をすてて保存しています。

なんと、増幅をしなくてもグラフが見えるようになりました!

まとめ

まだノイジーな音ですが、スタート時点と比べるとかなりの改善ができました。ちゃんとデータシートを読みましょう、、、

逆にここまで調査しないと、それっぽく聞こえるけれどちゃんと録音できていないってことになります。また、今回はノートパソコンのスピーカーから鳴らしているので、その影響でノイジーになっている可能性があります。

あとインターネットのESP32の情報はかなり昔のバージョンに関するものが多いですので気をつけましょう!

そしてこのブログもかなり嘘が書いてあることがあるので、気をつけて使ってください。

M5StackでLovyanGFXを試す その1 メモリの確認

概要

M5Stack Fireを入手したのでLovyanGFXの使い方を試してみます。M5StickCは画面が小さいので問題にならないことが、M5Stackでは考慮する必要があります。

描画用スプライトについて

画面に直接描画すると、描画している途中も表示されチラついて見えます。そのため描画用のスプライトを作成し、一括して描画する方法があります。

それではスプライトを作ってみましょう!

#include <LovyanGFX.hpp>

static LGFX lcd;
static LGFX_Sprite sprite;

static uint32_t tft_width ;
static uint32_t tft_height;

void setup(void)
{
  Serial.begin(115200);
  delay(100);

  lcd.init();

  tft_width = lcd.width();
  tft_height = lcd.height();

  // 空きメモリ確認
  Serial.printf("heap_caps_get_free_size(MALLOC_CAP_DMA):%d\n", heap_caps_get_free_size(MALLOC_CAP_DMA) );
  Serial.printf("heap_caps_get_largest_free_block(MALLOC_CAP_DMA):%d\n", heap_caps_get_largest_free_block(MALLOC_CAP_DMA) );

  // 画面サイズ確認
  Serial.printf("Width:%d, Height:%d\n", tft_width, tft_height);

  // 描画用スプライト作成
  void *p = sprite.createSprite(tft_width, tft_height);
  if ( p == NULL ) {
    Serial.println("メモリが足りなくて確保できない");
  }

  // 空きメモリ確認
  Serial.printf("heap_caps_get_free_size(MALLOC_CAP_DMA):%d\n", heap_caps_get_free_size(MALLOC_CAP_DMA) );
  Serial.printf("heap_caps_get_largest_free_block(MALLOC_CAP_DMA):%d\n", heap_caps_get_largest_free_block(MALLOC_CAP_DMA) );
}

void loop(void)
{
}

上記を実行した結果です。

heap_caps_get_free_size(MALLOC_CAP_DMA):287940
heap_caps_get_largest_free_block(MALLOC_CAP_DMA):124024
Width:320, Height:240
メモリが足りなくて確保できない
heap_caps_get_free_size(MALLOC_CAP_DMA):287940
heap_caps_get_largest_free_block(MALLOC_CAP_DMA):124024

使っているのはM5Stack Fireですが、PSRAMは無効にした状態で実行しています。画面は横320、縦240で問題ないのですが、スプライトの作成に失敗しています。

Arduinoで一般的な画面は1ドットあたり16ビット(2バイト)で管理することが標準的です。そのため320x240x2で153,600バイトのメモリが必要になります。

高速な転送をするためにはDMAが使えるメモリが必要なので空き容量を調べると287,940バイトです。一見メモリは足りているように見えますが、largestの値が124,024バイトです。ESP32のメモリは複数のメモリが飛び飛びで配置されているため、一度に確保できるメモリに制限があります。

解決方法

その1 小さいサイズを使う

横幅縦幅バイト
3202402153,600
3201802115,200
2402402115,200

ざっくりとサイズ別の容量です。どうやら縦と横のサイズを小さくすればメモリに収まりそうです。

試しに240×240で作成したところ、確保できました。

グラフなどの動的な部分と、それ以外のメニュー周りなどを分けてスプライトを確保することで回避することができそうです。実際のところ分割して転送量をなるべく少なくしたほうが高速描画が可能です。

空き容量最大空き容量
確保前287,940124,024
確保後172,720113,792
差分115,22010,232

上記がメモリの増減です。スプライトの容量より20バイト多く減っています。これはメモリ確保したときに内部で管理している領域分と推測できます。

さて、最大空き容量は若干減っていますが、それほど大きく変化していません。ただこの空き容量だとさらに240×240は確保できないですね。

その2 分割して確保

1つでは確保できないので、縦を2分割して320×120を確保してみます。

  sprites[0].createSprite(320, 120);
  sprites[1].createSprite(320, 120);

MovingIconsなどのスケッチ例が実際に分割して確保しています。

空き容量最大サイズ
確保前287,940124,024
確保後134,00446,908
差分153,93677,116

最低2分割すれば確保できそうです。ただし分割した場合にはちょっと描画が大変です。のちほど解説したいと思います。

その3 色数を減らす

//sprite.setColorDepth(1);   // 1ビット( 2色)パレットモードに設定
//sprite.setColorDepth(2);   // 2ビット( 4色)パレットモードに設定
//sprite.setColorDepth(4);   // 4ビット(16色)パレットモードに設定
sprite.setColorDepth(8);   // RGB332の8ビットに設定
sprite.createSprite(320, 240);

色数を減らすことで、1ピクセルあたり16ビットだったものが8ビット以下になります。ただし一般的な画面は16ビットで動いているため、転送する際に変換が必要となり、高速DMA転送が使えなくなります。

あまり速度と色数が必要ない場合には、お手軽に使えると思います。ただし色の指定方法などが変わりますので注意が必要です。

その4 PSRAMを使う

M5Stack Fireの場合にはPSRAMを有効にすることで、通常より低速ですが大きなメモリを使うことができます。

  sprites.setPsram(true);
  void *p = sprites.createSprite(320, 240);

通常はPSRAMを使わない設定になっているので、setPsram(true)を呼び出してPSRAMを使うように設定します。

PSRAMを使うことで大量に画面を確保することができますが、通常メモリよりかなり低速なので注意しましょう。

MovingIconsスケッチの分析

LovyanGFXのスケッチ例にあるMovingIconsを見ながら、使い方を確認してみます。このスケッチは3種類のアイコンを動かしながら大量に描画しています。

まず、描画ですが縦3分割して、320×80の描画を3回行っています。しかし描画用スプライトは2個しかありません。

どのように描画しているかというと、描画しているスプライトと転送しているスプライトに役割をわけています。

#スプライト描画画面転送
1スプライト1に描画エリア1を描画
2スプライト2に描画エリア2を描画スプライト1を描画エリア1に転送
3スプライト1に描画エリア3を描画スプライト2を描画エリア2に転送
4スプライト1を描画エリア3に転送

スプライト1とスプライト2は交互に描画と転送を繰り返します。スプライト1を画面にDMA転送しているしている間に、スプライト2に描画することができます。

DMA転送はCPUを使わないバックグラウンド転送なので、この技が使えます。色数が違ったりPSRAMを使った場合にはDMA転送が使えないので描画速度が落ちるはずです。

上記のことを頭においてスケッチ例をみてみると、動作が理解できると思います。

  for (int_fast16_t y = 0; y < tft_height; y += sprite_height) {
    flip = flip ? 0 : 1;
    sprites[flip].clear();
    for (size_t i = 0; i != obj_count; i++) {
      a = &objects[i];
      icons[a->img].pushRotateZoom(&sprites[flip], a->x, a->y - y, a->r, a->z, a->z, 0);
    }

とくに上記のループが初見ではわかりにくいですが、縦80ドットずつを3回に分けて描画しています。とりあえず全アイコンを描画してみて、描画担当範囲外の物はライブラリ側で描画しませんので無視されています。-yしているところがポイントです。

このスケッチを理解できていないと、LovyanGFXをうまく使えないと思いますのでがんばって読解してみてください。

ここは高速描画をするためには仕方ないのですが、本当は分割したスプライトをきれいにラッパーできると良さそうですね。

まとめ

なんとなくLovyanGFXというよりは、画面描画全般に関係する内容になってしまいました。次回があればもう少し突っ込んだ内容を紹介したいと思います。

マイク比較 M5StickC、M5Stack Fire、ESP-EYE

概要

マイク搭載ボードが3つあったので、マイクの取得方法とデータの比較をしてみました。簡易調査なのであとで録音してから更に調査したいと思います。

※ここに書いてあるスケッチは正しくありません。次回作以降で録音データを確認しながらチューニングをしていきます

M5Stack Fire

はじめてのM5StackはBASICかGRAYにする予定でしたのですが、マイクがほしかったのでFireを買ってみました。

赤いFireにはスピーカーとマイクが搭載されています。スピーカーはノイズをよく拾うのでスケッチを転送するときなどに音がします。。。

マイクはアナログ接続なのでADCを使って定期的に取得する方法になるのですが、I2S経由でも取得が可能です。

スケッチ例

// https://github.com/espressif/arduino-esp32/blob/master/libraries/ESP32/examples/I2S/HiFreq_ADC/HiFreq_ADC.ino

#include <driver/i2s.h>

#define I2S_NUM             I2S_NUM_0           // 0 or 1

#define I2S_SAMPLE_RATE     16000
#define I2S_BUFFER_SIZE     512
#define ADC_INPUT           ADC1_GPIO34_CHANNEL // ADC CHANNEL
#define ADC_UNIT            ADC_UNIT_1          // ADC1 or ADC2

int16_t i2s_sambles[I2S_BUFFER_SIZE];

void i2sMicInit() {
  i2s_config_t i2s_config = {
    .mode                 = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN),
    .sample_rate          = I2S_SAMPLE_RATE,
    .bits_per_sample      = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format       = I2S_CHANNEL_FMT_ALL_LEFT,
    .communication_format = I2S_COMM_FORMAT_I2S_MSB,
    .intr_alloc_flags     = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count        = 4,
    .dma_buf_len          = 512,
    .use_apll             = false,
    .tx_desc_auto_clear   = false,
    .fixed_mclk = 0
  };

  i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
  i2s_set_adc_mode(ADC_UNIT, ADC1_GPIO34_CHANNEL);
  i2s_set_clk(I2S_NUM, I2S_SAMPLE_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);
  i2s_adc_enable(I2S_NUM);
}

void setup() {
  Serial.begin(115200);
  delay(100);

  i2sMicInit();
}

void loop() {
  size_t bytes_read;
  i2s_read(I2S_NUM, (void *)i2s_sambles, sizeof(i2s_sambles), &bytes_read, portMAX_DELAY);

  // Display only the first data
  int16_t sample = 0x0fff - (i2s_sambles[0] & 0x0fff) - 0x0800;
  Serial.printf("%4d\n", sample);
}

I2S経由で取得した場合には、指定のサンプリングレートで自動的にデータを取得してくれます。自分でタイマーを用意する必要がないので楽ですね。

I2S_MODE_ADC_BUILT_INというオプションを渡して、ADCの設定をするのが特徴になります。ADCで取得した場合は0x6812みたいなデータが取得されて、先頭の6の部分はADCのチャンネル番号で、下3桁がデータになります。また、0xfffが最小で0x000が最大の、反転したデータになっているそうです。マイクなので補正しなくても大丈夫だと思いますが一応補正してあります。

1秒で16000サンプリングですので、1ミリ秒あたり16サンプリング。1サンプリングは2バイトになります。一度に512バイト取得しているので、256サンプリングで、16ミリ秒間のデータを取得しています。

表示部分は先頭の1サンプリング分だけ表示する簡易的な表示になります。全部表示するとすぐに画面が流れちゃうので、全体の傾向をみるためのサンプリング表示になります。

プロット

声を出すと、それっぽく表示されています。しかしマイナス方向にオフセットされていますね。

M5StickC

M5StickCのマイクは検証したことがあるので、なんとなく結果がわかっています。。。

スケッチ例

#include <M5StickC.h>
#include <driver/i2s.h>

#define I2S_NUM             I2S_NUM_0           // 0 or 1

#define I2S_SAMPLE_RATE     16000
#define I2S_BUFFER_SIZE     512

#define I2S_PIN_CLK         I2S_PIN_NO_CHANGE
#define I2S_PIN_WS          0
#define I2S_PIN_DOUT        I2S_PIN_NO_CHANGE
#define I2S_PIN_DIN         34

int16_t i2s_sambles[I2S_BUFFER_SIZE];

void i2sMicInit() {
  i2s_config_t i2s_config = {
    .mode                 = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM),
    .sample_rate          = I2S_SAMPLE_RATE,
    .bits_per_sample      = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format       = I2S_CHANNEL_FMT_ALL_LEFT,
    .communication_format = I2S_COMM_FORMAT_I2S,
    .intr_alloc_flags     = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count        = 4,
    .dma_buf_len          = 256,
    .use_apll             = false,
    .tx_desc_auto_clear   = false,
    .fixed_mclk           = 0
  };
  i2s_pin_config_t pin_config = {
    .bck_io_num           = I2S_PIN_CLK,
    .ws_io_num            = I2S_PIN_WS,
    .data_out_num         = I2S_PIN_DOUT,
    .data_in_num          = I2S_PIN_DIN,
  };

  i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM, &pin_config);
}

void setup() {
  M5.begin();

  i2sMicInit();
}

void loop() {
  size_t bytes_read;
  i2s_read(I2S_NUM, (void *)i2s_sambles, sizeof(i2s_sambles), &bytes_read, portMAX_DELAY);

  // Display only the first data
  Serial.printf("%6d\n", i2s_sambles[0]);
}

標準的なI2Sですが、PDMマイクなのでクロックのかわりにWS端子を使ってクロックの制御をしています。また、M5StickCのマイクはAXP192の初期化をしないと、電源が供給されないので注意してください。

プロット

安定のオフセットですね。。。手元にある4台を確認したところ-1500から-6000ぐらいの幅でオフセットがありました。個体差が激しいですね。。。

ESP-EYE

ESP32を開発したEspressif Systems社の純正開発ボードです。カメラとマイクが付いているモデルになります。たぶん国内で取り扱っている店舗はないかな?

Amazonのリンクは画像を見るためのものなので、ここからは買わないと方がいいと思います、、、

国内だとマルツさんでDigi-Key経由で取り寄せができますが、6000円以上にしてDigi-Keyから直接購入してみました。Digi-KeyやMouserでは2300円前後で、マルツ経由だと2800円前後+送料500円になります。6000円未満だとDigi-KeyやMouserは送料が2000円かかるので、単品だとマルツのほうがいいと思います。

#include <driver/i2s.h>

#define I2S_NUM             I2S_NUM_0           // 0 or 1

#define I2S_SAMPLE_RATE     16000
#define I2S_BUFFER_SIZE     512

#define I2S_PIN_CLK         26
#define I2S_PIN_WS          32
#define I2S_PIN_DOUT        I2S_PIN_NO_CHANGE
#define I2S_PIN_DIN         33

int16_t i2s_sambles[I2S_BUFFER_SIZE];

void i2sMicInit() {
  i2s_config_t i2s_config = {
    .mode                 = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate          = I2S_SAMPLE_RATE,
    .bits_per_sample      = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format       = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_I2S,
    .intr_alloc_flags     = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count        = 4,
    .dma_buf_len          = 256,
    .use_apll             = false,
    .tx_desc_auto_clear   = false,
    .fixed_mclk           = 0
  };
  i2s_pin_config_t pin_config = {
    .bck_io_num           = I2S_PIN_CLK,
    .ws_io_num            = I2S_PIN_WS,
    .data_out_num         = I2S_PIN_DOUT,
    .data_in_num          = I2S_PIN_DIN,
  };

  i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM, &pin_config);
}

void setup() {
  Serial.begin(115200);
  delay(100);

  i2sMicInit();
}

void loop() {
  size_t bytes_read;
  i2s_read(I2S_NUM, (void *)i2s_sambles, sizeof(i2s_sambles), &bytes_read, portMAX_DELAY);

  // Display only the first data
  Serial.printf("%6d\n", i2s_sambles[0]);
}

M5StickCとは接続しているI2Sマイクが違うのでパラメータも変わります。モノラルマイクなのですが、左チャンネルしかデータが取れません。ブログを検索するとデータシートは左だけれど右でしか録音できないって人もいました(怖い)。ちなみにM5StickCのは右でも左でも同じデータが取得できます。

プロット

すごいです、ちゃんと0が原点になっています! 数値もすごく安定していました。さすがに音声入力用に設計されているボードですね。

まとめ

思ったよりマイクの取得方法は難しかったです。3つとも取得方法が違っていました。このへんの情報は非常に少ないのでわかりにくいですね。

次回作

Digi-Keyで注文してみた

概要

日本で取り扱っていないボードがほしかったのでDigi-Keyで注文してみました。

Digi-Keyとは?

アメリカとカナダの間ぐらいにある電子部品のディストリビュータです。ある程度在庫を持っているので、アメリカからですが比較的早く到着することができます。

アカウント登録

これは事前に行っておいたほうがいいと思います。どうやら入力した住所とかをDigi-Keyの人がみて、確認後に修正しているようです。

登録直後に注文しても大丈夫だとは思いますが、早くものがほしい場合には事前にアカウント登録までを行ったほうがよさそうです。

送料について

6000円以上で送料無料になります。6000円未満の場合には2000円ぐらいの送料がかかるので6000円以上にするか、安いものだけの場合にはマルツ経由で購入したほうが安くなると思います。

在庫について

在庫切れしている商品が含まれている場合には、在庫ありだけ発送されて、残りは在庫切れ商品がすべて揃った段階で発送がされます。複数種類の在庫切れが入っている場合には、すべて揃うまで時間がかかるので注意する必要がありそうです。

送料は6000円以上であれば2個口にわかれても無料みたいです。

発送について

  • FedEx
  • UPS

私が注文したときには上記の2つが選べました。とりあえずUPSにしてみましたが、よく違いはわかりません。

注文から配達まで

  • 5月28日(木) 21:51 注文
  • 5月29日(金) 00:22 発送
  • 5月31日(日) 09:24 日本到着
  • 6月01日(月) 12:13 配達

私の場合には、上記で配達されました。土日を含むと若干スケジュールが変わると思いますが非常に早いですね。

秋月さんだと今は混んでいるので、これより時間かかる可能性もあります。。。

まとめ

購入したものについては、後ほどブログで紹介したいと思います。ほぼ同じようなサイトであるMouserにも興味があります。ちょっと取り扱い商品や在庫が違うのでものによっては使い分けたほうが良さそうです。

Wio TerminalとかはまだMouserにしかないのかな?

M5Stack ATOMとかは逆にDigi-Keyにしかなかったです。

M5StickC UIFlow V1.5.3更新差分調査 その1 モジュールの差異

概要

1.5.3がリリースされたので、差分を調べてみました。変更点が多いので、すべては把握できていません。モジュールの差異のみまずは調べました。

追加モジュール

  • __main__
  • _boot
  • _cloud
  • _deviceCfg
  • _env2
  • _uasyncio
  • _webrepl
  • apa106
  • button
  • deviceCfg
  • flashbdev
  • framebuf
  • hardware
  • inisetup
  • m5cloud
  • ntptime
  • uarray
  • utils
  • wifiCfg
  • wifiWebCfg

変更点がわかるのは書き出します。

__main__

個別にあったモジュールが__main__モジュール以下に移動しています。内部にはAxp192クラス、Bm8563クラス、M5Ledクラスがありました。

hardware

hwからの名称変更みたいです。

削除モジュール

  • app_manage
  • axp192
  • bm8563
  • flowSetup
  • lidar
  • logging
  • m5base
  • m5ucloud
  • microWebSocket
  • microWebSrv
  • microWebTemplate
  • mlx90640
  • modules
  • peripheral
  • statechoose
  • upip
  • upip_utarfile
  • wave

上記が削除されていました。

  • errno
  • json
  • random
  • select
  • ssl

このへんはいるけれど読み込むとおかしな動きになるような?

もうちょっと検証してみます。

まとめ

今回のバージョンアップはベースのMicro Pythonが1.11から1.12に変更になっています。それに伴いかなり大幅に変更されていました。

ちなみに_flowモジュールも増えていますが読み込むとUIFlowが壊れますので読み込まないでください、、、

ESP32のヒープメモリ管理 その1

概要

上記の内容を中心に、実験しながら確認してみました。

スタックとヒープ、スタティックメモリ

ESP32はArduino CoreやESP-IDFを使う場合にはFreeRTOS上で動いています。一時的なメモリはタスク作成時に割り当てられたスタックを利用します。

大きめのローカル変数を確保しようとしてエラーになるのは、スタックメモリが足りていません。一方ヒープメモリはmalloc()関数などにより確保されるメモリであり、比較的大きめのサイズで利用することが多いです。

スタティックメモリはグルーバル変数などの実行前に確保されているメモリになります。

RAMの種類

ESP32には複数種類の物理RAMが搭載されており、利用用途におうじて使い分ける必要があります。

DRAM(Data RAM)は、一番一般的なメモリでデータを保持するヒープとして使われます。IRAM(Instruction RAM)は、主にプログラムなどを保持するメモリとして使われます。汎用メモリとして使うためには32ビットアライメントでアクセスする必要があります。

そしてDRAMともIRAMとも利用可能なD/IRAMもあります。

つまり、プログラムはIRAMかD/IRAMしか利用できませんが、データは制限はありますがすべてのRAMに保存することができます。

IRAM領域にデータを保存する場合には、32ビットアライメントアクセスですので、32ビット単位で確保する必要があります。8ビットや16ビット単位でアクセスしようとすると例外が発生してしまいますので、32ビット単位で読み取って、必要ないデータをマスクするなどの処理が必要になります。

この他にSPI経由で接続されたPSRAMがあります。8MBの容量を持つ、専用RAMが販売されていますが、現在4MBまでしか利用することができません。また、接続するピンも固定されているので、基本的には自分で接続することはできないと思ったほうがいいです。

ESP32-WROVER-*などのモジュールではPSRAMが搭載されています。アクセス速度は遅いのですが、メモリをたくさん使えるようになるのでカメラなどの画像を扱う場合にはPSRAM搭載モジュールを採用している場合が多いです。

メモリ用途の種類

一般的なヒープメモリの他に、特殊用途で利用可能なメモリを確保する方法が用意されています。

DMA対応メモリは、SPIやI2Sなどにハードウエアを利用したDMA転送で使える領域のメモリです。SPI接続のPSRAMはDMA転送で利用できないので除外されます。

32ビットアライメント対応メモリは、主にIRAMのことですが通常データ領域には割り当てられないので、意図的に指定することで利用することが可能です。

外部SPIメモリはPSRAMのことです。内蔵メモリよりはアクセス速度が遅いですが、大きなメモリが必要になったときには指定することができます。

メモリ状況表示

void setup() {
  Serial.begin(115200);
  delay(100);

  Serial.printf("===============================================================\n");
  Serial.printf("Mem Test\n");
  Serial.printf("===============================================================\n");

  Serial.printf("esp_get_free_heap_size()                              : %6d\n", esp_get_free_heap_size() );
  Serial.printf("esp_get_minimum_free_heap_size()                      : %6d\n", esp_get_minimum_free_heap_size() );

  //xPortGetFreeHeapSize()(データメモリ)ヒープの空きバイト数を返すFreeRTOS関数です。これはを呼び出すのと同じheap_caps_get_free_size(MALLOC_CAP_8BIT)です。
  Serial.printf("xPortGetFreeHeapSize()                                : %6d\n", xPortGetFreeHeapSize() );

  //xPortGetMinimumEverFreeHeapSize()また、関連heap_caps_get_minimum_free_size()するものを使用して、ブート以降のヒープの「最低水準点」を追跡できます。
  Serial.printf("xPortGetMinimumEverFreeHeapSize()                     : %6d\n", xPortGetMinimumEverFreeHeapSize() );

  //heap_caps_get_free_size() さまざまなメモリ機能の現在の空きメモリを返すためにも使用できます。
  Serial.printf("heap_caps_get_free_size(MALLOC_CAP_EXEC)              : %6d\n", heap_caps_get_free_size(MALLOC_CAP_EXEC) );
  Serial.printf("heap_caps_get_free_size(MALLOC_CAP_32BIT)             : %6d\n", heap_caps_get_free_size(MALLOC_CAP_32BIT) );
  Serial.printf("heap_caps_get_free_size(MALLOC_CAP_8BIT)              : %6d\n", heap_caps_get_free_size(MALLOC_CAP_8BIT) );
  Serial.printf("heap_caps_get_free_size(MALLOC_CAP_DMA)               : %6d\n", heap_caps_get_free_size(MALLOC_CAP_DMA) );
  Serial.printf("heap_caps_get_free_size(MALLOC_CAP_PID2)              : %6d\n", heap_caps_get_free_size(MALLOC_CAP_PID2) );
  Serial.printf("heap_caps_get_free_size(MALLOC_CAP_PID3)              : %6d\n", heap_caps_get_free_size(MALLOC_CAP_PID3) );
  Serial.printf("heap_caps_get_free_size(MALLOC_CAP_PID3)              : %6d\n", heap_caps_get_free_size(MALLOC_CAP_PID4) );
  Serial.printf("heap_caps_get_free_size(MALLOC_CAP_PID4)              : %6d\n", heap_caps_get_free_size(MALLOC_CAP_PID5) );
  Serial.printf("heap_caps_get_free_size(MALLOC_CAP_PID5)              : %6d\n", heap_caps_get_free_size(MALLOC_CAP_PID6) );
  Serial.printf("heap_caps_get_free_size(MALLOC_CAP_PID6)              : %6d\n", heap_caps_get_free_size(MALLOC_CAP_PID7) );
  Serial.printf("heap_caps_get_free_size(MALLOC_CAP_PID7)              : %6d\n", heap_caps_get_free_size(MALLOC_CAP_PID3) );
  Serial.printf("heap_caps_get_free_size(MALLOC_CAP_SPIRAM)            : %6d\n", heap_caps_get_free_size(MALLOC_CAP_SPIRAM) );
  Serial.printf("heap_caps_get_free_size(MALLOC_CAP_INTERNAL)          : %6d\n", heap_caps_get_free_size(MALLOC_CAP_INTERNAL) );
  Serial.printf("heap_caps_get_free_size(MALLOC_CAP_DEFAULT)           : %6d\n", heap_caps_get_free_size(MALLOC_CAP_DEFAULT) );
  //Serial.printf("heap_caps_get_free_size(MALLOC_CAP_IRAM_8BIT)         : %6d\n", heap_caps_get_free_size(MALLOC_CAP_IRAM_8BIT) );
  Serial.printf("heap_caps_get_free_size(MALLOC_CAP_INVALID)           : %6d\n", heap_caps_get_free_size(MALLOC_CAP_INVALID) );

  //heap_caps_get_largest_free_block()ヒープ内の最大の空きブロックを返すために使用できます。これは、現在可能な最大の単一割り当てです。この値を追跡し、合計空きヒープと比較すると、ヒープの断片化を検出できます。
  Serial.printf("heap_caps_get_largest_free_block(MALLOC_CAP_EXEC)     : %6d\n", heap_caps_get_largest_free_block(MALLOC_CAP_EXEC) );
  Serial.printf("heap_caps_get_largest_free_block(MALLOC_CAP_32BIT)    : %6d\n", heap_caps_get_largest_free_block(MALLOC_CAP_32BIT) );
  Serial.printf("heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)     : %6d\n", heap_caps_get_largest_free_block(MALLOC_CAP_8BIT) );
  Serial.printf("heap_caps_get_largest_free_block(MALLOC_CAP_DMA)      : %6d\n", heap_caps_get_largest_free_block(MALLOC_CAP_DMA) );
  Serial.printf("heap_caps_get_largest_free_block(MALLOC_CAP_PID2)     : %6d\n", heap_caps_get_largest_free_block(MALLOC_CAP_PID2) );
  Serial.printf("heap_caps_get_largest_free_block(MALLOC_CAP_PID3)     : %6d\n", heap_caps_get_largest_free_block(MALLOC_CAP_PID3) );
  Serial.printf("heap_caps_get_largest_free_block(MALLOC_CAP_PID3)     : %6d\n", heap_caps_get_largest_free_block(MALLOC_CAP_PID4) );
  Serial.printf("heap_caps_get_largest_free_block(MALLOC_CAP_PID4)     : %6d\n", heap_caps_get_largest_free_block(MALLOC_CAP_PID5) );
  Serial.printf("heap_caps_get_largest_free_block(MALLOC_CAP_PID5)     : %6d\n", heap_caps_get_largest_free_block(MALLOC_CAP_PID6) );
  Serial.printf("heap_caps_get_largest_free_block(MALLOC_CAP_PID6)     : %6d\n", heap_caps_get_largest_free_block(MALLOC_CAP_PID7) );
  Serial.printf("heap_caps_get_largest_free_block(MALLOC_CAP_PID7)     : %6d\n", heap_caps_get_largest_free_block(MALLOC_CAP_PID3) );
  Serial.printf("heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)   : %6d\n", heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM) );
  Serial.printf("heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) : %6d\n", heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) );
  Serial.printf("heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT)  : %6d\n", heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT) );
  //Serial.printf("heap_caps_get_largest_free_block(MALLOC_CAP_IRAM_8BIT): %6d\n", heap_caps_get_largest_free_block(MALLOC_CAP_IRAM_8BIT) );
  Serial.printf("heap_caps_get_largest_free_block(MALLOC_CAP_INVALID)  : %6d\n", heap_caps_get_largest_free_block(MALLOC_CAP_INVALID) );

  //heap_caps_get_minimum_free_size()指定された機能を持つすべての領域の合計最小空きメモリを取得します。
  Serial.printf("heap_caps_get_minimum_free_size(MALLOC_CAP_EXEC)      : %6d\n", heap_caps_get_minimum_free_size(MALLOC_CAP_EXEC) );
  Serial.printf("heap_caps_get_minimum_free_size(MALLOC_CAP_32BIT)     : %6d\n", heap_caps_get_minimum_free_size(MALLOC_CAP_32BIT) );
  Serial.printf("heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT)      : %6d\n", heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT) );
  Serial.printf("heap_caps_get_minimum_free_size(MALLOC_CAP_DMA)       : %6d\n", heap_caps_get_minimum_free_size(MALLOC_CAP_DMA) );
  Serial.printf("heap_caps_get_minimum_free_size(MALLOC_CAP_PID2)      : %6d\n", heap_caps_get_minimum_free_size(MALLOC_CAP_PID2) );
  Serial.printf("heap_caps_get_minimum_free_size(MALLOC_CAP_PID3)      : %6d\n", heap_caps_get_minimum_free_size(MALLOC_CAP_PID3) );
  Serial.printf("heap_caps_get_minimum_free_size(MALLOC_CAP_PID3)      : %6d\n", heap_caps_get_minimum_free_size(MALLOC_CAP_PID4) );
  Serial.printf("heap_caps_get_minimum_free_size(MALLOC_CAP_PID4)      : %6d\n", heap_caps_get_minimum_free_size(MALLOC_CAP_PID5) );
  Serial.printf("heap_caps_get_minimum_free_size(MALLOC_CAP_PID5)      : %6d\n", heap_caps_get_minimum_free_size(MALLOC_CAP_PID6) );
  Serial.printf("heap_caps_get_minimum_free_size(MALLOC_CAP_PID6)      : %6d\n", heap_caps_get_minimum_free_size(MALLOC_CAP_PID7) );
  Serial.printf("heap_caps_get_minimum_free_size(MALLOC_CAP_PID7)      : %6d\n", heap_caps_get_minimum_free_size(MALLOC_CAP_PID3) );
  Serial.printf("heap_caps_get_minimum_free_size(MALLOC_CAP_SPIRAM)    : %6d\n", heap_caps_get_minimum_free_size(MALLOC_CAP_SPIRAM) );
  Serial.printf("heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL)  : %6d\n", heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL) );
  Serial.printf("heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT)   : %6d\n", heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT) );
  //Serial.printf("heap_caps_get_minimum_free_size(MALLOC_CAP_IRAM_8BIT) : %6d\n", heap_caps_get_minimum_free_size(MALLOC_CAP_IRAM_8BIT) );
  Serial.printf("heap_caps_get_minimum_free_size(MALLOC_CAP_INVALID)   : %6d\n", heap_caps_get_minimum_free_size(MALLOC_CAP_INVALID) );

  //heap_caps_get_info()multi_heap_info_t上記の関数からの情報に加えて、ヒープ固有の追加データ(割り当て数など)を含む構造体を返します。
  //Skip

  // heap_caps_print_heap_info()が返す情報の要約をstdoutに出力しheap_caps_get_info()ます。
  //Serial.printf("heap_caps_print_heap_info(MALLOC_CAP_INTERNAL) :\n");
  //heap_caps_print_heap_info(MALLOC_CAP_INTERNAL);

  // heap_caps_dump()そしてheap_caps_dump_all()意志出力は、ヒープ内の各ブロックの構造に関する情報を詳述します。これは大量の出力になる可能性があることに注意してください。
  //Serial.printf("heap_caps_dump() :\n");
  //heap_caps_dump(MALLOC_CAP_INTERNAL);
}

void loop() {
}

上記がメモリ状況表示系の関数を呼び出したスケッチ例です。一般的なESP32ボードと、PSRAMを搭載したESP32ボードで実行してみます。

===============================================================
Mem Test
===============================================================
esp_get_free_heap_size()                              :  288812
esp_get_minimum_free_heap_size()                      :  283140
xPortGetFreeHeapSize()                                :  288812
xPortGetMinimumEverFreeHeapSize()                     :  283140
heap_caps_get_free_size(MALLOC_CAP_EXEC)              :  202212
heap_caps_get_free_size(MALLOC_CAP_32BIT)             :  362208
heap_caps_get_free_size(MALLOC_CAP_8BIT)              :  288812
heap_caps_get_free_size(MALLOC_CAP_DMA)               :  288812
heap_caps_get_free_size(MALLOC_CAP_PID2)              :       0
heap_caps_get_free_size(MALLOC_CAP_PID3)              :       0
heap_caps_get_free_size(MALLOC_CAP_PID3)              :       0
heap_caps_get_free_size(MALLOC_CAP_PID4)              :       0
heap_caps_get_free_size(MALLOC_CAP_PID5)              :       0
heap_caps_get_free_size(MALLOC_CAP_PID6)              :       0
heap_caps_get_free_size(MALLOC_CAP_PID7)              :       0
heap_caps_get_free_size(MALLOC_CAP_SPIRAM)            :       0
heap_caps_get_free_size(MALLOC_CAP_INTERNAL)          :  362208
heap_caps_get_free_size(MALLOC_CAP_DEFAULT)           :  288812
heap_caps_get_free_size(MALLOC_CAP_INVALID)           :       0
heap_caps_get_largest_free_block(MALLOC_CAP_EXEC)     :  113792
heap_caps_get_largest_free_block(MALLOC_CAP_32BIT)    :  125024
heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)     :  125024
heap_caps_get_largest_free_block(MALLOC_CAP_DMA)      :  125024
heap_caps_get_largest_free_block(MALLOC_CAP_PID2)     :       0
heap_caps_get_largest_free_block(MALLOC_CAP_PID3)     :       0
heap_caps_get_largest_free_block(MALLOC_CAP_PID3)     :       0
heap_caps_get_largest_free_block(MALLOC_CAP_PID4)     :       0
heap_caps_get_largest_free_block(MALLOC_CAP_PID5)     :       0
heap_caps_get_largest_free_block(MALLOC_CAP_PID6)     :       0
heap_caps_get_largest_free_block(MALLOC_CAP_PID7)     :       0
heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)   :       0
heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) :  125024
heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT)  :  125024
heap_caps_get_largest_free_block(MALLOC_CAP_INVALID)  :       0
heap_caps_get_minimum_free_size(MALLOC_CAP_EXEC)      :  202192
heap_caps_get_minimum_free_size(MALLOC_CAP_32BIT)     :  356516
heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT)      :  283140
heap_caps_get_minimum_free_size(MALLOC_CAP_DMA)       :  283140
heap_caps_get_minimum_free_size(MALLOC_CAP_PID2)      :       0
heap_caps_get_minimum_free_size(MALLOC_CAP_PID3)      :       0
heap_caps_get_minimum_free_size(MALLOC_CAP_PID3)      :       0
heap_caps_get_minimum_free_size(MALLOC_CAP_PID4)      :       0
heap_caps_get_minimum_free_size(MALLOC_CAP_PID5)      :       0
heap_caps_get_minimum_free_size(MALLOC_CAP_PID6)      :       0
heap_caps_get_minimum_free_size(MALLOC_CAP_PID7)      :       0
heap_caps_get_minimum_free_size(MALLOC_CAP_SPIRAM)    :       0
heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL)  :  356516
heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT)   :  283140
heap_caps_get_minimum_free_size(MALLOC_CAP_INVALID)   :       0

上記が一般的なESP32ボードです。esp_get_free_heap_size()とxPortGetFreeHeapSize()が同じ数値を返しているので、同じ用途だと思われます。espがつくのがESP-IDFの関数で、xPortがつくのがFreeRTOSの関数ですね。どちらを使ってもよいと思いますが、汎用的なのはFreeRTOSの関数かもしれません。

メモリは確保して開放するを繰り返すと、空いている領域が飛び飛びになります。そのため実際の残メモリと、連続した一番広いメモリ領域は一致しません。freeが付いている関数は空き容量の合計で、minimumが付いている関数は実際には使えないだろうと思われる領域を除いた保守的な空き容量です。largestがついている関数は、一度に確保できる連続したメモリ領域のサイズです。

===============================================================
Mem Test
===============================================================
esp_get_free_heap_size()                              : 4483188
esp_get_minimum_free_heap_size()                      : 4477056
xPortGetFreeHeapSize()                                : 4483188
xPortGetMinimumEverFreeHeapSize()                     : 4477056
heap_caps_get_free_size(MALLOC_CAP_EXEC)              :  192880
heap_caps_get_free_size(MALLOC_CAP_32BIT)             : 4547252
heap_caps_get_free_size(MALLOC_CAP_8BIT)              : 4483188
heap_caps_get_free_size(MALLOC_CAP_DMA)               :  288936
heap_caps_get_free_size(MALLOC_CAP_PID2)              :       0
heap_caps_get_free_size(MALLOC_CAP_PID3)              :       0
heap_caps_get_free_size(MALLOC_CAP_PID3)              :       0
heap_caps_get_free_size(MALLOC_CAP_PID4)              :       0
heap_caps_get_free_size(MALLOC_CAP_PID5)              :       0
heap_caps_get_free_size(MALLOC_CAP_PID6)              :       0
heap_caps_get_free_size(MALLOC_CAP_PID7)              :       0
heap_caps_get_free_size(MALLOC_CAP_SPIRAM)            : 4194252
heap_caps_get_free_size(MALLOC_CAP_INTERNAL)          :  353000
heap_caps_get_free_size(MALLOC_CAP_DEFAULT)           : 4483188
heap_caps_get_free_size(MALLOC_CAP_INVALID)           :       0
heap_caps_get_largest_free_block(MALLOC_CAP_EXEC)     :  113792
heap_caps_get_largest_free_block(MALLOC_CAP_32BIT)    : 4194252
heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)     : 4194252
heap_caps_get_largest_free_block(MALLOC_CAP_DMA)      :  124968
heap_caps_get_largest_free_block(MALLOC_CAP_PID2)     :       0
heap_caps_get_largest_free_block(MALLOC_CAP_PID3)     :       0
heap_caps_get_largest_free_block(MALLOC_CAP_PID3)     :       0
heap_caps_get_largest_free_block(MALLOC_CAP_PID4)     :       0
heap_caps_get_largest_free_block(MALLOC_CAP_PID5)     :       0
heap_caps_get_largest_free_block(MALLOC_CAP_PID6)     :       0
heap_caps_get_largest_free_block(MALLOC_CAP_PID7)     :       0
heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)   : 4194252
heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) :  124968
heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT)  : 4194252
heap_caps_get_largest_free_block(MALLOC_CAP_INVALID)  :       0
heap_caps_get_minimum_free_size(MALLOC_CAP_EXEC)      :  192860
heap_caps_get_minimum_free_size(MALLOC_CAP_32BIT)     : 4541100
heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT)      : 4477056
heap_caps_get_minimum_free_size(MALLOC_CAP_DMA)       :  282804
heap_caps_get_minimum_free_size(MALLOC_CAP_PID2)      :       0
heap_caps_get_minimum_free_size(MALLOC_CAP_PID3)      :       0
heap_caps_get_minimum_free_size(MALLOC_CAP_PID3)      :       0
heap_caps_get_minimum_free_size(MALLOC_CAP_PID4)      :       0
heap_caps_get_minimum_free_size(MALLOC_CAP_PID5)      :       0
heap_caps_get_minimum_free_size(MALLOC_CAP_PID6)      :       0
heap_caps_get_minimum_free_size(MALLOC_CAP_PID7)      :       0
heap_caps_get_minimum_free_size(MALLOC_CAP_SPIRAM)    : 4194252
heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL)  :  346848
heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT)   : 4477056
heap_caps_get_minimum_free_size(MALLOC_CAP_INVALID)   :       0

PSRAMを搭載しているボードでの実験結果が上記です。PSRAMを搭載していると、空き容量が一桁違います。

数値をみていると、一番少なくて重要そうなのがDMA対応メモリです。PSRAMを利用できないので一番小さいサイズになっています。freeは288936ですが、largestが124968となっています。これはESP32のメモリ空間自体が複数に分割されているため、DMAで利用できるメモリは1個目が124968までとなります。分割してもう1個ぐらい確保できそうですが、DMA転送は連続しているメモリ領域である必要があるので利点が半減してしまいます。大きなサイズのDMA転送を行う場合には、他の用途で使われる前に、まずはDMA転送用のメモリを確保する必要がありそうですね。

DEFAULTが通常にメモリ確保したときの残容量です。数値をみてみるとDMA対応メモリとPSRAMメモリの合計になっています。32BITアライメントメモリは標準では利用しないようです。

まとめ

ざっくりとですが、ESP32のヒープメモリの管理がわかってきました。今後実際にメモリの確保をしながらどう変化するのか調べてみたいと思います。

M5StickC(ESP32)による「ELEGOO Arduino用UNO R3スターターキット」を利用したArduino入門 その12 LCDDisplay

現時点の情報です。最新情報はM5StickC非公式日本語リファレンスを確認してみてください。

概要

前回は赤外線受信でした。今回は「Lesson 14 LCDDisplay」です。LCD1602というディスプレイになります。

LCD1602とは?

秋月さんでは上記が近いと思います。キットとまったく同じものは販売していませんでした。

英数が横16文字で2行表示できるディスプレイです。液晶画面が青と緑の2種類あります。比較的安価なため、簡易的な表示にはよく使われます。ただしM5StickCにはすでに液晶があるので、あまり使う機会はなさそうです。

上のAmazonの商品はキットに付いている液晶の他に、小さな部品が付いています。左側がセットで、右側は最初から一体化しています。

上記が単品販売のものです。プライムでない商品は中国から送られてくるので一ヶ月以上到着までかかるので注意してください。

接続方式について

パラレル接続

このLCDの標準的な接続方法で、4本(もしくは8本)の信号線を使ってデータを送信します。一度に複数のデータを送信できる利点があるのですが、データ以外にも制御用の信号が必要になり、最低6本の信号線が必要になります。

しかしM5StickCは利用できる端子が5本しかないので、このLCDを制御することができません。一般的なESP32であればなんとか制御可能ですが、このLCDを直接制御することはあまりないようです。

I2C接続

Iが二乗の、アイスクエアシーと呼ぶのが正式みたいですが、IICやI2Cと単純に表記されていることが多いです。

データ転送用のクロックと、データ用の信号線2本で通信をするプロトコルです。比較的低速通信になりますが、複数の機材を平行に接続することができます。温度センサーなど低速通信でも問題ないものを複数接続できる利点があるので、よく使われています。

このLCDは基本的にI2C変換を使って制御することの方が多いようです。今回はキットには含まれていませんが、I2C接続で試してみたいと思います。

接続方法

いろいろためしたのですが、上記みたいな感じになりました。ジャンパーケーブルのオス・メスを使ってM5StickCと接続しています。

電源は写真では3V3に接続していますが、5Vに接続したほうがきれいに表示されました。LCDと変換ボードは見えにくいですがブレッドボードに接続しています。

I2Cは通信をする側のESP32が電圧を出力するので、相手が5VでもGPIOには3.3Vが出力されます。アナログ入力などの場合にはGPIOに5Vが入力される可能性があるのですが、I2Cではその点は問題ありません。

ただし、5Vを供給した場合には相手側がI2Cの信号も5Vを想定して動くので、3.3Vの信号だと受け取ってくれない可能性がありますので注意してください。

この角度だとわかるかな?

M5StickCLCD
GNDGND
5V OutVCC
GPIO0SDA
GPIO26SCL

上記の組み合わせです。0と26は逆でも動きます。32と33の組み合わせでも動きます。ただしI2Cは出力が必要ですので入力専用のGPIO36は利用することができません。

I2Cアドレス検索

#include <Wire.h>

void setup()
{
  Serial.begin(115200);
  Wire.begin(0, 26);
}

void loop()
{
  Serial.println("I2C Scan");
  for (int address = 1; address < 127; address++ ) {
    Wire.beginTransmission(address);
    int error = Wire.endTransmission();
    if (error == 0) {
      Serial.printf("%02X", address);
    } else {
      Serial.print(" .");
    }

    if (address % 16 == 0) {
      Serial.print("\n");
    }

    delay(10);
  }

  Serial.print("\n\n");
  delay(1000);
}

I2Cのデバイスを使う場合には、デバイスのアドレスを調べる必要があります。複数のデバイスを接続できるので、アドレスによってデバイスを選択する必要があります。

そのため同じアドレスのデバイスは同時に利用することができません。たいていのデバイスはI2Cアドレスを変更する機能をもっているので、アドレスがかぶった場合にはどちらかのアドレスを変更する必要があります。

  Wire.begin(0, 26);

WireクラスがI2Cのクラスになります。引数が利用するGPIOでこの場合0と26を利用すると宣言しています。

    Wire.beginTransmission(address);
    int error = Wire.endTransmission();

Wire.beginTransmission(address)でアドレスを指定して通信をスタートさせ、Wire.endTransmission()で通信を終了させます。このときのそのアドレスのデバイスがいなかった場合にはエラーになるので、エラーにならないアドレスにはデバイスがいることになります。

I2C Scan
 . . . . . . . . . . . . . . . .
 . . . . . . . . . . . . . . . .
 . . . . . .27 . . . . . . . . .
 . . . . . . . . . . . . . . . .
 . . . . . . . . . . . . . . . .
 . . . . . . . . . . . . . . . .
 . . . . . . . . . . . . . . . .
 . . . . . . . . . . . . . .

このスケッチを実行すると上記のように0x27のアドレスにデバイスがいるのがわかります。このときデバイスが発見できなかった場合には接続を見直してみてください。

I2Cなどの2本信号線を使う通信の場合には、逆にケーブルを接続していることが非常に多いです。迷ったら信号線を入れ替えるか、全部はずしてから接続しなおしてみてください。

ライブラリのインストール

非常に似たような名前のライブラリが大量にあるのですが、「LiquidCrystal I2C」というライブラリで「LiquidCrystal I2C allow」で検索すると発見しやすかったです。

シンプルスケッチ

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C i2clcd(0x27, 16, 2); // アドレス, 文字数, 行数

void setup() {
  Wire.begin(0, 26);
  i2clcd.init();
  i2clcd.backlight();
  i2clcd.setCursor(0, 0);
  i2clcd.print("Hello, world!");
}

void loop() {
  i2clcd.setCursor(0, 1);
  i2clcd.print(millis());

  delay(111);
}

シンプルなスケッチ例です。

LiquidCrystal_I2C i2clcd(0x27, 16, 2); // アドレス, 文字数, 行数

上記で画面の設定を行っています。アドレスは0x27で、1行に16文字、2行あるLCDであることを設定しています。

  Wire.begin(0, 26);

上記でGPIO0とGPIO26を使ってI2C通信をする初期化。

  i2clcd.init();
  i2clcd.backlight();

こちらでLCDの初期化を行っています。

  i2clcd.setCursor(0, 0);
  i2clcd.print("Hello, world!");

出力は上記のようにカーソルを設定してから、print()で出力します。カーソルは0から始まって、(0, 0)が1行目の先頭、(0, 1)が2行目の先頭になります。

さて、このスケッチで画面に表示されましたでしょうか?

私は表示されませんでした、、、個体差があると思いますが液晶の明るさ調整をしないと文字が見えないと思います。

液晶の明るさ調整

上記の赤で囲った場所に、青い可変抵抗があると思います。この抵抗をドライバーを使って左右に回すことで液晶の明るさ調整が可能になっています。3.3Vに接続した場合には一番端っこまで回すとなんとか文字が見えましたが、全体的に薄い文字になりました。

もともと5Vで動作する液晶なので、なるべく5Vで動かしたほうがいいとは思います。

他の選択肢

一般的にM5StickCは液晶画面があるので、外部の液晶はあまり必要としないと思います。必要とすればケーブルではなれた場所に表示を付けたいときだと思いますが、文字だけ表示する今回の液晶ではなく、画像が表示できる液晶が同じような値段であるので、こちらを使った方がおすすめです。

I2C接続であれば、同じような手順で接続して、液晶に対応したライブラリを使えば画像や文字を表示することができると思います。

M5StickCの画面のようなもう少し大きな液晶画面を使いたい場合には、信号線を4本以上使いますが、より高速なSPI接続をする必要があります。

上記ではM5StickC本体の液晶と同じ液晶基板を接続しています。

まとめ

文字だけのキャラクタ表示液晶は最近はなかなか使わないと思います。大量に中国から購入すると安くなりますが、個人で使うのであれば普通の液晶でも十分安いと思います。

今回はI2Cで接続しましたが、I2C変換ボードを使わなくても信号線を6本接続できるボードを使えばLiquidCrystalという標準ライブラリで利用することが可能です。ただし、液晶の調整ように可変抵抗が必要になったりと、かなり回路は面倒になります。

ESP32でLINX for LabVIEW入門 その3 PWM

概要

前回はデジタル入出力と、アナログ入力を行いました。今回はPWM出力を行いたいと思います。ライブラリを最新版にしてから実行するようにしてください。

PWMとは?

PWMとはデジタル出力を高速でオン、オフすることで明るさなどを調整するための仕組みです。

上記の場合、8スロットにわけて、2スロット分だけオンをした場合になります。全体の25%の時間がオンになっていますので、LEDなどは25%の明るさで光ります。

こちらは、8スロットにわけて、4スロット分だけオンをした場合になります。全体の50%の時間がオンになっていますので、LEDなどは50%の明るさで光ります。

上記が実際の出力をオシロスコープで測定した図ですが、0%、50%、100%の順番で出力を変更されているのがわかります。

スケッチ

#include <M5StickC.h>
#include <LinxESP32.h>
#include <LinxESP32WifiListener.h>
#include <LinxSerialListener.h>

LinxESP32* LinxDevice;

void setup()
{
  M5.begin();
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(BLACK);

  LinxDevice = new LinxESP32();

  // PWM & Servo Setup
  uint8_t pwmList[] = {0, 26};    // Max16Ch {0, 26, ...}
  uint16_t pwmFrequency = 12000;  // LED:12000, Servo:50
  for (int i = 0; i < sizeof(pwmList); i++) {
    ledcSetup(i, pwmFrequency, 8);
    pinMode(pwmList[i], OUTPUT);
    ledcAttachPin(pwmList[i], i);
  }

  LinxWifiConnection.Start(LinxDevice, 44300);
  LinxSerialConnection.Start(LinxDevice, 3);
}

void loop()
{
  LinxWifiConnection.CheckForCommands();
  LinxSerialConnection.CheckForCommands();

  M5.update();
  if (millis() % 1000 == 0) {
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.println("LinxESP32");
    M5.Lcd.print("IP  : ");
    M5.Lcd.println(WiFi.localIP());
    byte macAddress[6];
    WiFi.macAddress(macAddress);
    M5.Lcd.printf("MAC : %02X:%02X:%02X:%02X:%02X:%02X\n", macAddress[0], macAddress[1], macAddress[2], macAddress[3], macAddress[4], macAddress[5]);
  }

  if (M5.Axp.GetBtnPress() != 0) {
    ESP.restart();
  }

  delay(1);
}

上記はM5StickCで画面に表示もするスケッチ例です。PWMを使う場合にはあらかじめ、どのGPIOをPWMに使うのかを設定しておく必要があります。

  // PWM & Servo Setup
  uint8_t pwmList[] = {0, 26};    // Max16Ch {0, 26, ...}
  uint16_t pwmFrequency = 12000;  // LED:12000, Servo:50
  for (int i = 0; i < sizeof(pwmList); i++) {
    ledcSetup(i, pwmFrequency, 8);
    pinMode(pwmList[i], OUTPUT);
    ledcAttachPin(pwmList[i], i);
  }

上記が設定部分になります。PWMチャンネル0にGPIO0、PWMチャンネル1にGPIO26を設定しています。また、PWMの周波数もここで設定します。通常のPWMの場合は12000(12kHz)ぐらいでいいと思いますが、サーボを利用する場合には50に設定してください。

共存する場合にはループではなくて、個別に設定をしたほうがシンプルになると思います。

LINX – PWM 1 Channel.vi

上記の画面で、PWMのチャンネル数と出力比を設定します。PWMはGPIOではなく、PWMチャンネル番号の0から15を指定するので間違えないようにしてください。Duty Cycleは0から1までを設定できるので、0.5を指定すると50%の出力となります。

いつものRunではなく、その隣にあるRun Continuousボタンを押すと、パラメータを変更したものがすぐに反映して便利です。

処理内容もシンプルです。

LINX – PWM N Channel.vi

こちらも他のNチャンネルと一緒ですね。

処理もシンプルです。

LINX – Digital Write Square Wave.vi

こちらはtone関数で圧電スピーカーを鳴らすような処理です。ESP32はtone関数が無いので、PWMを使うようになっていますので、GPIOではなくPWMチャンネル数を指定してください。

また、このサンプルは音がなり終わる前に次のループが始まってしまうのでWaitを追加しました。実際のところLabVIEWを使って音を鳴らすのであればアクティブブザーなどを使って、通常のデジタル出力をしたほうがいいと思います。

LINX – Servo 1 Channel.vi

サーボを使う場合には、PWM周波数を50Hzに設定する必要があります。LabVIEWからはマイクロ秒単位のパルス幅を指定することで制御します。

SG90サーボの場合は、データシート上500マイクロ秒から2400マイクロ秒が指定できますが、このサンプルは2500まで設定できるので注意してください。

まとめ

PWMまわりを動くようにしてみました。アナログ出力も使えるようにしようとしたのですが、Arduino UNOでもサポートしておらず、サンプルもないので対応しないことにしました。

ESP32ハードウエア乱数生成

概要

テクニカルマニュアルを読んでいたら、1ページだけの機能があったので調べてみました。無線アンテナから入ってくるノイズを使った乱数生成で、内部の暗号化などのソースとして使われるものみたいです。

一方、通常ではアドレス定義もされていないので使うことはありません。

テクニカルマニュアルの内容

原文

24. Random Number Generator

24.1 Introduction
The ESP32 contains a true random number generator, whose values can be used as a basis for cryptographical operations, among other things.

24.2 Feature
It can generate true random numbers.

24.3 Functional Description
When used correctly, every 32-bit value the system reads from the RNG_DATA_REG register of the random number generator is a true random number. These true random numbers are generated based on the noise in the Wi-Fi/BT RF system. When Wi-Fi and BT are disabled, the random number generator will give out pseudo-random numbers.

When Wi-Fi or BT is enabled, the random number generator is fed two bits of entropy every APB clock cycle (normally 80 MHz). Thus, for the maximum amount of entropy, it is advisable to read the random register at a maximum rate of 5 MHz.

A data sample of 2 GB, read from the random number generator with Wi-Fi enabled and the random register read at 5 MHz, has been tested using the Dieharder Random Number Testsuite (version 3.31.1). The sample passed all tests.

DeepL翻訳

24. 乱数発生器

24.1 はじめに
ESP32 は、真の乱数発生器を内蔵しており、その値を暗号演算の基礎として使用することができます。

24.2 特徴
真の乱数を生成することができます。

24.3 機能説明
正しく使用すると、システムが乱数発生器のRNG_DATA_REGレジスタから読み出す32ビットの値はすべて真の乱数になります。これらの真の乱数は、Wi-Fi/BT RF システムのノイズに基づいて生成されます。Wi-FiとBTが無効になっている場合、乱数発生器は擬似乱数を出力します。

Wi-FiまたはBTが有効な場合、乱数発生器にはAPBクロックサイクル(通常は80MHz)ごとに2ビットのエントロピーが供給されます。したがって、エントロピーを最大にするためには、最大レート5MHzで乱数レジスタを読み出すことをお勧めします。

Wi-Fi を有効にして乱数発生器から読み込んだ 2GB のデータサンプルと、5MHz で読み込んだ乱数レジスタを、Dieharder Random Number Testsuite (バージョン 3.31.1) を使用してテストしました。サンプルはすべてのテストに合格しました。

www.DeepL.com/Translator(無料版)で翻訳しました。

解説

短い文章なのであまり解説が必要ないと思いますが、無線のノイズを使った乱数発生なので、無線を有効にしていないと疑似乱数になってしまうみたいです。生成スピードは周辺機器のクロック(APB)単位で2ビットなので、32ビット生成するのに16クロック必要になります。

CPUクロック(MHz)APBクロック(MHz)暗号生成周期(MHz)暗号生成時間(マイクロ秒)
2408050.2
1608050.2
808050.2
40402.50.4
20201.250.8
10100.6251.6

この情報を整理すると上記の関係になりそうです。一番遅いときでも秒間62.5万回暗号を生成でき、暗号生成時間は1.6マイクロ秒かかります。

2マイクロ秒のウエイトをいれて、毎秒50万回の暗号生成までであればどのCPU周波数でも安全そうですね。

スケッチ例

#include <WiFi.h>

#ifndef DR_REG_RNG_BASE
#define DR_REG_RNG_BASE  0x3ff75144
#endif

uint32_t getESP32Random(){
  delayMicroseconds(2);
  return READ_PERI_REG(DR_REG_RNG_BASE);
}

void setup() {
  Serial.begin(115200);
  delay(100);

  WiFi.begin();
}

void loop() {
  uint32_t rand1 = getESP32Random();
  uint32_t rand2 = getESP32Random();
  uint32_t rand3 = getESP32Random();

  Serial.printf("rand1 = %08X, rand2 = %08X, rand3 = %08X\n", rand1, rand2, rand3);
  delay(1000);
}

アドレスから読み出すだけですので、単純ですね。読み出す前に2マイクロ秒のウエイトを安全のために入れています。

まとめ

とっても便利な機能ですが、使われていない気がします。個人的に使うかと言われても微妙でたぶん使うことはないと思います、、、

M5StickC(ESP32)による「ELEGOO Arduino用UNO R3スターターキット」を利用したArduino入門 その11 赤外線受信モジュール

現時点の情報です。最新情報はM5StickC非公式日本語リファレンスを確認してみてください。

概要

前回はジョイスティックでした。今回は赤外線受信センサーです。

赤外線受信センサーとは?

秋月さんだと上記になります。主に赤外線リモコンの受信用センサーになります。キットに入っているのはモジュールになっていて、信号を受信するとLEDが光るのでわかりやすくなっています。

気をつける必要があるのが、赤外線には波長があり一般的に使われているのが940nm前後になります。センサーによって940nmだったり945nmあたりが一番感度が良くなっていますがこれぐらいの差であればそれほど気にしなくてもよいと思いますが、なるべく同じ波長のものをペアにして使ったほうがいいと思います。

また、商品ページに中心周波数が書いてありますが、基本的には38kHz前後を赤外線リモコンで使うことが多いみたいです。

赤外線のフォーマットについて

上記などのサイトが詳しいです。

上記などで細かい内部関数を使って受信をためしましたが、通常はライブラリを使いますので細かいことは知らなくても大丈夫です。

接続方法

ちょっとわかりにくいのですが、キットの赤外線受信センサーは以下の表の役割になっています。接続するGPIOはどこでも問題ないはずです。

赤外線受信センサーM5StickC
G(GND)GND
R(VCC)3V3
Y(Signal)GPIO26

ライブラリ準備

ライブラリマネージャーより「IRremoteESP8266」をあらかじめいれておきます。このライブラリの元になっている「IRremote」が有名ですが、ESP32の場合には「IRremoteESP8266」を使ったほうが楽です。

スケッチ例を開く(IRrecvDumpV3)

#include "BaseOTA.h"

#include <Arduino.h>
#include <IRrecv.h>
#include <IRremoteESP8266.h>
#include <IRac.h>
#include <IRtext.h>
#include <IRutils.h>

const uint16_t kRecvPin = 26;

const uint32_t kBaudRate = 115200;
const uint16_t kCaptureBufferSize = 1024;

#if DECODE_AC
const uint8_t kTimeout = 50;
#else   // DECODE_AC
const uint8_t kTimeout = 15;
#endif  // DECODE_AC

const uint16_t kMinUnknownSize = 12;

#define LEGACY_TIMING_INFO false
IRrecv irrecv(kRecvPin, kCaptureBufferSize, kTimeout, true);
decode_results results;  // Somewhere to store the results

void setup() {
  OTAwifi();  // start default wifi (previously saved on the ESP) for OTA
#if defined(ESP8266)
  Serial.begin(kBaudRate, SERIAL_8N1, SERIAL_TX_ONLY);
#else  // ESP8266
  Serial.begin(kBaudRate, SERIAL_8N1);
#endif  // ESP8266
  while (!Serial)  // Wait for the serial connection to be establised.
    delay(50);
  Serial.printf("\n" D_STR_IRRECVDUMP_STARTUP "\n", kRecvPin);
  OTAinit();  // setup OTA handlers and show IP
#if DECODE_HASH
  irrecv.setUnknownThreshold(kMinUnknownSize);
#endif  // DECODE_HASH
  irrecv.enableIRIn();  // Start the receiver
}

void loop() {
  if (irrecv.decode(&results)) {
    uint32_t now = millis();
    Serial.printf(D_STR_TIMESTAMP " : %06u.%03u\n", now / 1000, now % 1000);
    if (results.overflow)
      Serial.printf(D_WARN_BUFFERFULL "\n", kCaptureBufferSize);
    Serial.println(D_STR_LIBRARY "   : v" _IRREMOTEESP8266_VERSION_ "\n");
    Serial.print(resultToHumanReadableBasic(&results));
    String description = IRAcUtils::resultAcToString(&results);
    if (description.length()) Serial.println(D_STR_MESGDESC ": " + description);
    yield();  // Feed the WDT as the text output can take a while to print.
#if LEGACY_TIMING_INFO
    Serial.println(resultToTimingInfo(&results));
    yield();  // Feed the WDT (again)
#endif  // LEGACY_TIMING_INFO
    Serial.println(resultToSourceCode(&results));
    Serial.println();    // Blank line between entries
    yield();             // Feed the WDT (again)
  }
  OTAloopHandler();
}

コメント行を消していますが、編集すべき場所は使うGPIOの行のみになります。この状態で付属の赤外線リモコンを操作するとシリアル出力にリモコンのコードが表示されます。

Protocol  : NEC
Code      : 0xFF6897 (32 Bits)
uint16_t rawData[67] = {9434, 4488,  642, 540,  582, 568,  612, 540,  612, 540,  582, 568,  610, 540,  584, 568,  584, 568,  610, 1618,  640, 1620,  614, 1644,  642, 1614,  646, 1616,  612, 1644,  616, 1644,  616, 1642,  616, 566,  612, 1620,  642, 1616,  644, 538,  586, 1644,  612, 568,  588, 564,  594, 560,  608, 1622,  620, 562,  588, 564,  606, 1626,  614, 566,  590, 1642,  638, 1620,  612, 1644,  638};  // NEC FF6897
uint32_t address = 0x0;
uint32_t command = 0x16;
uint64_t data = 0xFF6897;

上記が付属リモコンの0ボタンを押したときの情報になります。実際に送信しているデータはrawDataの値であり、それをデータ化するとProtocolとCodeがわかります。このCodeをさらにデコードすると、addressとcommandがわかります。

Protocol  : NEC
Code      : 0xFF30CF (32 Bits)
uint16_t rawData[67] = {9268, 4456,  596, 572,  614, 538,  648, 498,  568, 572,  596, 544,  572, 570,  574, 568,  572, 570,  598, 1620,  604, 1672,  574, 1646,  602, 1648,  600, 1646,  602, 1648,  600, 1648,  600, 1646,  600, 570,  596, 546,  574, 1648,  600, 1650,  600, 570,  574, 568,  574, 570,  572, 572,  574, 1646,  602, 1650,  600, 570,  574, 570,  574, 1648,  626, 1624,  600, 1648,  602, 1652,  622};  // NEC FF30CF
uint32_t address = 0x0;
uint32_t command = 0xC;
uint64_t data = 0xFF30CF;

ちなみに、こちらが1のボタンを押したときのデータになります。addressは受信側と送信側で同じものを処理するので、リモコンなどによって固定です。commandの値を変更することでどのボタンかを見分けています。

Protocol  : NEC (Repeat)
Code      : 0xFFFFFFFFFFFFFFFF (0 Bits)
uint16_t rawData[3] = {9236, 2204,  626};  // NEC (Repeat) FFFFFFFFFFFFFFFF
uint64_t data = 0xFFFFFFFFFFFFFFFF;

あと特徴的なのがリピートです。同じボタンを押し続けると、このリピートが飛んできます。ボリューム変更などはリピートを受け付けたりしますが、一般的な処理では無視しても問題ありません。

Protocol  : UNKNOWN
Code      : 0x531F42F9 (8 Bits)
uint16_t rawData[15] = {242, 2692,  350, 1280,  370, 45816,  1916, 1060,  508, 324,  386, 286,  554, 1462,  2832};  // UNKNOWN 531F42F9

また、上記のようなデータも受信する場合もあります。赤外線はいろいろなところに飛んでいるので、無意味なデータをたまたま受信してしまうこともあります。

Protocol  : SONY
Code      : 0x10 (12 Bits)
uint16_t rawData[103] = {2382, 594,  596, 592,  596, 592,  596, 592,  596, 592,  596, 592,  598, 592,  596, 594,  1190, 592,  598, 592,  596, 594,  596, 592,  596, 27370,  2380, 594,  596, 594,  594, 594,  596, 592,  596, 592,  596, 592,  596, 592,  598, 592,  1194, 590,  596, 592,  596, 592,  596, 594,  596, 27380,  2380, 594,  596, 592,  594, 594,  596, 592,  596, 592,  596, 592,  596, 592,  596, 592,  1192, 592,  596, 592,  596, 596,  594, 592,  598, 27392,  2382, 592,  596, 592,  598, 592,  596, 592,  596, 592,  596, 594,  596, 592,  596, 594,  1190, 592,  596, 592,  596, 594,  596, 594,  596};  // SONY 10
uint32_t address = 0x1;
uint32_t command = 0x0;
uint64_t data = 0x10;

上記はソニーのテレビで1ボタンを押した場合のデータです。NECプロトコルが一般的なのですが、ソニーは独自プロトコルを採用しています。

Protocol  : PANASONIC_AC
Code      : 0x0220E004000000060220E00400312E80AF00000660000080001690 (216 Bits)
Mesg Desc.: Model: 4 (JKE), Power: On, Mode: 3 (Cool), Temp: 23C, Fan: 7 (Auto), Swing(V): 15 (Auto), Quiet: Off, Powerful: Off, Clock: 00:00, On Timer: Off, Off Timer: Off
uint16_t rawData[439] = {3500, 1754,  438, 410,  464, 1314,  436, 414,  462, 420,  462, 414,  462, 414,  462, 414,  462, 420,  462, 414,  464, 414,  462, 416,  464, 418,  462, 412,  462, 1314,  436, 414,  462, 420,  462, 414,  464, 414,  464, 414,  464, 418,  462, 414,  462, 1312,  436, 1312,  436, 1318,  436, 414,  464, 412,  462, 1314,  436, 420,  462, 414,  464, 414,  464, 414,  462, 422,  462, 414,  462, 414,  462, 414,  464, 418,  462, 416,  462, 414,  462, 414,  464, 418,  462, 416,  462, 414,  462, 414,  462, 420,  464, 414,  462, 414,  464, 414,  464, 418,  462, 414,  464, 414,  462, 416,  462, 420,  464, 412,  464, 414,  462, 414,  462, 420,  462, 412,  462, 1314,  434, 1314,  436, 418,  462, 414,  464, 414,  462, 416,  462, 412,  464, 9990,  3506, 1748,  438, 412,  462, 1314,  436, 414,  462, 420,  462, 414,  462, 414,  464, 414,  464, 418,  462, 416,  462, 414,  464, 414,  464, 418,  462, 412,  464, 1314,  436, 414,  464, 420,  462, 414,  464, 412,  464, 414,  462, 420,  462, 412,  464, 1312,  436, 1312,  436, 1318,  434, 414,  462, 412,  462, 1314,  436, 420,  462, 414,  464, 414,  462, 414,  464, 418,  462, 414,  464, 414,  464, 414,  464, 418,  462, 414,  462, 414,  464, 414,  464, 416,  464, 1314,  434, 414,  468, 408,  464, 416,  462, 1312,  436, 1314,  436, 414,  464, 420,  462, 412,  462, 1314,  436, 1312,  436, 1318,  436, 410,  464, 1314,  436, 414,  462, 420,  462, 414,  462, 414,  462, 414,  462, 420,  462, 414,  462, 414,  462, 412,  462, 1326,  432, 1312,  436, 1310,  436, 1312,  436, 1316,  438, 410,  464, 1314,  436, 410,  464, 1318,  434, 414,  464, 414,  464, 414,  464, 418,  462, 414,  464, 412,  464, 416,  462, 418,  462, 414,  462, 414,  464, 414,  462, 420,  462, 414,  464, 416,  462, 412,  464, 420,  462, 412,  464, 1312,  436, 1312,  436, 418,  462, 414,  462, 416,  462, 416,  462, 420,  462, 414,  462, 414,  462, 416,  462, 420,  462, 412,  464, 1312,  436, 1314,  436, 418,  462, 414,  462, 414,  462, 414,  462, 418,  462, 414,  464, 414,  462, 414,  462, 420,  462, 414,  464, 414,  462, 414,  462, 420,  462, 414,  462, 414,  464, 414,  462, 418,  462, 416,  464, 412,  464, 414,  466, 416,  462, 414,  462, 416,  462, 412,  462, 1320,  436, 412,  464, 412,  462, 414,  462, 422,  460, 416,  462, 414,  462, 414,  462, 422,  462, 412,  462, 1312,  436, 1312,  436, 420,  462, 1314,  436, 412,  464, 414,  462, 422,  462, 414,  462, 414,  462, 416,  462, 420,  462, 1314,  438, 412,  462, 412,  464, 1316,  436};  // PANASONIC_AC
uint8_t state[27] = {0x02, 0x20, 0xE0, 0x04, 0x00, 0x00, 0x00, 0x06, 0x02, 0x20, 0xE0, 0x04, 0x00, 0x31, 0x2E, 0x80, 0xAF, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x80, 0x00, 0x16, 0x90};

こちらが手元にあったパナソニック製エアコンの冷房ボタンを押した情報です。

Mesg Desc.: Model: 4 (JKE), Power: On, Mode: 3 (Cool), Temp: 23C, Fan: 7 (Auto), Swing(V): 15 (Auto), Quiet: Off, Powerful: Off, Clock: 00:00, On Timer: Off, Off Timer: Off

データが多いのでわかりにくいですが、上記の行が内容になります。エアコンのリモコンは差分だけではなく、すべての情報をまとめて送信しています。そのため、送信するデータ量は非常に大量になります。

受信最小スケッチ例

#include <IRrecv.h>

// PIN, Buffer Size, Timeout, Save Buffer
IRrecv irrecv(26, 1024, 50, true);

void setup() {
  Serial.begin(115200);
  delay(50);

  irrecv.enableIRIn();
}

void loop() {
  decode_results results;
  if (irrecv.decode(&results)) {
    if (results.decode_type == NEC && results.address == 0) {
      if (results.repeat == false) {
        // 受信
        Serial.printf("受信: %d\n", results.command);
      } else {
        // リピート
        Serial.printf("リピート受信\n");
      }
    }
  }

  delay(1);
}

IRrecvDumpV3はいろいろな機能がありましたが、必要なさそうなものを削ってみると結構シンプルでした。

赤外線送信について

赤外線送信を利用する場合には、上記のような赤外線LEDを使います。使い方は普通のLEDと同じですが、赤外線なので実際に光っているのを目視で確認することはできません。

デジタルカメラなどでは確認することができる場合があります。ただ最近のスマホ用のカメラは赤外線を見えなくしているものが多いようです。小さい自撮り用のインカメラは比較的赤外線を確認できるものが多いようです。

M5StickCの場合には、GPIO9に赤外線LEDが内蔵されているのでそのまま赤外線を送信することが可能です。ただし、ちょっと奥まった場所に設置しているので飛びが弱いです。1メートル程度の距離しか届かないので、もう少し遠距離に送信したい場合には外部に赤外線LEDを接続したほうがいいです。

また、受信ではaddressとcommandをデコードしてから解析していましたが、送信の場合には見本となるリモコンのコードを受信してみて、Codeを確認して、その値をそのまま送信してしまったほうがかんたんです。

上記の記事では、ダイソーの300円リモコンライトを受信してCodeを解析後に、M5StickCの内蔵赤外線LEDで操作する方法を紹介しています。

赤外線送受信ユニット

上記の送受信がセットになって、Groveケーブルで接続するだけで使えるユニットもあります。こちらのほうが若干送信が強いと思います。

まとめ

赤外線の送受信はライブラリを使うことで、比較的かんたんに行うことが可能です。最近のスマートスピーカーなどで使われている赤外線ユニットは、送信用の赤外線LEDを複数個使って、いろいろな方向に飛ばしています。一個だけの赤外線LEDだとピンポイントに方向を合わせないと難しそうです。