Go言語でMCU向けのプログラムが書けるTinyGoを使ってみています。前回は発振器まわりの制御レジスタを直接読んでみました。今回はクロックジェネレータ関係を調べようと思ったのですが1点障害あり、unsafeなモジュールなどにご登場いただいてしまいました。厳格なようでいてそんな操作もできるTinyGoはMCU向き?
※「AT SAMの部屋」 投稿順indexはこちら
※以下実験はSeeduino Xiaoボードで行っています。具体的にはMicrochip社AT SAMマイコンに特有の操作を扱っていますが、他のマイコンでも似たり寄ったりかと。
ポインタアクセスと型チェック
Go言語はモダーンな言語らしく型チェックが結構厳格です。Goは「ポインタ」も使えますが、アクセスする先のデータ幅などチェックしてくれるのでテキトーなアクセスは許してくれません。しかし、今回直面したのは、ハードウエアの制御の都合で「同じ番地にバイトとハーフワード」両方でアクセスしないとならないパターンです。かいつまむとこんな感じ。
-
- 9本あるクロックジェネレータの行き先(周辺回路)が36ある
- 上記の組み合わせとクロックの許可を制御するレジスタはたった16ビットの幅しかない
- 行き先の選択はIDフィールドに行き先の番号を「書き込む」ことで行う
- 対応のクロックジェネレータ番号や許可不許可の設定値を読み出すためには上記のIDフィールドに値を書いてから、読み出す必要がある
MCUで制御レジスタを読み書きする場合、通常リード・モディファイ・ライト操作を行います。
-
- レジスタを読んで
- 書き換えるフィールド以外が書き換わらないようにマスクしてから、書き換えるフィールドのみOR
- 作った値を書き戻す
ところが上記の16ビットレジスタの場合、リードモディファイ操作では1の時に読み出したレジスタ値は書き戻すときの行き先に対応した値ではないかもしれない(たまたま表に出ていた他の行き先の設定)ので、3でID指定するつもりで書き込むと無関係なビットに誤った値を書き込む可能性があります。リードモディファイライトではダメですな。最悪、動いていた筈のペリフェラルへのクロック供給が絶たれるかも知れません。
ATSAMマイコンの場合にこの手のレジスタ操作ができるのは、1本のレジスタにバイトでもワード(ハーフワード)でもアクセスできるからです。上記例の場合は、IDフィールドは下8ビットに置かれていて、書き込むとヤバイ奴らは上8ビットにまとめられています。まず下8ビットにIDを書き、その後上下16ビットを読み出してやれば完全な設定が得られます。ただこれを行うには同じアドレスのポインタをバイトだったりハーフワードだったり、厳格なGoの型チェックを回避して使用する必要があり、と。
Microchip社のATSAMD21G18の製品ページは以下に。今回のようなアクセスには以下からダウンロードできるデータシートを読まねば危ないです。
ATSAMD21G18 | Microchip Technology
作製した実験プログラム
TinyGoのATSAMマイコン用の「ドライバ」部分のソースでは、16ビット幅で一方通行の設定(書き込みのみで済む)はしていましたが、設定した値を読み出すような操作をしているところが見つかりませんでした(全部みたわけじゃないですが。)
そこで、以前に調べてあった、unsafeモジュールを使って、自前でポインタを作ってレジスタの読み出し(読み出しだけれど先に書き込みが必要)をやってみました。また、メモリマップドのペリフェラルのレジスタなので volatile モジュール(こういうモジュールがあるところがTinyGoがMCU向けにちゃんとしているところじゃないかと思います)も利用させてもらいました。
こんな感じ。
package main import ( "fmt" "time" "runtime/volatile" "unsafe" ) func dumpCLKCTRL(id uint8) { CLKCTRL_DIR8 := (*uint8)(unsafe.Pointer(uintptr(0x40000c02))) CLKCTRL_DIR16 := (*uint16)(unsafe.Pointer(uintptr(0x40000c02))) volatile.StoreUint8(CLKCTRL_DIR8, id) work := volatile.LoadUint16(CLKCTRL_DIR16) fmt.Printf("ID=0x%02X ", (work & 0x3F)) fmt.Printf("GEN=%d ", (work & 0x0F00)>>8) fmt.Printf("CLKEN=%d ", (work & 0x4000)>>14) fmt.Printf("WRTLOCK=%d\n", (work & 0x8000)>>15) } func main() { for { for id := 0; id < 0x25; id++ { dumpCLKCTRL(uint8(id)) } fmt.Println("-------------------------------") time.Sleep(5 * time.Second) } }
上記コードの実験結果
以下に上記コードを走らせた結果を示します。TinyGoでオブジェクトを作ったときのデフォルト設定を反映しているものと思います。
ID=でHEX番号が書き出されているのが、各ペリフェラルに対応するIDです。例えばID=0x04はRTC用のGCLK設定を示します。GEN=で表示されている番号は9本(0から8)あるクロックジェネレータの番号です。9本のクロックジェネレータには前回やったオシレータのソースが接続されています(設定は未ダンプです)CLKEN=1であればクロックは配給されており、0なら止まってます。WRTLOCKは書き込みロックの制御ですが、ロックされているエントリは無いようです。
ID=0x00 GEN=1 CLKEN=1 WRTLOCK=0 ID=0x01 GEN=0 CLKEN=0 WRTLOCK=0 ID=0x02 GEN=0 CLKEN=0 WRTLOCK=0 ID=0x03 GEN=2 CLKEN=0 WRTLOCK=0 ID=0x04 GEN=2 CLKEN=1 WRTLOCK=0 ID=0x05 GEN=0 CLKEN=0 WRTLOCK=0 ID=0x06 GEN=0 CLKEN=1 WRTLOCK=0 ID=0x07 GEN=0 CLKEN=0 WRTLOCK=0 ID=0x08 GEN=0 CLKEN=0 WRTLOCK=0 ID=0x09 GEN=0 CLKEN=0 WRTLOCK=0 ID=0x0A GEN=0 CLKEN=0 WRTLOCK=0 ID=0x0B GEN=0 CLKEN=0 WRTLOCK=0 ID=0x0C GEN=0 CLKEN=0 WRTLOCK=0 ID=0x0D GEN=0 CLKEN=0 WRTLOCK=0 ID=0x0E GEN=0 CLKEN=0 WRTLOCK=0 ID=0x0F GEN=0 CLKEN=0 WRTLOCK=0 ID=0x10 GEN=0 CLKEN=0 WRTLOCK=0 ID=0x11 GEN=0 CLKEN=0 WRTLOCK=0 ID=0x12 GEN=0 CLKEN=0 WRTLOCK=0 ID=0x13 GEN=0 CLKEN=0 WRTLOCK=0 ID=0x14 GEN=0 CLKEN=1 WRTLOCK=0 ID=0x15 GEN=0 CLKEN=1 WRTLOCK=0 ID=0x16 GEN=0 CLKEN=1 WRTLOCK=0 ID=0x17 GEN=0 CLKEN=1 WRTLOCK=0 ID=0x18 GEN=0 CLKEN=1 WRTLOCK=0 ID=0x19 GEN=0 CLKEN=1 WRTLOCK=0 ID=0x1A GEN=0 CLKEN=0 WRTLOCK=0 ID=0x1B GEN=0 CLKEN=0 WRTLOCK=0 ID=0x1C GEN=0 CLKEN=0 WRTLOCK=0 ID=0x1D GEN=0 CLKEN=0 WRTLOCK=0 ID=0x1E GEN=0 CLKEN=1 WRTLOCK=0 ID=0x1F GEN=0 CLKEN=0 WRTLOCK=0 ID=0x20 GEN=0 CLKEN=0 WRTLOCK=0 ID=0x21 GEN=0 CLKEN=0 WRTLOCK=0 ID=0x22 GEN=0 CLKEN=0 WRTLOCK=0 ID=0x23 GEN=0 CLKEN=0 WRTLOCK=0 ID=0x24 GEN=0 CLKEN=0 WRTLOCK=0
上記の生の結果をデータシート見ながら解釈すると
-
- GCLK_DFLL48_REF(前回やったDFLL48発振器のリファレンスクロック)にはクロックジェネレータの1番が使われている。(クロックジェネレータの1番は特殊で、他のクロックジェネレータのソースになることができる)
- GCLK_RTC(RTC、実時間時計)にはクロックジェネレータの2番が使われている。WDT(ウオッチドックタイマ)にも2番が接続されているがこちらはディセーブル。
- GCLK_USB(USBインタフェース)にはクロックジェネレータの0番が接続されている(多分この0番が48MHz)
- 汎用シリアルデバイスSERCOM(プログラムでUART、I2Cなどにできる)用のGCLK_SERCOMx_CORE(x=0..5)にはクロックジェネレータの0番が接続されている。
- GCLK_ADC(ADコンバータ用)にもクロックジェネレータの0番が接続されている。
- 上記に記載されていない他のペリフェラルへはまだクロックは供給(イネーブル)されていない。
準備済のペリフェラルの初期化関数を使用すると該当のペリフェラルについては適宜イネーブルされるのだと思います。そもそもTinyGoで制御関数等がサポートされていない「マイナー」な周辺については自前でクロック設定からするもんだろ~と。まあ読み出しダンプできたから、書き込みして制御できるよね。ホントか?