公開日:2022/10/01
title.png

22nd Season


#22-9 RTCを設計する(プロローグ)

 さて今回は、FPGAボード(Sipeed Lichee Tang Primer)でRTCを作ります。

RTCとは?

 RTC(Real Time Clock)とは、直訳すると「実時間時計」のことです。人間が普段よく目にしている、「午後1時23分45秒」みたいな時計のことを指しています。「2022年9月23日(金)」の様なカレンダ機能もセットでくっついている場合が多いです。

 実は、コンピュータではあまり実時間時計は使いません。使い難いから……。
 だって、秒は60までカウントしたら0秒に戻って、代わりに分が+1カウントアップされます。60でカウントアップだから、2進数が得意なコンピュータにとって10進数でさえ、ありません。
 そんな「桁上げ(キャリ)」が秒→分→時→日→月→年と、駆け上がって行くことになります。日カウンタからの桁上げは悲惨で、月によって最終日が異なる始末です。
 時計を刻む🔪だけでも面倒臭い。
 まして、「いついつから、いついつまで、何秒?」なんて知りたくなると、更に面倒臭いから……。今度は引き算で桁借り(ボロウ)が駆け上がることになります。

 Arduinoでも良く使うmillis()関数やmicros()関数等を使って、適切な基準時からの経過時間時計の方がずっと使い易かったりするのです。

 一方、ログに日付を残したり、LCDに時計を表示したりする時には、経過時間時計では訳が判らないので、人間が目にして理解しやすい実時間時計が必要となります。そんな時に活躍するのが、RTCという訳です。
 従ってRTCは、人間が見て判り易いように、数値を文字列に変換して使うことが多いです。バイナリ数値を文字列に変換するには、10のべき乗で割り算しながら、各桁の数値を知って行く必要があります。それが秒、分、時、日、月、年と沢山あると、それなりに手間と時間のかかる作業となります。
 そこで、RTCは数値を文字列に変換し易いように、BCD(Binary Coded Decimal)という形式でI/Fする場合が多いようです。

BCD(Binary Coded Decimal)

 例えば、「36分」をBCDで表現すると、「0x36分」となります。
 16進数の0x36は、10進数では53の事だけど、BCDの0x36は、10進数の53ではなくて36を表しています。
 16進数で使われるa〜fまでのアルファベットは使わずに、0〜9までの数字だけを使って、16進数表現をそのまま10進数として読むと元の数字になるのがBCDです。

 もう少し算数っぽく表現してみると:

  10進数値をΣai×10i=an×10n+……+a0×100と置く(0≦ai≦9の整数)。

  対応するBCD値=Σai×16i=an×16n+……+a0×160である。

 各桁の数字an〜a0はそのままで、基底が10 → 16に変わったもの、です。

 C言語など高級言語を使って、「printf ("%02d:%02d:%02d",hour,minute,second);」で、終わらせてしまう人にとっては、意味が判らないかもしれないけど、アセンブラでバイナリ数値→文字列変換することを考えたら、BCD数値→文字列変換がどんなに容易か解るはずです。

うるう年

 日カウンタから月カウンタへ桁上がりが発生する条件は、日カウンタ+1した時に、月の最終日を超えたかどうか?で決まります。月の最終日は「西向く士」に加えて、2月に関しては、うるう年かどうかの判定が必要です。

  うるう年の定義は:
    400で割り切れる年、または、
    4で割り切れて、100で割り切れない年

  言い換えると:
    4で割り切れる年は、基本的には、うるう年。
      ただし、その中で、100で割り切れる年は、うるう年から除外する。
        ただし、その中で、400で割り切れる年は、うるう年。

 定義通りに計算しようとすると、「割り切れる」の条件判定のために除算×3回が必要です。これをハードウェアで実装するのは大変面倒です。なにしろ、除算はステップ除算になって複数サイクル掛かってしまうし(順序回路)、論理規模も大きくなります。

 「#22-8 天気時計を作るよ(アト)」では、うるう年判定をアセンブラ(ソフトウェア)で工夫して簡略化してみたのですが、それでも除算×1回を避けられませんでした(詳細はこちら)。

