Digi-Keyで注文してみた

概要

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

Digi-Keyとは?

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

アカウント登録

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

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

送料について

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

在庫について

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

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

発送について

  • FedEx
  • UPS

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

注文から配達まで

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

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

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

まとめ

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

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

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

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

概要

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

追加モジュール

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

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

__main__

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

hardware

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

削除モジュール

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

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

  • errno
  • json
  • random
  • select
  • ssl

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

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

まとめ

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

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

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

概要

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

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

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

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

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

RAMの種類

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

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

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

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

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

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

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

メモリ用途の種類

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

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

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

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

メモリ状況表示

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

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

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

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

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

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

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

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

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

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

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

void loop() {
}

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

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

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

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

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

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

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

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

まとめ

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

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

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

概要

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

LCD1602とは?

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

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

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

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

接続方式について

パラレル接続

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

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

I2C接続

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

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

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

接続方法

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

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

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

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

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

M5StickCLCD
GNDGND
5V OutVCC
GPIO0SDA
GPIO26SCL

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

I2Cアドレス検索

#include <Wire.h>

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

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

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

    delay(10);
  }

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

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

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

  Wire.begin(0, 26);

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

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

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

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

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

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

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

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

シンプルスケッチ

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

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

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

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

  delay(111);
}

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

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

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

  Wire.begin(0, 26);

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

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

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

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

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

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

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

液晶の明るさ調整

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

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

他の選択肢

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

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

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

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

まとめ

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

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

ESP32でLINX for LabVIEW入門 その3 PWM

概要

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

PWMとは?

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

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

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

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

スケッチ

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

LinxESP32* LinxDevice;

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

  LinxDevice = new LinxESP32();

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

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

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

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

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

  delay(1);
}

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

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

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

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

LINX – PWM 1 Channel.vi

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

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

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

LINX – PWM N Channel.vi

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

処理もシンプルです。

LINX – Digital Write Square Wave.vi

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

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

LINX – Servo 1 Channel.vi

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

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

まとめ

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

ESP32ハードウエア乱数生成

概要

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

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

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

原文

24. Random Number Generator

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

24.2 Feature
It can generate true random numbers.

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

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

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

DeepL翻訳

24. 乱数発生器

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

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

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

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

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

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

解説

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

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

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

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

スケッチ例

#include <WiFi.h>

#ifndef DR_REG_RNG_BASE
#define DR_REG_RNG_BASE  0x3ff75144
#endif

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

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

  WiFi.begin();
}

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

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

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

まとめ

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

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

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

概要

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

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

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

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

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

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

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

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

接続方法

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

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

ライブラリ準備

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

スケッチ例を開く(IRrecvDumpV3)

#include "BaseOTA.h"

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

const uint16_t kRecvPin = 26;

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

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

const uint16_t kMinUnknownSize = 12;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

受信最小スケッチ例

#include <IRrecv.h>

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

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

  irrecv.enableIRIn();
}

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

  delay(1);
}

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

赤外線送信について

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

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

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

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

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

赤外線送受信ユニット

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

まとめ

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

ESP32でLINX for LabVIEW入門 その2 GPIO

概要

前回は環境構築を行いました。今回は基本的なGPIOを使ったデジタル入出力とアナログ入力を確認したいと思います。

スケッチ

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

LinxESP32* LinxDevice;

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

  LinxDevice = new LinxESP32();

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

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

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

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

  delay(1);
}

今回はBluetoothSerial接続をするので、スケッチ例のBluetoothSerialを使ってください。

上記はM5StickCを使ってWi-FiとBluetoothSerialの両方で待ち受けする場合のスケッチです。スケッチのサイズが標準サイズを超えてしまったので、No OTAを選択してビルドしています。まだちょっと動作が不安定なので、電源ボタンを押すとリセットするコードになっています。

デジタル出力

LINX – Blink (Simple).vi

右側のLED Controlのボタンを押すことでLチカすることができます。シンプルってのは手動って意味みたいです。

動きはシンプルですね。LED ValueでHIGHかLOWを指定しているだけです。

LINX – Blink (Advanced).vi

アドバンストは自動でLチカします。こちらが一般的なLチカですね。

