M5StickCの内部INT割込通知を利用する その2 MPU6886編

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

概要

M5StickCの内蔵IMUであるMPU6886の加速度センサーを利用して、ディープスリープ中に加速度を感じると復帰できるようにしました。

INT割込通知に関してはその1を参考にしてください。

スケッチ

#include <M5StickC.h>

void setImuInt(uint8_t threshold) {
  uint8_t buf;

  // MPU6886_PWR_MGMT_2
  Wire1.beginTransmission(MPU6886_ADDRESS);
  Wire1.write(MPU6886_PWR_MGMT_2);
  Wire1.endTransmission();
  Wire1.requestFrom(MPU6886_ADDRESS, 1);
  buf = Wire1.read();
  delay(10);
  Wire1.beginTransmission(MPU6886_ADDRESS);
  Wire1.write(MPU6886_PWR_MGMT_2);
  buf = buf | 0x07;
  Wire1.write(buf);
  Wire1.endTransmission();
  delay(10);

  // MPU6886_ACCEL_CONFIG2
  Wire1.beginTransmission(MPU6886_ADDRESS);
  Wire1.write(MPU6886_ACCEL_CONFIG2);
  Wire1.endTransmission();
  Wire1.requestFrom(MPU6886_ADDRESS, 1);
  buf = Wire1.read();
  delay(10);
  Wire1.beginTransmission(MPU6886_ADDRESS);
  Wire1.write(MPU6886_ACCEL_CONFIG2);
  buf = buf | 0x01;
  Wire1.write(buf);
  Wire1.endTransmission();
  delay(10);

  // MPU6886_INT_ENABLE
  Wire1.beginTransmission(MPU6886_ADDRESS);
  Wire1.write(MPU6886_INT_ENABLE);
  buf = 0x40; // Enable WOM_INT_EN
  Wire1.write(buf);
  Wire1.endTransmission();
  delay(10);

  // ACCEL_WOM_THR
  Wire1.beginTransmission(MPU6886_ADDRESS);
  Wire1.write(0x20); // ACCEL_WOM_THR
  buf = threshold;
  Wire1.write(buf);  // X-AXIS ACCELEROMETER
  Wire1.write(buf);  // Y-AXIS ACCELEROMETER
  Wire1.write(buf);  // Z-AXIS ACCELEROMETER
  Wire1.endTransmission();
  delay(10);

  // MPU6886_ACCEL_INTEL_CTRL
  Wire1.beginTransmission(MPU6886_ADDRESS);
  Wire1.write(MPU6886_ACCEL_INTEL_CTRL);
  Wire1.endTransmission();
  Wire1.requestFrom(MPU6886_ADDRESS, 1);
  buf = Wire1.read();
  delay(10);
  Wire1.beginTransmission(MPU6886_ADDRESS);
  Wire1.write(MPU6886_ACCEL_INTEL_CTRL);
  buf = buf | 0xC0; // ACCEL_INTEL_MODE
  Wire1.write(buf);
  Wire1.endTransmission();
  delay(10);

  // MPU6886_PWR_MGMT_1
  Wire1.beginTransmission(MPU6886_ADDRESS);
  Wire1.write(MPU6886_PWR_MGMT_1);
  Wire1.endTransmission();
  Wire1.requestFrom(MPU6886_ADDRESS, 1);
  buf = Wire1.read();
  delay(10);
  Wire1.beginTransmission(MPU6886_ADDRESS);
  Wire1.write(MPU6886_PWR_MGMT_1);
  buf = buf | 0x20;
  Wire1.write(buf);
  Wire1.endTransmission();
  delay(10);

  // INT_STATUS Clear 0x3a
  Wire1.beginTransmission(MPU6886_ADDRESS);
  Wire1.write(0x3a);
  Wire1.endTransmission();
  Wire1.requestFrom(MPU6886_ADDRESS, 1);
  buf = Wire1.read();
  delay(10);
}

void setup() {
  M5.begin();

  // RTC Init
  Wire1.beginTransmission(0x51);
  Wire1.write(0x00);
  Wire1.write(0x00);
  Wire1.write(0x00);
  Wire1.endTransmission();

  // GPIO35 pull-up
  M5.IMU.Init();

  // 衝撃が加わるとHIGHに通知(0x15-0x80ぐらいがおすすめ)
  setImuInt(0x20);

  // GPIO35(M5StickCのINT)がHIGHになったら起動
  pinMode(GPIO_NUM_35, INPUT);
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_35, HIGH);

  // ディープスリープ
  esp_deep_sleep_start();
}

void loop() {
}

IMUの設定なので、思ったより投げるコマンドが多くなりました。

基本的にはデータシートのWAKE-ON-MOTION INTERRUPTの項目を参考にしてください。

いろいろなパラメータをセットして、通知を有効にしてから加速度のしきい値をセットして、通知フラグをクリアすることで、その後の加速度で通知がされます。

今回はディープスリープの例だけですが、RTCと同じくattachInterrupt()を使うことで、動作させながら割込を発生させることができます。

まとめ

かなり苦戦していましたので、説明が雑になっています。わからないことがあったらTwitterかコメント欄で質問ください。

データシートが若干他のシリーズを元にしたままで、MPU6886向けでない記述があります。結局似ているシリーズのデータシートも参考にしながらなんとか動かすことができました。

また、MPU6886に連続して通信をする場合には、delay()をいれないとESP32がパニックになってリセットがかかりますので注意してください。

M5StickCの通知にはRTCとMPU6886の他に電源管理のAXP192が接続されていますが、AXP192でも試しているのですが割込は発生しているようなのですが、GPIO35の通信レベルが変わりません。

また、AXP192からの通知もあまり有効的に使えるものはないので、通知シリーズもここまでかもしれません。

ESP32のULPアセンブリ言語入門 その3

概要

前回は環境構築まで行いました。今回は命令の説明と、プログラムを実際に動かしてみたいと思います。

ULPプログラミングの方式

ULPのプログラムには2つの方式があります。今回はCマクロ方式で行いますが、アセンブリ方式も説明したいと思います。

アセンブリ方式

環境構築が難しいので今回採用していません。アセンブリ言語をアセンブラを使ってアセンブルします。一般的なアセンブリ言語の開発方法ですがESP32の場合にはちょっと使うのがむずかしいです。

ESP32 ULP Debuggerでデバッグ出力をした場合に表示されるのは、逆アセンブラしたアセンブリ言語になります。内部ではこの状態で動いています。

Cマクロ方式

Arduino IDEではC言語のコンパイルはできますが、ULPアセンブリはそのままでは利用できません。そのためC言語のマクロ形式でプログラミングすることができる環境が準備されています。

ただし、すべての命令がCマクロ方式で準備されているわけではないので、現状は利用できない命令があります。

ULPコプロセッサの構造

ULPでは変数となる汎用レジスタがR0、R1、R2、R3の4つあります。基本的に命令は汎用レジスタにしたいして実行するので、この4つの汎用レジスタと上手に使う必要があります。

計算などをする場合にはスローメモリなどの変数から汎用レジスタに代入し、計算後にスローメモリに計算結果を戻すなどの処理を行う必要があります。

また、汎用レジスタ以外にもループなどに利用できる8ビットカウンターレジスタもありますが、Cマクロ方式だと利用することができません。

命令紹介

今回使う命令をかんたんに紹介します。

ノーオペレーション – NOP

何もしない命令です。ほぼすべてのプロセッサで0を指定した場合には、なにもしない命令になっているはずです。

一番代表的な命令ですが、Cマクロだと定義されていませんので利用することができません。ESP32 ULP Debugger側で今度マクロを追加してみたいと思います。

代入 – MOVE

汎用レジスタに代入をする命令です。Cマクロの場合には代入する値によって2つに分かれています。

I_MOVI(reg_dest, imm_)

最後がIのものは引数がimm_になっており、符号付き16ビッチの整数(int16_t)を汎用レジスタに代入するCマクロです。

I_MOVI(R0, 0),  // R0 = 0

上記の命令で、R0レジスタに0を代入しています。

I_MOVR(reg_dest, reg_src)

最後がRのものは引数が両方汎用レジスタで、汎用レジスタ同士の代入のCマクロです。

I_MOVI(R0, R1),  // R0 = R1

上記の命令で、R0レジスタにR1レジスタの内容を代入しています。

メモリロード – LD

汎用レジスタにスローメモリの内容を代入する命令です。書式は1種類のみです。

I_LD(reg_dest, reg_addr, offset_)

dest汎用レジスタにsrc汎用レジスタのアドレス+offset_の内容を代入します。

I_LD(R0, R3, 0), // R0 = RTC_SLOW_MEM[R3+0]
I_LD(R1, R3, 1), // R1 = RTC_SLOW_MEM[R3+1]
I_LD(R2, R3, 2), // R2 = RTC_SLOW_MEM[R3+2]

上記にように利用します。あらかじめ汎用レジスタにスローメモリの配列番号を入れておき、その値をdestの汎用レジスタに代入します。

オフセットも引数に持っているので、連続したデータなどを代入する場合にR3などのアドレスを保存した汎用レジスタを操作することなく、指定することができます。

オフセットはわかりにくいので、なれていないうちはオフセットは0固定で、I_MOVI()で取得元アドレスを設定してから、I_LD()を呼び出したほうが安全だと思います。

メモリストア – ST

汎用レジスタからスローメモリに内容を代入する命令です。書式は1種類のみです。

I_ST(reg_val, reg_addr, offset_)

LDとほぼ同じ書式なのですが、STの場合には左側から右側に代入をしますので注意してください。

I_ST(R0, R3, 0),  // RTC_SLOW_MEM[R3+0] = R0
I_ST(R1, R3, 0),  // RTC_SLOW_MEM[R3+1] = R1

上記のように最初のレジスタの内容を、真ん中のレジスタが指定するアドレスに保存します。データの方向性がここだけ違うので注意してください。

足し算 – ADD

汎用レジスタの足し算をする命令です。こちらも2種類に分かれています。

I_ADDI(reg_dest, reg_src, imm_)

Iがついているので数値指定の命令です。

I_ADDI(R1, R0, 1)  // R1 = R0 + 1
I_ADDI(R2, R2, -1)  // R2 = R2 + (-1)

上記のように汎用レジスタに数値を足しています。符号付きですのでマイナスを指定することもできます。

I_ADDR(reg_dest, reg_src1, reg_src2)

汎用レジスタ同士の足し算です。

I_ADDR(R0, R1, R2)  // R0 = R1 + R2

こちらはシンプルです。

引き算 – SUB

ほぼ足し算と同じ構造です。

I_SUBI(reg_dest, reg_src, imm_)

Iがついているので数値指定の命令です。

I_SUBI(R0, R0, 1)  // R0 = R0 - 1

I_SUBR(reg_dest, reg_src1, reg_src2)

