M5StickCでSPI接続のSDカードを使う

使うことはないとは思いますが、手元にSPI接続のSDカードリーダーがあったので、、、

利用デバイス

いろいろなSDカードリーダーがありますが、これの右側みたいなやつを使いました。SPI接続以外にもいろいろとSDカードリーダーがあるので、気をつけて選びましょう。

接続方法

M5StickCSD
3V33V3
GNDGND
0CLK
26MISO
32MOSI
33CS

M5StickC側はどのピンでもアサイン可能ですが、初期化時に若い番号から並ぶようにアサインしました。

SDカードの仕様上、プルアップする必要がありますが、基板上に抵抗などの部品があれば内部でやってくれているはずですので、そのまま直結させます。

SDカードは3.3Vで動きますので、間違ってGROVE端子などの5Vを接続しないようにしましょう。

サンプルスケッチ

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

// PIN配置
enum { spi_sck = 0, spi_miso = 26, spi_mosi = 32, spi_ss = 33 };

void setup()
{
  // M5StickC初期化
  M5.begin();
  M5.Lcd.setRotation(3);

  // SPI初期化
  SPI.begin(spi_sck, spi_miso, spi_mosi, spi_ss);

  // SDカード初期化
  if (!SD.begin(spi_ss)) {
    M5.Lcd.println("Card Mount Failed");
    return;
  }

  // SDカード種別取得
  uint8_t cardType = SD.cardType();
  if (cardType == CARD_NONE) {
    M5.Lcd.println("None SD Card");
    return;
  }
  M5.Lcd.print("SD Card Type: ");
  if (cardType == CARD_MMC) {
    M5.Lcd.println("MMC");
  } else if (cardType == CARD_SD) {
    M5.Lcd.println("SDSC");
  } else if (cardType == CARD_SDHC) {
    M5.Lcd.println("SDHC");
  } else {
    M5.Lcd.println("UNKNOWN");
  }

  // SDカード容量取得
  uint64_t cardSize = SD.cardSize() / (1024 * 1024);
  M5.Lcd.printf("SD Card Size: %lluMB\n", cardSize);
}

void loop() {
}

SDカードをマウントしてから種別と容量を取得するだけのサンプルです。

まとめ

SDカードはSPI接続以外にも接続方法がありますが、M5StickCの場合利用できるピン数の関係でSPIしか利用できません。

今回4線全部利用していますが、CSはGNDに落とすことで使わなくすることができますが、SD.begin()で何らかのピンを指定しないといけないので、33を割り振っています。

M5StickCのQRCodeを調べてみた

単機能なんで、簡単だろうかと思ったら、いろいろわかりました。

QRコードとは?

二次元コードの一種で、一番使われている形式です。QRコードにもモデル1、モデル2、マイクロQRコードといろいろな種類があり、M5StickCで利用できるのは一番一般的なモデル2の誤り訂正レベルLになります。

iPhoneで読み取るためには

ここが一番重要で、普通に読み取ろうとしても読み取れません!

まず液晶の明るさが明るすぎて、画面が白飛びします。そしてQRコードが小さいので認識しません。カメラを近づけるとピンぼけします。

しかしながら、カメラの拡大機能を使って一番拡大すると、すんなり読み取れます。いままでiPhoneで試行錯誤しながらQRコード読み取っていた努力はなんだったんだろう、、、

サンプルコード

#include <M5StickC.h>

void setup() {
  M5.begin();

  M5.Lcd.qrcode("吾輩わがはいは猫である。名前はまだ無い。 どこで生れたかとんと見当けんとうがつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。吾輩はここで始めて人間というものを見た。しかもあとで聞くとそれは書生という人間中で一番獰悪どうあくな種族であったそうだ。この書生というのは時々我々を捕つかまえて煮にて食うという話である。",
    0, 45, 80, 15);
}

void loop() {
}

限界近くまで文字を詰め込んだ例です。こうみるとかなりシンプルですが、公式スケッチ例にはない、省略されたパラメータも一緒に指定したほうがよいと思います。

M5.Lcd.qrcode関数について

宣言

void 	qrcode (const char *string, uint16_t x=5, uint16_t y=45, uint8_t width=70, uint8_t version=7);
void 	qrcode (const String &amp;string, uint16_t x=5, uint16_t y=45, uint8_t width=70, uint8_t version=7);

文字列の型が違うだけで、他は一緒の関数がM5Displayクラスで宣言されています。関数の実体はqrcode.hにありますが、そちらは通常気にしなくても良いと思います。

引数

  • string : 表示させる文字列
  • x : 画面上の表示座標
  • y : 画面上の表示座標
  • width : QRコードの大きさ
  • version : QRコードのバージョン

できるだけ大きくQRコードを表示するためには、M5StickCの液晶画面のサイズである80をwidthに指定し、それにともないxを0にして幅いっぱいに表示させる必要があります。

また、versionが一番重要で、ここの指定で表示できる文字数などが決まります。

QRコードのバージョンについて

Versionセル数倍率横幅バイナリ
12136317
22537532
32925853
43326678
537274106
641141134
745145154
849149192
953153230
1057157271
1161161321
1265165367
1369169425
1473173458
1577177520

上記が指定できるバージョンです。ちなみに0を指定するとM5StickCがハングアップしますのでご注意ください。

セル数が重要で、縦横のドット数になります。画面いっぱいに表示する場合には80ドットまで利用できるので、80未満になるように拡大されて表示されます。

そのためバージョン1でもバージョン2でも表示されるサイズは違いますが、同じ倍率3なので、認識率は変わりません。

そのため倍率の変わり目のバージョン2、5、15を選ぶのが良いでしょう。またバージョンが上がると表示できる文字数が増えます。

バイナリと表現されているところが、表現できるバイト数です。数字だけとか記号が含まれない英数のみであれば、もう少し文字数が増えますが、URLとかテキストとかを表示する場合には、バイト数換算で計算したほうがわかりやすいと思います。

規定バイト数を超えたデータを表示した場合には、一見QRコードは表示されているように見えますが、認識できませんので注意しましょう。

  • バージョン2 : 32バイトまで
  • バージョン5 : 106バイトまで
  • バージョン15 : 520バイトまで

普段使うのは5あたりが無難な気がします。15はちょっといかついです。

まとめ

一番苦労したのがiPhoneでの読み取り方法でした。。。画面の明るさは9ぐらいが白飛びしにくいですが、拡大したら8から15までで読み取れました。

M5StickCでGROVEのアナログ出力をBluetoothSerialで飛ばす

昔に作ったものをM5StickCで作り直しました。

