M5StickCでUART(Serial)を使う

2本線での通信ですので、簡単に試せます。

UARTとは

送信と受信の通信線をクロスにつないだ通信方式です。2本の通信線で通信ができるので簡単ですが、フロー制御などがないので文字化けとか通信データの欠落などがでてきます。

ESP32では3系統のUARTが利用することができます。

  • Serial : USB接続した場合にPCと通信するシリアル
  • Serial1 : 2系統目のシリアル
  • Serial2 : 3系統目のシリアル

1系統目はPCとの接続に利用しています。2系統目と3系統目はどちらを使ってもいいですし、両方同時に使うことも可能です。

また、Bluetooth経由でワイヤレス接続のBluetoothSerialってのもありますが、こちらは後日紹介します。

ピン配置

入力専用のIO36を除き、外部接続可能な4PINはどんな組み合わせでもI2Cで通信が可能でした。

サンプル配線

M5StickCのみで実験をする場合の配線です。特徴としてはRXとTXを接続することです。RXがReciverで受信、TXはTransmitterで送信を意味しています。

  • Serial1 : RX(0), TX(26)
  • Serial2 : RX(32), TX(33)

上記にアサインしています。Groveは32,33の順で割り当てるのが標準ですが、EXT IOの0と26は逆でも構いませんが、I2Cと同じように若い番号から割り当てています。

サンプルコード

#include <M5StickC.h>

void setup() {
  M5.begin();

  Serial1.begin(115200, SERIAL_8N1, 0, 26); // EXT_IO
  Serial2.begin(115200, SERIAL_8N1, 32, 33);// Grove
}

void loop() {
  if (Serial.available()) {
    // Serial(PC) to Serial1(EXT_IO)
    int inByte = Serial.read();
    Serial1.write(inByte);
  }
  if (Serial1.available()) {
    // Serial1(EXT_IO) to Serial2(Grove)
    int inByte = Serial1.read();
    Serial2.write(inByte);
  }
  if (Serial2.available()) {
    // Serial2(Grove) to Serial(PC)
    int inByte = Serial2.read();
    Serial.write(inByte);
  }
}

Arduinoのシリアルモニタから送信したデータをSerial(USB)で受信して、Serial1(EXT_IO)に出力。Serial1(EXT_IO)で受信したデータをSerial2(Grove)に送信。Serial2(Grove)で受信したデータをSerial(USB)に送信というサンプルです。

通信速度について

サンプルは一番最速の「115200」を指定していますが、速度が早いとエラーになる可能性があがるので、もう少し遅い速度を指定したほうが安定すると思います。

  • 300
  • 1200
  • 2400
  • 4800
  • 9600
  • 14400
  • 19200
  • 28800
  • 38400
  • 57600
  • 115200

あたりが指定できますが、相手と同じ速度を指定する必要があるので、相手が固定値だと選択肢がないですが、速度が必要ない場合には9600あたりか、少し早い19200あたりの方がエラーは少ないと思います。

遅めで通信をして、速度が必要になった場合に早くするほうが苦労が少ないと思います。

外部装置との接続例

ESP32の開発ボードとM5StickCをUARTで接続する実験をしてみました。

M5StickC側サンプルスケッチ

#include <M5StickC.h>

void setup() {
  M5.begin();

  Serial2.begin(115200, SERIAL_8N1, 32, 33);
}

void loop() {
  if (Serial2.available()) {
    int inByte = Serial2.read();
    Serial.write(inByte);
  }

  if (Serial.available()) {
    int inByte = Serial.read();
    Serial2.write(inByte);
  }
}

Grove側の32と33を使って接続しています。ESP32から送信されたデータをSerial2(Grove)で受信して、Serial(USB)に送信しています。また、Serial(USB)から送信されたデータをSerial2(Grove)に送信しています。

ESP32開発ボード側サンプルスケッチ

void setup() {
  Serial.begin(115200);  
  Serial2.begin(115200, SERIAL_8N1, 32, 33);
}

