chakokuのブログ(rev4)

テック・コミック・DTM・・・ごくまれにチャリ

STM32C0でハンドアセンブルでLチカ

背景:おちゃめなマイコン、SMT32C011J4Mが気に入って、ハンドアセンブルでプログラムに取り組む
課題:前回ADDが実行できたので、次はLチカをやってみる
結論:いろいろあったが*1Lチカはできた。どれぐらいの早さでチカチカしているのか周波数を計測したところ6MHzであった(システムクロック48MHzの1/8)
詳細:

次は周辺I/Oを触ってみるということで、Lチカをやってみる。1番ピンは、PB7という名前になっている。

これはGPIO_Bの7番と理解して、GPIO関連のレジスタのベースの番地は、0x5000_0400

GPIOx_BSRRレジスタがあって、これは該当ビットに1を書くことでGPIOの出力を0/1に制御できる

GPIOx_BSRRレジスタのオフセットは、0x18なので、GPIOBの出力を0/1したかったら、0x5000_0400 + 0x18の番地にマップされている、GPIOB_BSRRの7bit(BS7)と7+16bit(BR7)に1を書きこめば1/0制御できるはず

gdbで特定メモリの番地に値を書くには、set命令を使う
以下でBS7をセット (PB7がHになるはず((GPIOブロックにクロックを供給しろとか言う仕様でなければ)))

set {int} 0x50000418 = 0x00000080

以下でBR7をセット (PB7がLになるはず)

set {int} 0x50000418 = 0x00800000

やってみたが、ポートはH/Lせず。操作する前からLEDがうっすら光っているのが怪しい。INPUTモードになっているというオチは?
GPIOBのレジスタをダンプすると以下

(gdb) x/11xw  0x50000400
0x50000400:     0x0000a000      0x000000c0      0x0000f000      0x00005000
0x50000410:     0x000000c0      0x000000c0      0x00000000      0x00000000
0x50000420:     0x66000000      0x00000000      0x00000000


0x5000_0400の値が 0x0000_a000なので、GPIOB_MODERと見比べると、MODE7が0b10、MODE6が0b10となっている。0b10とはAlternate functionなので、少なくとも普通のGPIOとしては設定されていない。よって、GPIOB_MODERのMODE7を0b01に設定する必要あり

以下でMODE7を0b01に設定、MODE6は設定を変えたくないので0b10を上書き

set {int} 0x50000400 = 0x00006000
(gdb) x/xw  0x50000400
0x50000400:     0x00006000

値が変わったことを確認。(alternate functionからOUTPUTに変えたのにLEDはうっすら光っている。失敗の予感)
再度以下でLチカをやってみた。一応点滅はする。が、、LEDはめちゃ暗い。リーク電流で光っているぐらいに暗い。

(gdb) set {int} 0x50000418 = 0x00000080
(gdb) set {int} 0x50000418 = 0x00800000
(gdb) set {int} 0x50000418 = 0x00000080
(gdb) set {int} 0x50000418 = 0x00800000

仕様書に書かれているリセット後デフォルト設定に頼って、GPIOのレジスタ値をほとんど設定していないのがまずいのかも。
■追記
この後間違ってマイコンの電源を抜いてしまってからまったく思うような動きにならなくなった。なぜだろうか
(正確にはLEDにつながっているGNDと思って、試しに3.3Vにつないだ。それはマイコンのGNDだった。この時、LEDは明々と光った。GPIOのブロックを壊してしまった?)
■追記
マイコンは2つ買っていたので、もう一つの方を基板に貼り付けて動かしてみた。結果は以下

(gdb) x/11xw  0x50000400
0x50000400:     0x0000a000      0x000000c0      0x0000f000      0x00005000
0x50000410:     0x000000c0      0x00000000      0x00000000      0x00000000
0x50000420:     0x66000000      0x00000000      0x00000000

初期値はAlternativeになっていると。OUTPUTに変更

(gdb) set {int} 0x50000400 = 0x00006000
(gdb) x/11xw  0x50000400
0x50000400:     0x00006000      0x000000c0      0x0000f000      0x00005000
0x50000410:     0x00000040      0x00000000      0x00000000      0x00000000
0x50000420:     0x66000000      0x00000000      0x00000000

以下でLHしてみる。やはりうっすら光っている。逆流しているのか?と思うぐらいに薄い

