しまねソフト研究開発センター(略称 ITOC)にいます、東です。
この記事は、mrubyファミリ (組み込み向け軽量Ruby) Advent Calendar 2024 の2日目に投稿しています。
前回 に続き、mruby/c を、STマイクロエレクトロニクス社製のマイコンボード「Nucleo-L476RG」へ移植を行ったので、その顛末を記事にします。
目標
- mruby, mruby/c共通ペリフェラルインターフェースガイドラインに従った I/O クラスライブラリを使えるようにする
- バイトコードのみを書き込む方法(この記事 の方法1)を実現させる
準備するもの
STM32マイコン評価ボード Nucleo-L476RG
(https://www.st.com/ja/evaluation-tools/nucleo-l476rg.html)
開発環境 STM32CubeIDE (ver 1.15.1)
(https://www.st.com/ja/development-tools/stm32cubeide.html)
作業手順
最初からフルスペックで移植を行うのは、かなりハードルが高いので、やはりある程度ステップバイステップで行うのが良いと思います。いくつかの方法を試した結果、以下の方法がベストプラクティスではないかと思います。
- 新規にプロジェクトを作る
- Nucleo-F401RE用のソースコードを持ってきてマージし、最低限の構成で、まずはビルドを通す
- I/O クラスライブラリを、ひとつひとつ移植する
- フラッシュメモリへのバイトコード書き込み機能を移植する
余談ですが、先日STマイクロの方より CubeIDEによるマイコンの変更方法には、まず新規プロジェクトを作ってから、そこへ既存プロジェクトをインポートする方法があることを教えて頂きました。実際にやってみたのですが、あたりまえですが非互換部分の修正はそれなりに必要ですし、結果的に今回の用途では、ステップバイステップで手作業によるマージが安全かつ納得感があるという結論に達しました。
1. 新規にプロジェクトを作る
では実際の作業に入ります。
別記事「mruby/cをSTM32マイコンで動かす Chapter01: 環境構築」 に書いた方法と同様の手順でプロジェクトを作ります。
Board Selector をクリックしてボードセレクタタブに変更し、Commercial Part Number 欄に、"L476" と入力すれば NUCLEO-L476RG が検索されますので、ボードリスト欄で選べばOKです。
プロジェクト名は、任意でかまいません。今回私は、"mrubyc-nucleo-L476RG" とつけました。
Initialize all peripherals whth their default Mode? ダイアログは、Yes をクリックします。
デフォルトデバイスの確認
プロジェクトが作成されましたら、デフォルトでマップされた以下のデバイスを確認します。
- オンボードLEDのポート番号 = PA5 (前回F401REでも、PA5)
- オンボードスイッチのポート番号 = PC13 (前回F401REでも、PC13)
- USBシリアルにマップされたUART番号 = UART2 (前回F401REでも、UART2)
幸いなことに、今回のボード L476RG は、チュートリアルで使ったボード F401RE と同じポート番号、UART番号でした。
2. Nucleo-F401RE用のソースコードを持ってきてマージし、最低限の構成で、まずはビルドを通す
必要最低限のソースコードのマージ
- こちら (https://github.com/HirohitoHigashi/mrubyc-nucleo-F401RE/tree/chapter08_mrbwrite) より、ソースコードを zip でダウンロードし、展開します。
- 展開したファイルから、Core/mrubyc_src ディレクトリを、新プロジェクトの同じ場所にコピーします。
- 新プロジェクトに、Core/mrubyc ディレクトリを作ります。
- 展開したファイルの、mrubyc ディレクトリからファイル makefile, mrbc.exe, start_mrubyc.c を、新プロジェクトの Core/mrubyc ディレクトリへコピーします。
自動生成されたソースコードへ追記
自動生成されたファイル2つへ、以下の通り追記します。
/* USER CODE BEGIN 2 */
void start_mrubyc(void);
start_mrubyc();
/* USER CODE END 2 */
/* USER CODE BEGIN SysTick_IRQn 0 */
void mrbc_tick(void);
mrbc_tick();
/* USER CODE END SysTick_IRQn 0 */
ビルドプロパティーの変更
- メニューから、Project > Properties を選び、ダイアログを開きます。
- ダイアログ左ペインの C/C++ Build > Settings をクリックします。
- 右ペインの Configuration: を、All configurations に変更します。
- 右ペインの Tool Settings タブをクリックし、MCU Settings をクリックします。
- Use float with printf from newlib-nano 欄のチェックをつけます。
- 右ペインの Build Steps タブをクリックします。
- Pre-build steps の Command: 欄へ、以下の通り入力します。
cd ..\\Core\\mrubyc; make
start_mrubyc.c ファイルの変更
とにかく最初はビルドを通すことを目的として、ビルドエラーになるところをコメントアウトします。
- stm32f4_uart.h, mrbc_firm.h の #include
// #include "stm32f4_uart.h"
// #include "mrbc_firm.h"
- check_boot_mode の中を #if 0 で無効化
int check_boot_mode( void )
{
const int MAX_WAIT_CYCLE = 256;
int ret = 0;
#if 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 );
#endif
return ret;
}
- uart_init()
// uart_init();
- 各クラスの初期化
// 各クラスの初期化
// void mrbc_init_class_gpio(void);
// mrbc_init_class_gpio();
// void mrbc_init_class_uart(void);
// mrbc_init_class_uart();
// void mrbc_init_class_adc(void);
// mrbc_init_class_adc();
// void mrbc_init_class_pwm(void);
// mrbc_init_class_pwm();
// void mrbc_init_class_i2c(void);
// mrbc_init_class_i2c();
// void mrbc_init_class_spi(void);
// mrbc_init_class_spi();
- タスクの登録のところは、 #if でバイトコード書き込み機能サポート(前半)と、prepared bytecode(後半)が分けてあるので、#if 0 にして後半を有効化
// タスクの登録
#if 0
void *task = 0;
...
ビルド!
ビルドしてエラー無く通るかを確認します。
なお、最初の1回目は、task1.rb から task1.c を生成するタイミングの問題により必ず失敗しますが、もう一度ビルドすると成功します。
3. I/O クラスライブラリを、ひとつひとつ移植する
ビルドが通ったら、I/Oクラスライブラリの移植を行います。
GPIOクラス
- ダウンロードしたファイルの、mrubyc ディレクトリからファイル stm32f4_gpio.h, stm32f4_gpio.c を、新プロジェクトの Core/mrubyc ディレクトリへコピーします。
- 先ほどコメントアウトした start_mrubyc.c ファイルの GPIO クラスの初期化呼び出しを有効にします。
// 各クラスの初期化
void mrbc_init_class_gpio(void);
mrbc_init_class_gpio();
ビルドすると成功し、エルチカなども正しく動作します。
UARTクラス
まず、UARTユニットが何個使えそうか、データシートや CubeIDE のコンフィグ画面を使って調べます。
以前は、UART1, UART2, UART6の 3つを有効化していました。L476RGでは、UART, UART2 は、同様に使えそうですが、UART6は存在しませんでした。よって今回は、UART1とUART2だけ使えるようにします。もし不足するようでしたら、UART3,4,5があるので、それを有効にした版を作ることもできると思います。
ピンアサインは、以下の通りです。
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 |
CubeIDE を使って、UART1および2の設定をします。
Project Explorer から、(プロジェクト名).ioc をダブルクリックし、コンフィグレーション画面を開き、Connectivity > USART1 をクリックします。
USART1 Mode and Configuration 画面
- Mode欄を Asynchronous に変更
- Parameter Settings タブ > Basic Parameters > Baud Rate を、9600 Bits/s に変更
- DMA Settingsタブをクリック
- [Add] ボタンをクリック
- Select欄が表示されるので、USART1_RXに変更
- Mode を、Circularに変更
同様に、UART2も設定をします。
設定を保存することで、コードの自動生成が始まります。
次にダウンロードしたファイルの適用を行います。
- ダウンロードしたファイルの、mrubyc ディレクトリからファイル stm32f4_uart.h, stm32f4_uart.c を、新プロジェクトの Core/mrubyc ディレクトリへコピーします。
- start_mrubyc.c ファイルからコメントアウトした UART 関連部分を有効にします。
#include "stm32f4_uart.h"
...
uart_init();
...
void mrbc_init_class_uart(void);
mrbc_init_class_uart();
- UART6が無いので、コピーした stm32f4_uart.c から、UART6に関連する部分を削除もしくはコメントアウトします。
//extern UART_HandleTypeDef huart6;
...
// UART6
// &(UART_HANDLE){
// .unit_num = 6,
// .delimiter = '\n',
// .hal_uart = &huart6,
...
// case 6:
- HALライブラリで定義してある構造体メンバ名 NDTR が CNDTR に変わっているので、こちらも変更します。
static inline int uart_get_wr_pos( const UART_HANDLE *hndl )
{
return hndl->rxfifo_size - hndl->hal_uart->hdmarx->Instance->CNDTR;
}
以上で終了です。
ADCクラス
このチップはADCユニットを3つ (ADC1, ADC2, ADC3) 持っていますが、ボードのA0〜A5シルクで示されたピンアサインから、ADC1をマルチプレクサで切り替えて使う事が想定されているようです。
ピン割り当て
ボード上のシルク印刷(=ADピン番号)と、GPIOチャネル、A/Dチャネルの対応は、以下の通りになっています。
CN | pin | SILK | GPIO | ADC | Channel |
---|---|---|---|---|---|
CN8 | 1 | A0 | PA0 | 1 | ADC_CHANNEL_5 |
CN8 | 2 | A1 | PA1 | 1 | ADC_CHANNEL_6 |
CN8 | 3 | A2 | PA4 | 1 | ADC_CHANNEL_9 |
CN8 | 4 | A3 | PB0 | 1 | ADC_CHANNEL_15 |
CN8 | 5 | A4 | PC1 | 1 | ADC_CHANNEL_2 |
CN8 | 6 | A5 | PC0 | 1 | ADC_CHANNEL_1 |
お手本にしている F401REとは Channel が異なっていますので、その対応をします。
CubeIDE を使って、ADC1の設定をします。
- ADC1をクリックして選び、上記表の各チャンネルモードプルダウンで、
Single-ended
を選びます。
設定を保存することで、コードの自動生成が始まります。
次にダウンロードしたファイルの適用を行います。
- ダウンロードしたファイルの、mrubyc ディレクトリからファイル stm32f4_adc.c を、新プロジェクトの Core/mrubyc ディレクトリへコピーします
- 以下の通り、ADC_CHANNEL_* を変更します
} const TBL_ADC_CHANNELS[] = {
// GPIO ADC ch. silk
{{1, 0}, ADC_CHANNEL_5}, // PA0 5 A0
{{1, 1}, ADC_CHANNEL_6}, // PA1 6 A1
{{1, 4}, ADC_CHANNEL_9}, // PA4 9 A2
{{2, 0}, ADC_CHANNEL_15}, // PB0 15 A3
{{3, 1}, ADC_CHANNEL_2}, // PC1 2 A4
{{3, 0}, ADC_CHANNEL_1}, // PC0 1 A5
};
- start_mrubyc.c ファイルからコメントアウトした ADC 関連部分を有効にします。
void mrbc_init_class_adc(void);
mrbc_init_class_adc();
ここでビルドをしてみますが、いくつかビルドエラーが発生しました。
../Core/mrubyc/stm32f4_adc.c:99:21: error: 'ADC_SAMPLETIME_3CYCLES' undeclared (first use in this function); did you mean 'ADC_SAMPLETIME_2CYCLE_5'?
99 | .SamplingTime = ADC_SAMPLETIME_3CYCLES,
| ^~~~~~~~~~~~~~~~~~~~~~
| ADC_SAMPLETIME_2CYCLE_5
どうやら UART の時と同じく、ADC もHALライブラリの仕様がチップ間で多少違うようです。ここはマルチプレクサを変更したいだけなのですが、ADC_ChannelConfTypeDef 構造体をきちんと作らないと駄目のようです。幸い、Core/Src/main.c の MX_ADC1_Init() 初期化関数の中に、初期パラメータの設定があったのでそれをそのまま使います。
static uint32_t read_sub(mrbc_vm *vm, mrbc_value v[], int argc)
{
int idx = *((int *)(v[0].instance->data));
ADC_ChannelConfTypeDef sConfig = {
.Channel = TBL_ADC_CHANNELS[idx].channel,
.Rank = ADC_REGULAR_RANK_1,
.SamplingTime = ADC_SAMPLETIME_2CYCLES_5,
.SingleDiff = ADC_SINGLE_ENDED,
.OffsetNumber = ADC_OFFSET_NONE,
.Offset = 0,
};
これでビルドは、通ります。
が、テストしてみると、どうもADCの値が正しく表示されません。
CubeMX が生成した L476 のコードと、F401 のコードを注意深く見比べてみると、GPIO のアナログモード設定に使う定数が、GPIO_MODE_ANALOG
ではなく、 GPIO_MODE_ANALOG_ADC_CONTROL
でなければならないようです。命名から類推するに、L476では単にアナログにする場合と ADC で使うためにアナログにする場合とでハード的に分けてあるため、ソフトウェアもそれに従う必要があるようですね。
理由がわかったので、そちらも変更します。GPIO_MODE_ANALOG
を設定しているのは、stm32f4_gpio.c です。
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_ADC_CONTROL;
これでビルドしてテストすると、今度は正しく電圧値が取得できました。
PWMクラス
お手本にしている F401RE で用意した PWM チャネル(タイマー番号とチャネル)は、以下の通りです。
CN | pin | SILK | GPIO | Usage | (PWM) |
---|---|---|---|---|---|
CN5 | 5 | MISO/D12 | PA6 | TIM3_CH1 | |
CN5 | 4 | PWM/MOSI/D11 | PA7 | TIM3_CH2 | |
CN5 | 3 | PWM/CS/D10 | PB6 | TIM4_CH1 | |
CN5 | 2 | PWM/D9 | PC7 | TIM3_CH2 | |
CN9 | 8 | D7 | PA8 | TIM1_CH1 | |
CN9 | 7 | PWM/D6 | PB10 | TIM2_CH3 | |
CN9 | 6 | PMW/D5 | PB4 | TIM3_CH1 | |
CN9 | 5 | D4 | PB5 | TIM3_CH2 | |
CN8 | 1 | A0 | PA0 | AD0 | TIM2_CH1 |
CN8 | 2 | A1 | PA1 | AD1 | TIM2_CH2 |
CN8 | 4 | A3 | PB0 | AD8 | TIM3_CH3 |
一つ一つタイマー番号とチャネルが、L475REではどうか調べます。幸いなことにすべて合致しています。
CubeIDEを使って、タイマーの設定をします。
- Timersの中から、TIM1,2,3,4 の各チャンネルプルダウンを、 以下の通り
PWM Generation No Output
に設定します。
TIM | Channel |
---|---|
TIM1 | 1 をPWM Generation No Outputに設定 |
TIM2 | 1 2 3 をPWM Generation No Outputに設定 |
TIM3 | 1 2 3 をPWM Generation No Outputに設定 |
TIM4 | 1 をPWM Generation No Outputに設定 |
設定を保存することで、コードの自動生成が始まります。
次にダウンロードしたファイルの適用を行います。
- ダウンロードしたファイルの、mrubyc ディレクトリからファイル stm32f4_pwm.c を、新プロジェクトの Core/mrubyc ディレクトリへコピーします。
- start_mrubyc.c ファイルからコメントアウトした PWM 関連部分を有効にします。
void mrbc_init_class_pwm(void);
mrbc_init_class_pwm();
以上で終了です。
I2Cクラス
I2Cは、シルクにあるSCL/D15, SDA/D14を使います。これも手本にしているF401REと同じ割り当てになっています。
CN | pin | SILK | GPIO | Usage |
---|---|---|---|---|
CN5 | 10 | SCL/D15 | PB8 | I2C1_SCL |
CN5 | 9 | SDA/D14 | PB9 | I2C1_SDA |
- Connectivity > I2C1 をクリックします
- I2C 欄を、I2C に変更します
デフォルトでは、PB6, PB7 にピンアサインされるので、それぞれ PB8, PB9 に変更します。
設定を保存することで、コードの自動生成が始まります。
次にダウンロードしたファイルの適用を行います。
- ダウンロードしたファイルの、mrubyc ディレクトリからファイル stm32f4_i2c.c を、新プロジェクトの Core/mrubyc ディレクトリへコピーします。
- start_mrubyc.c ファイルからコメントアウトした I2C 関連部分を有効にします。
void mrbc_init_class_i2c(void);
mrbc_init_class_i2c();
以上で終了です。
SPIクラス
SPIは、ボード上にシルク SCK,MISO,MOSI の表示がありますが、オンボードデバイスのLEDとピンアサインが被っています。よって、シルク印刷は無視することにして、以下の通り、手本にした F401REと同じアサインで移植することにします。
CN | pin | SILK | GPIO | Usage |
---|---|---|---|---|
CN7 | 1 | PC10 | SPI3_SCK | |
CN7 | 2 | PC11 | SPI3_MISO | |
CN7 | 3 | PC12 | SPI3_MOSI |
- Connectivity > SPI3 をクリックします
- Mode 欄を、Full-Duples Master に変更します
- Data Size を 8 Bits に変更します
- Prescaler を 128 に変更します
設定を保存することで、コードの自動生成が始まります。
次にダウンロードしたファイルの適用を行います。
- ダウンロードしたファイルの、mrubyc ディレクトリからファイル stm32f4_spi.c を、新プロジェクトの Core/mrubyc ディレクトリへコピーします。
- start_mrubyc.c ファイルからコメントアウトした SPI 関連部分を有効にします。
void mrbc_init_class_spi(void);
mrbc_init_class_spi();
以上で終了です。
ここまでで CubeIDE で設定したピンアサインのスクリーンショットです。
4. フラッシュメモリへのバイトコード書き込み機能を移植する
最後は、バイトコード書き込み機能の移植です。
リファレンスマニュアル(RM0351) を確認すると、今回ターゲットとしている L476RGと前回の F401RE とは、フラッシュメモリバンクの考え方が違います。
F401REはセクターという単位でメモリが管理されていましたが、L476RGはより一般的と思われる 2KBごとのページで管理されています。
HALライブラリでは、消去はページ単位で行う事ができます。
書き込み単位は、8バイトです。
MPU | 管理単位 | 消去 | 書き込み |
---|---|---|---|
F401RE | セクタ (16〜128KB) | セクタ単位 | 1,2,4 バイトずつ |
L476RG | ページ (2KB) | ページ単位 | 8バイトずつ |
これらを元に、以下のとおり設計しました。
- サイズは、F401REと同じ 128KBとする
- アドレス範囲は、0x80E0000 - 80FFFFF
- ページでいうと、448 〜 551 の 64ページ
リンカファイルの編集
バイトコード用のアドレス範囲をリンカが誤って使用しないように、予約します。
CubeIDEの左ペイン ProjectExplorer から、STM32L476RGTX_FLASH.ld をダブルクリックして開きます
メモリ定義の行を探し、以下の通り書き換えて保存します
/* Memories definition */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 96K
RAM2 (xrw) : ORIGIN = 0x10000000, LENGTH = 32K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 896K
IREP (r) : ORIGIN = 0x80E0000, LENGTH = 128K
}
バイトコード受信・書き込みプログラムの移植
次にダウンロードしたファイルの適用を行います。
ダウンロードしたファイルの、mrubyc ディレクトリからファイル mrbc_firm.h mrbc_firm.h を、新プロジェクトの Core/mrubyc ディレクトリへコピーします。
mrbc_firm.c を、上記の違いを反映して変更します。
アドレス範囲
const uint32_t IREP_START_ADDR = 0x080E0000; // 128KB
const uint32_t IREP_END_ADDR = 0x080FFFFF; // (see: cmd_clear function)
clearコマンド
static int cmd_clear(void)
{
HAL_FLASH_Unlock();
FLASH_EraseInitTypeDef erase = {
.TypeErase = FLASH_TYPEERASE_PAGES,
.Banks = FLASH_BANK_2,
.Page = 448,
.NbPages = 64,
};
...
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 & 7); // align 8 byte.
irep_write_end = irep_write_addr_ + size;
while( irep_write_addr_ < irep_write_end ) {
uint64_t data = 0;
for( int i = 7; i >= 0; i-- ) {
data <<= 8;
data |= p[i];
}
HAL_StatusTypeDef sts;
sts = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, irep_write_addr_, data);
if( sts != HAL_OK ) {
STRM_PUTS("-ERR Flash write error.\r\n");
HAL_FLASH_Lock();
return -1;
}
p += 8;
irep_write_addr_ += 8;
}
HAL_FLASH_Lock();
STRM_PUTS("+DONE\r\n");
return 0;
}
showprogコマンド
...
addr += size + (-size & 7); // align 8 byte.
...
pickup_task関数
...
addr += size + (-size & 7); // align 8 byte.
...
start_mrubyc.c ファイルを元に戻す
最初にコメントアウトした部分を全て元に戻します。面倒なら、もう一度ダウンロードしたファイルを使って上書きしても良いです。
以上で移植作業はすべて終了です。
おわりに
ファイル全体は、github リポジトリにありますので、そちらをご覧ください。
今回は、Nucleo-F401RE 用に移植した mruby/c の VM、ペリフェラルクラスライブラリ、バイトコード書き込み機能を、Nucleo-L476RG へ移植しました。
同じ ST 社製のマイコンですが、FシリーズからLシリーズへと、シリーズを超えての移植だったため、いくつか引っかかるポイントがありました。
HAL ライブラリを使っていれば、移植作業は簡単になるといううたい文句ですが、アンドキュメンテッドなことも多く、CubeMX で生成されたコードを読み解きながらの作業も必要でした。やはり HALでプロセッサの変更は怖くないといった理想には、なかなか届かないですね。
私のコードにも、何カ所かもう少し工夫をすれば移植が簡単になるポイントが発見できました。たとえば、バイトコード書き込み単位に由来するアドレスアラインが、今はマジックナンバーで数カ所に書いてありますが、マクロ化するなどして変更を1カ所にまとめる方が良さそうです。
色々なマイコンに移植するとこのような改良点が見つかって良いですが、あまりやり過ぎると抽象的になりすぎてしまい、かえって読みづらくなったり、パフォーマンスに問題が発生したりと、バランスが難しいところです。