こんにちは、shotaです。
前回の記事ではSPI機能を使って磁気式エンコーダMA702と通信しました。
ESP32のSPI機能について詳しく説明しているので、まず先にこちらをご覧ください。
そして、今回も同じくSPI機能を使うのですが、趣向を変えてC++でコーディングします。
実装するのは、モーショントラッキングセンサICM-20648との通信機能です。
ブロクで書いたサンプルコードはGitHubに公開しています。
https://github.com/ShotaAk/especial/tree/master/examples
プログラムを書く前の下準備
さて今回もプログラムを書く前に、Especialの回路図やESP32モジュールのデータシートを読み、情報を集めます。
モーショントラッキング回路の確認
まずはじめに、Especialの回路図を確認します。
↓Especialの回路図はこちらです。今回必要なところは、モーショントラッキング回路です。
especial回路図.pdf
↓モーショントラッキング回路の設計ブログ記事はこちらです
shotaのマイクロマウス研修17 回路設計⑤:ESP32ソフト書き込み基板と間違い
モーショントラッキングICのICM-20648とESP32は、6AXIS_CS、6AXIS_SCLK、6AXIS_MOSI、6AXIS_MISOの4本線で通信します。
ESP32への接続先は次のとおりです。
これらのピンが、高速通信できるIO MUXピンなのか、それともGPIO Matrixピンなのか、ESP-IDFプログラミングガイドを見てみましょう。