汎用レジスタ同士の引き算です。

I_SUBR(R0, R0, R1)  // R0 = R0 - R1

停止 – HALT

ULPを停止する命令です。プログラムの最後には必ず追加してください。

I_HALT()

Cマクロも引数はありません。

命令まとめ

命令アセンブリCマクロ処理
ノーオペレーションNOP
代入MOVEI_MOVI(reg_dest, imm_)
I_MOVR(reg_dest, reg_src)
reg_dest = imm_
reg_dest = reg_src
メモリロードLDI_LD(reg_dest, reg_addr, offset_)reg_dest = MEM[reg_addr + offset_]
メモリストアSTI_ST(reg_val, reg_addr, offset_)MEM[reg_addr + offset_] = reg_val
足し算ADDI_ADDI(reg_dest, reg_src, imm_)
I_ADDR(reg_dest, reg_src1, reg_src2)
reg_dest = reg_src + imm_
reg_dest = reg_src1 + reg_src2
引き算SUBI_SUBI(reg_dest, reg_src, imm_)
I_SUBR(reg_dest, reg_src1, reg_src2)
reg_dest = reg_src – imm_
reg_dest = reg_src1 – reg_src2
停止HALTI_HALT()

サンプルスケッチ

#include "esp32/ulp.h"
#include "driver/rtc_io.h"
#include "UlpDebug.h"

// Slow memory variable assignment
enum {
  SLOW_COUNT,           // Counter
  SLOW_COUNT2,          // Counter2

  SLOW_PROG_ADDR        // Program start address
};

void ULP_BLINK(uint32_t us) {
  // Set ULP activation interval
  ulp_set_wakeup_period(0, us);

  // Slow memory initialization
  memset(RTC_SLOW_MEM, 0, 8192);

  // Init
  RTC_SLOW_MEM[SLOW_COUNT] = 0;
  RTC_SLOW_MEM[SLOW_COUNT2] = 0;

  // ULP Program
  const ulp_insn_t  ulp_prog[] = {
    I_MOVI(R3, SLOW_COUNT),                 // R3 = SLOW_COUNT
    I_LD(R0, R3, 0),                        // R0 = RTC_SLOW_MEM[R3(SLOW_COUNT)]
    I_LD(R1, R3, 1),                        // R1 = RTC_SLOW_MEM[R3(SLOW_COUNT)+1]
    I_ADDI(R0, R0, 1),                      // R0 = R0 + 1
    I_SUBI(R1, R1, 1),                      // R1 = R1 - 1
    I_ST(R0, R3, 0),                        // RTC_SLOW_MEM[R3(SLOW_COUNT)] = R0
    I_ST(R1, R3, 1),                        // RTC_SLOW_MEM[R3(SLOW_COUNT)+1] = R1
        
    I_HALT()                                // Stop the program
  };

  // Run the program shifted backward by the number of variables
  size_t size = sizeof(ulp_prog) / sizeof(ulp_insn_t);
  ulp_process_macros_and_load(SLOW_PROG_ADDR, ulp_prog, &size);
  ulp_run(SLOW_PROG_ADDR);
}

void setup() {
  // For debug output
  Serial.begin(115200);
  delay(50);

  // Execute ULP program at 1000ms intervals
  ULP_BLINK(1000000);
}

void loop() {
  // For debug output
  ulpDump();

  // Wait
  delay(1000);
}

ここまで紹介した命令を組み合わせたスケッチになります。

抜粋

enum {
  SLOW_COUNT,           // Counter
  SLOW_COUNT2,          // Counter2

  SLOW_PROG_ADDR        // Program start address
};

変数定義です。2つのカウンターを準備しています。

  // Init
  RTC_SLOW_MEM[SLOW_COUNT] = 0;
  RTC_SLOW_MEM[SLOW_COUNT2] = 0;

初期値は両方とも0です。

  const ulp_insn_t  ulp_prog[] = {
    I_MOVI(R3, SLOW_COUNT),                 // R3 = SLOW_COUNT
    I_LD(R0, R3, 0),                        // R0 = RTC_SLOW_MEM[R3(SLOW_COUNT)]
    I_LD(R1, R3, 1),                        // R1 = RTC_SLOW_MEM[R3(SLOW_COUNT)+1]
    I_ADDI(R0, R0, 1),                      // R0 = R0 + 1
    I_SUBI(R1, R1, 1),                      // R1 = R1 - 1
    I_ST(R0, R3, 0),                        // RTC_SLOW_MEM[R3(SLOW_COUNT)] = R0
    I_ST(R1, R3, 1),                        // RTC_SLOW_MEM[R3(SLOW_COUNT)+1] = R1
        
    I_HALT()                                // Stop the program
  };

R3にカウンターのアドレスを代入して、R0にカウンターをロード、R1にはオフセット1しているのでカウンター2をロードしています。

その後R0は1を足して、R1は1を引いています。

その後スローメモリにカウンターとカウンター2をストアして保存しています。

実行結果

====================================
0000 : 00E30001 DATA     1				// ST ADDR:0x0007
0001 : 0103FFFF DATA    -1				// ST ADDR:0x0008
0002 : 72800003 PROG MOVE   R3,   0			// R3 = 0
0003 : D000000C PROG LD     R0, R3,   0			// R0 = MEM[R3+0]
0004 : D000040D PROG LD     R1, R3,   1			// R1 = MEM[R3+1]
0005 : 72000010 PROG ADD    R0, R0,   1			// R0 = R0 + 1
0006 : 72200015 PROG SUB    R1, R1,   1			// R1 = R1 - 1
0007 : 6800000C PROG ST     R0, R3,   0			// MEM[R3+0] = R0
0008 : 6800040D PROG ST     R1, R3,   1			// MEM[R3+1] = R1
0009 : B0000000 PROG HALT				// HALT
====================================
0000 : 00E30002 DATA     2				// ST ADDR:0x0007
0001 : 0103FFFE DATA    -2				// ST ADDR:0x0008
0002 : 72800003 PROG MOVE   R3,   0			// R3 = 0
0003 : D000000C PROG LD     R0, R3,   0			// R0 = MEM[R3+0]
0004 : D000040D PROG LD     R1, R3,   1			// R1 = MEM[R3+1]
0005 : 72000010 PROG ADD    R0, R0,   1			// R0 = R0 + 1
0006 : 72200015 PROG SUB    R1, R1,   1			// R1 = R1 - 1
0007 : 6800000C PROG ST     R0, R3,   0			// MEM[R3+0] = R0
0008 : 6800040D PROG ST     R1, R3,   1			// MEM[R3+1] = R1
0009 : B0000000 PROG HALT				// HALT

シリアルモニタを開くと、上記のような出力がされます。1秒間隔で一番上のDATAの2列の数字が変わっていくのがわかると思います。

一番左の0000からはじまるのがスローメモリの配列番号です。16進数で0から始まって07ff(10進で2047)まであります。デバッグ出力の場合には、NOPが複数続いていたらそれ以降のデータは出力しないようにしています。

その次の00E30001などの8桁が、スローメモリに保存されているデータになります。16進数で8桁ですので32ビットになります。この数値を解析したのが、これより右側にかかれているものになります。

数値自体は理解する必要はありませんので、DATAかPROGより右側の情報だけ見てください。DATAの場合には入っている数値になります。ここの中身は符号ありの16ビット(int16_t)ですので-32768から32767の数値になります。

DATAの場合一番右側にADDRと書かれています。これはこのデータを保存した命令が実行されたアドレスになります。0x0007と0x0008がありますが、そのアドレスにあるSTからスローメモリに保存されたことがわかります。

複数の場所からスローメモリを変更する場合に、どこから書き込まれた数値なのかを判別することができます。

PROGの場合にはアセンブリ言語で表示されています。//以下はC言語風の簡易的なコメントになります。ESP32 ULP Debuggerは汎用的なツールとして作ったので、C言語マクロ方式ではなく、アセンブリ方式での出力になってしまっています。

まとめ

やっとプログラミングまでたどり着きました。自作ライブラリのESP32 ULP Debuggerはまだおかしなところがあるので、手を入れていく予定です。このブログを書きながらマイナスの数値がおかしかったので修正を入れています。。。

次回はLチカを行う予定です。

M5StickCにレゴテクニック互換ブロックをためす

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

概要

M5Stack社の製品はレゴ社のテクニックシリーズのブロックで固定することができる、穴があります。

テクニック互換ブロックを購入したみたので、どんな使い方ができそうかを確認してみました。

レゴテクニックとは?

LEGO® Technic™とは、レゴ社のモーターとかギアとかを組み合わせて作るシリーズで、結構高いです。。。

参考資料

上記、宮田さんのスライドで紹介されています。

昔からM5Stackを使っている人で、M5GOとか持っていると有名だったのでしょうが、M5StickCしか使ったことがなかったので、あの穴の正体知りませんでした。

購入品

上記の商品を購入しました。互換ブロックの詰め合わせ250グラムで1500円前後です。

250グラムとなっていますが、ランダム詰め合わせではなく、基本書いてある物が入っていました。

到着物

四角いダンボールで到着しました。M5StickCは大きさ確認用です。10ドル超えているので、受け取りにははんこが必要で、到着までの追跡が可能です。

中身はドサッと袋に入っています。

広げたところです。基本最低同じものが2つはあるのかな?

右上に大量にある黒いパーツが意味不明でしたが、よくみたら判明しました。

無限軌道とかクローラーとかキャタピラと呼ばれる部品ですね。黒い大きめのギアがクローラーのギアみたいですが、片側1個しかないのであまり使い所が、、、

ダイソーの150円シリーズのケースに入れてみました。ぴったりです!

使い方

手元にあったのはM5StickCのホルダーと、プロトユニットです。

上記でホルダーをはじめて使ってみましたが、この穴がレゴ用だったとは、、、

でもこのホルダーを使っている作例を、他で見たことがないです、、、

固定してみました。M5StickCは横5穴で、ユニットは横3穴ですね。四角いブロックは便利そうです。

まとめ

ギアとか、関節とかがあるので非常にいろいろ遊べそうです。ただしテクニックのブロックがよくわからないので、どう使えばいいのかは研究が必要です。

なにかテクニックの商品を買って、組み立ててみると自然に覚えるのでしょうがちょっと高くて手が出ません、、、

モーター使わないやつだと安いのもあるんですね。タイヤ付きでこの値段ぐらいだったら購入してもいいかな、、、

ただ、そもそも普通のレゴを持っていないので、どうしよう。。。

M5StickCの内部INT割込通知を利用する その1 RTC編

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

概要

M5StickCにはGPIO35にINT割込の結線がされています。しかしながらプルアップされていなかったので、使えないと思っていましたが使う方法がありました。

今回はRTCの時間を使った割込方法を説明したいと思います。

INTとは?

