1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-02-12

この文章は、ZephyrRTOS Advent Calendar 2024の17日目のエントリとして書かれました。

はじめに

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

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

ゲームの遊び方

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

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

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

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

準備とbuild

micro:bitでZephyr開発環境を作る方法は、macOSでZephyrのmicro:bitデモを動かす 1 をご覧ください。

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

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

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

west flash --runner jlink

コード

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

加速度センサーLSM303用のコードであるsrc/lsm303_ll.csrc/lsm303_ll.hについての解説は、Zephyr and the BBC Microbit V2 Tutorial Part 3: I2C 2 をご覧ください。実際のコードは、こちら 3 です。

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

  • ボタンスイッチA(GPIO)
  • 5x5 LED
  • 加速度センサー:LSM303(I2C)
  • ブザー(PWM)
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で使われているこの方法に落ち着きました。
初期化処理は少し複雑に見えるかもしれませんが、ほとんどが割り込みとコールバック関数割り当てに関するものです。

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 4 をご覧ください。

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

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

  • mb_display_image():画像パターンの表示
  • mb_display_print():文字列の表示
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を指定してあげます。

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に解説されている lsm303_ll.hlsm303_ll.c を利用しています。

main.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 5 をご覧ください。
初期化シーケンスなどに関しては、LSM303DLHCデータシート 6 をご覧ください。

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

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で遊んでみませんか?

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

  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

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

  5. https://docs.zephyrproject.org/apidoc/latest/group__i2c__interface.html

  6. https://www.st.com/resource/en/datasheet/lsm303dlhc.pdf

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?