概要

M5StickCにGROVE接続のアナログセンサーを取り付けて、そのデータを0.5秒間隔でBluetoothSerialで送信します。

PCからBluetoothSerialに接続して、そのデータをProcessingを使って受信して、グラフ化とCSV出力を行います。

機材一覧

Processingとは?

Arduino IDEの元の元になったアプリケーションで、主にグラフィカルな処理を得意としている開発環境です。

Javaで動作しているので、Arduino IDEとは開発言語が違いますが、比較的簡単にマルチプラットフォームで動くプログラムを作ることが可能です。

利用ライブラリ

  • controlP5

プルダウンやグラフの描画が簡単にできるcontrolP5を利用していますので、事前にライブラリに追加しておいてください。追加方法はほぼArduino IDEと同じなのですぐにわかると思います。

上記のページにProcessingやcontrolP5の追加などを、わかりやすくまとめられています。

GSR(Galvanic Skin Response)センサとは?

皮膚への電流の流れやすさを測定するセンサで、汗などの影響によって測定値が変動します。手元にあったので利用しただけで、実際のところあまり使い所がありません。

M5StickC側のコード

#include <M5StickC.h>
#include <BluetoothSerial.h>

BluetoothSerial SerialBT;
uint64_t chipid;
char chipname[256];

void setup() {
  chipid = ESP.getEfuseMac();
  sprintf( chipname, "M5StickC_%04X", (uint16_t)(chipid >> 32));
  M5.begin();
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.printf("Bluetooth: %s\n", chipname);
  M5.Lcd.printf("Ver: %s %s\n", __DATE__, __TIME__);
  M5.Lcd.println();
  M5.Lcd.printf("Val:");

  SerialBT.begin(chipname);
}

void loop() {
  int val = analogRead(33);

  Serial.println(val);
  SerialBT.println(val);
  M5.Lcd.setCursor(8*4, 8*3);
  M5.Lcd.printf("%4d", val);
  delay(500);
}

chipidを取得して、SerialBTの待受名を指定しているので、複数のM5StickCがある場合でも個別に選択して接続が可能です。

同じ名前の方が好ましい場合はSerialBT.begin()の待受名を固定してください。

内容はGROVE端子のアナログ入力(IO33)を取得して、画面への表示とSerialとSerialBTにデータを送信しています。

SerialBTしか使わないのであれば、Serialに送信しないほうがバッテリーが持つかもしれません。

delay(500)なので、約0.5秒間隔での送信になります。どんどんずれていくので、厳密に送信したい場合には時刻などを見て、インターバルで送信する必要があります。

ちなみに、このコードで約25分の動作が可能です。LCDのバックライトを暗くするとか、ボタンで消せるようにするなどで、もう少し動作時間を伸ばすことは可能だと思います。

Processing側のコード

import processing.serial.*;
import controlP5.*;
import java.util.*;
 
ControlP5 cp5;
Chart myChart;
 
int sizeW = 640;          // 画面横幅 640以上
int sizeH = 480;          // 画面縦幅 480以上
int samplingCount = 10;   // 未選択時のサンプリング数
int serialSpeed = 115200; // 未選択時の速度
int minValue = 0;         // グラフの最小値
 
Serial myPort;
 
int lf = 10;
int miliSec;
PrintWriter fp;
float maxValue;
Slider slider;
ArrayList<Integer> samplingList = new ArrayList<Integer>();
 
List<String> ls = new ArrayList<String>();
List<String> lc = new ArrayList<String>();
 
void settings() {
  size(sizeW, sizeH);
}
 
void setup() {
  int oldSec = second();
  for (int i = 0; i < 3000; i++) {
    // 起動時間からのミリ秒と時刻のミリ秒の差分を探す
    if ( oldSec != second() ) {
      miliSec = millis() % 1000;
      break;
    }
    delay(1);
  }
  cp5 = new ControlP5(this);
 
  // sampling
  lc.add("10");
  lc.add("20");
  lc.add("30");
  lc.add("40");
  lc.add("50");
  lc.add("100");
  lc.add("500");
  lc.add("1000");
  cp5.addScrollableList("sampling_count")
    .setPosition(sizeW-200, 0)
    .setType(ControlP5.LIST)
    .setSize(200, 200)
    .setBarHeight(20)
    .setItemHeight(20)
    .addItems(lc)
    ;
 
  // speed
  ls.add("9600");
  ls.add("19200");
  ls.add("38400");
  ls.add("57600");
  ls.add("115200");
  cp5.addScrollableList("serial_speed")
    .setPosition(sizeW-200, 190)
    .setType(ControlP5.LIST)
    .setSize(200, 200)
    .setBarHeight(20)
    .setItemHeight(20)
    .addItems(ls)
    ;
 
  List l = Arrays.asList(Serial.list());
  cp5.addScrollableList("serial_port")
    .setPosition(sizeW-200, 320)
    .setType(ControlP5.LIST)
    .setSize(200, 200)
    .setBarHeight(20)
    .setItemHeight(20)
    .addItems(l)
    ;
 
  String fileName = createFileName();
  fp = createWriter(fileName);
 
  myChart = cp5.addChart("")
    .setPosition(0, 00)
    .setSize(sizeW-250, sizeH)
    .setRange(0, 1)
    .setView(Chart.LINE);
 
  myChart.getColor().setBackground(color(255, 100));
 
  myChart.addDataSet("val");
  myChart.setColors("val", color(255, 255, 255), color(255, 0, 0));
  myChart.setData("val", new float[1000]);
 
  myChart.addDataSet("avg");
  myChart.setColors("avg", color(0, 255, 0), color(255, 0, 0));
  myChart.setData("avg", new float[1000]);
 
  myChart.addDataSet("min");
  myChart.setColors("min", color(0, 0, 255), color(255, 0, 0));
  myChart.setData("min", new float[1000]);
 
  myChart.setStrokeWeight(1.5);
 
  slider = cp5.addSlider("Value", 0, 1, 50, sizeW-250, 0, 10, sizeH);
}
 
float peek = 0;
float lastpeek = 0;
int mode = 0;
int lastvalue = 0;
 
