M5StickCのPartition Tablesを調べる

/efont/を使うときに、プログラムサイズを広げたかったので調べました。

メモリーマップ

https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/general-notes.html#application-memory-layout

上記がデフォルトのメモリマップです。First-stage bootloaderが最初に起動して、Second-stage bootloaderをメモリに展開して、Second-stage bootloaderを実行します。

その後Partition tablesを見て、app0からプログラムをロードして実行するのが通常の流れですが、ここは変更しないところなので、知らなくても大丈夫です。

Arduino IDEでPartition Tablesの指定方法

上記のメニューから、初期値、No OTA、Minimal SPIFFSが選択できるのが標準の状態で、自分で作成することで、「No OTA Minimal SPIFFS」などの自分専用の設定を追加することができます。

Arduino IDEで指定できるPartition Tables

NamedefaultNo OTAMinimal SPIFFS
nvs20,48020,48020,480
otadata8,1928,1928,192
app01,310,7202,097,1521,966,080
app11,310,72001,966,080
eeprom4,0964,0964,096
spiffs1,503,2322,027,520192,512

上記が標準で入っている設定値です。

Partition解説

nvs(Non-volatile storage)

不揮発性ストレージで、電源を切っても保存される領域です。nvs_get_blob()関数などにより、名前をつけた値を保存することができます。ここのサイズはあまり変更しないようです。

Wi-Fiのアクセスポイントを保存するときとかに利用したりします。ただし、暗号化されていないのと、中身を取り出すことができるので、パスワードなどを保存して置くと、抜き出される可能性があります。

otadata

OTA(Over The Air)はWi-Fi経由でプログラムを更新する仕組みで、そのためのプログラムが入っている領域です。OTAを利用しない場合でも、この領域は必要で、固定サイズになります。

app0

実際のプログラムが入っている領域です。プログラムの領域が足りなくなった場合には、他の領域を減らして、ここの領域を増やすことができます。

通常は暗号化されていませんが、暗号化することも可能ですがちょっと複雑です。

app1

OTAを利用する場合には、app0とapp1の交互にプログラムを書き込んでいき、書き込みに失敗した場合でも、書き換え前のプログラムが残っている状態にします。

そのためapp0とapp1の大きさは同じにする必要があります。ただしOTAを利用しない場合には0で構いません。

eeprom(Electrically Erasable Programmable Read-Only Memory)

こちらもnvsと同じく不揮発性ストレージです。名前でアクセスする機能はなく、アドレス単位でのアクセスになります。構造体を使うことで簡単に複数の設定を保存したり、取得したいすることができます。

こちらも、通常は暗号化されていないのと、中身を取り出すことができるので、パスワードなどを保存して置くと、抜き出される可能性があります。

spiffs(Serial Peripheral Interface Flash File System)

SPIバス経由で接続されている内部フラッシュを利用した、ファイルシステムです。

ESP-WROOM-32 ( ESP32 ) SPIFFS アップローダープラグインの使い方

上記を参考に、dataフォルダを作って、ESP32 Sketch Data Uploadを実行するとフォルダの中身をESP32に転送してくれます。

spiffsの利点として、一度転送すれば上書きされることがないので、プログラムの転送サイズが減ります。OTAなどを利用した場合、プログラム内部にデータを内蔵しておくとapp0とapp1で同じデータが存在するので、spiffsにデータを置くことで容量を有効に使うことができます。

欠点として、初期状態で転送ツールがセットアップされないので、使うまでがちょっと面倒です。サイズが小さい場合にはプログラムの中に内蔵したほうがシンプルになると思います。

自分でPartition設定を作る

あまり自分でPartitionを編集する必要はないのですが、標準で用意されているプログラム容量は2Mまでなので、もっと大きなプログラムを転送したい場合には、自分で設定する必要があります。

C:\Users\%username%\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.2\tools\partitions

Windowsの場合には、上記にPartition Tablesが保存されています。ベースになるものをコピーして、名前を変更してから書き換えます。

# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x5000,
otadata,  data, ota,     0xe000,  0x2000,
app0,     app,  ota_0,   0x10000, 0x300000,
eeprom,   data, 0x99,    0x310000,0x1000,
spiffs,   data, spiffs,  0x311000,0xEF000,

上記がOTAを使わなくして、最大限app0のサイズを大きくしたものです。ただし各Partition最大が3Mまでの様で、3M以上を指定しても3Mとして動いています。

そのため、残りをspiffsに割り当てています。サイズを変更する場合には、増減したPartitionの次の領域のOffsetなどもずれるので、自分で計算して更新する必要があります。

ESP32はフラッシュが4Mですので、最後のOffsetとSizeを足した結果が4M以下になっている必要があります。

Arduino IDEに登録する

C:\Users\%username%\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.2\boards.txt

Windowsの場合には、上記に各ボードの設定ファイルがあるので、これを書き換えます。これはESP32のライブラリが更新されると上書きされるので、更新された場合には再度編集する必要があります。

m5stick-c.menu.PartitionScheme.default=Default
m5stick-c.menu.PartitionScheme.default.build.partitions=default
m5stick-c.menu.PartitionScheme.no_ota=No OTA (Large APP)
m5stick-c.menu.PartitionScheme.no_ota.build.partitions=no_ota
m5stick-c.menu.PartitionScheme.no_ota.upload.maximum_size=2097152
m5stick-c.menu.PartitionScheme.min_spiffs=Minimal SPIFFS (Large APPS with OTA)
m5stick-c.menu.PartitionScheme.min_spiffs.build.partitions=min_spiffs
m5stick-c.menu.PartitionScheme.min_spiffs.upload.maximum_size=1966080
m5stick-c.menu.PartitionScheme.no_ota_min_spiffs=No OTA Minimal SPIFFS (Large APPS without OTA)
m5stick-c.menu.PartitionScheme.no_ota_min_spiffs.build.partitions=no_ota_min_spiffs
m5stick-c.menu.PartitionScheme.no_ota_min_spiffs.upload.maximum_size=3145728