void loop() {
  if (Serial2.available()) {
    int inByte = Serial2.read();
    Serial.write(inByte);
  }

  if (Serial.available()) {
    int inByte = Serial.read();
    Serial2.write(inByte);
  }
}

ほぼ一緒ですが、M5.begin()の中で実行されているSerial.begin()を自分で呼び出す必要があります。

実行の注意点

Arduino IDEのシリアルモニタは1つのシリアルしか開けません。開発ボードを変更すると他の設定も一度に変わってしまうので、別のパソコンとかに繋げて実験するのが一番動かしやすいです。

UARTの注意点

電圧差異

M5StickCは内部は3.3Vで動いていますが、一般的なArduinoボードは5Vで動いています。そのまま接続すると電圧が異なりますので、繋げないほうがいいです。

M5StickCは実際には5V信号を入力しても、壊れない気がしますがデータシート上は5Vは許容していませんので、電圧変換の回路やICを間にいれて接続する必要があります。

物によっては12Vとかを流してくるシリアルもあるので、安易に繋ぐのはやめましょう。

速度差異

お互いに同じ速度を設定しないと通信ができません。複数機材を接続するのであれば、お互いに同じ値を設定するればよいのですが、LTEモジュールなどの通信機器と接続するためには、相手側の初期値で接続してから、変更するなどをする必要があるので注意しましょう。

エラー処理

基本的に1バイト単位で送受信する機能しかありませんので、途中でエラーが発生したり、相手側が受信を失敗しているかを確認する方法が標準では提供されていません。

エラー処理は自分で考慮する必要があるので注意して使ってください。

まとめ

UART自体は簡単に利用することができます。ただし長い距離を通信させることや、高速で通信する場合にはエラー処理などがちょっと不安ですので、注意が必要そうです。

パソコンと接続するのであればBluetoothSerialを使うことで、速度とエラー処理などの心配がほぼ無くなるのでおすすめです。

M5StickCのライブラリ0.0.7がリリース

リリースされていたので、差分を調べてみました。

クラス構造

  • Buttonクラス追加
  • MPU6866クラス追加

ボタン管理用クラスと、I2CセンサーのMPU6866が増えましたが、中身はジャイロとかあるから加速度センサーかな?

これI2Cのアドレスが0x68なんで今後増える外付け用ライブラリか、M5Stack系のライブラリが混入したのかな?

もしくは、現行の加速度センサーがなんかおかしいので載せ替えか、追加?

個別変更点

AXP192クラス

  • uint16_t  GetVusbinData (void) : USBからの電圧取得
  • uint16_t  GetIusbinData (void) : USBからの電流取得
  • uint16_t  GetVapsData (void) : 内部動作電圧取得
  • uint8_t  GetBtnPress (void) : 割り込みでのボタン状態取得(IRQステータスレジスタ3)
  • void  SetSleep (void) : スリープ設定
  • void  DeepSleep (uint64_t time_in_us=0) : ディープスリープ設定
  • void  LightSleep (uint64_t time_in_us=0) : ライトスリープ設定
  • uint8_t  GetWarningLeve (void) : 警告レベル取得

上記が追加されています。GetVusbinData()系はプルリクエスト送ったらマージしてくれました。

スリープ系が結構増えていますね。

IMUクラス

  • void getAccelAdc (int16_t *ax, int16_t *ay, int16_t *az)
  • void getGyroAdc (int16_t *gx, int16_t *gy, int16_t *gz)
  • void getTempAdc (int16_t *t)

上記が増えていました。Adcだとint16_tで、既存のDataだとfloatで返却していますね。

スケッチ例