void draw() {
  // 画面クリア
  background(0);
 
  // 何かデータを受信したら
  if ( myPort != null &amp;&amp; myPort.available() > 0) {
    String data = myPort.readStringUntil(lf); // 文字列を受信
    if ( data != null ) {
      float value = float(trim(data));
      String timeStr = year()+"/"+month()+"/"+day()+" "+hour()+":"+minute()+":"+second()+".";
      int milisec = (millis()-miliSec)%1000;
      if ( milisec < 10 ) {
        timeStr = timeStr + "00";
      } else if ( milisec < 100 ) {
        timeStr = timeStr + "0";
      }
      timeStr = timeStr + milisec;
 
      fp.print( timeStr+",");
      fp.println(int(trim(data)));
      fp.flush();
 
      // 最大値更新
      if ( maxValue < value ) {
        maxValue = value;
        myChart.setRange(minValue, maxValue);
        slider.setRange(minValue, maxValue);
      }
 
      // 測定値追加
      samplingList.add((int)value);
      myChart.push("val", value);
 
      // 規定サンプル数以上の場合先頭削除
      if ( samplingCount < samplingList.size() ) {
        samplingList.remove(0);
      }
 
      // 移動平均計算
      int sum = 0;
      for ( int i = 0; i < samplingList.size(); i++ ) {
        sum += samplingList.get(i);
      }
      myChart.push("avg", sum / samplingList.size());
      slider.setValue(int(value));
 
      if ( lastvalue + 2 < ( sum / samplingList.size() ) ) {
        if ( mode == 1 ) {
          int j = myChart.getDataSet( "min" ).size() - 3;
          while ( 0 <= j &amp;&amp; myChart.getDataSet("min").get( j ).getValue() == -1 ) {
            // 最後の値を探す
            j--;
          }
          println(j);
          if ( 0 <= j ) {
            lastpeek = peek;
            peek = sum / samplingList.size();
            j += 1;
            int jstart = j;
            println(j);
            for (; j < myChart.getDataSet( "min" ).size(); j++ ) {
              myChart.getDataSet("min").get( j ).setValue( lastpeek + ( ( peek - lastpeek)  / ( 1000 - jstart ) ) * ( j - jstart ) );
            }
          }
 
          mode = 0;
          myChart.push("min", peek );
        } else {
          myChart.push("min", -1 );
        }
      } else {
        myChart.push("min", -1 );
        if( ( sum / samplingList.size() ) + 20 < lastvalue ){
          mode = 1;
        }
      }
       
      lastvalue = sum / samplingList.size();
    }
  }
}
 
void serial_port(int n) {
  println(Serial.list()[n]);
  if ( myPort != null ) {
    myPort.stop();
  }
  myPort = new Serial(this, Serial.list()[n], serialSpeed);
}
 
void serial_speed(int n) {
  serialSpeed = Integer.parseInt( ls.get(n) );
}
 
void sampling_count(int n) {
  samplingCount = Integer.parseInt( lc.get(n) );
  samplingList.clear();
}
 
String createFileName() {
  String fileName= nf(year(), 2) + nf(month(), 2) + nf(day(), 2) +"-"+ nf(hour(), 2) + nf(minute(), 2) + nf(second(), 2);
  fileName += ".csv";
  return fileName;
}

白が測定値で、緑が移動平均です。BluetoothSerialの場合には速度は何を選択しても通信が可能で、シリアルポートを選択すると受信を開始します。

プログラムのあるディレクトリにCSVファイルが出力されますので、そこに受信時間と受信した値が保存されます。

まとめ

比較的簡単にM5StickCを使うとGROVEセンサーの値をリアルタイムでグラフ化と保存が可能でした。

何も考えずに使っても25分は動きました。体に固定して、ワイヤレスでデータ取得するデバイスとしても使いやすそうですね。

IIJ、個人向けIoTデータ可視化・監視サービス「Machinist(マシニスト)」を提供開始

リリースがあったので、ちょっと使ってみた。

リリース

https://www.iij.ad.jp/news/pressrelease/2019/0711-2.html

概要

Ambientみたいな感じで、時系列データを定期的にアップすると保存してくれて、グラフ化してくれます。

https://machinist.iij.jp/getting-started/

上記のGetting Startedに使い方が書いてあります。

プラン

フリー(無料)

  • 10メトリック
  • 直近1ヶ月分のデータを保存

スタンダード(\680円/月)

  • 100メトリック
  • 最大6ヶ月分のデータを保存

680円でも半年分か。。。

データの制約

個人的にはどれぐらいの粒度で保存できるのかが気になります。

  • 1エージェントあたりの1分間のデータの送信可能回数は120回
  • 1メトリックあたりのデータの保存可能間隔は1分

データは1分間隔じゃないと保存されないみたいです。データを見た限り秒は保存されていないので、同一分にデータ保存すると上書きです。

一日のデータポイント数って上限じゃなくって、1分1データが何個までって制約ですね。

まとめ

似たようなサービスだとAmbientとMilkcocoa、AWS IoT Things Graph、Blynkでもグラフ化できますが、ちゃんと商用プランを用意してくれているのが少ないので、今後に期待です。

M5StickC(ESP32)でダイソーのBluetoothシャッターを操作

いろいろ試しましたが、普通の方法では使うことができませんでしたので、ちょっと黒魔術を使っています。

M5StickCで実験しましたが、コード的には単なるESP32です。

Arduino IDE版ESP32 1.0.2ライブラリの問題点

リセットがかかる

特定のデバイスでCharacteristicを取得しようとするとリセットがかかります!

結構検索して事例が出てきますが、なかなかライブラリが更新されません。

  • C:\Users\%USERNAME%\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.2\libraries\BLE\src\BLERemoteService.cpp

Windowsだと上記のファイルが原因です。

