100日後に完走するマウス(倉澤ズズくん)

IMU(ジャイロ・加速度センサ)との通信(ICM-42688-P) – 100日後に完走するマウス【283日目】 Part.9

100日後に完走するマウス(倉澤ズズくん)

はじめに

お久しぶりです。倉澤ズズくんです。
前回は距離センサについて扱いました。今回はIMUについて触れます。センサ類の実験としては最後になります。

IMUの概要

IMUとは、慣性センサのユニットです。
角速度(回転角の速度)を計測するジャイロセンサと、加速度を計測する加速度センサの両方の機能を搭載しているセンサのユニットを指します。

 

IMU(ジャイロと加速度の6軸)
引用:ICM-42688-Pのデータシート

私のマウスに搭載するIMUはICM-42688-Pです。XYZの角速度3軸・加速度3軸を加えた6軸の計測ができ、旋回時の角速度・直進時の加速度の調整をしやすくなります。

ICM-42688-P実装済みモジュール

ICM-42688-Pはチップ部品であるため、直接ブレッドボードに挿すことができません。今回は動作確認のため、既に基板に実装済みの製品を購入して使用します。一般的なユニバーサール基板やブレッドボードの2.54[mm]ピッチとなっているので実験に適しており、リフローの環境が無い方はこのまま自身のマウスに搭載すれば使用できます。

SPI通信(マイコンとIMUの通信)

ICM-42688-PではSPII2C等の方式で通信が可能です。今回はSPIで通信します。
SPIでは4本の信号線でデータを送受信し、I2Cではこれを2本の信号線で行います。SPIは配線が多くなるが通信が速く、I2Cでは配線が少ないが通信が遅いです。

今回は高速なSPIの方式で通信します。

SPI通信の方法

SPI通信について簡単な解説をします。詳細には、Analog Devices社のサイトで分かりやすくまとめられています。

SPI通信の信号線

SPI通信には4つの信号線を用います。

  • SCLK (Serial Clock): クロック信号を提供し、データの同期を取ります。
  • MOSI (Master Out Slave In): マスターデバイスからスレーブデバイスへのデータ転送を行います。
  • MISO (Master In Slave Out): スレーブデバイスからマスターデバイスへのデータ転送を行います。
  • CS (Chip Select): スレーブデバイスを選択し、通信を開始します。マスターはこの線を使用して特定のスレーブデバイスとの通信を有効にします。

マスターとは制御する側、スレーブと制御される側を示します。今回は、「マスター側がマイコン」、「スレーブ側がIMU」の関係です。

センサからのデータを得るには任意のアドレスを送ります。送ったアドレスに応じてセンサからはデータが返ってきます。

通信の流れ

通信の流れは以下のとおりです。

  1. マスターデバイスはCS信号をLOWにして特定のスレーブデバイスをアクティブにし、通信を開始します。
  2. マスターはSCLKによってクロック信号を提供します。(信号の周期を決定する。)
  3. データはMOSIまたはMISO線を介してビット単位で同期的に送受信されます。
  4. データ転送が完了したら、マスターはCSをHIGHにして通信を終了します。

Mode

SPI通信にはModeがあります。こちらの説明についてkoraさんのブログで簡潔な説明と表でまとめられているので引用します。

※SPI通信は、クロックの極性 (クロックのアイドル状態) とクロックの位相 (データ受信のタイミング) について4つのモードが定義されています。
参考:ANALOG DEVICES AN-1248 アプリケーション・ノート

クロックの極性 クロックの位相
Mode 0 アイドル状態がLOW クロックの立ち上がり(1番目のエッジ)にデータ受信
Mode 1 アイドル状態がLOW クロックの立ち下がり(2番目のエッジ)にデータ受信
Mode 2 アイドル状態がHIGH クロックの立ち下がり(1番目のエッジ)にデータ受信
Mode 3 アイドル状態がHIGH クロックの立ち上がり(2番目のエッジ)にデータ受信

ICM-42688-PでのSPI通信

ICM-42688-Pの設定

ICM-42688-Pのデータシートの52ページにSPIでの通信方法が書かれています。

