要約
- NordicSemiconductor社製のnRF52832、nRF52840、nRF54L15、nRF9151でmruby/cを動かしてみた
- 成果は以下のGitHubリポジトリで公開していますが、如何なる保証もなく直接的または間接的な損害について如何なる責任も負いません
https://github.com/uist1idrju3i/nRF52DK-mrubyc-zephyr
開発環境の構築 (macOS)
- Windows環境はPristine buildに時間が掛かるため推奨しない
- west ncs-sbomを利用したい場合はLinux環境を推奨する(Ubuntu 22.04推奨)
- Xcode Command Line Toolsをインストール(
% xcode-select --install
) - AppleSilicon環境の場合はRosettaをインストール(
% softwareupdate --install-rosetta
) - VisualStudioCodeをダウンロードしてインストール
- nRF Command Line Toolsをダウンロードしてインストール
- nRF Utilをダウンロードしてパスを通してインストール
% nrfutil install device % nrfutil install completion
- ITOC様配布の「mrubyコンパイラ3.3.0_mac」を任意の場所に展開してパスを通す
- mrubyコンパイラは
% rbenv local mruby-3.3.0
等でもインストールできるが、macOSでは環境によっては正しくないバイトコードを生成する※場合のあるmrbcがビルドされるため、ITOC様配布版の利用を推奨
※補足: #6322がMergeされた後のmrubyを用いると当該問題は生じなかった
- mrubyコンパイラは
- JLinkをインストール
- nRF Connect for VS Code Extension Packをインストール
- VisualStudioCode内のnRF ConnectのWELCOMEから「Manage toolchains」と「Manage SDKs」をクリックして必要なバージョンをインストール
- macOSのバージョンによってはnRF Connect SDKのインストールに長い時間を要する場合があるので、事前に最新版のgitをインストールしておくと良い
パスの通し方の例 (参考)
- mruby-3.3.0-mac.zipを展開して生成されたmruby-3.3.0-macを/Applicationsに配置する
- ダウンロードしたnrfutilを/Applications/Nordic Semiconductor/bin/に配置する
- パーミッションを忘れずに設定しておく (例:
% chmod +x nrfutil
) - 以下のようなコマンドでパスを通す
uist1idrju3i@MacBookKuro ~ % cd /usr/local/bin uist1idrju3i@MacBookKuro bin % sudo ln -s /Applications/Nordic\ Semiconductor/bin/nrfutil nrfutil uist1idrju3i@MacBookKuro bin % sudo ln -s /Applications/mruby-3.3.0-mac/mrbc mrbc uist1idrju3i@MacBookKuro bin % sudo ln -s /Applications/mruby-3.3.0-mac/mrdb mrdb uist1idrju3i@MacBookKuro bin % sudo ln -s /Applications/mruby-3.3.0-mac/mruby mruby uist1idrju3i@MacBookKuro bin % sudo ln -s /Applications/mruby-3.3.0-mac/mruby-strip mruby-strip
ポーティング作業
- サンプルコード(Blinky)から作業を開始した
- 以下のようにSoftResetを使用する設定にしておくと都度Resetボタンを押下しなくても良い
- .vscode/settings.json
{ "C_Cpp.clang_format_style": "Google", "editor.formatOnSave": true, "editor.formatOnType": true, "nrf-connect.toolchain.path": "${nrf-connect.toolchain:2.8.0}", "nrf-connect.topdir": "${nrf-connect.sdk:2.8.0}", "nrf-connect.flash.softreset": true }
- .vscode/settings.json
mruby/cをnRF Connect SDK(Zephyr)にポーティングする
- mruby/c公式のminimal.hを出発点として作業を行った
- 最小限の作業としては
#include <zephyr/kernel.h>
を追加して、#define hal_idle_cpu() ((k_msleep(MRBC_TICK_UNIT)), mrbc_tick()) // delay 1ms
とするだけでも動作する - タイマを使用しない場合はスレッドとキューを使って1ms周期を作ってみたが、負荷はそれなりに高そう(以下はnRF52832で測定)
- タイマを使用しない場合(MRBC_NO_TIMER)
Thread analyze: thread_analyzer : STACK: unused 584 usage 440 / 1024 (42 %); CPU: 0 % : Total CPU cycles used: 432 mrubyc_halthread : STACK: unused 176 usage 208 / 384 (54 %); CPU: 2 % : Total CPU cycles used: 36427 sysworkq : STACK: unused 872 usage 152 / 1024 (14 %); CPU: 2 % : Total CPU cycles used: 36421 logging : STACK: unused 216 usage 552 / 768 (71 %); CPU: 0 % : Total CPU cycles used: 877 idle : STACK: unused 272 usage 48 / 320 (15 %); CPU: 91 % : Total CPU cycles used: 1164145 main : STACK: unused 608 usage 416 / 1024 (40 %); CPU: 2 % : Total CPU cycles used: 36607 ISR0 : STACK: unused 1840 usage 208 / 2048 (10 %)
- タイマを使用する場合
Thread analyze: thread_analyzer : STACK: unused 576 usage 448 / 1024 (43 %); CPU: 0 % : Total CPU cycles used: 165 logging : STACK: unused 280 usage 488 / 768 (63 %); CPU: 0 % : Total CPU cycles used: 302 idle : STACK: unused 272 usage 48 / 320 (15 %); CPU: 97 % : Total CPU cycles used: 636754 main : STACK: unused 600 usage 424 / 1024 (41 %); CPU: 2 % : Total CPU cycles used: 18201 ISR0 : STACK: unused 1840 usage 208 / 2048 (10 %)
- タイマを使用しない場合(MRBC_NO_TIMER)
最終的には以下のコードになった
- hal.h
/*! @file @brief Hardware abstraction layer for Zephyr. <pre> Copyright (C) 2015- Kyushu Institute of Technology. Copyright (C) 2015- Shimane IT Open-Innovation Center. This file is distributed under BSD 3-Clause License. </pre> */ #ifndef MRBC_SRC_HAL_H_ #define MRBC_SRC_HAL_H_ #include <zephyr/kernel.h> #define MRBC_TICK_UNIT 1 #define MRBC_TIMESLICE_TICK_COUNT 10 #if !defined(MRBC_NO_TIMER) #define hal_init() ((void)0) void hal_enable_irq(void); void hal_disable_irq(void); #define hal_idle_cpu() ((k_msleep(MRBC_TICK_UNIT))) // delay 1ms #else #define hal_init() ((void)0) #define hal_enable_irq() (k_sched_unlock()) #define hal_disable_irq() (k_sched_lock()) #define hal_idle_cpu() ((k_msleep(MRBC_TICK_UNIT))) // delay 1ms #endif int hal_write(int fd, const void *buf, int nbytes); #define hal_flush(fd) ((void)0) #define hal_abort(s) ((void)0) #endif
- hal.c
#include "hal.h" #include <stdlib.h> #include <zephyr/init.h> #include <zephyr/irq.h> #include <zephyr/kernel.h> #include <zephyr/sys/printk.h> #include "../../../mrubyc/src/mrubyc.h" #define HAL_WRITE_BUFFER_SIZE 255 #define MEMORY_SIZE (1024 * 20) static uint8_t memory_pool[MEMORY_SIZE]; /* ===== prototype ===== */ static int mrubyc_halinit(void); /* ===== kernel ===== */ SYS_INIT(mrubyc_halinit, APPLICATION, 0); #if !defined(MRBC_NO_TIMER) /* ===== use timer ===== */ static unsigned int hal_irq_lock_key; void hal_enable_irq(void) { irq_unlock(hal_irq_lock_key); k_sched_unlock(); } void hal_disable_irq(void) { k_sched_lock(); hal_irq_lock_key = irq_lock(); } static void mrubyc_haltimerhandler(struct k_timer *const timer) { mrbc_tick(); } K_TIMER_DEFINE(mrubyc_haltimer, mrubyc_haltimerhandler, NULL); #else /* ===== MRBC_NO_TIMER ===== */ void mrubyc_haltick(struct k_work *const work) { mrbc_tick(); } K_WORK_DEFINE(mrubyc_halwork, mrubyc_haltick); static int mrubyc_halmain(void) { while (1) { k_work_submit(&mrubyc_halwork); k_msleep(1); } return EXIT_FAILURE; } K_THREAD_DEFINE(mrubyc_halthread, 384, mrubyc_halmain, NULL, NULL, NULL, -2, K_ESSENTIAL, 0); #endif /* ===== mrubyc_halinit ===== */ static int mrubyc_halinit(void) { mrbc_init(memory_pool, MEMORY_SIZE); #if !defined(MRBC_NO_TIMER) k_timer_start(&mrubyc_haltimer, K_NO_WAIT, K_MSEC(1)); #endif return EXIT_SUCCESS; } /* ===== hal_write ===== */ int hal_write(int fd, const void *buf, int nbytes) { char buffer[HAL_WRITE_BUFFER_SIZE] = {0}; if (HAL_WRITE_BUFFER_SIZE < nbytes) { return -1; } for (int i = 0; i < nbytes; i++) { buffer[i] = ((char *)buf)[i]; } printk("%s", buffer); return nbytes; }
mruby/c VMとのデータのやりとり(c→mruby)
- 基本的には便利なマクロを活用する
- NIL
SET_NIL_RETURN();
- BOOL
SET_FALSE_RETURN();
SET_TRUE_RETURN();
SET_BOOL_RETURN((bool)true);
- INT
-
SET_INT_RETURN((int32_t)123);
(MRBC_INT16やMRBC_INT64を使用しないとき)
-
- FLOAT, DOUBLE
-
SET_FLOAT_RETURN((float)123.0f);
(MRBC_USE_FLOAT=1のとき) -
SET_FLOAT_RETURN((double)123.0);
(MRBC_USE_FLOAT=2のとき)
-
- NIL
- マクロが準備されていないものは
SET_RETURN
を使う- Array
static void c_sample_array(mrb_vm *vm, mrb_value *v, int argc) { float tmp[3] = {0.0f, 1.0f, 3.14f}; mrb_value ret = mrbc_array_new(vm, 3); for (int i = 0; i < 3; i++) { mrb_value v = mrbc_float_value(vm, tmp[i]); mrbc_array_set(&ret, i, &v); } SET_RETURN(ret); }
- Array
mruby/c VMとのデータのやりとり(mruby→c)
- 基本的には便利なマクロを活用する(第一引数を取得する場合の例)
GET_TT_ARG(1);
GET_INT_ARG(1);
GET_ARY_ARG(1);
GET_ARG(1);
GET_FLOAT_ARG(1);
GET_STRING_ARG(1);
MRBC_ISNUMERIC(v[1]);
- マクロが準備されていないものでも以下のような形で取得できた
- String
static void c_sample_string(mrb_vm *vm, mrb_value *v, int argc) { char tmp_char[255]; size_t tmp_size = 0; if (MRBC_TT_STRING == v[1].tt) { uint8_t *string = v[1].string->data; for (size_t i = 0; i < sizeof(tmp_char) / sizeof(tmp_char[0]); i++) { if ((i == v[1].string->size) || ('\0' == string[i])) { tmp_size = i; break; } tmp_char[i] = (char)string[i]; } } }
- Array
static void c_sample_array(mrb_vm *vm, mrb_value *v, int argc) { uint8_t array[3] = {0}; if (MRBC_TT_ARRAY == v[1].tt) { if (3 != v[1].array->n_stored) return; for (size_t i = 0; i < 3; i++) { array[i] = (uint8_t)v[1].array->data[i].i; } } } static void c_sample_array2(mrb_vm *vm, mrb_value *v, int argc) { uint8_t c_array[3] = {0}; if (MRBC_TT_ARRAY == v[1].tt) { if (3 == v[1].array->n_stored) { mrb_value mrbc_array = GET_ARY_ARG(1); for (size_t i = 0; i < 3; i++) { mrb_value value = mrbc_array_get(&mrbc_array, i); c_array[i] = value.i; } } } }
- String
- Symbolも使ってみた
typedef enum { kSymbolLED1, kSymbolTSize // enum size } symbol_t; static int16_t symbol_id_table[kSymbolTSize]; static void symbol_init(void) { mrbc_symbol_new(0, "led1"); symbol_id_table[kSymbolLED1] = (int16_t)mrbc_search_symid("led1"); } static bool get_bool(const mrbc_vtype kType) { if (MRBC_TT_TRUE == kType) { return true; } else if (MRBC_TT_FALSE == kType) { return false; } return false; } static void c_set_led(mrb_vm *vm, mrb_value *v, int argc) { int16_t tgt = -1; bool req = false; SET_FALSE_RETURN(); // 返値の初期値をfalseにする // ============================== MRBC_KW_ARG(part, state); do { if (!MRBC_KW_MANDATORY(part)) break; if (!MRBC_KW_END()) break; // 操作対象を取得 if (MRBC_TT_SYMBOL == part.tt) { tgt = (int16_t)part.i; } else { break; } if (MRBC_KW_ISVALID(state)) { req = get_bool(state.tt); } } while (0); MRBC_KW_DELETE(part, state); // ============================== // LED1 if (symbol_id_table[kSymbolLED1] == tgt) { drv_gpio_set(kDrvGpioLED1, req); SET_TRUE_RETURN(); } }
- mruby/cのバージョンを取得したかった
- C言語側から簡単に取得する方法は用意されていなさそうだが、Ruby側からは以下のように取得できる(以下のコードは最新版のmruby/cで動作を確認している)
printf "#{RUBY_ENGINE} #{MRUBYC_VERSION} (mruby:#{MRUBY_VERSION} ruby:#{RUBY_VERSION})\n"
- C言語側から簡単に取得する方法は用意されていなさそうだが、Ruby側からは以下のように取得できる(以下のコードは最新版のmruby/cで動作を確認している)
特定の条件でのみ実行可能なメソッドを作る
- ライセンスやAPIキーを持っている人だけがアクセスできるメソッドを作る想定
- mrbc_define_methodを実行しないようにする
mrb_class *class_led; class_led = mrbc_define_class(0, "LED", mrbc_class_object); mrbc_define_method(0, class_led, "set", c_set_led); if (true == allow_expert_method()) { mrbc_define_method(0, mrbc_class_object, "low_level_api", c_led1_write); }
特定のタスクからのみ実行可能なメソッドを作る
- mruby/cが複数タスクを実行可能であることを活かして、管理者権限タスクと一般ユーザタスクのような使い分けを想定(管理者権限タスクでのみ実行可能なメソッドを作りたい)
-
vm->vm.vm_id
でIDが取得できるため実行時に照合するstatic uint8_t valid_id = 0; mrbc_tcb *tcb[MAX_VM_COUNT] = {NULL, NULL, NULL, NULL, NULL}; tcb[0] = mrbc_create_task(kBytecode0, NULL); valid_id = tcb[0]->vm.vm_id; // 管理者権限タスク tcb[1] = mrbc_create_task(kBytecode1, NULL); tcb[2] = mrbc_create_task(kBytecode2, NULL);
static void c_led1_write(mrb_vm *vm, mrb_value *v, int argc) { if (valid_id == vm->vm.vm_id) { // Valid } else { // Not valid } }
今後の取り組み
- ZephyrのUserspace機能を利用して非特権レベルでmruby/c VMを実行する
- heatshrinkを利用してmrubyバイトコードを圧縮する
- Thingy:91 Xへの移植を行い、搭載されているセンサ類をRubyで制御してみたい
- nRF54L15のRISC-V coprocessorを使ってみたい
参考
SoCによる実行時間の違い (nRF52840, nRF9151, nRF54L15)
今回の実験に用いたSoCは搭載しているプロセッサやクロックが異なっているので、Thread analyzerの結果も異なる
- nRF52832/nRF52840 (64 MHz Cortex-M4 with FPU)
Thread analyze: thread_analyzer : STACK: unused 576 usage 448 / 1024 (43 %); CPU: 0 % : Total CPU cycles used: 165 logging : STACK: unused 280 usage 488 / 768 (63 %); CPU: 0 % : Total CPU cycles used: 302 idle : STACK: unused 272 usage 48 / 320 (15 %); CPU: 97 % : Total CPU cycles used: 636754 main : STACK: unused 600 usage 424 / 1024 (41 %); CPU: 2 % : Total CPU cycles used: 18201 ISR0 : STACK: unused 1840 usage 208 / 2048 (10 %)
- nRF9151 (64 MHz Arm Cortex-M33)
Thread analyze: thread_analyzer : STACK: unused 576 usage 448 / 1024 (43 %); CPU: 0 % : Total CPU cycles used: 194 logging : STACK: unused 240 usage 528 / 768 (68 %); CPU: 0 % : Total CPU cycles used: 405 idle : STACK: unused 272 usage 48 / 320 (15 %); CPU: 97 % : Total CPU cycles used: 780760 main : STACK: unused 600 usage 424 / 1024 (41 %); CPU: 2 % : Total CPU cycles used: 22788 ISR0 : STACK: unused 1776 usage 272 / 2048 (13 %)
- nRF54L15 (128 MHz Arm Cortex-M33)
Thread analyze: thread_analyzer : STACK: unused 576 usage 448 / 1024 (43 %); CPU: 0 % : Total CPU cycles used: 9300 logging : STACK: unused 240 usage 528 / 768 (68 %); CPU: 0 % : Total CPU cycles used: 16583 idle : STACK: unused 272 usage 48 / 320 (15 %); CPU: 98 % : Total CPU cycles used: 88441161 main : STACK: unused 600 usage 424 / 1024 (41 %); CPU: 1 % : Total CPU cycles used: 1172626 ISR0 : STACK: unused 1824 usage 224 / 2048 (10 %)
動作確認環境
- MacBook Pro (2021, M1 Pro, macOS Sequoia 15.1.1)
- toolchains v2.8.0
- nRF Connect SDK v2.8.0
- nrfutil 7.12.0
- nrfutil device 2.7.7
- mruby/c release3.3.1
- nRF54L15 ( ※技適未取得機器を用いた実験等の特例制度 )
- nRF52832 ( FDK HY0020 [R]006-001190 )
- nRF9151 ( Nordic nRF9151 [R]005-103556 [T]D24-0036005 )
- nRF52840 ( KagaFEI EJ2840AA2 [R]005-103131 )
- Nordic Development Kit
技適未取得機器を用いた実験等の特例制度
この無線設備は、電波法に定める技術基準への適合が確認されておらず、法に定める特別な条件の下でのみ使用が認められています。
この条件に違反して無線設備を使用することは、法に定める罰則その他の措置の対象となります。
参考文献
- https://www.s-itoc.jp/support/technical-support/mrubyc/
- https://github.com/mrubyc/mrubyc/blob/master/doc/c_KeywordArgument.md
- https://academy.nordicsemi.com/
- https://devzone.nordicsemi.com/
- https://docs.nordicsemi.com/
- https://docs.zephyrproject.org/latest/
- https://github.com/zephyrproject-rtos/zephyr/tree/main/samples
- https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/installation/recommended_versions.html
- https://docs.nordicsemi.com/bundle/nrfutil/page/guides/installing_commands.html
- https://www.musen-connect.co.jp/blog/course/other/application-memo-for-radio-law-exceptions-system/