void BLERemoteService::retrieveCharacteristics() {
	log_v(">> getCharacteristics() for service: %s", getUUID().toString().c_str());

	removeCharacteristics(); // Forget any previous characteristics.

	uint16_t offset = 0;
	esp_gattc_char_elem_t result;
	while (true) {
		uint16_t count = 10;  // this value is used as in parameter that allows to search max 10 chars with the same uuid
		esp_gatt_status_t status = ::esp_ble_gattc_get_all_char(
			getClient()->getGattcIf(),
			getClient()->getConnId(),
			m_startHandle,
			m_endHandle,
			&amp;result,
			&amp;count,
			offset
		);

count = 10となっていますが、ここが1以外だと正しく取得できませんでいした。

https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/BLERemoteService.cpp

ESP32のBLEライブラリ作者の作業用Githubだとここが、1になっていました。ただし、ESP32にはこの更新は取り込まれていません。

このリポジトリもアーカイブ状態になっているので、今後どうなるんだろう?

同一CharacteristicUUIDがあると1つしか取得できない

これはCharacteristicUUIDをキーにしたMapでgetCharacteristics()で返却してくるので、同一UUIDがあると1つにまとめられてしまいます。

キーボード系デバイスだと同じUUIDが複数あったりするので、個別キーが取得できなくなります。

解決方法

ライブラリの書き換え

このリポジトリBLERemoteService.cppBLERemoteService.hを、ESP32のライブラリに上書きすれば動くようになります。

個人的にはライブラリには極力手を入れたくないので、他の解決方法も探しました。

ラッパー関数を作って無理やり修正する

エラーがでる箇所はわかっているので、そこの処理だけ書き換えた関数を作ってみました。しかしながらBLERemoteCharacteristicクラスのコンストラクタがPrivateなのです!

private:
	BLERemoteCharacteristic(uint16_t handle, BLEUUID uuid, esp_gatt_char_prop_t charProp, BLERemoteService* pRemoteService);
	friend class BLEClient;
	friend class BLERemoteService;
	friend class BLERemoteDescriptor;

フレンドクラスを指定しているので、BLERemoteServiceの内部からは呼べるのですが、自作クラスからは呼び出せません。

そこで、アクセス指定子の無効化を参考にして、黒魔術で乗り切ることにしました。

https://github.com/tanakamasayuki/M5StickC-examples/blob/master/BLE_clientEx/BLEDeviceEx.h

#ifndef __BLEDEVICE_EX_H__
#define __BLEDEVICE_EX_H__

// 内部関数などにアクセスするためにprivateを無効化する
#define private public
#include "BLEDevice.h"
#undef private

std::map<uint16_t, BLERemoteCharacteristic*>* retrieveCharacteristicsEx( BLERemoteService* pRemoteService ) {
  ESP_LOGD(LOG_TAG, ">> retrieveCharacteristics() for service: %s", getUUID().toString().c_str());

  pRemoteService->removeCharacteristics(); // Forget any previous characteristics.

  uint16_t offset = 0;
  esp_gattc_char_elem_t result;
  while (true) {
    uint16_t count = 1;  // this value is used as in parameter that allows to search max 10 chars with the same uuid
    esp_gatt_status_t status = ::esp_ble_gattc_get_all_char(
      pRemoteService->getClient()->getGattcIf(),
      pRemoteService->getClient()->getConnId(),
      pRemoteService->m_startHandle,
      pRemoteService->m_endHandle,
      &amp;result,
      &amp;count,
      offset
    );

    if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) {   // We have reached the end of the entries.
      break;
    }

    if (status != ESP_GATT_OK) {   // If we got an error, end.
      ESP_LOGE(LOG_TAG, "esp_ble_gattc_get_all_char: %s", BLEUtils::gattStatusToString(status).c_str());
      break;
    }

    if (count == 0) {   // If we failed to get any new records, end.
      break;
    }

    ESP_LOGD(LOG_TAG, "Found a characteristic: Handle: %d, UUID: %s", result.char_handle, BLEUUID(result.uuid).toString().c_str());

    // We now have a new characteristic ... let us add that to our set of known characteristics
    BLERemoteCharacteristic *pNewRemoteCharacteristic = new BLERemoteCharacteristic(
      result.char_handle,
      BLEUUID(result.uuid),
      result.properties,
      pRemoteService
    );

    pRemoteService->m_characteristicMap.insert(std::pair<std::string, BLERemoteCharacteristic*>(pNewRemoteCharacteristic->getUUID().toString(), pNewRemoteCharacteristic));
    pRemoteService->m_characteristicMapByHandle.insert(std::pair<uint16_t, BLERemoteCharacteristic*>(result.char_handle, pNewRemoteCharacteristic));
    offset++;   // Increment our count of number of descriptors found.
  } // Loop forever (until we break inside the loop).

  pRemoteService->m_haveCharacteristics = true; // Remember that we have received the characteristics.
  ESP_LOGD(LOG_TAG, "<< retrieveCharacteristics()");

  return &amp;pRemoteService->m_characteristicMapByHandle;
} // retrieveCharacteristicsEx

#endif

素晴らしい!

しかしながら、もちろん非推奨です。ライブラリを書き換えるのと、黒魔術だと同じぐらいグレーな気がします。

最新版のretrieveCharacteristics()を元に、privateに直接アクセスして同じような処理をしています。

複数UUID問題に関しては、内部にm_characteristicMapByHandleというMapがあるのですが、getCharacteristicsByHandle()が1.0.2だと実装されていないので、直接返却しています。

サンプルスケッチ

https://github.com/tanakamasayuki/M5StickC-examples/tree/master/BLE_clientEx

/**
   A BLE client example that is rich in capabilities.
   There is a lot new capabilities implemented.
   author unknown
   updated by chegewara
*/

#include "BLEDeviceEx.h"

// The remote service we wish to connect to.
static BLEUUID serviceUUID("1812");

static boolean doConnect = false;
static boolean connected = false;
static boolean doScan = false;
static BLEAdvertisedDevice* myDevice;

static void notifyCallback(
  BLERemoteCharacteristic* pBLERemoteCharacteristic,
  uint8_t* pData,
  size_t length,
  bool isNotify) {
  Serial.print("Notify callback for characteristic ");
  Serial.print(pBLERemoteCharacteristic->getUUID().toString().c_str());
  Serial.print(" of data length ");
  Serial.print(length);
  Serial.print(" data: ");
  for ( int i = 0 ; i < length ; i++ ) {
    Serial.printf( "%02X ", pData[i] );
  }

  Serial.println();
}

class MyClientCallback : public BLEClientCallbacks {
    void onConnect(BLEClient* pclient) {
    }

    void onDisconnect(BLEClient* pclient) {
      connected = false;
      Serial.println("onDisconnect");
    }
};

bool connectToServer() {
  Serial.print("Forming a connection to ");
  Serial.println(myDevice->getAddress().toString().c_str());

  BLEClient*  pClient  = BLEDevice::createClient();
  Serial.println(" - Created client");

  pClient->setClientCallbacks(new MyClientCallback());

  // Connect to the remove BLE Server.
  pClient->connect(myDevice);  // if you pass BLEAdvertisedDevice instead of address, it will be recognized type of peer device address (public or private)
  Serial.println(" - Connected to server");

  // Obtain a reference to the service we are after in the remote BLE server.
  BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
  if (pRemoteService == nullptr) {
    Serial.print("Failed to find our service UUID: ");
    Serial.println(serviceUUID.toString().c_str());
    pClient->disconnect();
    return false;
  }
  Serial.println(" - Found our service");

  std::map<uint16_t, BLERemoteCharacteristic*>* mapCharacteristics = retrieveCharacteristicsEx(pRemoteService);
  for (std::map<uint16_t, BLERemoteCharacteristic*>::iterator i = mapCharacteristics->begin(); i != mapCharacteristics->end(); ++i) {
    if (i->second->canNotify()) {
      Serial.println(" - Add Notify");
      i->second->registerForNotify(notifyCallback);
    }
  }

  connected = true;
  return true;
}
/**
   Scan for BLE servers and find the first one that advertises the service we are looking for.
*/
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    /**
        Called for each advertising BLE server.
    */
    void onResult(BLEAdvertisedDevice advertisedDevice) {
      Serial.print("BLE Advertised Device found: ");
      Serial.println(advertisedDevice.toString().c_str());

      // We have found a device, let us now see if it contains the service we are looking for.
      if (advertisedDevice.haveServiceUUID() &amp;&amp; advertisedDevice.isAdvertisingService(serviceUUID)) {

        BLEDevice::getScan()->stop();
        myDevice = new BLEAdvertisedDevice(advertisedDevice);
        doConnect = true;
        doScan = true;

      } // Found our server
    } // onResult
}; // MyAdvertisedDeviceCallbacks