データシートからは、先程記述したSPI通信の方法と同じような手順が書かれています。加えて、SPI通信に必要な設定に以下の情報も 読み取れます。

  • Mode: 0 or 3
  • MSB: First
  • Clock: 最大24M[Hz]

ICM-42688-PのSPI通信プロトコル

ICM-42688-Pと通信するには2Byte(16bit)の通信で完了します。1Byte目の最上位bitが読み込みフラグとなっています(0:書き込み、1:読み込み)。下位7bitに読みたいデータのアドレスを指定した値を送信します。2Byte目には、書き込みモードでは書き込むデータ、読み込みモードではダミーデータを送信します。読み込みモードではダミーデータを送信したタイミング、つまり2Byte目にこちらが欲しいセンサ値を返してくれます。

ダミーデータとは、1bit送信すると同時に1bit値が返ってくるというSPI通信での性質上用いるものです。1Byte目にセンサのどの値が欲しいかをアドレスで指定し、それに応じて返ってきた2Byte目のセンサ値を受信するため、マイコンからIMUに何かしらのデータを送信する必要があります。ただし、飽くまで受信するために値を送信するだけなので、ここの値は何でも良いのです。

STM32CubeMXの設定

ICM-42688-Pの通信方法がわかったところでSTM32CubeMXの設定をします。

STM32F446では3つのSPI通信ポートがありますが、今回は3つ目のSPI3を使用します。赤丸で囲ったSPI3のピンを選択したら、Modeの項目で以下の2点を設定します。

  • Mode: Full-Duplex Master
    • データの送受信を同時に行います
  • Hardware NSS Signal: Disable
    • CSピンは独立したGPIOを使ったソフトウェア側で行うのでDisableにします

次に、Parametor Settings項目で先程のデータシートで読み取った条件を満たすような値を設定します。

STM32CubeMXでのSPI機能の設定

CSピンはソフトウェアで行うので独立したGPIOピンを設定して制御します。SPI通信開始時にはLOW、通信終了時に適宜HIGHに切り替えます。今回使用するピンはPD2です。

プログラム

SPI通信を実装したクラスは以下のようになります。このクラスを利用して読み書きを行います。

マイクロマウスではジャイロと加速度両方の要素をプログラムに組み込む予定ですが、今回は加速度を取得する機能だけを実装しています。実験時に加速度の方が視覚的に分かりやすく、以下のソースコード実装例を簡単化できるためです。

SPI通信を用いたICM42688-Pのクラス

#include "arm_math.h"
#include "spi.h"
#include "digital_out.h"


namespace mslh {

class IMUICM42688P {

public:
    IMUICM42688P(SPI_HandleTypeDef &hspi, GPIO_TypeDef *cs_x, uint16_t cs_pin)
    : hspi_(hspi)
    , cs_x_(cs_x)
    , cs_pin_(cs_pin)
    , accel_range_(2.0f) {
    }

    void init() {
        HAL_SPI_Init(&_hspi);
        HAL_GPIO_WritePin(cs_x_, cs_pin_, GPIO_PIN_SET);
        setConfigs();
    }

    uint8_t getWhoAmI() {
        const uint8_t address = 0x75;   //< Input who_am_i: 0x75, return: 0x47
        return read(address);  // 受け取るのは2バイト目のデータだけ
    }

    float32_t getAccelX() {
        // 0x23: upper Byte,  0x24: lower Byte
        const int16_t raw_accel_x = static_cast<int16_t>(((read(0x1F) << 8) | read(0x20)));
        return (accel_range_ * static_cast<float32_t>(raw_accel_x) / static_cast<float32_t>(INT16_MAX));
    }

    float32_t getAccelY() {
        // 0x23: upper Byte,  0x24: lower Byte
        const int16_t raw_accel_y =  static_cast<int16_t>((read(0x21) << 8) | read(0x22));
        return (accel_range_ * static_cast<float32_t>(raw_accel_y) / static_cast<float32_t>(INT16_MAX));
    }