ブリンクのところは、TでTrueを与えて、Not回路で反転しているようです。左右の上下矢印がShift Registerという機能で、変数的なものかな?

全体ループの中にタイマーが入っているので、一定時間でループしているようです。

LINX – Digital Write N Channels.vi

わかりにくい画面なんですが、縦に4つ並んでいるのが各チャンネルの指定で、左側の数値が何個無効にするかを指定します。0を指定すると4チャンネルで、1を指定すると3チャンネル有効になります。

中身は1チャンネルと同じですね。

デジタル入力

LINX – Digital Read 1 Channel.vi

GPIOを指定して、状態が表示されます。シンプルですね。

こちらも特別なことは指定なさそうです。

LINX – Digital Read N Channels.vi

こちらはなぜか3チャンネル分しかありません。左側のが無効にする項目数は同じです。この場合全3チャンネル中1チャンネル無効にしているので、2チャンネル有効になっています。

M5StickCのボタンはプルアップされているので、通常は右側のLED表示が光っていますが、ボタンを押すと、消灯します。

処理はシンプルですね。

アナログ入力

かんたんにグラフにプロットできます。ただしESP32は無線を使うとADC2に接続されたGPIOはアナログ入力ができなくなるので注意してください。

処理もシンプルですね。

LINX – Analog Read N Channels.vi

複数のアナログ入力もかんたんにグラフ化できます。ただし無効設定したものがGPIO0のデータを取得してしまっているように見えます、、、

これはLINX側のサンプルAI側がいけないのかな?

こちらも処理は単純ですね。

Control 1 Type Def?

このチャンネル設定をしている場所を開くと、この画面がでてきます。いろいろ編集できそうですが、このブログでは深入りしないことにします。

プログラミングを楽しもうを読んでみた

前回紹介した無料の電子書籍を読んでみました。中学生向けと書いてありますが、ちょっと難易度が高い気がします、、、

書籍の中でシフトレジスタや、Type Defも説明されていました。ただ、LabVIEWはまわりに経験者がいないとなかなか使い方が難しいツールな気がします。

まとめ

基本的なGPIOの動作を確認することができました。PWMやDACなどの特殊な出力は次回以降に触ってみたいと思います。

続編

ESP32のArduino Core analogRead()関数検証

概要

前回アナログ入力の内部調査して、すこし気になるところがあったので、いろいろ設定を変えながら調べてみました。

減衰

ESP32は1.1Vを基準電圧として、12ビットのADCを行っています。そのため減衰器を使って入力電圧を下げてから測定をしています。

減衰減衰率減衰比減衰前減衰後
ADC_11db0.281838293約1 : 3.63.3 V0.93 V
ADC_6db0.501187234約1 : 21.8 V0.90 V
ADC_2_5db0.749894209約1 : 1.341.2 V0.90 V
ADC_0db11 : 10.9 V0.90 V

減衰器は4種類あり、減衰しない0db、約75%にする-2.5db、約50%にする-6db、約28%にする-11dbがあります。標準は-11dbになっており、3.6Vを入力してもADCには1Vしか渡さなくなっています。

0Vから1.1VまでADCは測定できるはずですが、実際には補正がされており0Vから0.9Vぐらいまでの範囲を測定しているようです。そのため3.3Vを入力した場合、最大値の4095になりますので3.6Vの入力でも最大値のままです。

右側の2列が0.9Vぐらいになる場合の想定電圧です。減衰を変えながら何ボルトでADCの値が4095になるのかを実機にて確認してみました。

利用機材

ESP32は手持ちのM5StickCを使いましたが、標準的なESP32でもそれほど変わらない動きだと思います。2018年以前に製造されたESP32の場合にはADCの補正が若干違うとデータシートに書いてありました。

安定化電源としてZK-DP3Dを利用しました。この機材はUSBから電圧と電流の制御が可能な小型電源です。非常に便利ですが最低電圧が1Vでしたので、一部の実験ができませんでした。

スケッチ

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

  pinMode(26, ANALOG);
  analogSetAttenuation(ADC_11db);
}

void loop() {
  Serial.println(analogRead(26));
  delay(10);
}

引数のdbを変えながら、電圧を変えながら出力が安定して4095になる電圧を計測しました。個体差があり、回数も1度だけですので参考程度にしてください。

結果

