この文章は、ZephyrRTOS Advent Calendar 2024 1の7日目のエントリとして書かれました。
はじめに
これまで、何本かmicro:bitでZephyrを使う記事を書いてきましたが、全てサンプルを動かすというレベルのものでした。
そろそろ、なにか適当なアプリケーションを書きたいなぁと思ったところに、Zephyr and the BBC Microbit V2 Tutorial Part 3: I2C 2 というI2Cデバイスを使う記事があることに気が付きました。これは、内蔵I2Cに接続されている加速度センサーLSM303を使うという話になっています。
「micro:bitって外側端子にもI2Cが出ているから、ひょっとして外付けでI2Cデバイスが使えるんじゃね」と考えたので、使えないか試してみることにしました。
最終的にI2C接続のジョイスティックを動かすことができたので、報告したいと思います。
ハードウエア
利用したハードウエアは以下の通りです。
ハードウエア | SKU | 役割 |
---|---|---|
micro:bit v2.0 | SEDU-066006 | メインボード |
M5Stack用 I2Cジョイスティックユニット | M5STACK-U024-C | I2Cジョイスティック |
M5:Bit micro:bit用変換ボード | M5STACK-A051 | micro:bitからI2Cを出す |
GROVE - 4ピン-ジャンパメスケーブル | SEEED-110990028 | M5:Bitとジョイスティック接続用 |
接続イメージは、以下の図の通りです。
以下に各デバイスに関して説明します。
micro:bit
今回もmicro:bitを利用しました。
J-Link化したmicro:bit v2.0 3 を使っています。
初期設定などの詳細は、macOSでZephyrのmicro:bitデモを動かす 4 をご覧ください。
最新のmicro:bitはv2.2はJ-Link化できませんので、注意してください。
M5Stack用 I2Cジョイスティックユニット
M5Stack用 I2Cジョイスティックユニット 5 は、Grove接続のI2Cジョイスティックです。
プロトコルは非常に簡単で、I2Cアドレス0x52のレジスタ0x00から3バイト読み込むと、それぞれx方向の値,y方向の値,btn状態が得られます。
初期化などは必要ありません。
接続用ハードウエア
以下のハードウエアをmicro:bitとジョイスティックの接続用に使いました。
M5:Bit micro:bit用変換ボード
M5:Bit micro:bit用変換ボード 6 は、micro:bitのコネクタをピンに出すためボードです。この種のボードとしては、非常に安価になっています。
I2Cが右下の端子で2系統出せるようになっています。I2CがGroveで出ていればもっと良かったのですが…
さらに、シリアルはGrove端子に出ているので、簡単に利用できます。
この製品に限らず、micro:bitからI2Cを出すボードは各種出ていますので、どれを使っても問題ありません。
売り切れが多いですが、"grove micro:bit"で検索する 7 とそのような商品が出てきます。
いくつかの商品は直接Grove接続できるので、次のケーブルを使わなくても、普通のGroveケーブルでジョイスティックと接続できるようになります。
GROVE - 4ピン-ジャンパメスケーブル
GROVE - 4ピン-ジャンパメスケーブル 8 は、グローブケーブルを2.54 mmピッチのジャンパメスコネクタに変換するケーブルです。
M5:Bit側が4ピンオスコネクタで、ジョイスティック側がGroveになるため、必要になります。
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_I2C=y
Cmakefile:CMakeLists.txt
アプリケーションbuild用のcmake
の設定ファイルです。
ソースは、main.c
だけなので、それを指定しているだけです。
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
のため、これを指定しています。
&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
を使ってシリアルコンソールに出力します。
#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_GPIO=y
CONFIG_DISPLAY=y
CONFIG_MICROBIT_DISPLAY=y
CONFIG_PWM=y
CONFIG_I2C=y
Device Tree Source Overlay:app.overlay
今回は、PWMに対する定義の追加が必要です。
他は、これまでと同じです。
&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
を表示して音を出す機能を追加しています。
#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のコールバックを用いています。
#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);
...
敵機の処理をしている部分以外はこれまでのものと同じなので、詳細は省略します。
#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することを仮定しています。
$ 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を強制的に利用するために指定しています。
$ 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
を使いました。
# シリアルデバイスの確認
$ 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 のようになります。
m5stack_joystick_display
m5stack_joystick_display
の動作の様子は、以下の動画 10 のようになります。
おわりに
サンプルを使うのではなく、はじめてZephyrでコードを書きました。
標準のDTSで定義されていないバスの情報などがアプリケーション側で上書きできたりするのには、ちょっと驚きました。
参考になりましたら、幸いです。