Zephyr+micro:bitでドットを食べるゲームを作ったよ!!

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

# はじめに

最近、ZephyrRTOSで遊んでいますが、楽しいです。
組み込み開発環境が比較的簡単に構築できることが驚きです。

micro:bitには、色々なセンサーやアクチュエーターが最初から利用可能になっています。
そこで、ひとつゲームでも作ってみるかということで、簡単なゲームを作ってみました。

# ゲームの遊び方

<iframe width="560" height="315" src="https://www.youtube.com/embed/3Ja93G07zkE?si=fGYy4KOmJSnWIxQO" 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>

このゲームは、制限時間内にいくつのドットを食べることができるかを競うゲームです。

はじめに、Aボタンを押してゲームを開始します。

micro:bitを傾けることで、自機のドットが上下左右に移動します。
このドットをもう一つ現れるドットに重ねることで、そのドットを食べて点数を獲得することができます。
この時、食べたことがわかるように、音が出ます。

制限時間が終わると、"GAME OVER!!"の文字に続いて、スコアが表示されます。

# 準備とbuild

micro:bitでZephyr開発環境を作る方法は、[macOSでZephyrのmicro:bitデモを動かす](https://qiita.com/610t/items/65feb82e35db99568d25) [^1] をご覧ください。

[^1]: https://qiita.com/610t/items/65feb82e35db99568d25

またbuild手順も同じページで解説してありますが、実際には以下のようなコマンドを実行します。ディレクトリ名は適宜変更してください。

```shell
west build -p always -b bbc_microbit_v2 samples/boards/bbc/microbit/dot_eater
```

flashは以下のように行います。micro:bitはJ-Linkファームウエア化されていることを仮定しています。

```shell
west flash --runner jlink
```

# コード

コードの全ては、 https://github.com/610t/zephyr-playground/tree/main/samples/boards/bbc/microbit/dot_eater にあります。

加速度センサーLSM303用のコードである`src/lsm303_ll.c`と`src/lsm303_ll.h`についての解説は、[Zephyr and the BBC Microbit V2 Tutorial Part 3: I2C](https://zephyrproject.org/zephyr-and-the-bbc-microbit-v2-tutorial-part-3-i2c/) [^2] をご覧ください。実際のコードは、[こちら](https://github.com/fduignan/zephyr_bbc_microbit_v2/tree/main/zephyr_3.7.0/lsm_303_low_level) [^3] です。

[^2]: https://zephyrproject.org/zephyr-and-the-bbc-microbit-v2-tutorial-part-3-i2c/
[^3]: https://github.com/fduignan/zephyr_bbc_microbit_v2/tree/main/zephyr_3.7.0/lsm_303_low_level

他の部分は、基本的に、micro:bitデモの`display`と`sound`デモのコードから流用しています。
このゲームでは、micro:bitの以下の様な機能を使っています。

- ボタンスイッチA(GPIO)
- 5x5 LED
- 加速度センサー:LSM303(I2C)
- ブザー(PWM)

```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>

#include "lsm303_ll.h"

bool game_flag = false;

// ボタンスイッチ(GPIO)用構造体
static const struct gpio_dt_spec sw0_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(sw0), gpios);

// PWM用構造体
static const struct pwm_dt_spec pwm = PWM_DT_SPEC_GET(DT_PATH(zephyr_user));

// ボタンコールバック: ゲームスタート用
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)
{
        // ディスプレイ用構造体
        struct mb_display *disp = mb_display_get();
 
        // GPIO用コールバック構造体
        static struct gpio_callback button_cb_data;

        // GPIOの初期化
        //// ピンの設定
        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);  

        // 加速度センサーの初期化
        int ret = lsm303_ll_begin();
        if (ret < 0) {
                printf("\nError initializing lsm303.  Error code = %d\n",ret);  
                while(1);
        }

        int score=0;
        while (1) {
                int x,y;
                int x_accel,y_accel,z_accel;
                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<400;timer++) {
                        // 加速度センサーの値を取得
                        x_accel = lsm303_ll_readAccelX();
                        y_accel = lsm303_ll_readAccelY();
                        z_accel = lsm303_ll_readAccelZ();
                        printk("Accel:(%d,%d,%d),t:%d\n",x_accel,y_accel,z_accel,timer);

                        // ディスプレイに表示  
                        x = (int)(x_accel+1000)*5/2000;
                        y = 4-(int)(y_accel+1000)*5/2000;

                        // ディスプレイへの画像パターン表示:mb_display_image()
                        struct mb_image pixel = {};
                        pixel.row[y] = BIT(x);
                        pixel.row[e_y] = BIT(e_x);
                        mb_display_image(disp, MB_DISPLAY_MODE_SINGLE, SYS_FOREVER_US, &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を使って音を出す
                                pwm_set_dt(&pwm, pwm.period, pwm.period / 2U);
                                k_sleep(K_MSEC(60));
                                pwm_set_dt(&pwm, 0, 0);
                        }
                        k_msleep(25);
                }
                // 文字列"GAME OVER!!"の表示:mb_display_print()
                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(2000);
                game_flag = false;
        }
}
```

## コード解説
ここでは、コードに関して少し細かく説明していきます。

### ボタンスイッチ(GPIO)

ボタンスイッチ(GPIO)の処理は、今回はコールバック関数を用いて行いました。
もっと単純に値を取ることもできるかと思ったのですが、うまくいかなかったのでサンプル`sound`で使われているこの方法に落ち着きました。
初期化処理は少し複雑に見えるかもしれませんが、ほとんどが割り込みとコールバック関数割り当てに関するものです。

```c:main.c:ボタンスイッチ関連
#include <zephyr/drivers/gpio.h>
  ...
// ボタンスイッチ(GPIO)用構造体
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)
{
        if (pins & BIT(sw0_gpio.pin)) {
                printk("A pressed\n");
                game_flag = true;
        }
}
  ...
        // GPIO用コールバック構造体
        static struct gpio_callback button_cb_data;
        
        // GPIOの初期化
        //// ピンの設定
        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);                      
  ...
```

### 5x5 LED

5x5 LEDは、簡単に利用できるようなサポート関数が用意されています。
詳細は、[BBC micro:bit display APIs](https://docs.zephyrproject.org/apidoc/latest/group__mb__display.html) [^4] をご覧ください。

[^4]: https://docs.zephyrproject.org/apidoc/latest/group__mb__display.html

初期化には、`mb_display_get()`を使います。

このコードでは、表示のために、以下の二つの関数を利用しました。

- `mb_display_image()`:画像パターンの表示
- `mb_display_print()`:文字列の表示

```c:main.c:5x5 LED関連
#include <zephyr/display/mb_display.h>
  ...
        // ディスプレイ用構造体
        struct mb_display *disp = mb_display_get();
  ...
                        // ディスプレイへの画像パターン表示:mb_display_image()
                        struct mb_image pixel = {}; // 画像構造体を全て0で初期化
                        pixel.row[y] = BIT(x);      // (x,y)のドットを点灯
                        mb_display_image(disp, MB_DISPLAY_MODE_SINGLE, SYS_FOREVER_US, &pixel, 1);
  ...
                // 文字列"GAME OVER!!"の表示:mb_display_print()
                mb_display_print(disp, MB_DISPLAY_MODE_SINGLE, 0.25 * MSEC_PER_SEC, "GAME OVER!!");
   ...
                // 書式フォーマットありの整数表示
                mb_display_print(disp, MB_DISPLAY_MODE_SINGLE, 1 * MSEC_PER_SEC, "%d", score);
   ...
```

### 音を出す(PWM)

micro:bit付属のブザーは、PWMを経由して利用することができます。

初期化には、`PWM_DT_SPEC_GET(DT_PATH(zephyr_user))`マクロを利用します。

実際に音を出すときには、`pwm_set_dt(&pwm, pwm.period, pwm.period / 2U);`で周期`period`を指定して音を出します。
音を止めるには、周期に0を指定してあげます。

```c:main.c:PWM関連
#include <zephyr/drivers/pwm.h>
  ...
// PWM用構造体
static const struct pwm_dt_spec pwm = PWM_DT_SPEC_GET(DT_PATH(zephyr_user));
  ...
                                // PWMを使って音を出す
                                pwm_set_dt(&pwm, pwm.period, pwm.period / 2U);
                                k_sleep(K_MSEC(60));
                                pwm_set_dt(&pwm, 0, 0);
  ...
```

### 加速度センサー:LSM303(I2C)

加速度センサー周りのコードは、[Zephyr and the BBC Microbit V2 Tutorial Part 3: I2C](https://zephyrproject.org/zephyr-and-the-bbc-microbit-v2-tutorial-part-3-i2c/)に解説されている [lsm303_ll.h](https://github.com/fduignan/zephyr_bbc_microbit_v2/blob/main/zephyr_3.7.0/lsm_303_low_level/src/lsm303_ll.h)と[lsm303_ll.c](https://github.com/fduignan/zephyr_bbc_microbit_v2/blob/main/zephyr_3.7.0/lsm_303_low_level/src/lsm303_ll.c)  を利用しています。

`main.c`での初期化や加速度センサーの値を取る関数は、ここで提供されているものを利用しました。

```c:main.c:5x5 加速度センター関連
#include "lsm303_ll.h"
  ...
        // 加速度センサーの初期化
        int ret = lsm303_ll_begin();
  ...
                        // 加速度センサーの値を取得
                        x_accel = lsm303_ll_readAccelX();
                        y_accel = lsm303_ll_readAccelY();
                        z_accel = lsm303_ll_readAccelZ();
  ...
```

実際の実装を見てみると、I2C関連の処理は以下のようになっています。
詳しくは、[I2C Interface](https://docs.zephyrproject.org/apidoc/latest/group__i2c__interface.html) [^5] をご覧ください。
初期化シーケンスなどに関しては、[LSM303DLHCデータシート](https://www.st.com/resource/en/datasheet/lsm303dlhc.pdf) [^6] をご覧ください。

[^5]: https://docs.zephyrproject.org/apidoc/latest/group__i2c__interface.html
[^6]: https://www.st.com/resource/en/datasheet/lsm303dlhc.pdf

初期設定などのデータの書き込みには`i2c_reg_write_byte()`を、実際のデータの読み込みには`i2c_burst_read()`や`i2c_read()`を使っています。

```c:lsm303_ll.c
#include <zephyr/drivers/i2c.h>
  ...
// I2C用構造体
static const struct device *i2c;
  ...
    // I2Cデバイスの初期化
	i2c = DEVICE_DT_GET(DT_NODELABEL(i2c0));
  ...
    // LSM303が利用可能か調べる:0x70がLSM303のI2Cアドレス
    uint8_t dummy_value[5];
    nack=i2c_read(i2c,dummy_value,1,0x70);
  ...
    // LSM303の初期化:
    //// LSM303を起動 (max speed, all accel
	lsm303_ll_writeRegister(0x20,0x77);     channels)
    //// 高解像度モード +/- 2g を有効化
	lsm303_ll_writeRegister(0x23,0x08);    
    //// 温度補償と連続10Hz変換を有効化
	lsm303_ll_writeMagRegister(0x60,0x80);
    //// ensure bytes from different readings don't get mixed up.
	lsm303_ll_writeMagRegister(0x62,0x10); 
  ...
// X軸の加速度を求める
int lsm303_ll_readAccelX() {
  ...
    // X軸データを読み込む
    //// LSM303_ACCEL_ADDRESS(=0x19): デバイスアドレス
    //// 0xa8: 開始アドレス
    //// buf,2: 2byte読み込み
	i2c_burst_read(i2c,LSM303_ACCEL_ADDRESS,0xa8, buf,2);
  ...
// レジスタへのデータ書き込み用補助関数
int lsm303_ll_writeRegister(uint8_t RegNum, uint8_t Value) {
  ...
    // データの書き込み: 
	nack=i2c_reg_write_byte(i2c,LSM303_ACCEL_ADDRESS,RegNum,Value);
  ...
```

# おわりに

Zephyrとmicro:bitを使って、簡単なゲームを作ってみました。
micro:bitは、5x5 LEDや加速度センサーなどが最初からあるので、色々な用途で利用できる可能性があると思います。

あなたも、Zephyrとmicro:bitで遊んでみませんか?