─Advanced
│  ├─*AXP192
│  │  ├─*PowerWake
│  │  └─*sleep
│  ├─Display
│  │  ├─Cellular_Automata
│  │  ├─drawXBitmap
│  │  ├─HZK16
│  │  ├─TFT_Clock
│  │  ├─TFT_Float_Test
│  │  ├─TFT_Pie_Chart
│  │  ├─TFT_Print_Test
│  │  ├─TFT_Spiro
│  │  ├─TFT_Starfield
│  │  ├─TFT_String_Align
│  │  └─TFT_Terminal
│  ├─I2C_Tester
│  └─QRCode
├─Basics
│  ├─AXP192
│  ├─Display
│  ├─FactoryTest
│  ├─HelloWorld
│  ├─Micophone
│  ├─*MPU6866
│  ├─RTC
│  └─SH200I
├─Games
│  ├─*Dices
│  └─FlappyBird
├─*Hat
│  ├─*ENV
│  ├─*MLX90640
│  ├─*NCIR_HAT
│  ├─*PIR
│  └─*SPEAKER
└─*Unit
    ├─*ADC_ADS1100
    ├─*ANGLE
    ├─*BUTTON
    ├─*CardKB
    ├─**Color
    ├─*DAC_MCP4725
    ├─*Dual_Button
    ├─*Earth
    ├─*ENV
    ├─*EXT_IO
    ├─*Fingerprint
    ├─*GPS
    │  └─*GPSRaw
    ├─*HEART
    │  └─*MAX30100_RawData
    ├─*IR
    ├─*Joystick
    ├─*Light
    ├─*Makey
    ├─*MLX90640
    ├─*NCIR
    ├─*NEOFLASH
    ├─*NEOPIXEL
    │  └─*display_rainbow
    ├─*PaHUB
    ├─*PbHUB
    ├─*PIR
    ├─*Relay
    ├─*RFID
    ├─*RGB
    ├─*TOF_VL53L0X
    ├─*TRACE
    └─*WEIGHT

ものすごく増えています!

*が先頭についているものが新規追加です。

まとめ

細かいところは、あとで検証しますがM5StickC.hにコメントが追加されていますが、クラス名がM5Stackになっているので、修正してもわないと。。。

