しまねソフト研究開発センター(略称 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に名前を変えて、プロジェクトに組み込みます。今後ヘッダファイルも必要になるので、今の段階で作っておきます。
#ifndef STM32F4_GPIO_H
#define STM32F4_GPIO_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// empty yet.
#ifdef __cplusplus
}
#endif
#endif
#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 へ、ポートを記録する構造体を以下のように定義します。併せてこの後必要になる定数も定義しておきます。
#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()
関数に委譲する
ということを行っています。
/*! 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()
関数へ定義をを追加します。
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
に書いて、実験してみます。
pb3 = GPIO.new("PB3", GPIO::OUT) # PB3ピンを出力に設定する
while true
pb3.write( 1 )
sleep 1
pb3.write( 0 )
sleep 1
end
こんな感じで、点滅します。
その他の関数
ここまでできたら、後は残りのメソッドを頑張って書いていくだけです。
例えば、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クラスを実装します。