フェーズ3

E3-03 連続読み(複数バイト):加速度X軸を2バイト読んで、センサ値の取り方に慣れる

シリーズ:実験シリーズ(フェーズ3)
対応ロードマップ:フェーズ3 / E3-03
この記事で扱う範囲:E3-02 で 1バイトのレジスタ読みができたので、今回は 2バイトを連続で読む ところまで進める。まずは 加速度X軸の生データを読んで、2バイトを1つの値として扱える ところまでを扱う。

1. 目的

今回は、I2Cで 複数バイトにまたがるデータを続けて読む方法 を確認します。
前回は 1バイトのレジスタ読みでしたが、実際のセンサ値は 2バイト以上で表されることが多いです。
そこで今回は、加速度X軸の2バイトデータを例にして、複数バイトをまとめて読み、1つの値として扱う流れ に慣れることを目的とします。

2. 前提・環境

2-1. 前提

この記事は、次の内容が終わっている前提で進めます。

  • フェーズ0:ビルド / 書き込み / main到達 / UARTログ出力
  • E2-02:1msごとの時刻を作る
  • E2-03:周期処理
  • E2-05:状態で整理(IDLE / RUN / ERROR)
  • E3-00:I2C配線の“詰まりどころ”を先に潰す
  • E3-01:0x68 に対して応答があるか確認する
  • E3-02:WHO_AM_I を1バイト読める

2-2. 使用するもの

  • 評価ボード:RTK7EKA8M2S00001BE
  • e² studio + FSP
  • Tera Term
  • IMUモジュール:QCIOT-ICM42688P(PMOD BOARD ICM-42688-P)
  • USB-UART変換ケーブル(またはUSB-UART変換モジュール)
  • ジャンパ線(UART接続用)

2-3. 今回の前提条件

今回は、前回(E3-02)の下記条件をそのまま使います。

  • 通信方式:I2C
  • 接続方法:QCIOT-ICM42688P の J1 を評価ボードの Pmod1(J26)へ接続
  • 評価ボード側設定:SW4-1 = OFF、SW4-2 = ON
  • AD0:Low(J4 にジャンパキャップあり)
  • I2Cアドレス:0x68

2-4. 今回使うレジスタ

今回は、加速度X軸の生データを読むために、次の2つのレジスタを使います。

  • ACCEL_DATA_X1 = 0x1F(上位バイト)
  • ACCEL_DATA_X0 = 0x20(下位バイト)

ICM-42688-P では、この2バイトをつなげて 1つの16bitデータ として扱います。
また、加速度やジャイロの出力は 符号付きの16bit値 です。

今回は「符号付きの細かい理屈を完全理解すること」までは目的にしません。
まずは、

  • 2バイトを読める
  • 2バイトを1つにまとめられる
  • 正負を持つ値として出てくる

ことが分かれば十分です。

2-5. 今回の動作イメージ

今回は、起動後に次の流れで通信します。

  1. UART を Open する
  2. I2C Master を Open する
  3. 加速度出力を使えるように最低限の設定を入れる
  4. 読みたい先頭レジスタ番号 0x1F を書く
  5. 2バイト連続で読む
  6. 上位バイトと下位バイトをまとめる
  7. できた値を UART に出す

今回確認したいのは、
「2バイト連続で読み、その2バイトを1つのセンサ値として扱えること」 です。

3. 今回の変更点

3-1. 配線変更

配線は E3-02 と同じ、下記のままです。

  • QCIOT-ICM42688P の J1 を評価ボードの Pmod1(J26)へ接続
  • 評価ボード側は SW4-1 = OFF、SW4-2 = ON
  • J4 にジャンパキャップあり

3-2. 設定変更

FSP の I2C Master 設定は、E3-02 と同じ下記のままで進めます。

  • 通信方式:I2C Master
  • アドレス幅:7bit
  • 通信速度:100kHz
  • スレーブアドレス:0x68
  • Callback:i2c_master_callback

3-3. コード変更

