この文章は、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.c
とsrc/lsm303_ll.h
についての解説は、Zephyr and the BBC Microbit V2 Tutorial Part 3: I2C 2 をご覧ください。実際のコードは、こちら 3 です。
他の部分は、基本的に、micro:bitデモのdisplay
とsound
デモのコードから流用しています。
このゲームでは、micro:bitの以下の様な機能を使っています。
- ボタンスイッチA(GPIO)
- 5x5 LED
- 加速度センサー:LSM303(I2C)
- ブザー(PWM)
#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
で使われているこの方法に落ち着きました。
初期化処理は少し複雑に見えるかもしれませんが、ほとんどが割り込みとコールバック関数割り当てに関するものです。
#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()
:文字列の表示
#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を指定してあげます。
#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.hとlsm303_ll.c を利用しています。
main.c
での初期化や加速度センサーの値を取る関数は、ここで提供されているものを利用しました。
#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()
を使っています。
#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で遊んでみませんか?
-
https://zephyrproject.org/zephyr-and-the-bbc-microbit-v2-tutorial-part-3-i2c/ ↩
-
https://github.com/fduignan/zephyr_bbc_microbit_v2/tree/main/zephyr_3.7.0/lsm_303_low_level ↩
-
https://docs.zephyrproject.org/apidoc/latest/group__mb__display.html ↩
-
https://docs.zephyrproject.org/apidoc/latest/group__i2c__interface.html ↩