//! Licensed under the MIT license. See LICENSE file in the project root for full license information.
/**
 * \par Copyright (C), 2016-2017, M5Stack
 * \class M5Stack
 * \brief   M5StickC library.
 * @file    M5StickC.h

あとはESP32プラットフォーム以外でもコンパイルできていたのが、エラーになるように修正されていました。

M5StickCでI2C通信をする

I2C通信を実験してみました。

概要

ESP32は最大2組のI2Cが可能で、M5StickCは内部で1つ使っています。

入力専用のIO36を除き、外部接続可能な4PINはどんな組み合わせでもI2Cで通信が可能でした。

接続方法

M5StickCは上側に外部接続用のピンソケットと、下側にGrove端子がついています。IO26とIO0のような組み合わせも可能でしたが、通常はピンソケットのIO0とIO26、Grove端子のIO32とIO33の組み合わせで使うと思います。

おすすめピンアサイン

  • ピンソケット SDA:IO0, SCL:26
  • Grove端子 SDA:IO32, SCL:33

GroveのI2C端子が上記のアサインなので、逆に使うと混乱します。ピンソケット側はどちらでも構わないのですが、OfficialのI2Cを使ったHATがこのピンアサインだったので、こちらを使ったほうが無難だと思います。

同時に2系統使えるの?

内部のI2C通信を使わないのであれば、同時に利用が可能でした。

void setup() {
  M5.begin();

  Wire.begin(32, 33);
  Wire1.begin(0, 26);
}

Wireが1系統目のI2C通信で、Wire1が2系統目のI2C通信です。通常Wire1は電源管理などの内部接続用I2Cとして使われています。

M5.begin()で電源管理の初期化をしてしまえば、あとは接続していなくても問題ないかと思いますので、Wire1.begin()で別のピンにアサインしなおします。

3系統同時に使えないの?

#include <M5StickC.h>

void setup() {
  M5.begin();

  Wire.begin(32, 33);
}

void loop() {
  byte error, address;
  int nDevices;

  Serial.println("Scanning... Wire");

  nDevices = 0;
  for (address = 1; address < 127; address++ )
  {
    Wire.beginTransmission(address);
    error = Wire.endTransmission();

    if (error == 0)
    {
      Serial.print("I2C device found at address 0x");
      if (address < 16)
        Serial.print("0");
      Serial.print(address, HEX);
      Serial.println("  !");

      nDevices++;
    }
    else if (error == 4)
    {
      Serial.print("Unknown error at address 0x");
      if (address < 16)
        Serial.print("0");
      Serial.println(address, HEX);
    }
  }
  if (nDevices == 0)
    Serial.println("No I2C devices found\n");
  else
    Serial.println("done\n");

  Serial.println("Scanning... Wire1");
  Wire1.begin(0, 26);

  nDevices = 0;
  for (address = 1; address < 127; address++ )
  {
    Wire1.beginTransmission(address);
    error = Wire1.endTransmission();

    if (error == 0)
    {
      Serial.print("I2C device found at address 0x");
      if (address < 16)
        Serial.print("0");
      Serial.print(address, HEX);
      Serial.println("  !");

      nDevices++;
    }
    else if (error == 4)
    {
      Serial.print("Unknown error at address 0x");
      if (address < 16)
        Serial.print("0");
      Serial.println(address, HEX);
    }
  }
  if (nDevices == 0)
    Serial.println("No I2C devices found\n");
  else
    Serial.println("done\n");

  Serial.println("Scanning... Wire1-2");
  Wire1.begin(21, 22);

  nDevices = 0;
  for (address = 1; address < 127; address++ )
  {
    Wire1.beginTransmission(address);
    error = Wire1.endTransmission();

    if (error == 0)
    {
      Serial.print("I2C device found at address 0x");
      if (address < 16)
        Serial.print("0");
      Serial.print(address, HEX);
      Serial.println("  !");

      nDevices++;
    }
    else if (error == 4)
    {
      Serial.print("Unknown error at address 0x");
      if (address < 16)
        Serial.print("0");
      Serial.println(address, HEX);
    }
  }
  if (nDevices == 0)
    Serial.println("No I2C devices found\n");
  else
    Serial.println("done\n");

  delay(5000);
}

I2Cのアドレススキャナで検証しましたが、使う前にbigin()でアドレス指定をすれば使えそうです。

ただ、複数使うのはトラブルになりそうなので、可能であればピンソケットかGroveのどちらか1系統だけを使ったほうが安全だと思います。

ESP32のPWM出力は255が最大じゃなかった

ledcWrite()の指定がちょっとモヤッていたのだが、上記をみてやっぱり255じゃないのがわかりました。

ESP32のPWM出力について

ESP32でPWM出力を行う場合には、事前に周波数と分解能をセットします。

#include <M5StickC.h>
 
int PIN = 26;
int PWMCH = 0;
 
void setup() {
  M5.begin();
 
  pinMode(PIN, OUTPUT);
  ledcSetup(PWMCH, 7812.5, 8); // 7812.5Hz, 8Bit(256段階)
  ledcAttachPin(PIN, PWMCH);
  ledcWrite(PWMCH, 128);  //  50%(1.7V)
}
 
void loop() {
}

上記の場合、ledcSetup(PWMCH, 7815.5, 8)で7812.5Hzの8ビット精度です。

7812.5Hzは1秒間に7812.5回の周波ですので、1回あたり128usになります。

上記がオシロスコープの結果ですが、1マスが10usで、ONとOFFのワンセットで13マス弱ですので128usぐらいですね。

8ビット精度ってことは256段階ですから、128usを更に256分割の0.5us単位で制御していることになります。

上記がledcWrite(PWMCH, 1)の結果です。128のときには256分の128なので、50%が電圧出力でしたが、今回は256分の1の出力になっています。

さらに拡大した図ですが、1マス100nsなので0.1usが5マスの0.5us間のパルスが出力されています。ledcWrite(PWMCH, 2)にすると倍の1us間のパルスが出力されるはずです。

最大値は8ビット精度の場合には256になります。DACは0から255までですが、8ビットの場合PWMは0から256までの範囲になるので注意してください。

分解能別最大周波数

Bit分解能最大周波数
16655361,220.70Hz
15327682,441.41Hz
14163844,882.81Hz
1381929,765.63Hz
12409619,531.25Hz
11204839,062.50Hz
10102478,125.00Hz
9512156,250.00Hz
8256312,500.00Hz
7128625,000.00Hz
6641,250,000.00Hz
5322,500,000.00Hz
4165,000,000.00Hz
3810,000,000.00Hz
2420,000,000.00Hz
1240,000,000.00Hz

最大周波数を利用したい場合にはビット数1で40MHzになります。この場合には分解能が2ですので、ONとOFFが交互にくる50%の矩形波しか出力できません。

一番分解能を上げると16ビットで1220.70Hzまで周波数が下がります。8ビットの場合には312.5KHzですので、この数値以下を指定して利用しましょう。

参考サイト

M5StickCのIOについて調べてみた

とりあえず1PIN単位で動くものだけ調べてみました。

PIN配置

GROVEのVOUTは5Vでした。しなしながらIO32とIO33のアナログ入力は3.3Vまでなので、アナログのGROVEセンサーとかをつなげると物によってはちゃんと動かない気がします。

PIN設定

PINIOMapFunction
IO26I/OExtended IO portGPIO26, DAC_2, ADC2_CH9, RTC_GPIO7, EMAC_RXD1
IO36(SENSOR_VP)IExtended IO portGPIO36, ADC1_CH0, RTC_GPIO0
IO0I/OMicrophone SCL
Extended IO port
ADC2_CH1, TOUCH1, RTC_GPIO11, CLK_OUT1, EMAC_TX_CLK
IO32I/OGROVE SDA32K_XP (32.768 kHz crystal oscillator input), ADC1_CH4, TOUCH9, RTC_GPIO9
IO33I/OGROVE SCL32K_XN (32.768 kHz crystal oscillator output), ADC1_CH5, TOUCH8, RTC_GPIO8

外部からアクセスができる上記5PINが調査対象です。

調査結果

PINdigitalRead()analogRead()touchRead()dacWrite()digitalWrite()ledcWrite()
IO26NGNG
IO36NGNGNGNGNG
IO0NGNGNG
IO3233NG
IO3332NG

IO26はADC2_CH9に繋がっているのに、Wi-fi使っていない場合でもアナログ入力できませんでした。上側にあるポートからはアナログ入力できなそうですね。

タッチセンサーがまた微妙で、ESP32のデータシートがおそらく間違っていて、内部的に逆に接続されている気がします。

調査方法

digitalRead() デジタル入力 0(1.65V未満) or 1(1.65V以上)

#include <M5StickC.h>

int PIN = 32;

void setup() {
  M5.begin();

  pinMode( PIN, INPUT);
}

void loop() {
  Serial.printf("%04d\n", digitalRead(PIN) );
  delay(500);
}

上記コードのPIN変数を変えていって実験しました。digitalRead()はどのポートでも利用できます。

analogRead() アナログ入力 0(0V)-4095(3.3V)

#include <M5StickC.h>

int PIN = 32;

void setup() {
  M5.begin();

  pinMode( PIN, INPUT);
}

void loop() {
  Serial.printf("%04d\n", analogRead(PIN) );
  delay(500);
}

アナログ入力はGROVE端子のIO32とIO33でしか正しく取れません。上にある端子の場合0と4095のどちらかの数値になりますので、digitalRead()の結果を4095倍している値になっています。

touchRead() 静電容量取得 0に近いほうがタッチ

#include <M5StickC.h>

int PIN = 32;
boolean touched = false;
int threshold = 16;

void gotTouch() {
  touched = true;
}

void setup() {
  M5.begin();

  pinMode( PIN, INPUT);
  touchAttachInterrupt(PIN, gotTouch, threshold);
}

void loop() {
  if (touched) {
    Serial.println("touch!");
    touched = false;
  }
  Serial.printf("%4d\n", touchRead(PIN) );
  delay(500);
}

touchRead()は触ると数値が小さくなるので、触らないときの数字と、触って下がったときの数字の中間か、やや低い値をthresholdに設定してください。

タッチは初めて実験してみましたが、混乱しました。IO32とIO33の結果が逆でした。

コードを見ても、データシートも見ても問題ないはずだったので、他のESP32 Devボードで実験してみたら、やっぱり逆です。

たぶんデータシートが間違っていますね。

typedef struct {
    uint8_t reg;      /*!< GPIO register offset from DR_REG_IO_MUX_BASE */
    int8_t rtc;       /*!< RTC GPIO number (-1 if not RTC GPIO pin) */
    int8_t adc;       /*!< ADC Channel number (-1 if not ADC pin) */
    int8_t touch;     /*!< Touch Channel number (-1 if not Touch pin) */
} esp32_gpioMux_t;

