まえがき
前回、STM32でRust LEDを表示装置に DMA & printlnの最後で、RTCで時計を表示しようかなとつぶやきました。
実は、ちょこちょこっと作って、print_led!(led, "{}:{}:{}",hh,mm,ss);
を動かそうと、そんなつもりで手がけてみると、意外と大変でした。
レジスタを一からいじってRTCを動かすという実例の一つにもなるかなと、この記事も書いておきます。
RTC
STM32F401には、リアルタイムクロック(RTC)が搭載されています。MCUのVBAT端子に別電源で電圧をかけておけば、メインの電源を落としてもリセットをしても、計時をつづけてくれます。
2系統のアラームや、周期的ウェイクアップ信号などの機能もついていて、結構豊富な機能です。
今回使用しているNUCLEO-F401REには、VBAT端子が端子台に出ていて、RTC用に、32.768kHzの発振器も基板上に実装されています。ハードとしては、準備がすでに整っている状態です。が、NUCLEO-F401REの回路図をよく見てみると、VBATが、VDDと直結されています。そのまま、VBATに電圧を供給すると、VDDとバッティングします。このため、VBATにバッテリー等をつなぐ際は、SB45のハンダブリッジ(実装は、0Ωのチップ抵抗)をカットする必要が有ります。ちなみに、VBATに電圧を供給せずに運用しても、主電源が供給されている間は、内部的に、主電源で動くようになっているのですが、VBATを別電源にしない場合は、VDDとつなぐように推奨されています。
さて、バックアップ電源まで持つ、この回路ですが、中のハードウェアの実装も結構複雑だったりします。
RTCそのものは、比較的、単純なカウンタとして実装されています。秒以下から始まり、秒・分・時・日・月・年の各フィールドを、自動でカウントアップしてくれます。クロックソースとしては、32.768KHzの外部発振器、または、精度的にはそれなりの32KHzの内部発振器のどちらかから選ぶことが出来ます。
ところで、このRTC回路ですが、VBATによるバックアップが出来ますので、電源系統が、バックアップドメインとしてCPU等の他の回路とは分けられています。クロックも、独自の外部発振器を使用するので、これも別回路です。CPUのリセットでも時計が止まらないように、リセット系も別回路となります。基本回路が、別系統扱いのものが多い上に、不正な書き込みから保護するためと称して、他の回路にはない、レジスタの保護回路まで装備されています。かくして、RTCを使用するためには、電源制御・クロック制御・そしてRTCの制御と3つのブロックに渡る設定が必要になります。マニュアルも、3章に渡る各関係部分が絡み合い、妙に初期手順が複雑になります。
電源コントローラ(バックアップドメイン)
RTC回路の電源系統は、リファレンスではバックアップドメインと表現されています。この系統に属するレジスタ郡は、CPUのリセット回路回路からも独立し、VBATが生きている限り、リセットや電源OFFで、リセットされることはありません。また、デフォルトで書き込み保護がかかっています。
まずは、この書き込み保護の解除から、設定は始まります。
この部分の手順は、次の通り。
- 電源インターフェースのクロックの有効化(RCC_APB1ENRのPWRENビットをセット)
- バックアップドメイン書き込み保護の無効化(PWR_CRのDBPビットをセット)
- バックアップレギュレータの有効化(PWR_CSRのBREビットをセット)
- バックアップレギュレータの安定を待つ(PWR_CSRのBRRビットがセットされるまで)
クロック
さて、クロック関係のセットアップです。STMのクロックの全体像は、大きく分けて、HSE・HSI系統と、LSE・LSI系統の2つの系統からなります。前者は、CPUを始めとするシステム全体のクロックです。そして、後者が、今回使用するRTC関係のクロックになります。(実際は、双方が他者のクロック源を取れるので、スッキリはしていません。)
RTCは、クロック源として、
- LSI(チップ内蔵の発振器 32kHz)
- LSE(外付けの水晶等の発振器 32.768kHz)
- HSE(システム系統の外付け発振器を分周)
の3つから一つを選んで使用します。通常は、上2つのうちのどちらかで、LSIの精度が、±1.5%程度と低めですので、今回は、LSEを使用します。
ここから設定するレジスタ群も、殆どがバックアップドメインに存在し、書き込み保護がされています。また、クロック関係は一回設定すると、再設定にはバックアップドメインのリセットが必要となります。通常のCPU系統のリセットではリセットされません。
RTCの価値は、何があろうと、時計を刻み続けることに有ります。CPUがリセットされると、プログラムの初期化シーケンスを実行するわけですが、RTCの初期化は本来ならやりたくありません。となると、「以前に初期化したことがあるときは」「RTCの初期化はとばす」とやるべきです。これ、条件の部分の判断をどうやるか、悩ましい感じになります。でも、あっちこっちに仕掛けられている変更禁止のおかげで、何も考えずに、初期化してしまっても問題がありません。おかげで、プログラムが単純化出来ます。
では、クロックの初期設定手順です。
- LSEの電源を入れる(RCC_BDCRのLSEONをセット)
- LSEの安定化を待つ(RCC_BDCRのLSERDYのセットを待つ)
- RTCのクロック選択をLSEに切り替える(RCC_BDCRのRTCSELを01(LSE)に)
- RTCの電源を入れる(RCC_BDCRのRTCENビットをセット)
前章の電源ドメインの設定と合わせて、ここまでの処理をとりあえず一つの関数にまとめます。
fn init_pwr_block(&self) {
//バックアップドメインセットアップ
self.device.RCC.apb1enr.modify(|_, w| w.pwren().enabled());
self.device.PWR.cr.modify(|_, w| w.dbp().set_bit());
if !self.device.PWR.csr.read().brr().bits() {
self.device.PWR.csr.modify(|_, w| w.bre().set_bit());
while !self.device.PWR.csr.read().brr().bits() {}
}
if self.device.RCC.bdcr.read().lserdy().is_not_ready() {
self.device.RCC.bdcr.modify(|_, w| w.lseon().on());
while self.device.RCC.bdcr.read().lserdy().is_not_ready() {}
}
if self.device.RCC.bdcr.read().rtcen().is_disabled() {
self.device.RCC.bdcr.modify(|_, w| {
w.rtcsel().lse();
w.rtcen().enabled()
});
}
}
愚直に、設定手順を進めているだけですが、念の為、いくつかの設定は、現在の設定の確認を入れています。
時刻の設定
さて、ここまでで、時計はもう動き始めています。バックアップドメインリセット直後は、時計は、0年1月1日00:00:00から動き始めます。
そして、この時計は、通常のCPUリセットでは、リセットされません。BAT端子に独自に電源を設けた場合は、電源リセットでもリセットされません。
時刻の初期化・修正の手順は、同じ手順となります。
まず、バックアップドメインの書き込み保護は、解除したままにされていることにします。本当は、この書き込み保護は、書き込みした後は保護状態に戻すべきです。つまり、手抜きです=^・・;=。本来なら、適切なタイミングで、PWR_CRのDBPビットをクリアして、再び、必要な時にDBPビットをセットし直すべきでしょう。
さて、時刻の設定です。この後の処理は、やっと、RTCレジスタ郡の操作となります。
ここで、RTCの構成ですが、先のクロック設定で設定したクロック源は、32.768kHzです。RTCは、これを2段のプリスケーラを通して、1Hzのクロックを生成し、このクロックでカウンタを起動しています。この2段のプリスケーラーは、前半が非同期で後半が同期回路となっていて、電力を節減するために、できるだけ前段のプリスケーラの値を大きくすることが推奨されています。この2つのプリスケーラは、ともに、RTC_PRERレジスタで設定できますが、実は、クロック源が32.768kHzであれば、初期値のままで使えます。ちなみに、非同期プリスケーラが128分周で、同期プリスケーラが256分周です。
さて、このRTCレジスタ群の大半のビットは、さっきのバックアップドメインの書き込み保護とは別に、さらに書き込み保護がかかっています。そして、その上に、時刻に関わるビットとRTCのプレスケーラーに関する設定は、初期化モードに入らないと修正できないという、3重の保護になっています。
では、時刻の設定手順です。
- RTCレジスタ群の書き込み保護の解除
- RTC_WPRレジスタに0xCAを書き込む
- RTC_wPRレジスタに0x53を書き込む
- RTCを初期化モードに切り替える(RTC_ISRレジスタのINITビットをセット)
- RTCが初期化モードに入るのを待つ(RTC_ISRレジスタのINITFがセットされるまで待つ)
- RTCの時刻表示を24時間表示に切り替え(RTC_CRレジスタのFMTビットをクリア)【注:12時間表記にしたければセット。この場合、次にセットするRTC_TRレジスタのPMビットが有効になる。】
- 日時の書き込み(このレジスタは、全てBCD表記となっている。従って十進の各桁ごとに設定が必要。?Tフィールドに10の桁を、?Uレジスタに1の桁をセットする。各4ビットで構成されているが、0x0〜0x9までの数値しかセットしてはいけない。)
- 年の書き込み(RTC_DRレジスタのYT・YUフィールドに、年の下二桁の各桁をセット)
- 月の書き込み(RTC_DRレジスタのMT・MUフィールドに各桁をセット)
- 日の書き込み(RTC_DRレジスタのDT・DUフィールドに各桁をセット)
- 曜日の書き込み(月曜日が0b001で、日曜日が0b111。0b000は禁止)
- am・pmのセット(今回は24時間表記なので、無効。RTC_TRレジスタのpmビットをクリア)
- 時の書き込み(RTC_TRレジスタのHT・HUフィールドに各桁をセット)
- 分の書き込み(RTC_TRレジスタのMNT・MNUフィールドに各桁をセット)
- 秒の書き込み(RTC_TRレジスタのST・SUフィールドに各桁をセット)
- 初期化モードの終了。(RTC_ISRレジスタのINITビットをクリアする)
- 書き込みロックの復帰(RTC_WPRレジスタに0xFFをセット。)
初期化モードの終了をセット時点より、RTCの基本クロック(32.768kHz)の、4クロック後に時計が再スタートします。
この部分のソースは、3関数からなります。初期化部分の時刻の設定までと、初期化モードの終了部分、そして、曜日の計算です。
初期化の最初の部分、INITFの待ちに、RTCクロックで2クロック、そして、最後に、4クロックがかかるので、別の時刻源から正確な時刻が取れる時に、この方が有利かなと・・・まぁ、2クロックで60μSEC程度の話なので、気持ちですねぇ。
/// RTCの時刻設定の準備
/// 時刻の設定は、exec_set_timeで行う。
pub fn set_time_pre(device: &stm32f401::Peripherals, date_time: &DateTime) {
let r = &device.RTC;
//書き込みロック解除
r.wpr.write(|w| unsafe { w.key().bits(0xCA) });
r.wpr.write(|w| unsafe { w.key().bits(0x53) });
//initモード切り替え
r.isr.modify(|_, w| w.init().set_bit());
while r.isr.read().initf().bits() == false {
cortex_m::asm::nop();
}
//24時間制の指定
r.cr.modify(|_, w| w.fmt().clear_bit());
//日時書き込み
r.dr.modify(|_, w| unsafe {
w.yt().bits(date_time.year / 10);
w.yu().bits(date_time.year % 10);
w.wdu().bits(Self::daytoweek(&date_time));
w.mt().bit(date_time.month >= 10);
w.mu().bits(date_time.month % 10);
w.dt().bits(date_time.date / 10);
w.du().bits(date_time.date % 10)
});
//時刻書き込み
r.tr.modify(|_, w| unsafe {
w.pm().clear_bit();
w.ht().bits(date_time.hour / 10);
w.hu().bits(date_time.hour % 10);
w.mnt().bits(date_time.min / 10);
w.mnu().bits(date_time.min % 10);
w.st().bits(date_time.sec / 10);
w.su().bits(date_time.sec % 10)
});
}
/// 時刻設定を実行する。時計を再スタートする。
pub fn exec_set_time(device: &stm32f401::Peripherals) {
let r = &device.RTC;
//initモード解除(時計スタート)
r.isr.modify(|_, w| w.init().clear_bit());
//書き込みロックの復帰
r.wpr.write(|w| unsafe { w.key().bits(0xff) });
}
/// 日付より曜日を求める
pub fn daytoweek(date_time: &DateTime) -> u8 {
let c = date_time.year / 100;
let g = 5 * c + c / 4;
let ym100 = 20;
let m_kou = 26 * (date_time.month + 1) / 10;
(date_time.date + m_kou + ym100 + ym100 / 4 + g + 5) % 7 + 1
}
先程、各数値は、BCDで各桁4ビットとまとめちゃいましたが、実は、嘘です。十の桁が4ビットも必要のないところが、何箇所か、ビット数が削られています。その中で、プログラムに波及してしまうのが、「月」の10の桁です。1か0しかないので1ビットになってしまい、stmf401クレートのインターフェースがbool扱いになっています。そのため、ここだけちょっと記述が不自然になります。
daytoweek関数のアルゴリズムは、ツェラーの公式をそのまま書いてあるだけです。公式の導出は、検索すれば、私なんぞより、よっぽどわかりやすく書いてあるところが多々有りますので省略。
なお、実験はしてませんが、あり得ない数値列を入れても、そのまま素直に動き出し、ありえない挙動をするという情報を、どこかで見ました。日付と時刻の数値が正当であることは、プログラム側で保証する必要が有ります。
時刻の初期化を一度行うと、RTC_ISRレジスタのINITSフラグがセットされます。このフラグを見れば、日時を初期化したかどうか判定することが出来ます。
日付と時刻の読み出し
後は、日付と時刻を読み出すだけです。
手順は、先の、RTC_DRレジスタと、RTC_TRレジスタを読み出すだけ。BCDなので、10進扱いで数値を組めば、終わりです。
注意事項が、一点。APB1のクロック周波数(普通は、システムクロックに同じ。)が、RTCのクロック周波数(32.768kHz)の7倍以上、つまり、229.376kHz以上であることが条件となっています。まぁ、普通は単位が違うでしょうから大丈夫でしょう。万が一、これを下回るときは、同期の関係で、2回の読み出しを行い、値が同じことを確認する必要が有ります。
/// 時刻の取得
/// 戻り値:(時,分,秒)のタプル
pub fn get_time(&self) -> (u32, u32, u32) {
let r = &self.device.RTC.tr.read();
let h = (r.ht().bits() as u32) * 10 + (r.hu().bits() as u32);
let m = (r.mnt().bits() as u32) * 10 + (r.mnu().bits() as u32);
let s = (r.st().bits() as u32) * 10 + (r.su().bits() as u32);
(h, m, s)
}
/// 日付の取得
/// 戻り値:(年,月,日,曜日)のタプル
/// 曜日の数値は、月曜日が1で日曜日が7
pub fn get_date(&self) -> (u32, u32, u32, u32) {
let r = &self.device.RTC.dr.read();
let y = (r.yt().bits() as u32) * 10 + (r.yu().bits() as u32) + 2000;
let m = if r.mt().bits() { 10 } else { 0 } + (r.mu().bits() as u32);
let d = (r.dt().bits() as u32) * 10 + (r.du().bits() as u32);
let w = r.wdu().bits() as u32;
(y, m, d, w)
}
単純ではあるのですが、月の10の位はbool扱いですから、そのままでは、数値として扱えませんので、少々小細工がしてあります。
rustのifは、式扱いで、値を返せるので、式に組み込んでしまいました。Cで言う、? : 演算子みたいな使い方です。
SysTick (おまけ)
後は、メイン関数を作って、終わりと行きたいところですが、周期的にプログラムを起こして、表示を更新するところと、時計設定のボタンの扱いで「長押し」の判定をするのに、周期実行が欲しくなりました。TIM??ペリフェラルを使ってもよいのですが、いかにも大げさです。限りある高性能なTIMペリフェラルをこれで、2つ専有するのは、ちょっともったいない・・・。
ところで、OSがある環境では、このような要求は、必ずAPIで満たしてくれます。マルチタスクのタスク管理やいろいろな用途で、周期実行は必ず必要になるので、システムのベースに必ずといっていいほど組み込まれているものです。
おそらく、そのために、SysTickというハードウェアがCortex-Mのコア内に組み込まれています。これは、単機能のタイマーです。単純に、システムクロックを設定した値までカウントするだけ。カウントアップしたら、イベントと例外を発生することが出来ます。
例外扱いとなっているのは、多分、Cortex-Mのコア内の回路となっているためです。プログラムから見れば、割込みとまったく変わりません。
さて、これも、レジスタで設定も出来ますが、実は、cortex_mクレートにとっても便利な関数が用意されています。
使い方は、この関数のおかげで、至って簡単。関数は、cortex_m::SYST構造体のメソッドです。set_reload()で周期をシステムクロック単位で指定して、set_clock_source()で、クロック種別を指定、必要ならば、enable_interrupt()で、例外発生を有効にすれば設定は終わり。enable_counter()でカウンターが起動します。
でも、この例外は当然一つしかありません。そこで、コールバック関数を登録して、これを順次呼び出すための構造体を作りました。
コールバック関数の管理のために、Vec相当のものが欲しくなったので、heaplessクレートを導入します。これは、ヒープを使わずにVecやキューなどを使わせてくれるクレートです。ヒープを使わないので、固定長になるのが欠点です。まぁ、今回の用途では、決め打ちでも大きな害はないでしょう。
では、mod systickの全ソースをさくっとあげちゃいます。
use cortex_m::interrupt::{free, CriticalSection, Mutex};
use cortex_m::peripheral::{syst::SystClkSource, SYST};
use cortex_m_rt::exception;
use heapless::consts::U8;
use heapless::Vec;
pub struct SysTick;
impl SysTick {
pub fn new(cs: &CriticalSection, syst: SYST, sys_freq: u32, cycle: u32) -> SysTick {
MY_SYSTICK.borrow(cs).replace(Some(syst));
INTERRUPT_FUNC.borrow(cs).replace(Some(Vec::new()));
let mut syst = MY_SYSTICK.borrow(cs).borrow_mut();
syst.as_mut().unwrap().set_reload(sys_freq / cycle);
syst.as_mut().unwrap().clear_current();
syst.as_mut().unwrap().set_clock_source(SystClkSource::Core);
syst.as_mut().unwrap().enable_interrupt();
SysTick
}
pub fn enable_counter(&self, cs: &CriticalSection) {
let mut syst = MY_SYSTICK.borrow(cs).borrow_mut();
syst.as_mut().unwrap().enable_counter();
}
pub fn add_callback_function(&self, cs: &CriticalSection, func: fn() -> ()) {
let mut table = INTERRUPT_FUNC.borrow(cs).borrow_mut();
table
.as_mut()
.unwrap()
.push(func)
.expect("Interrupt table over fullow");
}
}
static MY_SYSTICK: Mutex<RefCell<Option<SYST>>> = Mutex::new(RefCell::new(None));
static INTERRUPT_FUNC: Mutex<RefCell<Option<Vec<fn() -> (), U8>>>> = Mutex::new(RefCell::new(None));
# [exception]
fn SysTick() {
free(|cs| {
let work = INTERRUPT_FUNC.borrow(cs).borrow();
let table = work.as_ref().unwrap();
for func in table.into_iter() {
func();
}
});
}
普通だと、#[interrupt]で割込み関数の指定ですが、今回は、#[exception]になります。例外になって唯一見える違いはこれだけです。
例のごとく、例外関数と、構造体の間で、コールバック関数の配列を共有する必要があるので、コールバック関数の配列を持つVecは、Mutex>でくるんでいます。static変数は、定数でしか初期化できませんから、更に、Optionでくるむ必要があります。
SysTick()では、クリティカルセクション内で、登録された全ての関数を呼び出しています。そのため、コールバック関数は、短時間で処理を終えることが必要です。
また、システムに唯一のリソースを使っていますから、当然、この構造体も一つしか存在することが出来ません。2つ作ると、new()でやっている初期化処理がダブりますので困ったことになります。本来、その防護処置をすべきですが、今回は、サボっています。使用する際の制約事項とします。
対策するとすれば、ペリフェラルのtake()相当の関数を作ることになるでしょう。
メイン関数
ここまで来たら、後は、全部を組み合わせて、時計の形に仕立てるだけですが、時刻設定のためのUI絡みで膨れ上がり、ずいぶん大きなソースになってしまいしまた。main.rsが400行強に=^・・;=
時計のコントロールに関する大半は、ControllerClockという構造体に分離しています。
main()関数では、各種初期化を実行して、SYSTICKで起こされる度にController::run()を呼び出すだけにしています。
このrun()関数では、スイッチの状態に応じて「時計の表示」か「日時の設定」のいずれかの処理を実行するかのモード判定をするだけにしてあります。
時計の表示【display_clock()】も、スイッチの状態に応じて、「時刻の表示」「日付の表示」「曜日の表示」を切り分けているだけです。
表示本体は、RTC関係の設定をモジュールにしてしまったおかげで、シンプルになっています。
fn display_time(&mut self) {
let (hh, mm, ss) = self.rtc.get_time();
if self.old_sec != ss {
self.old_sec = ss;
print_led!(self.led, "{:>2}:{:>02}:{:>02}\n", hh, mm, ss);
}
}
fn display_date(&mut self) {
let (yy, mm, dd, _) = self.rtc.get_date();
let yy = yy - 2000;
print_led!(self.led, "{:>02}/{:>2}/{:>2}\n", yy, mm, dd);
}
fn display_week(&mut self) {
let (_, _, _, w) = self.rtc.get_date();
let week_name = match w {
1 => "Mon.",
2 => "Tue.",
3 => "Wed.",
4 => "Thu.",
5 => "Fri.",
6 => "Sat.",
7 => "Sun.",
_ => "None",
};
print_led!(self.led, "{}\n", week_name);
}
表示すべき値を取得して、print_led!を呼び出しているだけです。これが初期の目標でした。
後のコードの大半は、モードの切り替え、それも主に、日時の設定で今何を設定しているかを管理するために費やされています。長い割に、退屈なので、掲載は省略します。
ソースコード全体
今回もいつくかのモジュールを追加し、全体がわりと大きくなっています。ここに私が書いてきた、ほぼ全てのモジュールを使用しています。
前回、MatrixLedの時に、githubにあげたものも、関連クレートが無いので中途半端んだったので、今回、関連クレートを全部、ひとつのクレートにまとめ上げて、clockとしてgithubに掲載することにします。
githubの掲載場所は、次のとおりです。
https://github.com/mitoneko/clock.git
前回の記事のgithubのurlもこっそり、こちらに直す予定です。
ここまで、お付き合いいただいた方、ありがとうございました。