mruby/cをNordic SoCで動かしてみた

# 要約
* NordicSemiconductor社製のnRF52832、nRF52840、nRF54L15、nRF9151でmruby/cを動かしてみた
* 成果は以下のGitHubリポジトリで公開していますが、如何なる保証もなく直接的または間接的な損害について如何なる責任も負いません
    https://github.com/uist1idrju3i/nRF52DK-mrubyc-zephyr

# 開発環境の構築 (macOS)
* Windows環境はPristine buildに時間が掛かるため推奨しない
* [west ncs-sbom](https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/scripts/west_commands/sbom/README.html)を利用したい場合はLinux環境を推奨する(Ubuntu 22.04推奨)
1. Xcode Command Line Toolsをインストール（`% xcode-select --install`）
1. AppleSilicon環境の場合はRosettaをインストール（`% softwareupdate --install-rosetta`）
1. VisualStudioCodeを[ダウンロード](https://code.visualstudio.com/)してインストール 
1. nRF Command Line Toolsを[ダウンロード](https://www.nordicsemi.com/Products/Development-tools/nRF-Command-Line-Tools/Download)してインストール
1. nRF Utilを[ダウンロード](https://www.nordicsemi.com/Products/Development-tools/nRF-Util)してパスを通してインストール
    ~~~~console
    % nrfutil install device
    % nrfutil install completion
    ~~~~
1. ITOC様配布の「[mrubyコンパイラ3.3.0_mac](https://www.s-itoc.jp/support/technical-support/mrubyc/mrubyc-download/)」を任意の場所に展開してパスを通す
    * mrubyコンパイラは`% rbenv local mruby-3.3.0`等でもインストールできるが、macOSでは環境によっては正しくないバイトコードを生成する※場合のあるmrbcがビルドされるため、ITOC様配布版の利用を推奨
    ※補足: [#6322](https://github.com/mruby/mruby/pull/6322)がMergeされた後のmrubyを用いると当該問題は生じなかった
1. JLinkをインストール
1. nRF Connect for VS Code Extension Packを[インストール](https://marketplace.visualstudio.com/items?itemName=nordic-semiconductor.nrf-connect-extension-pack)
1. 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`)
* 以下のようなコマンドでパスを通す
    ~~~console
    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
    ```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
    }
    ```

## mruby/cをnRF Connect SDK(Zephyr)にポーティングする
* mruby/c公式の[minimal.h](https://github.com/mrubyc/mrubyc/blob/master/hal/minimal.h)を出発点として作業を行った
* 最小限の作業としては`#include <zephyr/kernel.h>`を追加して、`#define hal_idle_cpu() ((k_msleep(MRBC_TICK_UNIT)), mrbc_tick())  // delay 1ms`とするだけでも動作する
* タイマを使用しない場合はスレッドとキューを使って1ms周期を作ってみたが、負荷はそれなりに高そう(以下はnRF52832で測定)
  * タイマを使用しない場合(MRBC_NO_TIMER)
    ```console
    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 %)
    ```
  * タイマを使用する場合
    ```console
     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 %)
    ```

### 最終的には以下のコードになった
* hal.h
    ```c
    /*! @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
    ```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のとき)
* マクロが準備されていないものは`SET_RETURN`を使う
  * Array
    ```c
    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);
    }
    ```

## 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
    ```c
    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
    ```c
    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;
          }
        }
      }
    }
    ```
* Symbolも使ってみた
    ```c
    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で動作を確認している）
    ```ruby
    printf "#{RUBY_ENGINE} #{MRUBYC_VERSION} (mruby:#{MRUBY_VERSION} ruby:#{RUBY_VERSION})\n"
    ```

## 特定の条件でのみ実行可能なメソッドを作る
* ライセンスやAPIキーを持っている人だけがアクセスできるメソッドを作る想定
* mrbc_define_methodを実行しないようにする
    ```c
    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が取得できるため実行時に照合する
    ```c
    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);
    ```
    ```c
    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](https://github.com/atomicobject/heatshrink)を利用してmrubyバイトコードを圧縮する
- [Thingy:91 X](https://hello.nrfcloud.com/model/PCA20065/)への移植を行い、搭載されているセンサ類をRubyで制御してみたい
- nRF54L15のRISC-V coprocessorを使ってみたい

# 参考
## SoCによる実行時間の違い (nRF52840, nRF9151, nRF54L15)
今回の実験に用いたSoCは搭載しているプロセッサやクロックが異なっているので、Thread analyzerの結果も異なる
* nRF52832/nRF52840 (64 MHz Cortex-M4 with FPU)
    ```console
     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)
    ```console
    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)
    ```console
    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](https://github.com/mrubyc/mrubyc/tree/release3.3.1)
- nRF54L15 ( ※技適未取得機器を用いた実験等の特例制度 )
- nRF52832 ( FDK [HY0020](https://www.fdk.co.jp/cyber-j/electronic_components/module/hy0020.html) [R]006-001190 )
- nRF9151 ( Nordic [nRF9151](https://www.nordicsemi.com/Products/Wireless/Low-power-cellular-IoT/nRF91-Series-certifications/nRF9151-Global-and-regulatory?lang=en#infotabs) [R]005-103556 [T]D24-0036005 )
- nRF52840 ( KagaFEI [EJ2840AA2](https://www.kagafei.com/jp/products/wireless-modules/bluetooth/EJ2840AA2.html) [R]005-103131 )
- Nordic Development Kit
![IMG_3986.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3931783/9ff4384b-f64f-46d9-fcd7-ae4c43712c15.jpeg)


## 技適未取得機器を用いた実験等の特例制度
この無線設備は、電波法に定める技術基準への適合が確認されておらず、法に定める特別な条件の下でのみ使用が認められています。
この条件に違反して無線設備を使用することは、法に定める罰則その他の措置の対象となります。

# 参考文献
* 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/