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

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

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

# 目標

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

# 今回の方針

* ピンの指定は、STM32マイコンのポートを文字列で指定する。（例： "PA0"）
* 入出力などの設定は、あらかじめ行うわけには行かないので、このクラスで行う
* C言語のみで実装する

実装は、Rubyでインターフェースを、C言語で低レイヤーをと、使い分ける方法が実は圧倒的に容易に実装が可能です。しかし、本記事では、C言語だけでも実装が可能で、その具体的な方法を示す意味でも、C言語だけで実装してみます。


# HALライブラリ調査

メーカー製 HAL リファレンスマニュアル (UM1725) や、CubeMXでのコード自動生成により、STM32のGPIO設定方法を調査します。その結果、mruby/c の GPIOクラスの実装には、以下の項目を設定するようコードを書けば実現できそうだと言うことが分かりました。

```
  /*Configure GPIO pin : PB5 */
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  GPIO_InitStruct.Pin = GPIO_PIN_5;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
						 GPIO_MODE_INPUT	 Input Floating Mode
						 GPIO_MODE_OUTPUT_PP Output Push Pull Mode
						 GPIO_MODE_OUTPUT_OD Output Open Drain Mode
						 GPIO_MODE_AF_PP	 Alternate Function Push Pull Mode
						 GPIO_MODE_AF_OD	 Alternate Function Open Drain Mode
						 GPIO_MODE_ANALOG	 Analog Mode
			
  GPIO_InitStruct.Pull = GPIO_NOPULL;
						 GPIO_NOPULL		No Pull-up or Pull-down activation
						 GPIO_PULLUP		Pull-up activation
						 GPIO_PULLDOWN		Pull-down activation

  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
```

ポート番号が、`GPIOB` や、`GPIO_PIN_5` など、全て define された値で表現されているので、定義は単純な数値であったとしても、それを使うようプログラミングをしなければなりません。
一般的なユーザプログラムなら、決め打ちでコードが書けるのでそれほど問題にはならなさそうですが、今回のような抽象度が少し上がったライブラリを書く場合は、変換テーブルを作るなどしなければならず、少し面倒ですね。


# 作業手順

全ての手順を説明すると膨大な量になるため、ポイントを絞って説明します。

## 雛形の作成

まずは雛形をつくり、コンパイルだけ通るようにします。
以下が、mruby/c でのクラスの最低限の雛形です。

```ruby
#include "mrubyc.h"

/*! read　メソッドの実装部
*/
static void c_myclass_read(mrbc_vm *vm, mrbc_value v[], int argc)
{
  mrbc_print("MyClass#read was called.\n");
}

/*! write　メソッドの実装部
*/
static void c_myclass_write(mrbc_vm *vm, mrbc_value v[], int argc)
{
  mrbc_print("MyClass#write was called.\n");
}

/*! set up the my class.
*/
void mrbc_init_class_myclass( void )
{
  mrbc_class *cls = mrbc_define_class(0, "MyClass", 0);

  // define methods
  mrbc_define_method(0, cls, "read", c_myclass_read);
  mrbc_define_method(0, cls, "write", c_myclass_write);

  // define constants
  mrbc_set_class_const(cls, mrbc_str_to_symid("CONST1"), &mrbc_integer_value(1));
}
```

雛形では、

 * クラス `MyClass` の定義
 * メソッド `read`及び`write`の2つを定義
 * クラス定数 `CONST1` の定義

を行い、それを `mrbc_init_class_myclass()` 関数にまとめています。これを適切な位置、たとえば、mrbc_init()関数の直後で呼び出せば良いです。

```ruby
  mrbc_init(memory_pool, MRBC_MEMORY_SIZE);
  mrbc_init_class_myclass();
```

これを、GPIOに名前を変えて、プロジェクトに組み込みます。今後ヘッダファイルも必要になるので、今の段階で作っておきます。

```c:Core/mrubyc/stm32f4_gpio.h
#ifndef STM32F4_GPIO_H
#define STM32F4_GPIO_H

#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

// empty yet.

#ifdef __cplusplus
}
#endif
#endif
```

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