最後の3行が追加した行です。既存の行に追加することで新しい設定が追加されます。

設定を反映させるためにはArduino IDEを再起動する必要がありますので、再起動したらメニューに追加されているはずです。

M5StickCで/efont/を使ってみた

東雲フォントを使おうかと思いましたが、どうせならUNICODEが使える/efont/を使えるか検証してみました。

http://openlab.ring.gr.jp/efont/

結果

できました!

16ドットフォントを入れてみましたが、これなら十分実用できる品質ですね。

サンプルコード

#include <M5StickC.h>
#include "efontUTF16.h"
#include "efontUTF16M5StickC.h"

void setup() {
  M5.begin();
  M5.Lcd.setRotation(0);
  M5.Lcd.setCursor(0, 0);

  printEfont("新しい朝が来た希望の朝が");
  printEfont("新しい朝", 0, 16*4);
  printEfont("新しい朝", 0, 16*6, 2);
}

void loop() {
}

自作ライブラリ部分はまだ公開用に手をいれないといけないので、今後公開する予定です。フォントデータはベタッとUTF16の全フォントデータをフラッシュ領域に読み込んでいます。

全部だと2M弱、ハングル文字あたりを削ると1.3Mぐらい。コードサイズ的に通常だと動かないのでNo OTAとか、プログラム領域が大きいモードにしないと動きません。

転送時間も結構かかるので、SPIFFSから読み込むバージョンも作ってみたいと思います。

M5StickCでArduino用美咲フォントライブラを使ってみた

とりあえず、気軽に使えそうなライブラリで日本語実験です。

利用 ライブラリとフォント

上記をお借りいたしました。

コード

#include <M5StickC.h>
#include "misakiUTF16.h"

void setup() {
  M5.begin();
  M5.Lcd.setRotation(3);
  M5.Lcd.setCursor(0, 0);
  
  writeKnj("新しい朝がきた希望の朝が\n123\n");

  M5.Lcd.setTextSize(2);
  writeKnj("新しい朝がきた希望の朝が\n123\n");

  M5.Lcd.setTextSize(3);
  M5.Lcd.setTextColor(RED, BLUE);
  writeKnj("新しい朝がきた希望の朝が\n123\n");
}

void loop() {
}

void writeKnj(char *str) {
  int posX = M5.Lcd.getCursorX();
  int posY = M5.Lcd.getCursorY();
  uint8_t textsize = M5.Lcd.textsize;
  uint32_t textcolor = M5.Lcd.textcolor;
  uint32_t textbgcolor = M5.Lcd.textbgcolor;
  
  byte font[8];
  
  while( *str != 0x00 ){
    // 改行処理
    if( *str == '\n' ){
      // 改行
      posY += 8 * textsize;
      posX = M5.Lcd.getCursorX();
      str++;
      continue;
    }

    // 文字横幅
    int width = 8 * textsize;
    if( *str < 0x80 ){
      // 半角
      width = 4 * textsize;
    }

    // フォント取得
    str = getFontData( font, str );

    // 背景塗りつぶし
    M5.Lcd.fillRect(posX, posY, width, 8 * textsize, textbgcolor);

    // 取得フォントの描画
    for (uint8_t row = 0; row < 8; row++) {
      for (uint8_t col = 0; col < 8; col++) {
        if( (0x80 >> col) &amp; font[row] ){
          int drawX = posX + col * textsize;
          int drawY = posY + row * textsize;
          if( textsize == 1 ){
            M5.Lcd.drawPixel(drawX, drawY, textcolor);
          } else {
            M5.Lcd.fillRect(drawX, drawY, textsize, textsize, textcolor);
          }
        }
      }
    }

    // 描画カーソルを進める
    posX += width;
  }

  // カーソルを更新
  M5.Lcd.setCursor(posX, posY);
}

実行結果

さすがに8ドットフォントだと文字が小さいですね。東雲フォントの16ドットぐらいだときれいに表示されるかな?

実験としては描画は問題なさそうなので、もう少し他のフォントで実用レベルのライブラリを作りたいと思います。

SPIFFSは事前転送とかが面倒そうなので、オンメモリで動かすことを前提として進めていく予定です。

M5StickC組み込み漢字をテスト

実はM5StickCのライブラリには組み込みの漢字フォントが入っています。

フォントデータについて

PROGMEM指定されているので、利用するときにはRAMではなくて、FLASHに保存されていて、プログラムからの参照がなければ、実際には転送されないみたいです。

#include <M5StickC.h>

void setup() {
  Serial.println( sizeof( HZK16 ) );
}

void loop() {
}

ちなみに上記コードだと267616って返ってきますが、実際のフォントデータはありません。

#include <M5StickC.h>

void setup() {
  for( int i = 0 ; i < 3 ; i++ ){
    Serial.println( HZK16[i] );
  }
}

void loop() {
}

上のコートでもスケッチのサイズが増えていないので、データはありません。

#include <M5StickC.h>

void setup() {
  for( int i = 0 ; i < 4 ; i++ ){
    Serial.println( HZK16[i] );
  }
}

void loop() {
}

for文のループ回数を4回以上にすると、スケッチサイズが増えてフォントがFLASH領域に転送されます。

これはコンパイラの最適化とかの関係なのかな?