ハードウェア化するための検討

 ハードウェア化するために、もう少し踏み込んだ制限を設けて、簡略化した方が良い様です。

 定義より、うるう年の周期は400年だと解ります。まず合理的な制限として、BCDによる年カウンタを0x000〜0x399の400年間に限定します。
 読み出した年カウンタ(3桁BCD値)に、0x2000を加算すると0x2000年〜0x2399年までカウントできて、必要十分です。仮に、もっと先の西暦年としたい場合には、0x2400を足せば良いだけです。これで0x2400年〜0x2799年までカウントできます。
 加算する2000年や2400年、つまり基準年が、400の倍数になっていることが条件です。

 実際にうるう年かどうかを判定してみましょう。
 西暦年のバイナリ値Ybin(=0〜399)が判れば、4で割った余りが0の年は「うるう年の可能性がある」となります。幸い、4は2冪数です。2冪数の除算および剰余算は、シフト演算とAND演算で置き換えることができます。

  除算: A÷2N=A>>N ……(シフト演算)
  剰余算: A%2N=A&(2N−1) ……(AND演算)

 つまり、Ybin&(4−1)が0ならば、「うるう年の可能性がある」となります。
 後は、うるう年から除外しなくてはならない、「100で割り切れて、400で割り切れない年」の例外処理が必要となります。0〜399 → 400年間の中で、4で割り切れる年が、うるう年ではなくなるのは100年、200年、300年の3回だけなので、うるう年は以下式で判定できます。

  leap=(Ybin&(4−1)==0) && (Ybin!=100 && Ybin!=200 && Ybin!=300)

 あらら、除算要らなくなった。良かった良かった(1^−^)。

BCD値 → バイナリ値変換

 RTCの年カウンタは、こちらで議論したように、BCD表現されています。前節のうるう年判定を実行するためには、BCDで表現された年カウンタYbcdを、バイナリ値Ybinに換算しないとなりません。

 BCDの各桁を、Ybcd2〜Ybcd0と表現すると、BCD表現のYbcdからバイナリ表現のYbin変換は以下式となります。

  Ybin=(Ybcd2×10+Ybcd1)×10+Ybcd0

 10での乗算が2回と加算が2回となりました。乗算と言っても定数10なので、シフト演算と加算に帰着できます(参考:定数乗算)。

  A×10=A×0b1010=A×8+A×2=(A<<3)+(A<<1)

※10以外の定数でも、同様にシフト演算と加算で計算できます。ステップ乗算回路の応用なので、こちら「#16-2 Moscoviumで四則演算する」の記事を最初から読んでいただいた方が、理解を深められるかもしれません。