こんな構造体があって、ArduinoではGPIOを管理していました。

PINregrtcadctouch
00x4411111
10x88-1-1-1
20x4012122
30x84-1-1-1
40x4810100
50x6c-1-1-1
60x60-1-1-1
70x64-1-1-1
80x68-1-1-1
90x54-1-1-1
100x58-1-1-1
110x5c-1-1-1
120x3415155
130x3814144
140x3016166
150x3c13133
160x4c-1-1-1
170x50-1-1-1
180x70-1-1-1
190x74-1-1-1
200x78-1-1-1
210x7c-1-1-1
220x80-1-1-1
230x8c-1-1-1
240-1-1-1
250x24618-1
260x28719-1
270x2c17177
280-1-1-1
290-1-1-1
300-1-1-1
310-1-1-1
320x1c949
330x20858
340x1446-1
350x1857-1
360x0400-1
370x0811-1
380x0c22-1
390x1033-1

こんな感じのマトリクスで、内部レジスタのアドレスとか番号が並んでいます。

PINregrtcadctouch
320x1c949
330x20858

該当部分だけ抜きですと、RTCとTouchとかはPIN番号大きい方から割り当てていますが、ADCは逆です。Touchもデータシート上は上記の記述ですが、実際の実装はIO32がT8でIO33がT9になっていると思われます。