/*! read　メソッドの実装部
*/
static void c_gpio_read(mrbc_vm *vm, mrbc_value v[], int argc)
{
  mrbc_print("GPIO#read was called.\n");
}

/*! write　メソッドの実装部
*/
static void c_gpio_write(mrbc_vm *vm, mrbc_value v[], int argc)
{
  mrbc_print("GPIO#write was called.\n");
}

/*! set up the GPIO class.
*/
void mrbc_init_class_gpio( void )
{
  mrbc_class *cls = mrbc_define_class(0, "GPIO", 0);

  // define methods
  mrbc_define_method(0, cls, "read", c_gpio_read);
  mrbc_define_method(0, cls, "write", c_gpio_write);

  // define constants
  mrbc_set_class_const(cls, mrbc_str_to_symid("CONST1"), &mrbc_integer_value(1));
}
```

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

GPIOクラスの仕様を見ると、インスタンスを生成し、それを使って High/Low の出力をするように記されています。

```ruby
pb3 = GPIO.new("PB3", GPIO::OUT)  # PB3ピンを出力に設定する
pb3.write( 1 )                    # PB3に1(High) を出力する
```

これを実現するためには、各インスタンスが、自分が管理するポート（例では、"PB3"）を覚えておく必要があります。mruby/cでは、これを以下の方法を使って実現します。

### Cヘッダへの構造体定義

先ほど作った stm32f4_gpio.h へ、ポートを記録する構造体を以下のように定義します。併せてこの後必要になる定数も定義しておきます。

```c:Core/mrubyc/stm32f4_gpio.h
#ifndef STM32F4_GPIO_H
#define STM32F4_GPIO_H

#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

/*!
  physical pin describer.
*/
typedef struct PIN_HANDLE {
  uint8_t port;         // A=1,B=2,..,G=7
  uint8_t num;          // 0..15
} PIN_HANDLE;

#define GPIO_IN                 0x01
#define GPIO_OUT                0x02
#define GPIO_ANALOG             0x04
#define GPIO_HIGH_Z             0x08
#define GPIO_PULL_UP            0x10
#define GPIO_PULL_DOWN          0x20
#define GPIO_OPEN_DRAIN         0x40

#ifdef __cplusplus
}
#endif
#endif
```

### newメソッドの定義と実装

定義した構造体 `PIN_HANDLE` を含めてインスタンスを生成するために、newメソッドを定義してデフォルトの動作をオーバライドします。
newメソッドでは、
  * mrbc_instance_new() 関数で、インスタンス用のメモリを確保する
  * 併せて PIN_HANDLE のサイズを確保するよう、指示している
  * 確保した PIN_HANDLE の利用は、PIN_HANDLEへのポインタとして利用する
  * 引数をパースしエラーがないか確認しながら、実際の処理は、`gpio_set_pin_handle()`関数と、`gpio_setmode()`関数に委譲する

ということを行っています。

```c:Core/mrubyc/stm32f4_gpio.c
/*! constructor
*/
static void c_gpio_new(mrbc_vm *vm, mrbc_value v[], int argc)
{
  v[0] = mrbc_instance_new(vm, v[0].cls, sizeof(PIN_HANDLE));

  PIN_HANDLE *pin = (PIN_HANDLE *)v[0].instance->data;

  if( argc != 2 ) goto ERROR_RETURN;
  if( gpio_set_pin_handle( pin, &v[1] ) != 0 ) goto ERROR_RETURN;
  if( (mrbc_integer(v[2]) & (GPIO_IN|GPIO_OUT|GPIO_HIGH_Z)) == 0 ) goto ERROR_RETURN;
  if( gpio_setmode( pin, mrbc_integer(v[2]) ) < 0 ) goto ERROR_RETURN;
  return;

 ERROR_RETURN:
  mrbc_raise(vm, MRBC_CLASS(ArgumentError), "GPIO initialize");
}