今回は次の処理を追加します。

  • 加速度を使えるように、最低限の設定を書き込む
  • 先頭レジスタ番号 0x1F を送る
  • 続けて 2バイト読む
  • 読み出した2バイトを 1つの16bit値 にまとめる
  • その値を UART に出す

前回E3-02では「1バイトだけ読む」でしたが、今回は
「連続した複数バイトを読んで、まとめて1つの値にする」 ところが違いです。

4. 手順

4-1. E3-02 と同じ条件どおりに接続する

まず、E3-02 と同じ、下記の条件どおりに接続します。

  • QCIOT-ICM42688P の J1 を評価ボードの Pmod1(J26)へ接続
  • SW4-1 = OFF
  • SW4-2 = ON
  • J4 にジャンパキャップあり

4-2. E3-02 の I2C Master 設定が入ったプロジェクトをベースにする

今回は、E3-02 で WHO_AM_I 読みまでできたプロジェクトをそのまま使います。
つまり、

  • UARTログが出せる
  • I2C Master が Open できる
  • WHO_AM_I が読める

状態から始めます。

4-3. USB-UARTケーブルのUARTと評価ボードを接続する

E3-02 と同じように、Tera Term で UART ログを見られる状態にします。

4-4. 今回は「加速度X軸の2バイト」を読む

今回は、加速度X軸のデータを読みます。

使うレジスタは次の2つです。

  • 0x1F:加速度X軸 上位バイト
  • 0x20:加速度X軸 下位バイト

今回は先頭の 0x1F を指定して、2バイトまとめて読む 形で進めます。

このとき大事なのは、
「0x1F を読んで終わり」ではなく、「0x1F から 2バイト連続で読む」
ということです。

4-5. 加速度を使えるように最低限の設定を書く

WHO_AM_I はそのまま読めましたが、加速度データを読むには、加速度側を使える状態にしておく必要があります。

今回は初心者向けに、最低限だけ次の設定を入れます。

  • PWR_MGMT0 に書いて、加速度を ON にする
  • ACCEL_CONFIG0 に書いて、加速度の範囲と更新周期を決める

今回は細かい意味を全部追わず、

  • 加速度を有効にする
  • 読みやすい設定にする

くらいの理解で十分です。

4-6. コードを追加する

今回は、次の流れになるようにコードを追加します。

  1. UART を Openする
  2. I2C Master を Openする
  3. スレーブアドレスを 0x68 に設定する
  4. 加速度用の最低限の設定を書く
  5. 先頭レジスタ番号 0x1F を送る
  6. 2バイト読む
  7. 上位バイトと下位バイトを1つにまとめる
  8. まとめた値を UART に出力する

5. コード

※下記は考え方を分かりやすくするためのシンプルな例です。
※ I2Cインスタンス名、UART送信関数名、コールバックの有無は、自分のプロジェクトに合わせて置き換えてください。
※ FSPの使い方や環境差によっては、関数名や引数の細かい部分を調整してください。

#include "hal_data.h"
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <stdint.h>

#define IMU_I2C_ADDR              (0x68U)

/* ICM-42688-P Register */
#define IMU_REG_ACCEL_DATA_X1     (0x1FU)
#define IMU_REG_PWR_MGMT0         (0x4EU)
#define IMU_REG_ACCEL_CONFIG0     (0x50U)

/* 設定値
 * PWR_MGMT0 = 0x03
 *   GYRO_MODE  = OFF
 *   ACCEL_MODE = LN
 *
 * ACCEL_CONFIG0 = 0x68
 *   ACCEL_FS_SEL = ±2g
 *   ACCEL_ODR    = 100Hz
 */
#define IMU_PWR_MGMT0_ACCEL_LN    (0x03U)
#define IMU_ACCEL_CONFIG0_2G_100HZ (0x68U)

#define I2C_TIMEOUT_COUNT         (100000U)

static volatile bool g_i2c_done  = false;
static volatile bool g_i2c_error = false;

static void uart_print(const char * p_text);
static bool imu_write_register_1byte(uint8_t reg_addr, uint8_t value);
static bool imu_read_register_bytes(uint8_t reg_addr, uint8_t * p_buffer, uint32_t length);
static bool imu_accel_init(void);