ここ以外のいろんな場所でも逆に定義されているので、ライブラリは修正されない気もします。

GROVE – タッチセンサ

一応商品としてはありますので、使うときにはIO32を取得してください。

dacWrite() アナログ出力 0(0V)-255(3.3V)

#include <M5StickC.h>

int PIN = 26;

void setup() {
  M5.begin();

  pinMode(PIN, OUTPUT);
}

void loop() {
  dacWrite(PIN, 0);
  delay(500);
  dacWrite(PIN, 128);
  delay(500);
  dacWrite(PIN, 255);
  delay(500);
}

dacWrite()はIO26でしか使えません。(内部的にはIO25でも可能)

IO26からの出力にLEDと抵抗に接続してものをオシロスコープで測定した結果です。若干電圧低下していますが、概ね255で3.3V程度、128でその半分の電圧が出力されています。

digitalWrite() デジタル出力 LOW(0V) or HIGH(3.3V)

#include <M5StickC.h>

int PIN = 32;

void setup() {
  M5.begin();

  pinMode(PIN, OUTPUT);
}

void loop() {
  digitalWrite(PIN, HIGH);
  delay(500);
  digitalWrite(PIN, LOW);
  delay(500);
}

IO36はIOがInputのみなので、出力には使えませんが、それ以外のPINでは使えました。

ledcWrite() PWM出力

#include <M5StickC.h>

int PIN = 32;
int PWMCH = 0;

void setup() {
  M5.begin();

  pinMode(PIN, OUTPUT);
  ledcSetup(PWMCH, 12000, 8);
  ledcAttachPin(PIN, PWMCH);
}

void loop() {
  ledcWrite(PWMCH, 0);    //   0%(0.0V)
  delay(500);
  ledcWrite(PWMCH, 128);  //  50%(1.7V)
  delay(500);
  ledcWrite(PWMCH, 256);  // 100%(3.3V)
  delay(500);
}

PWMはdigitalWrite()で出力を定期的にON/OFFさせることで、擬似的なdacWrite()に似た動きになります。PWMは4PIN同時に利用することが可能で、細かい設定はledcSetup()で行っています。サンプルは周波数が 12KHz で分解能が8ビット(0-256)になっています。