    float32_t getAccelZ() {
        // 0x23: upper Byte,  0x24: lower Byte
        int16_t raw_accel_z = static_cast<int16_t>((read(0x23) << 8) | read(0x24));
        return (_accel_range * static_cast<float32_t>(raw_accel_z) / static_cast<float32_t>(INT16_MAX));
    }

private:
    uint8_t read(const uint8_t address) {

        constexpr uint16_t size = 2;
        const uint8_t read_mode = 0b10000000;  //< (read:1, write:0)
        const uint8_t tx_data[size] = {static_cast<uint8_t>(read_mode | address), 0x00};
        uint8_t rx_data[size] = {0x00, 0x00};

        HAL_GPIO_WritePin(cs_x_, cs_pin_, GPIO_PIN_RESET);  // CS pin: LOW
        HAL_SPI_TransmitReceive(&_hspi, (uint8_t*)tx_data, (uint8_t*)rx_data, size, 100);
        HAL_GPIO_WritePin(cs_x_, cs_pin_, GPIO_PIN_SET);

        return rx_data[1];  //< データは2バイト目に格納される
    }

    void write(const uint8_t address, const uint8_t data) {

        constexpr uint16_t size = 2;
        // const uint8_t write_mode = 0b00000000;  //< (read:1, write:0)
        const uint8_t tx_data[size] = {address, data};

        HAL_GPIO_WritePin(cs_x_, cs_pin_, GPIO_PIN_RESET);  // CS pin: LOW
        HAL_SPI_Transmit(&_hspi, (uint8_t*)tx_data, size, 100);
        HAL_GPIO_WritePin(cs_x_, cs_pin_, GPIO_PIN_SET);  // CS pin: HIGH
    }

    void setConfigs() {
        HAL_Delay(10);
        setPowerManage();
        HAL_Delay(10);
        setAccelConfig();
        HAL_Delay(10);
    }

    // 加速度のレンジの設定
    void setAccelConfig() {
        constexpr uint8_t accel_conf_addr = 0x50;
        constexpr uint8_t accel_conf_data = 0b01100110;  // range:2[g], rate:1k[Hz]
        write(accel_conf_addr, accel_conf_data);
    }

    // 温度・ジャイロ・加速度のON/OFFやモードの設定
    void setPowerManage() {
        constexpr uint8_t accel_conf_addr = 0x4E;
        constexpr uint8_t accel_conf_data = 0b00011111;  // range:2[g], rate:1k[Hz]
        write(accel_conf_addr, accel_conf_data);
    }


    SPI_HandleTypeDef &_hspi;
    GPIO_TypeDef *cs_x_;
    const uint16_t cs_pin_;
    float32_t accel_range_;  // default: 2.0f
    float32_t imu_range_;
};

}  // namespace mslh

IMUの初期設定

void init()

加速度やジャイロの計測範囲を設定する関数として作成したsetAccelConfigsetPowerManageを実行します。IMUの起動時に設定を書き込みます。

IMUのセンサ値を読む

uint8_t read(const uint8_t address)

センサの読みたいデータのアドレスを引数として渡します。センサデータが返ってきます。

ICM-42688-Pでの値の読み書きには2Byte(16bit)の通信で完了します。1Byte目の最上位bitが読み込みフラグの1を、下位7bitに読みたいデータのアドレスを指定した値を設定します(OR演算でデータを成形)。2Byte目にはダミーデータとして0x00を設定します。この2Byteのデータを送信し、返ってきた2Byte目のデータを読み取ります。

HAL_GPIO_WritePin(CSポート, CSピン, GPIO_PIN_RESET)でCSピンをLOWにして通信開始、HAL_SPI_TransmitReceive(SPIハンドラ, (uint8_t*)送信データ, (uint8_t*)受信データ, データサイズ[2Byte], タイムアウト時間)でデータの送受信、HAL_GPIO_WritePin(CSポート, CSピン, GPIO_PIN_SET)でピンをHIGHにして通信終了です。

最後に受信したデータ(2Byte目)を関数の返り値として設定します。

IMUに値を書き込む

void write(const uint8_t address, const uint8_t data)

センサの読みたいデータのアドレスと書き込みデータを引数として渡し、センサに値を書き込みます。センサの設定をする際に使用します。