void i2c_master_callback(i2c_master_callback_args_t * p_args)
{
    if (NULL == p_args)
    {
        return;
    }

    switch (p_args->event)
    {
        case I2C_MASTER_EVENT_ABORTED:
        {
            g_i2c_error = true;
            g_i2c_done  = true;
            break;
        }

        case I2C_MASTER_EVENT_RX_COMPLETE:
        case I2C_MASTER_EVENT_TX_COMPLETE:
        {
            g_i2c_error = false;
            g_i2c_done  = true;
            break;
        }

        default:
        {
            break;
        }
    }
}

void hal_entry(void)
{
    fsp_err_t err;
    uint8_t accel_raw[2] = {0U, 0U};
    int16_t accel_x = 0;
    char msg[96];

    /* UART Open */
    err = R_SCI_B_UART_Open(&g_uart0_ctrl, &g_uart0_cfg);
    if (FSP_SUCCESS != err)
    {
        while (1)
        {
            ;
        }
    }

    uart_print("ACCEL X read start\r\n");

    /* I2C Open */
    err = R_IIC_MASTER_Open(&g_i2c_master0_ctrl, &g_i2c_master0_cfg);
    if (FSP_SUCCESS != err)
    {
        uart_print("I2C open error\r\n");
        while (1)
        {
            ;
        }
    }

    err = R_IIC_MASTER_SlaveAddressSet(&g_i2c_master0_ctrl,
                                       IMU_I2C_ADDR,
                                       I2C_MASTER_ADDR_MODE_7BIT);
    if (FSP_SUCCESS != err)
    {
        uart_print("Slave address set error\r\n");
        while (1)
        {
            ;
        }
    }

    if (!imu_accel_init())
    {
        uart_print("Accel init error\r\n");
        while (1)
        {
            ;
        }
    }

    if (imu_read_register_bytes(IMU_REG_ACCEL_DATA_X1, accel_raw, 2U))
    {
        accel_x = (int16_t)(((uint16_t)accel_raw[0] << 8) | accel_raw[1]);

        snprintf(msg, sizeof(msg),
                 "ACCEL_X raw = 0x%02X%02X (%d)\r\n",
                 accel_raw[0], accel_raw[1], accel_x);
        uart_print(msg);

        if (accel_x >= 0)
        {
            uart_print("ACCEL_X is plus or zero\r\n");
        }
        else
        {
            uart_print("ACCEL_X is minus\r\n");
        }
    }
    else
    {
        uart_print("ACCEL_X read error\r\n");
    }

    while (1)
    {
        ;
    }
}

static bool imu_accel_init(void)
{
    if (!imu_write_register_1byte(IMU_REG_PWR_MGMT0, IMU_PWR_MGMT0_ACCEL_LN))
    {
        return false;
    }

    /* 加速度をOFFからONにした直後は少し待つ */
    for (volatile uint32_t i = 0; i < 50000U; i++)
    {
        __asm volatile ("nop");
    }

    if (!imu_write_register_1byte(IMU_REG_ACCEL_CONFIG0, IMU_ACCEL_CONFIG0_2G_100HZ))
    {
        return false;
    }

    return true;
}

static bool imu_write_register_1byte(uint8_t reg_addr, uint8_t value)
{
    fsp_err_t err;
    uint8_t write_buf[2];
    uint32_t timeout;

    write_buf[0] = reg_addr;
    write_buf[1] = value;

    g_i2c_done  = false;
    g_i2c_error = false;

    err = R_IIC_MASTER_Write(&g_i2c_master0_ctrl, write_buf, 2U, false);
    if (FSP_SUCCESS != err)
    {
        return false;
    }

    timeout = I2C_TIMEOUT_COUNT;
    while ((false == g_i2c_done) && (timeout > 0U))
    {
        timeout--;
    }

    if ((0U == timeout) || (true == g_i2c_error))
    {
        return false;
    }

    return true;
}

