STM32のRTCの落とし穴

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時間制モードの設定である。
f:id:licheng:20181112170301p:plain

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; // 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;

  // ここで必ずRTCが初期化される
  hrtc.Instance = RTC;
  (中略)
  if (HAL_RTC_Init(&hrtc) != HAL_OK) 
  {
    _Error_Handler(__FILE__, __LINE__);
  }

  // 日時設定ずみでなければ設定する
  // (RTCバックアップレジスタにマジックナンバーがあるかで判断)
  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__);
    }
    // RTCバックアップレジスタにマジックナンバーを書き込み
    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;
  
  // 最低限のRTCの設定
  hrtc.Instance = RTC;
  (中略)
  hrtc.Lock = HAL_UNLOCKED;
  hrtc.State = HAL_RTC_STATE_READY;
  HAL_RTC_MspInit(&hrtc); // これを実行しないとRTCが動作しないようである

  // 日時設定ずみでなければ設定する
  if(HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0) != 0x32F2){
    
    // ここでRTCの完全初期化 (日時未設定のときのみ完全初期化する)
    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" // STM32F7xxの場合
    HAL_PWR_EnableBkUpAccess();
    LL_RCC_ForceBackupDomainReset();
    HAL_Delay(10);
    LL_RCC_ReleaseBackupDomainReset();
    HAL_PWR_EnableBkUpAccess();

バックアップドメインリセット後の再設定

バックアップドメインリセット後は、当然ながらRTCの再設定が必要である。また、注意すべきはRCCのBDCRレジスタもバックアップドメインに属しているので再設定が必要となる。これをしないとそもそもRTCにクロックが供給されず、動作しない。

    uint32_t bdcr = RCC->BDCR; // BDCRの値を退避
    
    // バックアップドメインのリセット
    HAL_PWR_EnableBkUpAccess();
    LL_RCC_ForceBackupDomainReset();
    HAL_Delay(10);
    LL_RCC_ReleaseBackupDomainReset();
    HAL_PWR_EnableBkUpAccess();
    
    RCC->BDCR = bdcr; // BDCRの値を復旧
    
    // RTCの再設定の例 (CubeMXが生成するMX_RTC_Init関数からコピペ) 
    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);