RP2040 SDKなし2 Clock、UART
: リセット解除の間違いを修正。
今回はClockとUARTを設定してパソコンに繋ぎ、キーボードからの入力をオウム返しするプログラムを作成する。
前回: RP2040 SDKなしでLチカ
ソースコード: git/ex2
動作環境
- Void Linux
- cross-arm-none-eabi-binutils-2.32_2
- GNU Make 4.4.1
- minicom version 2.7.1
- FT234X 超小型USBシリアル変換モジュール
Clock
リング発振回路
RP2040にはリング発振回路というのが内蔵されている。これは自分の出力を反転させようとするもので、不安定だが高速で消費電力の少ないクロックとして用いられる。RP2040は電源を入れると、このリング発振回路を動作用のクロックとして用いている。この発振回路の周波数は、チップの製造過程での誤差、動作時の電圧、動作温度によって変動するので、正確な周波数が必要な用途には向かない。
水晶発振子
秋月電子通商で購入したRP2040マイコンボードには外部クロックとして、12MHzの水晶発振子が付属する。水晶発振子はリング発振回路より電力を消費するが、より正確である。
PLL
水晶振動子を入力として、周波数を数倍にしたものを出力するもの。電気的な話はよく知らない。データシートの「2.18.2. Calcurating PLL parameters」によると、入力周波数をFREF
としたときの出力周波数は(FREF / REFDIV) × FBDIV / (POSTDIV1 × POSTDIV2)
となる。これらの変数はそれぞれ設定用のレジスタに値を保存することで変更できる。
UART
Universal Asynchronous Receiver/Transmitterの略。2本の線だけで通信できる。プロトコルは詳しく知らないが、rp2040がよしなにやってくれる。rp2040では同時に二個まで利用できる。どのGPIOピンを使うかもある程度自由に選べる。どのピンが使えるかはデータシートの「2.19.2. Function Select」に書かれている。今回はGPIO0とGPIO1を使う。パソコンとの接続には、秋月電子通商で売っているFT234X 超小型USBシリアル変換モジュールを使用した。UARTで接続するためのパソコン側のソフトウェアはminicomを使用した。僕の環境ではシリアル変換モジュールをパソコンにUSB接続すると、/dev/ttyUSB0
として認識されるので、
$ minicom -D /dev/ttyUSB0
とすると接続できる。
main.s
初期設定
後で見るように、UARTの動作には多分水晶発振子とPLLが必要なので、まずはそれを設定する。起動後、メインのプログラムが読み込まれるまでのboot2
は前回と同じものである。main.s
ではまず前回と同様に初期スタックポインタとエントリーポイントを定義する:
.section .vectors
vectors:
.word 0x20040000 // initial SP
.word (reset+1) // entry point
続いて利用するサブシステムのリセットを解除する。PLLとUARTが追加されている。今回使うUARTはUART0だけである。なお、UARTはclock_periが有効化されるまでリセット状態の解除が完了しないようなので、unreset_chkからは外してある:
.section .text
reset:
// unreset gpio, pll_sys, uart0
ldr r0, =(1 << 22 | 1 << 12 | 1 << 5) // uart0 | pll_sys | io_bank0
ldr r3, resets_base
ldr r1, atomic_clr
str r0, [r3, r1] // RESETS: RESET
mov r1, #1
lsl r1, #22
bic r0, r1 // uart stays in reset state until clock_peri is enabled
unreset_chk:
ldr r1, [r3, #0x8] // RESETS: RESET_DONE
bic r0, r1
bne unreset_chk
/* ... */
atomic_clr:
.word 0x00003000
resets_base:
.word 0x4000c000
GPIOの設定
次にGPIOの役割を設定する。前回はLEDを点滅させるためにGPIO25をSIOに設定したが、今回はGPIO0とGPIO1をUART0にする:
// set gpio functions
ldr r3, io_bank0_base
mov r0, #2 // uart0
mov r1, #0x4
str r0, [r3, r1] // IO_BANK0: GPIO0_CTRL
mov r1, #0xc
str r0, [r3, r1] // IO_BANK0: GPIO1_CTRL
/* ... */
io_bank0_base:
.word 0x40014000
Clockの設定
Clockの設定をする。まずは水晶発振子を起動する。水晶発振子は起動してから周波数が安定するまで少し時間がかかるようで、その間待たないといけない。この時間は1msあれば十分だとデータシートに書いている。この待ち時間はXOSC: STARTUPレジスタに、256サイクル単位で記述する。データシートによると初期のリング発振子は最大で12MHzなので、(12 * 10^6 * 1 * 10^-3) / 256 = 47
をこのレジスタにセットする。ところでデータシートではこの計算はリング発振子ではなく水晶発振子の周波数で書かれている。起動直後でまだ使えない水晶発振子の周波数を使うのはなんでやろ。SDKでもpico-sdk/src/rp2_common/hardware_xosc/xosc.c
で、
#define STARTUP_DELAY (((((XOSC_MHZ * MHZ) / 1000) + 128) / 256) * PICO_XOSC_STARTUP_DELAY_MULTIPLIER)
と定義されている(PICO_XOSC_STARTUP_DELAY_MULTIPLIERは1)。とりあえず47に設定しているが、試しに0や1にしても動いた。よくわからん。
待ち時間を設定したら発振子を起動する。XOSC: CTRLに起動用のコマンド的なものを入力し、周波数が安定するのを待つ。
以上を実装したのが以下のコード:
// setup xosc
ldr r3, xosc_base
mov r0, #47 // start up delay for 12MHz rosc (xosc?)
str r0, [r3, #0xc] // XOSC: STARTUP
ldr r0, =(0xfab << 12 | 0xaa0)
str r0, [r3, #0] // XOSC: CTRL
wait_xosc:
ldr r0, [r3, #0x4] // XOSC: STATUS
lsr r0, r0, #31 // STABLE bit
beq wait_xosc
/* ... */
xosc_base:
.word 0x40024000
PLLの設定
水晶発振子が起動できたので、次にPLLを設定する。CPUが133MHzまで対応しているので133MHzになるようにした。
PLLは入力となる振動(ここでは水晶発振子の振動)を加工して周波数を上げたり下げたりする。出力の周波数は以下の式で決まる:
(FREF / REFDIV) * FBDIV / (POSTDIV1 * POSTDIV2)
FREFは入力の周波数(ここでは12MHz)で、その他の変数はプログラマが設定できる。ただしデータシートによると(FREF / REFDIV)は5MHz以上でないといけないので、REFDIVは1である。また、FBDIVは16〜320、POSTDIV1とPOSTDIV2は1〜7で、POSTDIV1とPOSTDIV2に違う値を代入する場合、POSTDIV1に大きい方を入れたほうが消費電力が少なくなるとのことなので、133MHzにするには、FBDIV=133、POSTDIV1=6、POSTDIV=2とすればいい(POSTDIV1=4、POSTDIV2=3も可能だが、pico-sdkに付属するvcocalc.pyというスクリプトのコメントには、この2つの値の差が大きい方がいいと書いている)。
PLL設定の手順は、FBDIVの設定、PLLとVCOの起動、VOCが安定するまで待機、POSTDIV1とPOSTDIV2の設定、Post Dividerの起動、そして最後にシステムとUARTのクロックを今設定したPLLに変更、である。以上を実装したのが以下のコード:
// setup pll_sys 133MHz
ldr r3, pll_sys_base
// set feedback divider
mov r0, #133
str r0, [r3, #0x8] // PLL: FBDIV_INT
// power on pll and vco
ldr r0, =(1 << 5 | 1) // VCOPD | PD
ldr r1, atomic_clr
add r1, r1, #0x4
str r0, [r3, r1] // PLL: PWR
// wait vco to lock
wait_vco:
ldr r0, [r3, #0] // PLL: CS
lsl r0, r0, #31
beq wait_vco
// setup post dividers
ldr r0, =(4 << 16 | 3 << 12)
str r0, [r3, #0xc] // PLL: PRIM
// power on post divider
mov r0, #8 // POSTDIVPD
str r0, [r3, r1] // PLL: PWR
// set system clock clksrc_pll_sys
ldr r3, clocks_base
ldr r0, =(0x0 << 5 | 0x1)
str r0, [r3, #0x3c] // CLOCKS: CLK_SYS_CTRL
// enable clk_peri
mov r0, #1
lsl r0, r0, #11
str r0, [r3, #0x48] // CLOCKS: CLK_PERI_CTRL
/* ... */
atomic_clr:
.word 0x00003000
clocks_base:
.word 0x40008000
pll_sys_base:
.word 0x40028000
UARTの設定
データシートによるとUART設定の手順は以下の通り:
- リセットの解除
- clock_periの設定
- UARTの有効化
- FIFOの有効化
- 転送速度の設定
- フォーマットの設定
上の2つは既に終えている。残りの部分はこの順番どおりに設定しても動かなかった。C言語で書かれたサンプルを見ると、クロックを設定した後、転送速度の設定、UARTの有効化、FIFOの有効化の順になっている。そのとおりにすると動いた。理由はよく理解していないが、変数を設定してから起動するほうが素直ではある。
転送速度はminicomのデフォルトである115200 baudに設定する。データシート「4.2.7.1. Baud Rate Calculation」の計算式において、クロック周波数を125MHzから133MHzに変えて計算して、BRDI=72、BDRF=0.157(=10/64)となる。この数値をUART: UARTIBRD、UART: UARTFBRDレジスタにそれぞれ代入する。
UARTの有効化はUART: UARTCRレジスタのUARTENビットをセットすることで行う。C言語のサンプルでは同じレジスタのRXE、TXEビットもセットしているが、この2つはもともと1になっているのでほっといてよさそう。
FIFOの有効化はUART: UARTLCR_HレジスタのFENビットをセットすることで行う。また、同じレジスタの他のビットで、データーのフォーマットを設定できる。ここではminicomのデフォルトに合わせてWLENを8bitにする。
以上をまとめると以下のようになる:
// setup uart0
ldr r3, uart0_base
// set baudrate 115200
// BDRI = 72, BDRF = 0.157 (10 / 64)
mov r0, #72
str r0, [r3, #0x24] // UART: UARTIBRD
mov r0, #10
str r0, [r3, #0x28] // UART: UARTFBRD
// enable uart0
mov r0, #1 // UARTEN
ldr r1, atomic_set
add r1, r1, #0x30
str r0, [r3, r1] // UART: UARTCR
// enable FIFO and set format
ldr r0, =(3 << 5 | 1 << 4) // WLEN = 8, FEN = 1
str r0, [r3, #0x2c] // UART: UARTLCR_H
/* ... */
atomic_set:
.word 0x00002000
uart0_base:
.word 0x40034000
UARTの入出力
設定が終わったので実際にUARTの入出力を処理するコードを書く。まずUARTからの出力は、出力したいバイトをUART: UARTDRに書き込むことで行う。その際、書き込まれたデータは一時的に出力用FIFOに保持されるので、このFIFOが満杯でないことを確認する必要がある。FIFOの状態はUART: UARTFRレジスタで確認できる。このレジスタのTXFFの値が1であればデータを書き込めないので、0になるまで待機する。関数名はputbyte
にした。また出力したいデータはr0
レジスタにの下位8ビットに入れられているものとした。書き込めるデーターは8ビットだけなので、0xff
と論理積をとってから書き込んでいる:
putbyte:
ldr r3, uart0_base
mov r1, #1
lsl r1, r1, #5 // TXFF
txff:
ldr r2, [r3, #0x18] // UART: UARTFR
tst r1, r2
bne txff
mov r1, #0xff
and r0, r0, r1
str r0, [r3, #0] // UART: UARTDR
bx lr
/* ... */
uart0_base:
.word 0x40034000
入力はUART: UARTDRの下位8ビットを読むことで得られる。UARTからの入力は、一時的に入力用FIFOに保存される。このFIFOが空の状態でデータを読んでも意味がないので、FIFOが空でないことを確認する必要がある。これはUART: UARTFRレジスタのRXFEを読むことで確認できる。本来は入力があったときに割り込みを発生させて、それまではCPUを休ませるか別の処理をさせておくべきだが、とりあえずここではループでFIFOの状態を確認し続けている。関数名はgetbyte
にした。
読み込んだデータはr0
レジスタに保存している:
getbyte:
ldr r3, uart0_base
mov r1, #1
lsl r1, r1, #4 // RXFE
rxfe:
ldr r2, [r3, #0x18] // UART: UARTFR
tst r1, r2
bne rxfe
ldr r0, [r3, #0] // UART: UARTDR
mov r1, #0xff
and r0, r0, r1
bx lr
/* ... */
uart0_base:
.word 0x40034000
あとはこの2つの関数をループの中で交互に呼び出せば、オウム返しするだけのプログラムが完成する:
loop:
bl getbyte
bl putbyte
b loop
リング発振回路でUARTは動くんかな?
UARTの通信には正確なクロックが必要である。その為上ではclk_peri
として水晶発振子とPLLを用いた。ところがpico-examplesのhello_uartではmain()
関数で水晶発振子を設定していない。そこでリング発振回路を用いてみたのだが、どうもうまく通信できない。出力されている正確な周波数も分からないのであきらめることにした。オシロスコープなんていうものは持っていない。
pico-sdk
ところがどうも調べているとSDKを使った場合、デフォルトではクロック周波数は125MHzになっているらしい。どうやら水晶発振子もPLLもmain()
が呼ばれる前に設定されているようである。
pico-examplesのサンプルプログラムはビルドすると自動で逆アセンブリしたファイルを出力してくれる。これを見ると、最初の256バイトは前回説明したboot2のコードで、その後ろにベクターテーブルが続く。ベクターテーブルの最初は初期スタックポインタで、0x20042000
になっている。次はエントリーポイントで、0x100001f7
である:
10000100 <__VECTOR_TABLE>:
10000100: 20042000 .word 0x20042000
10000104: 100001f7 .word 0x100001f7
Thumbモードなので実際のエントリーポイントは1
引いた、0x100001f6
である。この場所ではまず自分のCPUIDを調べて、1
であれば待機状態に移行する。RP2040はデュアルコアである。起動直後はCPUIDが0
のコアだけで処理をして、CPUIDが1
のコアはプログラマが必要に応じて起動することになっている。このためCPUIDが1
のコアは起動してすぐに待機状態に入ることがデータシートに書かれている。しかしこの処理はユーザーの書いたプログラムじゃなくて内蔵ROMにある起動用プログラムが担当するみたいに書かれてるんやけど、なんでSDKではユーザープログラムの一部として組み込んでるんかな?
100001f6 <_reset_handler>:
100001f6: 481d ldr r0, [pc, #116] ; (1000026c <hold_non_core0_in_bootrom+0xe>)
100001f8: 6800 ldr r0, [r0, #0]
100001fa: 2800 cmp r0, #0
100001fc: d12f bne.n 1000025e <hold_non_core0_in_bootrom>
上のコードの最初のldr
は、0xd0000000
(M0PLUS: CPUIDレジスタ)をロードしている。最後の飛び先0x1000025e
はCPUIDが1
のCPUを待機させる処理である:
1000025e <hold_non_core0_in_bootrom>:
1000025e: 4809 ldr r0, [pc, #36] ; (10000284 <hold_non_core0_in_bootrom+0x26>)
10000260: f001 fb9c bl 1000199c <rom_func_lookup>
10000264: 4700 bx r0
10000266: 0000 .short 0x0000
/* ... */
10000284: 00005657 .word 0x00005657
内蔵フラッシュに書きこまれた関数を呼びだしている。呼びだしに使うコードは0x00005657
('W' | 'V' << 8
)である。データシートを見ると、この関数は_wait_for_vector()
という名前で、CPUIDが1のCPUを寝かしつけるのに使われると書いている。この部分のソースコードをpico-sdkで探すとpico-sdk/src/rp2_common/pico_standard_link/crt0.S
というのが見付かった:
$ find pico-sdk/src -type f | xargs grep -l _reset_handler
pico-sdk/src/rp2_common/pico_standard_link/crt0.S
このファイルによると:
// Only core 0 should run the C runtime startup code; core 1 is normally
// sleeping in the bootrom at this point but check to be sure
だそうである。やっぱり無駄やん。内蔵フラッシュのプログラムにバグがあってもこのコードのせいで見付かりにくくなってない?知らんけど。
続いて.data
領域と.bss
領域のコピー、初期化のようである。多分OSの本かなんかで習ったメモリマップの話:
100001fe: a40d add r4, pc, #52 ; (adr r4, 10000234 <data_cpy_table>)
10000200: cc0e ldmia r4!, {r1, r2, r3}
10000202: 2900 cmp r1, #0
10000204: d002 beq.n 1000020c <_reset_handler+0x16>
10000206: f000 f812 bl 1000022e <data_cpy>
1000020a: e7f9 b.n 10000200 <_reset_handler+0xa>
1000020c: 4918 ldr r1, [pc, #96] ; (10000270 <hold_non_core0_in_bootrom+0x12>)
1000020e: 4a19 ldr r2, [pc, #100] ; (10000274 <hold_non_core0_in_bootrom+0x16>)
10000210: 2000 movs r0, #0
10000212: e000 b.n 10000216 <bss_fill_test>
10000214 <bss_fill_loop>:
10000214: c101 stmia r1!, {r0}
10000216 <bss_fill_test>:
10000216: 4291 cmp r1, r2
10000218: d1fc bne.n 10000214 <bss_fill_loop>
最後にいろいろ呼びだす:
1000021a <platform_entry>:
1000021a: 4917 ldr r1, [pc, #92] ; (10000278 <hold_non_core0_in_bootrom+0x1a>)
1000021c: 4788 blx r1
1000021e: 4917 ldr r1, [pc, #92] ; (1000027c <hold_non_core0_in_bootrom+0x1e>)
10000220: 4788 blx r1
10000222: 4917 ldr r1, [pc, #92] ; (10000280 <hold_non_core0_in_bootrom+0x22>)
10000224: 4788 blx r1
10000226: be00 bkpt 0x0000
10000228: e7fd b.n 10000226 <platform_entry+0xc>
/* ... */
10000278: 10001819 .word 0x10001819
1000027c: 100002dd .word 0x100002dd
10000280: 10001909 .word 0x10001909
一つめのblx
は0x10001818
(runtime_init
)を、二つめは0x100002dc
(main
)を、最後のは0x10001908
(exit
)を、それぞれ呼んでいる。このruntime_init
はアセンブリでは分かりにくいのでソースコードを探してみると、以下のものが見付かった:
$ find pico-sdk/src -type f | xargs grep -l runtime_init
pico-sdk/src/rp2_common/pico_runtime/runtime.c
pico-sdk/src/rp2_common/pico_standard_link/crt0.S
pico-sdk/src/common/pico_sync/include/pico/mutex.h
最後のmutex.h
は関係なさそう。二つめのcrt0.S
は呼びだしてるだけ。一つめのruntime.c
が多分探しているものである。これを見るとまず各種周辺機器を一度リセットし、リセット状態を解除している。使わんやつも初期化してない?その後clocks_init()
を呼んでいる。この関数はpico-sdk/src/rp2_common/hardware_clocks/clocks.c
で定義されている。これを見ると、xosc_init()
を呼んで水晶発振子を初期化した後、clk_peri
を125MHzに設定している:
clock_configure(clk_peri,
0,
CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLK_SYS,
125 * MHZ,
125 * MHZ);
やっぱり水晶発振子じゃないとあかんのかな。
CMake
上ではビルドしたバイナリを逆アッセンブルして読んだ。わざわざこんなことをしなくてもMakefile読めばなにがどうなって最終生成物に辿りつくのか分かればいいのだが、そうもいかない。このSDKとpico-examplesにはビルドシステムとしてCMakeなるものが使われている。これがどうも複雑でよく分からない。勉強する気にもならん。上で見たcrt0.S
やruntime.c
といったファイルもhello_uart
で本当に使われているものなのかもよく分からない。こんな煩雑なものは本当に必要なのかな。無駄に複雑にしてるだけとちゃうんかな。特に僕は勉強用に使ってるので、ソースコードの依存関係をもっと分かりやすくしてくれないと、内部でなにがどうなってるのか理解しにくい。何度か頑張って読もうとしたが、面白くないのでやめた。数百行のファイルをあっちからこっちからinclude
してるし、大文字ばかりの変数だらけで目が痛い。こんなものを扱えるというのはえらいええ頭してはるんやね。