減衰4095になった電圧
ADC_11db3.13 V
ADC_6db1.83 V
ADC_2_5db1.31 V
ADC_0db

減衰なしの場合には、機材の関係で1V未満の電圧を設定できないので計測できませんでした。手元の環境だと-11dbの標準設定では3.13Vで最大値になってしまいました。手元の安いテスターで確認したところ3.12Vでしたのでそれほどずれてはいないはずです。

事前予測との差を考えると、やはり減衰後に0.9Vぐらいの場合に4095になるのではないかと推測することができます。

サイクル数

ADCの測定時間は、サイクル数で指定しています。CPUサイクルなのか周辺機器サイクルなのかを調べてみました。

Arduino Coreの標準値は8ですが、ESP-IDFは初期値の9になっています。

スケッチ

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

  pinMode(36, ANALOG); // ADC1
  pinMode(26, ANALOG); // ADC2

  uint8_t cpu_list[] = {240, 160, 80, 40, 20, 10};

  for (int cpu = 0; cpu < sizeof(cpu_list) ; cpu++) {
    setCpuFrequencyMhz(cpu_list[cpu]);
    for (int cycles = 7; cycles < 10; cycles++) {
      analogSetCycles(cycles);

      // ADC1
      unsigned long start_time = millis();
      for (int i = 0; i < 10000; i++) {
        analogRead(36);
      }
      unsigned long adc1_time = millis() - start_time;
      Serial.printf("CPU = %d, Cycles = %d, adc1_time = %lu\n", cpu_list[cpu], cycles, adc1_time);

      // ADC2
      start_time = millis();
      for (int i = 0; i < 10000; i++) {
        analogRead(26);
      }
      unsigned long adc2_time = millis() - start_time;
      Serial.printf("CPU = %d, Cycles = %d, adc2_time = %lu\n", cpu_list[cpu], cycles, adc2_time);
    }
  }
}

void loop() {
}

ADC1とADC2で内部処理が異なるので、別に確認しています。ループで1万回アナログ入力を行い、経過時間を測定しました。

結果

CPU周波数サイクルADC1(ミリ秒)ADC2(ミリ秒)
240796113
2408101117
2409101119
1607103121
1608103121
1609103121
807120133
808120133
809120134
407158175
408167176
409167175
207253278
208254278
209253279
107524546
108526546
109524546

CPU速度に応じて、経過時間が変化しているのでCPUクロック数で測定しているようです。また、ADC1とADC2では、ADC1の方が高速で取得可能でした。これはADC2は各種設定値を測定のたびに設定しないといけないオーバーヘッドがあるためと思われます。

サンプル数

複数回ADCの値を取得して、誤差をへらす機能があります。標準は1回で、複数回取得を行いません。ただし、データシートで公開されていない機能で、次期Arduino Coreでは廃止予定になります。

スケッチ

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

  pinMode(36, ANALOG); // ADC1
  pinMode(26, ANALOG); // ADC2

  for (int samples = 1; samples <= 3; samples++) {
    analogSetSamples(samples);

    // ADC1
    unsigned long start_time = millis();
    for (int i = 0; i < 10000; i++) {
      analogRead(36);
    }
    unsigned long adc1_time = millis() - start_time;
    Serial.printf("Samples = %d, adc1_time = %lu\n", samples, adc1_time);

    // ADC2
    start_time = millis();
    for (int i = 0; i < 10000; i++) {
      analogRead(26);
    }
    unsigned long adc2_time = millis() - start_time;
    Serial.printf("Samples = %d, adc2_time = %lu\n", samples, adc2_time);
  }
}

void loop() {
}

CPUの変更をなくし、デフォルトの1回から3回まで変更した場合の時間を調べました。

結果

サンプルADC1(ミリ秒)ADC2(ミリ秒)
1100118
2127144
3153171

サンプル数を増やすと時間はかかっていますが、思ったより増えませんでした。実際にADCを読み込んでいる時間よりも、analogRead()関数で各種ADCの設定をやっている処理がオーバーヘッドになっている可能性があります。

分周

サイクルでCPUのクロックサイクル数を使ってADCの測定をしていましたが、分周設定もあり、何クロックを1サイクルにするのかを指定することができます。Arduino Coreでは1が標準ですが、ESP-IDFでは無指定でESP32の標準値の2になっています。

