【チュートリアル】mruby/cをSTM32マイコンで動かす Chapter03: ハードウェアタイマーの使用

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

[mruby/cをSTマイクロエレクトロニクス社製の32bitマイコンSTM32で動かす記事](https://qiita.com/HirohitoHigashi/items/7ef01d074baa27a3b2e7)、
今回はその第3回、ハードウェアタイマーを使うよう設定を追加します。


# 目標
ハードウェアタイマーを使って sleep の時間を正確にするとともに、sleep 中は　CPUを止めて消費電力の削減をはかる。


# ビルドシステムの調整

まず、前回　mruby/c のプログラムを書き換えるたびに、コマンドプロンプト画面で mrbc.exe を動かす必要があった作業を自動化します。以下の2種類の方法がありますが、どちらでもかまいません。

## 方法1　バッチファイルを使う

ITOCのサイトからダウンロードした mruby コンパイル済パッケージには、本家にはないバッチファイル `mrbc.bat` も一緒に入っています。これを、プロジェクトフォルダの、Core/mrubyc フォルダへコピーします。
![STM32Tuto03-スクリーンショット 2024-05-23 15.03.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/b99cc619-c3f7-307e-db42-221173900905.png)

メニューから、Project > Properties を選び、ダイアログを開きます。

ダイアログ左ペインの C/C++ Build > Settings をクリックし、右ペインの Build Steps タブをクリックします。

Pre-build steps の Command: 欄へ、以下の通り入力します。
```
cd ..\\Core\\mrubyc; mrbc.bat
```
![STM32Tuto03-スクリーンショット 2024-05-23 15.09.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/13b9a890-97a6-8ad0-86ce-282370e79252.png)

[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] をクリックし、ファイルを作ります。

![STM32Tuto03-スクリーンショット 2024-05-23 15.41.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/8b4660b7-f754-168a-cce7-843809044621.png)

以下の内容を入力します。
最終行の前空白（インデント）は、スペースではなく TABですので気をつけて入力します。
```makefile:Core/mrubyc/makefile
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
```

![STM32Tuto03-スクリーンショット 2024-05-23 15.46.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/f6ee6468-784a-95e6-f7bb-f4f8a3280ae6.png)

これでビルド時に、.rb が更新されているときだけ、.c ファイルも更新するようになりました。


# ハードウェアタイマーを使う

前回は、sleep の時間が指定時間よりもだいぶ長い時間 sleep するようでした。これは、スケジューラが正確に計時する方法が無いからです。これをハードウェアタイマを使って解決します。

以下の手順で行います。

1. 前回行った タイマー未使用の宣言を取り下げる
2. SysTick タイマーで、mrbc_tick() をコールする
3. 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] ボタンをクリックします。
![STM32Tuto03-スクリーンショット 2024-05-23 16.25.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/6b0a4c5a-15ad-e921-8d4c-3eca5c099eee.png)

## 2. SysTick タイマーで、mrbc_tick() をコールする

左ペイン Project Explorer の画面から、Core > Src とたどり、stm32f4xx_it.c をダブルクリックして開きます。
![STM32Tuto03-スクリーンショット 2024-05-23 16.27.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/c882e6f3-d26a-5a05-04e5-2a40fe045a35.png)

右ペインに表示された C言語ソースコードから、`/* USER CODE BEGIN SysTick_IRQn 0 */` の箇所を探し、その下に以下のコードを追記します。

```c:Core/Src/stm32f4xx_it.c
  /* 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 をダブルクリックして開きます。

ファイルの内容を、以下の通り書き換えます。

```c: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 に戻しています）
```ruby:Core/mrubyc/task1.rb
while true
  led_write( 1 )
  sleep 1
  led_write( 0 )
  sleep 1
end
```

![STM32Tuto03-Demo1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/728957c0-59c1-1039-e334-26904a53e3e8.gif)

---
**（注意）**
まれに、書き込みが終わっても正しくユーザプログラムが開始しないことがありました。原因は不明ですが、ターゲットボード上の黒い RESET スイッチを押すと、正しく動き始めます。


# プログラムの sleep 時には CPU も sleep させる

この Ruby プログラムでは、ほとんど　sleep の待ち時間が占めており、その間 CPU は何もしていないことになります。このような場合、使用電力の削減を狙って、マイコンを低消費電力モードにするのが良案です。
Cortex-M4 にも低消費電力モードがあります。マニュアルによると、いくつかのモードが用意されていますが、

  * SysTick タイマーでウェイクアップが必要
  * 周辺デバイスの動作維持
  * メモリ(RAM)内容の維持

といった条件を満たす必要があり、それを満たすモードに入るには、以下のようにすれば良いことが分かります。

```c
HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI)
```

この関数を、hal.h の `hal_idle_cpu()` マクロに設定します。

```c: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()    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に電流計を接続することによって測定することができますので、シャント抵抗とオシロスコープを使って観測してみます。

|スリープモードにしない場合|スリープモードにする場合|
|--|--|
|![scope_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/371f63ab-026a-c2ac-21c1-faec9b2921d3.png)|![scope_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/98b13d95-8123-899e-d6d9-3c1c177472d4.png)|
|約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と入力すると、よしなに設定してくれると思います。
![STM32Tuto03-スクリーンショット 2024-05-24 17.59.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/6f5a3bbb-c401-a2a6-a2d1-66877bf73f05.png)

ちなみに、初期値（内蔵RC使用時）は以下の通り。
![STM32Tuto03-スクリーンショット 2024-05-24 18.08.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/56b48f2c-5d1f-af0d-6415-06cab77e7f58.png)

## 効果

効果はてきめんです。

|  | SysTick実測値(kHz) | 誤差 (%) |
|:-|:-:|:-:|
| 内蔵 RC | 1.0176 | 1.76 |
| 外部 クリスタル | 1.0000 | 0.00 |


測定は、いま手元に周波数カウンタが無いので、オシロスコープ (キーサイト DSOX2002A）で簡易的に測定しました。ですが、時間（およびその逆数の周波数）は、身近な物理量のなかで最も有効桁数を確保しやすい物理量なので、信じても間違いないでしょう。


# おわりに

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

https://github.com/HirohitoHigashi/mrubyc-tuto-stm32/tree/chapter03

今回は、ハードウェアタイマーを使うことを目標に、mruby/c の hal を整備しました。
次回は halの残り（コンソール関係）の整備をしていきます。