展示会でマイコンをもらってから、mruby/c を動かすまで


## 0. はじめに

九州工業大学の田中と申します。mruby/cに関連する研究に携わっています。
https://mruby-lab.github.io/

以前、ET展示会のST社のブースでNucleoマイコンボードをいただいたのですが、そのまま放置したままになっていました。せっかくなので、mruby/cを入れて動かしてみました。といっても、Lチカなのですが。。。

以下のような内容を順に説明します。

1. 開発環境のセットアップ
1. C言語でのLチカ
1. mruby/cを動かしてみる
1. mruby/cでLチカ

動かそうとするボードは、STの[Nucleo-F401RE](https://www.st.com/ja/evaluation-tools/nucleo-f401re.html)。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104404/8afe3642-56e7-a146-352b-3a826c7a9abe.png)

このボードの概要は以下の通り。mruby/cを動かすには十分なスペックです。

- ARM Cortex-M4, 84 MHz
- Flash memory: 512 KB
- SRAM: 96 KB 

STM32のボードであれば、違うボードでも同じような手順で実行できると思います。

（USBは、USB mini Bなので、ケーブルを探した。。。）

## 1. 開発環境のセットアップ

開発環境は、STが提供しているSTM32用統合開発環境 [STM32CubeIDE](https://www.st.com/ja/development-tools/stm32cubeide.html) です。これを普通にセットアップします。

「File」→「New」→「STM32 project」で、新規プロジェクトを作成します。最初の入力ボックスに「NUCLEO-F401RE」と入力すると、ボードの情報が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104404/67adb67f-f7ff-5127-bcf9-efd874d06889.png)

あとは、「Next」を押していくことで、新規プロジェクトが作成されます。プロジェクト名は適当な名前を付けてください（ここでは、`demo`というプロジェクト名）。自動的に、`main.c`も作成されます。

**注意： フォルダ名、ファイル名に日本語が含まれると正しくコンパイルできないことがあります。**

後ほど、LEDを使うので、そのピン番号を確認しておきます。STから提供されている回路図でも確認できますが、ここでは、設定ツールで確認してみます。

IDEの「Project Explorer」に表示されている`demo.ioc`をダブルクして、ペリフェラル（I/Oなど）の設定画面を開きます。「system Core」の中にある「GPIO」で、LEDのピン番号を確認します。

画面の通り、LD2のピン番号は「PA5」となっています。ちなみに、ユーザのボタンスイッチ B1のピン番号は「PC13」です（今回は使わない）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104404/dcd6b911-5a6c-8ae5-9fb9-eef834cf169f.png)

確認出来たら、`demo.ioc`のタブを閉じます。

## 2. C言語でのLチカ

C言語でLチカのプログラムを作成します。まず、`main.c`をダブルクリックして、エディタにコードを出します。`main.c`は、「Core」-「Src」内にあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104404/9010d97f-6b81-6cc4-539d-6d9d77083a00.png)

すでにコードが自動生成されています。ここで、自分のコードを追加していくことになるのですが、必ず、`USER CODE BEGIN`～`USER CODE END`の区間に追加します。これ以外の場所に追加したコードは、IDEの機能により自動生成される際に削除されてしまいます。

例えば、以下のような区間に自分のコードを追加できます。

```C
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */
```

ここでは、main関数の部分にコードを追加するので、main関数の`USER CODE BEGIN`～`USER CODE END`の区間に、以下のように追加します。

```C
・・・
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, 1);
	HAL_Delay(500);
	HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, 0);
	HAL_Delay(500);

    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
・・・
```

LEDのピン番号は PA5 だったので、`GPIOA` と `GPIO_PIN_5` を引数に渡しています。また、HAL_GPIO_WritePin関数の第3引数はGPIO_PinState型で、`GPIO_PIN_SET`や`GPIO_PIN_RESET`を指定するのですが、ここでは簡単に`1`、`0` を使っています。

あとは、IDEの実行（Run）ボタンで実行させます。プログラムがマイコンボードに書き込まれて実行を開始します。正しく動作すると、ボード上の緑LED（LD2）が点滅します。

## 3. mruby/cを動かしてみる

いよいよ、今回の本題です。mruby/cを動かしてみます。

STM32マイコンは、多くのドキュメントが提供されていることから、mruby/cを載せることに大きな問題はありません。