ちなみにfor文じゃなくて中身の行を並べても、スケッチサイズは増えませんでしたので、フォントは転送されていません。

M5.Lcd.loadHzk16();

正規のフォントロード関数を呼び出すと、実際に転送されるので使うときにはちゃんとロード関数を呼びましょう。呼び出していないときには、メモリ上少しだけ無駄になっていますが、大きなフォントデータが読み込まれていることはないと思います。

サンプルプログラム

#include <M5StickC.h>

void setup() {
  // 表示文字列セット
  char* AscStr="ASCII: \nABCDEFG12";
  char GbkStr[11] = { 0xB4, 0xF3, // 大
                      0xD0, 0xDC, // 熊
                      0xC3, 0xA8, // 猫
                      0xB4, 0xF3, // 大
                      0xBA, 0xC3, // 好
                      0x00
                    };
  
  M5.begin();

  // フォントデータロード
  M5.Lcd.loadHzk16();

  // 文字色白、背景色黒に設定
  M5.Lcd.setTextColor(WHITE, BLACK);
  
  // ハイライトカラーを赤に設定
  M5.Lcd.setHighlightColor(RED);
  
  // Hzk Font1(16ピクセル)でサイズ等倍、カーソル左上でASCII文字列表示
  M5.Lcd.setTextSize(1);
  M5.Lcd.setCursor(0,0,1);
  M5.Lcd.writeHzk(AscStr);

  // ハイライトして同じものを表示
  M5.Lcd.highlight(true);
  M5.Lcd.setCursor(0,32);
  M5.Lcd.writeHzk(AscStr);

  // 通常Font2(16ピクセル)でASCII文字列表示
  M5.Lcd.setTextSize(1);
  M5.Lcd.setCursor(0,64,2);
  M5.Lcd.print(AscStr);
  M5.Lcd.setTextFont(1);

  // ハイライトオフで漢字表示
  M5.Lcd.highlight(false);
  M5.Lcd.setCursor(0,102);
  M5.Lcd.writeHzk(GbkStr);
  
  // ハイライトオンで漢字表示
  M5.Lcd.highlight(true);
  M5.Lcd.setCursor(0,122);
  M5.Lcd.writeHzk(GbkStr);
}

void loop() {
}

サンプルのHZK16を少しだけ改造してあります。HZKフォントは等角フォントなので、標準のプロポーショナルフォントとはASCII文字でも表示が異なります。

実行結果

表示されていますね。ただし、これってGBK文字コードで、フォントも中国語しかないので、まあ使えません。

仕組み的には同じ方法で、日本語フォントもプログラム内蔵できるので今度チャレンジしてみたいと思います。

MkDocsでM5StickCの日本語リファレンスを書き始めてみた

Blogだとばらばらしちゃうので、MkDocsでまとめてみました。

まだ1ページだけですが、まとめるよりフォーマット決めるのが大変でした。

https://lang-ship.com/reference/unofficial/M5StickC/

MkDocsのインストール

Windows10上の場合、Microsoft Store経由だとpipでインストールできなかったので、オフィシャルサイトからダウンロードしてインストールしました。

https://www.python.org/

インストール場所を「C:\Program Files (x86)」とかにするといろいろ面倒そうなので、「C:\Pg\Python37」みたいな感じのPathにいれました。

https://www.mkdocs.org/

あとはmkdocsのインストール手順通り

  • pip install –upgrade pip
  • pip install mkdocs

でセットアップできます。

プロジェクト作成

  • mkdocs new my-project

プロジェクト設定

「my-project\mkdocs.yml」を編集します。

テーマ変更

https://squidfunk.github.io/mkdocs-material/

Materialを使おうと思いましたが、日本語検索がうまく動かなかったので、githubから最新版をダウンロードしてきて使っています。

https://github.com/squidfunk/mkdocs-material

上記からダウンロードしてきて、「my-project\mkdocs-material\material」にいれてあげます。

設定ファイル

markdown_extensions:
  - codehilite
  - admonition
  - toc:
      permalink: true

theme:
  name: null
  custom_dir: 'mkdocs-material/material'
  language: 'ja'
  palette:
    primary: 'indigo'
    accent: 'indigo'
  font:
    text: 'Roboto'
    code: 'Roboto Mono'
  logo:
    icon: 'cloud'
  feature:
    tabs: true

とりあえず、この設定はいまは作ってみました。

M5StickCのDisplay周り解析

サンプルスケッチにあるものだけ抜き出しています。すべての関数はAPIリファレンスなどで確かめてください。

https://lang-ship.com/reference/M5StickC/0.0.5/class_m5_display.html

代表的な関数列挙