(gdb) set {int} 0x50000418 = 0x00000080
(gdb) set {int} 0x50000418 = 0x00800000
(gdb) set {int} 0x50000418 = 0x00000080

次また同じ絵が撮れるか分からないので記念に撮影

GPIOのレジスタマップのうち、どれかが思った値になっていないと思われる。初期値に頼らず、正しい値にすれば動くと思われる。全てのビットを突き合わせる必要あり

■追記
とりあえずリセット初期値を設定かと思って、以下の設定したらマイコンが暴走したような気がする(OpenOCDは通信エラーが出た)

set {int} 0x50000404 = 0x00000000

Hに設定した時の電圧は1.7Vで、Lに設定した時の電圧は0Vであった。なんでこんな中途半端(3.3V/2)な電圧か??と思っていたのだが、レジスタを再確認すると、GPIOB_OTYPERにより、GPIO_Bの7番はOpenDrainに設定されていた。だから、Lの時はGNDに接続されて0Vになるが、Hの時はHi-Zなのではなかろうか。だとすると、PullUpすれば点滅するはず。あるいは、レジスタをOpenDrainからPushPullに変更すると。いずれかの方法で正しく点滅すると期待。
■追記
回路に手を入れて、Pin1とLEDの間に3.3KのPullUpを追加、この状態で、以下の作業を行ったところ、LEDの点灯、消灯を確認できた

(gdb) set {int} 0x50000400 = 0x00006000
(gdb) x/11xw  0x50000400
0x50000400:     0x00006000      0x000000c0      0x0000f000      0x00005000
0x50000410:     0x00000040      0x00000000      0x00000000      0x00000000
0x50000420:     0x66000000      0x00000000      0x00000000
(gdb) set {int} 0x50000418 = 0x00000080
(gdb) set {int} 0x50000418 = 0x00800000

次はPull抵抗をつけずに、Pinの仕様をOpen Drainから、Push Pullに変えて、どうなるかを確認する。
現在は0xc0なので、OT7, OT6がOpen Drainになっている。

(gdb) x/xw  0x50000404
0x50000404:     0x000000c0

OT6は変更したくないので、0x40を書き込む。

set {int} 0x50000404 = 0x00000040

設定後確認

(gdb) x/xw  0x50000404
0x50000404:     0x00000040

LEDチカチカさせてみる

set {int} 0x50000418 = 0x00000080
set {int} 0x50000418 = 0x00800000

LEDは普通に明るく光る<->消える、となった。ここまではgdbから手動でGPIOレジスタを操作していたので、これを機械語でやりたい。SRAM領域に手動でアセンブルした機械語を書きこんで、実行させたい。機械語によるLチカなので、SystemClockが48MHzなら1/5~1/6ぐらいの速度で回ると期待。(8MHzでLチカになるのでは)
ただ、ARMの場合、レジスタへのイミディエートMOVEが8bit長らしく、今回のような32bit長だと4回MOVEするか、何か手を考えないといけない。
二―モックで言うところの、以下を使ってGPIOのレジスタに定数を書き込む。右側のコメントは機械語のHEX値

STR  R1, [R0]    # 6001
STR  R2, [R0]    # 6002

上記プログラムは、R0:GPIOレジスタの番地、R1:GPIOをHにする即値、R2:GPIOをLにする即値が入っているのを前提としている。
これをRAMの先頭から、gdbで書き込む。逆アセンブルすると以下。正しい機械語が書かれていると判断。