8ビットの場合、256分割してそのうち何個を出力するかの指定なので、出力しないの0と全部出力するの256までの257段階で制御が可能です。

上記はキャプチャ用に周波数50Hzに落として実験しましたが、上記のように128(50%)を指定するとONとOFFが交互にきて、平均すると半分の電圧相当になります。

LEDの明るさ調整とかであればdacWrite()でなくて、PWM制御で十分明るさが変わります。

まとめ

GROVEの電源が5Vなんで、ちょっと注意が必要そうですね。あと本家だといろいろな拡張HATが開発されているようですので、楽しみです。

今後はI2Cとか2PIN以上必要な通信を調べていきたいと思います。

Arduino(M5StickC)でefont Unicodeフォント表示 完結編

/efont/さんのUnicode Fontを利用して、Arduino用のフォントライブラリを作ってみました。

いろいろ実験した結果、文字単位で読み込むかを決めて、フォント用のテーブルから探す方式がメモリ効率と速度のバランスが一番良かったです。

利用フォント

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

上記のf16.bdfとb16.bdfを利用させていただき、16ピクセルフォントを作成しました。

作成物

https://github.com/tanakamasayuki/efont

フォントサイズ

対象オプション文字数フォント容量
すべてefontEnableAll.h21,727738,718
AsciiefontEnableAscii.h1916,494
CJK漢字efontEnableCJK.h19,379658,886
簡体字中国語efontEnableCn.h18,077614,618
日本語efontEnableJa.h10,835368,390
日本語(常用+α)efontEnableJaMini.h4,107139,638
韓国語efontEnableKr.h8,319282,846
繁体字中国語efontEnableTw.h13,555460,870

/efont/は2万文字強収録されており、256文字単位での読み込みだと使っていないエリアがたくさんあって無駄になっていたので、1文字単位で読み込んでいます。

16ピクセルフォントなので、1文字あたり32Byteの字形データ(PROGMEM領域)と、UTF16の文字コードテーブルで2Byte使うので、1文字あたり34Byteです。

このサイズだったら全部読み込んで、メモリが足りなくなってから使わない文字を減らす運用でも大丈夫かもしれません。

ちなみに0(U+0030)の文字が有効になっていない場合には、フォントの指定をしていないとみなして、efontEnableAll.hを読み込むようにしています。

実行結果

サンプルコード

#include <M5StickC.h>
#include "efontEnableAll.h"
//#include "efontEnableAscii.h"
//#include "efontEnableCJK.h"
//#include "efontEnableJa.h"
#include "efont.h"
#include "efontM5StickC.h"

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

  printEfont("Hello", 0, 16*0);
  printEfont("こんにちは", 0, 16*1);
  printEfont("你好", 0, 16*2);
  printEfont("안녕하세요", 0, 16*3);
  printEfont("Доброе утро", 0, 16*4);
  printEfont("Päivää", 0, 16*6);
  printEfont("Здравствуйте", 0, 16*7);
}

void loop() {
}

ビルド結果

最大1310720バイトのフラッシュメモリのうち、スケッチが977224バイト(74%)を使っています。
最大327680バイトのRAMのうち、グローバル変数が14652バイト(4%)を使っていて、ローカル変数で313028バイト使うことができます。

ちなみにM5StickCのスケッチ例「HelloWorld」との比較。

スケッチフラッシュRAM
efont977,22414,652
HelloWorld226,74814,532
750,476120

んー、もう少しグローバル変数使っている気がするけれど、調べたらESP32実機だとconstつけたデータは、PROGMEMをつけていなくてもフラッシュ領域になるみたいですね。

まとめ

描画が遅いのはちょっとどうにかしないといけないのですが、描画周りはちょっと後にして他の機能を検証する予定です。

[実験] Arduino(M5StickC)でefont Unicodeフォント表示 SPIFFS版

/efont/さんのUnicode Fontを利用して、Arduino用のフォントライブラリを作ってみました。