カテゴリ関数名日本語
画面M5.Lcd.height()ディスプレイの縦幅を返す(回転に追従)
画面M5.Lcd.width()ディスプレイの横幅を返す(回転に追従)
画面M5.Lcd.setRotation()ディスプレイの向きを設定(0:M5が下, 1:電源ボタンが下, 2:上下反転, 3:RSTボタンが下)
画面M5.Lcd.setCursor()カーソルの座標とフォントを指定
M5.Lcd.color565()RGBデータを16ビットカラーに変換
テキストM5.Lcd.drawString()座標を指定して文字列を描画
テキストM5.Lcd.drawCentreString()座標を指定して文字列を中央寄せで描画
テキストM5.Lcd.drawRightString()座標を指定して文字列を右寄せで描画
テキストM5.Lcd.print()カーソル位置に文字列を描画
テキストM5.Lcd.println()カーソル位置に文字列を改行付きで描画
テキストM5.Lcd.printf()カーソル位置に文字列をprintf書式で描画
描画M5.Lcd.fillScreen()画面全体を塗りつぶす
描画M5.Lcd.drawPixel()点を描画
描画M5.Lcd.drawLine()線を描画
描画M5.Lcd.drawCircle()塗りつぶしの無い円を描画
描画M5.Lcd.drawRect()塗りつぶしの無い四角を描画
描画M5.Lcd.fillCircle()塗りつぶしの円を描画
描画M5.Lcd.fillRect()塗りつぶしの四角を描画
描画M5.Lcd.fillTriangle()塗りつぶしの三角形を描画
描画M5.Lcd.drawBitmap()Bitmap(BMP)形式の画像を描画
描画M5.Lcd.drawXBitmap()X Bitmap(XBM)形式の画像を描画
フォントM5.Lcd.highlight()テキストハイライトの設定(true:ハイライト色, false:フォント背景色 or 塗りつぶし無し)
フォントM5.Lcd.setHighlightColor()テキストハイライト色の設定
フォントM5.Lcd.setTextColor()フォント色の指定
フォントM5.Lcd.setTextDatum()テキスト描画開始座標指定(0:TopLeft, 1:TopCenter, 2:TopRight, 3…8)
フォントM5.Lcd.setTextFont()テキストフォントの設定(1-8)
フォントM5.Lcd.setTextSize()テキストサイズの設定(1-7)
テキストM5.Lcd.drawChar()Adafruit GLCDフォントで1文字描画
テキストM5.Lcd.drawNumber()7桁までのlong intの数値を描画
テキストM5.Lcd.drawFloat()7桁までのfloatの数値を描画
システムM5.Lcd.write()テキストを実際に書き込む
システムM5.Lcd.writecommand()ディスプレイにコマンド送信
システムM5.Lcd.writedata()ディスプレイにデータ送信

サンプルスケッチで使われている関数を拾い出してきました。ただし中国語フォント関係(HZK16)は抜いてあります。

画面サイズ

  • int16_t M5.Lcd.height()
  • int16_t M5.Lcd.width()

画面サイズを返却するだけの関数です。

画面の向き

  • M5.Lcd.setRotation( r )

M5Stackの画面方向と同じようです。M5StickCしか触ったことないので、実験しないとわかりませんでした。

画面カーソル位置

  • M5.Lcd.setCursor(x, y)
  • M5.Lcd.setCursor(x, y, font)

カーソルの場所とフォントを設定します。

  • uint16_t c = M5.Lcd.color565(r, g, b)

RGBから2バイトの色に変換します。

座標指定テキスト描画

  • M5.Lcd.drawString(string, x, y)
  • M5.Lcd.drawString(string, x, y, font)
  • M5.Lcd.drawCentreString(…)
  • M5.Lcd.drawRightString(…)

標準的なテキスト描画ですが、次のカーソルを利用した方がサンプルでは多用していました。

カーソル利用テキスト描画

  • M5.Lcd.print(string)
  • M5.Lcd.println(string)
  • M5.Lcd.printf(“%d”, i)

printとprintlnは改行無し、有りの出力形式で、printfは書式指定の出力です。両方ESP32のPrintクラスで定義されている関数で、Serial.println()などと同じように使うことができます。

内部的には文字列操作してから M5.Lcd.write()関数で描画しているのですが、drawString()系とは別関数で描画しています。

画面全体塗りつぶし

  • M5.Lcd.fillScreen(color)

画面全体を単色で塗りつぶします。色の定義は以下でされていました。

#define TFT_BLACK       0x0000      /*   0,   0,   0 */
#define TFT_NAVY        0x000F      /*   0,   0, 128 */
#define TFT_DARKGREEN   0x03E0      /*   0, 128,   0 */
#define TFT_DARKCYAN    0x03EF      /*   0, 128, 128 */
#define TFT_MAROON      0x7800      /* 128,   0,   0 */
#define TFT_PURPLE      0x780F      /* 128,   0, 128 */
#define TFT_OLIVE       0x7BE0      /* 128, 128,   0 */
#define TFT_LIGHTGREY   0xC618      /* 192, 192, 192 */
#define TFT_DARKGREY    0x7BEF      /* 128, 128, 128 */
#define TFT_BLUE        0x001F      /*   0,   0, 255 */
#define TFT_GREEN       0x07E0      /*   0, 255,   0 */
#define TFT_CYAN        0x07FF      /*   0, 255, 255 */
#define TFT_RED         0xF800      /* 255,   0,   0 */
#define TFT_MAGENTA     0xF81F      /* 255,   0, 255 */
#define TFT_YELLOW      0xFFE0      /* 255, 255,   0 */
#define TFT_WHITE       0xFFFF      /* 255, 255, 255 */
#define TFT_ORANGE      0xFDA0      /* 255, 180,   0 */
#define TFT_GREENYELLOW 0xB7E0      /* 180, 255,   0 */
#define TFT_PINK        0xFC9F

// Next is a special 16 bit colour value that encodes to 8 bits
// and will then decode back to the same 16 bit value.
// Convenient for 8 bit and 16 bit transparent sprites.
#define TFT_TRANSPARENT 0x0120

描画

  • M5.Lcd.drawPixel(x, y, color)
  • M5.Lcd.drawLine(x0, y0, x1, y1, color)
  • M5.Lcd.drawCircle(x0, y0, r, color)
  • M5.Lcd.drawRect(x, y, w, h, color)
  • M5.Lcd.fillCircle(x0, y0, r, color)
  • M5.Lcd.fillRect(x0, y0, x1, y1, color)
  • M5.Lcd.fillTriangle(x0, y0, x1, y1, x2, y2, color)

一般的な描画関数もあります。この他にもdrawEllipse()とかfillCircle()とかありましたが、サンプルスケッチに無かったのでここでは取り上げません。