デバイスによって、イベントが発生した場合に通知する機能があります。今回はリアルタイムクロック(RTC)ですので、指定した時間になったことを通知するアラート機能と、インターバルタイマーがあります。

インターバルタイマーはTickerクラスなどを使ってもそれほどできることが変わらないので、指定した時間になったら通知してくれるアラート機能を使ってみました。

事前準備

多くのデバイスが通知を行う場合には、INTピンをLOWに落とす操作で通知を行います。そのため、回路上はプルアップされておりマイコンなどの通知を受信するデバイスはHIGHからLOWに信号レベルが変わった瞬間をトリガーにして、通知を受け取ります。

M5StickCはGPIO35がINT回路と接続しているのですが、デフォルトでプルアップされていません。GPIO35なのでESP32側でプルアップもできないので諦めていたのですが、接続されているデバイスをすべて調べたところ、IMUのMPU6886はアクティブハイという設定があり、プルアップすることができました。(SH200Qは同じ設定がありましたが、GPIO35はHIGHになりませんでした。途中で配線が変わった可能性があります)

M5.IMU.Init()を呼び出すとGPIO35の信号がプルアップされますので、INT割込通知を利用する場合にはIMUを利用しなくても初期化する必要があります。

上記の資料が参考になります。

ESP32の通知受信方法

複数あるのですが、GPIO35の信号レベルが変わったら割込を発生させる設定がありますので、今回はそれを使ってみました。

Arduinoの割込は結構難しくて、割込関数の中身で実行できる関数にはかなり制限があります。I2Cアクセスなどはできませんので注意してください。

おそらくは、別スレッドで動いている処理に、通知やキュー、もしくはセマフォを使って渡してあげる必要があります、、、

1秒程度の遅延が許されるのであれば、GPIO35がLOWになっているか定期的に確認して、LOWの場合に処理するようにすればかんたんにアラートが使えると思います。

RTCアラート設定スケッチ例

#include <M5StickC.h>

RTC_TimeTypeDef RTC_TimeStruct;
RTC_DateTypeDef RTC_DateStruct;

void intFunc() {
  Serial.println("Int");
}

void setAlarm(int minute, int hour = -1, int day = -1, int dayOfTheWeek = -1 ) {
  if ( minute == -1 ) {
    minute = 0x80;
  } else {
    // bin to bcd
    minute = minute % 60;
    minute = minute + 6 * (minute / 10);
  }
  if ( hour == -1 ) {
    hour = 0x80;
  } else {
    // bin to bcd
    hour = hour % 24;
    hour = hour + 6 * (hour / 10);
  }
  if ( day == -1 ) {
    day = 0x80;
  } else {
    // bin to bcd
    day = day + 6 * (day / 10);
  }
  if ( dayOfTheWeek == -1 ) {
    dayOfTheWeek = 0x80;
  } else {
    // bin to bcd
    dayOfTheWeek = dayOfTheWeek + 6 * (dayOfTheWeek / 10);
  }

  // Init RTC
  Wire1.beginTransmission(0x51);
  Wire1.write(0x00);
  Wire1.write(0x00);
  Wire1.write(0x00);
  Wire1.endTransmission();

  // Set Alarm
  Wire1.beginTransmission(0x51);
  Wire1.write(0x09);
  Wire1.write((uint8_t)minute);
  Wire1.write((uint8_t)hour);
  Wire1.write((uint8_t)day);
  Wire1.write((uint8_t)dayOfTheWeek);
  Wire1.endTransmission();

  // Enable Alarm
  Wire1.beginTransmission(0x51);
  Wire1.write(0x01);
  Wire1.write(0x02);
  Wire1.endTransmission();
}

void setup() {
  M5.begin();

  // GPIO35 pull-up
  M5.IMU.Init();

  pinMode(35, INPUT);
  attachInterrupt(35, intFunc, FALLING);

  M5.Rtc.GetTime(&RTC_TimeStruct);
  setAlarm(RTC_TimeStruct.Minutes + 1);
}

void loop() {
  M5.Rtc.GetTime(&RTC_TimeStruct);
  M5.Rtc.GetData(&RTC_DateStruct);
  Serial.printf("%04d-%02d-%02d ", RTC_DateStruct.Year, RTC_DateStruct.Month, RTC_DateStruct.Date);
  Serial.printf("%02d : %02d : %02d ", RTC_TimeStruct.Hours, RTC_TimeStruct.Minutes, RTC_TimeStruct.Seconds);
  Serial.println(digitalRead(35));

  // Next Alert
  if (digitalRead(35) == LOW) {
    setAlarm(RTC_TimeStruct.Minutes + 1);
  }

  delay(1000);
}

事前にRTCの時間をセットしておいてから、実行してみてください。RTCは一度セットすれば他のスケッチを転送しても設定されたままで、電源を切っても保持されます。

上記などで一度RTCに時刻を設定しておけば、それほど時間はずれないと思います。

setAlarm()の関数がアラートを設定する関数になります。毎時0分にアラートをセットする場合にはsetAlarm(0)。14時30分に設定するにはsetAlarm(30, 14)で設定します。

中身は理解しなくても大丈夫です。気になる人はRTCのデータシートを見てください。

肝としては、M5.IMU.Init()をかならず呼ぶこと。RTCは電源を切っても内部のバッテリーで動いているので、前回の設定値をクリアすることになります。

  // Init RTC
  Wire1.beginTransmission(0x51);
  Wire1.write(0x00);
  Wire1.write(0x00);
  Wire1.write(0x00);
  Wire1.endTransmission();

上記が初期化ですが、このコードを呼び出さないと、前回のアラート設定が残っているのでGPIO35をLOWに設定したままになったりします。

あとで、M5StickCのライブラリにプルリクエストを出して、初期化関数を追加したいと思います。

  // Next Alert
  if (digitalRead(35) == LOW) {
    setAlarm(RTC_TimeStruct.Minutes + 1);
  }

attachInterrupt()での割込は表示以外には利用していないので、上記が実質のアラート割込処理になります。現在時刻の1分後の0秒に再度アラートを仕掛けています。この設定で毎分0秒に通知が発生します。

設定する値は0-59ですが、setAlarm()の中で60のあまりを求めているので65などを設定しても、5になっているはずです。

RTCでのディープスリープ復帰スケッチ

#include <M5StickC.h>

RTC_DATA_ATTR int bootCount = 0; // RTCスローメモリに変数を確保

RTC_TimeTypeDef RTC_TimeStruct;
RTC_DateTypeDef RTC_DateStruct;

void setAlarm(int minute, int hour = -1, int day = -1, int dayOfTheWeek = -1 ) {
  if ( minute == -1 ) {
    minute = 0x80;
  } else {
    // bin to bcd
    minute = minute % 60;
    minute = minute + 6 * (minute / 10);
  }
  if ( hour == -1 ) {
    hour = 0x80;
  } else {
    // bin to bcd
    hour = hour % 24;
    hour = hour + 6 * (hour / 10);
  }
  if ( day == -1 ) {
    day = 0x80;
  } else {
    // bin to bcd
    day = day + 6 * (day / 10);
  }
  if ( dayOfTheWeek == -1 ) {
    dayOfTheWeek = 0x80;
  } else {
    // bin to bcd
    dayOfTheWeek = dayOfTheWeek + 6 * (dayOfTheWeek / 10);
  }

  // Init RTC
  Wire1.beginTransmission(0x51);
  Wire1.write(0x00);
  Wire1.write(0x00);
  Wire1.write(0x00);
  Wire1.endTransmission();

  // Set Alarm
  Wire1.beginTransmission(0x51);
  Wire1.write(0x09);
  Wire1.write((uint8_t)minute);
  Wire1.write((uint8_t)hour);
  Wire1.write((uint8_t)day);
  Wire1.write((uint8_t)dayOfTheWeek);
  Wire1.endTransmission();

  // Enable Alarm
  Wire1.beginTransmission(0x51);
  Wire1.write(0x01);
  Wire1.write(0x02);
  Wire1.endTransmission();
}

