1
0

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

Last updated at Posted at 2024-06-28

しまねソフト研究開発センター(略称 ITOC)にいます、東です。

mruby/cペリフェラルライブラリのSTM32マイコンへの実装の記事、今回はその第2回、GPIOクラスを実装します。

目標

GPIOクラスのAPIガイドライン に従って、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 でのクラスの最低限の雛形です。

#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()関数の直後で呼び出せば良いです。

  mrbc_init(memory_pool, MRBC_MEMORY_SIZE);
  mrbc_init_class_myclass();

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

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
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 の出力をするように記されています。

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

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

Cヘッダへの構造体定義

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

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()関数に委譲する

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

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() 関数へ定義をを追加します。

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の内容を確認してセットする処理を書きます。

/*! 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 定数が、単純な数値ではないので、テーブル引きをして変換する作業をしています。

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構造体を使うには、ポインタへキャストする
/*! 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 に書いて、実験してみます。

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

その他の関数

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

/*! 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 リポジトリにありますので、そちらをご覧ください。

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

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0