ESP32マウス(shota) マウス自作研修

ESP32マウスPart.37 C++でESP32を動かす(SPIでモーションセンサと通信する)

C++でESP32を動かす(ICM-20648のライブラリ作成) ESP32マウス(shota)

こんにちは、shotaです。

前回の記事ではSPI機能を使って磁気式エンコーダMA702と通信しました。
ESP32のSPI機能について詳しく説明しているので、まず先にこちらをご覧ください。

そして、今回も同じくSPI機能を使うのですが、趣向を変えてC++でコーディングします
実装するのは、モーショントラッキングセンサICM-20648との通信機能です。

ブロクで書いたサンプルコードはGitHubに公開しています。
https://github.com/ShotaAk/especial/tree/master/examples

初めてのオリジナルマウス Especial

プログラムを書く前の下準備

さて今回もプログラムを書く前に、Especialの回路図やESP32モジュールのデータシートを読み、情報を集めます。

モーショントラッキング回路の確認

まずはじめに、Especialの回路図を確認します。

↓Especialの回路図はこちらです。今回必要なところは、モーショントラッキング回路です。
especial回路図.pdf

↓モーショントラッキング回路の設計ブログ記事はこちらです
shotaのマイクロマウス研修17 回路設計⑤:ESP32ソフト書き込み基板と間違い

Especial – モーショントラッキング回路

Especial – ESP32の接続先

モーショントラッキングICのICM-20648とESP32は、6AXIS_CS6AXIS_SCLK6AXIS_MOSI6AXIS_MISOの4本線で通信します。

ESP32への接続先は次のとおりです。

  • 6AXIS_CSはIO5
  • 6AXIS_SCLKはIO18
  • 6AXIS_MOSIはIO19
  • 6AXIS_MISOはIO21
  • これらのピンが、高速通信できる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に公開しているので、こちらもご覧ください。

    ShotaAk/especial
    ESP32を搭載したマイクロマウスのプログラム. Contribute to ShotaAk/especial development by creating an account on GitHub.

    C++でプログラムを作成する場合は、ソースファイルの拡張子を.cppにして、C++ファイルであることを明示します。
    ヘッダーファイルの拡張子は.hです。.hppでも構いません。

    ソースコードの作成と解説

    それではソースコードを作成します。
    作成するために、ESP-IDFのexamples/system/cpp_exceptionsexamples/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.cppicm20648.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にアップロードしたコードの閲覧をお勧めします。

    ShotaAk/especial
    ESP32を搭載したマイクロマウスのプログラム. Contribute to ShotaAk/especial development by creating an account on 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ライブラリを更新して、加速度や角速度の値を取得します。

    タイトルとURLをコピーしました