https://lang-ship.com/reference/M5StickC/0.0.5/class_t_f_t__e_s_p_i.html

上記APIリファレンスとかで詳しくはみてください。

画像描画

  • M5.Lcd.drawBitmap(x, y, w, h, data)
  • M5.Lcd.drawXBitmap(x, y, bitmap, w, h, color)

drawBitmap()は2バイト単位の色データが並んでいる画像形式で、drawXBitmap()は1ビット1ピクセルの画像で、color色で塗りつぶします。

ハイライト

  • M5.Lcd.highlight(bool)
  • M5.Lcd.setHighlightColor(color)

ハイライトをtrueに設定すると、指定している背景色じゃなくて、ハイライト色で塗りつぶします。背景色を都度指定すればいい気がするのですが、よくわかりません。

フォント色

  • M5.Lcd.setTextColor(color)
  • M5.Lcd.setTextColor(color, bgcolor)

色の定義は以下です。TFTのと一緒な気がします。。。

#define BLACK               0x0000      /*   0,   0,   0 */
#define NAVY                0x000F      /*   0,   0, 128 */
#define DARKGREEN           0x03E0      /*   0, 128,   0 */
#define DARKCYAN            0x03EF      /*   0, 128, 128 */
#define MAROON              0x7800      /* 128,   0,   0 */
#define PURPLE              0x780F      /* 128,   0, 128 */
#define OLIVE               0x7BE0      /* 128, 128,   0 */
#define LIGHTGREY           0xC618      /* 192, 192, 192 */
#define DARKGREY            0x7BEF      /* 128, 128, 128 */
#define BLUE                0x001F      /*   0,   0, 255 */
#define GREEN               0x07E0      /*   0, 255,   0 */
#define CYAN                0x07FF      /*   0, 255, 255 */
#define RED                 0xF800      /* 255,   0,   0 */
#define MAGENTA             0xF81F      /* 255,   0, 255 */
#define YELLOW              0xFFE0      /* 255, 255,   0 */
#define WHITE               0xFFFF      /* 255, 255, 255 */
#define ORANGE              0xFD20      /* 255, 165,   0 */
#define GREENYELLOW         0xAFE5      /* 173, 255,  47 */
#define PINK                0xF81F

テキスト座標

  • M5.Lcd.setTextDatum(datum)

テキストの原点をどこに設定するか指定します。Advanced/Display/TFT_String_Alignのサンプルスケッチを参考にしてください。

フォント指定

  • M5.Lcd.setTextFont(font)
1Adafruit 8ピクセルASCIIフォント
216ピクセルASCIIフォント
3未設定
426ピクセルASCIIフォント
5未設定
626ピクセル数字フォント
748ピクセル7セグ風フォント
875ピクセル数字フォント

フォントの種類ごとに使える文字と、大きさが違うので注意してください。

フォントサイズ

  • M5.Lcd.setTextSize(size)

1-7まで指定できて、元の大きさを何倍にするかを指定します。

その他

  • M5.Lcd.drawChar()
  • M5.Lcd.drawNumber()
  • M5.Lcd.drawFloat()
  • M5.Lcd.write()
  • M5.Lcd.writecommand()
  • M5.Lcd.writedata()

いろいろありますが、内部的に利用していて直接呼び出すことはあまりないと思います。

まとめ

もうちょっとフォント周りと画像周りは検証したいですが、よく使う関数は列挙できたと思います。あとでもう少し見やすい形にまとめたいと思います。

ArduinoのAPIリファレンスってどこにあるの?

なんとなくググったページのコードとか、サンプルスケッチを見ながら作っていましたが、ちゃんとしたAPIリファレンスってどこにあるんだろう?

Arduinoオフィシャル言語リファレンス

オフィシャルの言語リファレンスは日本語版もありました。ただ検索だとあまり引っかからないです。上位に出てくるのは古いバージョンのリファレンスなので注意しましょう。

よく見たら日本語版は翻訳終わっていないと英語版が表示されるんじゃなくて、項目ごとなくなっているところが結構ありますね。英語版を見たほうが安全かもしれません。

ESP32とM5StickC

上記はオフィシャルソースだけれど、APIリファレンスはないのかな?

自作APIリファレンス(Doxygen)

さすがに無いと解析に不便なので、オフィシャルのライブラリをそのままDoxygenで出力してみました。

M5StickCの日本語リファレンスを作ろうかと思ったけれど、M5Displayまわりが大量の関数があって、ちょっと時間かかりそうなのでサンプルスケッチの確認からやろうかな。

M5StickCの6軸IMU(SH200Q)検証

謎の動きをしています。。。ジャイロと加速度の数値が逆な気がしますし、軸もおかしいような?

情報

IMUは英語のページと、データシートがあるので良かったです。

データシートでの軸

IMUは使ったことがないのですが、図を見る限り普通の座標系な気がします。

実験

机の上に、横に置いて、上から見た図の写真です。この方向からいろいろな方向に動かして、加速度とジャイロの反応を調べました。

表示されているのが上から、ジャイロの現在値、最大値、最小値。加速度の現在地、最大値、最小値です。ボタンを押すと数値をリセットするので、調べ終わったらリセットして、他の方向を調べています。

傾けて停止した場合、ジャイロの数値は変化した状態で安定し、加速度は0に近くなるはずが、逆に見えます。以下上が加速度、下がジャイロとして検証しています。データシート上は正しい数値を読み出している気がしますが、なんでだろう?

加速度(acc)

X軸加速度

左右に動かしても反応しなかったのですが、なんと右側を持ち上げるとプラスに反応。左側を持ち上げるとマイナスの加速度でした。

