【チュートリアル】mruby/cをSTM32マイコンで動かす Chapter02: mruby/cでElChika（えるちか）

[しまねソフト研究開発センター](https://www.s-itoc.jp)（略称 ITOC）にいます、東です。

[mruby/cをSTマイクロエレクトロニクス社製の32bitマイコンSTM32で動かす記事](https://qiita.com/HirohitoHigashi/items/7ef01d074baa27a3b2e7)、
今回はその第2回、mruby/c のポーティングを行い、ElChika[^1]（えるちか = LEDの点滅）を行います。

[^1]: ElChika : Electro Luminescence　Chika Chika ですんで、LED (Light Emitting Diode) に限らずそれっぽいものを点滅させてます。


# 目標

ST社製マイコン評価ボード 「Nucleo-F401RE」 のオンボードLEDを、mruby/c のコードを使って点滅させる。


# 前提知識

mruby/c は、RubyのソースコードをPC上であらかじめバイトコード（中間コード）に変換し、それをターゲットマイコン上の VM(Virtual machine) を使って実行します。

![STM32Tuto02-mrubyc動作フロー2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/40d64f14-2b2e-e58d-2423-a47fdcfa63d3.png)


バイトコードはmrubyとコンパチブルなので、mrubyプロジェクトから配布されているコンパイラ (mrbc.exe) をそのまま利用します。

![STM32Tuto02-mrubyc動作フロー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/e290fbc4-b1cd-21c6-9302-15843e8e30c6.png)

VM自体は、C言語(C99) を使って書いてあります。
mruby/c プログラムを動作させるやり方は、以下の2種類が考えられます。
1. あらかじめVMをROMに書き込んでおき、あとからバイトコードだけを書き込んで実行する方法
2. mruby/cのバイトコードをC言語の配列へ出力し、他のC言語ファイルとともにビルドしマイコンに書き込む方法

今回は、2の方法を説明します。

## スケジューラ

mruby/cの特徴の一つとして、スケジューラを内包し、OSなしでも複数のrubyプログラムの同時実行が可能です。ですがスケジューラとmruby/c VM本体は疎結合であるように設計しており分離可能です。すなわち、mrubyのように　VM 本体だけでも使う事ができます。具体的方法は、sample_c フォルダの sample_no_scheduler.c を見てください。
今回は、スケジューラを使う方針にします。

## ハードウェアタイマー

スケジューラは、プリエンプティブな動作を実現するために、ハードウェアタイマーを必要とします。しかしポーティングの初期段階でいきなりフルスペック動作をさせるのはハードルが高いので、ハードウェアタイマーなしで疑似動作させるためのモードがあります。
今回は、ハードウェアタイマーを使わずに動作確認をすることにします。
（ハードウェアタイマーを使うのは次回以降とします）

---
**（参考）**
このほかにも、PicoRuby (https://github.com/picoruby/picoruby) という、
 * コンパイラを独自開発
 * VMは mruby/c を使う

という処理系もあります。



# 今回の方針

* mruby/c のバイトコードをC言語のバイト列へ変換し、STM32CubeIDE 上でリンクする方式とする
* スケジューラを使う
* ハードウェアタイマーを使わない



# 準備するもの

* STM32マイコン評価ボード Nucleo-F401RE
(https://www.st.com/ja/evaluation-tools/nucleo-f401re.html)
* 開発環境　STM32CubeIDE (ver 1.15.1)
(https://www.st.com/ja/development-tools/stm32cubeide.html)
* mruby/c ソースコード
(https://github.com/mrubyc/mrubyc/releases)
* mrubyコンパイラ mrbc.exe 
(https://www.s-itoc.jp/support/technical-support/mrubyc/mrubyc-download/)

mruby/c は、release3.3 のソースコードをダウンロードします。
mrubyコンパイラは、バージョン 3.3.0 を、ダウンロードします。

[前回の記事](https://qiita.com/HirohitoHigashi/items/7ef01d074baa27a3b2e7) を参考に、STM32CubeIDE の環境構築を終わらせておきます。


# 作業手順

ポーティングから　mruby/c サンプルプログラムを動かすまでの手順は、以下の順番で行います。

1. mruby/c のソースコードを開発環境へ持ってくる
2. タイマー未使用を宣言する
3. mruby/c 用の hal ファイルを作る
4. mruby/c を実行するC関数を作る
5. Ruby (mruby/c) のプログラムを作る
6. mrbc.exe を使って、mrubyバイトコードファイルを作る
7. 全体をビルドし、ターゲットデバイスに書き込む

途中、フォルダの作成など、現時点でのベストプラクティスと思う方法を紹介します。
それではやっていきます。

## 1. mruby/c のソースコードを開発環境へ持ってくる

CubeIDEの左ペイン Project Explorer 上、`Core` の上で右クリックし、New > Folder を選びます。
![STM32Tuto02-スクリーンショット 2024-05-20 17.16.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/17f121d5-d814-f97c-a639-a3b015dbc5c8.png)

New Folder ダイアログで、Folder name 欄へ、`mrubyc_src` と入力し、[Finish] をクリックし、フォルダを作ります。
![STM32Tuto02-スクリーンショット 2024-05-20 17.18.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/434cade5-ae86-dfb5-353e-1b5d9a82e23d.png)

ダウンロードした mruby/c ソースコードを任意の場所へ展開します。

展開したフォルダの src フォルダにある全ファイルを、先ほどの mrubyc_src フォルダへコピーします。
コピーの方法は、ドラッグ＆ドロップでも良いですし、[Ctrl]-[A] → [Ctrl]-[C] → [Ctrl]-[V] でもできます。
![STM32Tuto02-スクリーンショット 2024-05-20 17.22.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/5db431ba-01fd-5d8f-7d1d-a1638ccdec31.png)

## 2. タイマー未使用を宣言する

この章では簡単のため、ハードウェアタイマーを使わずに動かします。そのための宣言をプロジェクト設定に追加します。

メニューから、Project > Properties を選び、ダイアログを開きます。

ダイアログ左ペインの C/C++ General > Paths and Symbols をクリックし、右ペインの　Symbols タブをクリックします。

Languages が `GNU C` になっていることを確認し、[Add...] ボタンをクリックします。

Add symbol ダイアログが開くので、Name欄に、`MRBC_NO_TIMER` と入力して　[OK]　をクリックします。
![STM32Tuto02-スクリーンショット 2024-05-20 18.46.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/c5b7534f-7c44-a17e-13b0-a9e9c840a9cd.png)

[Apply and Close] をクリックしてダイアログを閉じます。[^2]


[^2]: このほかにも、同 Properties ダイアログの、C/C++ Build > Settings > Tool Settings > MCU GCC Compiler > Preprocessor > Define symbols の画面でも同様の定義ができます。試した結果、どちらでも同じ効果でした。


## 3. mruby/c 用の hal ファイルを作る

STM32環境にもHALがあり、ややこしいですがそれとは別物です。
以下の手順で、Core/mrubyc_src フォルダへ、`hal.h` ファイルを作ります。

CubeIDEの左ペイン Project Explorer の、先ほど作った `mrubyc_src` フォルダの上で右クリックし、New > Header File を選びます。

New Header File ダイアログが開くので、Header file: 欄へ、`hal.h` と入力して [Finish] をクリックします。
![STM32Tuto02-スクリーンショット 2024-05-20 19.01.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/8ae1fc1f-7e51-16e1-ea58-c8fec9c5936a.png)

ファイルの内容を、以下の通り入力します。

```c:Core/mrubyc_src/hal.h
#ifndef MRBC_SRC_HAL_H_
#define MRBC_SRC_HAL_H_

#define MRBC_TICK_UNIT 1
#define MRBC_TIMESLICE_TICK_COUNT 10

#define hal_init()        ((void)0)
#define hal_enable_irq()  ((void)0)
#define hal_disable_irq() ((void)0)
#define hal_idle_cpu()    (HAL_Delay(MRBC_TICK_UNIT), mrbc_tick())

int hal_write(int fd, const void *buf, int nbytes);
int hal_flush(int fd);
void hal_abort(const char *s);

#endif
```

mruby/c で必要とする hal は、4つのマクロ (define) と、3つの関数、たったこれだけです。
今回のレポートではとりあえず動かすことを重要視し、ダミーで定義して後々拡張します。

名前から大まかな動作は想像できると思いますが、簡単にそれぞれを説明します。

<dl>
<dt>hal_init
<dd>hal独自の初期化が必要になる場合、このマクロに定義する。

<dt>hal_enable_irq
<dd>CPUの割り込み許可処理関数を記述する。

<dt>hal_disable_irq
<dd>CPUの割り込み禁止処理関数を記述する。

<dt>hal_idle_cpu
<dd>CPUをアイドルモードにするための処理関数を記述する。

<dt>int hal_write(int fd, const void *buf, int nbytes)
<dd>mruby/c の putc などの標準出力先を記述する。

<dt>int hal_flush(int fd)
<dd>標準出力にバッファを持つ場合のバッファフラッシュを記述する。

<dt>void hal_abort(const char *s)
<dd>なんらかの原因で abort する場合にコールする関数を記述する。
</dl>

hal_idle_cpu マクロに関して、今回のようにハードウェアタイマーを使用しないモードでCPUを止めてしまうと、起きるためのトリガーがなくなって本当に動作が完全停止してしまうので、1ミリ秒の時間待ちと、併せて mruby/c のスケジューラ内部処理に必要な関数 `mrbc_tick()` を記述します。


## 4. mruby/c を実行するC関数を作る

mruby/c を実行する関数を、新たに Core/mrubyc/start_mrubyc.c ファイルを作って記述します。

先ほどと同じように、CubeIDEの左ペイン Project Explorer 上、`Core` の上で右クリックし、New > Folder を選びます。

New Folder ダイアログで、Folder name 欄へ、`mrubyc` と入力し、[Finish] をクリックし、フォルダを作ります。

できた mrubycフォルダ上で右クリックし、New > Source File と選びます。

New Source File ダイアログの Source file 欄へ、`start_mrubyc.c` と入力します。
![STM32Tuto02-スクリーンショット1 2024-05-20 18.02.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/3ee9d27a-8b10-4caa-23c1-927373bd30b8.png)

以下の内容を入力します。

```c:Core/mrubyc/start_mrubyc.c
#include "main.h"
#include "../mrubyc_src/mrubyc.h"

static void c_led_write(mrbc_vm *vm, mrbc_value v[], int argc);

/* mruby/c プログラムが使うワークメモリの確保 */
#define MRBC_MEMORY_SIZE (1024*30)
static uint8_t memory_pool[MRBC_MEMORY_SIZE];

/*! mruby/c プログラムの実行開始
*/
void start_mrubyc( void )
{
  mrbc_init(memory_pool, MRBC_MEMORY_SIZE);

  // ユーザ定義メソッドの登録
  mrbc_define_method(0, 0, "led_write", c_led_write);

  // タスクの登録
  extern const uint8_t task1[];
  mrbc_create_task( task1, 0 );

  // 実行開始
  mrbc_run();
}


/*! オンボードLED ON/OFF メソッドの実装
*/
static void c_led_write(mrbc_vm *vm, mrbc_value v[], int argc)
{
  int on_off = GET_INT_ARG(1);
  HAL_GPIO_WritePin( GPIOA, GPIO_PIN_5, on_off );
}

/*! HAL（ダミー）
*/
int hal_write(int fd, const void *buf, int nbytes)
{
  return 0;
}
int hal_flush(int fd)
{
  return 0;
}
void hal_abort(const char *s)
{
}
```

前回、C言語で動作確認をした時に書き換えた Core/Src/main.c を、以下の通り start_mrubyc 関数をコールするように書き換えます。

```c:Core/Src/main.c
  /* USER CODE BEGIN 2 */
  void start_mrubyc(void);
  start_mrubyc();
  /* USER CODE END 2 */
```


## 5. Ruby (mruby/c) のプログラムを作る

mruby/c のソースファイル　`task11.rb` を作ります。

左ペイン Project Explorer 上、`mrubyc` の上で右クリックし、New > File を選びます。

Create New File ダイアログで、File name 欄へ、`task1.rb` と入力し、[Finish] をクリックし、ファイルを作ります。

以下の内容を入力します。
```ruby:Core/mrubyc/task1.rb
while true
  led_write( 1 )
  sleep 1
  led_write( 0 )
  sleep 1
end
```

## 6. mrbc.exe を使って、mrubyバイトコードファイルを作る

ダウンロードした mruby コンパイラのフォルダから、mrbc.exe を、先ほどの mrubyc フォルダへコピーします。

コマンドプロンプトを起動し、先のフォルダへ cd します。
```bat
cd \PATH\TO\PROJECT\tuto01\Core\mrubyc
```

mrbc.exe コンパイラで、rubyソースコードをバイトコードに変換します。
```bat
mrbc.exe -Btask1 task1.rb
```

Rubyソースコードがバイトコードに変換されて、`task1.c` ファイルが生成されます。
task1.c ファイルを表示して、どのように出力されたかを確認しておくと良いでしょう。

```c:task1.c
#include <stdint.h>
#ifdef __cplusplus
extern
#endif
const uint8_t task1[] = {
0x52,0x49,0x54,0x45,0x30,0x33,0x30,0x30,0x00,0x00,0x00,0x70,0x4d,0x41,0x54,0x5a,
  (snip...)
0x00,0x05,0x73,0x6c,0x65,0x65,0x70,0x00,0x45,0x4e,0x44,0x00,0x00,0x00,0x00,0x08,
};
```

ここでできた `task1` 配列が、「4. mruby/c を実行するC関数を作る」 の箇所で記述したプログラムのタスクの登録部で使われます。


## 7. 全体をビルドし、ターゲットデバイスに書き込む

### ビルド

メニューから、Project > Build Project を選びます。
下部コンソールペインに、Build Finished. と表示されるのを確認します。いくつか Warning が出るかと思いますが、一旦ここは無視しても良いです。

### 書き込み

メニューから、Run > Run を選んでターゲットへ書き込みます。

オンボードのLEDが点滅したでしょうか。

![STM32Tuto02-Demo1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/c71b17f8-3e33-1586-bc0a-b269043cd408.gif)


## mruby/c プログラムの書き換え

プログラムを書き換えて、点滅の速度をもっと早くしてみましょう。

task1.rb を、以下のように書き換えて保存します。

```ruby:Core/mrubyc/task1.rb
while true
  led_write( 1 )
  sleep 0.1        # ここと
  led_write( 0 )
  sleep 0.5        # ここ
end
```

保存するのを忘れないようにしましょう。
次に、コマンドプロンプトで、書き換えたrubyソースコードをバイトコードに変換します。
```
mrbc.exe -Btask1 task1.rb
```

CubeIDE で、ビルドしてターゲットへ書き込みます。

メニューから、Run > Run を選んでターゲットへ書き込みます。
先ほどは、ビルドと書き込みを別々に行っていましたが、じつは書き込み前に自動的にビルドされるので、書き込みの操作だけでOKです。

実行が始まると、LEDが先ほどより早く点滅します。

![STM32Tuto02-Demo2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/d3e411f3-964c-1363-150c-fde6eec8e58e.gif)


# おわりに

ファイル全体は、github リポジトリにありますので、そちらをご覧ください。

https://github.com/HirohitoHigashi/mrubyc-tuto-stm32/tree/chapter02

今回は、とりあえず動かすことを目標に、mruby/c をポーティングしました。
このままでは、mruby/c のプログラムを書き換えたら都度 mrbc.exe を動かさないといけないので、不便ですね。また、最初のプログラムでは、`sleep 1` と指定しているのに、明らかに 1秒より長い周期で点滅しているように見えます。

次回はこのあたりのことを、解決していこうと思います。