void setup() {
  Serial.begin(115200);
  Serial.println("Starting Arduino BLE Client application...");
  BLEDevice::init("");

  // Retrieve a Scanner and set the callback we want to use to be informed when we
  // have detected a new device.  Specify that we want active scanning and start the
  // scan to run for 5 seconds.
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setInterval(1349);
  pBLEScan->setWindow(449);
  pBLEScan->setActiveScan(true);
  pBLEScan->start(5, false);
} // End of setup.


// This is the Arduino main loop function.
void loop() {

  // If the flag "doConnect" is true then we have scanned for and found the desired
  // BLE Server with which we wish to connect.  Now we connect to it.  Once we are
  // connected we set the connected flag to be true.
  if (doConnect == true) {
    if (connectToServer()) {
      Serial.println("We are now connected to the BLE Server.");
    } else {
      Serial.println("We have failed to connect to the server; there is nothin more we will do.");
    }
    doConnect = false;
  }

  // If we are connected to a peer BLE Server, update the characteristic each time we are reached
  // with the current time since boot.
  if (connected) {
  } else if (doScan) {
    BLEDevice::getScan()->start(0);  // this is just eample to start scan after disconnect, most likely there is better way to do it in arduino
  }

  delay(1000); // Delay a second between loops.
} // End of loop

ほぼSDKのスケッチのままですが、BLEDevice.hのかわりに黒魔術で汚染されたBLEDeviceEx.hを読み込んでいます。

動作例(ダイソーシャッターリモコン)

characteristic一覧

UUID役割機能
2a4aHID InformationBroadcast:X Read:O WriteNoResponse:X Write:X Notify:X Indicate:X
2a4bReport MapBroadcast:X Read:O WriteNoResponse:X Write:X Notify:X Indicate:X
2a4cHID Control PointBroadcast:X Read:X WriteNoResponse:O Write:X Notify:X Indicate:X
2a4dReportBroadcast:X Read:O WriteNoResponse:X Write:X Notify:O Indicate:X
2a4dReportBroadcast:X Read:O WriteNoResponse:X Write:X Notify:O Indicate:X
2a4eProtocol ModeBroadcast:X Read:O WriteNoResponse:O Write:X Notify:X Indicate:X

複数のReportがありますが、1.0.2のライブラリだと1つしか取得することができませんでした。このシャッター以外のリモコンも試してみたのですが、どっちはReportが6個もありました!

実行時ログ

BLE Advertised Device found: Name: AB Shutter3       , Address: ff:ff:c1:??:??:??, appearance: 961, serviceUUID: 00001812-0000-1000-8000-00805f9b34fb
Forming a connection to ff:ff:c1:??:??:??
 - Created client
 - Connected to server
 - Found our service
 - Add Notify
 - Add Notify
We are now connected to the BLE Server.
Notify callback for characteristic 00002a4d-0000-1000-8000-00805f9b34fb of data length 2 data: 01 00 
Notify callback for characteristic 00002a4d-0000-1000-8000-00805f9b34fb of data length 2 data: 00 00 
Notify callback for characteristic 00002a4d-0000-1000-8000-00805f9b34fb of data length 2 data: 00 28 
Notify callback for characteristic 00002a4d-0000-1000-8000-00805f9b34fb of data length 2 data: 01 00 
Notify callback for characteristic 00002a4d-0000-1000-8000-00805f9b34fb of data length 2 data: 00 00 
Notify callback for characteristic 00002a4d-0000-1000-8000-00805f9b34fb of data length 2 data: 00 00 

ダイソーのシャッターは「AB Shutter3」って名前で、HIDとして動いています。

2つNotifyが登録されていますので、2つのReportが正しく認識しているのがわかります。

iOSキー

Notify callback for characteristic 00002a4d-0000-1000-8000-00805f9b34fb of data length 2 data: 01 00 
Notify callback for characteristic 00002a4d-0000-1000-8000-00805f9b34fb of data length 2 data: 00 00 

2つあるキーのうち、iOSキーを押した場合、01 00(ボリュームアップ)と00 00(キーアップ)が飛んできます。

Androidキー

Notify callback for characteristic 00002a4d-0000-1000-8000-00805f9b34fb of data length 2 data: 00 28 
Notify callback for characteristic 00002a4d-0000-1000-8000-00805f9b34fb of data length 2 data: 01 00 
Notify callback for characteristic 00002a4d-0000-1000-8000-00805f9b34fb of data length 2 data: 00 00 
Notify callback for characteristic 00002a4d-0000-1000-8000-00805f9b34fb of data length 2 data: 00 00 

Androidキーを押した場合には、00 28(エンター)と01 00 (ボリュームアップ) と00 00(キーアップ)が2つ飛んできます。

エンターとボリュームアップは別のReportから飛んでくるので、pBLERemoteCharacteristic->getHandle()でどのHandleかを確かめることで、その区別もつきます。キーアップはHandleみないとどっちのキーがアップしたのかわからないですが、この機材の場合にはそこまで見なくても判定できそうです。

参考サイト

まとめ

ちょっとBluetoothは不安定なので、どこまで実用的に使えるかは微妙なところがあります。ブツブツ切れたり、わりとハングアップしたりとケアをしないといけないことが多そうです。

サンプルスケッチは今後なるべくGithubにも保存して公開していくつもりです。

M5StickCのButtonクラスを調べてみた

Homeボタンと右ボタン、そして電源ボタンの制御について調べてみました。

ボタンについて

ざっくりと上記で取得できます。

ホームボタンと右ボタンについて

M5.update()

