Zephyrでmicro:bitの外部I2Cを使ってみた

この文章は、[ZephyrRTOS Advent Calendar 2024](https://qiita.com/advent-calendar/2024/zephyr) [^1]の7日目のエントリとして書かれました。

[^1]: https://qiita.com/advent-calendar/2024/zephyr

# はじめに
これまで、何本かmicro:bitでZephyrを使う記事を書いてきましたが、全てサンプルを動かすというレベルのものでした。

そろそろ、なにか適当なアプリケーションを書きたいなぁと思ったところに、[Zephyr and the BBC Microbit V2 Tutorial Part 3: I2C](https://zephyrproject.org/zephyr-and-the-bbc-microbit-v2-tutorial-part-3-i2c/) [^2] というI2Cデバイスを使う記事があることに気が付きました。これは、内蔵I2Cに接続されている加速度センサーLSM303を使うという話になっています。

[^2]: https://zephyrproject.org/zephyr-and-the-bbc-microbit-v2-tutorial-part-3-i2c/

「micro:bitって外側端子にもI2Cが出ているから、ひょっとして外付けでI2Cデバイスが使えるんじゃね」と考えたので、使えないか試してみることにしました。

最終的にI2C接続のジョイスティックを動かすことができたので、報告したいと思います。

# ハードウエア
利用したハードウエアは以下の通りです。
|ハードウエア|SKU|役割|
|--|--|--|
|[micro:bit v2.0](https://www.switch-science.com/products/6600)|SEDU-066006|メインボード|
|[M5Stack用 I2Cジョイスティックユニット](https://www.switch-science.com/products/8101)|M5STACK-U024-C|I2Cジョイスティック|
|[M5:Bit micro:bit用変換ボード](https://www.switch-science.com/products/6469)|M5STACK-A051|micro:bitからI2Cを出す|
|[GROVE - 4ピン-ジャンパメスケーブル](https://www.switch-science.com/products/1048)|SEEED-110990028|M5:Bitとジョイスティック接続用|

接続イメージは、以下の図の通りです。

![接続イメージ図](https://i.gyazo.com/02ef0634a0e027f23c3c49a001e3ba05.png)

以下に各デバイスに関して説明します。

## micro:bit
今回もmicro:bitを利用しました。
J-Link化した[micro:bit v2.0](https://www.switch-science.com/products/6600) [^3] を使っています。
初期設定などの詳細は、[macOSでZephyrのmicro:bitデモを動かす](https://qiita.com/610t/items/65feb82e35db99568d25) [^4] をご覧ください。

[^3]: https://www.switch-science.com/products/6600
[^4]: https://qiita.com/610t/items/65feb82e35db99568d25

::: note warn
最新のmicro:bitはv2.2はJ-Link化できませんので、注意してください。
:::

## M5Stack用 I2Cジョイスティックユニット
https://www.switch-science.com/products/8101

[M5Stack用 I2Cジョイスティックユニット](https://www.switch-science.com/products/8101) [^5] は、Grove接続のI2Cジョイスティックです。

プロトコルは非常に簡単で、I2Cアドレス0x52のレジスタ0x00から3バイト読み込むと、それぞれx方向の値,y方向の値,btn状態が得られます。
初期化などは必要ありません。

[^5]: https://www.switch-science.com/products/8101

## 接続用ハードウエア
以下のハードウエアをmicro:bitとジョイスティックの接続用に使いました。

### M5:Bit micro:bit用変換ボード
https://www.switch-science.com/products/6469

[M5:Bit micro:bit用変換ボード](https://www.switch-science.com/products/6469) [^6] は、micro:bitのコネクタをピンに出すためボードです。この種のボードとしては、非常に安価になっています。
I2Cが右下の端子で2系統出せるようになっています。I2CがGroveで出ていればもっと良かったのですが…
さらに、シリアルはGrove端子に出ているので、簡単に利用できます。

[^6]: https://www.switch-science.com/products/6469

この製品に限らず、micro:bitからI2Cを出すボードは各種出ていますので、どれを使っても問題ありません。
売り切れが多いですが、["grove micro:bit"で検索する](https://www.switch-science.com/search?q=grove+micro%3Abit) [^7] とそのような商品が出てきます。
いくつかの商品は直接Grove接続できるので、次のケーブルを使わなくても、普通のGroveケーブルでジョイスティックと接続できるようになります。

[^7]: https://www.switch-science.com/search?q=grove+micro%3Abit

### GROVE - 4ピン-ジャンパメスケーブル
https://www.switch-science.com/products/1048

[GROVE - 4ピン-ジャンパメスケーブル](https://www.switch-science.com/products/1048) [^8] は、グローブケーブルを2.54 mmピッチのジャンパメスコネクタに変換するケーブルです。
M5:Bit側が4ピンオスコネクタで、ジョイスティック側がGroveになるため、必要になります。

[^8]: https://www.switch-science.com/products/1048

::: note warn
Seeed製品のGroveケーブルとM5Stack製品のGroveケーブルでは、黄色と白の信号線の色が反対になっているので注意してください。
私は、これで数時間溶かしました…
:::

# ソフトウエア:デモプログラムソースコード

ここでは、ソフトウエアについて解説します。

全てのコードは、 https://github.com/610t/zephyr-playground で公開しています。

## ジョイスティックの値をシリアルに出力する:`m5stack_joystick`
今回作成したアプリケーションのソースコードは、以下の通りです。
githubでは、 https://github.com/610t/zephyr-playground/tree/main/samples/boards/bbc/microbit/m5stack_joystick にあります。
他のサンプルと同じように、`zephyr/samples/boards/bbc/microbit/m5stack_joystick/`に以下のように配置していることを仮定しています。
- `prj.conf`
- `CMakeLists.txt`
- `app.overlay`
- `src/`
  - `main.c`

### プロジェクト設定ファイル:`prj.conf`
プロジェクトで利用する機能を指定します。
今回は、I2Cを有効にしています。

```config:prj.conf
CONFIG_I2C=y
```

### Cmakefile:`CMakeLists.txt`
アプリケーションbuild用の`cmake`の設定ファイルです。
ソースは、`main.c`だけなので、それを指定しているだけです。

```cmake:CMakeLists.txt
cmake_minimum_required(VERSION 3.13.1)

find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(m5stack_joystick)

target_sources(app PRIVATE src/main.c)
```

### Device Tree Source Overlay:`app.overlay`
Device Tree Source(DTS)の上書きを行う場合に利用するファイルです。

外部I2C端子である`i2c1`の定義を行っています。
外部I2Cでは、SDLが`0x20`、SCLが`0x1a`のため、これを指定しています。

```dts:app.overlay
&pinctrl {
        i2c1_default: i2c1_default {
                group1 {
                        psels = <NRF_PSEL(TWIM_SDA, 0, 0x20)>,
                                <NRF_PSEL(TWIM_SCL, 0, 0x1a)>;
                };
        };
};

&i2c1 {
        compatible = "nordic,nrf-twim";
        status = "okay";
        pinctrl-0 = <&i2c1_default>;
        pinctrl-names = "default";
};
```

### アプリケーション本体:`src/main.c`
アプリケーション本体です。
ソースコードをシンプルにするため、返り値を確認してのエラー処理などは全くしてませんので、利用する時はちゃんとするようにしてください。

`i2c = DEVICE_DT_GET(DT_NODELABEL(i2c1))`でI2Cバスの初期化を行うだけで、このデバイスの場合は初期化処理の必要はありません。

データは、`i2c_burst_read(i2c,0x52,0x00,val,3);`で3バイトのデータ`val[]`を受け取ります。
|変数|意味|
|--|--|
|`val[0]`|x方向の値|
|`val[1]`|y方向の値|
|`val[2]`|ボタンの状態|

受け取った値は、`printk`を使ってシリアルコンソールに出力します。

```c:src/main.c
#include <stdio.h>

#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <zephyr/drivers/i2c.h>

static const struct device *i2c;

int main(void)
{
        uint8_t val[3];

        // I2Cの初期化
        i2c = DEVICE_DT_GET(DT_NODELABEL(i2c1));
        k_msleep(1000);

        while (1) {
                // I2Cアドレス0x52のレジスタ0x00から3バイト読み込み
                i2c_burst_read(i2c,0x52,0x00,val,3);

                // 結果の出力
                printk("(%d,%d):%d\n",val[0],val[1],val[2]);

                k_msleep(100);
        }
}
```

## ジョイスティックの状態を5x5 LED画面に表示する:`m5stack_joystick_display`
micro:bitには、5x5 LED画面があるので、ジョイスティックの状態をここに表示することを考えてみます。
githubでのコードは、 https://github.com/610t/zephyr-playground/tree/main/samples/boards/bbc/microbit/m5stack_joystick_display です。
更に、ジョイスティックを押し込んだ時には、`B`を表示し、音をだすことにします。

ここでは、変更が必要なソースだけを紹介します。
`CMakeLists.txt`と`app.overlay`は、`m5stack_joystick`と同じものを利用します。

### プロジェクト設定ファイル:`prj.conf`
今回は、I2C以外にもディスプレイ関係を有効にしています。
更に、音を出すために、PWMも有効にしています。

```config:prj.conf
CONFIG_GPIO=y
CONFIG_DISPLAY=y
CONFIG_MICROBIT_DISPLAY=y
CONFIG_PWM=y
CONFIG_I2C=y
```

### Device Tree Source Overlay:`app.overlay`
今回は、PWMに対する定義の追加が必要です。
他は、これまでと同じです。

```dts:app.overlay
&pinctrl {
        i2c1_default: i2c1_default {
                group1 {
                        psels = <NRF_PSEL(TWIM_SDA, 0, 0x20)>,
                                <NRF_PSEL(TWIM_SCL, 0, 0x1a)>;
                };
        };
};

&i2c1 {
        compatible = "nordic,nrf-twim";
        status = "okay";
        pinctrl-0 = <&i2c1_default>;
        pinctrl-names = "default";
};

/ {
        zephyr,user {
                /* period cell corresponds to initial period */
                pwms = <&pwm1 0 PWM_USEC(1500) PWM_POLARITY_NORMAL>;
        };
};

```

### アプリケーション本体:`src/main.c`
アプリケーション本体です。
`m5stack_joystick`にジョイスティックの値に応じて5x5 LEDに表示を行う機能と、ジョイスティックが押し込まれた時に`B`を表示して音を出す機能を追加しています。

```c:src/main.c
#include <stdio.h>

#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/pwm.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/device.h>

#include <zephyr/display/mb_display.h>

static const struct pwm_dt_spec pwm = PWM_DT_SPEC_GET(DT_PATH(zephyr_user));
static const struct device *i2c;

int main(void)
{
        uint8_t val[3];
        struct mb_display *disp = mb_display_get();
        int x,y;

        // PWMの初期化
        if (!pwm_is_ready_dt(&pwm)) {
                printk("%s: device not ready.\n", pwm.dev->name);
                return 0;
        }

        // I2Cの初期化
        i2c = DEVICE_DT_GET(DT_NODELABEL(i2c1));
        k_msleep(1000);

        while (1) {
                k_msleep(100);
                // I2Cアドレス0x52のレジスタ0x00から3バイト読み込み
                i2c_burst_read(i2c,0x52,0x00,val,3);

                // 結果の出力
                printk("(%d,%d):%d\n",val[0],val[1],val[2]);

                // ディスプレイに表示  
                x = 4-(int)(val[0]*5/255);
                y = (int)(val[1]*5/255);
                struct mb_image pixel = {};
                pixel.row[y] = BIT(x);
                mb_display_image(disp, MB_DISPLAY_MODE_SINGLE, 250, &pixel, 1);

                // ジョイスティックのボタンが押されているときの処理
                //// 画面に"B"を表示して、音を出す
                if(val[2]) {
                        mb_display_print(disp, MB_DISPLAY_MODE_SINGLE, 1 * MSEC_PER_SEC, "B");
                        pwm_set_dt(&pwm, pwm.period, pwm.period / 2U);
                        k_sleep(K_MSEC(60));
                        pwm_set_dt(&pwm, 0, 0);
                }
        }
}

```

## ドットを食べるゲーム:`m5stack_joystick_dot_eater`
最後に、ジョイスティックで自機を操作して、現れるドットを食べていくゲームを作りました。
githubのコードは、 https://github.com/610t/zephyr-playground/tree/main/samples/boards/bbc/microbit/m5stack_joystick_dot_eater です。

はじめに、'A'が表示されている状態でボタンAを押すことでゲームが始まります。
ジョイスティックで自機を動かして、もうひとつのドットで表示される敵機と位置を合わせてください。それで、そのドットを食べることができます。
制限時間内にいくつ食べられるかを競います。
'GAME OVER!!'が表示された後で、スコアが表示されるようになっています。

`src/main.c`以外のファイルは、`m5stack_joystick_display`と同じです。

### アプリケーション本体:`src/main.c`
このプログラムでは、新たにボタンAを扱うために、GPIOのコールバックを用いています。

```c:main.c:GPIOの処理
#include <zephyr/drivers/gpio.h>
  ...
static const struct gpio_dt_spec sw0_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(sw0), gpios);
  ...
// ボタンコールバック: ゲームスタート用
static void button_pressed(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
  // 内容省略
}
  ...
	// GPIOの初期化
	static struct gpio_callback button_cb_data;
    /// ピンの設定
	gpio_pin_configure_dt(&sw0_gpio, GPIO_INPUT|GPIO_ACTIVE_HIGH);
    //// 割り込みの設定
	gpio_pin_interrupt_configure_dt(&sw0_gpio, GPIO_INT_EDGE_TO_ACTIVE);
    //// 割り込み用コールバック関数button_cb_dataの割り当て
	gpio_init_callback(&button_cb_data, button_pressed, BIT(sw0_gpio.pin));
    //// コールバック関数を登録
	gpio_add_callback(sw0_gpio.port, &button_cb_data);
  ...
```

敵機の処理をしている部分以外はこれまでのものと同じなので、詳細は省略します。

```c:main.c
#include <stdio.h>
#include <stdlib.h>

#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/pwm.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/device.h>

#include <zephyr/display/mb_display.h>

bool game_flag = false;

static const struct gpio_dt_spec sw0_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(sw0), gpios);
static const struct pwm_dt_spec pwm = PWM_DT_SPEC_GET(DT_PATH(zephyr_user));
static const struct device *i2c;

// ボタンコールバック: ゲームスタート用
static void button_pressed(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
	if (pins & BIT(sw0_gpio.pin)) {
		printk("A pressed\n");
		game_flag = true;
	}
}

int main(void)
{
	uint8_t val[3];
	struct mb_display *disp = mb_display_get();
        int x,y;

	// PWMの初期化
	if (!pwm_is_ready_dt(&pwm)) {
		printk("%s: device not ready.\n", pwm.dev->name);
		return 0;
	}

	// I2Cの初期化
	i2c = DEVICE_DT_GET(DT_NODELABEL(i2c1));
	k_msleep(1000);

	// GPIOの初期化
	static struct gpio_callback button_cb_data;
	gpio_pin_configure_dt(&sw0_gpio, GPIO_INPUT|GPIO_ACTIVE_HIGH);
	gpio_pin_interrupt_configure_dt(&sw0_gpio, GPIO_INT_EDGE_TO_ACTIVE);
	gpio_init_callback(&button_cb_data, button_pressed, BIT(sw0_gpio.pin));
	gpio_add_callback(sw0_gpio.port, &button_cb_data);

	int score=0;
	while (1) {
		int e_x,e_y;

		// Aボタンでゲーム開始
		while(!game_flag) {
			mb_display_print(disp, MB_DISPLAY_MODE_SINGLE, MSEC_PER_SEC, "A");
			k_msleep(100);
		}

		e_x=rand()%5;
		e_y=rand()%5;

		for(int timer=0;timer<100;timer++) {
			k_msleep(100);
			// I2Cアドレス0x52のレジスタ0x00から3バイト読み込み
			i2c_burst_read(i2c,0x52,0x00,val,3);

			// 結果の出力
			printk("(%d,%d):%d\n",val[0],val[1],val[2]);

			// ディスプレイに表示  
			x = 4-(int)(val[0]*5/255);
			y = (int)(val[1]*5/255);
			struct mb_image pixel = {};
			pixel.row[y] = BIT(x);
			pixel.row[e_y] = BIT(e_x);
			mb_display_image(disp, MB_DISPLAY_MODE_SINGLE, 250, &pixel, 1);
			printk("Pos:dot(%d,%d)==enemy(%d,%d)?\n",x,y,e_x,e_y);
			if(x==e_x && y==e_y) {
				score++;
				printk("score:%d\n",score);
				e_x=rand()%5;
				e_y=rand()%5;
				pwm_set_dt(&pwm, pwm.period, pwm.period / 2U);
				k_sleep(K_MSEC(60));
				pwm_set_dt(&pwm, 0, 0);
			}

			// ジョイスティックのボタンが押されているときの処理
			//// 画面に"B"を表示して、音を出す
			if(val[2]) {
				mb_display_print(disp, MB_DISPLAY_MODE_SINGLE, 1 * MSEC_PER_SEC, "B");
				pwm_set_dt(&pwm, pwm.period, pwm.period / 2U);
				k_sleep(K_MSEC(60));
				pwm_set_dt(&pwm, 0, 0);
			}
			k_msleep(25);
		}
		mb_display_print(disp, MB_DISPLAY_MODE_SINGLE, 0.25 * MSEC_PER_SEC, "GAME OVER!!");
		k_msleep(3000);

		mb_display_print(disp, MB_DISPLAY_MODE_SINGLE, 1 * MSEC_PER_SEC, "%d", score);
		k_msleep(3000);
		game_flag = false;
	}
}
```

# 動作手順
動作手順は、以下の通りです。

## build
以下のコマンドでbuildします。
サンプルディレクトリ(`samples/boards/bbc/microbit/m5stack_joystick`)は適切に設定してください。ここでは、`m5stack_joystick`をbuildすることを仮定しています。

```shell:build
$ west build -p always -b bbc_microbit_v2 samples/boards/bbc/microbit/m5stack_joystick
   ...
[151/151] Linking C executable zephyr/zephyr.elf
Memory region         Used Size  Region Size  %age Used
           FLASH:       27556 B       512 KB      5.26%
             RAM:        5808 B       128 KB      4.43%
        IDT_LIST:          0 GB        32 KB      0.00%
Generating files from /Users/mutoh/work/zephyrproject/zephyr/build/zephyr/zephyr.elf for board: bbc_microbit_v2
```

## flash
J-Link化したmicro:bitでは、以下のコマンドでflashできます。
`--runner jlink`オプションは必要ない場合もあるようですが、J-Linkを強制的に利用するために指定しています。

```shell:flash
$ west flash --runner jlink
-- west flash: rebuilding
ninja: no work to do.
-- west flash: using runner jlink
-- runners.jlink: reset after flashing requested
-- runners.jlink: JLink version: 8.12
-- runners.jlink: Flashing file: /Users/mutoh/work/zephyrproject/zephyr/build/zephyr/zephyr.hex
```

## 動作確認
`printk`の出力は、シリアルコンソールに出されるため、これを確認する必要があります。
今回は、`minicom`を使いました。

```shell:minicomコマンドライン(macOS)
# シリアルデバイスの確認
$ ls -l /dev/cu.*
crw-rw-rw-  1 root  wheel  0x16000005  1  6 11:55 /dev/cu.Bluetooth-Incoming-Port
crw-rw-rw-  1 root  wheel  0x16000011  1 16 08:14 /dev/cu.usbmodem0007820596381
# minicomの起動
$ minicom -D /dev/cu.usbmodem0007820596381
(14,68):0
(33,39):0
(46,29):0
(58,19):1
(59,18):0
(59,39):0
(110,146):1
(104,162):0
(99,196):0
(75,239):1
  ...
```

### `m5stack_joystick`

`m5stack_joystick`の動作の様子は、以下の動画 [^9] のようになります。

<iframe width="560" height="315" src="https://www.youtube.com/embed/rk4jJjwS5Mw?si=NUlJzxE_zU_O-tf6" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

[^9]: [https://youtu.be/rk4jJjwS5Mw](https://youtu.be/rk4jJjwS5Mw)

### `m5stack_joystick_display`

`m5stack_joystick_display`の動作の様子は、以下の動画 [^10] のようになります。

<iframe width="560" height="315" src="https://www.youtube.com/embed/ZFt2L00lDps?si=4DQBTuDhRPftONJR" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

[^10]: [https://www.youtube.com/watch?v=ZFt2L00lDps](https://www.youtube.com/watch?v=ZFt2L00lDps)

# おわりに
サンプルを使うのではなく、はじめてZephyrでコードを書きました。
標準のDTSで定義されていないバスの情報などがアプリケーション側で上書きできたりするのには、ちょっと驚きました。
参考になりましたら、幸いです。