ハード制御の話の前回の続きです。今回はハードレジスタの制御方法についての説明です。
前回の記事
特にメモリマップド方式であればプログラム上は単なるポインタ変数に対するアクセスと変わりはありません。しかし、ハード制御独特のお作法・ルールがあり、その部分に注意が必要です。特に今回はメモリマップド方式のレジスタアクセスに絞って説明します。
方法1. Writeアクセス
まずはごく一般的な方法です。例えば「データ出力を行うハード制御レジスタに出力開始コマンドをセットする」という方法です。
Writeアクセスのコード例
#define DMA_SEND_START_REG (0x10000000) // ハードレジスタアドレス
#define DMA_SEND_START_COMMAND (0x1) // コマンド例:ビットアサインで機能が割り当てられることが多い
*DMA_SEND_START_REG = DMA_SEND_START_COMMAND; //これがWriteアクセスです。
単にポインタ変数へ代入でOKです!
一点注意しないといけないのは、上記のレジスタに書き込んだ値は必ずしもそのまま読めるわけではないということです。 何が読めるのかはハード仕様書の記載参照が必要になります。
例えば、すべてのbitで1がセットされて読めるとか、0がセットされて読めるとか、不定値が読めるとか、現在のハードウェアのステータスが読める……等々。そのため、ハード仕様次第では以下のような不思議なコードも成立します(バグっていません)。
#define DMA_SEND_START_REG (0x10000000)
#define DMA_SEND_START_COMMAND (0x1)
#define DMA_SEND_COMPLETE (0x80000000)
*DMA_SEND_START_REG = DMA_SEND_START_COMMAND; //★Writeアクセス
while (true) {
if (DMA_SEND_COMPLETE & *DMA_SEND_START_REG) break;
// grepしても*DMA_SEND_START_REGを更新しているのは
// 上記★Writeアクセス行しかないのになぜか条件成立して無限ループを抜ける!
}
}
方法2. Readリセット(空読み)
これもよく使われる方法なのですが、方法1と対での呼称でReadアクセスと記載しなかったのはReadリセット(空読み)の呼称のほうが動きがわかりやすいと思ったからです。
※例えばハード仕様書に以下の情報が記載されていた場合のコード例です
・割り込み要因レジスタ(address:0x10000000, size:32bit, behavior:read reset
Readリセット(空読み)のコード例
#define DMA_SENDCOMP_INTERRUPT_REG (0x10000000)
void dma_send_complete_interrupt() { // 割り込みハンドラ
int val = *DMA_SENDCOMP_INTERRUPT_REG; // 割り込み要因レジスタを空読み
// 読み込んだval値は特に利用しない(Readする処理自体にハード的な意味がある)
~DMA送信完了処理を実行~
return; // ★アセンブラレベルだと単なるreturn命令ではなくreturn exception命令になります。
}
この割り込み要因をクリアしないと割り込み関数からreturnしても(コード例の★部分)、すぐにdma_send_complete_interrupt()がコールされてしまい、さらにreturnしても、すぐにdma_send_complete_interrupt()がコールされて……を繰り返しそのまま無限コールになります。 そのため、空読み処理はとても大事なコードになります(このコードがあれば割り込み処理からリターンできる)。
上記空読み処理で注意が必要なのは、空読み処理はコンパイラの最適化オプションと極めて相性が悪いということです。
上記のコード例だとコンパイラの「速度最適化orサイズ最適化」共に該当しやすくて、その場合、空読みに該当するコードが消える可能性があります(コンパイラが空読みを無意味なコードと判断してしまう)。 最適化されたコンパイル後のアセンブラソースを見ると、本当に空読み該当コード部分がありません。 空読み処理の次の行にval++;などを入れたら最適化で消える現象が防げそうな気がしますが、本当に意味のないコードになりますので、あまりお勧めはできません。
もちろん上述の通りval++は無意味なコードですが、その前の空読み行はとても大事なコードです。 そのため、最適化対策としてハードレジスタの制御処理にvolatile修飾子を使う方法があります。
#define DMA_SENDCOMP_INTERRUPT_REG (0x10000000)
例1
volatile int* dma_send_reg = DMA_SENDCOMP_INTERRUPT_REG;
int interrupt_status = *dma_send_reg; // このコードは最適化で消えない。
例2
int* dma_send_reg = DMA_SENDCOMP_INTERRUPT_REG;
volatile int interrupt_status = *dma_send_reg; // このコードは最適化で消えない。
volatileの意味は「揮発性の」です。すぐに値が変わってしまう変数という意味です。 値が頻繁に変わってしまう特殊な領域への読み書きということをコンパイラに提示してコンパイラに最適化防止を指示しているわけです。
デバイスドライバーのソースコードでvolatile修飾子を見かけたら、ほぼハードレジスタ制御関連のコードと思ってよいでしょう。
方法3. Read Modify Write(リードモディファイライト)
何やら難しいイメージを持つかもしれませんが、要はリード(読んで)・モディファイ(変更して)・ライト(書き込む)……というだけです。
ハードレジスタの特定のビットのみに特殊な意味があって、他のビットは変更したくないみたいな用途で使われます。
Read Modify Write(リードモディファイライト)のコード例
#define DMA_SENDCOMP_INTERRUPT_REG (0x10000000)
#define DMA_SENDCOMP_INTERRUPT_STATUS (0x80000000) // 最上位bitに情報set
volatile int* dma_send_reg = DMA_SENDCOMP_INTERRUPT_REG;
int interrupt_status = *dma_send_reg; // まずはレジスタから情報を読み取る:Read
interrupt_status =| DMA_SENDCOMP_INTERRUPT_STATUS; // 最上位ビットを立てる:Modify
*dma_send_reg = interrupt_status; // 最上位bit以外は変更しない形で書き込む;Write
このタイプのレジスタアクセス方法には亜種が多く、
-
目的のbitが1となっていたら同じく1を立てた値を書き込む(他のbitは変更しない)
→そうすると、目的のbitが1→0に変化してハード制御が完了する。 -
目的のbitが1となっていたらそのbitに0をセットした値を書き込む(他のbitは変更しない)
→そうすると、目的のbitが1→0に変化してハード制御が完了する。
以上のようなハードの特性によって、いろいろ処理が変わります。
ハード仕様書をしっかり確認して、レジスタ制御処理を組む必要があります。特に前者だとソフトウェアロジック上は意味のないコードになりますね(bit1をチェックしてbit1を書き込んでいるわけですから……。これも最適化ロジックに引っ掛かりそう)。しかし、ハード制御上は大事な処理になります。
こういうハード制御処理は、ハード仕様書に不備がなければ、情報を網羅しているハード仕様書を生成AIに食わせることで、ほとんど一瞬でコード生成してくれるかと思います。
しかし、吐き出されたコードの妥当性を検証できなければ、どれだけ生成AIが優秀でも実際には(怖くて)使えないコードになってしまいます。そして、過去コードのメンテナンスも怖くてできないのかと思います。
生成AI自体は便利なものですが、やはり必要最小限の知識は持っておいたほうが良いでしょうね。