ボタンの状態を更新する関数です。Buttonクラスを利用する場合にはloop()の先頭に近い場所で必ず実行するようにしましょう。

この関数を呼ばないとボタンの状態は更新されません。

isPressed()

今現在ボタンを押しているかを返却します。ボタンを押している間は常にTRUEが戻ってきます。

isReleased()

今現在ボタンを離しているかを返却します。ボタンを押していない間は常にTRUEが戻ってきます。

wasPressed()

ボタンを押してから最初に呼び出した時だけTRUEを返却します。1度しか状態を取得できないので注意して呼び出しましょう。

wasReleased()

ボタンを押して、離してから最初に呼び出した時だけTRUEを返却します。1度しか状態を取得できないので注意して呼び出しましょう。一般的にボタンを押した判定はこの関数の戻り値で判定したほうが自然だと思います。

pressedFor(ms)

ボタンをms以上押している場合にTRUEが返却されます。次のループでもTRUEを返却するので長押し処理で数値をカウントアップする場合などに利用すると良いと思います。

releasedFor(ms)

ボタンを離してからms以上経過している場合にTRUEが返却されます。ボタン連打防止などでこの関数で一定以上時間経過後にフラグ更新して、次の入力を受け付ける処理などに利用できると思います。

wasReleasefor(ms)

ms以上ボタンを押して、離してから最初に呼び出した時だけTRUEを返却します。1度しか状態を取得できないので注意して呼び出しましょう。

長押しを戻るボタンに設定した場合などに利用すると、便利だと思います。

lastChange()

最後にボタンの状態が変更された時の millis() の値が返却されます。現在のmillis()からの差分が経過時間になります。

電源ボタン

M5.Axp.GetBtnPress()

  • 戻り値1:電源ボタンを1秒以上押した場合
  • 戻り値2:電源ボタンを1秒未満押してから離した場合
  • 戻り値0:上記以外

この関数は0以外の数値は1度しか取得できないので注意してください。

また6秒以上電源ボタンを押すと、電源が切れるので長押しの操作はあまり適していません。

サンプルスケッチ

#include <M5StickC.h>

void setup() {
  M5.begin();
  M5.Lcd.setRotation(3); // 画面に入らないので横向きにする
}

void loop() {
  // Buttonクラスを利用するときには必ずUpdateを呼んで状態を更新する
  M5.update();

  // カーソル初期化
  M5.Lcd.setCursor(0, 0);

  // ホームボタンが現在押されているか?
  M5.Lcd.print("BtnA.isPressed():");
  M5.Lcd.println( M5.BtnA.isPressed() );

  // ホームボタンが現在離しているか?
  M5.Lcd.print("BtnA.isReleased():");
  M5.Lcd.println( M5.BtnA.isReleased() );

  // ホームボタンを押したか?(1度だけ取得可能)
  if ( M5.BtnA.wasPressed() ) {
    Serial.println("BtnA.wasPressed() == TRUE");
  }

  // ホームボタンを離したか?(1度だけ取得可能)
  if ( M5.BtnA.wasReleased() ) {
    Serial.println("BtnA.wasReleased() == TRUE");
  }

  // ホームボタンを現在ms以上押しているか?
  if ( M5.BtnA.pressedFor(1000) ) {
    Serial.println("BtnA.pressedFor(1000) == TRUE");
  }

  // ホームボタンを離してからms以上経過しているか?
  M5.Lcd.print("BtnA.releasedFor(1000):");
  M5.Lcd.println( M5.BtnA.releasedFor(1000) );

  // ホームボタンをms以上押してから離したか?(1度だけ取得可能)
  if ( M5.BtnA.wasReleasefor(1000) ) {
    Serial.println("BtnA.wasReleasefor(1000) == TRUE");
  }

  // ホームボタンが最後に更新した起動経過時間 millis()
  M5.Lcd.print("BtnA.lastChange():");
  M5.Lcd.println( M5.BtnA.lastChange() );

  // 空行を追加
  M5.Lcd.println();

  // 右ボタンが現在押されているか?
  M5.Lcd.print("BtnB.isPressed():");
  M5.Lcd.println( M5.BtnB.isPressed() );

  // 右ボタンが現在離しているか?
  M5.Lcd.print("BtnB.isReleased():");
  M5.Lcd.println( M5.BtnB.isReleased() );

  // 右ボタンを押したか?(1度だけ取得可能)
  if ( M5.BtnB.wasPressed() ) {
    Serial.println("BtnB.wasPressed() == TRUE");
  }

  // 右ボタンを離したか?(1度だけ取得可能)
  if ( M5.BtnB.wasReleased() ) {
    Serial.println("BtnB.wasReleased() == TRUE");
  }

  // 右ボタンを現在ms以上押しているか?
  if ( M5.BtnB.pressedFor(1000) ) {
    Serial.println("BtnB.pressedFor(1000) == TRUE");
  }

  // 右ボタンを離してからms以上経過しているか?
  M5.Lcd.print("BtnB.releasedFor(1000):");
  M5.Lcd.println( M5.BtnB.releasedFor(1000) );

  // 右ボタンをms以上押してから離したか?(1度だけ取得可能)
  if ( M5.BtnB.wasReleasefor(1000) ) {
    Serial.println("BtnB.wasReleasefor(1000) == TRUE");
  }

  // 右ボタンが最後に更新した起動経過時間 millis()
  M5.Lcd.print("BtnB.lastChange():");
  M5.Lcd.println( M5.BtnB.lastChange() );

  // 電源ボタンの状態取得(一度しか0以外のステータスは取得できない)
  int axpButton = M5.Axp.GetBtnPress();
  if ( axpButton == 1 ) {
    // 1秒以上電源ボタンを押している
    Serial.println("M5.Axp.GetBtnPress() == 1");
  }
  if ( axpButton == 2 ) {
    // 1秒未満電源ボタンを押して離した
    Serial.println("M5.Axp.GetBtnPress() == 2");
  }
}

まとめ

電源ボタンも利用できるようになったので、ボタンの利用用途が広がりますね。ただ取得できるタイミングが違うので、完全に同じように使うことはできないみたいです。

普通のボタンクリックは wasReleased() と axpButton == 2 が同じような動きになりますが、電源の長押しは1秒以上押したら1度だけ1が返却されるので、押しっぱなしかどうかがわからないんですよね。

6秒以上電源ボタン押していると電源切れるので、長押しは電源ボタンにはあまり割り振らないほうがいいですね。

M5StickC 0.0.7のAXP192追加関数を調べてみた

Githubのソースは確認していましたが、リリースされたSDKで動作検証もしてみました。