mruby/cを載せるためには、以下の作業を行います。これは他のマイコンの場合にもほぼ同じです。

- ソースコード（xxx.cとxxx.h）を持ってくる
- halを作成する（hal.cとhal.h）
- mruby/cのサンプルプログラムを持ってくる
- mruby/cのVMを起動するコードを追加する

### ソースコード（xxx.cとxxx.h）を持ってくる

mruby/cのリポジトリを適当なフォルダ（IDEのプロジェクトとは別のフォルダ）にcloneして、ここから必要なソースコードをコピーして利用します。

本当はgitとの連携がうまくできれば、美しいのですが、いろいろ分からない設定があったので、今回はコピー＆ペーストでソースを持ってきます。

必要なファイルは、mruby/cの「src」フォルダ内のファイルです。IDEのプロジェクトでは、ソースファイルとヘッダファイルが別のフォルダで管理されているので、それぞれ振り分けてコピーします。

まず、IDEでフォルダを作成します。IDEの「Project Explorer」の「Src」と「Inc」にそれぞれ、mrubycというフォルダを作成します。マウスを右クリックして表示されるメニューから「New」→「Folder」として、フォルダ名「mrubyc」を入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104404/ba0e71cd-88b0-078d-190c-7516433fcf08.png)

このフォルダ作成により、実際のプロジェクトのファイルが格納されている場所にも「mrubyc」フォルダが作成されています。

mrubycの「src」フォルダ内のすべてのソースファイル（xxx.c）を、「Src」の「mrubyc」フォルダにコピーします。同様に、mrubycの「src」フォルダ内のすべてのヘッダファイル（xxx.h）を、「Inc」の「mrubyc」フォルダにコピーします。

次に、コンパイル時のヘッダファイルとしてmrubycフォルダも参照させるので、その設定をします。

IDEの「Project Explorer」の一番上の「demo」を右クリックして、「Properties」を選択します。プロジェクトの設定画面で、「C/C++ General」→「Paths and Symbols」の「Include」で「Add...」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104404/a0b9015e-c91e-e758-1cdb-7a8f9e8b9e9b.png)

以下のように、`Core/Inc/mrubyc` を入力して、mrubycのヘッダファイルが置かれているフォルダが参照されるようにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104404/d375ea68-0a7a-4992-70dc-44976e4d2ac3.png)

### halを作成する（hal.cとhal.h）

HAL(Hardware Abstraction Layer)のファイルを作成します。mrubyu/c VMの実行に必要で、ハードウェアに依存するような関数が格納されているファイルで、具体的には、`hal.c`と`hal.h` を作成します。なお、今回は、必要最小限の関数のみを作成します。

IDEの「Project Explorer」の「Src」の中の「mrubyc」に`hal.c`というファイルを作成します。「Src」の中の「mrubyc」をマウスで右クリックして表示されるメニューから「New」→「Source File」を選択して、`hal.c`を作成します。同じようにして、「Inc」の中の「mrubyc」に`hal.h`を作成します。

`hal.c`に以下の内容を記述します。

```C
#include <stdio.h>
#include "rrt0.h"
#include "hal.h"
#include "main.h"

void hal_init(void){
}

void hal_enable_irq(void){
}

void hal_disable_irq(void){
}

void hal_idle_cpu(void){
	HAL_Delay(MRBC_TICK_UNIT);
	mrbc_tick();
}

extern UART_HandleTypeDef huart2;

int hal_write(int fd, const void *buf, int nbytes){
	HAL_UART_Transmit(&huart2, buf, nbytes, -1);
	return nbytes;
}
```

`hal.h`に以下の内容を記述します。

```C
#ifndef INC_MRUBYC_HAL_H_
#define INC_MRUBYC_HAL_H_


#define MRBC_TICK_UNIT 10    // 10 [ms]
#define MRBC_TIMESLICE_TICK_COUNT 10

void hal_init(void);
void hal_enable_irq(void);
void hal_disable_irq(void);
void hal_idle_cpu(void);

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


#endif /* INC_MRUBYC_HAL_H_ */
```

- hal_idle_cpu関数
  `MRBC_TICK_UNIT`の時間を待ちます。mruby/cのsleepメソッド（時間経過を待つメソッド）を実行する際の時間をとるためなどに利用します。