論理規模を確認する

 バイナリ値変換を含む、うるう年判定のRTLをVerilog-HDLで記述すると、以下となりました。

  // year: bcd to binary conversion
  wire [8:0] bin_yea2={ 5'h0,bcd_yea[11:8] };
  wire [8:0] bin_yea1={ 5'h0,bcd_yea[7:4] };
  wire [8:0] bin_yea0={ 5'h0,bcd_yea[3:0] };
  wire [8:0] bin_yea3=
      (bin_yea2[8:0]<<3) + (bin_yea2[8:0]<<1) + bin_yea1[8:0];
  wire [8:0] bin_yea=
      (bin_yea3[8:0]<<3) + (bin_yea3[8:0]<<1) + bin_yea0[8:0];
  // leap year
  wire leap_psb=( bin_yea[1:0]==2'h0 );
  wire leap_exc=(
      bin_yea[8:0]==9'd100 ||
      bin_yea[8:0]==9'd200 ||
      bin_yea[8:0]==9'd300);
  assign rsub_leap=( leap_psb && !leap_exc );


 論理規模としては:

 と、ステップ除算回路と順序回路で構成することを考えたら、ずいぶんと小さくできたはずです。順序回路も不要で、ただの組み合わせ論理となりました。
 スピードの方も、ここは32kHzドメインの回路なので32kHzクロック1周期(約30.5μs)で収束すれば良く、STAで確認するまでもなく余裕でメットするでしょう。

RTCに求められる機能

 まず第一に、実時間に合わせて、MCUがどのような状態であっても時を刻み続けること。これが一番大事です。
 例えば、MCUのリセット中でも、他のモードに切り替わっている間も、最悪、電源が遮断されていても、バックアップバッテリがRTCに供給される限り、時を刻み続けることが要求されます。だって、MCUがリセット中だからって、現実世界の時間は、止まって待ってはくれないでしょう?

 ここから派生するのが低消費電力(電流)であること、です。
 USBなどからの主電源が遮断された時、バックアップバッテリ(電池など)からの給電で長時間動作できる必要があり、そのためにはRTC自体の消費電流量が小さいことが求められます。

消費電流の削減

 消費電流量を削減する方法の1つとして、クロック周波数を落とすことが挙げられます。
 これは、32kHzクロックを外部から供給してもらうことで、解決します。ただ、外部で32k発振クロックを用意するのが面倒な場合でも使用可能な様に、内部24MHzクロックを分周して32kHzクロックを生成しておこうと思います。
 内部クロックと外部クロックが選択可能な仕様とします。

 もう1つの方法は、面積を小さくすることです。
 現在のLSIに広く使われているCMOSロジック回路は、信号が動く時だけ電流が流れます。どこに流れるか?と言うと、信号配線に付いた既成容量(コンデンサ)に充放電する時に流れます。
 面積が大きくなると、平均配線長が長くなり、既成容量値も大きくなるため消費電流が大きくなってしまいます。
 面積を小さくすることは、論理規模を縮小することと等価なので、なるべく論理規模が小さくなるような設計を行う必要があります。

低クロック周波数動作

 基本的に、CMOSロジック回路では、信号が変化(0⇔1)する時しか電流が流れません。
 24MHzクロックでは1秒間に24M回、電子回路が動作して信号も変化します。対して、32kHzクロックでは1秒間に32k回しか、動作(信号変化)しません。→ 32kHzの方が低消費電力、よって、バッテリが長持ちするという理屈です。
 また、メインのLSIが動作しなくても、32kHzクロックで動作するRTCだけがバックアップバッテリで動作し続けることで、実時間を刻み続けることができます。
 これが、RTCを32kHzクロックで動作させる最大の利点です。

 クロック周波数は低ければ低いほど、消費電流を抑えることができるため、32kHzより、もっと遅いクロック(例えば1Hzとか)で動作させた方が、もっと低消費電力になる。の、だけど、そんな遅いクロックは安定して、かつ、精度良く発振させることができないので、昔から、安定発振と低周波数が適度にバランスした32,768Hzが使われている様です。
 32,768Hzは2冪数(=215)なので、普通の1/2分周器×15段(15ビットプリスケーラ)で1sイベントを作り出せます。


 実は、今時の最先端LSIプロセスでは、回路が動作していない時にも、比較的大きな電流が流れます。

  →回路動作時に流れるダイナミック電流に対して、
    動作していない時にも流れるスタティック電流。

 最新プロセスで作った最新LSIだと、クロック周波数を落としてもスタティック電流が大きくて、低消費電流動作には適していない場合があります。使い古された昔のプロセスで製造したLSIの方が、消費電流を抑えて使えたりします。皮肉ですね。
 FPGAボードも、最先端プロセスを使っているし、バックアップバッテリなんてものも使えない、32kHz発振もできないので、実用上あまり意味は無いのだけど、RTLとしては低消費電力を目指して設計しようと思います。

不要論理の削減

 ロバスト性や、不正値が入力された場合の回復などの論理は、ユーザに向けては親切だけど、そういう論理は、正しい入力をする限り必要のない回路なので、少なくともRTCに於いては削減の対象とします。→論理規模削減、ひいては低消費電力化の一環です。

 例えばBCD値。0x00〜0x99までのBCD値は、途中の0x3e等の値は取りません。
 RTCは実時間時計なので、毎回、秒カウンタに+1するだけです。各桁からの桁上がりもすべて+1しか発生しません。+3とか+6することはありません。
 この場合、10進数なら各桁の数値が0x9+0x1=0xaとなったら、その桁は0x0にクリアして、1つ上の桁に桁上がりが発生します。
 この桁上がり判定は、0xaの1ポイントで行うこととします。

 例えば、C言語で書くと、以下の通りです。

  bcd0++;
  if (bcd0==0xa)
  {
    bcd0=0x0;
    bcd1++;  // 桁上がり
  }


 入力が正常範囲にある限り、問題なく動作するコードです。
 しかし、ソフトウェア(C言語など)では以下の様に記述した方が、よりロバストなソフトウェアとなります。

  bcd0++;
  if (bcd0>=0xa)
  {
    bcd0=0x0;
    bcd1++;  // 桁上がり
  }


 わかりますよね?判定に不等号を使います。
 もし、ユーザが、0x3eなど、BCDとして不正な数値を設定したとしても、不等号で判定しておけば、最短の時間で正常な範囲に引き戻すことができます。

 電子回路でこれをやるとどうなるかと言うと:

 と、なって、不等号の方が論理規模が大きくなってしまいます。RTCの論理規模をなるべく小さくするため、不等号は使わず、等号を使ってRTLを組みます。

  他にも:

 これ(↑)は「仕様」です。正しいBCD値や正しいカレンダ値を書き込むのは、ソフトウェア(ユーザ)側の責任となります。こうすることで、例外に対応するための余計な論理を削減して、論理規模を小さくすることができます。

 多少の論理規模増大は、最先端のLSIプロセスでは気にならない程かもしれませんが、論理規模→面積→消費電流がバッテリの持ち時間に影響するとなれば、この程度の仕様制限でも論理規模を縮小する方が、RTCにとっては正義です。

※念押しですが、ソフトウェア(ユーザ)が正常範囲のデータを入力する限り、上のような制限下でも正常動作となります。言い換えると、極めて合理的な制限です。

32kHzクロック動作

 一口に32kHzって記述してしまっていますが、32,768Hzです。
 2冪数(=215)なので、普通の1/2分周器×15段(15ビットプリスケーラ)で1sイベントを作り出すことができ、RTCとの親和性が高い周波数です。
 FPGAボードでは、32kHzの発振はできないので、外部回路で32kHzクロックを発振させておいて、そのクロックをFPGAボードに入力して使う必要があります。

 しかし、FPGAボードの外部から32kHzクロックを入力しないと使えないのでは、ちょっと使い勝手が悪いですよね。どうせ、バックアップバッテリ動作なんてできないFPGAボードなので、内部の24MHzクロックを分周して32kHzクロックを作り、動作できる様にしておきたいと思います。

24MHz → 32kHz分周器

 本節では、CPU等を駆動するシステムクロック(24MHz)から32,768Hzクロックを作ろうとしています。

 24MHz÷32,768Hz=732.421875です。正確に等号です(有限小数)。とは言っても、分周比は2冪数では無いばかりか、そもそも整数でさえありません。

 2つの周波数を素因数分解してみると:

  32,768Hz=215
  24MHz=24,000,000=29×3×56

 なので、

  24MHz÷32,768Hz=3×56÷26=46,875÷64=732…27
    =732+27÷64

 つまり、24MHz → 32,768Hzの分周クロックは、732周期の分周器(プリスケーラ)と、733周期の分周器を27÷64の比で混合すると、生成できます。

非整数比分周器

 一般に、クロックf1[Hz]をクロックf2[Hz]に分周するアルゴリズムを考えてみます。f1>>f2で、分周比D=f1÷f2が整数ではない場合とします。

 ブロック図を以下に示します。

a229_fracdiv.png

 では、f1=24MHzとf2=32,768Hzに当てはめてみましょう。
 分周比D=f1÷f2は無限小数ではありません(前節)。

  D=f1÷f2=24,000,000÷32,768=Di(=732)+27÷64

 よって累積誤差errに加減算する値は、×64倍すると整数になります。

  err+=(Di−D)×64=−27または、
  err+=((Di+1)−D)×64=37。

 累積誤差err→0となる様に制御して行きます。
 ミクロには、f1÷Di=24MHz÷732≒32,787Hzと、f1÷(Di+1)≒32,742Hzのクロックが発生しますが、マクロには、f1÷(Di+27÷64)=32,768Hzとなって、f2=32,768Hzに収束します。

※本アルゴリズムは、無限小数となる周波数比でも適応可能ですが、整数化する時の解像度(上の×64に相当する乗数の大きさ)に従って、分周されたf2≒f1÷Dの精度に影響します。今回の分周比(有限小数)だと、×64倍によって正確に誤差を補償できるので、十分な時間が経過するとf1÷Di’→f2に収束します。


 各周期の半分(Di>>1)でクロックをトグルさせています。累積誤差err次第でDiは偶数/奇数両方を取るので、デューティは50%から少しずれる事にも留意してください。エッジトリガF/Fに対するクロックとして使用する場合、特に問題ない程度のズレだと考えられます。

クロック生成回路

 32kHzクロックをFPGAボードの外部から入力してもらう場合と、内部24MHzクロックからの分周器で生成する場合を選択する回路を設けます(下図)。

a229_clkgen.png

 外部クロック入力rtc_clkinが発振しているかどうか、判定してEAVL信号を出力してあります。外部クロックが発振していない時には、ESELビットを外部クロック側に設定できない様、論理的にガードを掛けるための信号です。
 前の節で指摘した様に、RTCはMCUがリセット中であろうが、他のモードに遷移しようが、クロックと電源が存在する間は正しく時間を刻まなくてはなりません。
 よって、上の回路はリセットで初期化される回路を持ちません。リセットで初期化されてしまうと、MCUにリセットを掛けた時に状態が変化してしまって、RTCのカウントが停止してしまう場合があるためです。

 ただし、内部クロックと外部クロックを選択する状態ビットESELは、リセットが掛からないので困ることがあります。電源投入時、ESEL=0なのか、1なのか、確定できないことです。
 実はFPGAボードの場合、回路を書き込んだ時点で、どうやらF/Fは0に初期化されている様なので、電源投入時のESEL=0を前提として論理を組んで矛盾しません。
 FPGAではない普通のLSIの場合は、0/1確定しないので、電源投入時だけリセットがかかる様な手段を考えなくてはならず、これが結構厄介です。普通は電源Vccの立ち上がりを検出して、パワーオンリセット信号と言うのを作り、対処しています(アナログ回路)。
 パワーオンリセットは電源投入時だけアサートされて、リセット端子によるリセットではアサートされない信号です。

 ともあれ、今回はFPGAなので、電源投入時ESEL=0で初期化されている前提で論理を設計しました。

 実は、IPネットリストでのシミュレーションは、ちゃんとESEL=0で初期化されていて、FPGAボードでの実動作と符合していました。困ったのはRTLシミュレーションでした。RTLでは、リセットのかからないF/Fは不確定値1'bxなので、ESEL=1'bxとなって回路が動作しません。
 こちらは、RTLにinitialブロックで、ESELビットを0に初期化する記述を追加して解決しました。

  // rtc clock select
  reg rckg_esel;
  initial
    rckg_esel=1'b0;  // これが無いと、シミュレーションできない。
  always @(posedge clk)
    begin
      if (rckg_esel_lat)
        rckg_esel<=rckg_esel_dat;
    end


 RTLにinitialブロックなんて記述して、「エラー無く論理合成できるのか?」と、思ったけど、難なくエラーなしで合成できてしまいました。
 initialブロックは論理合成に対して何の影響も与えないみたいです(無いのと同じ)。

 つまり、シミュレーションで動作確認する時だけ、initialブロックが機能します。その結果、製造された実物の回路には、initialブロックの初期化に起因する回路は全く含まれません。
 万一、実物の動作とRTLの動作(シミュレーション)の相違によって、シミュレーションでバグを発見できなかった場合、実物が出来上がってから嵌まることになります。そういう可能性を残してしまうinitialブロックは、RTLに記述しないことが賢明です。
 今回はFPGAボードだったため、実物の動作と合わせるために許容しています。

非同期設計

 システムクロック24MHzと、32kHzクロックは非同期なので、32kHzのRTCからカウンタ値を読み出したり書き込んだりする時は、双方向で非同期転送となります。
 非同期クロック間のデータ転送にはハンドシェイクを使います(過去に解説済み、リンク先を参照)。
 CPU(24MHz動作)から見た時、かなり遅い32kHzクロックとハンドシェイクする必要があるので、24MHzドメイン → 32kHzドメインへのデータ転送には時間がかかります(32kHzクロック数周期)。

 逆に、32kHzドメイン → 24MHzドメインへのデータ転送は、clk32k↑(立ち上がりエッジ)で論理が変化・収束した後、clk32k↓で転送しているので、32kHz周期でデータをアップデートできます(下図)。

a229_sync.png

 SYNC信号は24MHz同期の信号で、クロックclk32kの立ち下がりエッジを検出した信号です。SYNC信号のタイミングで、32kHzドメインのカウンタレジスタを毎回24MHzドメインへ転送します。このタイミングでは、32kHzドメインの信号は確定(valid)していて変化しないので、ハンドシェイクなしでデータを転送することができます。

 これを実現するため、clk32kのタイミング制約は、立ち下がりエッジまでの1/2周期で収束させるよう、周波数としては倍の64kHzにマージンを取って設定しておきます。以下にタイミング制約ファイルの中身を示しておきます。

  # ********************************
  # for RTC400 synthesis
  # ********************************
  #Created Clock
  create_clock -name clk -period 40 -waveform {0 20} [get_ports {clk}]
  create_clock -name clk32k -period 15200 -waveform {0 7600} [get_nets {clk32k}]
  #Set False Path
  set_false_path -from [get_clocks {clk}] -to [get_clocks {clk32k}]
  set_false_path -from [get_clocks {clk32k}] -to [get_clocks {clk}]


 数値の単位はnsなので、clk32kの周波数を1÷15,200ns≒65.8kHzと設定しています。この設定でタイミングがメットすれば、clk32k↑で変化した信号が、次のclk32k↓までに確定することが保証され、clk32k↓のタイミングで24MHzドメインへのデータ転送が実行可能となります。
 clk⇔clk32k間にフォルスパスも設定しておかないと、余計なタイミングエラーが山の如く出力されます。

 今使っているFPGAボードの論理は速いので、RTC程度の論理は、普通にMHzオーダで動くと思います。よって、タイミング制約が32kHzだろうが64kHzだろうが関係ないと思われますが、もっと遅いFPGA等を使う場合にも同じタイミング制約が使えるように、意味のある値で設定しておくこととします。


 RTCという機能ブロックに対する要求事項を含む基本設計と、24MHz内部クロックから32,768Hzクロックを分周するアルゴリズムなど、詳細設計に至るまで一通り舐めてみました。次回、RTCとして全体を組み上げ、考察と実験でテーマを閉じたいと思います。


btn_arduino.png

btn_prev.png btn_next.png

home