最新版の詳細はM5StickC日本語リファレンスで確認してください。

追加関数

内部動作電圧取得 GetVapsData()

内部の動作している電圧を取得する関数です。あまり使うことはないと思いますが、この電圧が下がってくると、バッテリー残量少ないです。

電源ボタン取得 GetBtnPress()

電源ボタンの状態が取れるようになりました!
これで3ボタン使えるので、リモコン系を作るときには便利ですね。

ただし、ボタンの状態じゃなくって、1秒以下のクリックか、1秒以上ボタンを押していたってイベントだけ取得できます。

低電圧状態チェック GetWarningLeve()

APS電圧が約3.4V以下になっていると1が戻ってきます。低電圧になったら警告を出すなどの場合に利用します。

スリープ状態に移行 SetSleep()

CPUのスリープじゃなくって、画面とか外部装置のスリープです。電源ボタンを1秒以下でクリックすると復帰します。

未操作が続いたら画面を消すとかに使えるのかな?

ライトスリープ状態に移行 LightSleep()

内部でSetSleep()で外部装置をスリープしてからESP32のライトスリープ状態に入ります。タイマーをセットするとその秒数経過すると復帰します。

ただし、SetSleep()で電源ボタンを押すと画面だけ復帰して、CPUはスリープ状態のままなので、操作する人が混乱しないように設計したほうがいいと思います。

ディープスリープ状態に移行 DeepSleep()

こちらも、内部でSetSleep()で外部装置をスリープしてからESP32のディープスリープ状態に移行します。

こちらも電源ボタンを押すと画面が復帰しますが、ディープスリープタイマーが来ると再起動するので、使い方に気をつけましょう。

まとめ

使えるボタンが増えたのが非常に嬉しいです。日本語リファレンスはちょっとずつ更新していますが、まだまだ足りない項目がたくさん残っています。

M5StickCでBLEデバイスを検索する

BLEを使ってみようとしましたが、まずはデバイスのUUIDを調べないといけないので、調べるツールを作ってみました。

BLEとは?

Bluetooth 4.0で規定されているBluetooth Low Energyです。Bluetooth LEとも表現されることがあるみたいです。

3.0まではClassicとかSimpleとかでよばれていることが多いみたいです。

BLEデバイスの構造

デバイスアドレス

6バイトで前半3バイトが製造会社のIDで、後半3バイトが任意に使えて製造番号的なものが入っています。 11:22:33:44:55:66みたいな形で表現されていることが多いです。

この番号は重複しないので、同じデバイスを複数持っていてもすべて違う番号になっています。

デバイス名

こちらの文字列でもデバイスの識別ができます。ただしすべてのデバイスでデバイス名が定義されているわけではないようです。

サービスUUID

マウスだったり、キーボードだったり特定の機能を表すサービスのIDです。複数の機能がある場合には、複数のサービスが登録されています。

サービスを識別するのがサービスUUIDになります。一般的には有名なサービスは定義済みになっており、この中のサービスを提供していることとなります。

定義済みサービスUUID

https://www.bluetooth.com/ja-jp/specifications/gatt/services/

キャラクタリスティックUUID

サービスの中にさらにキャラクタリスティックがあります。こちらは実際の値を保存しているもので、書き込みや読み込みなどの実際の操作を行うことができる機能のUUIDです。

バッテリー残量だったり、キーボードなどの入力値などの機能です。こちらも有名なものは定義済みになっています。

定義済みキャラクタリスティックUUID

https://www.bluetooth.com/ja-jp/specifications/gatt/characteristics/

サンプルスケッチ

#include "BLEDevice.h"

// 検索するBLEデバイス。serviceUUIDを調べる場合には空にする(例はHuman Interface Device"00001812-0000-1000-8000-00805f9b34fb")
static BLEUUID serviceUUID("1812");

static BLEAdvertisedDevice* myDevice;

// 接続してCharacteristic一覧を取得
bool connectToServer() {
  Serial.print("接続先 : ");
  Serial.println(myDevice->getAddress().toString().c_str());
  BLEClient*  pClient  = BLEDevice::createClient();
  pClient->connect(myDevice);

  // サービス取得
  BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
  if (pRemoteService == nullptr) {
    pClient->disconnect();
    return false;
  }

  // Characteristic一覧
  Serial.println("characteristic一覧");
  std::map<std::string, BLERemoteCharacteristic*>* mapCharacteristics = pRemoteService->getCharacteristics();
  for (std::map<std::string, BLERemoteCharacteristic*>::iterator i = mapCharacteristics->begin(); i != mapCharacteristics->end(); ++i) {
    Serial.print(" - characteristic UUID : ");
    Serial.print(i->first.c_str());
    Serial.print(" Broadcast:");
    Serial.print(i->second->canBroadcast()?'O':'X');
    Serial.print(" Read:");
    Serial.print(i->second->canRead()?'O':'X');
    Serial.print(" WriteNoResponse:");
    Serial.print(i->second->canWriteNoResponse()?'O':'X');
    Serial.print(" Write:");
    Serial.print(i->second->canWrite()?'O':'X');
    Serial.print(" Notify:");
    Serial.print(i->second->canNotify()?'O':'X');
    Serial.print(" Indicate:");
    Serial.print(i->second->canIndicate()?'O':'X');
    Serial.println();
  }

  // stop
  Serial.println("プログラム停止!");
  while (1) delay(1000);
}

// 検索したデバイスを受信するコールバック関数
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
      Serial.print("BLE デバイス発見 : ");
      Serial.println(advertisedDevice.toString().c_str());

      if (advertisedDevice.haveServiceUUID() &amp;&amp; advertisedDevice.isAdvertisingService(serviceUUID)) {
        // 指定デバイスだったら接続する
        BLEDevice::getScan()->stop();
        myDevice = new BLEAdvertisedDevice(advertisedDevice);
      }
    }
};

void setup() {
  Serial.begin(115200);
  Serial.println("BLEデバイス検索開始...");
  BLEDevice::init("");

  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setInterval(1349);
  pBLEScan->setWindow(449);
  pBLEScan->setActiveScan(true);
  pBLEScan->start(5, false);
}

void loop() {
  if (myDevice != NULL) {
    connectToServer();
  }

  delay(1000);
}

最初はserviceUUIDを空にして実行すると、待受中のBLEデバイスすべてを列挙することができます。

その後にCharacteristicを取得したいserviceUUIDを指定することで、そのserviceUUIDが持つCharacteristicを列挙します。