Y軸加速度

Y軸は手前に傾けるとプラス、手前を持ち上げるとマイナスでした。

Z軸加速度

Z軸は左に回転するとプラス、右に回転するとマイナスでした。

ジャイロ(Gyro)

X軸ジャイロ

元の状態が0で、手前を持ち上げるとプラスで、写真の状態で0.5になり、奥を持ち上げるとマイナスになります。

Y軸ジャイロ

元の状態が0で、右側を持ち上げるとプラスで、写真の状態で0.5になり、左側を持ち上げるとマイナスになります。

Z軸ジャイロ

元の状態が0.5で、どの方向にでも90度傾けると0.0になっていたように思えます。

実験プログラム

#include <M5StickC.h>

int16_t accX = 0;
int16_t accY = 0;
int16_t accZ = 0;

int16_t gyroX = 0;
int16_t gyroY = 0;
int16_t gyroZ = 0;

void setup() {
  // put your setup code here, to run once:
  M5.begin();
  M5.IMU.Init();
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextSize(1);
  M5.Lcd.setCursor(40, 0);
  M5.Lcd.println("SH200I TEST");
  M5.Lcd.setCursor(0, 15);
  M5.Lcd.println("  X       Y       Z");
  pinMode(M5_BUTTON_HOME, INPUT);
  pinMode(M5_BUTTON_RST, INPUT);
}

float max_gyroX = 0;
float min_gyroX = 0;
float max_gyroY = 0;
float min_gyroY = 0;
float max_gyroZ = 0;
float min_gyroZ = 0;

float max_accX = 0;
float min_accX = 0;
float max_accY = 0;
float min_accY = 0;
float max_accZ = 0;
float min_accZ = 0;

void loop() {
  if (digitalRead(M5_BUTTON_HOME) == LOW || digitalRead(M5_BUTTON_RST) == LOW ) {
    max_gyroX = 0;
    min_gyroX = 0;
    max_gyroY = 0;
    min_gyroY = 0;
    max_gyroZ = 0;
    min_gyroZ = 0;

    max_accX = 0;
    min_accX = 0;
    max_accY = 0;
    min_accY = 0;
    max_accZ = 0;
    min_accZ = 0;
    delay(1000);
  }

  // put your main code here, to run repeatedly:
  M5.IMU.getGyroData(&amp;gyroX, &amp;gyroY, &amp;gyroZ);
  M5.IMU.getAccelData(&amp;accX, &amp;accY, &amp;accZ);

  if ( max_gyroX < ((float) gyroX) * M5.IMU.gRes ) {
    max_gyroX = ((float) gyroX) * M5.IMU.gRes;
  }
  if ( ((float) gyroX) * M5.IMU.gRes < min_gyroX ) {
    min_gyroX = ((float) gyroX) * M5.IMU.gRes;
  }

  if ( max_gyroY < ((float) gyroY) * M5.IMU.gRes ) {
    max_gyroY = ((float) gyroY) * M5.IMU.gRes;
  }
  if ( ((float) gyroY) * M5.IMU.gRes < min_gyroY ) {
    min_gyroY = ((float) gyroY) * M5.IMU.gRes;
  }

  if ( max_gyroZ < ((float) gyroZ) * M5.IMU.gRes ) {
    max_gyroZ = ((float) gyroZ) * M5.IMU.gRes;
  }
  if ( ((float) gyroZ) * M5.IMU.gRes < min_gyroZ ) {
    min_gyroZ = ((float) gyroZ) * M5.IMU.gRes;
  }

  if ( max_accX < ((float) accX) * M5.IMU.aRes ) {
    max_accX = ((float) accX) * M5.IMU.aRes;
  }
  if ( ((float) accX) * M5.IMU.aRes < min_accX ) {
    min_accX = ((float) accX) * M5.IMU.aRes;
  }

  if ( max_accY < ((float) accY) * M5.IMU.aRes ) {
    max_accY = ((float) accY) * M5.IMU.aRes;
  }
  if ( ((float) accY) * M5.IMU.aRes < min_accY ) {
    min_accY = ((float) accY) * M5.IMU.aRes;
  }

  if ( max_accZ < ((float) accZ) * M5.IMU.aRes ) {
    max_accZ = ((float) accZ) * M5.IMU.aRes;
  }
  if ( ((float) accZ) * M5.IMU.aRes < min_accZ ) {
    min_accZ = ((float) accZ) * M5.IMU.aRes;
  }

  M5.Lcd.setCursor(0, 24);
  M5.Lcd.printf("%+7.2f %+7.2f %+7.2f\n", ((float) gyroX) * M5.IMU.gRes, ((float) gyroY) * M5.IMU.gRes, ((float) gyroZ) * M5.IMU.gRes);
  M5.Lcd.printf("%+7.2f %+7.2f %+7.2f\n", max_gyroX, max_gyroY, max_gyroZ);
  M5.Lcd.printf("%+7.2f %+7.2f %+7.2f\n", min_gyroX, min_gyroY, min_gyroZ);

  M5.Lcd.setCursor(0, 50);
  M5.Lcd.printf("%+7.2f %+7.2f %+7.2f\n", ((float) accX) * M5.IMU.aRes, ((float) accY) * M5.IMU.aRes, ((float) accZ) * M5.IMU.aRes);
  M5.Lcd.printf("%+7.2f %+7.2f %+7.2f\n", max_accX, max_accY, max_accZ);
  M5.Lcd.printf("%+7.2f %+7.2f %+7.2f\n", min_accX, min_accY, min_accZ);
}

サンプルスケッチのSH200Iに最小値と最大値を保存するようにした物になります。温度計は表示上邪魔なので消してしまいました。