void setup(){
  M5.begin();

  // 起動回数カウントアップ
  bootCount++;
  Serial.printf("起動回数: %d ", bootCount);

  // 起動方法取得
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause();
  switch(wakeup_reason){
    case ESP_SLEEP_WAKEUP_EXT0      : Serial.printf("外部割り込み(RTC_IO)で起動\n"); break;
    case ESP_SLEEP_WAKEUP_EXT1      : Serial.printf("外部割り込み(RTC_CNTL)で起動 IO=%llX\n", esp_sleep_get_ext1_wakeup_status()); break;
    case ESP_SLEEP_WAKEUP_TIMER     : Serial.printf("タイマー割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_TOUCHPAD  : Serial.printf("タッチ割り込みで起動 PAD=%d\n", esp_sleep_get_touchpad_wakeup_status()); break;
    case ESP_SLEEP_WAKEUP_ULP       : Serial.printf("ULPプログラムで起動\n"); break;
    case ESP_SLEEP_WAKEUP_GPIO      : Serial.printf("ライトスリープからGPIO割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_UART      : Serial.printf("ライトスリープからUART割り込みで起動\n"); break;
    default                         : Serial.printf("スリープ以外からの起動\n"); break;
  }

  // GPIO35(M5StickCのINT)がLOWになったら起動
  pinMode(GPIO_NUM_35, INPUT);
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_35, LOW);

  // タイマーセット
  M5.Rtc.GetTime(&RTC_TimeStruct);
  setAlarm(RTC_TimeStruct.Minutes + 1);

  // ディープスリープ
  esp_deep_sleep_start();
}

void loop(){
}

ディープスリープの例です。こちらのほうが割込はシンプルになっています。Arduino IDEのシリアルモニタにタイムスタンプを表示するオプションをつけて実行していると、毎分0秒にディープスリープから復帰する様子が確認できます。

まとめ

実は結構はまっていましたが、できてしまえばシンプルです。ただRTCは前回の設定を持ったままなので、このスケッチを実行したあとには前回のアラート設定が残ったままです。

INT通知をする場合にはIMUとRTCの初期化を必ずしてから使う必要がありそうです。あとはAXP192とMPU6886がINT割込通知がありますので、今後触ってみたいと思います。

続編

ESP32のULPアセンブリ言語入門 その2

概要

前回はn進数と単位で終わってしまいました。今回はULPの概要についてまとめてみました。

ULPアセンブリの特徴

ULPはESP32のメインプロセッサではなく、省電力でちょっとしたことを動かすコプロセッサーです。最近の携帯電話なども歩数カウントなどの管理を行うコプロセッサーと、メインプロセッサーを分離することで省電力化しています。

ULPのコプロセッサーは、ESP32のメインプロセッサーとは独立して動いています。プロセッサー間の通信機能などはないのですが、どちらからでもアクセスできるメモリがあり、そのメモリをかいして、データのやり取りなどを行います。

スローメモリと呼ばれるメモリがあり、このメモリはリセットやディープスリープをしても内容が保持されている特徴があります。電源を切ると内容が消えてしまうので、通常のメモリなのですが、ディープスリープ中などにも電源を供給し、内容を保持しています。

ESP32には8KByteのスローメモリを搭載しており、1KByteは1,024Byteなので、8,192Byteになります。

このスローメモリはデータの受け渡しの他に、ULPで動くプログラムをロードするメモリでもあります。Arduino上では、プログラムとデータを区別して管理する必要は通常ありませんが、スローメモリでは、プログラムなのかデータなのかを自分で管理する必要があります。

スローメモリは8,192Byteありますが、ULPは32bitのプロセッサーのため、32bit単位で一般には管理します。32bitは4Byteなので、8,192Byteの場合4Byteが2,048個あることになります。

ULPでは1つの命令が4Byte、1つのデータも4Byteになります。そのため命令とデータをあわせて2,048個まで利用することができます。

ディープスリープしても消えない変数として、2,048個のデータとして使うこともできますし、プログラムとデータを好きな割合で使えます。

アセンブリの命令長とは?

ULPは32bitのプロセッサなので、4Byteの命令で動いています。最近のシンプルな動きをするプロセッサは動いているbit数と命令長が同じものが多いです。

パソコンなどで利用しているCPUに関しては、8bit時代の命令に、16bitの命令を追加してと拡張してきているので、単純な命令は短く、複雑な命令は長くなっています。

このあたりはRISCとCISCなどで検索すると解説サイトが出てくると思います。

環境構築

さて、実際にULPプログラミングの環境を作ってみたいと思います。このサイトの方針として、検索すると、良質なサイトがたくさんあるような話題については、細かく書きません。

環境構築はWindows、Mac、Linuxで方法が違うのと、画面などが変わりやすいために検索する場合のキーワードだけかく方針です。環境構築は検索しやすい項目であり、いろいろ気になった環境を構築してみると、学びが大きいのでぜひ自分で調べながらチャレンジしてもらいたいと思います。

また、今回は一般的な開発環境であるArduino IDE上での開発を紹介したいと思います。ESP32の開発ボードは何を使っても基本的には問題ありません。ただしLチカなどを行う関係上、外部にピンがでていないような開発ボードは避けてください。

説明ではM5StickCを利用していきますが、ピン番号を変えれば他の開発ボードでも動くと思います。

Arduino IDE

上記から環境に応じたArduino IDEをダウンロードしてきてください。WEB EDITORがありますが、現状はESP32の開発に対応していないので、Downloadを利用してください。

ESP32ボードの追加

Arduino IDEは、標準ではArduinoボードの開発しか対応していません。ESP32の開発をする場合には、ボードの追加という設定が必要になります。

上記が英語ですが、Google翻訳などを使えば内容がわかると思います。ここで安定版のURLを設定から追加するのですが、過去変わったことがありますので、できれば設定方法などで検索したサイトの情報ではなく、上記の正式なURLが更新されていないかを確認してから設定を行ってください。

ESP32 ULP Debuggerのライブラリ追加

必須ではありませんが、ULPの勉強では便利なので追加してください。スローメモリの内容を解析して、シリアル出力してくれるライブラリです。

ライブラリマネージャからULPで検索することで、探すことができると思います。

ULPプログラミングの環境構築確認

スケッチ例からESP32 ULP Debuggerの中にある、UlpBlinkを開いてください。あとで内容の解説をするので、中身を理解する必要はありません。

ボードをESP32の接続している開発ボードに変更し、シリアルポートなどを設定してからこのスケッチを動かしてください。

スケッチが動かない場合には、環境構築が失敗していますので、もう一度環境を見直してください。

転送が終わったあとに、シリアルモニタを開いて、定期的に文字が流れているのかを確認してください。

====================================
0000 : 01430001 DATA     1				// ST ADDR:0x000A
0001 : 72800003 PROG MOVE   R3,   0			// R3 = 0
0002 : D000000C PROG LD     R0, R3,   0			// R0 = MEM[R3+0]
0003 : 820A0001 PROG JUMPR  +  5,   1, LT		// IF R0 < 1 THAN GOTO 0x0008
0004 : 1AD40500 PROG REG_WR 0x0000,  21,  21,   1	// RTC_IO[21:21] = 1
0005 : 72800000 PROG MOVE   R0,   0			// R0 = 0
0006 : 6800000C PROG ST     R0, R3,   0			// MEM[R3+0] = R0
0007 : 8000002C PROG JUMP   0x000B			// GOTO 0x000B
0008 : 1AD40100 PROG REG_WR 0x0000,  21,  21,   0	// RTC_IO[21:21] = 0
0009 : 72800010 PROG MOVE   R0,   1			// R0 = 1
000A : 6800000C PROG ST     R0, R3,   0			// MEM[R3+0] = R0
000B : 4000EA60 PROG WAIT   60000			// Delay 60000
000C : 4000EA60 PROG WAIT   60000			// Delay 60000
000D : 4000EA60 PROG WAIT   60000			// Delay 60000
000E : 4000EA60 PROG WAIT   60000			// Delay 60000
000F : 4000EA60 PROG WAIT   60000			// Delay 60000
0010 : 4000EA60 PROG WAIT   60000			// Delay 60000
0011 : 4000EA60 PROG WAIT   60000			// Delay 60000
0012 : 4000EA60 PROG WAIT   60000			// Delay 60000
0013 : 4000EA60 PROG WAIT   60000			// Delay 60000
0014 : 4000EA60 PROG WAIT   60000			// Delay 60000
0015 : 4000EA60 PROG WAIT   60000			// Delay 60000
0016 : 4000EA60 PROG WAIT   60000			// Delay 60000
0017 : 4000EA60 PROG WAIT   60000			// Delay 60000
0018 : 4000EA60 PROG WAIT   60000			// Delay 60000
0019 : B0000000 PROG HALT				// HALT

上記のような文字列が流れていれば、環境構築が成功しています。

ULP最小スケッチ例

環境構築が完了し、ULPプログラミングが可能になったところで、最低限のスケッチをベースに解説を行いたいと思います。

#include "esp32/ulp.h"
#include "UlpDebug.h"

void setup() {
  // シリアルの初期化
  Serial.begin(115200);

  // スローメモリの初期化
  memset(RTC_SLOW_MEM, 0, 8192);

  // ULPのプログラム
  const ulp_insn_t  ulp_prog[] = {
    I_HALT()  // 終了
  };

  // 300ミリ秒(300,000マイクロ秒)間隔でULPを実行
  ulp_set_wakeup_period(0, 300000);

  // プログラムのサイズを設定
  size_t size = sizeof(ulp_prog) / sizeof(ulp_insn_t);
  ulp_process_macros_and_load(0, ulp_prog, &size);

  // ULPを実行
  ulp_run(0);
}

void loop() {
  // デバッグ出力
  ulpDump();

  // 1秒待つ
  delay(1000);
}

上記が最小限のプログラムになります。

スケッチ解説

  // シリアルの初期化
  Serial.begin(115200);

ESP32 ULP Debuggerはシリアル出力で表示しますので、初期化をしています。

  // スローメモリの初期化
  memset(RTC_SLOW_MEM, 0, 8192);

起動直後はランダムな数字が入っていますので、まずはスローメモリをすべて初期化します。

  // ULPのプログラム
  const ulp_insn_t  ulp_prog[] = {
    I_HALT()  // 終了
  };

ULPのプログラム自体はあとで説明をしますが、I_HALT()はULPを終了する命令です。

  // 300ミリ秒(300,000マイクロ秒)間隔でULPを実行
  ulp_set_wakeup_period(0, 300000);

1つ目の引数で周期の方法を指定しますが、0のSENS_ULP_CP_SLEEP_CYC0_REG以外は通常利用しません。

2つ目の引数でマイクロ秒単位で、呼び出し間隔を設定します。例では300ミリ秒間隔ですがULPプログラムはあまり時間精度が高くないので、結構ずれた時間間隔で呼び出されます。

  // プログラムのサイズを設定
  size_t size = sizeof(ulp_prog) / sizeof(ulp_insn_t);
  ulp_process_macros_and_load(0, ulp_prog, &size);

プログラムの大きさを計算して、スローメモリの0番目(先頭)にプログラムをロードします。

  // ULPを実行
  ulp_run(0);

スローメモリの0番目(先頭)からプログラムを実行します。以後、ulp_set_wakeup_period()で設定した間隔ごとにULPプログラムが実行されます。

  // デバッグ出力
  ulpDump();

スローメモリの状況をシリアルに出力します。

  // 1秒待つ
  delay(1000);

あまり大量に出力しても、読みにくいので1秒間隔で状態を出力するようにウエイトを入れます。

実行例

====================================
0000 : B0000000 PROG HALT				// HALT

今回なにもしていないプログラムなので、HALTしか表示されていません。

実用的なスケッチ例

最小スケッチ例の場合には、ちょっと省略しすぎていてデータ管理などができていません。スケッチ例のESP32 ULP Debuggerの中にある、UlpBlinkを再度開いてもらい、差分を説明します。

#include "esp32/ulp.h"
#include "driver/rtc_io.h"
#include "UlpDebug.h"

// Slow memory variable assignment
enum {
  SLOW_BLINK_STATE,     // Blink status

  SLOW_PROG_ADDR        // Program start address
};

void ULP_BLINK(uint32_t us) {
  // Set ULP activation interval
  ulp_set_wakeup_period(0, us);

  // Slow memory initialization
  memset(RTC_SLOW_MEM, 0, 8192);

  // Blink status initialization
  RTC_SLOW_MEM[SLOW_BLINK_STATE] = 0;

  // PIN to blink (specify by +14)
  const int pin_blink_bit = RTCIO_GPIO26_CHANNEL + 14;
  const gpio_num_t pin_blink = GPIO_NUM_26;

  // GPIO26 initialization (set to output and initial value is 0)
  rtc_gpio_init(pin_blink);
  rtc_gpio_set_direction(pin_blink, RTC_GPIO_MODE_OUTPUT_ONLY);
  rtc_gpio_set_level(pin_blink, 0);

  // ULP Program
  const ulp_insn_t  ulp_prog[] = {
    I_MOVI(R3, SLOW_BLINK_STATE),           // R3 = SLOW_BLINK_STATE
    I_LD(R0, R3, 0),                        // R0 = RTC_SLOW_MEM[R3(SLOW_BLINK_STATE)]
    M_BL(1, 1),                             // IF R0 < 1 THAN GOTO M_LABEL(1)

    // R0 => 1 : run
    I_WR_REG(RTC_GPIO_OUT_REG, pin_blink_bit, pin_blink_bit, 1), // pin_blink_bit = 1
    I_MOVI(R0, 0),                          // R0 = 0
    I_ST(R0, R3, 0),                        // RTC_SLOW_MEM[R3(SLOW_BLINK_STATE)] = R0
    M_BX(2),                                // GOTO M_LABEL(2)

    // R0 < 1 : run
    M_LABEL(1),                             // M_LABEL(1)
    I_WR_REG(RTC_GPIO_OUT_REG, pin_blink_bit, pin_blink_bit, 0),// pin_blink_bit = 0
    I_MOVI(R0, 1),                          // R0 = 1
    I_ST(R0, R3, 0),                        // RTC_SLOW_MEM[R3(SLOW_BLINK_STATE)] = R0

    M_LABEL(2),                             // M_LABEL(2)
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_HALT()                                // Stop the program
  };

  // Run the program shifted backward by the number of variables
  size_t size = sizeof(ulp_prog) / sizeof(ulp_insn_t);
  ulp_process_macros_and_load(SLOW_PROG_ADDR, ulp_prog, &size);
  ulp_run(SLOW_PROG_ADDR);
}

void setup() {
  // For debug output
  Serial.begin(115200);

  // Execute ULP program at 300ms intervals
  ULP_BLINK(300000);
}

void loop() {
  // For debug output
  ulpDump();

  // Wait
  delay(1000);
}

データ項目の管理

// Slow memory variable assignment
enum {
  SLOW_BLINK_STATE,     // Blink status

  SLOW_PROG_ADDR        // Program start address
};

上記のenumでデータを管理しています。スローメモリの先頭からデータをenumでデータを割り当てていき、最後のSLOW_PROG_ADDRがプログラムが開始するアドレスになります。

上記の場合には1つだけ変数があることになります。

ULP初期化関数分離

void ULP_BLINK(uint32_t us) {
  // Set ULP activation interval
  ulp_set_wakeup_period(0, us);

  // Slow memory initialization
  memset(RTC_SLOW_MEM, 0, 8192);

  // Blink status initialization
  RTC_SLOW_MEM[SLOW_BLINK_STATE] = 0;

ULP関連の処理が増えていきますので、関数を作って分離しています。標準的な処理のほかに、引数で読み出し間隔を設定しています。

RTC_SLOW_MEM[SLOW_BLINK_STATE] = 0で、変数の初期化もしています。この初期化の前にmemset()ですべて0に設定しているので、必要ないのですが初期化もれを防ぐために、明示的に書いてあります。

ピンの初期化

  // PIN to blink (specify by +14)
  const int pin_blink_bit = RTCIO_GPIO26_CHANNEL + 14;
  const gpio_num_t pin_blink = GPIO_NUM_26;

  // GPIO26 initialization (set to output and initial value is 0)
  rtc_gpio_init(pin_blink);
  rtc_gpio_set_direction(pin_blink, RTC_GPIO_MODE_OUTPUT_ONLY);
  rtc_gpio_set_level(pin_blink, 0);

ここは今後に細かい解説をしますが、pinMode()に近い設定を行っております。

プログラムロードと実行

  // Run the program shifted backward by the number of variables
  size_t size = sizeof(ulp_prog) / sizeof(ulp_insn_t);
  ulp_process_macros_and_load(SLOW_PROG_ADDR, ulp_prog, &size);
  ulp_run(SLOW_PROG_ADDR);

プログラムのサイズを計算し、enumのSLOW_PROG_ADDRがあらわすプログラムの開始アドレスにプログラムをロードして、実行も行います。

データ部分に関してはすべてを0に初期化して、個別に代入しているのでここではロードしません。

ULPのプログラムに関しては、enumを利用する方法以外でも可能です。しかしながら一般的には利用したほうが管理が楽だと思われますので、この方式で解説をすすめていきたいと思います。

まとめ

また、ULPプログラミングをすることなく終わってしまいました。次回は実際にULPプログラミングをしながら命令を解説したいと思っています。

続編

M5StackとM5Stickの選び方(2020年2月)

概要

M5Stack社の製品が増えてきて、どれを使ったほうがいいのかがわかりにくくなってきたので、選び方をかんたんにまとめてみました。

M5Stackとは?

M5Stackを開発している会社がM5Stack社です。M5Stack社の代表的な製品がM5Stackです。そのため、M5Stackと書いてある場合には会社名なのか製品名なのかがわかりません。

M5Stack社の本体一覧

上記が日本の代理店であるスイッチサイエンスさんの一覧ページです。ここにない本体は基本的に日本では流通していません。

新製品などは海外から通販したほうが安くて早いのですが、日本国内の電波法などの関係で使えないものが混ざっています。基本的スイッチサイエンスさんで取り扱いのない商品は日本で使えないものと思ってください。

M5Stackシリーズ

M5Stackは5センチ角のケースに入っているマイコンのシリーズです。ただし大きさの基準は中身にはいっている基板サイズなので、ケースを含めた本体サイズは5.4センチ角になります。

最初にざっくりと解説をして、あとで選び方を書きたいと思います。

M5Stack Basic

一番標準的な製品です。少し大きめの画面と、SDカードとスピーカー、ボタンが3つあります。バッテリーは150mAhのものが内蔵されています。

M5Stack Gray

灰色になって、9軸の加速度センサーが搭載されました。フラッシュの容量も大きくなったので、大きなプログラムも書き込むことができます。

M5Stack Faces

Grayの本体にキーボードをセットにしたものです。また、キーボード側にバッテリーが増設されています。

M5Stack Fire

GrayにさらにマイクとGrove端子の追加、バッテリー容量を増やした本体に、PSRAMという外付けメモリが増設されているボトムが付属しています。

M5GO

Fireの本体に、各種センサーをセットにしたものです。ただし、FireにあったPSRAMが増設されているボトムは付属していません。

一覧

M5Stack BasicM5Stack GrayM5Stack FacesM5Stack FireM5GO
SoCESP32ESP32ESP32ESP32ESP32
画面320×240320×240320×240320×240320×240
フラッシュ4M16M16M16M16M
PSRAM4M
Grove I2C
Grove GPIO
Grove UART
スピーカー
マイク
IMU9軸9軸9軸9軸
ボタン33333
SDカード
バッテリー150150600600600
価格3,5754,2907,0406,3258,305

価格はスイッチサイエンスさんの価格です。

M5Stickシリーズ

縦型の本体で、バッテリーを内蔵しているシリーズです。

M5Stick

日本未発売で、日本で使うことができない製品です。新規開発している製品には組み込まれていましたが、日本で販売はなさそうです。

M5StickC

M5Stack Basicよりもいま一番人気なのがこのM5StickCだと思います。Cはコンパクトの略で、グレーのM5Stickよりも小さいサイズになっています。

日本で正式販売されており、Basicとの差はSDカードスロットがなくなり、スピーカーの変わりにマイクを搭載しています。

M5StickT

日本未発売で、日本で使うことができない製品です。赤外線サーモセンサーを搭載しており、暗闇での監視や、基板などで発熱している場所の特定などが可能です。

ただし、サーモセンサーだけで300ドル以上するので、本体が5万円前後になると思います。日本でも発売する可能性はありますが、一般用途では高いので使いにくいと思います。

M5StickV

カメラ付きのM5StickCに見えますが、中身はまったく違うシリーズです。カメラを利用したAI開発用のボードになります。

開発ツールから開発言語まで、すべて違うので注意してください。動かすためには別途SDカードが必要になります。また、他のボードと違い無線を内蔵していません。

一覧

M5StickM5StickCM5StickTM5StickV
SoCESP32ESP32ESP32K210
画面128×64160×80240×135240×135
フラッシュ4M4M4M16M
PSRAM
Grove I2C
Grove GPIO
Grove UART
スピーカー
マイク
IMU9軸6軸6軸6軸
ボタン1222
SDカード
バッテリー8080300200
価格未発売1,980未発売3,080

一般的にM5Stickと言ったらM5StickCだと思われます。M5StickVは中身がかなり特殊なので、起動してサンプルを動かすまででも、かなり難しいです。

それ以外のものは、とりあえず日本で販売していないので気にしなくても大丈夫です。

M5Cameraシリーズ

M5Camera

このカメラ付きモデルはかなり特殊で、M5Stickシリーズと比べると画面もバッテリーも内蔵していません。普通のESP32にカメラとケースを付けたものになります。

ESP32でかんたんにカメラを利用したい場合には便利です。

一覧

M5Camera
SoCESP32
画面
フラッシュ4M
PSRAM4M
Grove I2C
Grove GPIO
Grove UART
スピーカー
マイク
IMU
ボタン0
SDカード
バッテリー
価格1,815

実はM5Cameraは昔のバージョンや色違いなどがあるのですが、あまり一般的でないので現在販売中のモデルのみ紹介します。

Unitシリーズ

M5Stack社の製品はGrove端子を利用して、機能を拡張することができます。拡張するものをユニットとよんでいます。

UnitV AI Camera

M5StackVからバッテリーなどをなくし、最小限にした製品です。基本的にはM5StackやM5StickCと組み合わせて利用することになります。

M5Stack用ミニサーマルカメラユニット

赤外線サーモカメラのユニットです。M5StickTと比べると利用されているセンサーは安いものになります。

ATOMシリーズ

M2ATOMとして開発されていたシリーズです。M2ですので、内部の基板が2センチ角です。まだ日本での販売はありませんし、日本で使うこともできません。

M5StickCから機能を減らし、一番シンプルにした製品群です。バッテリーも内蔵されていないのですが、M5StickCより利用できるピンが多いので拡張性は高いです。

ATOMだけではちょっと単機能過ぎますが、外部拡張する前提の組み込みでは安いのでかなり使えそうです。

ATOM Lite

一番シンプルなATOMです。LEDはフルカラーが1個だけ搭載されています。個人的にはLiteの方がATOMの本命になるかなと思っています。

ATOM Matrix

M5StackCの画面の変わりに5×5の25個フルカラーLEDが搭載されているようなモデルです。ATOMをとりあえず触ってみるときにはMatrixを選ぶ人が多いと思います。

慣れてきたら、LEDはそんなに個数が必要ないと思うので安いLiteを大量に使う方が良さそうです。

M5Stackシリーズの選び方

基本的にはM5Stack Basicが一番基本的な製品となります。スイッチサイエンスさんが動画で選び方をアップしているので、こちらも参考にしてください。

去年の年間売上ランキングとかを見ると、M5Stack Basicでいいのかなーって思っていますが、私はM5StickC以外持っていません。

M5Stickシリーズの選び方

M5StickCはとりあえず1つあると便利だと思います。M5StickVはちょっと癖があるので、AI開発にしては気軽に開発が可能ですが、M5StickCなどの開発に比べると非常に難しいです。

用途別の選び方

とりあえずなにか欲しい

M5StickCをおすすめします。とりあえず一通りの用途で利用できると思います。

キーボードが欲しい

M5Stack Facesが無難ですが、M5StickCにキーボードHAT(日本未発売)かキーボードユニットを組み合わせる方法もあります。キーボード系はキーは取得できますが、処理は自分で作る必要があるので、あまり実用的ではないかもしれません、、、

赤外線サーモカメラが欲しい

M5StickTは高いのと日本で利用できないので、M5StackかM5StickCにM5Stack用ミニサーマルカメラユニットを接続する形がいいと思います。

カメラが欲しい

カメラを使うのはなかなか難しくて、画面もバッテリーも必要ないのであればM5Cameraが最適です。画面が必要ある場合には、カメラ搭載している製品とM5StackかM5StickCを接続して利用する必要があります。

M5StickCとUnitVが最近のトレンドですがUnitVを使うのはかなり大変です。M5StickCとM5Cameraを無線で通信する方法などが比較的難易度が低い気がします。

他のシステムを無線化したい

今後ATOMがこの領域に入ってきますが、現在はM5StickCが適しています。

AI開発を行いたい

無線通信をしないのであればM5StickV。無線を利用する場合にはM5StickCとUnitVがおすすめです。

V系はSDカードが必須で、本体とSDカードの相性問題があるので、まず起動するのだけでも大変です、、、

購入場所

Amazonのリンクを貼っていますが、商品の画像を見せたいだけですので、即納で欲しいものがある場合以外には他のお店で買うことをおすすめします。

スイッチサイエンス

日本の総代理店ですので、一番確実です。千石電商さんや秋月電子通商さんで購入してもスイッチサイエンスさん経由ですので安心です。

スイッチサイエンスさんのシールがはられている、正規取扱品を購入するのをまずはおすすめします。

M5Stack社直販

上記が直販サイトです。日本で買うよりも安いですが、届くまでに2週間ぐらいはかかる気持ちで注文してください。

日本で使えない製品も含んでいたり、新製品は一ヶ月ぐらい到着まで時間がかかったりするので、外国からの通販に慣れていない人にはおすすめしません。

また、初期不良や故障などの対応も英語で行う必要があります。

まとめ

いろいろ書いてありますが、M5StickCしか私は持っていません。。。

最初の一個はオレンジ色のM5StickCがおすすめです。安いですしね。購入してみるとわかるのですが、触っていると楽しいので増えていきます。一番お財布にやさしいのはM5StickCになります。

M5Stackの場合には、深く触るためにはBasic、Gray、Fireの3種類は必要になりますのでそれだけで結構な金額になります。

UnitVも試してみたいですが開発言語や環境がまったく違うので、まだ手を出していません。

ATOMが中国で発売されましたが、日本で販売する準備をしている途中みたいです。ATOMは安くて非常に使われそうな気もしますが、まずはM5StickCで作って、もう少し安くしようとATOM Liteに載せ替えるみたいな使い方の方がいい気がします。

もしくはラジコンなどの無線化で、ATOMを組み込むみたいな使い方ですね。無線系の新製品は中国から直接購入すると日本で使えるものと、使えないものが混ざって届く可能性があるので、なるべく初物はスイッチサイエンスさんから購入することをおすすめします。

Grove端子(HY2.0-4P)について調べてみた その2

概要

上記のあとに、他の部品も購入してみたので、続編です。

春節前のセールで購入したので今よりかなり安いです。そして今買っても中国からの発送が止まっている場合があるので注意してください。。。

コネクタ抜き

コネクタから端子を抜くピンを二種類買いました。

最低限必要な3本だけのセット。1本ピンのと、2ミリ幅の2本ピン、2.54ミリ幅の2本ピンのセットです。普段使うのはこれで大丈夫な気もします。

上記の店舗で140円で購入しましたが、今見ると159円で送料無料から103円と倍ぐらいの金額になっています。。。

いろいろついているフルセット。たくさんありすぎてよくわかりません。

上記のセットで36本組みで、394円で買いましたが、今は521円とかなり値上がりしています。

今買うんだったら中国からの配送は止まっている気がするのでAmazonプライムの即納の方がいいかな?

3本セットのでもいいのですが、Amazonだとやすいのが販売されていませんでした。中国発送だとAliExpressからと変わりませんからね。

コネクタの抜き方

ピンを抜くのは、上記の隙間にピンをいれて、爪を引き上げると抜けます。隙間が2ミリのピンを使うと抜きやすいですが、一本ずつ細いマイナスドライバーとかで抜くこともできると思います。

端子を抜くとこんな感じのコンタクトピンになっています。

裏側は平らになっています。

HY2.0-4P 基板はんだ付けタイプコネクタ

ケーブル側は前回かったのと変わらなかったですが、基板側がストレートタイプの物です。L字のHYもあるとは思いますが、秋月さんの純正でもいいと思います。

上記サイトで152円でした。今もほとんど値段は変わっていないですね。

HY2.0 コンタクトピン

ついに安いコンタクトピンを見つけました。前回は1個あたり4円以上と微妙な値段でしたが、こちらは1個1円ちょっと!

今見るとターミナルが100個で121円ですが、これはセール時に107円で購入しました。この商品はHY2.0-2Pのバラ売り用のページなのですが、ターミナルだけのページもあります。ターミナルのバラ売りだと1000個で1815円。たぶんこっちの価格が小売の正規価格な気がしますが、100個売りの方が断然安かったです。

これ以上の単価は淘宝とかで調達しないとだめな気がします。

まとめ

安いコンタクトピンを発見したので、既存ケーブルから純正コネクタだけ回収して、新規ケーブルで作り直すことができるようになりました!

みなさんも、他の規格のコネクタとの変換ケーブルなどを自作してはどうでしょうか?

ただ、圧着ペンチだけは安いのを買うのではなく、エンジニアのちゃんとしたやつを購入するか、ちゃんと圧着されないことを前提にペンチなどで潰して、はんだで固定などの壊れる前提の割り切りが必要です。

ESP32のULPアセンブリ言語入門 その1

概要

ESP32のコプロセッサー用ULPアセンブリ言語の入門向けの情報をまとめました。

ULPとは?

ESP32に搭載されているコプロセッサーで、Ultra Low Powerの略です。非常にかんたんな命令しか利用できませんが、比較的単純なためマシン語の入門に適していると思います。

アセンブリ、アセンブラ、アセンブル?

本来CPUは数値の命令コードでのみ実行することが可能です。数値の命令コードをマシン語や機械語などと呼びます。さすがに数値のみだと理解が難しいので、数値に1対1で対応するプログラミング言語が作られ、アセンブリ言語と呼ばれています。

あとはラテン語とか英語とかの接尾語の変化で、アセンブリ言語をマシン語に変換するプログラムをアセンブラと呼び、変換する行為をアセンブルと呼びます。

後ろ2つは、コンパイラとコンパイルの対応と同じです。

n進法とは?

アセンブリ言語の説明に入る前に、数をどのように表記するのかを整理します。

10進数とは?

普段使っている数字は、10進数だと思います。10進数で56は、1の位が6、10の位が5で56と表記しています。

一番右側の位が1で、その左は10進数なので10、さらに左はさらに10倍して100の位になります。

各位に入っている数値を掛け算したものを、すべて足すことで合計がわかります。

60進数とは?

ちょっと、特殊な例ですが10進数以外に日常で使っているのが60進数になります。時間と分の関係が60進数です。日まで入れると60じゃないので、この2桁だけ考えてください。

一番右側の位が1で、その左が60進数なので60の位です。掛け算して足してあげることで、合計の分が計算できます。

この10進数と60進数の考え方が理解できましたでしょうか? まだよくわからない場合には、もう一度読み直してもらいたいです。理解しないまま次を読むとさらに混乱すると思います。

2進数とは?

説明のため4桁になっていますが、基本的な考えは一緒です。一番右の位は1で、その左は2進数なので2の位、その左はさらに2倍して4の位、その左はさらに2倍して8の位になります。

10進数の場合には、各位には0から9までの数字が入ります。10の場合には上の位に繰り上がりますからね。同じように60進数の場合には0から59までが入ります。

2進数の場合には0から1の数字が入ります。

n進数の場合には各位には0からn-1までの数字が入ります。こう説明すると一気に混乱するので、理解しなくても大丈夫です。ただこの説明から入る入門書が多く、2進数の理解を難しくしていると思います。

コンピューターの内部では、基本的に2進数で動いているのですが、2進数は欠点があり、非常に桁数が多くなります。

ESP32は32ビットコンピューターで、1ビットが2進数の1桁に相当しますので、32桁も必要になります。

10101010101010101011110001110001みたいな表記になっても、上記のルールで右から1,2,4,8,16,32,64,,,と計算していけばわかりますが、ちょっと多すぎてよくわからないですよね?

そこで人が見る数字には16進数を使います。

16進数とは?

いきなりAと書いてありますが、16進数の場合には0から15までの数字が各桁に入ります。できれば1文字で表記したいので、トランプみたいに10はAに置き換えています。

上にあるのが、対応表です。AからFまでの記号を使って表現しています。

2進数と16進数の対応ですが、2進数4桁が16進数1桁に対応しています。2進数で1111は8+4+2+1で15になります。16進数ですとFですね。

16進数2桁だと、2進数8桁の8ビットに相当します。8ビットが基本的な最低単位となっており、1バイトとよんだりします。

上に出ていた10101010101010101011110001110001の2進数を変換する場合には4桁に区切って、1010,1010,1010,1010,1011,1100,0111,0001この4桁を16進数に置換します。するとAAAABC71になります。

ここまでくると覚える必要はありません。基本的はWindowsの電卓などを利用して計算してください。

Windows10だと電卓の種類をプログラマーに変更すると、HEX(16進数)、DEC(10進数)、OCT(8進数)、BIN(2進数)がかんたんにわかります。

エクセルでもHEX2BIN()みたいな関数で変換が可能です。8進数は説明していませんでしたが、ファイルの管理権限などを管理するパーミッションで利用されています。16進数の半分なので、2進数3桁分をまとめたものになります。

ここまで書いておいてなんですが、最終的には16進数の数値から10進数の数値が変換できれば問題ありません。

BC71みたいな16進数があったら、電卓でBC71を入力し、10進数だと48241だとわかれば大丈夫です。

n進数のまとめ

10進数2進数16進数
000
111
2102
3113
41004
51015
61106
71117
810008
910019
101010A
111011B
121100C
131101D
141110E
151111F
161 000010
2551111 1111FF
2561 0000 0000100
5111 1111 11111FF
51210 0000 0000200
102311 1111 11113FF
1024100 0000 0000400

上記が対応です。1から16ぐらいまでは暗記していると便利です。

10進数2進数8進数16進数
Arduino表記1000b110010001440x64
printf書式%dなし%o%x

プログラム中の表記方法ですが、10進数はそのまま記述、2進数はバイナリを表す0bを先頭に追加。8進数は0を先頭に追加。16進数は0xを先頭に追加します。

昔は8進数をよく使っていたので、10進数と区別するために先頭に0を追加した名残だと思います。その後16進数を使うようになったので0xが追加されたのかな?

数字の0だけれど、8をあらわすオクトパスなどのOctに由来しているかもしれません。

printf書式については、電卓の表記と一緒で、10進数はDECの%d、8進数はOCTの%o、16進数はHEXの%xになります。xは大文字で書くと、abcdefの文字も大文字になります。2進数は標準だと出力できないみたいです。

プログラム中に2進数を使うことはほぼないですが、データシートなどではビットで操作する場合に0b00000001みたいな記述が出てきます。

単位について

ビット(bit)

2進数の1桁に相当します。0か1かのあるなしで、バイナリやbinなどのと表記されていることがあります。

バイト(Byte)

8bit=1Byteです。ビットとバイトは両方bからはじまるのでビットは小文字のb、バイトは大文字のBを使うことが多いですが、bだけだと判断できないことも多いです。

キロ(K)

1000g=1kgなどのときに使う記号です。一般的には1,000をあらわしますが、コンピューターでは1,024をあらわすことが多いです。

これは2進数の0b100 0000 0000が1024(0x40)に相当するためです。

パソコンの空き容量などの表示では1,024のKが使われます。しかしハードディスクなどのメディア販売時の容量は1,000のkが使われます。そのため1,000kByteのメディアを購入しても、パソコン上では976KByteと認識したりします。

多くの場合1,000の場合には小文字のk、1,024の場合には大文字のKが使われますが、Kだけだと判断できないことも多いです。

コンピューターだと通信速度は1,000のkですが、それ以外は1,024のKが多いはずです。

メガ(M)、ギガ(G)、テラ(T)、ペタ(P)

こちらも大きさを表すものです。

1000の場合1024の場合
1K1,0001,024
1M1,000,0001,048,576
1G1,000,000,0001,073,741,824
1T1,000,000,000,0001,099,511,627,776
1P1,000,000,000,000,0001,125,899,906,842,620

上記の対応になります。基本的には3桁増えていくのですが、1,024の倍数だとTまで行くと差が1割近くあります。1TByteのハードディスクを買ってきても、パソコンが認識するのは0.9TByteぐらいになってしまいます。

携帯の通信容量のギガがなくなるってのは、上記のGです。通信系は1,000の単位系が多いので、1Gは1,000,000,000Byteになります。

このKが1,000なのか、1,024なのかはわかりにくいので注意してください。昔はMでも差が5%弱と少なかったのであまり気にしなかったのですが、Pだと12.5%も出てきますので、無視できない差です。

ミリ(m)、マイクロ(u)、ナノ(n)

こちらも大きさをあらわす記号です。

1m0.001
1u0.000001
1n0.000000001

こちらはすべて1000分の1をあらわします。コンピューターの場合には時間をあらわすときによく利用します。

1秒は1sec、もしくは1sと表記することが多く、0.001秒を1ms、0.000001秒を1us、0.000000001秒を1usなどと表記します。

プログラムではミリ秒ぐらいを利用することが多いですが、内部的にはマイクロ秒やナノ秒などの単位でコンピューターは動いています。

まとめ

ULPの解説をまったくしていませんが、基礎的なことから解説をはじめて、コンピュター内部でどのように動いているのかという、仕組みも一緒に解説できればと思っています。

続編

ESP32のGPIO入力について

概要

ESP32の入力に関してかんたんにまとめてみました。digitalRead()とanalogRead()、touchRead()が対象です。厳密にはGPIO入力ではありませんが、hallRead()とtemperatureRead()も紹介します。

上記、出力についてと共通している部分は説明を簡略化してあります。

動作電圧について

ESP32は3.0Vから3.6Vまでの電圧で動作させることができます。本解説では3.3Vで動かした場合の数値で説明を行います。

ピンの接続について

入力を行う場合には、ピンの接続について確認を行う必要があります。基本的には何も接続されていない場合の入力値については信用できません。

これは、内部のセンサーに静電気のような電荷が残っていることがあり、未接続の場合には残っている電荷の値を読み取っていることになります。

とくにESP32の場合にはピンに複数の機能を割り当てられるマトリックス機能があり、ピンの裏側でセレクターで、センサーとピンを動的に切り替えながら読み取っているので、未接続のピンの場合には一つ前に読み取ったピンの電荷が残っている場合があります。

入力の種類について

ESP32では以下の5種類の入力があります。

デジタル入力 digitalRead()

一番標準的なGPIOの入力です。データシートによると3.3Vで動かした場合には、LOWはLow-level input voltageがVDD-0.3から0.25×VDDとあり、-0.3Vから0.825Vまでの入力をLOWと判定します。

HIGHはHigh-level input voltageが0.75×VDDからVDD+0.3とあり、2.475Vから3.6VまでがHIGHと判定されます。

VDDは最大3.6Vまでなので、High-level input voltageはVDD+0.3の3.9Vまでがデータシート上の入力最大値となります。そのため、5Vの信号をESP32に接続した場合にはデータシート上は、規定外の電圧となります。

一般的に3.3V駆動のマイコンに5V信号を入力すると、かんたんに壊れる場合が多いので注意してください。ESP32はデータシート上は保証していませんが、5V信号を入力しても、すぐに壊れることはないのですが、データシート上では保証していないことを理解して利用してください。

繰り返しますが、ESP32と同じ感覚で、他の3.3Vマイコンに5V信号を渡すとかんたんに壊れるので注意してください。STM32などはピンごとに5V入力をしてもよいかが規定されていますので、5Vトレラントかどうかを確認してから5Vを入力するか、レベルコンバーターなどを利用して3.3Vに変換してから接続してください。

ESP32でも5V入力は基本的には避けたほうが好ましいです。

また、実際の判定は3.3Vの半分である1.65Vより低い電圧か、高い電圧かでLOWとHIGHを判定していますが、データシート上は上下25%の範囲のみ保証しているので、中途半端な電圧を入力すると、ノイズの影響を受けやすいのでやめましょう。

5VのマイコンにESP32の3.3V信号を入力した場合には、5Vの50%である2.5V以上なのでHIGHで入力可能ですが、5Vの75%は3.75Vのため、多くのマイコンでは保証外の信号電圧になると思います。

アナログ入力 analogRead()

アナログ入力は、ADCとも呼ばれ、電圧を測定します。ESP32は標準だと0から4095までの値が取得できます。

注意したいのが、VDDを基準電圧としますので3.3Vで動かしている場合と、3.0Vで動かしている場合で取得した値の電圧が異なります。

電圧 = VDD / 4096 * 取得値

上記のような計算を行うことで電圧が求められます。一般的には3.3Vの場合決め打ちでも問題ありませんが、省電力のため3.0Vなどに電圧を下げた場合には注意しましょう。

また、ESP32内蔵のADCはそれほど精度が高くありません。内部的に補正がある程度されているみたいですが、低い電圧のときと高い電圧のときで測定精度に差がでています。精度が必要な用途であれば、I2C接続の電圧計などのほうが適しています。

ただし、通常用途であれば内蔵ADCで問題がでることは少ないと思います。

内部的はADC1とADC2の2つADCモジュールが内蔵されており、無線利用時には内部でADC2を利用してしまうため、ADC1しか使えなくなってしまいます。

無線を利用したいときにADC2に接続されているピンからアナログ入力を行う場合には注意して利用してください。基本的には無線を利用する前にアナログ入力を行い、その後に無線を利用してから、ディープスリープかリセットを行って再起動します。

無線を利用する前のレジスタの値を保存しておいて、無線停止後に元に戻すことでADC2が再度使えるようになりますが、多くの場合には再起動をして、クリーンな環境に戻すか、ADC1のピンを利用したほうが好ましいと思います。

また、ADCの内部的にはVDDを1V程度に減衰して測定しています。0Vから1Vまでなどのようにより低い電圧を高精度で測定する場合には、減衰器の設定を変えることで精度が上がりますが、多くの場合外部のADCを利用したほうが好ましいです。

複数回測定して、平均値を取得するなどの設定もありますが、通常用途の場合にはデフォルトの状態で利用して、移動平均などの適切なフィルタを利用したほうが精度があがりやすいはずです。

タッチ入力 touchRead()

タッチ入力は、静電容量を利用したタッチセンサーです。ピンに一瞬電荷をチャージして、直後にピンの電荷を確認することでタッチの確認をしています。これは人が触れた場合などに、ピンの電荷が人に移動することで、タッチを判定しています。

入力値は電荷の値なので、絶対値ではなくアナログ値での取得になります。タッチセンサーを引き回す方法により、取得値がかわってくるので、実際に実験をして触っていない場合の値と、触った場合の値を確認してから実装する必要があります。

とくにジャンパワイヤで実験していたものを、基板作成してタッチエリアを作成した場合など条件が異なると、取得値も異なります。さらに、実際にためすとGPIO32とGPIO33のタッチセンサーが逆に認識されているようですので、テストしてから基板などの作成をしてください。

タッチセンサーとADCは別の回路のため、無線を利用している場合でもADC2に接続されているピンを利用することができます。

ホールセンサー hallRead()

ESP32に内蔵されている磁力センサーです。ESP32に磁石を近づけると値が小さくなるセンサーです。

対象物に磁石をつけて、接近センサーなどとして利用することが可能です。ただしM5StickCなどの、内部に磁石を内蔵しているマイコンの場合には反応が悪くなります。

内部温度センサー temperatureRead()

ESP32の内部温度のセンサーです。気温ではなく、内部の温度のため高負荷でESP32を利用した場合には上昇していきます。

長時間安定動作をさせたい場合などは、定期的に内部温度を測定し、熱暴走を起こしていないのかを確認する用途には利用できます。

気温を測定することは難しいですので、その場合にはI2C接続などの温度計を利用してください。

ピンモード

Arduinoではすべてのピンを利用前に初期化する必要があります。初期化し忘れても動くことがおおいですが、ピンによって起動時に設定されているモードが違うので、すべてのピンを初期化してから使うようにしたほうが安全です。

特に、アナログ入力は初期化しないと、analogRead()を呼び出しても、デジタル入力相当の0(LOW)か4095(HIGH)のどちらかしか取得できないピンがありますので、忘れずに初期化してください。

INPUT

pinMode(PIN, INPUT);

一番標準的なデジタル入力です。ピンに接続されている電圧に応じてLOWかHIGHが返却されます。

未接続のピンの場合、もどり値が不定ですのでLOWかHIGHがランダムに戻ってくる形になります。未接続なのでLOWが戻ってくるわけではないので注意してください。

INPUT_PULLUP

pinMode(PIN, INPUT_PULLUP);

VDD(3.3V)に抵抗経由でプルアップされているデジタル入力です。何も接続していない場合には常に3.3VですのでHIGHが入力されます。ピンの先にスイッチなどを接続して、スイッチオンでGNDに接続する形にすると電圧が0VになるのでLOWが戻ってきます。

未接続の場合でも常にHIGHが戻ってきます。スイッチなどを接続した場合にONでLOW、OFFでHIGHと信号が逆転したように見えますが、ノイズに強い方式になります。

I2Cなどの通信も内部で自動的にINPUT_PULLUPに設定されており、信号をLOWに落とすことで、通信を行っています。

入力用のモードですが、プルアップは出力を内部で利用しているので、入力専用ピンでは利用できないので注意してください。

INPUT_PULLDOWN

pinMode(PIN, INPUT_PULLDOWN);

GND(0V)に抵抗経由でプルダウンされているデジタル入力です。何も接続していない場合には常に0VですのでLOWが入力されます。ピンの先にスイッチなどを接続して、スイッチオンでVCCに接続する形にすると電圧が3.3VになるのでHIGHが戻ってきます。

未接続の場合でも常にLOWが戻ってきます。スイッチなどを接続した場合でもONでHIGH、OFFでLOWとわかりやすい状態で、ノイズにも強い方式になります。

INPUT_PULLUPとINPUT_PULLDOWNは、未接続状態でも抵抗経由でどこかに接続されているため、信号が安定しています。両方とも基本的な効果は同じなのですが、歴史的経緯によりINPUT_PULLUPがよく利用されています。

値を直接指定して、PULLUPとPULLDOWNを同時に指定すると、1.65Vでプルアップ?された状態になるらしいですが、良い子は真似をしないでください。

入力用のモードですが、プルダウンは出力を内部で利用しているので、入力専用ピンでは利用できないので注意してください。

ANALOG

pinMode(PIN, ANALOG);

アナログ入力を行う端子です。ADCに接続されているピンでのみ利用可能です。

ピンによっては初期化しないでanalogRead()を呼び出すと0(LOW)か4095(HIGH)しか返却しないので注意してください。

ピン一覧

GPIO種類ADCINPUTPULLUPPULLDOWNANALOG
GPIO0I/OADC2
GPIO1I/O×
GPIO2I/OADC2
GPIO3I/O×
GPIO4I/OADC2
GPIO5I/O×
GPIO6I/O×
GPIO7I/O×
GPIO8I/O×
GPIO9I/O×
GPIO10I/O×
GPIO11I/O×
GPIO12I/OADC2
GPIO13I/OADC2
GPIO14I/OADC2
GPIO15I/OADC2
GPIO16I/O×
GPIO17I/O×
GPIO18I/O×
GPIO19I/O×
GPIO21I/O×
GPIO22I/O×
GPIO23I/O×
GPIO24I/OADC2
GPIO25I/OADC2
GPIO26I/OADC2
GPIO32I/OADC1
GPIO33I/OADC1
GPIO34IADC1××
GPIO35IADC1××
GPIO36IADC1××
GPIO37IADC1××
GPIO38IADC1××
GPIO39IADC1××

INPUTはすべてのピンで利用できます。ただし利用しているボードにより、用途が決まっているピンがありますので、自由に利用できるピンから選んでください。

また、多くの場合にはGPIO0は3.3Vに外部からプルアップされています。そのためアナログ入力をしても3.3VかGNDかの入力しかできません。

INPUT_PULLUPとINPUT_PULLDOWNは内部で出力の機能を利用しているので、入力専用の端子であるGPIO34以降では利用できませんので注意してください。

また、ANALOGは利用できるピンがかなり限定されていますので注意してください。特に無線を利用する場合にはADC2に接続しているピンが利用できなくなります。

まとめ

アナログ入力は内部パラメーターがかなり多いですが、あまり利用はしません。興味がある人は調べてみるとより深く知れると思います。

プルアップとプルダウンはちょっとわかりにくいため、検索して他のサイトで回路図付きの解説を確認したほうがよいと思います。(余裕があれば回路図今度書きます、、、)

ESP32のGPIO出力について

概要

ESP32の出力に関してかんたんにまとめてみました。digitalWrite()、ledcWrite()、dacWrite()を対象とします。

動作電圧に関して

ESP32は3.0Vから3.6Vまでの電圧で動作させることができます。昔はもう少し低電圧で動作が可能でしたが、無線を利用時に不安定になるので、最低電圧が上がっていった経緯があります。

データシートPower supply voltageの項目を見ると、3.0Vから3.6Vまでで、推奨が3.3Vになっています。本解説では3.3Vで動かした場合の数値で説明を行います。

出力の種類について

ESP32では以下の3種類の出力があります。

デジタル出力 digitalWrite()

デジタル出力は一番シンプルな出力です。LOWの場合には0V、HIGHの場合には3.3Vの電圧を出力します。

LOWの場合には0Vを出力するよりは、外部からの電気を吸い込む動きになります。LOWを出力すると内部でGNDに接続するイメージで利用してください。

ESP32の場合GPIOから出力した場合、あまり電流を流せないですが、外部電源からGPIOに接続してLOW出力することで、より多くの電流を吸い込む方向で流すことが可能です。

また、厳密にはESP32内部での電圧低下などもありますので、データシートによるとLOWのときにはLow-level output voltageが0.1×VDDとありますので0Vから0.33V、HIGHのときにはHigh-level output voltageが0.8×VDDで、2.64Vから3.3Vが出力されます。

実際のところLOWはほぼ0Vで、HIGHも3.0V以下が出力されることは少なく、3.0V以下になった場合には入力電源が足りていない可能性が高いです。

他の出力でも出力電圧は同様にぶれますが、説明が複雑になるのでLOWで0V、HIGHで3.3Vとして説明を続けます。

PWM出力 ledcWrite()

少しわかりにくいパルス幅変調出力です。デジタル出力はLOWかHIGHの2種類でしたが、PWM出力は、擬似的に出力量を制御することができます。

上記の図の場合8分割したスロットのうち、4つだけHIGHの出力をしています。周期的に出力されていますので、LEDなどの点灯に利用した場合には50%の明るさで光ります。

上記が8分の2の出力です。この場合には25%の点灯になります。

PWM出力をする前に、PWMの初期化を行う必要があります。上記は16スロットに分割した例です。スロット数を増やすことで、より細かい粒度で出力を調整することができます。

もう一つ設定しないといけないのが、1スロットの時間です。時間を倍にすると上記のように横幅が広がります。

時間の設定は内部タイマーで行います。一定クロックが経過したら、カウントアップする設定になっており、スロット単位で出力をLOWとHIGHを切り替えています。

CPUクロックを下げることで、PWMの最高クロックも下がってしまうので注意してください。標準的な設定では12kHz(12000)で、1サイクルは256スロット(8ビット)に設定することが多いようです。

通信用のクロック周波数などを作成する場合には上記の設定が重要になり、HIGHとLOWを繰り返すため、スロットを2スロット(1ビット)に設定し、希望するクロック数で動かしたりします。それ以外のLEDの明るさ制御などは、それほど細かい設定を気にしなくても利用できます。

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

上記の場合には8ビットで256スロットあるので、出力なしの0から、すべてのスロットがHIGHになっている256までの出力となります。

上記がオシロスコープで測定した図です。左から0%、50%、100%と出力を変えながら実験したときのデータになります。

アナログ出力 dacWrite()

PWM出力は擬似的な出力制御でしたが、アナログ出力は出力電圧を直接することができます。

上記が0%、50%、100%出力をオシロスコープで確認したときのデータです。PWMは主にLEDの明るさ制御ですが、アナログ出力は主にスピーカーを鳴らす場合に利用することが多いです。

dacWrite()は0から255までを指定することができ、0が0Vで255が3.3V出力になります。PWMの場合には256が最高値でしたが、dacWrite()では255が最高値なので注意してください。

スピーカー出力の場合には、無音が128で、上下に波形が出る形になりますが、初期化前の0から無音状態の128に急に出力を変更したときにノイズが乗るので注意しましょう。諦めるか、徐々に数字をあげて128にしてから再生することでノイズ低減が可能です。

ピンモード

Arduinoではすべてのピンを利用前に初期化する必要があります。初期化し忘れても動くことがおおいですが、ピンによって起動時に設定されているモードが違うので、すべてのピンを初期化してから使うようにしたほうが安全です。

OUTPUT

  pinMode(PIN, OUTPUT);

一番標準的な出力で、基本的にはこれを出力しておけば問題ありません。入力の場合にはアナログとデジタル出力でモードが違いますが、出力はOUTPUTで問題ありません。

デジタル出力やPWM出力の場合、LOWの場合にはGNDに接続して、外部から電気を吸い込み、HIGHの場合には3.3Vを出力します。アナログ出力の場合には0はデジタル出力やPWM出力のLOWと同じですが、1-255までは任意の電圧を出力します。

OUTPUT_OPEN_DRAIN

  pinMode(PIN, OUTPUT_OPEN_DRAIN);

オープンドレイン出力モードです。デジタル出力とPWM出力のモードになります。LOWのときにはGNDに接続して外部からの電気を吸い込む動きで変わらないですが、HIGHのときには3.3Vを出力するのではなく、ハイインピーダンスという、なにも接続されていない状態になります。

これはLOWのときに電気を吸い込む動きだけを行いたい場合、HIGHで3.3Vを出力してもその電力は無駄になるので、出力しないモードです。

ピン一覧

GPIO種類デジタル出力PWM出力アナログ出力
GPIO0I/O×
GPIO1I/O×
GPIO2I/O×
GPIO3I/O×
GPIO4I/O×
GPIO5I/O×
GPIO6I/O×
GPIO7I/O×
GPIO8I/O×
GPIO9I/O×
GPIO10I/O×
GPIO11I/O×
GPIO12I/O×
GPIO13I/O×
GPIO14I/O×
GPIO15I/O×
GPIO16I/O×
GPIO17I/O×
GPIO18I/O×
GPIO19I/O×
GPIO21I/O×
GPIO22I/O×
GPIO23I/O×
GPIO25I/O
GPIO26I/O
GPIO27I/O×
GPIO32I/O×
GPIO33I/O×
GPIO34I×××
GPIO35I×××
GPIO36I×××
GPIO37I×××
GPIO38I×××
GPIO39I×××

GPIO34以降は入力専用のため、出力には一切利用できません。

アナログ出力はGPIO25とGPIO26でのみ利用が可能です。

PWMは最大16チャンネルまでのサポートのため、同時に利用できるのは16個までです。

関連情報

まとめ

出力はPWMとオープンドレインがちょっと難しいですが、入力に比べればシンプルです。

上記に入力についても書いたので、合わせて読んでもらえればと思います。