STM32のRTCを使ってみてハマった落とし穴などについて書く。
- 環境は、STM32F767ZI (Nucleo-F767ZI) + HALドライバ + CubeMX + IAR EWARM
- 以下では単にSTM32と書くが、STM32F767ZIでしか試していない。
- やや不可解な内容も含むので誤りがあればご指摘ください。
異常な時刻になるバグ
STM32のRTCは、12時間制モードで23時などのありえない時刻を設定すると、バグって異常な動作をはじめるようだ。具体的には、23:59:59のあと日付が変わらずに24:00:00になってしまい、その後も日付が変わらないままにありえない時刻が進んでいく。
下図は時刻が24:00:02になった状態のRTCのレジスタ値である。CR=0x00000040は12時間制モードの設定である。
HAL_RTC_SetTime関数の注意点
HALドライバのHAL_RTC_SetTime関数はRTCの時刻を設定するAPIだが、気を付けないと前述の異常な時刻になるバグを発生させてしまう。
RTC_TimeTypeDef time;
time.Hours = hour;
time.Minutes = min;
time.Seconds = sec;
HAL_RTC_SetTime(&hrtc, &time, FORMAT_BIN);
これで一見問題なさそうだが、じつはRTC_TimeTypeDef構造体は、時・分・秒以外にもメンバ変数がある。これらが不定値であるとRTCのレジスタに意図しない値が設定され、場合によっては前述のバグを引き起こすのだ。あまり使わない設定項目だがちゃんと設定してやる。
RTC_TimeTypeDef time;
time.TimeFormat = RTC_HOURFORMAT_24;
time.DayLightSaving = RTC_DAYLIGHTSAVING_NONE ;
time.StoreOperation = RTC_STOREOPERATION_RESET;
memset(&time, 0x00, sizeof(time));
time.Hours = hour;
time.Minutes = min;
time.Seconds = sec;
HAL_RTC_SetTime(&hrtc, &time, FORMAT_BIN);
RTC関係のレジスタはバックアップドメインと呼ばれ、主電源とは別のバッテリー(ないしコンデンサ)によってバックアップされ、主電源OFF時も内容が保持される。また、バックアップドメインには32バイトの自由に使えるRTCバックアップレジスタ(RTC_BKPxR)があり、不揮発なデータやフラグの保存場所としてアプリケーションから利用できる。
HAL_RTC_Init関数の注意点
HALドライバのHAL_RTC_Init関数はRTCを初期化するAPIだが、初期化の際に秒以下の時刻がリセットされる。つまり平均0.5秒時刻が遅れることになり、起動のたびにHAL_RTC_Init関数を実行すれば100回で約50秒時刻が遅れる。これを回避するには、起動時に時計が設定済みであれば自分で最低限の設定だけしてHAL_RTC_Init関数は呼ばないようにする。HALドライバのコードを修正することもできるが、保守性を著しく損なうので避けることにした。最低限の設定の内容については後述する。
※ ほんとうにそんなイケてないライブラリなのだろうか? ぼくの使い方が悪いのだろうか?
CubeMXが生成する初期化コードの問題点
CubeMXが生成する初期化コードではRTCの初期化はMX_RTC_Init関数で行われる。
static void MX_RTC_Init(void)
{
RTC_TimeTypeDef sTime;
RTC_DateTypeDef sDate;
hrtc.Instance = RTC;
(中略)
if (HAL_RTC_Init(&hrtc) != HAL_OK)
{
_Error_Handler(__FILE__, __LINE__);
}
if(HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0) != 0x32F2){
(中略)
if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK)
{
_Error_Handler(__FILE__, __LINE__);
}
(中略)
if (HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN) != HAL_OK)
{
_Error_Handler(__FILE__, __LINE__);
}
HAL_RTCEx_BKUPWrite(&hrtc,RTC_BKP_DR0,0x32F2);
}
}
つまり、時計が設定済みであろうがなかろうが、起動時に必ずHAL_RTC_Init関数が呼ばれてしまい、そのたびに時刻が平均0.5秒遅れる。このコードは本来は手で編集すべきものではないが、不都合なので修正する。ただし、CubeMXでコードを生成し直すたびに修正内容が消されてしまうので、必ずバージョン管理ツールを使用して慎重に再編集を行う。修正内容については後述する。
※ ほんとうにそんなイケてないライブラリなのだろうか? ぼくの使い方が悪いのだろうか?
最低限の設定
起動時に時刻未設定の場合のみHAL_RTC_Init関数を呼び、時刻設定ずみであればhrtc構造体に設定と状態を与え、HAL_RTC_MspInit関数を呼べばよい。具体的には前述のMX_RTC_Init関数を以下のように修正する。
static void MX_RTC_Init(void)
{
RTC_TimeTypeDef sTime;
RTC_DateTypeDef sDate;
hrtc.Instance = RTC;
(中略)
hrtc.Lock = HAL_UNLOCKED;
hrtc.State = HAL_RTC_STATE_READY;
HAL_RTC_MspInit(&hrtc);
if(HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0) != 0x32F2){
if (HAL_RTC_Init(&hrtc) != HAL_OK)
{
_Error_Handler(__FILE__, __LINE__);
}
(中略)
if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK)
{
_Error_Handler(__FILE__, __LINE__);
}
(中略)
if (HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN) != HAL_OK)
{
_Error_Handler(__FILE__, __LINE__);
}
HAL_RTCEx_BKUPWrite(&hrtc,RTC_BKP_DR0,0x32F2);
}
}
※ ほんとうにそんなイケてないライブラリなのだろうか? ぼくの使い方が悪いのだろうか?
バックアップドメインのリセット
前述の異常な時刻になるバグのようにRTCが異常な状態に陥った場合、その状態は主電源をOFFしてもバッテリーによって保持される。バックアップドメインをリセットするには下記のようなコードを実行する。なお、このコードではHALドライバだけでなく一部LLドライバのAPIを使用している。適宜LLドライバのソースをプロジェクトに取り入れ、ヘッダをインクルードすること。
#include "stm32f7xx_ll_rcc.h"
HAL_PWR_EnableBkUpAccess();
LL_RCC_ForceBackupDomainReset();
HAL_Delay(10);
LL_RCC_ReleaseBackupDomainReset();
HAL_PWR_EnableBkUpAccess();
バックアップドメインリセット後の再設定
バックアップドメインリセット後は、当然ながらRTCの再設定が必要である。また、注意すべきはRCCのBDCRレジスタもバックアップドメインに属しているので再設定が必要となる。これをしないとそもそもRTCにクロックが供給されず、動作しない。
uint32_t bdcr = RCC->BDCR;
HAL_PWR_EnableBkUpAccess();
LL_RCC_ForceBackupDomainReset();
HAL_Delay(10);
LL_RCC_ReleaseBackupDomainReset();
HAL_PWR_EnableBkUpAccess();
RCC->BDCR = bdcr;
hrtc.Instance = RTC;
hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
hrtc.Init.AsynchPrediv = 127;
hrtc.Init.SynchPrediv = 255;
hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
HAL_RTC_Init(&hrtc);