【チュートリアル】mruby/cをSTM32マイコンで動かす Chapter05: GPIOの拡張と、複数プログラムの動作

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

[mruby/cをSTマイクロエレクトロニクス社製の32bitマイコンSTM32で動かす記事](https://qiita.com/HirohitoHigashi/items/7ef01d074baa27a3b2e7)、
今回はその第5回、GPIOの拡張と、複数プログラムの動作を行います。


# 目標

* オンボードのプッシュスイッチ入力を、mruby/c で読み取りができるようにする
* 外部に追加したLEDを、mruby/c から点滅できるようにする
* 複数のプログラムを、協調して動かすことができるようにする

# 準備するもの

* LED
* 固定抵抗器 （330Ω程度）
* ブレッドボードなど

LEDと固定抵抗器は、いずれもリードタイプを用意します。


# オンボードのプッシュスイッチ入力を、mruby/c で読み取りができるようにする

## ピンアサインの確認

Nucleoボードには、ユーザー入力用のプッシュスイッチが用意してあります。IDEのピンアサイン画面、もしくは回路図でピンアサイン（接続先）を確認すると、PC13　に接続されていることが分かります。

![STM32Tuto05-スクリーンショット 2024-06-04 13.42.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/44ab925d-832b-127e-0bc3-d01902df0274.png)

## mruby/c メソッドの作成

スイッチの入力を読み取るための、ユーザー拡張メソッドを作成します。これは、C言語で作成します。
仕様は、以下の通りとします。
 * sw_read() という名前にする
 * スイッチが押されていると1を、押されていないと0を返す

### 手順

CubeIDEの左ペイン Project Explorer から、Core > mrubyc とたどり、`start_mrubyc.c` をダブルクリックして開きます。

start_mrubyc() 関数の、コメント「ユーザ定義メソッドの登録」　あたりへ、以下通り muby/cから使うメソッド名と、実行するC関数を対応づける宣言を書きます。
```c
  mrbc_define_method(0, 0, "sw_read", c_sw_read);
```

次に、実際にそのメソッドを実行するC関数を書きます。
```c
/*! オンボードSW 読み取りメソッドの実装
*/
static void c_sw_read(mrbc_vm *vm, mrbc_value v[], int argc)
{
  switch( HAL_GPIO_ReadPin( GPIOC, GPIO_PIN_13 ) ) {
  case GPIO_PIN_SET:
    SET_INT_RETURN( 0 );
    break;
  case GPIO_PIN_RESET:
    SET_INT_RETURN( 1 );
    break;
  }
}
```

ファイルの先頭付近に、プロトタイプ宣言も必要です。
```c
static void c_sw_read(mrbc_vm *vm, mrbc_value v[], int argc);
```

<details>
<summary>start_mruby.c ファイル全体はこちら</summary>

```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);
static void c_sw_read(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);
  mrbc_define_method(0, 0, "sw_read", c_sw_read);

  // タスクの登録
  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 );
}

/*! オンボードSW 読み取りメソッドの実装
*/
static void c_sw_read(mrbc_vm *vm, mrbc_value v[], int argc)
{
  switch( HAL_GPIO_ReadPin( GPIOC, GPIO_PIN_13 ) ) {
  case GPIO_PIN_SET:
    SET_INT_RETURN( 0 );
    break;
  case GPIO_PIN_RESET:
    SET_INT_RETURN( 1 );
    break;
  }
}

/*! HAL
*/
int hal_write(int fd, const void *buf, int nbytes)
{
  extern UART_HandleTypeDef huart2;
  HAL_UART_Transmit( &huart2, buf, nbytes, HAL_MAX_DELAY );

  return nbytes;
}
int _write(int file, char *ptr, int len)
{
  return hal_write(file, ptr, len);
}

int hal_flush(int fd)
{
  return 0;
}
void hal_abort(const char *s)
{
}
```
</details>

## テストプログラムの作成

テスト用 mruby/c プログラムを作ります。簡単に、スイッチを押したらオンボードLEDが点灯するだけのプログラムとします。

### 手順

Project Explorer から、Core > mrubyc とたどり、`task1.rb` をダブルクリックして開きます。

以下の通り変更します。

```ruby:Core/mrubyc/task1.rb
while true
  sw = sw_read()
  led_write( sw )
end
```

## 実行

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

正常に動作していれば、青色のプッシュスイッチを押すと LED が点灯し、はなすと消えるのが確認できます。



# 外部に追加したLEDを、mruby/c から点滅できるようにする

次に、追加部品を使って、外部拡張端子に接続したLEDを制御してみます。

## ピンアサインの決定

まずは、接続するピンを決めます。ボードを眺めると、右下のコネクタ下から３番目に D2 と印刷された端子が、PWM等の他の機能が被ってなさそうなので、これに決めます。

回路図を見て、D2 はマイコンのGPIO番号 PA10 であることを見つけます。

![STM32Tuto05-スクリーンショット 2024-06-04 15.06.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/66103548-5577-c758-c4ef-9c1ce1f4558f.png)

## 外部LEDの取付け

以下の写真と回路図を参考に、LEDを取り付けます。

![STM32Tuto05-スクリーンショット 2024-06-04 16.02.06.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/33aae7bf-a6c5-14af-f035-d518b225a1bd.jpeg)
![STM32Tuto05-schema1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/17e3a29a-cca2-b522-0fc3-6f4a39843085.png)

## PA10ピンをGPIO出力に設定

Project Explorer から、（プロジェクト名）.ioc （ここでは、tuto01.ioc） をダブルクリックし、コンフィグレーション画面を開きます。

Pinout view 画面でICのPA10端子をクリックするとコンテキストメニューが開くので、`GPIO_Output` を選びます。
![STM32Tuto05-スクリーンショット 2024-06-04 15.10.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/d0a25867-1c51-dc8f-2294-072031b2b840.png)

メニューから、File > Save で保存すると、コード生成するかというダイアログが開きますので、[Yes] をクリックします。

## mruby/c メソッドの作成

PA10ピンを制御するための、ユーザー拡張メソッドを作成します。
今回はLEDをつなぎますが、実際は汎用出力なので、gpio_pa10_write() という名前のメソッドにします。

### 手順

Project Explorer から、Core > mrubyc とたどり、`start_mruby.c` をダブルクリックして開きます。

start_mrubyc() 関数の、コメント「ユーザ定義メソッドの登録」　あたりへ、以下通り muby/cから使うメソッド名と、実行するC関数を対応づける宣言を書きます。
```c
  mrbc_define_method(0, 0, "gpio_pa10_write", c_gpio_pa10_write);
```

次に、実際にそのメソッドを実行するC関数を書きます。
```c
/*! GPIO PA10 write メソッドの実装
*/
static void c_gpio_pa10_write(mrbc_vm *vm, mrbc_value v[], int argc)
{
  int on_off = GET_INT_ARG(1);
  HAL_GPIO_WritePin( GPIOA, GPIO_PIN_10, on_off );
}
```

ファイルの先頭付近に、プロトタイプ宣言も必要です。
```c
static void c_gpio_pa10_write(mrbc_vm *vm, mrbc_value v[], int argc);
```

## テストプログラムの作成

テスト用 mruby/c プログラムを作ります。やっぱり「えるちか」ですね。

### 手順

Project Explorer から、Core > mrubyc とたどり、`task1.rb` をダブルクリックして開きます。

以下の通り変更します。

```ruby:Core/mrubyc/task1.rb
while true
  gpio_pa10_write( 1 )
  sleep 1
  gpio_pa10_write( 0 )
  sleep 1
end
```

## 実行

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

![STM32Tuto05-Demo1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/5d7b6aea-d484-4775-9ab0-22fecef83112.gif)

無事にLEDが点滅したでしょうか。


# 複数のプログラムを、協調して動かすことができるようにする

mruby/c の特徴の一つとして、OSなしで複数のプログラムを同時に（プリエンプティブに）動作させる機能があります。これを試してみましょう。

## 仕様

* 通常時、外部LEDはゆっくり（１秒ごと）点滅し、オンボードLEDは消灯している
* プッシュスイッチが押されている間、外部LEDは早く（0.2秒ごと）点滅し、オンボードLEDは点灯する

これを、２つのプログラムに分けます。

プログラム1: スイッチ監視とオンボードLED担当
プログラム2: 外部LEDのエルチカ担当

mruby/c では、グローバル変数がプログラム間で共有されるので、今回はグローバル変数をプロセス間通信に使います。

## 手順

### task1.rb の変更

Project Explorer から、Core > mrubyc とたどり、`task1.rb` をダブルクリックして開きます。

以下の通り変更します。

```ruby:Core/mrubyc/task1.rb
$cycle_delay = 1

while true
  sw = sw_read()
  led_write( sw )

  if sw == 1
    $cycle_delay = 0.2
  else
    $cycle_delay = 1
  end
end
```

### task2.rb の新規作成

Project Explorer から、Core > mrubyc とたどり、mrubyc の上で右クリックし、New > File を選びます。

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

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

```ruby:Core/mrubyc/task2.rb
sleep 1    # task1で、$cycle_delay へ代入される事を確実にするため

while true
  gpio_pa10_write( 1 )
  sleep $cycle_delay
  gpio_pa10_write( 0 )
  sleep $cycle_delay
end
```

### task2 の実行を登録

Project Explorer から、Core > mrubyc とたどり、start_mrubyc.c をダブルクリックして開きます。

start_mrubyc() 関数の「タスクの登録」 コメント付近へ、task2 の実行を登録します。
```c
  // タスクの登録
  extern const uint8_t task1[];
  mrbc_create_task( task1, 0 );
  extern const uint8_t task2[];   // 追加
  mrbc_create_task( task2, 0 );   // 追加
```

### ビルド

いつも通り、メニューから、Run > Run もしくは、Project > Build Project を選んでビルドしてください。今回は恐らく以下のようにtask2のシンボルが見つからないと、ビルドエラーが出ると思います。
```
undefined reference to `task2'
```

そのまま、もう一度ビルドしてみてください。今度は正常に終了すると思います。
これは、task2.c が作られるタイミングの問題で、1度目のビルド時はまだ、task2.c が新規作成されておらず、ビルド終了後に IDE が、task2.c ができていることを認識するので、2度目は成功するという仕組みです。
もし、2度目以降もtask2のシンボルが見つからないとエラーが出るようなら、mruby フォルダに task2.c ができていることを確認し、それを手動で IDE に登録してください。

## 実行

![STM32Tuto05-Demo2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/cf9b265d-e40d-672f-7921-95ef3c9b8a07.gif)



# おわりに

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

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

今回は、オンボード以外のデバイスの追加と、それを mruby/c から使うための拡張関数をつくりました。また、複数のmruby/cプログラムを動かして、それらを協調動作させました。

このチュートリアルで説明した一通りの方法は、最初に説明したとおり、mruby/c を C言語の既存ソースコード、もしくは自動生成コードとともにビルドし、ターゲットに書き込む方法です。見方を変えれば、すでにC言語だけで書かれた既存機器のファームウェアを mruby/c で機能拡張するという場合に、ここで説明した方法は適しているとも言えます。
多機能、高機能、もしくはコンフィギャラブルな機器が求められる場合において、[DSLとして mruby/c を採用する](https://qiita.com/HirohitoHigashi/items/42f1a570cc35c3306493)のは結構良いアイデアだと思います。

今回はGPIOだけを扱いましたが、その他のペリフェラルをクラス化するためのガイドラインをmruby, mruby/c 共通で定めています。
https://github.com/mruby/microcontroller-peripheral-interface-guide

https://github.com/mruby/microcontroller-peripheral-interface-guide

別記事で、これらの API の移植を行う記事も書きますので、ご期待ください。


５回に分けて書いたポーティング記事も、今回で最後です。おつきあい、ありがとうございました。