(gdb) x/2i 0x20000000
   0x20000000:  str     r1, [r0, #0]
   0x20000002:  str     r2, [r0, #0]

本来のプログラムなら、レジスタに書き込む32bitの即値(Immediate value)の設定もプログラム内で実行すべきだが、あまりに手間なので、gdbのコマンドでレジスタに値を書き込む(ここはチート)

(gdb) i r
r0             0x50000418          1342178328
r1             0x80                128
r2             0x800000            8388608
      *略*

pcを0x20000000に設定して一行ずつ走らせると、点灯->消灯になるはず。以下の実行により点灯、消灯となった。

(gdb) set $pc = 0x20000000
(gdb) ni
0x20000002 in ?? ()
(gdb) ni
0x20000004 in ?? ()

これに分岐を加えると、HLを繰り返して高速な点滅が可能になる。BALは上位5bitが11100、下位11bit(n11)で命令数を指定するのだが、、若い番地に飛ぶのにどうしたらいいのか? n11は符号付数値なのか??

仮に符号付だとして、-1となるように、 機械語として、0xE7を0x2000006に書き込んでみる

(gdb) set {short} 0x20000004 = 0xe7ff
(gdb) x/3i 0x20000000
   0x20000000:  str     r1, [r0, #0]
   0x20000002:  str     r2, [r0, #0]
=> 0x20000004:  b.n     0x20000006

次のアドレスに飛ぶだけであった。マイナス度合いが足りない?

(gdb) set {short} 0x20000004 = 0xe7fc
(gdb) x/3i 0x20000000
   0x20000000:  str     r1, [r0, #0]
   0x20000002:  str     r2, [r0, #0]
=> 0x20000004:  b.n     0x20000000

0x7FCとすると、4番地から0番地まで戻るようであった。あとから時間をかけて理解するとして、、これでループするのか、ni(next instruction?)を何度か打って確認する。
以下の通り、0番地->2番地-> 4番地->0番地を繰り返している(長くなるので上位アドレスは略)

(gdb) ni
0x20000000 in ?? ()
(gdb) ni
0x20000002 in ?? ()
(gdb) ni
0x20000004 in ?? ()
(gdb) ni
0x20000000 in ?? ()
(gdb) ni
0x20000002 in ?? ()
(gdb) ni
0x20000004 in ?? ()
(gdb) ni
0x20000000 in ?? ()
(gdb) ni
0x20000002 in ?? ()
(gdb) ni
0x20000004 in ?? ()
(gdb) ni
0x20000000 in ?? ()

これで、c(continue)を打てばずっと回るはず。。LEDは光り続けている(単に暴走しているだけ?)
Ctrl-Cで止めてPCを見ると、4番地(0x2000_0004)なのでOK

(gdb) c
Continuing.
^C
Program received signal SIGINT, Interrupt.
0x20000004 in ?? ()
(gdb) i r
r0             0x50000418          1342178328
r1             0x80                128
r2             0x800000            8388608
r3             0x66000000          1711276032
r4             0x20000360          536871776
r5             0x40005400          1073763328
r6             0xffffffff          -1
r7             0xffffffff          -1
r8             0xffffffff          -1
r9             0xffffffff          -1
r10            0xffffffff          -1
r11            0xffffffff          -1
r12            0xffffffff          -1
sp             0x20000d80          0x20000d80
lr             0x1fff0cc1          536808641
pc             0x20000004          0x20000004
xpsr           0x1000000           16777216
msp            0x20000d80          0x20000d80
psp            0xfffffffc          0xfffffffc
primask        0x1                 1
basepri        0x0                 0
faultmask      0x0                 0
control        0x0                 0

再度cで走らせて、オシロでどれぐらいの早さでループしているかを確認
以下の通り、山の間隔は164nsで、周波数は6MHzであった。システムクロックが48MHzなので、1/8まで速度が落ちている。渋めに見積もって(?)8MHzかと思っていたけど、さらに下がって6MHzであった。
マシンサイクルが示されていて、ブランチは3サイクル、ストアは2サイクルかかると。今回のプログラムはストア2回、ブランチ1回なので、計算上は、7サイクルかかるはずだが、+1どこかでかかっているということか。。

ループ実行までできたけど、全て手打ちで再現性が低い。次回も同じことができると思えないので、今日の最終形をまとめる

# set value to registers
set $r0 = 0x50000418
set $r1 = 0x00000080
set $r2 = 0x00800000

# write machine instructoins to SRAM
set {short} 0x20000000 = 0x6001
set {short} 0x20000002 = 0x6002
set {short} 0x20000004 = 0xe7ff

# check assemble code
x/3i 0x20000000

# set PC
set $pc = 0x20000000

# continue
c

なお、上記プログラムが正常に動作するには、gdbのメモリ操作コマンドを使って、GPIOのレジスタにおいて、GPB7をOUTPUTにして、PUSH PULLに設定する必要あり

■追記
機械語一覧に分岐の数の数え方が書かれている。命令差分に対してさらに-2とある。2つ前に戻るには命令差分としては-2、さらに-2するから-4と。0x7FCは11bit長の符号付き整数とすると、-4。一応つじつまは合う。

■参考URL
Cortex-M0 ARMマシン語表(抜粋)

*1:仕様書斜め読みというが主な原因で