ESP32 SPIのIOMUXピン https://docs.espressif.com/projects/esp-idf/en/v3.3.1/api-reference/peripherals/spi_master.html#gpio-matrix-and-iomux
う〜ん、VSPIのIO MUXピンが使えるかと思いましたが、MISOがIO23ではなくIO21に接続されているので、IO MUXピンではありません。
なぜこうなったかというと、回路パターン作成時にピン機能の割り当てを変更したからです(私が犯人です)。
SPI通信線よりもLED信号線を割り当てたほうが綺麗にパターンを引けたためです。
この記事に詳細が書かれています:shotaのマイクロマウス研修21 アートワーク作成の続き(メイン基板の配線開始)
ちなみにこれをネガティブに考える必要はありません。
まず、GPIO Matrixによって、自由にピン機能を変更できることがESP32の強みです。
そして、GPIO Matrixを使うとしても、40MHzまではクロックスピードをあげられます。十分速いです。
しかも、ICM-20648の最大通信クロックは7MHzです。つまり、IO MUXピンを使えなくても何も問題ありません。
プログラムの作成
SPI機能の説明は前回の記事に書いたので説明を省きます。
さっそくプログラムを作成しましょう。
プロジェクトのフォルダ構成
今回はC++でコーディングすることと、ICM20648のライブラリを作成することに挑戦します。
そのため、プロジェクトのmainフォルダ内のファイル構成がこれまでと少し変わります。
次のようにファイルを作成します。
CMakeLists.txt : これまでも作成していましたが、中身が少し変わります。
component.mk : 変更なし
icm20648.cpp : 新規作成。ICM20648ライブラリのソースファイルであり、C++で書きます
icm20648.h : 新規作成。ICM20648ライブラリのヘッダーファイル
main.cpp : これまでも作成していましたが、C++で書きます
ソースコードはGitHubに公開しているので、こちらもご覧ください。
C++でプログラムを作成する場合は、ソースファイルの拡張子を.cppにして、C++ファイルであることを明示します。
ヘッダーファイルの拡張子は.hです。.hppでも構いません。
ソースコードの作成と解説
それではソースコードを作成します。
作成するために、ESP-IDFのexamples/system/cpp_exceptionsとexamples/system/cpp_pthreadを参考にしました。
https://github.com/espressif/esp-idf/tree/release/v3.3/examples/system/cpp_exceptions
https://github.com/espressif/esp-idf/tree/release/v3.3/examples/system/cpp_pthread
CMakeLists.txt
まず、CMakeLists.txtを次のように書きます。
set(COMPONENT_SRCS "main.cpp" "icm20648.cpp" ) set(COMPONENT_ADD_INCLUDEDIRS ".") register_component()
COMPONENT_SRCSに、main.cppとicm20648.cppを追加しました。
main.cpp
つぎに、main.cppを次のように実装します。
#include <iostream> #include <freertos/FreeRTOS.h> #include <freertos/task.h> #include "icm20648.h" const static gpio_num_t GPIO_MOSI = GPIO_NUM_19; const static gpio_num_t GPIO_MISO = GPIO_NUM_21; const static gpio_num_t GPIO_SCLK = GPIO_NUM_18; const static gpio_num_t GPIO_CS = GPIO_NUM_5; /* Inside .cpp file, app_main function must be declared with C linkage */ extern "C" void app_main(){ ICM20648::init(GPIO_MOSI, GPIO_MISO, GPIO_SCLK, GPIO_CS); while(1){ std::cout<< "WHO AM I:" <<std::hex<<ICM20648::read_who_am_i()<<std::endl; vTaskDelay(1000 / portTICK_PERIOD_MS); } }
Cで書いたソースコードと似ていますが、ところどころ違いがあります。
まず、std::cout関数や、vTaskDelay関数、ICM20648の関数を使うためヘッダーファイルをインクルードします。
#include <iostream> #include <freertos/FreeRTOS.h> #include <freertos/task.h> #include "icm20648.h"
また、app_main関数の頭にextern “C”という文字が追加されました。
ESP-IDFでC++を扱う場合、app_main関数にこれを追記しなければなりません。
extern “C”ってなんやねん!と思う方は調べてみてください。私は次のページが参考になりました。
C++のマングルとextern “C” { | wagavulin’s blog
while文ではicm20648ライブラリのread_who_am_i関数を使って、WHO_AM_I情報を読み取っています。
while(1){ std::cout<< "WHO AM I:" <<std::hex<<ICM20648::read_who_am_i()<<std::endl; vTaskDelay(1000 / portTICK_PERIOD_MS); }
WHO_AM_Iについては、ICM-20648のデータシートの42ページ目に仕様が書かれています。

ICM-20648 WHO_AM_Iレジスタの情報
https://invensense.tdk.com/wp-content/uploads/2017/07/DS-000179-ICM-20648-v1.2-TYP.pdf
データシートより、レジスタのアドレス0x00を読み取ると、0xE0が返ってくることがわかります。
icm20648.h
続いて、ICM20648ライブラリのヘッダーファイルicm20648.hです。
#ifndef ICM20648_H #define ICM20648_H namespace ICM20648 { void init(const int mosi_io_num, const int miso_io_num, const int sclk_io_num, const int cs_io_num); int read_who_am_i(void); } #endif /* !ICM20648_H */
このESP32マウスブログシリーズでヘッダーファイルを作成するのは初めてです。これまでは、main.cしか作成していませんでした。
今回のヘッダーファイルには、ライブラリの関数宣言のみを記載します。
関数の中身はソースファイルに定義します。
ファイルの上下にある#ifndef ICM20648_Hや#endif は、インクルードガードというものです。
詳しくはここに書きませんが、インクルードガードは、ヘッダーファイルが複数のファイルでインクルードされるとき、
関数や変数の多重定義を防いでくれます。
(今回はヘッダーファイルに定義が書かれていないので、
インクルードガードの役目を果たしていません。ただのおまじないです。)
namespace ICM20648{ }は名前空間というC言語にはない機能です。
名前空間を使うことで、ただのinit関数が、ICM20648内のinit関数(ICM20648::init)になります。
init( ) だけだと、何の初期化関数なのかわからないですよね。名前空間、便利です。
icm20648.cpp
最後実装するのが、ICM20648ライブラリのソースファイルicm20648.cppです。
行数が多いので、GitHubにアップロードしたコードの閲覧をお勧めします。
#include "icm20648.h" #include <cstring> #include <driver/spi_master.h> static spi_device_handle_t spidev_; uint8_t transaction(const uint8_t cmd, const uint8_t addr, const uint8_t data){ // ICM-20648と通信するデータ読み込み、書き込み兼用関数 const size_t DATA_LENGTH = 8; uint8_t recv_data=0; spi_transaction_t trans; memset(&trans, 0, sizeof(trans)); // 構造体をゼロで初期化 // flags: SPI_TRANS_ではじまるフラグを設定できる // trans.flags = SPI_TRANS_USE_RXDATA; trans.cmd = cmd; trans.addr = addr; trans.length = DATA_LENGTH; // データ長 bit // trans.rxlength = 16; // デフォルトでlengthと同じになるので設定不要 // trans.user = NULL; // ユーザ定義の変数、コールバックを使うときに役立つ trans.tx_buffer = &data; // 送信バッファのポインタ // trans.tx_data; // SPI_TRANS_USE_TXDATAフラグを立てれば使用可能 trans.rx_buffer = &recv_data; // 受信バッファのポインタ // trans.rx_data; // SPI_TRANS_USE_RXDATAのフラグを立てれば使用可能 // 通信開始 esp_err_t ret; ret=spi_device_polling_transmit(spidev_, &trans); assert(ret==ESP_OK); return recv_data; } uint8_t readRegister(const uint8_t address){ // レジスタデータを読み取る const uint8_t READ_COMMAND = 1; uint8_t raw_data = transaction(READ_COMMAND, address, 0x00); return raw_data; } void ICM20648::init(const int mosi_io_num, const int miso_io_num, const int sclk_io_num, const int cs_io_num){ esp_err_t ret; // SPIバスの設定 spi_bus_config_t buscfg; buscfg.mosi_io_num = mosi_io_num; // Master Out Slave Inのピン buscfg.miso_io_num = miso_io_num; // Master In Slave Outのピン buscfg.sclk_io_num = sclk_io_num; // MasterSPI Clockのピン buscfg.quadwp_io_num = -1; // Quad SPIのWPピン。使わないので-1をセット。 buscfg.quadhd_io_num = -1; // Quad SPIのHDピン。使わないので-1をセット。 buscfg.max_transfer_sz = 2; // 最大送信バイト数。 // flags: SPICOMMON_BUSFLAG_で始まるフラグをセットできる buscfg.flags = SPICOMMON_BUSFLAG_MASTER; ret = spi_bus_initialize(VSPI_HOST, &buscfg, 1); ESP_ERROR_CHECK(ret); // SPIデバイスの設定 spi_device_interface_config_t devcfg; devcfg.command_bits = 1; // コマンドフェーズのビット長 devcfg.address_bits = 7; // アドレスフェーズのビット長 devcfg.dummy_bits = 0, // アドレスフェーズとデータフェーズ間のビット長 devcfg.mode = 3; // SPIのモード devcfg.duty_cycle_pos = 0, // クロックのデューティ比。0で、デフォルトの50%がセットされる。 devcfg.cs_ena_pretrans = 0; // 送信処理前にCSをアクティブにし続けるサイクル数 devcfg.cs_ena_posttrans = 0; // 送信処理後にCSをアクティブにし続けるサイクル数 devcfg.clock_speed_hz = 7*1000*1000; // クロックスピードを7MHzに設定 // input_delay_ns: SCLKとMISOの間にある、 // スレーブのデータが有効になるまでの最大遅延時間。 // CSをアクティブにして、MISOが送信されるまでに、追加で遅延を設ける。 // 8MHz以上のクロックスピードを使うときに必要だけど、 // 正確な値が分からなければ0を設定してね。 devcfg.input_delay_ns = 0; // devcfg.spics_io_num = NULL, // CSピン。後ほど設定する devcfg.flags = 0; // SPI_DEVICE_で始まるフラグを設定できる devcfg.queue_size = 1; // transactionのキュー数。1以上の値を入れておく。 devcfg.pre_cb = NULL; // transactionが始まる前に呼ばれる関数をセットできる devcfg.post_cb = NULL;// transactionが完了した後に呼ばれる関数をセットできる // デバイス設定のCSピンだけ書き換える devcfg.spics_io_num = cs_io_num; ret = spi_bus_add_device(VSPI_HOST, &devcfg, &spidev_); ESP_ERROR_CHECK(ret); } int ICM20648::read_who_am_i(void){ const uint8_t ADDR_WHO_AM_I = 0x00; return readRegister(ADDR_WHO_AM_I); }
前回の記事で実装したコードと似ているので、違うところのみを抽出して説明します。
まずはじめにライブラリのインクルードについて説明します。
icm20648.cppはICM20648ライブラリのソースファイルなので、icm20648.hのインクルードは必須です。
cstringはtransaction関数内で使用している、memset関数を使うために読み込みます。
#include "icm20648.h" #include <cstring> #include <driver/spi_master.h>
ESP32のSPI通信では、spi_device_handle_t構造体の変数が通信を操作します。
今回はspidev_というstaticなグローバル変数を用意しました。
static spi_device_handle_t spidev_;
つぎに、init関数です。ICM20648の名前空間で宣言されているので、定義するときはICM20648::initと書きます。
この関数ではSPIデバイスの初期設定を行います。
ICM-20648のデータシートの31ページ目にあるSPI通信の仕様を参考にしました。
通信クロックは最大7MHzです。
通信パケットは、コマンド1ビット(Read/Write)、アドレス7ビット、データ8ビット(か、それ以上)です。

ICM-20648 SPI Operational Features
https://invensense.tdk.com/wp-content/uploads/2017/07/DS-000179-ICM-20648-v1.2-TYP.pdf
void ICM20648::init(const int mosi_io_num, const int miso_io_num, const int sclk_io_num, const int cs_io_num){ --- 省略 --- // SPIデバイスの設定 spi_device_interface_config_t devcfg; devcfg.command_bits = 1; // コマンドフェーズのビット長 devcfg.address_bits = 7; // アドレスフェーズのビット長 devcfg.dummy_bits = 0, // アドレスフェーズとデータフェーズ間のビット長 devcfg.mode = 3; // SPIのモード devcfg.clock_speed_hz = 7*1000*1000; // クロックスピードを7MHzに設定 --- 省略 --- }
そして今回の主役であるread_who_am_i関数です。
WHO_AM_Iのアドレス0x00のデータを読み取って返します。
int ICM20648::read_who_am_i(void){ const uint8_t ADDR_WHO_AM_I = 0x00; return readRegister(ADDR_WHO_AM_I); }
他の関数については説明を省略します。
コメントをたくさん書いてあるので、読めば解ると思います。
プログラムの実行
それではビルドして書き込み、シリアルモニタを起動します。
$ cd ~/esp/especial/examples/6_motion_tracking $ make flash monitor --- 省略 --- I (54) cpu_start: Starting scheduler on PRO CPU. I (0) cpu_start: Starting scheduler on APP CPU. WHO AM I:e0 WHO AM I:e0 WHO AM I:e0 WHO AM I:e0 WHO AM I:e0
実行すると1秒毎にWHO_AM_Iの値が表示されます。
ICM-20648のデータシートのとおりE0が返ってきているので、通信が成功しました。
次回の記事
次回は、ICM20648ライブラリを更新して、加速度や角速度の値を取得します。