ただし、同じ種類のデバイスが複数待受中になっている場合は、serviceUUIDだけで接続してしまうと、違うデバイスに接続しにいってしまう場合があるので、その場合にはアドレスやデバイス名などを条件に加えてください。

実行結果例

BLEデバイス検索開始...
BLE デバイス発見 : Name: AB Shutter3       , Address: ff:ff:c1:??:??:??, appearance: 961, serviceUUID: 00001812-0000-1000-8000-00805f9b34fb
接続先 : ff:ff:c1:??:??:??
characteristic一覧
[E][BLERemoteCharacteristic.cpp:273] retrieveDescriptors(): esp_ble_gattc_get_all_descr: Unknown
[E][BLERemoteCharacteristic.cpp:273] retrieveDescriptors(): esp_ble_gattc_get_all_descr: Unknown
[E][BLERemoteCharacteristic.cpp:273] retrieveDescriptors(): esp_ble_gattc_get_all_descr: Unknown
[E][BLERemoteCharacteristic.cpp:273] retrieveDescriptors(): esp_ble_gattc_get_all_descr: Unknown
 - characteristic UUID : 00002a4a-0000-1000-8000-00805f9b34fb Broadcast:X Read:O WriteNoResponse:X Write:X Notify:X Indicate:X
 - characteristic UUID : 00002a4b-0000-1000-8000-00805f9b34fb Broadcast:X Read:O WriteNoResponse:X Write:X Notify:X Indicate:X
 - characteristic UUID : 00002a4c-0000-1000-8000-00805f9b34fb Broadcast:X Read:X WriteNoResponse:O Write:X Notify:X Indicate:X
 - characteristic UUID : 00002a4d-0000-1000-8000-00805f9b34fb Broadcast:X Read:O WriteNoResponse:X Write:X Notify:O Indicate:X
 - characteristic UUID : 00002a4e-0000-1000-8000-00805f9b34fb Broadcast:X Read:O WriteNoResponse:O Write:X Notify:X Indicate:X
プログラム停止!

ダイソーのスマホ用リモートシャッターを調べたものです。コンパイルオプションでエラーを表示するようにしているので、エラーが4行見えます。

この結果を見ると定義済みのcharacteristicが5つ見えます。

  • 2a4a : HID Information
  • 2a4b : Report Map
  • 2a4c : HID Control Point
  • 2a4d : Report
  • 2a4e : Protocol Mode

ただ、おそらくこのデバイスはcharacteristicの構造がおかしいようでして、ただしく利用することができませんでした。

iPhoneでcharacteristicを調べるアプリ「GATTBrowser」を使っても「180a : PnP ID」と「180f : Battery Level」の2つしか見えませんでした。

検索して、このデバイスに接続してcharacteristicを取得すると、端末にリセットがかかるので、標準ライブラリだと利用することができないと思います。

検索しても同じようにリセットがかかっている人が多いので、接続先デバイスによってESP32からBLE接続をすると本体のリセットがかかって、接続できないようです。

他のデバイスで接続する実験を行ってみたいと思います。

ESP32-WROOM-32Uの技適更新?

更新通知が来たけれどなんだろう?

該当の技適

https://lang-ship.com/giteki/detail.php?number=211-171103

んー、写真が無かったのが追加されているのと社名が「Espressif Systems (Shanghai) PTE Ltd.」から「Espressif Systems (Shanghai) Co., Ltd.」になっている?

まあ、該当モジュールは外部アンテナバージョンで、申請内容に違いはないのできにしないようにします。

M5StickCでSPI通信をする

本体に内蔵しているのと同じSPI接続のST7735Sに160×80ピクセルの0.96インチOLEDを接続してみました。

SPIとは?

3線+αの信号線を使って通信する通信方式です。I2Cに比べて高速通信が可能で、M5StickCでは画面表示などに利用しています。

ArduinoMaster側Slave側別名用途
SCKSCKSCKSCLK, SCLデータ送信のクロックをマスターが送信する
MISOSDISDODC, D/Cマスターの受信、スレーブの送信をする
MOSISDOSDISDAマスターの送信、スレーブの受信をする
SSSSCS特定スレーブのCSを0Vにすることで通信先を選択する

基本は3線で通信を行い、複数の通信先(スレーブ)がいる場合には、マスター側が複数のSS用PINを操作して、通信先のCSを0Vにすることで通信先を選択する。

相手先が1つだけの場合には、配線の段階でCSをGNDに接続することで常に選択されている状態となる。

名称は接続先のデバイスによって命名が様々なので、接続先に注意する必要がある。またSPI以外の通信線も接続する必要があるデバイスもあるので混乱しやすい。

SPIサポート

M5StickCでは最大4系統のSPI通信をサポートしています。

  • SPI0 : 内部Flashへの接続に利用済み
  • SPI1 : 内部Flashへの接続に利用済み
  • HSPI : 未使用
  • VSPI : 内蔵画面への接続に利用済み

M5StickCで追加で利用できるSPIは1系統のみになります。ただし、そもそも4線しか使えるピンが無いので外部に2系統使うことはできません。

接続デバイス

手元にあったST7735Sと接続してみました。M5StickCに内蔵しているのと同じスペックの画面ですが、内蔵クラスはPIN番号などが固定化されているので、そのまま使えませんでした。

ST7735Sのピン配置

ST7735M5StickC内容
GNDGND共通GND
VCC3V3電源
SCL0SCK
SDA32MOSI
RES33リセット
DC26MISO
CSGNDこのデバイスを選択
BLK未接続バックライト制御

上記の接続にしました。CSはGNDに直結したので3線でSPI自体は通信できますが、画面のリセット用の1線必要になっています。

接続例

Groveはメス端子は販売していますが、ケーブルは自作できないので、この手の接続をするときにはきれいに接続するのは難しいかもしれません。

サンプルスケッチ

#include <M5StickC.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <SPI.h>

Adafruit_ST7735 tft = Adafruit_ST7735(-1, 0, 26, 32, 33); // cs, dc, mosi, sclk, rst

uint16_t color = 0;

void setup() {
  M5.begin();

  tft.initR(INITR_MINI160x80);
}

void loop() {
  tft.fillScreen(color);
  M5.Lcd.fillScreen(color);
  color += 0x0100;
}

最低限のコードだけに削っているものになります。AdafruitとM5StickCでcolorの型が違うので、違う色が表示されますが、画面の色が塗り替わっているまでは確認できました。

Adafruit ST7735とその親クラスにあたるAdafruit GFXを利用させていただいています。

まとめ

M5StickCでSPI通信は使えました。しかし自由に使えるPINが4線しかないので、接続とかがいろいろ大変そうですね。