昨日、ARM Cortex-Mシリーズで割り込みからの復帰に使われるEXC_RETURNの話を書いた。
さらに面倒な話があって、それが浮動小数点数レジスタのことである。
今回使っているマイコンはCortex-M33なのだが、浮動小数点演算用のVFPが付いている。
ARMv7で言うところのM3, M4に対応するARMv8のプロセッサがM33で、
M3とM4はVFPを積んでいるかどうかがだいたいの差で、
M33のVFPはオプションだったはずだが、大概積んでいる印象はある。
今回は浮動小数点演算を行うシステムなのでVFPを活用している。
整数の汎用レジスタとは別にVFPには単精度だと16個の浮動小数点数レジスタとFPSCRという状態レジスタがある。
これらのレジスタは割り込み処理で退避する対象となっている。
Cortex-Mシリーズではレジスタの退避はハードウェア的に行われるが、
浮動小数点機能を使わなければ32bitレジスタ8個(R0~R3, R12, LR, PC, xPSR)で済むのに、
浮動小数点を使うと+17個ということで大変である。
ただ、ここをサボるための仕組みがあるんですね。
Cortex-M4(F) Lazy Stacking and Context Switching – Application Note 298
M4の話だが、M33も仕組みは全く同じである。
まず、CONTROLレジスタにFPCAというビットがある。
初期値は0だが、浮動小数点命令を使うと自動的に1にセットされる。
FPCA=0で割り込みが発生すると8レジスタのみの退避を行う。
FPCA=1で割り込みが発生すると8+17レジスタの退避を行う……わけではない。
FPCCRレジスタの設定によるのだが、初期値(LSPEN=1, ASPEN=1)の場合は、
スタックポインタに8+17レジスタ分の領域を確保して、基本の8レジスタだけの退避を行う。
その上でCONTROL.FPCA=0, FPCCR.LSPACT=1 に設定する。
割り込みハンドラに浮動小数点演算がないパターンでは、FPCA=0のまま、EXC_RETURNに到達する。
この場合は割り込みハンドラ中にVFPレジスタは一切変化しなかったので、
CONTROL.FPCA=1, FPCCR.LSPACT=0 に戻すだけで完了となる。
あとは基本レジスタの復帰を行って、スタックポインタを戻して割り込み元に戻る。
割り込み処理の多くは浮動小数点演算を含まないだろうから、このパターンが比較的多いはず。
では、浮動小数点演算があった場合はどうなるか。
浮動小数点演算を行おうとすると、FPCA=0→1への変化が生じるが、
このときにFPCCR.LSPACT=1がセットされていたら、この時点でVFPレジスタの退避が行われる。
このようにレジスタ退避を後回しにする仕組みを”Lazy state preservation”と呼んでいる。
FPCA=1でEXC_RETURNに到達した場合は、VFPレジスタも復帰して元の処理に戻る。
で、この仕組みでもEXC_RETURNのアドレスが重要な意味を持っていて、
M33ではEXC_RETURNの4bit目が割り込み時点でのFPCAによって変わる。
割り込み時にFPCA=0の場合は4bit目は1、復帰時にスタックポインタを8レジスタ分だけ戻すことも表す。
割り込み時にFPCA=1の場合は4bit目は0、こちらはVFPレジスタ分もスタックポインタが進んでいて、
EXC_RETURN到達時のCONTROL.FPCAの値に応じて復帰要否が変わると。
で、昨日書いたCPU例外から再びmain関数に戻ってくる場合の話だが、
具体的にはEXC_RETURN=0xFFFFFFB8へジャンプすると、
Threadモード、Mainスタック、Non-Secure、特権モード、FPCA=0というリセット時と同様の状態になる。
ただ、このまま動かし続けるとそれはそれで問題があって、
それはFPCCR.LSPACT=1がセットされていると言うことである。
この状態で浮動小数点命令を実行すると誤ったレジスタ退避をしようとする。
ジャンプ後にFPCCRレジスタの値を明示的に設定する処理を入れれば問題は解消した。
割り込みハンドラでFPCA=0にセットする仕組みは多重割り込みにも効いて、
浮動小数点演算をしない割り込みハンドラ実行中に割り込みが発生した場合、
割り込みハンドラのレジスタ退避という観点では8レジスタの退避のみでよい。
レジスタ退避用のスタックサイズの削減にも寄与してるわけですね。
Mainスタックだけ使う前提で言えば、
- main関数(VFP使用) スタック消費200byte+レジスタ退避104byte
- 優先度2の割り込み(VFP不使用) スタック消費8byte+レジスタ退避32byte
- 優先度1の割り込み(VFP使用) スタック消費40byte+レジスタ退避104byte
- 優先度0の割り込み(VFP不使用) スタック消費8byte+レジスタ退避32byte
- MPU例外ハンドラ スタック消費0byte
で、合計528byte消費する可能性があるわけですね。
全体的にはあまり深く考えなくても効率よく動くように考えられているが、
通常と異なる操作をするといろいろ踏んでしまうんですね。
この失敗がなければ気づかなかったことは多かったわけですけど。