- hal_write関数
  mruby/cの出力（putsメソッドなど）をシリアルに出力します。シリアルモニタなどを使うことで、出力を確認できます。

### mruby/cのサンプルプログラム（バイトコード）を持ってくる

mruby/cのリポジトリに入っているサンプルプログラム（`sample_c\sample_include_bytecode.c`）を持ってきます。`sample_include_bytecode.c`を、「Src」にコピーします。

このバイトコードは、以下のmrubyプログラムから生成されたものです。

```Ruby
while true
  puts "sample"
  sleep 1
end
```

### mruby/cのVMを起動するコードを追加する

IDEの「Project Explorer」の「Src」に`mrubyc.c`というファイルを作成します。

`mrubyc.c`に以下の内容を記述します。start_mrubyc関数が、mruby/c VMを起動して、mruby/cバイトコード（`sample_include_bytecode.c`内の`mrbbuf`配列に入っている）を実行します。

```C
#include <stdio.h>
#include "mrubyc.h"
#include "vm.h"
#include "rrt0.h"
#include "main.h"
#include "class.h"

extern const uint8_t mrbbuf[];

#define MRBC_MEMORY_SIZE (1024*30)
static uint8_t memory_pool[MRBC_MEMORY_SIZE];


void start_mrubyc(void){
	mrbc_init(memory_pool, MRBC_MEMORY_SIZE);

	if( mrbc_create_task(mrbbuf, 0) != NULL ){
		mrbc_run();
	}
}
```

最後に、start_mrubyc関数を呼び出すように、`main.c`のmain関数に以下のように記述します。

```C
・・・
  /* USER CODE BEGIN 2 */

  start_mrubyc();

  /* USER CODE END 2 */
・・・
```

IDEの実行（Run）ボタンで実行させます。

シリアルモニタ（例えば、TeraTerm）で実行を確認できます。TeraTermでは、以下の設定をしてください。

- Serial portの Speed: 115200
- Terminal の NewLine(Receive): AUTO

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104404/a622c65d-5e0f-2981-2f10-95cce188fb85.png)

## 4. mruby/cでLチカ

mruby/cのVMが起動出来たら、あとは簡単です。ここでは、mruby/cでLチカをさせるので、mrubyプログラムは以下のようになります。

```Ruby
while true
  led 1
  sleep 0.5
  led 0
  sleep 0.5
end
```

このプログラムを、例えば `sample.rb` として作成します。mrubyコンパイラ（mrbcコマンド）で、以下のようにしてバイトコード（のCの配列）を生成します。

```
mrbc -Bmrbbuf sample.rb
```

`-B`コマンドは、バイトコードをCの配列（ここでの配列名はmrbbuf）として、Cのソースファイルを生成するためのコマンドです。

mruby/cのプログラムでは、ledメソッドを使っています。このメソッドを記述することになります。Objectクラスにledメソッドを追加したいと思います。

`mrubyc.c`に以下を追加します。

```C
・・・
// 以下を追加
static void c_object_led(mrb_vm *vm, mrb_value v[], int argc)
{
	HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, v[1].i);
}


void start_mrubyc(void){
	mrbc_init(memory_pool, MRBC_MEMORY_SIZE);

	mrbc_class *cls = mrbc_get_class_by_name("Object");  // 追加
	mrbc_define_method(0, cls, "led", c_object_led);     // 追加

	if( mrbc_create_task(mrbbuf, 0) != NULL ){
		mrbc_run();
	}
}
```

IDEの実行（Run）ボタンでLチカのプログラムが動きます。マイコンボード上のLEDが点滅すると思います。


# おわりに

今回、**とりあえず** mruby/cを動かす手順を示しました。

しかし、mrbc_idle_cpu関数でHAL_Delayを使っています。HAL_Delayはループにより時間待ちをしているため、CPUがフル稼働しています。タイマー割込みを使って、この問題を解決できます。機会があれば、そのための改良について記事をアップしたいと思っています。

質問、コメントなどありましたら、よろしくお願いします。

# リファレンス

- [mruby/cリポジトリ](https://github.com/mrubyc/mrubyc)
- [ここで示したSTMCubeIDEのプロジェクト](https://github.com/mruby-lab/stm32_mrubyc)