スケッチ

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

  pinMode(36, ANALOG); // ADC1
  pinMode(26, ANALOG); // ADC2

  for (int div = 1; div <= 3; div++) {
    analogSetClockDiv(div);

    // ADC1
    unsigned long start_time = millis();
    for (int i = 0; i < 10000; i++) {
      analogRead(36);
    }
    unsigned long adc1_time = millis() - start_time;
    Serial.printf("div = %d, adc1_time = %lu\n", div, adc1_time);

    // ADC2
    start_time = millis();
    for (int i = 0; i < 10000; i++) {
      analogRead(26);
    }
    unsigned long adc2_time = millis() - start_time;
    Serial.printf("div = %d, adc2_time = %lu\n", div, adc2_time);
  }
}

void loop() {
}

こちらも1から3まで変化させてみました。

結果

分周ADC1(ミリ秒)ADC2(ミリ秒)
1100117
2127145
3155172

ほぼ、サンプルと同じ結果になっています。

ビットレート

標準は12ビットですが、8ビットから12ビットまで選択することができます。

スケッチ

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

  pinMode(36, ANALOG); // ADC1
  pinMode(26, ANALOG); // ADC2

  for (int width = 8; width <= 12; width++) {
    analogSetWidth(width);

    // ADC1
    unsigned long start_time = millis();
    for (int i = 0; i < 10000; i++) {
      analogRead(36);
    }
    unsigned long adc1_time = millis() - start_time;
    Serial.printf("width = %d, adc1_time = %lu\n", width, adc1_time);

    // ADC2
    start_time = millis();
    for (int i = 0; i < 10000; i++) {
      analogRead(26);
    }
    unsigned long adc2_time = millis() - start_time;
    Serial.printf("width = %d, adc2_time = %lu\n", width, adc2_time);
  }
}

void loop() {
}

8ビットから12ビットまで調べました。

結果

ビットADC1(ミリ秒)ADC2(ミリ秒)
895114
995114
1095113
1197114
12100117

結果は変わらないと思っていたのですが、ビット数が少ないほうが早かったです。データ転送時間が減るためでしょうか?

Arduino CoreとESP-IDFの差

設定値Cycles分周
Arduino Core81
ESP-IDF92

Arduino CoreはESP-IDFと違う設定値を使っている場所がありました。ESP-IDFはESP32の標準値ですので、Arduino Coreだけ独自設定値になっています。

Arduino Coreの次期バージョンではサイクル数の指定関数がなくなるので、デフォルト値が8から9に変更される予定です。

厳密な速度差ではないですが、パラメータの差でどれぐらいの速度差がでるのかを測定してみました。

スケッチ

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

  pinMode(36, ANALOG); // ADC1
  pinMode(26, ANALOG); // ADC2

  for (int type = 0; type < 3; type++) {
    if (type == 0) {
      analogSetCycles(8);
      analogSetClockDiv(1);
    } else if (type == 1) {
      analogSetCycles(9);
      analogSetClockDiv(1);
    } else if (type == 2) {
      analogSetCycles(9);
      analogSetClockDiv(2);
    }

    // ADC1
    unsigned long start_time = millis();
    for (int i = 0; i < 10000; i++) {
      analogRead(36);
    }
    unsigned long adc1_time = millis() - start_time;
    Serial.printf("type = %d, adc1_time = %lu\n", type, adc1_time);

    // ADC2
    start_time = millis();
    for (int i = 0; i < 10000; i++) {
      analogRead(26);
    }
    unsigned long adc2_time = millis() - start_time;
    Serial.printf("type = %d, adc2_time = %lu\n", type, adc2_time);
  }
}

void loop() {
}

現状のArduino Core1.0.4と次期バージョン、ESP-IDFの標準値の3種類測定しています。

結果

設定値Cycles分周ADC1(ミリ秒)ADC2(ミリ秒)
Arduino Core(1.0.4)81100117
Arduino Core(次期)91101120
ESP-IDF(ESP32標準)92130146

次期バージョンでは若干ADCの速度が遅くなりそうです。ただしESP32の標準値より早い設定にしているのが気になります。今回はADCの精度を測定できていないですが、時間を短くすると精度に影響を与えるはずです。

