しまねソフト研究開発センター(略称 ITOC)にいます、東です。
mruby/cをSTマイクロエレクトロニクス社製の32bitマイコンSTM32で動かす記事、
今回はその第3回、ハードウェアタイマーを使うよう設定を追加します。
目標
ハードウェアタイマーを使って sleep の時間を正確にするとともに、sleep 中は CPUを止めて消費電力の削減をはかる。
ビルドシステムの調整
まず、前回 mruby/c のプログラムを書き換えるたびに、コマンドプロンプト画面で mrbc.exe を動かす必要があった作業を自動化します。以下の2種類の方法がありますが、どちらでもかまいません。
方法1 バッチファイルを使う
ITOCのサイトからダウンロードした mruby コンパイル済パッケージには、本家にはないバッチファイル mrbc.bat
も一緒に入っています。これを、プロジェクトフォルダの、Core/mrubyc フォルダへコピーします。
メニューから、Project > Properties を選び、ダイアログを開きます。
ダイアログ左ペインの C/C++ Build > Settings をクリックし、右ペインの Build Steps タブをクリックします。
Pre-build steps の Command: 欄へ、以下の通り入力します。
cd ..\\Core\\mrubyc; mrbc.bat
[Apply and Close] をクリックしてダイアログを閉じます。
この操作により、ビルド前に自動的に mrbc.exe を起動してコンパイルが行われます。
方法2 Makefile を使う
先の方法は、STM32CubeIDE 以外でも、Pre-build 設定さえあれば汎用的に使えますが、.rb ファイルを編集していない時でも必ず .c ファイルが更新されるという動作をします。
速度的には十分早いので問題ないですが、ちょっと不細工かと思う面もあります。
CubeIDE 内部では、gnu make が動いているので、これを利用すると .rb ファイルが更新された時だけ .c ファイルも更新するという動作をさせることもできます。
手順
左ペイン Project Explorer 上、mrubyc の上で右クリックし、New > File を選びます。
Create New File ダイアログで、File name 欄へ、makefile
と入力し、[Finish] をクリックし、ファイルを作ります。
以下の内容を入力します。
最終行の前空白(インデント)は、スペースではなく TABですので気をつけて入力します。
MRBC = cmd /C mrbc.exe
RBSRCS = $(wildcard *.rb)
CSRCS = $(RBSRCS:.rb=.c)
.PHONY : all
all: $(CSRCS)
%.c : %.rb
$(MRBC) -B$(@:.c=) $^
メニューから、Project > Properties を選び、ダイアログを開きます。
ダイアログ左ペインの C/C++ Build > Settings をクリックし、右ペインの Build Steps タブをクリックします。
Pre-build steps の Command: 欄へ、以下の通り入力します。
cd ..\\Core\\mrubyc; make
これでビルド時に、.rb が更新されているときだけ、.c ファイルも更新するようになりました。
ハードウェアタイマーを使う
前回は、sleep の時間が指定時間よりもだいぶ長い時間 sleep するようでした。これは、スケジューラが正確に計時する方法が無いからです。これをハードウェアタイマを使って解決します。
以下の手順で行います。
- 前回行った タイマー未使用の宣言を取り下げる
- SysTick タイマーで、mrbc_tick() をコールする
- hal.h へ割り込み許可/禁止を定義
今回ターゲットにしているマイコン STM32F401 は、ARM Cortex-M4 CPUコアを搭載しており、CPUコア自体にSysTickという簡単なハードウェアタイマーを内蔵しています。
ST製 HAL ライブラリの仕様を調査すると、SysTickタイマーを使って 1ms サイクルで割り込みをかける用途に使っており、割り込みハンドラはユーザにも開放されています。別途ペリファラルのタイマーを使用する方法もありますが、有限のリソースをスケジューラが使用してしまうのはもったいないので、ここでは HALライブラリの SysTick タイマーに同居する方法を採用しました。
1. 前回行った タイマー未使用の宣言を取り下げる
メニューから、Project > Properties を選び、ダイアログを開きます。
ダイアログ左ペインの C/C++ General > Paths and Symbols をクリックし、右ペインの Symbols タブをクリックします。
Languages が GNU C になっていることを確認し、前回追加したシンボル MRBC_NO_TIMER
をクリックして選択状態にしてから、[Delete] ボタンをクリックします。
2. SysTick タイマーで、mrbc_tick() をコールする
左ペイン Project Explorer の画面から、Core > Src とたどり、stm32f4xx_it.c をダブルクリックして開きます。
右ペインに表示された C言語ソースコードから、/* USER CODE BEGIN SysTick_IRQn 0 */
の箇所を探し、その下に以下のコードを追記します。
/* USER CODE BEGIN SysTick_IRQn 0 */
void mrbc_tick(void);
mrbc_tick();
/* USER CODE END SysTick_IRQn 0 */
ここは、SysTick_Handler関数の中で、1msごとに割り込み処理によってコールされます。mruby/c スケジューラがタイマー割り込みによって必要としている処理は、mrbc_tick() をコールすることだけです。
3. hal.h へ割り込み許可/禁止を定義
前回は、割り込みを使用しない前提でしたので、ほとんどのマクロを無効化していました。今回は必要な箇所をきちんと記述します。
左ペイン Project Explorer の画面から、Core > mrubyc_src とたどり、hal.h をダブルクリックして開きます。
ファイルの内容を、以下の通り書き換えます。
#ifndef MRBC_SRC_HAL_H_
#define MRBC_SRC_HAL_H_
#include "main.h"
#define MRBC_TICK_UNIT 1
#define MRBC_TIMESLICE_TICK_COUNT 10
#define hal_init() ((void)0)
#define hal_enable_irq() __enable_irq()
#define hal_disable_irq() __disable_irq()
#define hal_idle_cpu() ((void)0)
int hal_write(int fd, const void *buf, int nbytes);
int hal_flush(int fd);
void hal_abort(const char *s);
#endif
変更点は、hal_enable_irq と、hal_disable_irq の実体を記述した事と、それに伴って必要になるヘッダの include、および、NO_TIMER用に仮に設定していた hal_idle_cpu を無効化した点です。
割り込みの enable/disable は、TickTimer の割り込みだけを有効化/無効化でも良いですが、より汎用的に(簡易に)使う事ができる方法を選定しています。
一通り終わりましたら、ビルドしてターゲットへ書き込みます。
ほぼ正確な秒数で、sleep するようになります。
(以下のデモは、Rubyのプログラムは、1秒ごとの sleep に戻しています)
while true
led_write( 1 )
sleep 1
led_write( 0 )
sleep 1
end
(注意)
まれに、書き込みが終わっても正しくユーザプログラムが開始しないことがありました。原因は不明ですが、ターゲットボード上の黒い RESET スイッチを押すと、正しく動き始めます。
プログラムの sleep 時には CPU も sleep させる
この Ruby プログラムでは、ほとんど sleep の待ち時間が占めており、その間 CPU は何もしていないことになります。このような場合、使用電力の削減を狙って、マイコンを低消費電力モードにするのが良案です。
Cortex-M4 にも低消費電力モードがあります。マニュアルによると、いくつかのモードが用意されていますが、
- SysTick タイマーでウェイクアップが必要
- 周辺デバイスの動作維持
- メモリ(RAM)内容の維持
といった条件を満たす必要があり、それを満たすモードに入るには、以下のようにすれば良いことが分かります。
HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI)
この関数を、hal.h の hal_idle_cpu()
マクロに設定します。
#ifndef MRBC_SRC_HAL_H_
#define MRBC_SRC_HAL_H_
#include "main.h"
#define MRBC_TICK_UNIT 1
#define MRBC_TIMESLICE_TICK_COUNT 10
#define hal_init() ((void)0)
#define hal_enable_irq() __enable_irq()
#define hal_disable_irq() __disable_irq()
#define hal_idle_cpu() HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI)
int hal_write(int fd, const void *buf, int nbytes);
int hal_flush(int fd);
void hal_abort(const char *s);
#endif
これで、ビルドしてターゲットへ書き込みます。
書き込みが終わったタイミング(=実行が始まったタイミング)で、CubeIDE がターゲットに接続できないと文句を言いますが、これは CPUがきちんんとスリープモードに入っている証拠ですので問題ありません。
このボードは、CPUに流れる電流を、JP6に電流計を接続することによって測定することができますので、シャント抵抗とオシロスコープを使って観測してみます。
スリープモードにしない場合 | スリープモードにする場合 |
---|---|
約15mA | 約6mA |
測定結果の値が高い部分はLEDの電流を含んでいるので、低い部分を比較すると、約15mAから6mAと、半分以下の電流に低減できていることがわかります。
おまけ - オシレータを内蔵から外部へ変更
ここからは蛇足ですので、設定してもしなくても良いです。
STM32CubeIDE で自動生成したコードは、デフォルトのクロックソースに内蔵RCオシレータを使うよう設定されます。メーカーによると内蔵RCオシレータは、工場出荷時に25℃の条件下にて±1%の誤差内に調整されています。しかしながら、AN5067 で述べられるとおり、実装条件や周囲温度によって影響をうけ、正確さはそれほど期待できません。
Nucleo-F401RE ボードの回路図を見ると、ST-Link プログラマ側にあるマイコンチップは、8MHz の水晶がクロックソースになっており、さらにその出力は、メイン側の STM32F401RE に接続されているようです。こういった使い方は、恐らく保証対象外だとは思いますが、メーカー製のボードですし、試しに使用する設定すると確かに動きます。
方法
CubeIDEの Clock Configration 画面を開きます。
クロックソースを HSI
から HSE
に変更し、以下の通り各プリスケーラやPLLの逓倍率を変更します。
たぶん、HCLK
欄へ84と入力すると、よしなに設定してくれると思います。
効果
効果はてきめんです。
SysTick実測値(kHz) | 誤差 (%) | |
---|---|---|
内蔵 RC | 1.0176 | 1.76 |
外部 クリスタル | 1.0000 | 0.00 |
測定は、いま手元に周波数カウンタが無いので、オシロスコープ (キーサイト DSOX2002A)で簡易的に測定しました。ですが、時間(およびその逆数の周波数)は、身近な物理量のなかで最も有効桁数を確保しやすい物理量なので、信じても間違いないでしょう。
おわりに
ファイル全体は、github リポジトリにありますので、そちらをご覧ください。
今回は、ハードウェアタイマーを使うことを目標に、mruby/c の hal を整備しました。
次回は halの残り(コンソール関係)の整備をしていきます。