まとめ

サンプルスケッチを動かしてみて、数値の動きが意図していたのと違っていたので実験してみましたが、予想と違う結果になりました。

加速度は純粋な座標軸じゃなくて、座標軸に対する回転方向の加速度な気がします。ただ、加速度とジャイロで軸の方向が違う気がするので謎が深まります。

ジャイロはおそらく重力を基準としているので、チップの付いている向きと重力の向きが違うのかもしれません。

ちょっと腑に落ちないところがあるので、他の人も検証してもらいたいです。私の端末がおかしいのか、そもそもデータシートから間違っているのかが知りたいです。。。

M5StickCのRTCをブラウザからセットする

NTPを使ってセットしたほうが簡単ですが、Wi-Fi接続環境がない場合用に、APモードとして動かして、ブラウザから時刻をセットできるようなものを作ってみました。

コード

#include <M5StickC.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include <WiFiAP.h>
#include "time.h"

const char *ssid = "M5StickC";
const char *password = "";

WiFiServer server(80);

RTC_TimeTypeDef RTC_TimeStruct;
RTC_DateTypeDef RTC_DateStruct;

int lastDrawTime = 0;

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

  M5.Lcd.setTextSize(1);
  M5.Lcd.setCursor(40, 0, 2);
  M5.Lcd.println("RTC AP TEST");

  Serial.println("Configuring access point...");

  WiFi.softAP(ssid, password);
  IPAddress myIP = WiFi.softAPIP();
  Serial.print("AP IP address: ");
  Serial.println(myIP);
  server.begin();

  M5.Lcd.print("AP : " );
  M5.Lcd.println(ssid);
  M5.Lcd.print("IP : " );
  M5.Lcd.println(myIP);

}

void loop() {
  WiFiClient client = server.available();

  if (client) {
    Serial.println("New Client.");
    String currentLine = "";

    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        Serial.write(c);
        if (c == '\n') {
          if (currentLine.length() == 0) {
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html; charset=utf-8;");
            client.println();

            client.println("<!DOCTYPE HTML>");
            client.println("<html>");
            client.println("<head>");
            client.println("<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />");
            client.println("<script>");

            client.println("function clickBtn(){");
            client.println("  var jikan= new Date();");
            client.println("  document.time.year.value = jikan.getFullYear();");
            client.println("  document.time.mon.value  = jikan.getMonth()+1;");
            client.println("  document.time.day.value  = jikan.getDate();");
            client.println("  document.time.week.value = jikan.getDay();");
            client.println("  document.time.hour.value = jikan.getHours();");
            client.println("  document.time.min.value  = jikan.getMinutes();");
            client.println("  document.time.sec.value  = jikan.getSeconds();");
            client.println("}");

            client.println("</script>");


            client.println("</head>");
            client.println("<body>");

            client.println("<form method=\"get\" name=\"time\">");
            client.println("<table>");
            client.println("<tr><th>year</th><td><input type=\"text\" name=\"year\" value=\"1900\" />1990-2099</td></tr>");
            client.println("<tr><th>mon</th><td><input type=\"text\" name=\"mon\" value=\"1\" />1-12</td></tr>");
            client.println("<tr><th>day</th><td><input type=\"text\" name=\"day\" value=\"1\" />1-31</td></tr>");
            client.println("<tr><th>week</th><td><input type=\"text\" name=\"week\" value=\"0\" />0-6</td></tr>");
            client.println("<tr><th>hour</th><td><input type=\"text\" name=\"hour\" value=\"0\" />0-23</td></tr>");
            client.println("<tr><th>min</th><td><input type=\"text\" name=\"min\" value=\"0\" />0-59</td></tr>");
            client.println("<tr><th>sec</th><td><input type=\"text\" name=\"sec\" value=\"0\" />0-59</td></tr>");
            client.println("<tr><th></th><td><input type=\"button\" value=\"ブラウザの時間をセットする\" onclick=\"clickBtn()\" /></td></tr>");
            client.println("<tr><th></th><td><input type=\"submit\" value=\"更新\" /></td></tr>");
            client.println("</table>");
            client.println("</form>");

            client.println("</body>");
            client.println("</html>");

            Serial.println("html Rendering");

            break;
          } else if (currentLine.indexOf("GET /?") == 0) {
            int pos1 = 0;
            int pos2 = 0;
            int val = 0;

            // Set RTC time
            RTC_TimeTypeDef TimeStruct;
            RTC_DateTypeDef DateStruct;

            // year
            pos1 = currentLine.indexOf('year=', pos2);
            pos2 = currentLine.indexOf('&amp;', pos1);
            val = currentLine.substring(pos1 + 1, pos2).toInt();
            DateStruct.Year = val;

            pos1 = currentLine.indexOf('mon=', pos2);
            pos2 = currentLine.indexOf('&amp;', pos1);
            val = currentLine.substring(pos1 + 1, pos2).toInt();
            DateStruct.Month = val;

            pos1 = currentLine.indexOf('day=', pos2);
            pos2 = currentLine.indexOf('&amp;', pos1);
            val = currentLine.substring(pos1 + 1, pos2).toInt();
            DateStruct.Date = val;

            pos1 = currentLine.indexOf('week=', pos2);
            pos2 = currentLine.indexOf('&amp;', pos1);
            val = currentLine.substring(pos1 + 1, pos2).toInt();
            DateStruct.WeekDay = val;

            pos1 = currentLine.indexOf('hour=', pos2);
            pos2 = currentLine.indexOf('&amp;', pos1);
            val = currentLine.substring(pos1 + 1, pos2).toInt();
            TimeStruct.Hours = val;

            pos1 = currentLine.indexOf('min=', pos2);
            pos2 = currentLine.indexOf('&amp;', pos1);
            val = currentLine.substring(pos1 + 1, pos2).toInt();
            TimeStruct.Minutes = val;

            pos1 = currentLine.indexOf('sec=', pos2);
            pos2 = currentLine.indexOf(' ', pos1);
            val = currentLine.substring(pos1 + 1, pos2).toInt();
            TimeStruct.Seconds = val;

            M5.Rtc.SetTime(&amp;TimeStruct);
            M5.Rtc.SetData(&amp;DateStruct);

            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html; charset=utf-8;");
            client.println();

            client.println("<!DOCTYPE HTML>");
            client.println("<html>");
            client.println("<head>");
            client.println("<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />");
            client.println("</head>");
            client.println("<body>");
            client.println("更新しました<br />");
            client.println("[<a href=\"/\">戻る</a>]<br />");

            Serial.println("RTC Update");
            Serial.println("html Rendering");

            break;
          } else {
            currentLine = "";
          }
        } else if (c != '\r') {
          currentLine += c;
        }
      }
    }

    client.stop();
    Serial.println("Client Disconnected.");
  }

  if( lastDrawTime + 500 < millis() ){
    static const char *wd[7] = {"Sun", "Mon", "Tue", "Wed", "Thr", "Fri", "Sat"};
    M5.Rtc.GetTime(&amp;RTC_TimeStruct);
    M5.Rtc.GetData(&amp;RTC_DateStruct);
    M5.Lcd.setCursor(0, 16 * 2);
    M5.Lcd.printf("Data: %04d-%02d-%02d(%s)\n", RTC_DateStruct.Year, RTC_DateStruct.Month, RTC_DateStruct.Date, wd[RTC_DateStruct.WeekDay]);
    M5.Lcd.printf("Time: %02d : %02d : %02d\n", RTC_TimeStruct.Hours, RTC_TimeStruct.Minutes, RTC_TimeStruct.Seconds);
    lastDrawTime = millis();
  }
}