static bool imu_read_register_bytes(uint8_t reg_addr, uint8_t * p_buffer, uint32_t length)
{
    fsp_err_t err;
    uint32_t timeout;

    if ((NULL == p_buffer) || (0U == length))
    {
        return false;
    }

    /* 1) 読みたい先頭レジスタ番号を書く */
    g_i2c_done  = false;
    g_i2c_error = false;

    err = R_IIC_MASTER_Write(&g_i2c_master0_ctrl, ®_addr, 1U, true);
    if (FSP_SUCCESS != err)
    {
        return false;
    }

    timeout = I2C_TIMEOUT_COUNT;
    while ((false == g_i2c_done) && (timeout > 0U))
    {
        timeout--;
    }

    if ((0U == timeout) || (true == g_i2c_error))
    {
        return false;
    }

    /* 2) 指定バイト数だけ連続で読む */
    g_i2c_done  = false;
    g_i2c_error = false;

    err = R_IIC_MASTER_Read(&g_i2c_master0_ctrl, p_buffer, length, false);
    if (FSP_SUCCESS != err)
    {
        return false;
    }

    timeout = I2C_TIMEOUT_COUNT;
    while ((false == g_i2c_done) && (timeout > 0U))
    {
        timeout--;
    }

    if ((0U == timeout) || (true == g_i2c_error))
    {
        return false;
    }

    return true;
}

static void uart_print(const char * p_text)
{
    fsp_err_t err;

    err = R_SCI_B_UART_Write(&g_uart0_ctrl, (uint8_t *) p_text, strlen(p_text));
    if (FSP_SUCCESS != err)
    {
        while (1)
        {
            ;
        }
    }

    for (volatile uint32_t i = 0; i < 1000000U; i++)
    {
        __asm volatile ("nop");
    }
}

6. コードの見方(今回大事なところだけ)

6-1. なぜ 0x1F から 2バイト読むのか

今回は ACCEL_DATA_X1 が先頭です。
その次のアドレスに ACCEL_DATA_X0 があります。
そのため、

  • 先頭アドレス 0x1F を指定する
  • 2バイト読む

とすると、

  • 1バイト目:上位バイト
  • 2バイト目:下位バイト

として受け取れます。

ここが、今回の 連続読み の基本です。

6-2. なぜ (raw[0] << 8) | raw[1] なのか

2バイトをそのまま別々に見ても、加速度の値としては扱いにくいです。
そこで、

  • 上位バイトを左へ8bitずらす
  • 下位バイトを足し合わせる

ことで、1つの16bit値にまとめます。

accel_x = (int16_t)(((uint16_t)accel_raw[0] << 8) | accel_raw[1]);

最初は、

  • accel_raw[0] が上側
  • accel_raw[1] が下側
  • 2つをつないで1つにしている

と分かれば十分です。

6-3. なぜ int16_t にしているのか

加速度の値は、プラスにもマイナスにもなります。
そのため、符号付き の型で受けています。

今回は、

  • 傾きや向きによって正負が変わることがある
  • そのため uint16_t ではなく int16_t で受ける

という入口だけ分かればOKです。
「符号付きの詳しい仕組み」は、次以降で少しずつ慣れれば大丈夫です。

7. 実行結果

7-1. 確認したい動き

今回の実験では、次のようなログになればOKです。

ACCEL X read start  
ACCEL_X raw = 0x0123 (291)  
ACCEL_X is plus or zero

または、向きによっては次のようなログでもOKです。

ACCEL X read start  
ACCEL_X raw = 0xFF20 (-224)  
ACCEL_X is minus

値そのものは、置き方や向きで変わります。
今回の段階では、毎回まったく同じ数値になること よりも、

  • 2バイトが読めている
  • 1つの値として表示できている
  • 向きを変えると値が変わることがある

のほうが大事です。

7-2. 今回確認できればよいこと

今回の段階では、次が確認できれば十分です。

  • 2バイト連続読み ができる
  • 上位バイトと下位バイトを 1つの16bit値 にできる
  • その値を 符号付きの数値 として扱える

8. ハマりポイント/原因と対策

8-1. ACCEL_X read error になる