void mrbc_init_class_gpio( void )
{
  mrbc_class *cls = mrbc_define_class(0, "GPIO", 0);

  // define methods
  mrbc_define_method(0, cls, "new", c_gpio_new);
  mrbc_define_method(0, cls, "read", c_gpio_read);
  mrbc_define_method(0, cls, "write", c_gpio_write);
  (略）
}
```

### クラス定数の定義

`GPIO::OUT` などのクラス定数を定義するために、`mrbc_init_class_gpio()` 関数へ定義をを追加します。

```c:Core/mrubyc/stm32f4_gpio.c
void mrbc_init_class_gpio( void )
{
  （略）
  
  // define constants
  mrbc_set_class_const(cls, mrbc_str_to_symid("IN"),         &mrbc_integer_value(GPIO_IN));
  mrbc_set_class_const(cls, mrbc_str_to_symid("OUT"),        &mrbc_integer_value(GPIO_OUT));
  mrbc_set_class_const(cls, mrbc_str_to_symid("HIGH_Z"),     &mrbc_integer_value(GPIO_HIGH_Z));
  mrbc_set_class_const(cls, mrbc_str_to_symid("PULL_UP"),    &mrbc_integer_value(GPIO_PULL_UP));
  mrbc_set_class_const(cls, mrbc_str_to_symid("PULL_DOWN"),  &mrbc_integer_value(GPIO_PULL_DOWN));
  mrbc_set_class_const(cls, mrbc_str_to_symid("OPEN_DRAIN"), &mrbc_integer_value(GPIO_OPEN_DRAIN));
}
```

### 下請け関数

set_pin_handle() 関数は、引数`pin`で指示された PIN_HANDLE構造体へ、`val`の内容を確認してセットする処理を書きます。

```c
/*! PIN handle setter

  @param  pin		dist.
  @param  val		src.
  @retval 0		No error.
*/
int gpio_set_pin_handle( PIN_HANDLE *pin, const mrbc_value *val )
{
  if( val->tt != MRBC_TT_STRING ) goto ERROR_RETURN;
  const char *s = mrbc_string_cstr(val);

  // in case of "PA0"
  if( s[0] == 'P' && ('A' <= s[1] && s[1] <= 'Z') ) {
    pin->port = s[1] - 'A' + 1;
    pin->num = mrbc_atoi( s+2, 10 );
    if( pin->num > 15 ) goto ERROR_RETURN;
    return 0;
  }

 ERROR_RETURN:
  pin->port = 0;
  pin->num = 0;

  return -1;
}
```

gpio_setmode() 関数は、実際にピンを指示された状態へ設定する関数です。
ここで、`GPIO_PIN_*` や `GPIOx` 定数が、単純な数値ではないので、テーブル引きをして変換する作業をしています。

```c
static uint16_t const TBL_NUM_TO_STM32PIN[/* num */] = {
  GPIO_PIN_0,  GPIO_PIN_1,  GPIO_PIN_2,  GPIO_PIN_3,
  GPIO_PIN_4,  GPIO_PIN_5,  GPIO_PIN_6,  GPIO_PIN_7,
  GPIO_PIN_8,  GPIO_PIN_9,  GPIO_PIN_10, GPIO_PIN_11,
  GPIO_PIN_12, GPIO_PIN_13, GPIO_PIN_14, GPIO_PIN_15 };

static GPIO_TypeDef * const TBL_PORT_TO_STM32GPIO[/* port */] = {
  0, GPIOA, GPIOB, GPIOC, GPIOD, GPIOE, 0, 0, GPIOH,
};

/*! set (change) mode

  @param  pin	target pin.
  @param  mode	mode. Sepcified by GPIO_* constant.
  @return int	zero is no error.
*/
int gpio_setmode( const PIN_HANDLE *pin, unsigned int mode )
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  GPIO_InitStruct.Pin = TBL_NUM_TO_STM32PIN[pin->num];

  if( mode & (GPIO_IN|GPIO_OUT|GPIO_ANALOG|GPIO_HIGH_Z|GPIO_OPEN_DRAIN) ) {
    if( mode & GPIO_ANALOG ) {
      GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;

    } else if( mode & GPIO_IN ) {
      GPIO_InitStruct.Mode = GPIO_MODE_INPUT;

    } else if( mode & GPIO_OUT ) {
      GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;

    } else if( mode & GPIO_OPEN_DRAIN ) {
      GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;

    } else {
      return -1;
    }

    GPIO_InitStruct.Pull = GPIO_NOPULL;
  }

  if( mode & GPIO_PULL_UP ) {
    GPIO_InitStruct.Pull = GPIO_PULLUP;
  }
  if( mode & GPIO_PULL_DOWN ) {
    GPIO_InitStruct.Pull = GPIO_PULLDOWN;
  }
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;

  HAL_GPIO_Init( TBL_PORT_TO_STM32GPIO[pin->port], &GPIO_InitStruct);
  return 0;
}
```

## writeメソッドの実装

writeメソッドを実装します。writeは引数を一つとり、0ならLowレベルを、1ならHighレベルを出力する仕様です。
ポイントは、以下です。
  * インスタンス (self) は、v[0] に入っている
  * インスタンスの PIN_HANDLE構造体を使うには、ポインタへキャストする

```ruby
/*! write

  gpio1.write( 0 or 1 )
*/
static void c_gpio_write(mrbc_vm *vm, mrbc_value v[], int argc)
{
  PIN_HANDLE *pin = (PIN_HANDLE *)v[0].instance->data;

  if( v[1].tt != MRBC_TT_INTEGER ) return;

  int val = mrbc_integer(v[1]);
  if( 0 <= val && val <= 1 ) {
    HAL_GPIO_WritePin( TBL_PORT_TO_STM32GPIO[pin->port],
                       TBL_NUM_TO_STM32PIN[pin->num], val );
  } else {
    mrbc_raise(vm, MRBC_CLASS(RangeError), 0);
  }
}
```

ところで、HAL_GPIO_WritePin() 関数の第3引数は、マニュアルでは、`GPIO_PIN_SET`, `GPIO_PIN_RESET` を与えるのが正式ですが、さすがにこれはやり過ぎのように感じます。マニュアルでは、enumであること、RESET, SET の順で書いてあること、定義を見ても単純に0と1を定義（列挙）してあることなどから、このプログラムでは引数の整数値 1 or 0 をそのまま与えるように書いても問題ないと判断しました。

## テスト

これで、エルチカができるようになりました。
`task1.rb` に書いて、実験してみます。

```ruby:Core/mrubyc/task1.rb
pb3 = GPIO.new("PB3", GPIO::OUT)  # PB3ピンを出力に設定する

