主頁 | 自己紹介 | 日記 | 農業 | 台所 | 電算機 | | 本棚 | | Git

RP2040 SDKなし2 Clock、UART


: リセット解除の間違いを修正。

今回はClockとUARTを設定してパソコンに繋ぎ、キーボードからの入力をオウム返しするプログラムを作成する。

前回: RP2040 SDKなしでLチカ
ソースコード: git/ex2

動作環境

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設定の手順は以下の通り:

上の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

一つめのblx0x10001818(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.Sruntime.cといったファイルもhello_uartで本当に使われているものなのかもよく分からない。こんな煩雑なものは本当に必要なのかな。無駄に複雑にしてるだけとちゃうんかな。特に僕は勉強用に使ってるので、ソースコードの依存関係をもっと分かりやすくしてくれないと、内部でなにがどうなってるのか理解しにくい。何度か頑張って読もうとしたが、面白くないのでやめた。数百行のファイルをあっちからこっちからincludeしてるし、大文字ばかりの変数だらけで目が痛い。こんなものを扱えるというのはえらいええ頭してはるんやね。

参考