原因

  • E3-02 の 1バイトの読み出しが成功するときと失敗するときがある
  • 読みたいレジスタ番号を送ったあとで、そのデータを読む処理まで正しく進めていない
  • 2バイト読み出し処理に変更したときに、読み出しが終わる前に次の処理へ進んでしまっている
  • 2バイトを受け取る場所や、2バイトを読む指定が正しくない
  • タイムアウトの時間が短すぎる

対策

  • まず E3-02 の WHO_AM_I = 0x47 が、毎回同じように読める状態に戻す
  • 「先頭レジスタ番号を書いてから、まとめて読む」流れになっているか見直す
  • コールバックで TX_COMPLETE / RX_COMPLETE を受けているか確認する
  • まずは 2バイトの読み出しだけに絞り、余分な処理を入れない

8-2. 値がいつも 0 付近や不自然な値になる

原因

  • 加速度を有効にする設定が入っていない
  • 設定を書いたあと、少し待たずにすぐ読んでいる
  • 読み始めるレジスタ番号がずれている
  • 上位バイト / 下位バイトの組み立て方が間違っている

対策

  • PWR_MGMT0 への設定を書いているか確認する
  • 設定を書いたあとに少し待ってから読む
  • 読み始めるレジスタが 0x1F になっているか確認する
  • ((upper << 8) | lower) の形になっているか見直す

8-3. 正負が逆に見える/大きすぎる値に見える

原因

  • uint16_t のまま扱っていて、符号付きになっていない
  • 上位バイトと下位バイトの順番を逆にしている
  • 16bit値にまとめる前と後の型変換が合っていない

対策

  • 最終的に int16_t で受ける形にする
  • raw[0] を上位、raw[1] を下位として扱っているか確認する
  • まずは UART に 上位バイト / 下位バイト / 合成後の値 を全部出して切り分ける

8-4. Accel init error になる

原因

  • 設定用レジスタ書き込みができていない
  • I2Cアドレス設定や通信の基本条件が崩れている
  • 生成コードの名前と、コード中の名前が合っていない

対策

  • まず E3-01 / E3-02 に戻り、0x68 応答確認と WHO_AM_I 読み出しを再確認する
  • 設定レジスタへの書き込みを1つずつ切り分ける
  • g_i2c_master0 などの名前が生成コードと一致しているか確認する
  • FSP設定保存後に Generate Project Content を実行する

9. 今回わかったこと

今回の実験で大事なのは、
センサ値は 1バイトではなく、複数バイトをまとめて1つの値として扱うことが多い
ということです。

そして、

  • 先頭レジスタを指定する
  • 必要なバイト数を連続で読む
  • 上位と下位をまとめる

という流れができると、
今後の 加速度・ジャイロ・温度などの読み取り に進みやすくなります。

10. 次回やること

E3-04 “成功体験センサ”を1個読む:I2Cで値が取れる感覚を、より分かりやすいセンサでも確認する

11. 関連リンク

  • 実験シリーズ:E2-02 タイマで1ms毎にカウントアップするストップウォッチを作る(リンク)
  • 実験シリーズ:E2-03 タイマで周期処理(リンク)
  • 実験シリーズ:E2-05 状態で整理(IDLE / RUN / ERROR)(リンク)
  • 実験シリーズ:E3-00 I2C配線の“詰まりどころ”を先に潰す(リンク)
  • 実験シリーズ:E3-01 I2C接続確認:0x68 に対して応答があるか確認する(リンク)
  • 実験シリーズ:E3-02 レジスタ値の読み出し:WHO_AM_I を読んで、相手が ICM-42688-P であることを確認する(リンク)
  • 基礎シリーズ:電気回路 はじめに読む(リンク)
  • 基礎シリーズ:電子回路 はじめに読む(リンク)
  • 基礎シリーズ:GNDとは?なぜ基準点が必要なのか?(リンク)
  • 基礎シリーズ:マイコン(RA8M2) はじめに読む(リンク)
  • 基礎シリーズ:組み込み開発 はじめに読む(リンク)
  • 基礎シリーズ:C言語 はじめに読む(リンク)

-フェーズ3