while true
  pb3.write( 1 )
  sleep 1
  pb3.write( 0 )
  sleep 1
end
```

こんな感じで、点滅します。

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


## その他の関数

ここまでできたら、後は残りのメソッドを頑張って書いていくだけです。
例えば、readは以下のようになります。

```ruby
/*! read

  x = gpio1.read() -> Integer
*/
static void c_gpio_read(mrbc_vm *vm, mrbc_value v[], int argc)
{
  PIN_HANDLE *pin = (PIN_HANDLE *)v[0].instance->data;

  SET_INT_RETURN( HAL_GPIO_ReadPin( TBL_PORT_TO_STM32GPIO[pin->port],
                                    TBL_NUM_TO_STM32PIN[pin->num] ));
}
```

ここでも、マニュアルによれば HAL_GPIO_ReadPin の戻り値は GPIO_PinState 列挙体であるとのことですが、先ほどと同じ理由により、そのままメソッドの戻り値としています。

また、GPIOクラスは、クラスメソッドが定義されています。一方、mruby/c の現在の実装では、クラスメソッドとインスタンスメソッドを区別していません。よって、クラスメソッドも上記と同じ要領で記述、定義します。


# おわりに

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

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

今回は、GPIOクラスを実装しました。マイコン界の hello, world! である、ElChika も書けるようになりました。

次回は、ADCクラスを実装します。