シリーズ:実験シリーズ(フェーズ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. 今回の動作イメージ
今回は、起動後に次の流れで通信します。
- UART を Open する
- I2C Master を Open する
- 加速度出力を使えるように最低限の設定を入れる
- 読みたい先頭レジスタ番号 0x1F を書く
- 2バイト連続で読む
- 上位バイトと下位バイトをまとめる
- できた値を 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. コードを追加する
今回は、次の流れになるようにコードを追加します。
- UART を Openする
- I2C Master を Openする
- スレーブアドレスを 0x68 に設定する
- 加速度用の最低限の設定を書く
- 先頭レジスタ番号 0x1F を送る
- 2バイト読む
- 上位バイトと下位バイトを1つにまとめる
- まとめた値を 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言語 はじめに読む(リンク)