mruby/cペリフェラルライブラリのSTM32マイコンへの実装 Chapter08: バイトコード書き込み機能実装編

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

[mruby/cペリフェラルライブラリのSTM32マイコンへの実装の記事](https://qiita.com/HirohitoHigashi/items/4a88aad1523455ae9857)、今回はその第8回、バイトコード書き込み機能を実装します。
正確には、バイトコード書き込み機能はペリフェラルライブラリガイドラインの範疇外ですが、STM32マイコンへの実装ということもあり、当該記事で扱います。


# おさらい

mruby/c は、RubyのソースコードをPC上であらかじめバイトコード（中間コード）に変換し、それをターゲットマイコン内の VM(Virtual machine) を使って実行します。
![STM32mrbc08-mrubyc動作フロー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/1f3da453-9655-ab41-3c41-a2412cbfed2f.png)

具体的に、マイコンボードで mruby/c プログラムを動作させるやり方は、以下の2種類が考えられます。

<dl>
<dt>方法1
<dd>あらかじめVMをROMに書き込んでおき、あとからバイトコードだけを書き込んで実行する方法
<dt>方法2
<dd>mruby/cのバイトコードをC言語の配列へ出力し、他のC言語ファイルとともにビルドしマイコンに書き込む方法
</dl>

本連載では、前回まで全て 方法2 でやってきました。

![STM32mrbc08-方法2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/2f0262ed-5cbb-ba86-13d7-856cbabc3e2b.png)

この方法のメリットは、以下の2点があげられます。
 * 既存資産(C言語プログラムなど)が生かせる
 * マイコンが持っているすべての機能を使うことができる

一方、デメリットとしては、
 * 開発環境の構築など、開発開始前の作業が多い
 * 試行錯誤が、少しやりにくくなる

などが、考えられます。

今回は、方法1「あらかじめVMをROMに書き込んでおき、あとからバイトコードだけを書き込んで実行する」ことにチャレンジします。

![STM32mrbc08-方法1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/fafd6347-2b8e-da71-cb76-f6a55bc75aa7.png)

この方法のメリットとしては、
 * mruby/cのみで簡単に開発が始められ、試行錯誤もやりやすい

ということが考えられますが、一方デメリットとしては、
 * VM側に用意されている機能以外を使いたい場合にはVMの拡張が必要

であるということで、ビギナー向けの環境や、顧客もしくは現場カスタマイズが必要なプロダクトで、それに mruby/c (DSL) を使うケースなどに特に向いていると考えています。


# 目標

 * 方法1が実現できるよう、バイトコードの受信と内蔵フラッシュメモリへの書き込み機能を実装する


# 今回の方針

 * バイトコード書き込みプログラム・プロトコルは、mruby/c github で公開されている標準のものを使う
[https://github.com/mrubyc/mrbwrite](https://github.com/mrubyc/mrbwrite)
 * UART2のUSBシリアル経由で、プログラムを書き込む
 * バイトコード書き込み先は、内蔵のフラッシュメモリとする

書き込みのための物理的接続方法として、UART2がコンソールとなっており、USBで接続できて都合が良いので、それを使います。

## HALライブラリ調査

メーカー製 HAL リファレンスマニュアル (UM1725) で、HALで内蔵フラッシュメモリを自己書き換えする方法を調査します。また、MPUのリファレンスマニュアル (RM0368) で、メモリマップなどを調べます。
その結果、以下の事が分かりました。

 * フラッシュメモリはSector0からSector7までセグメント化して管理されている

![STM32mrbc08-FlashMemoryMap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/269485a8-9d56-8e22-08f3-dcd952a4d01a.png)

 * 書き込み前に、セクタ単位で消去が必要
 * 消去のAPIは、`HAL_FLASHEx_Erase()` 関数
 * 書き込みは、1,2,4,8バイト単位で行うことができる
 * 書き込みのAPIは、`HAL_FLASH_Program()` 関数
 * 消去や書き込みの前に、`HAL_FLASH_Unlock()` でFlashへの書き込みロック解除が必要

CubeIDEのBuildAnalyzerを見ると、Sector0(0x0800 0000) から割り込みベクタ、続いてプログラム(.text)が書かれます。一方、反対端の Sector7(0x0806 0000) は空いているので、ここをバイトコード書き込み先とするほうが簡単そうです。


# 作業手順

では、実際の作業に入ります。

## リンカファイルの編集

Sector7のアドレス範囲をリンカが誤って使用しないように、予約します。

 * CubeIDEの左ペイン ProjectExplorer から、`STM32F401RETX_FLASH.ld` をダブルクリックして開きます
 * メモリ定義の行を探し、以下の通り書き換えて保存します
```:STM32F401RETX_FLASH.ld
/* Memories definition */
MEMORY
{
  RAM    (xrw)   : ORIGIN = 0x20000000,   LENGTH = 96K
  FLASH  (rx)    : ORIGIN = 0x08000000,   LENGTH = 384K
  IREP   (rx)    : ORIGIN = 0x08060000,   LENGTH = 128K
}
```

## バイトコード受信プロトコルの実装

mrbc_firm.c を作り、UART2を使ったバイトコード受信プロトコルを実装します。
プロトコル自体は、POP3プロトコルを模倣したテキストチャット形式のものです。

プロトコルのシーケンスを以下に引用します。

:::note info
典型的なmrbファイル（250バイト）書き込み例
```
mrbwrite:                :TARGET
   |                        |
   |  CRLF,CRLF,...         |    返信があるまで、Syncコードとして何度かCRLFを送信する
   +----------------------->|
   |                        |
   |  "+OK mruby/c"         |
   |<-----------------------+
   |                        |
   |  "clear"               |
   +----------------------->|
   |                        |
   |  "+OK"                 |
   |<-----------------------+
   |                        |
   |  "write 250"           |
   +----------------------->|
   |                        |
   |  "+OK Write bytecode"  |
   |<-----------------------+
   |                        |
   | (mrb file 250 bytes)   |
   +----------------------->|
   |                        |
   |  "+DONE"               |
   |<-----------------------+
   |                        |
   |  "execute"             |
   +----------------------->|
   |                        |
   |  "+OK"                 |
   |<-----------------------+
   |                        |
```
:::

実装したプログラムは以下の通りです。

<details>
<summary>クリックでプログラム全体の表示</summary>

```c:mrbc_firm.c
/*! @file
  @brief
  Receive bytecode and write to FLASH.

  <pre>
  Copyright (C) 2024- Shimane IT Open-Innovation Center.

  This file is distributed under BSD 3-Clause License.

  </pre>
*/

//@cond
#include <stdint.h>
#include <string.h>
//@endcond

#include "main.h"
#include "../mrubyc_src/mrubyc.h"
#include "stm32f4_uart.h"


#define VERSION_STRING   "mruby/c v3.3 RITE0300 MRBW1.2"

const uint32_t IREP_START_ADDR = 0x08060000;	// This is sector 7
const uint32_t IREP_END_ADDR   = 0x0807FFFF;	//  (see: cmd_clear function)

static const char RITE[4] = "RITE";
static const char WHITE_SPACE[] = " \t\r\n\f\v";


#define STRM_READ(buf, len)	uart_read(UART_HANDLE_CONSOLE, buf, len)
#define STRM_GETS(buf, size)	uart_gets(UART_HANDLE_CONSOLE, buf, size)
#define STRM_PUTS(buf)		uart_write(UART_HANDLE_CONSOLE, buf, strlen(buf))
#define STRM_RESET()		uart_clear_rx_buffer(UART_HANDLE_CONSOLE)
#define SYSTEM_RESET()		HAL_NVIC_SystemReset()

static int cmd_help();
static int cmd_version();
static int cmd_reset();
static int cmd_execute();
static int cmd_clear();
static int cmd_write();
static int cmd_showprog();


static uint32_t irep_write_addr_;	//!< IREP file write point.

//! command table.
static struct COMMAND_T {
  const char *command;
  int (*function)();

} const TBL_COMMANDS[] = {
  {"help",	cmd_help },
  {"version",	cmd_version },
  {"reset",	cmd_reset },
  {"execute",	cmd_execute },
  {"clear",	cmd_clear },
  {"write",	cmd_write },
  {"showprog",	cmd_showprog },
};

static const int NUM_TBL_COMMANDS = sizeof(TBL_COMMANDS)/sizeof(struct COMMAND_T);


//================================================================
/*! command 'help'
*/
static int cmd_help(void)
{
  STRM_PUTS("+OK\r\nCommands:\r\n");

  for( int i = 0; i < NUM_TBL_COMMANDS; i++ ) {
    STRM_PUTS("  ");
    STRM_PUTS(TBL_COMMANDS[i].command);
    STRM_PUTS("\r\n");
  }
  STRM_PUTS("+DONE\r\n");
  return 0;
}


//================================================================
/*! command 'version'
*/
static int cmd_version(void)
{
  STRM_PUTS("+OK " VERSION_STRING "\r\n");
  return 0;
}


//================================================================
/*! command 'reset'
*/
static int cmd_reset(void)
{
  SYSTEM_RESET();
  return 0;
}


//================================================================
/*! command 'execute'
*/
static int cmd_execute(void)
{
  STRM_PUTS("+OK Execute mruby/c.\r\n");
  return 1;	// to execute VM.
}


//================================================================
/*! command 'clear'
*/
static int cmd_clear(void)
{
  HAL_FLASH_Unlock();

  FLASH_EraseInitTypeDef erase = {
    .TypeErase = FLASH_TYPEERASE_SECTORS,
    .Sector = FLASH_SECTOR_7,
    .NbSectors = 1,
    .VoltageRange = FLASH_VOLTAGE_RANGE_3,  // Device operating range: 2.7V to 3.6V
  };
  uint32_t error = 0;
  HAL_StatusTypeDef sts = HAL_FLASHEx_Erase(&erase, &error);
  HAL_FLASH_Lock();

  if( sts == HAL_OK && error == 0xFFFFFFFF ) {
    STRM_PUTS("+OK\r\n");
  } else {
    STRM_PUTS("-ERR\r\n");
  }

  irep_write_addr_ = IREP_START_ADDR;
  return 0;
}


//================================================================
/*! command 'write'
*/
static int cmd_write( void *buffer, int buffer_size )
{
  char *token = strtok( NULL, WHITE_SPACE );
  if( token == NULL ) {
    STRM_PUTS("-ERR\r\n");
    return -1;
  }

  // check size
  int size = mrbc_atoi(token, 10);
  uint32_t irep_write_end = irep_write_addr_ + size;
  if( (irep_write_end > IREP_END_ADDR) || (size > buffer_size) ) {
    STRM_PUTS("-ERR IREP file size overflow.\r\n");
    return -1;
  }

  STRM_PUTS("+OK Write bytecode.\r\n");

  // get a bytecode.
  uint8_t *p = buffer;
  int n = size;
  while (n > 0) {
    int readed_size = STRM_READ( p, size );
    p += readed_size;
    n -= readed_size;
  }

  // check 'RITE' magick code.
  p = buffer;
  if( strncmp( (const char *)p, RITE, sizeof(RITE)) != 0 ) {
    STRM_PUTS("-ERR No RITE code received.\r\n");
    return -1;
  }

  // Write bytecode to FLASH.
  HAL_FLASH_Unlock();

  size += (-size & 3);		// align 4 byte.
  irep_write_end = irep_write_addr_ + size;

  while( irep_write_addr_ < irep_write_end ) {
    uint32_t data = p[3] << 24 | p[2] << 16 | p[1] << 8 | p[0];

    HAL_StatusTypeDef sts;
    sts = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, irep_write_addr_, data);

    if( sts != HAL_OK ) {
      STRM_PUTS("-ERR Flash write error.\r\n");
      HAL_FLASH_Lock();
      return -1;
    }

    p += 4;
    irep_write_addr_ += 4;
  }
  HAL_FLASH_Lock();

  STRM_PUTS("+DONE\r\n");

  return 0;
}


//================================================================
/*! command 'showprog'
*/
static int cmd_showprog(void)
{
  uint8_t *addr = (uint8_t *)IREP_START_ADDR;
  int n = 0;
  char buf[80];

  STRM_PUTS("idx size offset\r\n");
  while( strncmp( (const char *)addr, RITE, sizeof(RITE)) == 0 ) {
    unsigned int size = 0;
    for( int i = 0; i < 4; i++ ) {
      size = (size << 8) | addr[8 + i];
    }
    mrbc_snprintf(buf, sizeof(buf), " %d  %-4d %p\r\n", n++, size, addr);
    STRM_PUTS(buf);

    addr += size + (-size & 3);	// align 4 byte.
  }

  int total = (IREP_END_ADDR - IREP_START_ADDR + 1);
  int used = (uintptr_t)addr - IREP_START_ADDR;
  int percent = 100 * used / total;
  mrbc_snprintf(buf, sizeof(buf), "total %d / %d (%d%%)\r\n", used, total, percent);
  STRM_PUTS(buf);
  STRM_PUTS("+DONE\r\n");

  return 0;
}


//================================================================
/*! receive bytecode mode
*/
int receive_bytecode( void *buffer, int buffer_size )
{
  char buf[50];

  STRM_PUTS("+OK mruby/c\r\n");

  while( 1 ) {
    // get the command string.
    if( STRM_GETS(buf, sizeof(buf)) < 0 ) {
      STRM_RESET();
      continue;
    }

    // split tokens.
    char *token = strtok( buf, WHITE_SPACE );
    if( token == NULL ) {
      STRM_PUTS("+OK mruby/c\r\n");
      continue;
    }

    // find command.
    int i;
    for( i = 0; i < NUM_TBL_COMMANDS; i++ ) {
      if( strcmp( token, TBL_COMMANDS[i].command ) == 0 ) break;
    }
    if( i == NUM_TBL_COMMANDS ) {
      STRM_PUTS("-ERR Illegal command. '");
      STRM_PUTS(token);
      STRM_PUTS("'\r\n");
      continue;
    }

    // execute command.
    if( (TBL_COMMANDS[i].function)(buffer, buffer_size) == 1 ) break;
  }

  return 0;
}


//================================================================
/*! pick up a task
*/
void * pickup_task( void *task )
{
  uint8_t *addr = (uint8_t *)IREP_START_ADDR;

  if( task ) {
    if( strncmp( task, RITE, sizeof(RITE)) != 0 ) return 0;

    addr = task;
    unsigned int size = 0;
    for( int i = 0; i < 4; i++ ) {
      size = (size << 8) | addr[8 + i];
    }

    addr += size + (-size & 3);	// align 4 byte.
  }

  if( strncmp( (const char *)addr, RITE, sizeof(RITE)) == 0 ) {
    return addr;
  }

  return 0;
}
```
</details>

プログラム全体は少々長いので、ポイントをかいつまんで説明します。

### 書き込みチャネルの使用APIを、マクロでWrapする

```c
#define STRM_READ(buf, len)     uart_read(UART_HANDLE_CONSOLE, buf, len)
#define STRM_GETS(buf, size)    uart_gets(UART_HANDLE_CONSOLE, buf, size)
#define STRM_PUTS(buf)          uart_write(UART_HANDLE_CONSOLE, buf, strlen(buf))
#define STRM_RESET()            uart_clear_rx_buffer(UART_HANDLE_CONSOLE)
```

今回はバイトコード書き込み方法として、UARTを選びました。前回作ったUARTサブルーチン関数を使うようマクロ定義しています。UART以外の書き込み方法に対応する場合、バイナリ入力(READ)、文字列入力(GETS)、文字列出力(PUTS)、リセットの4つに対応する事ができれば、ここを変更するだけで対応できる予定です。

### Flash(Sector7)の消去

消去は、プロトコルの clear コマンドによって行われます。
```c
static int cmd_clear(void)
{
  HAL_FLASH_Unlock();

  FLASH_EraseInitTypeDef erase = {
    .TypeErase = FLASH_TYPEERASE_SECTORS,
    .Sector = FLASH_SECTOR_7,
    .NbSectors = 1,
    .VoltageRange = FLASH_VOLTAGE_RANGE_3,  // Device operating range: 2.7V to 3.6V
  };
  uint32_t error = 0;
  HAL_StatusTypeDef sts = HAL_FLASHEx_Erase(&erase, &error);
  HAL_FLASH_Lock();

  if( sts == HAL_OK && error == 0xFFFFFFFF ) {
    STRM_PUTS("+OK\r\n");
  } else {
    STRM_PUTS("-ERR\r\n");
  }

  irep_write_addr_ = IREP_START_ADDR;
  return 0;
}
```

HAL_FLASH_Unock() でロックを解除してから、HAL_FLASHEx_Erase() で消去し、HAL_FLASH_Lock() で再度ロックを戻しています。

ここで一つ問題が発生しました。公開されている mrbwrite.exe プログラムでは、一つのコマンドのタイムアウトが1秒にハードコーディングされています。このチップの Sector7 消去時間は、1秒をほんの少し超過するようで、mrbwrite.exe プログラムがタイムアウトになってしまいます。消去終了を待たずに `+OK` ステータスを返すとか、回避策は考えられるのですが、後々大変になることがわかりきっているので、mrbwrite.exe プログラム側の改造（タイムアウト時間の延長）も、併せて行うことにします。

### バイトコード書き込み

バイトコードの書き込みは、`HAL_FLASH_Program()`関数で行います。マニュアルでは、1,2,4,8バイト単位で書き込めるので、簡単さを求めるなら1バイト単位で書き込む方法が選択肢としてあがります。
しかし、複数バイト同時に書き込んだ方が速度的に有利だろうと予想し、実際に実験してみると2倍以上の速度差があったため、ここでは4バイト単位で書き込むよう実装しました。なお8バイト単位で書き込むと、このチップでは書き込みエラーとなるようです。

```c
  uint8_t *p = buffer;

  while( irep_write_addr_ < irep_write_end ) {
    uint32_t data = p[3] << 24 | p[2] << 16 | p[1] << 8 | p[0];

    HAL_StatusTypeDef sts;
    sts = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, irep_write_addr_, data);
    ...
```

### コマンドチャットの実装

UART通信を使って、mrbwrite プロトコルを実際に駆動する部分です。

```c
int receive_bytecode( void *buffer, int buffer_size )
{
  char buf[50];

  STRM_PUTS("+OK mruby/c\r\n");

  while( 1 ) {
    // get the command string.
    if( STRM_GETS(buf, sizeof(buf)) < 0 ) {
      STRM_RESET();
      continue;
    }

    // split tokens.
    char *token = strtok( buf, WHITE_SPACE );
    if( token == NULL ) {
      STRM_PUTS("+OK mruby/c\r\n");
      continue;
    }

    // find command.
    int i;
    for( i = 0; i < NUM_TBL_COMMANDS; i++ ) {
      if( strcmp( token, TBL_COMMANDS[i].command ) == 0 ) break;
    }
    if( i == NUM_TBL_COMMANDS ) {
      STRM_PUTS("-ERR Illegal command. '");
      STRM_PUTS(token);
      STRM_PUTS("'\r\n");
      continue;
    }

    // execute command.
    if( (TBL_COMMANDS[i].function)(buffer, buffer_size) == 1 ) break;
  }

  return 0;
}
```

UARTから1行取り込んで、strtok関数を使ってトークナイズし、各コマンドの関数に実際の動作は委譲する戦略です。

### 書き込まれたタスクのピックアップ

この関数は、フラッシュメモリのIREP書き込みエリア(Sector7)から、複数個書かれているであろうバイトコードを順番にピックアップします。
mruby/c 実行準備ルーチンが、これを使って複数のタスクを生成するための関数です。

```c
void * pickup_task( void *task )
{
  uint8_t *addr = (uint8_t *)IREP_START_ADDR;

  if( task ) {
    if( strncmp( task, RITE, sizeof(RITE)) != 0 ) return 0;

    addr = task;
    unsigned int size = 0;
    for( int i = 0; i < 4; i++ ) {
      size = (size << 8) | addr[8 + i];
    }

    addr += size + (-size & 3);	// align 4 byte.
  }

  if( strncmp( (const char *)addr, RITE, sizeof(RITE)) == 0 ) {
    return addr;
  }

  return 0;
}
```

## バイトコード書き込みモードへスイッチする部分の実装

マイコン側がバイトコード書き込みモードになる方法はいくつか考えられますが、ここでは他のマイコン実装でよく使っている、「リセット直後の数秒間に書き込み依頼があれば受け付ける」という動作を採用します。

### 書き込みモードへ入るか判定するルーチンの実装

mruby/c の開始は、start_mrubyc() 関数から始まるようにデザインしたので、そこへ判定ルーチンを追加します。

```c:Core/mrubyc/start_mrubyc.c
/*! バイトコード書き込みモードに移行するか？

  (Strategy)
  LED1を点滅させながら、一定時間内にコンソール(UART)へ改行文字が入力されたら1を返す
*/
int check_boot_mode( void )
{
  const int MAX_WAIT_CYCLE = 256;
  int ret = 0;

  for( int i = 0; i < MAX_WAIT_CYCLE; i++ ) {
    HAL_GPIO_WritePin( GPIOA, GPIO_PIN_5,
		       ((i>>4) | (i>>1)) & 0x01 );	// Blink LED1
    if( uart_can_read_line( UART_HANDLE_CONSOLE )) {
      uart_clear_rx_buffer( UART_HANDLE_CONSOLE );
      ret = 1;
      break;
    }
    HAL_Delay( 10 );
  }
  HAL_GPIO_WritePin( GPIOA, GPIO_PIN_5, 0 );

  return ret;
}


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

  switch( check_boot_mode() ) {
  case 1:
    receive_bytecode( memory_pool, MRBC_MEMORY_SIZE );
    memset( memory_pool, 0, MRBC_MEMORY_SIZE );
    break;

  default:
    break;
  }

  mrbc_init(memory_pool, MRBC_MEMORY_SIZE);
  ...
```

check_boot_mode() 関数で、書き込みプロトコル開始の改行が入力されるかを判断しながら、タイムアウトまで待ちます。オンボードLEDがあるので、利用者へのわかりやすさのため点滅させながら、待つようにしています。
check_boot_mode() 関数で、書き込み開始となったら、先ほどの receive_bytecode() 関数に処理を任せます。

### タスクの登録

前回までは、task1[] という固定のアドレスに書かれたタスクを登録するコードを書いていました。

```c
  mrbc_create_task( task1, 0 );
```

これをやめて、以下の通り変更します。

```c:Core/mrubyc/start_mrubyc.c
  // タスクの登録
  void *task = 0;
  while( 1 ) {
    task = pickup_task( task );
    if( task == 0 ) break;

    mrbc_create_task( task, 0 );
  }
```

UARTで転送され、フラッシュメモリに書き込まれたバイトコードは、pickup_task() 関数によって個別にピックアップされ、mrbc_create_task() 関数でタスクとして登録されます。

以上で終了です。


# テスト：ターミナルソフトで通信の確認

シリアルターミナルソフト TeraTerm 等を使って、書き込みプロトコル通信部が正しく動作しているかを確認します。
わかりやすさのため、ローカルエコーをONにします。

  1. TeraTermを起動します
  2. メニューから、Setup > Terminal を選びます
  3. 表示されたダイアログの Local echo チェックボックスをONにします
![STM32mrbc08-SS_TeraTerm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/a52a7f81-8ac4-0591-c8a7-5e447b61782d.png)

ターゲットボードのリセットスイッチを押し、すばやくTeraTerm上でEnterキーを押します。うまく通信ができれば、`+OK mruby/c` と表示されます。

![STM32mrbc08-Demo_TeraTerm.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/d6f04e5b-9ec0-3649-1a0c-937619dc3c56.gif)

`help` や、`version` コマンドが効くことの確認ができます。


# テスト：プログラムを作って全体動作の確認

オンボードのLEDとプッシュスイッチを使ったプログラムを以下の仕様で作り、動作確認します。

 * LEDを使ってエルチカ、初期は0.1秒（100ms) ごとにON/OFFする
 * スイッチを押すごとに、100ms→200ms→300ms... と点滅周期が長くなる
 * スイッチを長押しすると、初期の100ms周期に戻る

この仕様から、以下の2つのタスクに分割して実装します。

 1. LEDを点滅させるタスク。周期は、グローバル変数 $sleep_time によって外部から変更できる。
 2. スイッチをポーリングにより監視し、$sleep_time を変更するタスク。

mruby/c は複数のタスクを同時（タイムスライス）に実行できる機能があるので、役割ごとにタスクを分割します。また、複数のタスク間でグローバル変数が共有される仕様なので、それをタスク間通信として利用します。

## 実装

```ruby:task1.rb
$sleep_time = 100

led = GPIO.new("PA5", GPIO::OUT)

while true
  led.write( 1 )
  sleep_ms $sleep_time
  led.write( 0 )
  sleep_ms $sleep_time
end
```

オンボードLED用のメソッド led_write が残してありますが、汎用性を考えてGPIOクラスを使ってみました。

次に、プッシュスイッチを受け持つタスクです。

```ruby:task2.rb
#
# プッシュスイッチクラス
#
class PushSwitch
  attr_accessor :polarity       # Set the value when the switch is pressed.
  attr_accessor :long_press_th

  # コンストラクタ
  def initialize(port)
    @polarity = 0
    @long_press_th = 10
    @long_press_cnt = 0

    @sw = GPIO.new(port, GPIO::IN)
    @sw1 = 1 - @polarity
  end

  # 読み込み
  #  (note) 一定期間（例：100ms）ごとにコールする
  def read
    @sw0 = @sw1
    @sw1 = @sw.read

    ret = (@sw1 == @polarity)
    if ret
      @long_press_cnt += 1
    else
      @long_press_cnt = 0
    end

    return ret
  end

  # スイッチを押されたか？
  def pressed?
    return @polarity == 0 ? (@sw0 > @sw1) : (@sw0 < @sw1)
  end

  # スイッチを離されたか？
  def released?
    return @polarity == 0 ? (@sw0 < @sw1) : (@sw0 > @sw1)
  end

  # スイッチを長押しされたか？
  def long_pressed?
    return @long_press_cnt == @long_press_th
  end
end

#
# メインループ
#
sw = PushSwitch.new("PC13")
n = 1

while true
  sw.read

  if sw.pressed?
    n += 1
    $sleep_time = n * 100
  end

  if sw.long_pressed?
    n = 1
    $sleep_time = n * 100
  end

  sleep_ms 100
end
```

スイッチを、押された、離された、長押しされた等の判断が必要になるので、その機能を`PushSwitch` クラスにまとめました。
プッシュスイッチを、while ループ内で約100msごとに `sw.read` メソッドを呼び出すことによってポーリングします。利用は`pressed?` 等のメソッドを使ってチャタリングの影響なく簡易に使う事ができます。


## ボードへの書き込み

ボードへの書き込みは、まず Rubyソースコードをバイトコードファイルに変換し、それを書き込みツール mrbwrite を使ってターゲットボードへ書き込みます。
ITOC製の IDE を使うと編集から書き込みまで自動化してくれますが、この記事では何をやっているかを明確にするためにあえて手動でやってみます。

mrubyコンパイラと、書き込みツール mrbwrite が必要です。あらかじめ、ITOCの以下のページからダウンロードして準備しておきます。
https://www.s-itoc.jp/support/technical-support/mrubyc/mrubyc-download/

ダウンロードするもの。
 1. mrubyコンパイラ　mruby/c 3.x 対応版
 2. mrbwrite バージョン　1.2

mrbwriteは、バージョン1.2から、Flash(Sector7)の消去の項で述べたタイムアウトに関する不具合を解消するために、タイムアウト時間を1秒から5秒に延長してあります。


## 手順

コマンドプロンプトを起動し、2つのソースコードからバイトコードファイル *.mrb を作ります。

```
>mrbc.exe task1.rb

>mrbc.exe task2.rb

>dir
2024/08/09  14:27    <DIR>          .
2024/08/09  13:46    <DIR>          ..
2024/08/09  14:27               203 task1.mrb
2024/08/09  14:27               984 task2.mrb
2024/08/06  12:57               148 task1.rb
2024/08/07  12:11             1,198 task2.rb
               4 個のファイル               3,301 バイト
               2 個のディレクトリ  104,419,921,920 バイトの空き領域
```

書き込みツール、mrbwrite を使って、ターゲットに mrb ファイルを書き込みます。
まず、--showline オプションを使って、ターゲットがどの COM ポートで認識されているか確認します。

```
>mrbwrite.exe --showline
COM3    STMicroelectronics STLink Virtual COM Port      STMicroelectronics
```

この場合、COM3でした。
調べたCOM番号を、 -l オプションに続けて指定し、ボーレートを -s オプションに続けて指定の後、mrb ファイルを指定します。mrbwrite 起動後、素早くターゲットのリセットスイッチを押します。

```
>mrbwrite.exe -l COM3 -s 9600 task1.mrb task2.mrb
　Start connection.
　（（（ここで素早くリセットスイッチを押す）））
OK.
Clear existed bytecode.
Writing task1.mrb
OK.
Writing task2.mrb
OK.
idx size offset
 0  203  $08060000
 1  984  $080600cc
total 1188 / 131072 (0%)
Start mruby/c program.
OK.
```

上記のようなメッセージが表示されて、書き込みが完了します。


## 実行結果

![STM32mrbc08-Demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/660332eb-874a-763c-2bfa-477b680528bd.gif)

スイッチを押すごとに点滅周期が長くなる事と、スイッチを長押しすると、最初の点滅周期に戻ることが確認できました。


# おわりに

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

https://github.com/HirohitoHigashi/mrubyc-nucleo-F401RE/tree/chapter08_mrbwrite

今回は、mrubyのバイトコードだけを書き換える方法を実行するために、mrbwrite プロトコルの実装を行いました。書き換えのための通信路にはUARTを使いましたが、ストリーム系の通信路なら何でも応用ができると思います。

8回に渡って連載したペリフェラルI/Oクラスのガイドラインの実装およびmrbwriteプロトコルの実装も、今回で最終回です。おつきあい、ありがとうございました。