Zephyr RTOS 〜 I2C 通信の方法と VL53L4CD の移植 〜

# 1. はじめに

この記事では、Zephyr で I2C 通信を用いたドライバを自前実装する流れについて説明していきます。

教材やプロジェクトの雛形として、下記リポジトリを公開しており、こちらをベースに話を進めていきます。

**公開リポジトリ(github)**

https://github.com/Corgeek/ZephyrOpsPlaybook

また、本記事は、下記 1. と 2. を踏まえた内容になっていますので、よろしければ参考にしていただければと思います。

1. [Zephyr RTOS 〜 Lチカのその先へ 〜(Qiita記事)](https://qiita.com/Corgeek/items/ca4c515ccf556551562f) [^1]
2. [Zephyr RTOS 〜 GPIO を叩く！ 〜(Qiita記事)](https://qiita.com/Corgeek/items/122a00e430ad0d9c297a) [^2]


# 2. 実機と制御対象

今回は、ToF センサーである VL53L4CD のデバイスドライバを Zephyr 向けに実装しました。
レジスタの設定や全体のフローは、STM が Arduino 向けに公開しているドライバのベタ移植です。
※ 完全移植ではなく測距機能のみとなります。

**製品ページ**

https://www.st.com/ja/imaging-and-photonics-solutions/vl53l4cd.html

**Arduino 向け VL53L4CDドライバ(github)**

https://github.com/stm32duino/VL53L4CD

また、実機は [rpi_pico](https://docs.zephyrproject.org/latest/boards/raspberrypi/rpi_pico/doc/index.html
) [^3] と [nucleo_f401re](https://docs.zephyrproject.org/latest/boards/st/nucleo_f401re/doc/index.html
) [^4] の両方で動作確認しています。

# 3. まずは動かしてみる

青色の基板が今回制御している ToF センサーで、ペンを近づけたり射線をずらしたりしながら、取得した距離(ミリメートル)を printk で背景のモニターに垂れ流しています。

<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38092/5027220c-0b74-f14d-9cf3-82daf807e133.gif width=50%> <img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38092/dedcff32-ca8f-aef9-dec8-247418394be7.png width=45%>

配線は以下の通り

| VL53L4CD 側 pin 名 | rpi_pico 側 pin 名 |
|---|---|
| GND | GND |
| VDD | 3V3 |
| SCL | GP5 |
| SDA | GP4 |
| XSHUT | GP26 |


## 3.1. 環境構築

OS 毎の構築手順は、リポジトリの README.md に記載してありますので、そちらを参照してください。

https://github.com/Corgeek/ZephyrOpsPlaybook/blob/master/README.md

ここでは一通りビルド環境が整い、書き込みができている状態から話を進めます。

:::note info
Windows / Ubuntu 両方の環境で openocd で動作することが確認できており、下記推奨です(Ubuntuは不要)。

「コマンドプロンプトを右クリック」から「管理者として実行」、もしくは「スタートキーを押しながら + X」 で出てくるコンテキストメニューから、「ターミナル(管理者)」で管理者権限付きターミナルを開き、下記コマンドで openocd をインストール。

```
choco install openocd
```

:::

debug probe はこちら

https://www.raspberrypi.com/documentation/microcontrollers/debug-probe.html

## 3.2. ターゲットの指定

playbook/scripts/setup.bat(Windows) もしくは playbook/scripts/setup.sh(Ubuntu) の BOARD_TYPE に対象のボード名を指定してそのスクリプトを実行。

```batch:playbook/scripts/setup.bat (for Windows)
set BOARD_TYPE=bbc_microbit_v2

set SCRIPT_DIR=%~dp0
pushd %SCRIPT_DIR%..
                  :
```
```bash:playbook/scripts/setup.sh (for Ubuntu)
BOARD_TYPE=rpi_pico

SCRIPT_PATH=`readlink -f ${0}`
SCRIPT_DIR=`dirname ${SCRIPT_PATH}`
                  :
```

下記のように west_env.bat が更新(生成)されます。

```bat:playbook/scripts/west_env.bat
BOARD_TYPE=rpi_pico
RUNNER_FLASH="--runner openocd"
RUNNER_DEBUG="--runner openocd"
                  :
```

## 3.3. Kconfig を利用したビルドオプションの変更

prj.conf の下記項目を y にしてドライバを有効にします。
rpi_pico 用の prj.conf は `playbook/boards/raspberrypi/rpi_pico/prj.conf` に配置しています。

```conf:playbook/boards/raspberrypi/rpi_pico/prj.conf
CONFIG_MEASURE_SENSOR=y
CONFIG_TOF_VL53L4CD=y
```
<details><summary>オマケ: menuconfig による設定変更</summary>

先述の直接編集とは別に、**ビルド済みの状態**で下記コマンドを実行すると linux kernel でもお世話になっている menuconfig による変更も可能です。

```bash:
(.venv) user@host:~/zephyrproject/playbook$ west build -t menuconfig
```

現時点では下記に VL53L4CD の設定を設けています。

```bash:
    Onboard device drivers  --->
[*] Enable Measure sensor driver
        Choose measure sensor driver (Enable VL53L4CD driver)  --->
```

<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38092/133a5cf4-6f48-d2a0-cb1c-d6ee6ebd6d6a.png width=48%> <img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38092/83d040ee-8f29-e92f-6d07-69001e85cfbb.png width=48%>

</details>

## 3.4. ログで確認するため printk で出力する

main loop で毎回センサーのシーケンスをまわし、センサーの値を gbf(global buffer) に格納させているので、それを取得して printk で表示させます。

```c:playbook/core/main.c
	uint16_t dist;									// 追記
	while (true) {
		seq_sensor_manager();
		gbf_get_measure(&dist);						// 追記
		printk("dist: %6d\n", dist);				// 追記
		k_msleep(100);
	}
```

## 3.5. ビルド＆書き込み

以上の変更を保存した上で、VSCode 上で Ctrl+Shift+B でメニューを開き `Rebuild` & `Flash`

# 4. 機能と役割の解説

主要なソースコードと役割は以下の通り(実行順)

| path | 役割 |
|---|---|
| core/main.c | main() 関数から始まるループ処理 |
| boards/raspberrypi/rpi_pico/rpi_pico.c | rpi_pico 固有の実装(GPIOやI2Cの初期化を呼ぶ) |
| include/boards/raspberrypi/rpi_pico/rpi_pico.h | rpi_pico 固有の定義(GPIOやI2Cの定義) |
| drivers/i2c/drv_i2c_common.c | i2c の初期化 |
| flow/seq_sensor_manager.c | センサードライバ関連を管理するフローで、VL53L4CDドライバの制御シーケンスを表現(仮) |
| drivers/sensor/measure/drv_tof_vl53l4cd.c | VL53L4CD のドライバ実装部分 |
| drivers/i2c/drv_i2c_common.c | read/write の汎用関数 |
| global/gbf_sensor_database.c | 取得したデータを他スレッドから参照できるように共有バッファに格納 |
| core/main.c | printk で結果出力 |

以降は、これらの各機能を掻い摘んで説明していきます。

## 4.1. I2C ドライバの初期化

下記で、I2C の状態確認と通信速度を設定(I2C_SPEED_STANDARD=100kHz)しています。

```c:playbook/drivers/i2c/drv_i2c_common.c
static struct i2c_bus_config s_i2c_dev[] = {
#ifdef I2C_100KHZ_BUS
    { .bus = I2C_100KHZ_BUS, .speed = I2C_SPEED_STANDARD, .is_ready = false },
#endif // I2C_100KHZ_BUS
};

bool drv_init_i2c(void)
{
    int ret = 0;

    for (size_t i = 0; i < ARRAY_SIZE(s_i2c_dev); ++i) {
        s_i2c_dev[i].is_ready = device_is_ready(s_i2c_dev[i].bus);
        if (s_i2c_dev[i].is_ready == false) {
            printk("i2c is not ready[%s]\n", s_i2c_dev[i].bus->name);
            continue;
        }

        ret = i2c_configure(s_i2c_dev[i].bus, I2C_MODE_CONTROLLER | I2C_SPEED_SET(s_i2c_dev[i].speed));
        if (ret) {
            printk("i2c_configure() failed[%s]\n", s_i2c_dev[i].bus->name);
            s_i2c_dev[i].is_ready = false;
            continue;
        }
    }

    bool result = true;
    for (size_t i = 0; i < ARRAY_SIZE(s_i2c_dev); ++i) {
        result |= s_i2c_dev[i].is_ready;
    }

    return result;
}
```
## 4.2. ピン配の定義箇所

実際のバスや xshut に使用しているピンは下記で定義しています。
`i2c0` バスを `I2C_100KHZ_BUS` として、`GPIO0_PIN26(GP26)` は `TOF_XSHUT` として別名を定義しています。

```c:playbook/include/boards/raspberrypi/rpi_pico/rpi_pico.h
#define I2C_100KHZ_BUS              DEVICE_DT_GET(DT_NODELABEL(i2c0))
#define TOF_XSHUT                   GPIO0_PIN26
```

なお、rpi_pico の `i2c0` は、デフォルトでは `SDA` が `GP4`, `SCL` が `GP5` に設定されています。

```zephyr/boards/raspberrypi/rpi_pico/rpi_pico-pinctrl.dtsi
	i2c0_default: i2c0_default {
		group1 {
			pinmux = <I2C0_SDA_P4>, <I2C0_SCL_P5>;
			input-enable;
			input-schmitt-enable;
		};
	};
```

## 4.3. インスタンス毎の設定を保持

ピン配などの設定は下記構造体として保持しています。
i2c のバスには `I2C_100KHZ_BUS` を、xshut には `TOF_XSHUT` を指定しており、
これらを VL53L4CD ドライバに渡して、実際の制御を走らせます。

```c:playbook/flow/seq_sensor_manager.c
static struct vl53l4cd_ctx s_tof_ctx[TOF_ID_MAX] = {
    [TOF_ID_1ST] = {
        .state = STS_SENSOR_INIT,
        .i2c = I2C_100KHZ_BUS,
        .xshut = TOF_XSHUT,
        .interrupt = GPIO_DUMMY,
        .addr = VL53L4CD_DEF_ADDR,
        .timing_budget_ms = TOF_TIMING_BUDGET_MS,
        .inter_measurement_ms = 0,
    },
};
```

## 4.4 I2C 送受信のラッパー関数

Zephyr のドライバとして i2c_read(), i2c_write() が提供されており、これらを利用して実際の通信を行っています。
また、レジスタ読み書き用として i2c_reg_read_byte() / i2c_reg_write_byte() も提供されていますが、2 byte レジスタ用の wreg や word/dword 用 がないため追加しています。
(その他、i2c_burst_read() / i2c_burst_write() なども提供されていますが、今回未使用)

```c:playbook/include/drivers/i2c/drv_i2c_common.h
/* for 8bit size register address */
//  int i2c_reg_write_byte(const struct device *dev, uint16_t dev_addr, uint8_t reg_addr, uint8_t value);
int i2c_reg_write_word(const struct device *const i2c_dev, uint16_t slv_addr, uint8_t reg_addr, uint16_t value);
int i2c_reg_write_dword(const struct device *const i2c_dev, uint16_t slv_addr, uint8_t reg_addr, uint32_t value);

//  int i2c_reg_read_byte(const struct device *dev, uint16_t dev_addr, uint8_t reg_addr, uint8_t *value)
int i2c_reg_read_word(const struct device *const i2c_dev, uint16_t slv_addr, uint8_t reg_addr, uint16_t *value);
int i2c_reg_read_dword(const struct device *const i2c_dev, uint16_t slv_addr, uint8_t reg_addr, uint32_t *value);

/* for 16bit size register address */
int i2c_wreg_write_byte(const struct device *const i2c_dev, uint16_t slv_addr, uint16_t reg_addr, uint8_t value);
int i2c_wreg_write_word(const struct device *const i2c_dev, uint16_t slv_addr, uint16_t reg_addr, uint16_t value);
int i2c_wreg_write_dword(const struct device *const i2c_dev, uint16_t slv_addr, uint16_t reg_addr, uint32_t value);

int i2c_wreg_read_byte(const struct device *const i2c_dev, uint16_t slv_addr, uint16_t reg_addr, uint8_t *value);
int i2c_wreg_read_word(const struct device *const i2c_dev, uint16_t slv_addr, uint16_t reg_addr, uint16_t *value);
int i2c_wreg_read_dword(const struct device *const i2c_dev, uint16_t slv_addr, uint16_t reg_addr, uint32_t *value);
```


## 4.5. センサー制御用フロー

簡素な作りですが、下記のように状態遷移形式でドライバの初期化から取得の流れを組んでおり、main() の while loop から周期的に駆動するようになっています。

```c:flow/seq_sensor_manager.c
        struct sensor_value value;

        switch (s_tof_ctx[i].state) {
        case STS_SENSOR_INIT:
            if (drv_init_tof(&s_tof_ctx[i]))
                s_tof_ctx[i].state = STS_SENSOR_SETUP;
            break;

        case STS_SENSOR_SETUP:
            if (drv_tof_setup(&s_tof_ctx[i]))
                s_tof_ctx[i].state = STS_SENSOR_READY;
            break;

        case STS_SENSOR_READY:
            if (drv_tof_start(&s_tof_ctx[i]) == 0)
                s_tof_ctx[i].state = STS_SENSOR_ACTIVE;
            break;

        case STS_SENSOR_ACTIVE:
            drv_tof_fetch(&s_tof_ctx[i], &value);
            gbf_set_measure((uint16_t)(value.val1));
            break;

        case STS_SENSOR_FATAL:
        default:
            break;
        }
```

# 5. おまけ

nucleo_f401re の雄姿と、ピン配＆dtsの設定も載せておきます。

<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38092/5c87d9dc-767f-715c-0a0c-4ad39501892d.png width=40%> 

| VL53L4CD 側 pin 名 | nucleo_f401re 側 pin 名 |
|---|---|
| GND | GND |
| VDD | 3V3 |
| SCL | PB_8 |
| SDA | PB_9 |
| XSHUT | PA_0 |

```c:playbook/include/boards/st/nucleo_f401re/nucleo_f401re.h
#define I2C_100KHZ_BUS              DEVICE_DT_GET(DT_NODELABEL(i2c1))
#define	GREEN_LED					GPIOA_PIN05
#define USER_BUTTON                 GPIOC_PIN13
#define TOF_XSHUT                   GPIOA_PIN00
```

I2C1 のデフォルト設定は、SCL = PB_8, SDA = PB_9
```zephyr/boards/st/nucleo_f401re/nucleo_f401re.dts
&i2c1 {
	pinctrl-0 = <&i2c1_scl_pb8 &i2c1_sda_pb9>;
	pinctrl-names = "default";
	status = "okay";
	clock-frequency = <I2C_BITRATE_FAST>;
};
```

# 6. まとめ

GPIO の制御に続いて、I2C を用いたデバイス制御方法について、実装例を交えて紹介しました。

I2C に於いても、dts で定義された設定を `struct device` 構造体経由でアクセスする事は変わりなく、
こちらも初期化をC言語で表現するか、静的に dts に記述してしまうかのどちらかになるかと思います。

「メモリの1byteは血の一滴」という昔(？)話もありますが、せっかく高い移植性を備えた Zephyr ですし、このような運用も今なら許されるのではないでしょうか…？

そろそろタイマーやスレッドが欲しくなってきましたが、開発環境の改善などもやりたいところ。折を見て追記していければと思います。

[^1]: Zephyr RTOS 〜 Lチカのその先へ 〜(Qiita記事) https://qiita.com/Corgeek/items/ca4c515ccf556551562f
[^2]: Zephyr RTOS 〜 GPIO を叩く！ 〜(Qiita記事) https://qiita.com/Corgeek/items/122a00e430ad0d9c297a
[^3]: Zephyr 公式ドキュメント rpi_pico 個別情報https://docs.zephyrproject.org/latest/boards/raspberrypi/rpi_pico/doc/index.html
[^4]: Zephyr 公式ドキュメント nucleo_f401re 個別情報 https://docs.zephyrproject.org/latest/boards/st/nucleo_f401re/doc/index.html