mruby/cペリフェラルライブラリのSTM32マイコンへの実装 Chapter07: UARTクラス実装編

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

[mruby/cペリフェラルライブラリのSTM32マイコンへの実装の記事](https://qiita.com/HirohitoHigashi/items/4a88aad1523455ae9857)、今回はその第7回、UARTクラスを実装します。

# 目標

[UARTクラスのAPIガイドライン](https://github.com/mruby/microcontroller-peripheral-interface-guide/blob/main/mruby_io_UART_ja.md) に従って、STM32マイコン（Nucleo F401RE) 向けの実装を完了させる。

# 今回の方針

* CubeIDEで設定したユニット UART1, UART2, UART6 をサポートする
* 各ユニットで、ピン割り当ては固定とする
* フロー制御はサポートしない
* C言語のみで実装する
* まずC言語から使えるサブルーチン関数群をつくり、mruby/cメソッド関数からはそれらを使うように構成する

UARTユニットも、他のペリフェラルと同様、ピン割り当ての自由度がそれほど高くありません。そのため、各ユニットの使用ピンは固定とします。
フロー制御(RTS/CTS)は、UART1についてはハード的に可能ですが、その分貴重なGPIOピンをとられてしまうので、今回はDisableを選択しています。また、UART2はフローなしでプログラマ基板へ配線されていますし、UART6はハードウェアフロー制御の機能がありません。
また、UARTはデバッグ用モニタやバイトコード書き込みプログラムといった C言語で書くべき他の仕組みからも使いたいので、C言語から使える関数群をつくり、mruby/cメソッド関数からはそれらを使うように構成します。


# UARTユニットの仕様と、使用方法調査

## ピン配置

| CN | pin | SILK | GPIO | Usage |
|-|-:|-|-|-|
| CN5 | 1 | D8 | PA9 | UART1_TX |
| CN9 | 3 | D2 | PA10 | UART1_RX |
| CN9 | 2 | TX/D1 | PA2 | (UART2_TX) |
| CN9 | 1 | RX/D0 | PA3 | (UART2_RX) |
| CN10 | 12 |  | PA12 | UART6_RX |
| CN10 | 14 |  | PA11 | UART6_TX |


## HALライブラリ調査

メーカー製 HAL リファレンスマニュアル (UM1725) や、CubeIDEでのコード自動生成により、HALでUARTを使う方法を調査します。
UART も I2C や SPI と同様に、ポーリング、割り込み、DMAの3種類の使用方法があり、APIもそれぞれで読み書き両方が定義されています。
例えばポーリングモードでは、以下の2つの関数が用意されています。
 * HAL_UART_Transmit()
 * HAL_UART_Receive()

一方、UART は I2C や SPI のようにペリフェラルに親子の関係は無いため、接続先から任意のタイミングでデータが送信されてくることを想定しなければならず、実際ガイドラインでもそのようにAPIが設計されています。

問題になるのは受信側で、`HAL_UART_Receive()` 関数ではその要求を満たせず使えません。割り込みやDMAに関しても似たようなもので、単に専用ハードウェアでポーリングが自動化されたというだけです。
ところが、STM32のDMAについてもう少し調べてみると、Circular mode というモードを持っています。このモード自体は珍しいものではなく、DMAが一定のメモリ範囲をアクセスし終えると自動的に範囲の先頭に戻って再度アクセスを始めるモードです。これはすなわち、リングバッファ(FIFO)です。残念ながらバッファオーバランの検出はできませんが、ソフトウェアを書くことなく、しかもCPUを介さずにリングバッファを構成できるのは魅力的なので、これを使ってみることにします。

その他、HALを使う上でいくつかの注意点がありました。

  * break 信号の出力機能は、少なくともHALライブラリレベルでは用意されていない
  * ボーレートジェネレータレジスタのビット数の関係で、現在のクロック設定では 300bps 等の極端に遅いボーレートはサポートできない


# 作業手順

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

## UARTのパラメータの調整

ガイドラインで示されたデフォルト値に、CubeIDE を使って設定します。
Project Explorer から、（プロジェクト名）.ioc をダブルクリックし、コンフィグレーション画面を開き、Connectivity > USART1 をクリックします。

USART1 Mode and Configuration 画面
![STM32mrbc07-CubeIDE01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/db660587-ae84-797e-867c-290e151ff4e1.png)

1. Mode欄を Asynchronous に変更
1. Parameter Settings タブ > Basic Parameters > Baud Rate を、9600 Bits/s に変更

次にDMAの設定変更です。
![STM32mrbc07-CubeIDE02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/31a667a4-8ff3-d422-42be-92b50870ba59.png)

1. DMA Settingsタブをクリック
1. [Add] ボタンをクリック
1. Select欄が表示されるので、USART1_RXに変更
1. Mode を、Circularに変更

ボーレートは、デフォルトでは、115200bps ですが、ガイドラインのデフォルト値は 9600bps ですので、ここではそれに従います。
同様に、UART2, UART6も変更しますが、UART2はコンソールですので、9600bpsでも115200bpsのままでも、どちらでも良いと思います。


## 雛形の作成

UARTは、前述の通り他の用途にも使うので、専用のヘッダファイルを用意します。

```c:Core/mrubyc/stm32f4_uart.h
#ifndef STM32F4_UART_H
#define STM32F4_UART_H

#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

#ifndef UART_SIZE_RXFIFO
#define UART_SIZE_RXFIFO 1024
#endif

typedef struct UART_HANDLE {
  uint8_t unit_num;             //!< UART unit number 1..
  uint8_t delimiter;            //!< line delimiter such as '\\n'
  uint16_t rx_rd;               //!< index of rxfifo for read.

  UART_HandleTypeDef *hal_uart;         //!< STM32 HAL library UART handle.
  int rxfifo_size;                      //!< FIFO size
  uint8_t rxfifo[UART_SIZE_RXFIFO];     //!< FIFO for received data.

} UART_HANDLE;

#ifdef __cplusplus
}
#endif
#endif
```

このヘッダファイルでは、HALライブラリ管理ハンドルへのポインタや受信用FIFOを内包した構造体、`UART_HANDLE` を用意し、mrubyライブラリからはこの `UART_HANDLE` を使うようデザインしました。

次に、[GPIOクラス実装編](https://qiita.com/HirohitoHigashi/items/9be9124f01a180fb68b1) と同様に、雛形のファイルを用意してから、これに肉付けしていきます。

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

static void c_uart_new(mrbc_vm *vm, mrbc_value v[], int argc) {}
static void c_uart_setmode(mrbc_vm *vm, mrbc_value v[], int argc) {}
static void c_uart_read(mrbc_vm *vm, mrbc_value v[], int argc) {}
static void c_uart_write(mrbc_vm *vm, mrbc_value v[], int argc) {}
static void c_uart_gets(mrbc_vm *vm, mrbc_value v[], int argc) {}
static void c_uart_puts(mrbc_vm *vm, mrbc_value v[], int argc) {}
static void c_uart_bytes_available(mrbc_vm *vm, mrbc_value v[], int argc) {}
static void c_uart_bytes_to_write(mrbc_vm *vm, mrbc_value v[], int argc) {}
static void c_uart_can_read_line(mrbc_vm *vm, mrbc_value v[], int argc) {}
static void c_uart_flush(mrbc_vm *vm, mrbc_value v[], int argc) {}
static void c_uart_clear_tx_buffer(mrbc_vm *vm, mrbc_value v[], int argc) {}
static void c_uart_clear_rx_buffer(mrbc_vm *vm, mrbc_value v[], int argc) {}
static void c_uart_send_break(mrbc_vm *vm, mrbc_value v[], int argc) {}

void mrbc_init_class_uart(void)
{
  mrbc_class *cls = mrbc_define_class(0, "UART", 0);

  mrbc_define_method(0, cls, "new",             c_uart_new);
  mrbc_define_method(0, cls, "setmode",         c_uart_setmode);
  mrbc_define_method(0, cls, "read",            c_uart_read);
  mrbc_define_method(0, cls, "write",           c_uart_write);
  mrbc_define_method(0, cls, "gets",            c_uart_gets);
  mrbc_define_method(0, cls, "puts",            c_uart_puts);
  mrbc_define_method(0, cls, "bytes_available", c_uart_bytes_available);
  mrbc_define_method(0, cls, "bytes_to_write",  c_uart_bytes_to_write);
  mrbc_define_method(0, cls, "can_read_line",   c_uart_can_read_line);
  mrbc_define_method(0, cls, "flush",           c_uart_flush);
  mrbc_define_method(0, cls, "clear_rx_buffer", c_uart_clear_rx_buffer);
  mrbc_define_method(0, cls, "clear_tx_buffer", c_uart_clear_tx_buffer);
  mrbc_define_method(0, cls, "send_break",      c_uart_send_break);

  mrbc_set_class_const(cls, mrbc_str_to_symid("NONE"), &mrbc_integer_value(0));
  mrbc_set_class_const(cls, mrbc_str_to_symid("ODD"), &mrbc_integer_value(1));
  mrbc_set_class_const(cls, mrbc_str_to_symid("EVEN"), &mrbc_integer_value(2));
}
```

クラス定義ファイル本体では、全てのメソッドの雛形と、ガイドラインで規定されたクラス定数を定義します。数値の0, 1, 2 に深い意味は無く、識別できれば良いです。

次に、`start_mrubyc()` 関数へ、今回作成したUART初期化用関数 mrbc_init_class_uart() をコールするよう書き足します。

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

  // 各クラスの初期化
  ...snip...
  void mrbc_init_class_uart(void);    // 追加
  mrbc_init_class_uart();             // 追加
```
この段階で、一度正しくビルドができるか確認します。
さらに注意深く確認するには、Rubyスクリプトでも UART クラスが使えるようになったかを確認するコードを書いておくと良いと思います。

```ruby:Core/mrubyc/task1.rb
uart = UART.new()

puts "DONE"
```

## UARTハンドルの初期化

stm32f4_uart.h で定義したUART_HANDLE構造体を、UART1, UART2, UART6の３つについて静的に用意し、初期化を行います。また、それらをユニット番号（整数）でアクセスしやすいように、配列　`TBL_UART_HANDLE[]` にしておきます。

```c:Core/mrubyc/stm32f4_uart.c
UART_HANDLE * const TBL_UART_HANDLE[/*unit*/] = {
  0,

  // UART1
  &(UART_HANDLE){
    .unit_num = 1,
    .delimiter = '\n',
    .hal_uart = &huart1,
    .rxfifo_size = UART_SIZE_RXFIFO,
  },

  // UART2
  &(UART_HANDLE){
    .unit_num = 2,
    .delimiter = '\n',
    .hal_uart = &huart2,
    .rxfifo_size = UART_SIZE_RXFIFO,
  },

  0,0,0,

  // UART6
  &(UART_HANDLE){
    .unit_num = 6,
    .delimiter = '\n',
    .hal_uart = &huart6,
    .rxfifo_size = UART_SIZE_RXFIFO,
  },
};
```

この配列を `stm32f4_uart.h` ヘッダファイルの中へ extern 宣言しておきます。また、コンソールに使うUARTを `UART_HANDLE_CONSOLE` という名前で参照できるように define も追加しておきます。

```c:Core/mrubyc/stm32f4_uart.h
extern UART_HANDLE * const TBL_UART_HANDLE[];

#define UART_HANDLE_CONSOLE TBL_UART_HANDLE[2]
```

## UART受信開始の記述

今回、DMAにて自動的に受信データをリングバッファに入れる方針にしました。その受信開始の指示はプログラムによって指示する必要があります。汎用性を考えて `uart_init()` という名前の関数にまとめておきます。

```c:Core/mrubyc/stm32f4_uart.c
void uart_init(void)
{
  for( int i = 0; i < sizeof(TBL_UART_HANDLE)/sizeof(UART_HANDLE *); i++ ) {
    UART_HANDLE *hndl = TBL_UART_HANDLE[i];
    if( !hndl ) continue;

    HAL_UART_Receive_DMA(hndl->hal_uart, hndl->rxfifo, hndl->rxfifo_size);
  }
}
```

`HAL_UART_Receive_DMA` 関数が、DMAを使って連続して受信を行う関数です。一度コールすれば、第２引数で示したポインタから始まるバッファに受信データを入れ、バッファの終わりになったら自動的にバッファの開始位置からまた受信データを入れる動作を繰り返します。
このUART受信開始は、なるべく早い段階で行うほうが後々都合が良いことが予想されるので、`start_mrubyc()` 関数の開始直後でコールするようにします。

```c:Core/mrubyc/start_mrubyc.c
void start_mrubyc( void )
{
  uart_init();
  ...
```

## UART機能提供サブルーチン群の作成

他のマイコン用実装も参考にしながら実装した結果、以下の8つのサブルーチン関数を用意することになりました。これらは、mruby/c のメソッド関数からだけではなく、別途C言語プログラムからも使うことになります。

```c
int uart_setmode(const UART_HANDLE *hndl, int baud, int parity, int stop_bits);
int uart_read(UART_HANDLE *hndl, void *buffer, int size);
int uart_write(UART_HANDLE *hndl, const void *buffer, int size);
int uart_gets(UART_HANDLE *hndl, void *buffer, int size);
int uart_is_readable(const UART_HANDLE *hndl);
int uart_bytes_available(const UART_HANDLE *hndl);
int uart_can_read_line(const UART_HANDLE *hndl);
void uart_clear_rx_buffer(UART_HANDLE *hndl);
```

それぞれ、おおよその動作は関数名から想像できると思います。
主要ないくつかの関数を説明します。

### uart_setmode 関数

ボーレートなどのパラメータを変更するための関数です。

```c
//================================================================
/*! set mode

  @memberof UART_HANDLE
  @param  hndl          target UART_HANDLE
  @param  baud          baud rate.
  @param  parity        0:none 1:odd 2:even
  @param  stop_bits     1 or 2
  @note いずれも設定変更しないパラメータは、-1 を渡す。
*/
int uart_setmode( const UART_HANDLE *hndl, int baud, int parity, int stop_bits )
{
  if( baud >= 0 ) {
    hndl->hal_uart->Init.BaudRate = baud;
  }

  switch( parity ) {
  case 0:
    hndl->hal_uart->Init.Parity = UART_PARITY_NONE;
    hndl->hal_uart->Init.WordLength = UART_WORDLENGTH_8B;
    break;
  case 1:
    hndl->hal_uart->Init.Parity = UART_PARITY_ODD;
    hndl->hal_uart->Init.WordLength = UART_WORDLENGTH_9B;
    break;
  case 2:
    hndl->hal_uart->Init.Parity = UART_PARITY_EVEN;
    hndl->hal_uart->Init.WordLength = UART_WORDLENGTH_9B;
    break;
  }

  if( 1 <= stop_bits && stop_bits <= 2 ) {
    static uint32_t const TBL_STOPBITS[] = { UART_STOPBITS_1, UART_STOPBITS_2 };
    hndl->hal_uart->Init.StopBits = TBL_STOPBITS[ stop_bits - 1 ];
  }

  if( HAL_UART_Init( hndl->hal_uart ) != HAL_OK ) return -1;

  return 0;
}
```

引数で与えたパラメータから、hal_uart (UART_HandleTypeDef) のメンバを変更し、最終的に HAL_UART_Initをコールしています。データシートでは、送受信中は変更しないようにとの記述がありましたが、送信はともかく受信に関しては相手が送ってくるので止めようがなく、無視して HAL_UART_Init をコールしています。

### uart_bytes_available 関数

受信バッファに溜まっているデータをバイト数で返す関数です。

```c
//================================================================
/*! check data length can be read.

  @memberof UART_HANDLE
  @param  hndl          target UART_HANDLE
  @return int           result (bytes)
*/
int uart_bytes_available( const UART_HANDLE *hndl )
{
  uint16_t rx_wr = uart_get_wr_pos(hndl);

  if( hndl->rx_rd <= rx_wr ) {
    return rx_wr - hndl->rx_rd;
  }
  else {
    return hndl->rxfifo_size - hndl->rx_rd + rx_wr;
  }
}

//================================================================
/*! get the Rx FIFO write position.
*/
static inline int uart_get_wr_pos( const UART_HANDLE *hndl )
{
  return hndl->rxfifo_size - hndl->hal_uart->hdmarx->Instance->NDTR;
}
```

ポイントは、DMAのトランザクション数を保持している NDTR レジスタと読み込みポイント `hndl->rx_rd` との差をとることで、受信バッファに溜まっているデータバイト数を計算する箇所です。

### uart_read 関数

受信データを得る関数です。実際には、受信FIFOからデータをコピーする動作をします。

```c
//================================================================
/*! Receive binary data.

  @memberof UART_HANDLE
  @param  hndl          target UART_HANDLE
  @param  buffer        pointer to buffer.
  @param  size          Size of buffer.
  @return int           Num of received bytes.

  @note                 If no data received, it blocks execution.
*/
int uart_read( UART_HANDLE *hndl, void *buffer, int size )
{
  uint8_t *buf = buffer;
  int cnt = size;

  while( cnt > 0 ) {
    int ba = uart_bytes_available(hndl);
    if( ba == 0 ) {
      __NOP(); __NOP(); __NOP(); __NOP();
      continue;
    }

    if( ba < cnt ) {
      cnt -= ba;
    } else {
      ba = cnt;
      cnt = 0;
    }

    // copy fifo to buffer
    for( ; ba > 0; ba-- ) {
      *buf++ = hndl->rxfifo[hndl->rx_rd++];
      if( hndl->rx_rd >= hndl->rxfifo_size ) hndl->rx_rd = 0;
    }
  }

  return size;
}
```

### uart_write 関数

こちらは、ポーリングで出力するだけの関数になっています。

```c
//================================================================
/*! Send out binary data.

  @memberof UART_HANDLE
  @param  hndl          target UART_HANDLE
  @param  buffer        pointer to buffer.
  @param  size          Size of buffer.
  @return               Size of transmitted.
*/
int uart_write( UART_HANDLE *hndl, const void *buffer, int size )
{
  HAL_UART_Transmit( hndl->hal_uart, buffer, size, HAL_MAX_DELAY );

  return size;
}
```

書き込みも DMA を使う事によってバックグラウンド化できますが、バッファの解放タイミングとか、実際の出力中にさらにプログラムから write を呼ばれた場合はどうするかとか、考えることが急激に増えるので、簡単のためここではポーリングで済ませています。


## コンストラクタの実装

では、mruby/c のメソッド関数の実装にはいります。
ガイドラインから、コンストラクタの仕様を確認します。（一部略）

:::note info
コンストラクタ (new) の仕様

```ruby
UART.new( id=nil, *params )
```
  * id で示す物理ユニットを指定して、UART オブジェクトを生成する。

オプションパラメータ

| param | type | description |
|-|-|-|
| baudrate | Integer | ボーレート (default 9600) |
| baud | 同上 |  |
| stop_bits | Integer | ストップビット (default 1) |
| parity | Const | パリティービット (default NONE) |
| unit | --- | ユニットの指定（パラメータidと同じ） |

パラメータ用定数

```ruby
UART::NONE
UART::EVEN
UART::ODD
```

実行例

```ruby
# UART1を、全てデフォルトパラメータで使う。
uart1 = UART.new( 1 )
```
:::

ユニットは数字(1, 2, 6) で指定することにして、その他はガイドライン通りの仕様で作ることにします。[^1]

```c
//================================================================
/*! UART constructor

  uart1 = UART.new( id, *params )	# id = 1,2 or 6
*/
static void c_uart_new(mrbc_vm *vm, mrbc_value v[], int argc)
{
  MRBC_KW_ARG( unit );

  // get UART unit num.
  int unit_num = 1;		// default is UART1
  if( argc >= 1 && v[1].tt == MRBC_TT_INTEGER ) {
    unit_num = mrbc_integer(v[1]);
  }
  if( MRBC_KW_ISVALID(unit) ) {
    if( unit.tt != MRBC_TT_INTEGER ) goto ERROR_RETURN;
    unit_num = mrbc_integer(unit);
  }
  switch( unit_num ) {
  case 1:
  case 2:
  case 6:
    break;  // OK
  default:
    goto ERROR_RETURN;
  }

  // allocate instance with UART_HANDLE pointer.
  v[0] = mrbc_instance_new(vm, v[0].cls, sizeof(UART_HANDLE *));
  *(UART_HANDLE**)(v[0].instance->data) = TBL_UART_HANDLE[unit_num];

  // process other parameters
  c_uart_setmode( vm, v, argc );
  goto RETURN;


 ERROR_RETURN:
  mrbc_raise(vm, MRBC_CLASS(ArgumentError), "UART initialize.");

 RETURN:
  MRBC_KW_DELETE( unit );
}
```

引数の一つ目、もしくはキーワード引数 `unit` より、ユニット番号を取り出します。
通信パラメータ引数の確認とハードウェアへの設定は、`c_uart_setmode` 関数へ委譲します。

## 各メソッドの実装

各メソッド関数の実装は、先に作ったサブルーチン関数をコールするだけになるので、引数および戻り値に関して mruby/c のデータ構造と、C言語の関数との橋渡しを記述する事が主な処理となります。
こちらも主要ないくつかのメソッド関数の実装だけ説明します。


### setmode メソッドの実装

setmode メソッドは、キーワード引数を使ってパラメータの指定をする仕様なので、`MRBC_KW_ARG()` マクロを使ってキーワード引数を取り出すコードを書きます。実際にハードウェアのパラメータを変更する処理は、先ほど説明した `mrbc_setmode()` 関数に任せます。

```c
//================================================================
/*! set mode

  uart1.setmode( *params )
*/
static void c_uart_setmode(mrbc_vm *vm, mrbc_value v[], int argc)
{
  MRBC_KW_ARG( baudrate, baud, data_bits, stop_bits, parity, flow_control, txd_pin, rxd_pin, rts_pin, cts_pin );
  if( !MRBC_KW_END() ) goto RETURN;

  UART_HANDLE *hndl = *(UART_HANDLE **)(v[0].instance->data);
  int baud_rate = -1;

  if( MRBC_KW_ISVALID(baudrate) ) baud_rate = mrbc_integer(baudrate);
  if( MRBC_KW_ISVALID(baud) ) baud_rate = mrbc_integer(baud);
  if( MRBC_KW_ISVALID(data_bits) ) goto ERROR_NOT_IMPLEMENTED;
  if( !MRBC_KW_ISVALID(stop_bits) ) stop_bits = mrbc_integer_value(-1);
  if( !MRBC_KW_ISVALID(parity) ) parity = mrbc_integer_value(-1);
  if( MRBC_KW_ISVALID(flow_control) ) goto ERROR_NOT_IMPLEMENTED;
  if( MRBC_KW_ISVALID(txd_pin) ) goto ERROR_NOT_IMPLEMENTED;
  if( MRBC_KW_ISVALID(rxd_pin) ) goto ERROR_NOT_IMPLEMENTED;
  if( MRBC_KW_ISVALID(rts_pin) ) goto ERROR_NOT_IMPLEMENTED;
  if( MRBC_KW_ISVALID(cts_pin) ) goto ERROR_NOT_IMPLEMENTED;

  if( baud_rate > 0 && baud_rate < 2400 ) goto ERROR_ARGUMENT;

  // set to UART
  if( uart_setmode( hndl, baud_rate, mrbc_integer(parity), mrbc_integer(stop_bits) ) != 0 ) goto ERROR_ARGUMENT;
  goto RETURN;


 ERROR_NOT_IMPLEMENTED:
  mrbc_raise(vm, MRBC_CLASS(NotImplementedError), 0);
  goto RETURN;

 ERROR_ARGUMENT:
  mrbc_raise(vm, MRBC_CLASS(ArgumentError), 0);
  goto RETURN;

 RETURN:
  MRBC_KW_DELETE( baudrate, baud, data_bits, stop_bits, parity, flow_control, txd_pin, rxd_pin, rts_pin, cts_pin );
}
```


### read メソッドの実装

readメソッドは、引数で指定されたバイト数を読み込んで、Stringで返すという動作なので、メソッド関数では、戻り値 Stringを用意してそのバッファへ受信データをコピーするように実装します。

```c
//================================================================
/*! read

  s = uart1.read(n)

  @param  n		Number of bytes receive.
  @return String	Received data.
*/
static void c_uart_read(mrbc_vm *vm, mrbc_value v[], int argc)
{
  UART_HANDLE *hndl = *(UART_HANDLE **)(v[0].instance->data);

  if( v[1].tt != MRBC_TT_INTEGER ) {
    mrbc_raise(vm, MRBC_CLASS(ArgumentError), 0);
    return;
  }

  int read_bytes = mrbc_integer(v[1]);
  mrbc_value ret = mrbc_string_new(vm, 0, read_bytes);
  char *buf = mrbc_string_cstr(&ret);

  uart_read( hndl, buf, read_bytes );
  buf[read_bytes] = '\0';

  SET_RETURN(ret);
}
```

### write メソッドの実装

writeは、引数で与えられた文字列からCポインタを取り出して `uart_write()` 関数に渡す処理を書きます。

```c
//================================================================
/*! write

  uart1.write(s)

  @param  s	  Write data.
*/
static void c_uart_write(mrbc_vm *vm, mrbc_value v[], int argc)
{
  UART_HANDLE *hndl = *(UART_HANDLE **)(v[0].instance->data);

  if( v[1].tt != MRBC_TT_STRING ) {
    mrbc_raise(vm, MRBC_CLASS(ArgumentError), 0);
    return;
  }

  int n = uart_write( hndl, mrbc_string_cstr(&v[1]), mrbc_string_size(&v[1]));
  SET_INT_RETURN(n);
}
```


## その他のメソッド

送信はバッファを持たない（ポーリング）仕様としたので、送信バッファに関するメソッド、`flush`, `clear_tx_buffer` は、なにもしない関数となります。また、`bytes_to_write` メソッドは、常に0を返すようにします。
```c
static void c_uart_flush(mrbc_vm *vm, mrbc_value v[], int argc)
{
  // nothing to do.
}
static void c_uart_bytes_to_write(mrbc_vm *vm, mrbc_value v[], int argc)
{
  // always zero return because no write buffer.
  SET_INT_RETURN( 0 );
}
```

break 信号の送信は、HALに機能が存在しないようなので、NotImplementedError 例外を起こすようにしました。
```c
static void c_uart_send_break(mrbc_vm *vm, mrbc_value v[], int argc)
{
  mrbc_raise(vm, MRBC_CLASS(NotImplementedError), 0);
}
```

他のメソッドなどは、後述する github リポジトリを参照してください。

# テスト

秋月電子から販売されていたGPS受信キット「AE-GNSS-EXTANT」を接続してみます。[^2]

![STM32mrbc07-GPS_Module.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/0f19a71b-0a07-2799-bae5-65f96a9d995a.jpeg)

GPSモジュールの電源へは、5Vを供給します。I/O電圧は 3.3V なので、そのまま結線すればOKです。UART6を使うことにして、TxDとRxDはクロスして配線します。

![STM32mrbc07-GPS_Connect.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/f2b2b586-4feb-d1b6-bfe5-eaca97b269e8.jpeg)

## 測位結果（位置）を緯度経度で表示するプログラムの作成

GPSモジュールからは、標準で NMEA0183 フォーマットの測位データが、1秒ごとに送られてきます。特に何も追加設定はせず、受信データをパースして緯度経度の座標を表示するプログラムを作ってみます。
NMEA0813 は、検索すればいくらでもフォーマットの解説情報が得られます。 [^3]
今回は、最も簡易に位置が取得できる　GxRMC フォーマットのデータのみを使うようにプログラミングします。

```ruby:Core/mrubyc/task1.rb
uart6 = UART.new(6)

def split_ddmm( s )
  idx = s.index(".").to_i - 2
  raise "Format error" if idx <= 0

  s[idx,0] = " "
  return s
end

while true
  s = uart6.gets
  next if !s.start_with?("$GNRMC")

  rmc = s.split(",")
  next if rmc[2] != "A"

  # 日付(UTC)
  day = rmc[9][0,2].to_i
  mon = rmc[9][2,2].to_i
  year = rmc[9][4,2].to_i + 2000

  # 時刻(UTC)
  hour = rmc[1][0,2].to_i
  min = rmc[1][2,2].to_i
  sec = rmc[1][4,2].to_i

  # 緯度
  south_north = {"N"=>"+", "S"=>"-"}[rmc[4]]
  latitude = split_ddmm(rmc[3])

  # 経度
  east_west = {"E"=>"+", "W"=>"-"}[rmc[6]]
  longitude = split_ddmm(rmc[5])

  printf("%d/%2d/%2d %2d:%02d:%02d(UTC) Lat,Long: %s%s, %s%s\n",
         year, mon, day, hour, min, sec,
         south_north, latitude, east_west, longitude )
end
```

## 結果

GPSモジュールは、電源を入れてから衛星を捕まえて測位が完了するまで、しばらく時間がかかります。シリアルコンソール　(TeraTerm) を開いて表示結果を確認すると、うまく測位ができていれば、以下の通りの1秒に1行のペースで現在位置が表示されます。

![STM32mrbc07-task1_demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/c281d908-44a5-a697-b9d9-3ca7d5959e54.gif)

## メーカー提供のGUIプログラムによる動作確認

GPSチップのメーカーであるMediatekから、Windowsで動作するGPSモジュールの動作確認プログラム「Mini GPS」が提供されています。mruby/c を使って、UARTとコンソール、つまり GPSモジュールとUSBシリアルとをバイパスするプログラムを書いて、Mini GPS を動かしてみます。

```ruby:Core/mrubyc/task2.rb
uart2 = UART.new(2)
uart6 = UART.new(6)

while true
  n = uart2.bytes_available
  if n > 0
    uart6.write( uart2.read(n))
  end
  n = uart6.bytes_available
  if n > 0
    uart2.write( uart6.read(n))
  end
end
```

## 結果

以下の通り、測位結果と衛星情報をグラフィカルに表示させることができます。

![STM32mrbc07-SS_MiniGPS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/239356e9-f58a-1923-ae0d-df7c2ad729a3.png)


# おわりに

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

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

今回は、UARTクラスを実装しました。UARTは全2重通信で親子の区別がないので、受信にはリングバッファ (FIFO) を構成し、送受信タイミングに関してプログラムが簡単になるよう工夫することができました。
これで、ガイドラインに定義された全てのペリフェラルについての実装が終わりました。

次回は最終回、今回作った　UARTサブルーチン関数群を使って、バイトコード書き込みプログラムを実装します。


[^1]: flow_control, data_bits 及びピンの指定は、ハードウェアの制約によりサポートしません。

[^2]: 秋月電子さんのGPSモジュールは、2024年7月現在、ほとんど終売となっていますね。どうしたんでしょうか。

[^3]: 私が初めてGPSモジュールを（試験的にですが）使った時は、まだ NMEA0813 は規格を購入しないといけない代物でした。GPSモジュール自体も戦略物資にあたるといわれ入手が容易ではありませんでした。