まとめ

ADCはいろいろな関数があったのですが、やっと中身を理解することができました。ただし次期バージョンからはESP-IDFの関数を呼び出す形に変更が加えられていたので、すっきりはしますが解析が面倒になります。

ただESP-IDFのソースと見比べると、Arduino Coreは排他制御を一切していないので、次期バージョンはESP-IDFの関数にのせ替えるんだと思います。

もしかしたらADC1とADC2は並行で動かすことができるかもしれませんが、次期バージョンからは排他制御が入って同時実行ができなくなっているようです。

追加資料

この記事は結構前に書いていたので、上記の資料を参考にしていないのですがADCは減衰後で0.1V以上から測定しているみたいです。なので0.1Vから0.9Vの範囲になりそうです。両端は0と4095になるべくしたいので、若干甘めに設定していると思います。

M5StickC(ESP32)による「ELEGOO Arduino用UNO R3スターターキット」を利用したArduino入門 その10 アナログジョイスティックモジュール

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

概要

前回は温湿度センサーでした。今回はアナログジョイスティックになります。

アナログジョイスティックとは?

秋月さんだと上記のような商品になります。PS2以降に搭載されているジョイスティックなどと同じような操作になります。スティックを左右、上下に動かすことができて、指を離すと中央に戻ります。スティック自体を押し込むことでボタンとしても操作可能です。

構造的には可変抵抗がX軸とY軸で2つはいっており、動かすことで抵抗値がかわり、結果として信号ピンに電圧が変化して測定可能です。スティックの押し込みに関しては、押し込むことでスイッチ用の信号線がGNDと接触して、信号が落ちます。そのためスティックの押し込みボタンを使う場合にはプルアップされている必要があります。

接続方法

図で使っているジョイスティックは、キットに付属していたのとピン配置が違っているので注意してください。

ジョイスティック端子用途M5StickC
GNDGNDGND
+5V基準電圧3V3
VRxX軸電圧GPIO26
VRyY軸電圧GPIO36
SWスイッチGPIO0

気をつける必要があるのがVCCです。ジョイスティックには+5Vと書かれていますが、ジョイスティックを端まで倒した場合に、抵抗が0となりその電圧が信号PINに流れてきます。ESP32は3.3Vで動いているので、接続も3.3Vにする必要があります。5Vを接続すると、ジョイスティック中央の場合2.5Vが信号線に戻ってきますので、中心がずれてしまいます。

スケッチ

void setup() {
  Serial.begin(115200);
  pinMode(26, ANALOG);
  pinMode(36, ANALOG);
  pinMode(0, INPUT); // GPIO0はすでにプルアップ状態、他のPINだとINPUT_PULLUPにする
}

void loop() {
  Serial.printf("X=%d, y=%d, sw=%d\n", analogRead(26), analogRead(36), digitalRead(0));
  delay(100);
}

単純な動作検証用のコードになります。今回スイッチはGPIO0に接続しています。GPIO0は特殊なPINで、基本的に常にプルアップされている端子になります。起動時にLOWの場合にファームウエアの書き込みモードになる端子なので、スイッチを押し込んだままESP32を再起動すると、以下のメッセージが表示されます。

rst:0x1 (POWERON_RESET),boot:0x0 (DOWNLOAD_BOOT(UART0/UART1/SDIO_FEI_FEO_V2))
waiting for download

基本的には使わない方がよい端子なのですが、M5StickCの場合には端子数が少ないので仕方ありません。ただし、起動時にLOWになっていると、正常に起動しなくなるのを覚えておいてください。

GPIO0以外に接続した場合には、プルアップしておく必要があるのでpinModeをINPUT_PULLUPにするのを忘れないでください。もしくは、外部回路でプルアップをする必要があります。

動作させてみる

X=4095, y=4095, sw=1
X=2058, y=4095, sw=1
X=1962, y=4095, sw=1
X=0, y=4095, sw=1
X=0, y=4095, sw=1
X=0, y=3639, sw=1
X=0, y=0, sw=1
X=1186, y=0, sw=1
X=1968, y=0, sw=1
X=4095, y=0, sw=1
X=4095, y=0, sw=1
X=1968, y=1847, sw=1
X=1968, y=1847, sw=1
X=1968, y=1851, sw=1
X=1967, y=1847, sw=1
X=1968, y=1849, sw=1
X=1968, y=1847, sw=0
X=1968, y=1849, sw=0
X=1969, y=1849, sw=1
X=1968, y=1847, sw=1
X=1968, y=1850, sw=1
X=1968, y=1847, sw=1