概要

APモードで起動したあとに、「M5StickC」というSSIDにスマホなどから接続して「http://192.168.4.1」にアクセスすると、上記のような画面になります。

「ブラウザの時間をセットする」ボタンを押すと、テキストボックスが現在の時間にセットされるので、続けて更新ボタンを押すとM5StickCの時刻が更新されます。

やっていることは非常に単純ですが、Web Serverの処理がべた書きだと長くなりました。なにかライブラリを使ったほうがいいのかな?

M5StickCのRTCをNTPサーバーからセットする

M5Stick-Cはバッテリーも搭載されているので、RTC(Real Time Clock)で時間が保持できます。しかしながらサンプルスケッチではコードの中に時間を記述していたので、NTPから設定できるようにしてみました。

コード

#include <M5StickC.h>
#include <WiFi.h>
#include "time.h"

const char* ssid       = "your_ssid";
const char* password   = "your_password";

const char* ntpServer =  "ntp.jst.mfeed.ad.jp";

RTC_TimeTypeDef RTC_TimeStruct;
RTC_DateTypeDef RTC_DateStruct;

void setup() {
  // put your setup code here, to run once:
  M5.begin();
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(BLACK);

  M5.Lcd.setTextSize(1);
  M5.Lcd.setCursor(40, 0, 2);
  M5.Lcd.println("RTC NTP TEST");

  // connect to WiFi
  Serial.printf("Connecting to %s ", ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println(" CONNECTED");

  // Set ntp time to local
  configTime(9 * 3600, 0, ntpServer);

  // Get local time
  struct tm timeInfo;
  if (getLocalTime(&amp;timeInfo)) {
    M5.Lcd.print("NTP : ");
    M5.Lcd.println(ntpServer);

    // Set RTC time
    RTC_TimeTypeDef TimeStruct;
    TimeStruct.Hours   = timeInfo.tm_hour;
    TimeStruct.Minutes = timeInfo.tm_min;
    TimeStruct.Seconds = timeInfo.tm_sec;
    M5.Rtc.SetTime(&amp;TimeStruct);

    RTC_DateTypeDef DateStruct;
    DateStruct.WeekDay = timeInfo.tm_wday;
    DateStruct.Month = timeInfo.tm_mon + 1;
    DateStruct.Date = timeInfo.tm_mday;
    DateStruct.Year = timeInfo.tm_year + 1900;
    M5.Rtc.SetData(&amp;DateStruct);
  }

  //disconnect WiFi
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);
}

void loop() {
  static const char *wd[7] = {"Sun","Mon","Tue","Wed","Thr","Fri","Sat"};
  
  // put your main code here, to run repeatedly:
  M5.Rtc.GetTime(&amp;RTC_TimeStruct);
  M5.Rtc.GetData(&amp;RTC_DateStruct);
  M5.Lcd.setCursor(0, 30);
  M5.Lcd.printf("Data: %04d-%02d-%02d\n", RTC_DateStruct.Year, RTC_DateStruct.Month, RTC_DateStruct.Date);
  M5.Lcd.printf("Week: %s\n", wd[RTC_DateStruct.WeekDay]);
  M5.Lcd.printf("Time: %02d : %02d : %02d\n", RTC_TimeStruct.Hours, RTC_TimeStruct.Minutes, RTC_TimeStruct.Seconds);
  delay(500);
}

概要

RTCのサンプルスケッチをベースに、Wi-Fiで接続してNTPから現在時刻を取得するように変更しただけのコードです。

アクセスポイントの情報を書き換えて転送すれば時刻が設定されるので、複数のM5Stick-Cに時刻設定するのは楽になるかな?