ICM-42688-Pでの値の読み書きには2Byte(16bit)の通信で完了します。1Byte目の最上位bitが読み込みフラグの0を、下位7bitに書き込みたいデータのアドレスを指定した値を設定します(書き込みと違い0なのでORによるデータ成形は省略)。2Byte目には書き込みたいデータを設定します。このread関数はwrite関数と違い一方的に値を書き込むので、値を読む必要はありません。

HAL_GPIO_WritePin(CSポート, CSピン, GPIO_PIN_RESET)でCSピンをLOWにして通信開始、HAL_SPI_Transmit(SPIハンドラ, (uint8_t*)送信データ, (uint8_t*)受信データ, データサイズ[2Byte], タイムアウト時間);でデータの送受信、HAL_GPIO_WritePin(CSポート, CSピン, GPIO_PIN_SET)でピンをHIGHにして通信終了です。

Who Am Iを読む

uint8_t getWhoAmI()

Who Am Iとは、SPIやI2C通信デバイスと正しく通信できているかを調べるためのアドレスです。デバイスごとに特定の数値が返ってくるように設定されています。ICM-42688-Pの場合、正しく通信できていれば0x47が返ってきます。

X/Y/Z軸の加速度を読む

float32_t getAccelX() / float32_t getAccelY()float32_t getAccelZ()

X,Y,Z軸の加速度をread関数で取得し、重力加速度の単位[g]に変換した値を返します。

実験

IMUとの通信実験をします。目的はマイコンとIMUの通信をしてデータを取得し、実際の物理量に変換できるかを検証します。

準備

回路

ブレッドボード上に回路を組み、マイコンボードNucleoF446と配線します。NucleoF446はPCとUSBケーブルで接続されています。

配線

ICM-42688-Pの評価ボード
引用:ストロベリー・リナックス

プログラム

先程作成したクラスを利用して実験用のプログラムを作成します。
Who Am I0x47が返ってくることを期待し、それを2秒間表示した後、X/Y/Z軸の加速度をコンソールで表示するプログラムを用意しました。

この関数test_imu()をmainファイルで呼び出して実験します。IMUの値は、シリアル通信を経由してPC上にコンソール表させて確認します。

void test_imu() {
    mslh::IMUICM42688P imu(hspi3, GPIOD, GPIO_PIN_2);
    imu.init();

    uint8_t who_am_i = imu.getWhoAmI();
    printf("Who Am I: 0x%x\r\n", who_am_i);
    HAL_Delay(2000);

    while (1) {
        const float32_t x_accel = imu.getAccelX();
        const float32_t y_accel = imu.getAccelY();
        const float32_t z_accel = imu.getAccelZ();
        printf("[Accel] X:%+3.1f | Y:%+3.1f | Z:%+3.1f\r\n", x_accel, y_accel, z_accel);
        HAL_Delay(10);
    }
}

方法

SPIの方式でマイコンとIMUの通信をします。加速度の計測結果を物理量に変換してコンソールに表示します。

実験の風景と、IMUのXYZ軸は画像のとおりです。X軸が画像右側、Y軸が画像奥側、Z軸が机に対して垂直上側にそれぞれ正の方向となっています。

実験風景とXYZ軸の向き

結果

実際に計測し、値が変化する様子を動画に収めました。

Who Am Iで正しく0x47が返ってきているのがわかります。

その後のIMUでは最初の位置でZ軸に対して+1.0[g](g:重力加速度)と示されています。本来であれば重力によって物体は垂直に落下しますが、置かれた机によって打ち消すようにその場にとどまります。その打ち消す加速度が重力とは反対方向に同じだけ(1[g])生じています。

X軸とY軸を垂直方向に変えたときもZ軸と同様の挙動となっており、IMUに発生している加速度が正しく計測できていることがわかります。

まとめ

今回はIMUについてSPI通信で正しく値のやりとりができるとこを確認しました。これでマイクロマウスにIMUを搭載しても大丈夫ということが分かりました。

次回は全体の機体製作について触れます。

全日本大会まで、残り321日!

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