上記のような出力がシリアル出力に出たと思います。X軸とY軸はセンサーによって方向が違うので注意してください。

X軸とY軸は何もしていないと中央値の2048前後、端っこまで倒すと最小値の0から、最大値の4095まで変化していました。スティックのボタンはいつもは1ですが、押し込むと0になっています。

気をつけないといけないのは、結構中央値などがずれてしまいます。2048を中央と定義して、差分で移動してしまうと常に移動し続けてしまうことになります。

補正版(-8から8までの17段階)

void setup() {
  Serial.begin(115200);
  pinMode(26, ANALOG);
  pinMode(36, ANALOG);
  pinMode(0, INPUT); // GPIO0はすでにプルアップ状態、他のPINだとINPUT_PULLUPにする
}

void loop() {
  int x = analogRead(26);
  int xd = (x - 2048) / 255;
  int y = analogRead(36);
  int yd = (y - 2048) / 255;

  Serial.printf("X=%d(%d), y=%d(%d), sw=%d\n", x, xd, y, yd, digitalRead(0));
  delay(100);
}

-8から8までの移動量に補正をしたスケッチです。中央がそれほどずれていないジョイスティックの場合には中央値は2048で、そこからの差分を少し丸めて取得すればそれっぽい数値になるはずです。

このコードの場合、ほんの少し動かした場合には反応しませんので、微妙な入力で使う場合には、起動時にチュートラルの場合の数値を保存しておき、その値を中央地にしてから補正したほうが正確になります。

ただし、アナログ値なので動かしていなくても結構数値は動くのは、あまり厳密な用途には向いていません。

センター補正スケッチ(-128から128)

int xc;
int yc;

void setup() {
  Serial.begin(115200);
  pinMode(26, ANALOG);
  pinMode(36, ANALOG);
  pinMode(0, INPUT); // GPIO0はすでにプルアップ状態、他のPINだとINPUT_PULLUPにする

  xc = analogRead(26);
  yc = analogRead(36);
}

void loop() {
  int x = analogRead(26);
  int xd = (x - xc) / 14; // 補正によって極値にならない可能性があるので14
  int y = analogRead(36);
  int yd = (y - yc) / 14; 

  // 14で割っているので、極値以上になっているのを極値に修正
  if (xd < -128) {
    xd = -128;
  }
  if (128 < xd) {
    xd = 128;
  }
  if (yd < -128) {
    yd = -128;
  }
  if (128 < yd) {
    yd = 128;
  }

  Serial.printf("X=%4d(%4d), y=%4d(%4d), sw=%d\n", x, xd, y, yd, digitalRead(0));
  delay(100);
}

起動時に取得した数値をセンターとして補正します。本当は複数回取得して平均をとったほうがいいと思います。また、センターがずれるとはじまで移動しても値が中途半端になる可能性があるので、判定の幅も小さくする必要があります。

ある程度はうまく動くと思いますが、アナログでジャンパー接続をしているとノイズなどの影響で変な値を拾ってしまうので、動かしていなくても-2などがでてくることがあります。

その他のセンサー

上記はM5Stack社のジョイスティックユニットです。M5StickのHY2.04P端子(Grove端子)にケーブルで接続することができます。ケーブルで接続する以外にも、直接本体に接続するジョイスティックHATもあります。

このユニットの中身はアナログジョイスティックなのですが、中に制御用ICがはいっていて、I2Cで接続しています。そのため、複数のI2Cユニットを接続することも可能です。

原理的には同じなのですが、基板で配線されているのでノイズの影響を受けにくい分ユニットのほうが精度は高いかもしれません。

まとめ

ジョイスティックは個体差があるので、すこし緩めの制御にしたほうが安全だと思います。厳し目の設定にすると、移動していないのに動いたり、端に移動しているつもりなのに移動できていなかったりと問題が発生してしまいます。

続編