PROGMEMだとプログラム転送時に毎回大きなフォントデータも転送する必要があるので、一度転送すればよいSPIFFSで作ってみました。

注意

こちらは実験ですので、実際に使う場合には以下のページを参考にしてください。

利用フォント

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

上記のf16.bdfとb16.bdfを利用させていただき、16ピクセルフォントを作成しました。

作成物

https://github.com/tanakamasayuki/efontUTF16spiffs

上記にとりあえず置いてみました。

フォントサイズ

1.4Mぐらいになりました。SPIFFSなので/efont/が収録しているUTF16のBMP文字はすべて取り込んであります。

実行結果

PROGMEM版と同じなのですが、最初1文字単位だと遅かったので、複数の文字を転送してから描画しています。SPIFFSだとやっぱりファイルオープンに時間がかかりますね。

サンプルコード

#include <M5StickC.h>
//#define EFONT_BUF_SIZE 64
#include "efontUTF16spiffsM5StickC.h"

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

  printEfont("新しい朝が来た希望の朝がabcd12345()+-12345", 0, 16*0);
}

void loop() {
}

PROGMEM版と比べると、フォント指定がなくなって、バッファサイズの指定が増えています。4とかに変更して実行してもらえればわかりますが、結構遅いので最大文字数より大きくしたほうがいいです。

まとめ

描画自体も遅いのですが、やっぱりSPIFFSのファイルロード時間も結構気になってしまいます。

必要な文字単位でPROGMEMに保存する方が、データ検索に時間がかかりますが使いやすいかもしれないですね。ちょっとそっちも検討してみます。

[実験] Arduino(M5StickC)でefont Unicodeフォント表示 PROGMEM版

/efont/さんのUnicode Fontを利用して、Arduino用のフォントライブラリを作ってみました。

注意

こちらは実験ですので、実際に使う場合には以下のページを参考にしてください。

利用フォント

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

上記のf16.bdfとb16.bdfを利用させていただき、16ピクセルフォントを作成しました。

作成物

https://github.com/tanakamasayuki/efontUTF16progmem

上記にとりあえず置いてみました。

フォントサイズ

対象フォントファイル名フォント容量
UNICODE(BMP)全部1,441,792
フォント無しefontUTF16DisableAll.h0
漢字以外efontUTF16DisableCJK.h507,904
ハングル以外efontUTF16DisableHang.h1,081,344
漢字のみefontUTF16OnlyCJK.h1,007,616
常用漢字のみefontUTF16OnlyMiniJapanese.h753,664

全部入れたところ、1.4Mぐらいになりましたので、少し減らすオプションを作りました。指定ファイルを事前に読み込むことで、対象フォントを絞り込むことができます。

256文字単位で管理しているので、ざっくりとしか間引けませんので常用漢字など飛び飛びでいろんな場所に分散しているものは、1文字単位で読み込んだ方が良さそうです。

実行結果

サンプルコード

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

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

  printEfont("Hello", 0, 16*0);
  printEfont("こんにちは", 0, 16*1);
  printEfont("你好", 0, 16*2);
  printEfont("안녕하세요", 0, 16*3);
  printEfont("Доброе утро", 0, 16*4);
  printEfont("Päivää", 0, 16*6);
  printEfont("Здравствуйте", 0, 16*7);
}

void loop() {
}

上記でUNICODE(BMP)全部が読み込まれます。描画部分は端末依存なのでM5StickC以外の端末で使う場合にはefontUTF16progmemM5StickC.h相当の処理を自作してください。

#include <M5StickC.h>
#include "efontUTF16progmemOnlyMiniJapanese.h"
#include "efontUTF16progmemM5StickC.h"

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

  printEfont("Hello", 0, 16*0);
  printEfont("こんにちは", 0, 16*1);
}

void loop() {
}

常用漢字しか使わないのであれば、上記のようにすればプログラムサイズを減らすことができます。

容量的にはSPIFFSで1.4Mのフォントデータを転送して使うのが楽そうな気がしますのでSPIFFSを使ったバージョンも作ってみたいと思います。

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から読み込むバージョンも作ってみたいと思います。