www.mtkn.jp

Manuscripts for my personal webpage.
git clone https://git.mtkn.jp/www.mtkn.jp
Log | Files | Refs | LICENSE

commit f9361c9bfb2739751844a44b44ef4a4407a02a70
parent a881f9efd3e9144d0d447a0dd12a69fb259acdf2
Author: Matsuda Kenji <info@mtkn.jp>
Date:   Sat, 24 Feb 2024 08:14:23 +0900

publish rp2040_2

Diffstat:
Mdata/weblog | 11+++++++++++
Mman/computer/index.html | 1+
Aman/computer/rp2040_2.html | 589+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dman/draft/rp2040_2.html | 579-------------------------------------------------------------------------------
Mman/index.html | 1+
Mpub/computer/index.html | 1+
Apub/computer/rp2040_2.html | 426+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpub/draft/rp2040_2.html | 421-------------------------------------------------------------------------------
Mpub/index.html | 1+
Mpub/rss.xml | 1651+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mpub/sitemap.xml | 7++++---
11 files changed, 2059 insertions(+), 1629 deletions(-)

diff --git a/data/weblog b/data/weblog @@ -138,3 +138,14 @@ 1707922800 /index.html 1707922800 /computer/index.html 1708095600 /books/index.html +1708527600 /computer/rp2040_2.html +1708527600 /computer/index.html +1708527600 /computer/rp2040_2.html +1708527600 /index.html +1708527600 /computer/rp2040_2.html +1708527600 /index.html +1708700400 /computer/rp2040_1.html +1708700400 /computer/rp2040_1.html +1708700400 /computer/rp2040_1.html +1708700400 /computer/rp2040_1.html +1708700400 /computer/rp2040_1.html diff --git a/man/computer/index.html b/man/computer/index.html @@ -5,6 +5,7 @@ Small is beautiful. <h2>RP2040</h2> <ul> <li><a href="rp2040_1.html">RP2040 SDKなしでLチカ</a></li> +<li><a href="rp2040_2.html">RP2040 SDKなし2 Clock、UART</a></li> </ul> <h2>Xlib</h2> <ul> diff --git a/man/computer/rp2040_2.html b/man/computer/rp2040_2.html @@ -0,0 +1,589 @@ +<h1>RP2040 SDKなし2 Clock、UART</h1> +<time>2024-02-22</time> +<p> +今回はClockとUARTを設定してパソコンに繋ぎ、\ +キーボードからの入力をオウム返しするプログラムを作成する。 +<p> +</p> +前回: <a href="rp2040_1.html">RP2040 SDKなしでLチカ</a><br> +ソースコード: <a href="https://git.mtkn.jp/rp2040">git</a>/ex2 +</p> + +<h2>動作環境</h2> +<ul> +<li>Void Linux + <ul> + <li>cross-arm-none-eabi-binutils-2.32_2</li> + <li>GNU Make 4.4.1</li> + <li>minicom version 2.7.1</li> + </ul> +</li> +<li><a href="https://akizukidenshi.com/catalog/g/g108461/">FT234X 超小型USBシリアル変換モジュール</a> +</li> +</ul> + +<h2>Clock</h2> + +<h3>リング発振回路</h3> +<p>\ +RP2040にはリング発振回路というのが内蔵されている。\ +これは自分の出力を反転させようとするもので、\ +不安定だが高速で消費電力の少ないクロックとして用いられる。\ +RP2040は電源を入れると、このリング発振回路を動作用のクロックとして\ +用いている。この発振回路の周波数は、チップの製造過程での誤差、動作時の電圧、\ +動作温度によって変動するので、正確な周波数が必要な用途には向かない。\ +</p> + +<h3>水晶発振子</h3> +<p>\ +秋月電子通商で購入したRP2040マイコンボードには外部クロックとして、\ +12MHzの水晶発振子が付属する。水晶発振子はリング発振回路より電力を消費するが、\ +より正確である。\ +</p> + +<h3>PLL</h3> +<p>\ +水晶振動子を入力として、周波数を数倍にしたものを出力するもの。\ +電気的な話はよく知らない。データシートの「2.18.2. Calcurating PLL parameters」\ +によると、入力周波数を<code>FREF</code>としたときの出力周波数は\ +<code>(FREF / REFDIV) × FBDIV / (POSTDIV1 × POSTDIV2)</code>となる。\ +これらの変数はそれぞれ設定用のレジスタに値を保存することで変更できる。\ +</p> + +<h2>UART</h2> +<p> +Universal Asynchronous Receiver/Transmitterの略。\ +2本の線だけで通信できる。\ +プロトコルは詳しく知らないが、rp2040がよしなにやってくれる。\ +rp2040では同時に二個まで利用できる。\ +どのGPIOピンを使うかもある程度自由に選べる。\ +どのピンが使えるかはデータシートの「2.19.2. Function Select」に書かれている。\ +今回はGPIO0とGPIO1を使う。\ +パソコンとの接続には、秋月電子通商で売っている\ +<a href="https://akizukidenshi.com/catalog/g/g108461/">FT234X 超小型USBシリアル変換モジュール</a>\ +を使用した。\ +UARTで接続するためのパソコン側のソフトウェアはminicomを使用した。\ +僕の環境ではシリアル変換モジュールをパソコンにUSB接続すると、\ +<code>/dev/ttyUSB0</code>として認識されるので、\ +</p> +<pre><code>\ +$ minicom -D /dev/ttyUSB0 +</code></pre> +<p> +とすると接続できる。 +</p> + +<h2>main.s</h2> +<h3>初期設定</h3> +<p> +後で見るように、UARTの動作には多分水晶発振子とPLLが必要なので、\ +まずはそれを設定する。\ +起動後、メインのプログラムが読み込まれるまでの<code>boot2</code>は\ +前回と同じものである。\ +<code>main.s</code>ではまず前回と同様に初期スタックポインタとエントリーポイント\ +を定義する: +</p> +<pre><code> .section .vectors +vectors: + .word 0x20040000 // initial SP + .word (reset+1) // entry point +</code></pre> +<p> +続いて利用するサブシステムのリセットを解除する。\ +PLLとUARTが追加されている。\ +今回使うUARTはUART0だけである: +</p> +<pre><code> .section .text +reset: + // unreset gpio, pll_sys, uart0 + ldr r0, =(1 &lt;&lt; 22 | 1 &lt;&lt; 12 | 1 &lt;&lt; 5) // uart0 | pll_sys | io_bank0 + ldr r3, resets_base + ldr r1, atomic_clr + str r0, [r3, r1] // RESETS: RESET +unreset_chk: + ldr r1, [r3, #0x8] // RESETS: RESET_DONE + tst r0, r1 + beq unreset_chk + +/* ... */ + +atomic_clr: + .word 0x00003000 +resets_base: + .word 0x4000c000 +</code></pre> + +<h3>GPIOの設定</h3> +<p> +次にGPIOの役割を設定する。\ +前回はLEDを点滅させるためにGPIO25をSIOに設定したが、\ +今回はGPIO0とGPIO1をUART0にする: +</p> +<pre><code> // 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 +</code></pre> + +<h3>Clockの設定</h3> +<p> +Clockの設定をする。\ +まずは水晶発振子を起動する。\ +水晶発振子は起動してから周波数が安定するまで少し時間がかかるようで、\ +その間待たないといけない。\ +この時間は1msあれば十分だとデータシートに書いている。\ +この待ち時間はXOSC: STARTUPレジスタに、256サイクル単位で記述する。\ +データシートによると初期のリング発振子は最大で12MHzなので、\ +<code>(12 * 10^6 * 1 * 10^-3) / 256 = 47</code>をこのレジスタにセットする。\ +ところでデータシートではこの計算はリング発振子ではなく\ +水晶発振子の周波数で書かれている。\ +起動直後でまだ使えない水晶発振子の周波数を使うのはなんでやろ。\ +SDKでも<code>pico-sdk/src/rp2_common/hardware_xosc/xosc.c</code>で、 +</p> +<pre><code>\ +#define STARTUP_DELAY (((((XOSC_MHZ * MHZ) / 1000) + 128) / 256) * PICO_XOSC_STARTUP_DELAY_MULTIPLIER) +</code></pre> +<p> +と定義されている(PICO_XOSC_STARTUP_DELAY_MULTIPLIERは1)。\ +とりあえず47に設定しているが、試しに0や1にしても動いた。よくわからん。\ +</p> +<p> +待ち時間を設定したら発振子を起動する。\ +XOSC: CTRLに起動用のコマンド的なものを入力し、周波数が安定するのを待つ。\ +</p> +<p> +以上を実装したのが以下のコード: +</p> +<pre><code> // 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 +</code></pre> + +<h3>PLLの設定</h3> +<p> +水晶発振子が起動できたので、次にPLLを設定する。\ +CPUが133MHzまで対応しているので133MHzになるようにした。\ +</p> +<p> +PLLは入力となる振動(ここでは水晶発振子の振動)を加工して\ +周波数を上げたり下げたりする。\ +出力の周波数は以下の式で決まる: +</p> +<pre>(FREF / REFDIV) * FBDIV / (POSTDIV1 * POSTDIV2)</pre> +<p> +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つの値の差が大きい方がいいと書いている)。 +</p> +<p> +PLL設定の手順は、FBDIVの設定、PLLとVCOの起動、VOCが安定するまで待機、\ +POSTDIV1とPOSTDIV2の設定、Post Dividerの起動、そして最後にシステムとUARTのクロック\ +を今設定したPLLに変更、である。\ +以上を実装したのが以下のコード: +</p> +<pre><code> // 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 &lt;&lt; 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 &lt;&lt; 16 | 3 &lt;&lt; 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 &lt;&lt; 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 +</code></pre> + +<h3>UARTの設定</h3> +<p> +データシートによるとUART設定の手順は以下の通り: +</p> +<ul> +<li>リセットの解除</li> +<li>clock_periの設定</li> +<li>UARTの有効化</li> +<li>FIFOの有効化</li> +<li>転送速度の設定</li> +<li>フォーマットの設定</li> +</ul> +<p> +上の2つは既に終えている。\ +残りの部分はこの順番どおりに設定しても動かなかった。\ +C言語で書かれたサンプルを見ると、クロックを設定した後、\ +転送速度の設定、UARTの有効化、FIFOの有効化の順になっている。\ +そのとおりにすると動いた。\ +理由はよく理解していないが、変数を設定してから起動するほうが素直ではある。\ +</p> +<p> +転送速度はminicomのデフォルトである115200 baudに設定する。\ +データシート「4.2.7.1. Baud Rate Calculation」の計算式において、\ +クロック周波数を125MHzから133MHzに変えて計算して、\ +BRDI=72、BDRF=0.157(=10/64)となる。\ +この数値をUART: UARTIBRD、UART: UARTFBRDレジスタにそれぞれ代入する。 +</p> +<p> +UARTの有効化はUART: UARTCRレジスタのUARTENビットをセットすることで行う。\ +C言語のサンプルでは同じレジスタのRXE、TXEビットもセットしているが、\ +この2つはもともと1になっているのでほっといてよさそう。\ +</p> +<p> +FIFOの有効化はUART: UARTLCR_HレジスタのFENビットをセットすることで行う。\ +また、同じレジスタの他のビットで、データーのフォーマットを設定できる。\ +ここではminicomのデフォルトに合わせてWLENを8bitにする。\ +</p> +<p> +以上をまとめると以下のようになる: +</p> +<pre><code> // 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 &lt;&lt; 5 | 1 &lt;&lt; 4) // WLEN = 8, FEN = 1 + str r0, [r3, #0x2c] // UART: UARTLCR_H + +/* ... */ + +atomic_set: + .word 0x00002000 +uart0_base: + .word 0x40034000 +</code></pre> + +<h3>UARTの入出力</h3> +<p> +設定が終わったので実際にUARTの入出力を処理するコードを書く。\ +まずUARTからの出力は、出力したいバイトをUART: UARTDRに書き込むことで\ +行う。\ +その際、書き込まれたデータは一時的に出力用FIFOに保持されるので、このFIFO\ +が満杯でないことを確認する必要がある。\ +FIFOの状態はUART: UARTFRレジスタで確認できる。\ +このレジスタのTXFFの値が1であればデータを書き込めないので、\ +0になるまで待機する。\ +関数名は<code>putbyte</code>にした。\ +また出力したいデータは<code>r0</code>レジスタにの下位8ビットに\ +入れられているものとした。\ +書き込めるデーターは8ビットだけなので、<code>0xff</code>と論理積をとってから\ +書き込んでいる: +</p> +<pre><code>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 +</code></pre> + +<p> +入力はUART: UARTDRの下位8ビットを読むことで得られる。\ +UARTからの入力は、一時的に入力用FIFOに保存される。\ +このFIFOが空の状態でデータを読んでも意味がないので、\ +FIFOが空でないことを確認する必要がある。\ +これはUART: UARTFRレジスタのRXFEを読むことで確認できる。\ +本来は入力があったときに割り込みを発生させて、それまでは\ +CPUを休ませるか別の処理をさせておくべきだが、とりあえずここでは\ +ループでFIFOの状態を確認し続けている。\ +関数名は<code>getbyte</code>にした。 +読み込んだデータは<code>r0</code>レジスタに保存している:\ +</p> +<pre><code>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 +</code></pre> +<p> +あとはこの2つの関数をループの中で交互に呼び出せば、\ +オウム返しするだけのプログラムが完成する: +</p> +<pre><code>loop: + bl getbyte + bl putbyte + b loop +</code></pre> + +<h2>リング発振回路でUARTは動くんかな?</h2> +<p>\ +UARTの通信には正確なクロックが必要である。\ +その為上では<code>clk_peri</code>として水晶発振子とPLLを用いた。\ +ところがpico-examplesのhello_uartでは<code>main()</code>関数で\ +水晶発振子を設定していない。そこでリング発振回路を用いてみたのだが、\ +どうもうまく通信できない。出力されている正確な周波数も分からないので\ +あきらめることにした。オシロスコープなんていうものは持っていない。\ +</p> + +<h3>pico-sdk</h3> +<p> +ところがどうも調べているとSDKを使った場合、\ +デフォルトではクロック周波数は125MHzになっているらしい。\ +どうやら水晶発振子もPLLも<code>main()</code>が呼ばれる前に\ +設定されているようである。\ +</p> +<p> +pico-examplesのサンプルプログラムはビルドすると自動で逆アセンブリした\ +ファイルを出力してくれる。これを見ると、最初の256バイトは前回説明した\ +boot2のコードで、その後ろにベクターテーブルが続く。\ +ベクターテーブルの最初は初期スタックポインタで、<code>0x20042000</code>\ +になっている。次はエントリーポイントで、<code>0x100001f7</code>である:\ +</p> +<pre><code>\ +10000100 &lt;__VECTOR_TABLE&gt;: +10000100: 20042000 .word 0x20042000 +10000104: 100001f7 .word 0x100001f7 +</code></pre> +<p> +Thumbモードなので実際のエントリーポイントは<code>1</code>引いた、\ +<code>0x100001f6</code>である。この場所ではまず自分のCPUIDを調べて、\ +<code>1</code>であれば待機状態に移行する。\ +RP2040はデュアルコアである。起動直後はCPUIDが<code>0</code>のコアだけで処理をして、\ +CPUIDが<code>1</code>のコアはプログラマが必要に応じて起動することになっている。\ +このためCPUIDが<code>1</code>のコアは起動してすぐに待機状態に入ることがデータシート\ +に書かれている。しかしこの処理はユーザーの書いたプログラムじゃなくて\ +内蔵ROMにある起動用プログラムが担当するみたいに書かれてるんやけど、\ +なんでSDKではユーザープログラムの一部として組み込んでるんかな? +</p> +<pre><code>\ +100001f6 &lt;_reset_handler&gt;: +100001f6: 481d ldr r0, [pc, #116] ; (1000026c &lt;hold_non_core0_in_bootrom+0xe&gt;) +100001f8: 6800 ldr r0, [r0, #0] +100001fa: 2800 cmp r0, #0 +100001fc: d12f bne.n 1000025e &lt;hold_non_core0_in_bootrom&gt; +</code></pre> +<p>\ +上のコードの最初の<code>ldr</code>は、<code>0xd0000000</code>\ +(M0PLUS: CPUIDレジスタ)をロードしている。\ +最後の飛び先<code>0x1000025e</code>はCPUIDが<code>1</code>のCPUを待機させる処理\ +である:\ +</p> +<pre><code>\ +1000025e &lt;hold_non_core0_in_bootrom&gt;: +1000025e: 4809 ldr r0, [pc, #36] ; (10000284 &lt;hold_non_core0_in_bootrom+0x26&gt;) +10000260: f001 fb9c bl 1000199c &lt;rom_func_lookup&gt; +10000264: 4700 bx r0 +10000266: 0000 .short 0x0000 +/* ... */ +10000284: 00005657 .word 0x00005657 +</code></pre> +<p>\ +内蔵フラッシュに書きこまれた関数を呼びだしている。呼びだしに使うコードは\ +<code>0x00005657</code>(<code>'W' | 'V' &lt;&lt; 8</code>)である。\ +データシートを見ると、この関数は<code>_wait_for_vector()</code>という名前で、\ +CPUIDが1のCPUを寝かしつけるのに使われると書いている。\ +この部分のソースコードをpico-sdkで探すと\ +<code>pico-sdk/src/rp2_common/pico_standard_link/crt0.S</code>\ +というのが見付かった:\ +</p> +<pre><code>\ +$ find pico-sdk/src -type f | xargs grep -l _reset_handler +pico-sdk/src/rp2_common/pico_standard_link/crt0.S +</code></pre> +<p>\ +このファイルによると: +</p> +<pre><code> // 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 +</code></pre> +<p>\ +だそうである。やっぱり無駄やん。内蔵フラッシュのプログラムにバグがあっても\ +このコードのせいで見付かりにくくなってない?知らんけど。\ +</p> + +<p>\ +続いて<code>.data</code>領域と<code>.bss</code>領域のコピー、初期化のようである。\ +多分OSの本かなんかで習ったメモリマップの話:\ +</p> +<pre><code>\ +100001fe: a40d add r4, pc, #52 ; (adr r4, 10000234 &lt;data_cpy_table&gt;) +10000200: cc0e ldmia r4!, {r1, r2, r3} +10000202: 2900 cmp r1, #0 +10000204: d002 beq.n 1000020c &lt;_reset_handler+0x16&gt; +10000206: f000 f812 bl 1000022e &lt;data_cpy&gt; +1000020a: e7f9 b.n 10000200 &lt;_reset_handler+0xa&gt; +1000020c: 4918 ldr r1, [pc, #96] ; (10000270 &lt;hold_non_core0_in_bootrom+0x12&gt;) +1000020e: 4a19 ldr r2, [pc, #100] ; (10000274 &lt;hold_non_core0_in_bootrom+0x16&gt;) +10000210: 2000 movs r0, #0 +10000212: e000 b.n 10000216 &lt;bss_fill_test&gt; + +10000214 &lt;bss_fill_loop&gt;: +10000214: c101 stmia r1!, {r0} + +10000216 &lt;bss_fill_test&gt;: +10000216: 4291 cmp r1, r2 +10000218: d1fc bne.n 10000214 &lt;bss_fill_loop&gt; +</code></pre> + +<p>\ +最後にいろいろ呼びだす:\ +</p> +<pre><code>\ +1000021a &lt;platform_entry&gt;: +1000021a: 4917 ldr r1, [pc, #92] ; (10000278 &lt;hold_non_core0_in_bootrom+0x1a&gt;) +1000021c: 4788 blx r1 +1000021e: 4917 ldr r1, [pc, #92] ; (1000027c &lt;hold_non_core0_in_bootrom+0x1e&gt;) +10000220: 4788 blx r1 +10000222: 4917 ldr r1, [pc, #92] ; (10000280 &lt;hold_non_core0_in_bootrom+0x22&gt;) +10000224: 4788 blx r1 +10000226: be00 bkpt 0x0000 +10000228: e7fd b.n 10000226 &lt;platform_entry+0xc&gt; +/* ... */ +10000278: 10001819 .word 0x10001819 +1000027c: 100002dd .word 0x100002dd +10000280: 10001909 .word 0x10001909 +</code></pre> +<p>\ +一つめの<code>blx</code>は<code>0x10001818</code>(<code>runtime_init</code>)を、\ +二つめは<code>0x100002dc</code>(<code>main</code>)を、\ +最後のは<code>0x10001908</code>(<code>exit</code>)を、\ +それぞれ呼んでいる。\ +この<code>runtime_init</code>はアセンブリでは分かりにくいので\ +ソースコードを探してみると、以下のものが見付かった:\ +</p> +<pre><code>\ +$ 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 +</code></pre> +<p>\ +最後の<code>mutex.h</code>は関係なさそう。\ +二つめの<code>crt0.S</code>は呼びだしてるだけ。\ +一つめの<code>runtime.c</code>が多分探しているものである。\ +これを見るとまず各種周辺機器を一度リセットし、リセット状態を解除している。\ +使わんやつも初期化してない?\ +その後<code>clocks_init()</code>を呼んでいる。\ +この関数は<code>pico-sdk/src/rp2_common/hardware_clocks/clocks.c</code>\ +で定義されている。これを見ると、<code>xosc_init()</code>を呼んで\ +水晶発振子を初期化した後、<code>clk_peri</code>を125MHzに設定している:\ +</p> +<pre><code> clock_configure(clk_peri, + 0, + CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLK_SYS, + 125 * MHZ, + 125 * MHZ); +</code></pre> +<p>\ +やっぱり水晶発振子じゃないとあかんのかな。\ +</p> + +<h2>CMake</h2> +<p>\ +上ではビルドしたバイナリを逆アッセンブルして読んだ。\ +わざわざこんなことをしなくてもMakefile読めばなにがどう\ +なって最終生成物に辿りつくのか分かればいいのだが、そうもいかない。\ +このSDKとpico-examplesにはビルドシステムとしてCMakeなるものが使われている。\ +これがどうも複雑でよく分からない。勉強する気にもならん。\ +上で見た<code>crt0.S</code>や<code>runtime.c</code>といったファイルも\ +<code>hello_uart</code>で本当に使われているものなのかもよく分からない。\ +こんな煩雑なものは本当に必要なのかな。無駄に複雑にしてるだけとちゃうんかな。\ +特に僕は勉強用に使ってるので、ソースコードの依存関係をもっと\ +分かりやすくしてくれないと、内部でなにがどうなってるのか理解しにくい。\ +何度か頑張って読もうとしたが、面白くないのでやめた。\ +数百行のファイルをあっちからこっちから<code>include</code>してるし、\ +大文字ばかりの変数だらけで目が痛い。\ +こんなものを扱えるというのはえらいええ頭してはるんやね。\ +</p> + + +<h2>参考</h2> +<ul> +<li> +<a href="https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf">RP2040 Datasheet.Raspberry Pi Foundation</a> +</li> +<li> +<a href="https://github.com/raspberrypi/pico-sdk">pico-sdk.github</a> +</li> +<li> +<a href="https://developer.arm.com/documentation/ddi0419/c/">ARMv6-M Architecture Reference Manual</a> +</li> +<li> +<a href="https://ja.wikipedia.org/wiki/%E3%83%AA%E3%83%B3%E3%82%B0%E3%83%BB%E3%82%AA%E3%82%B7%E3%83%AC%E3%83%BC%E3%82%BF">リング・オシレータ.Wikipedia</a> +</li> +<li> +<a href="https://www5.epsondevice.com/ja/information/technical_info/osc.html">水晶発振器とは? 原理と仕組み、水晶振動子との違い、選び方のポイントを解説.エプソン水晶デバイス</a> +</li> +</ul> diff --git a/man/draft/rp2040_2.html b/man/draft/rp2040_2.html @@ -1,579 +0,0 @@ -<h1>RP2040 SDKなし2 Clock, UART</h1> -<time>2023-05-10</time> -<p> -今回はClockとUARTを設定してパソコンに繋ぎ、\ -キーボードからの入力をオウム返しするプログラムを作成する。 -<p> -</p> -前回: <a href="rp2040_1.html">RP2040 SDKなしでLチカ</a><br> -ソースコード: <a href="https://git.mtkn.jp/rp2040">git</a>/ex2 -</p> - -<h2>動作環境</h2> -<ul> -<li>Void Linux - <ul> - <li>cross-arm-none-eabi-binutils-2.32_2</li> - <li>GNU Make 4.4.1</li> - <li>minicom version 2.7.1</li> - </ul> -</li> -<li><a href="https://akizukidenshi.com/catalog/g/g108461/">FT234X 超小型USBシリアル変換モジュール</a> -</li> -</ul> - -<h2>Clock</h2> - -<h3>リング発振回路</h3> -<p>\ -RP2040にはリング発振回路というのが内蔵されている。\ -これは自分の出力を反転させようとするもので、\ -不安定だが高速で消費電力の少ないクロックとして用いられる。\ -RP2040は電源を入れると、このリング発振回路を動作用のクロックとして\ -用いている。この発振回路の周波数は、チップの製造過程での誤差、動作時の電圧、\ -動作温度によって変動するので、正確な周波数が必要な用途には向かない。\ -</p> - -<h3>水晶発振子</h3> -<p>\ -秋月電子通商で購入したRP2040マイコンボードには外部クロックとして、\ -12MHzの水晶発振子が付属する。水晶発振子はリング発振回路より電力を消費するが、\ -より正確である。\ -</p> - -<h3>PLL</h3> -<p>\ -水晶振動子を入力として、周波数を数倍にしたものを出力するもの。\ -電気的な話はよく知らない。データシートの「2.18.2. Calcurating PLL parameters」\ -によると、入力周波数を<code>FREF</code>としたときの出力周波数は\ -<code>(FREF / REFDIV) × FBDIV / (POSTDIV1 × POSTDIV2)</code>となる。\ -これらの変数はそれぞれ設定用のレジスタに値を保存することで変更できる。\ -</p> - -<h2>UART</h2> -<p> -Universal Asynchronous Receiver/Transmitterの略。\ -電源と接地の他、2本の線だけで通信できる。\ -プロトコルは詳しく知らないが、rp2040がよしなにやってくれる。\ -rp2040では同時に二個まで利用できる。\ -どのGPIOピンを使うかもある程度自由に選べる。\ -どのピンが使えるかはデータシートの「2.19.2. Function Select」にかかれている。\ -今回はGPIO0とGPIO1を使う。\ -パソコンとの接続には、秋月電子通商で売っている\ -<a href="https://akizukidenshi.com/catalog/g/g108461/">FT234X 超小型USBシリアル変換モジュール</a>\ -を使用した。\ -</p> - -<h2>プログラム</h2> -<h3>初期設定</h3> -<p> -後で見るように、UARTの動作には多分水晶発振子とPLLが必要なので、\ -まずはそれを設定する。\ -起動後、メインのプログラムが読み込まれるまでの<code>boot2</code>は\ -前回と同じものである。\ -<code>main.s</code>ではまず前回と同様に初期スタックポインタとエントリーポイント\ -を定義する: -</p> -<pre><code> .section .vectors -vectors: - .word 0x20040000 // initial SP - .word(reset+1) // entry point -</code></pre> -<p> -続いて利用するサブシステムのリセットを解除する。\ -PLLとUARTが追加されている。\ -今回使うUARTはUART0だけである: -</p> -<pre><code> .section .text -reset: - // unreset gpio, pll_sys, uart0 - ldr r0, =(1 &lt;&lt; 22 | 1 &lt;&lt; 12 | 1 &lt;&lt; 5) // uart0 | pll_sys | io_bank0 - ldr r3, resets_base - ldr r1, atomic_clr - str r0, [r3, r1] // RESETS: RESET -unreset_chk: - ldr r1, [r3, #0x8] // RESETS: RESET_DONE - tst r0, r1 - beq unreset_chk - -/* ... */ - -atomic_clr: - .word 0x00003000 -resets_base: - .word 0x4000c000 -</code></pre> - -<h3>GPIOの設定</h3> -<p> -次にGPIOの役割を設定する。\ -前回はLEDを点滅させるためにGPIO25をSIOに設定したが、\ -今回はGPIO0とGPIO1をUART0にする: -</p> -<pre><code> // 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 -</code></pre> - -<h3>Clockの設定</h3> -<p> -Clockの設定をする。\ -まずは水晶発振子を起動する。\ -推奨発振子は起動してから周波数が安定するまで少し時間がかかるようで、\ -その間待たないといけない。\ -この時間は1msあれば十分だとデータシートに書いている。\ -この待ち時間はXOSC: STARTUPレジスタに、256サイクル単位で記述する。\ -データシートによると初期のリング発振子は最大で12MHzなので、\ -<code>(12 * 10^6 * 1 * 10^-3) / 256 = 47</code>をこのレジスタにセットする。\ -ところでデータシートではこの計算は水晶発振子の周波数で書かれている。\ -起動直後でまだ使えない水晶発振子の周波数を使うのはなんでやろ。\ -SDKでも<code>pico-sdk/src/rp2_common/hardware_xosc/xosc.c</code>で、 -</p> -<pre><code>\ -#define STARTUP_DELAY (((((XOSC_MHZ * MHZ) / 1000) + 128) / 256) * PICO_XOSC_STARTUP_DELAY_MULTIPLIER) -</code></pre> -<p> -と定義されている(PICO_XOSC_STARTUP_DELAY_MULTIPLIERは1)。\ -とりあえず47に設定しているが、試しに0や1にしても動いた。よくわからん。\ -</p> -<p> -待ち時間を設定したら、起動する。\ -XOSC: CTRLに起動用のコマンド的なものを入力し、周波数が安定するのを待つ。\ -</p> -<p> -以上を実装したのが以下のコード: -</p> -<pre><code> // 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 -</code></pre> - -<h3>PLLの設定</h3> -<p> -水晶発振子が起動できたので、次にPLLを設定する。\ -CPUが133MHzまで対応しているので133MHzになるようにした。\ -</p> -<p> -PLLは入力となる振動(ここでは水晶発振子の振動)を加工して\ -周波数を上げたり下げたりする。\ -出力の周波数は以下の式で決まる: -</p> -<pre>(FREF / REFDIV) * FBDIV / (POSTDIV1 * POSTDIV2)</pre> -<p> -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つの値の差が大きい方がいいと書いている)。 -</p> -<p> -PLL設定の手順は、FBDIVの設定、PLLとVCOの起動、VOCが安定するまで待機、\ -POSTDIV1とPOSTDIV2の設定、Post Dividerの起動、そして最後にシステムとUARTのクロック\ -を今設定したPLLに変更、である。\ -以上を実装したのが以下のコード: -</p> -<pre><code> // 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 &lt;&lt; 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 &lt;&lt; 16 | 3 &lt;&lt; 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 &lt;&lt; 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 -</code></pre> - -<h3>UARTの設定</h3> -<p> -データシートによるとUART設定の手順は以下の通り: -</p> -<ul> -<li>リセットの解除</li> -<li>clock_periの設定</li> -<li>UARTの有効化</li> -<li>FIFOの有効化</li> -<li>転送速度の設定</li> -<li>フォーマットの設定</li> -</ul> -<p> -上の2つは既に終えている。\ -残りの部分はこの順番どおりに設定しても動かなかった。\ -C言語で書かれたサンプルを見ると、クロックを設定した後、\ -転送速度の設定、UARTの有効化、FIFOの有効化の順になっている。\ -そのとおりにすると動いた。\ -理由はよく理解していないが、変数を設定してから起動するほうが素直ではある。\ -</p> -<p> -転送速度はminicomのデフォルトである115200 baudに設定する。\ -データシート「4.2.7.1. Baud Rate Calculation」の計算式において、\ -クロック周波数を125MHzから133MHzに変えて計算して、\ -BRDI=72、BDRF=0.157(=10/64)となる。\ -この数値をUART: UARTIBRD、UART: UARTFBRDレジスタにそれぞれ代入する。 -</p> -<p> -UARTの有効化はUART: UARTCRレジスタのUARTENビットをセットすることで行う。\ -C言語のサンプルでは同じレジスタのRXE、TXEビットもセットしているが、\ -この2つはもともと1になっているのでほっといてよさそう。\ -</p> -<p> -FIFOの有効化はUART: UARTLCR_HレジスタのFENビットをセットすることで行う。\ -また、同じレジスタの他のビットで、データーのフォーマットを設定できる。\ -ここではminicomのデフォルトに合わせてWLENを8bitにする。\ -</p> -<p> -以上をまとめると以下のようになる: -</p> -<pre><code> // 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 &lt;&lt; 5 | 1 &lt;&lt; 4) // WLEN = 8, FEN = 1 - str r0, [r3, #0x2c] // UART: UARTLCR_H - -/* ... */ - -atomic_set: - .word 0x00002000 -uart0_base: - .word 0x40034000 -</code></pre> - -<h3>UARTの入出力</h3> -<p> -設定が終わったので実際にUARTの入出力を処理するコードを書く。\ -まずUARTからの出力は、出力したいバイトをUART: UARTDRに書き込むことで\ -行う。\ -その際、書き込まれたデータは一時的に出力用FIFOに保持されるので、このFIFO\ -が満杯でないことを確認する必要がある。\ -FIFOの状態はUART: UARTFRレジスタで確認できる。\ -このレジスタのTXFFの値が1であればデータを書き込めないので、\ -0になるまで待機する。\ -関数名は<code>putchar</code>にした。\ -また出力したいデータは<code>r0</code>レジスタにの下位8ビットに\ -入れられているものとした。\ -書き込めるデーターは8ビットだけなので、<code>0xff</code>と論理積をとってから\ -書き込んでいる: -</p> -<pre><code>putchar: - 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 -</code></pre> - -<p> -入力はUART: UARTDRの下位8ビットを読むことで得られる。\ -UARTからの入力は、一時的に入力用FIFOに保存される。\ -このFIFOが空の状態でデータを読んでも意味がないので、\ -FIFOが空でないことを確認する必要がある。\ -これはUART: UARTFRレジスタのRXFEを読むことで確認できる。\ -本来は入力があったときに割り込みを発生させて、それまでは\ -CPUを休ませるか別の処理をさせておくべきだが、とりあえずここでは\ -ループでFIFOの状態を確認し続けている。\ -関数名は<code>getchar</code>にした。 -読み込んだデータは<code>r0</code>レジスタに保存している:\ -</p> -<pre><code>getchar: - 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 -</code></pre> -<p> -あとはこの2つの関数をループの中で交互に呼び出せば、\ -オウム返しするだけのプログラムが完成する: -</p> -<pre><code>loop: - bl getchar - bl putchar - b loop -</code></pre> - -<h2>リング発振回路でUARTは動くんかな?</h2> -<p>\ -UARTの通信には正確なクロックが必要である。\ -その為上では<code>clk_peri</code>として水晶発振子とPLLを用いた。\ -ところがpico-examplesのhello_uartでは<code>main()</code>関数で\ -水晶発振子を設定していない。そこでリング発振回路を用いてみたのだが、\ -どうもうまく通信できない。出力されている正確な周波数も分からないので\ -あきらめることにした。オシロスコープなんていうものは持っていない。\ -</p> - -<h3>pico-sdk</h3> -<p> -ところがどうも調べているとSDKを使った場合、\ -デフォルトではクロック周波数は125MHzになっているらしい。\ -どうやら水晶発振子もPLLも<code>main()</code>が呼ばれる前に\ -設定されているようである。\ -</p> -<p> -pico-examplesのサンプルプログラムはビルドすると自動で逆アセンブリした\ -ファイルを出力してくれる。これを見ると、最初の256バイトは前回説明した\ -boot2のコードで、その後ろにベクターテーブルが続く。\ -ベクターテーブルの最初は初期スタックポインタで、<code>0x20042000</code>\ -になっている。次はエントリーポイントで、<code>0x100001f7</code>である:\ -</p> -<pre><code>\ -10000100 &lt;__VECTOR_TABLE&gt;: -10000100: 20042000 .word 0x20042000 -10000104: 100001f7 .word 0x100001f7 -</code></pre> -<p> -Thumbモードなので実際のエントリーポイントは<code>1</code>引いた、\ -<code>0x100001f6</code>である。この場所ではまず自分のCPUIDを調べて、\ -<code>1</code>であれば待機状態に移行する。\ -RP2040はデュアルコアである。起動直後はCPUIDが<code>0</code>のコアだけで処理をして、\ -CPUIDが<code>1</code>のコアはプログラマが必要に応じて起動することになっている。\ -このためCPUIDが<code>1</code>のコアは起動してすぐに待機状態に入ることがデータシート\ -に書かれている。しかしこの処理はユーザーの書いたプログラムじゃなくて\ -内蔵ROMにある起動用プログラムが担当するみたいに書かれてるんやけど、\ -なんでSDKではユーザープログラムの一部として組み込んでるんかな? -</p> -<pre><code>\ -100001f6 &lt;_reset_handler&gt;: -100001f6: 481d ldr r0, [pc, #116] ; (1000026c &lt;hold_non_core0_in_bootrom+0xe&gt;) -100001f8: 6800 ldr r0, [r0, #0] -100001fa: 2800 cmp r0, #0 -100001fc: d12f bne.n 1000025e &lt;hold_non_core0_in_bootrom&gt; -</code></pre> -<p>\ -上のコードの最初の<code>ldr</code>は、<code>0xd0000000</code>\ -(M0PLUS: CPUIDレジスタ)をロードしている。\ -最後の飛び先<code>0x1000025e</code>はCPUIDが<code>1</code>のCPUを待機させる処理\ -である:\ -</p> -<pre><code>\ -1000025e &lt;hold_non_core0_in_bootrom&gt;: -1000025e: 4809 ldr r0, [pc, #36] ; (10000284 &lt;hold_non_core0_in_bootrom+0x26&gt;) -10000260: f001 fb9c bl 1000199c &lt;rom_func_lookup&gt; -10000264: 4700 bx r0 -10000266: 0000 .short 0x0000 -/* ... */ -10000284: 00005657 .word 0x00005657 -</code></pre> -<p>\ -内蔵フラッシュに書きこまれた関数を呼びだしている。呼びだしに使うコードは\ -<code>0x00005657</code>(<code>'W' | 'V' &lt;&lt; 8</code>)である。\ -データシートを見ると、この関数は<code>_wait_for_vector()</code>という名前で、\ -CPUIDが1のCPUを寝かしつけるのに使われると書いている。\ -この部分のソースコードをpico-sdkで探すと\ -<code>pico-sdk/src/rp2_common/pico_standard_link/crt0.S</code>\ -というのが見付かった:\ -</p> -<pre><code>\ -$ find pico-sdk/src -type f | xargs grep -l _reset_handler -pico-sdk/src/rp2_common/pico_standard_link/crt0.S -</code></pre> -<p>\ -このファイルによると: -</p> -<pre><code> // 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 -</code></pre> -<p>\ -だそうである。やっぱり無駄やん。内蔵フラッシュのプログラムにバグがあっても\ -このコードのせいで見付かりにくくなってない?知らんけど。\ -</p> - -<p>\ -続いて<code>.data</code>領域と<code>.bss</code>領域のコピー、初期化のようである。\ -多分OSの本かなんかで習ったメモリマップの話:\ -</p> -<pre><code>\ -100001fe: a40d add r4, pc, #52 ; (adr r4, 10000234 &lt;data_cpy_table&gt;) -10000200: cc0e ldmia r4!, {r1, r2, r3} -10000202: 2900 cmp r1, #0 -10000204: d002 beq.n 1000020c &lt;_reset_handler+0x16&gt; -10000206: f000 f812 bl 1000022e &lt;data_cpy&gt; -1000020a: e7f9 b.n 10000200 &lt;_reset_handler+0xa&gt; -1000020c: 4918 ldr r1, [pc, #96] ; (10000270 &lt;hold_non_core0_in_bootrom+0x12&gt;) -1000020e: 4a19 ldr r2, [pc, #100] ; (10000274 &lt;hold_non_core0_in_bootrom+0x16&gt;) -10000210: 2000 movs r0, #0 -10000212: e000 b.n 10000216 &lt;bss_fill_test&gt; - -10000214 &lt;bss_fill_loop&gt;: -10000214: c101 stmia r1!, {r0} - -10000216 &lt;bss_fill_test&gt;: -10000216: 4291 cmp r1, r2 -10000218: d1fc bne.n 10000214 &lt;bss_fill_loop&gt; -</code></pre> - -<p>\ -最後にいろいろ呼びだす:\ -</p> -<pre><code>\ -1000021a &lt;platform_entry&gt;: -1000021a: 4917 ldr r1, [pc, #92] ; (10000278 &lt;hold_non_core0_in_bootrom+0x1a&gt;) -1000021c: 4788 blx r1 -1000021e: 4917 ldr r1, [pc, #92] ; (1000027c &lt;hold_non_core0_in_bootrom+0x1e&gt;) -10000220: 4788 blx r1 -10000222: 4917 ldr r1, [pc, #92] ; (10000280 &lt;hold_non_core0_in_bootrom+0x22&gt;) -10000224: 4788 blx r1 -10000226: be00 bkpt 0x0000 -10000228: e7fd b.n 10000226 &lt;platform_entry+0xc&gt; -/* ... */ -10000278: 10001819 .word 0x10001819 -1000027c: 100002dd .word 0x100002dd -10000280: 10001909 .word 0x10001909 -</code></pre> -<p>\ -一つめの<code>blx</code>は<code>0x10001818</code>(<code>runtime_init</code>)を、\ -二つめは<code>0x100002dc</code>(<code>main</code>)を、\ -最後のは<code>0x10001908</code>(<code>exit</code>)を、\ -それぞれ呼んでいる。\ -この<code>runtime_init</code>はアセンブリでは分かりにくいので\ -ソースコードを探してみると、以下のものが見付かった:\ -</p> -<pre><code>\ -$ 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 -</code></pre> -<p>\ -最後の<code>mutex.h</code>は関係なさそう。\ -二つめの<code>crt0.S</code>は呼びだしてるだけ。\ -一つめの<code>runtime.c</code>が多分探しているものである。\ -これを見るとまず各種周辺機器を一度リセットし、リセット状態を解除している。\ -使わんやつも初期化してない?\ -その後<code>clocks_init()</code>を呼んでいる。\ -この関数は<code>pico-sdk/src/rp2_common/hardware_clocks/clocks.c</code>\ -で定義されている。これを見ると、<code>xosc_init()</code>を呼んで\ -水晶発振子を初期化した後、<code>clk_peri</code>を125MHzに設定している:\ -</p> -<pre><code> clock_configure(clk_peri, - 0, - CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLK_SYS, - 125 * MHZ, - 125 * MHZ); -</code></pre> -<p>\ -やっぱり水晶発振子じゃないとあかんのかな。\ -</p> - -<h2>CMake</h2> -<p>\ -上ではビルドしたバイナリを逆アッセンブルして読んだ。\ -わざわざこんなことをしなくてもMakefile読めばなにがどう\ -なって最終生成物に辿りつくのか分かればいいのだが、そうもいかない。\ -このSDKとpico-examplesにはビルドシステムとしてCMakeなるものが使われている。\ -これがどうも複雑でよく分からない。勉強する気にもならん。\ -上で見た<code>crt0.S</code>や<code>runtime.c</code>といったファイルも\ -<code>hello_uart</code>で本当に使われているものなのかもよく分からない。\ -こんな煩雑なものは本当に必要なのかな。無駄に複雑にしてるだけとちゃうんかな。\ -特に僕は勉強用に使ってるので、ソースコードの依存関係をもっと\ -分かりやすくしてくれないと、内部でなにがどうなってるのか理解しにくい。\ -何度か頑張って読もうとしたが、面白くないのでやめた。\ -数百行のファイルをあっちからこっちから<code>include</code>してるし、\ -大文字ばかりの変数だらけで目が痛い。\ -こんなものを扱えるというのはえらいええ頭してはるんやね。\ -</p> - - -<h2>参考</h2> -<ul> -<li> -<a href="https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf">RP2040 Datasheet.Raspberry Pi Foundation</a> -</li> -<li> -<a href="https://github.com/raspberrypi/pico-sdk">pico-sdk.github</a> -</li> -<li> -<a href="https://developer.arm.com/documentation/ddi0419/c/">ARMv6-M Architecture Reference Manual</a> -</li> -<li> -<a href="https://ja.wikipedia.org/wiki/%E3%83%AA%E3%83%B3%E3%82%B0%E3%83%BB%E3%82%AA%E3%82%B7%E3%83%AC%E3%83%BC%E3%82%BF">リング・オシレータ.Wikipedia</a> -</li> -<li> -<a href="https://www5.epsondevice.com/ja/information/technical_info/osc.html">水晶発振器とは? 原理と仕組み、水晶振動子との違い、選び方のポイントを解説.エプソン水晶デバイス</a> -</li> -</ul> diff --git a/man/index.html b/man/index.html @@ -9,6 +9,7 @@ <h2>更新履歴</h2> <a href="/rss.xml">RSS</a> <ul> +<li>2024-02-22 <a href="/computer/rp2040_2.html">RP2040 SDKなし2 Clock、UART</a></li> <li>2024-02-15 <a href="/computer/git_server.html">Gitサーバーの設定 on OpenBSD</a></li> <li>2024-02-09 <a href="/kitchen/recipe/miso.html">味噌</a></li> <li>2024-01-19 <a href="/computer/mailserver.html">メールサーバー構築 on OpenBSD with OpenSMTPD and Dovecot</a></li> diff --git a/pub/computer/index.html b/pub/computer/index.html @@ -29,6 +29,7 @@ Small is beautiful. <h2>RP2040</h2> <ul> <li><a href="rp2040_1.html">RP2040 SDKなしでLチカ</a></li> +<li><a href="rp2040_2.html">RP2040 SDKなし2 Clock、UART</a></li> </ul> <h2>Xlib</h2> <ul> diff --git a/pub/computer/rp2040_2.html b/pub/computer/rp2040_2.html @@ -0,0 +1,426 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <link rel="stylesheet" type="text/css" href="/style.css"> + <link rel="icon" type="image/x-icon" href="/pics/favicon.ico"> + <title>RP2040 SDKなし2 Clock、UART</title> +</head> +<body> + <header> + <a href="/">主頁</a> | + <a href="/about.html">自己紹介</a> | + <a href="/journal">日記</a> | + <a href="/farm">農業</a> | + <a href="/kitchen">台所</a> | + <a href="/computer">電算機</a> | + <a href="/poetry">詩</a> | + <a href="/books">本棚</a> | + <a href="/gallery">絵</a> | + <a href="https://git.mtkn.jp">Git</a> + </header> + <main> + <article> +<h1>RP2040 SDKなし2 Clock、UART</h1> +<time>2024-02-22</time> +<p> +今回はClockとUARTを設定してパソコンに繋ぎ、キーボードからの入力をオウム返しするプログラムを作成する。 +<p> +</p> +前回: <a href="rp2040_1.html">RP2040 SDKなしでLチカ</a><br> +ソースコード: <a href="https://git.mtkn.jp/rp2040">git</a>/ex2 +</p> + +<h2>動作環境</h2> +<ul> +<li>Void Linux + <ul> + <li>cross-arm-none-eabi-binutils-2.32_2</li> + <li>GNU Make 4.4.1</li> + <li>minicom version 2.7.1</li> + </ul> +</li> +<li><a href="https://akizukidenshi.com/catalog/g/g108461/">FT234X 超小型USBシリアル変換モジュール</a> +</li> +</ul> + +<h2>Clock</h2> + +<h3>リング発振回路</h3> +<p>RP2040にはリング発振回路というのが内蔵されている。これは自分の出力を反転させようとするもので、不安定だが高速で消費電力の少ないクロックとして用いられる。RP2040は電源を入れると、このリング発振回路を動作用のクロックとして用いている。この発振回路の周波数は、チップの製造過程での誤差、動作時の電圧、動作温度によって変動するので、正確な周波数が必要な用途には向かない。</p> + +<h3>水晶発振子</h3> +<p>秋月電子通商で購入したRP2040マイコンボードには外部クロックとして、12MHzの水晶発振子が付属する。水晶発振子はリング発振回路より電力を消費するが、より正確である。</p> + +<h3>PLL</h3> +<p>水晶振動子を入力として、周波数を数倍にしたものを出力するもの。電気的な話はよく知らない。データシートの「2.18.2. Calcurating PLL parameters」によると、入力周波数を<code>FREF</code>としたときの出力周波数は<code>(FREF / REFDIV) × FBDIV / (POSTDIV1 × POSTDIV2)</code>となる。これらの変数はそれぞれ設定用のレジスタに値を保存することで変更できる。</p> + +<h2>UART</h2> +<p> +Universal Asynchronous Receiver/Transmitterの略。2本の線だけで通信できる。プロトコルは詳しく知らないが、rp2040がよしなにやってくれる。rp2040では同時に二個まで利用できる。どのGPIOピンを使うかもある程度自由に選べる。どのピンが使えるかはデータシートの「2.19.2. Function Select」に書かれている。今回はGPIO0とGPIO1を使う。パソコンとの接続には、秋月電子通商で売っている<a href="https://akizukidenshi.com/catalog/g/g108461/">FT234X 超小型USBシリアル変換モジュール</a>を使用した。UARTで接続するためのパソコン側のソフトウェアはminicomを使用した。僕の環境ではシリアル変換モジュールをパソコンにUSB接続すると、<code>/dev/ttyUSB0</code>として認識されるので、</p> +<pre><code>$ minicom -D /dev/ttyUSB0 +</code></pre> +<p> +とすると接続できる。 +</p> + +<h2>main.s</h2> +<h3>初期設定</h3> +<p> +後で見るように、UARTの動作には多分水晶発振子とPLLが必要なので、まずはそれを設定する。起動後、メインのプログラムが読み込まれるまでの<code>boot2</code>は前回と同じものである。<code>main.s</code>ではまず前回と同様に初期スタックポインタとエントリーポイントを定義する: +</p> +<pre><code> .section .vectors +vectors: + .word 0x20040000 // initial SP + .word (reset+1) // entry point +</code></pre> +<p> +続いて利用するサブシステムのリセットを解除する。PLLとUARTが追加されている。今回使うUARTはUART0だけである: +</p> +<pre><code> .section .text +reset: + // unreset gpio, pll_sys, uart0 + ldr r0, =(1 &lt;&lt; 22 | 1 &lt;&lt; 12 | 1 &lt;&lt; 5) // uart0 | pll_sys | io_bank0 + ldr r3, resets_base + ldr r1, atomic_clr + str r0, [r3, r1] // RESETS: RESET +unreset_chk: + ldr r1, [r3, #0x8] // RESETS: RESET_DONE + tst r0, r1 + beq unreset_chk + +/* ... */ + +atomic_clr: + .word 0x00003000 +resets_base: + .word 0x4000c000 +</code></pre> + +<h3>GPIOの設定</h3> +<p> +次にGPIOの役割を設定する。前回はLEDを点滅させるためにGPIO25をSIOに設定したが、今回はGPIO0とGPIO1をUART0にする: +</p> +<pre><code> // 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 +</code></pre> + +<h3>Clockの設定</h3> +<p> +Clockの設定をする。まずは水晶発振子を起動する。水晶発振子は起動してから周波数が安定するまで少し時間がかかるようで、その間待たないといけない。この時間は1msあれば十分だとデータシートに書いている。この待ち時間はXOSC: STARTUPレジスタに、256サイクル単位で記述する。データシートによると初期のリング発振子は最大で12MHzなので、<code>(12 * 10^6 * 1 * 10^-3) / 256 = 47</code>をこのレジスタにセットする。ところでデータシートではこの計算はリング発振子ではなく水晶発振子の周波数で書かれている。起動直後でまだ使えない水晶発振子の周波数を使うのはなんでやろ。SDKでも<code>pico-sdk/src/rp2_common/hardware_xosc/xosc.c</code>で、 +</p> +<pre><code>#define STARTUP_DELAY (((((XOSC_MHZ * MHZ) / 1000) + 128) / 256) * PICO_XOSC_STARTUP_DELAY_MULTIPLIER) +</code></pre> +<p> +と定義されている(PICO_XOSC_STARTUP_DELAY_MULTIPLIERは1)。とりあえず47に設定しているが、試しに0や1にしても動いた。よくわからん。</p> +<p> +待ち時間を設定したら発振子を起動する。XOSC: CTRLに起動用のコマンド的なものを入力し、周波数が安定するのを待つ。</p> +<p> +以上を実装したのが以下のコード: +</p> +<pre><code> // 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 +</code></pre> + +<h3>PLLの設定</h3> +<p> +水晶発振子が起動できたので、次にPLLを設定する。CPUが133MHzまで対応しているので133MHzになるようにした。</p> +<p> +PLLは入力となる振動(ここでは水晶発振子の振動)を加工して周波数を上げたり下げたりする。出力の周波数は以下の式で決まる: +</p> +<pre>(FREF / REFDIV) * FBDIV / (POSTDIV1 * POSTDIV2)</pre> +<p> +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つの値の差が大きい方がいいと書いている)。 +</p> +<p> +PLL設定の手順は、FBDIVの設定、PLLとVCOの起動、VOCが安定するまで待機、POSTDIV1とPOSTDIV2の設定、Post Dividerの起動、そして最後にシステムとUARTのクロックを今設定したPLLに変更、である。以上を実装したのが以下のコード: +</p> +<pre><code> // 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 &lt;&lt; 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 &lt;&lt; 16 | 3 &lt;&lt; 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 &lt;&lt; 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 +</code></pre> + +<h3>UARTの設定</h3> +<p> +データシートによるとUART設定の手順は以下の通り: +</p> +<ul> +<li>リセットの解除</li> +<li>clock_periの設定</li> +<li>UARTの有効化</li> +<li>FIFOの有効化</li> +<li>転送速度の設定</li> +<li>フォーマットの設定</li> +</ul> +<p> +上の2つは既に終えている。残りの部分はこの順番どおりに設定しても動かなかった。C言語で書かれたサンプルを見ると、クロックを設定した後、転送速度の設定、UARTの有効化、FIFOの有効化の順になっている。そのとおりにすると動いた。理由はよく理解していないが、変数を設定してから起動するほうが素直ではある。</p> +<p> +転送速度はminicomのデフォルトである115200 baudに設定する。データシート「4.2.7.1. Baud Rate Calculation」の計算式において、クロック周波数を125MHzから133MHzに変えて計算して、BRDI=72、BDRF=0.157(=10/64)となる。この数値をUART: UARTIBRD、UART: UARTFBRDレジスタにそれぞれ代入する。 +</p> +<p> +UARTの有効化はUART: UARTCRレジスタのUARTENビットをセットすることで行う。C言語のサンプルでは同じレジスタのRXE、TXEビットもセットしているが、この2つはもともと1になっているのでほっといてよさそう。</p> +<p> +FIFOの有効化はUART: UARTLCR_HレジスタのFENビットをセットすることで行う。また、同じレジスタの他のビットで、データーのフォーマットを設定できる。ここではminicomのデフォルトに合わせてWLENを8bitにする。</p> +<p> +以上をまとめると以下のようになる: +</p> +<pre><code> // 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 &lt;&lt; 5 | 1 &lt;&lt; 4) // WLEN = 8, FEN = 1 + str r0, [r3, #0x2c] // UART: UARTLCR_H + +/* ... */ + +atomic_set: + .word 0x00002000 +uart0_base: + .word 0x40034000 +</code></pre> + +<h3>UARTの入出力</h3> +<p> +設定が終わったので実際にUARTの入出力を処理するコードを書く。まずUARTからの出力は、出力したいバイトをUART: UARTDRに書き込むことで行う。その際、書き込まれたデータは一時的に出力用FIFOに保持されるので、このFIFOが満杯でないことを確認する必要がある。FIFOの状態はUART: UARTFRレジスタで確認できる。このレジスタのTXFFの値が1であればデータを書き込めないので、0になるまで待機する。関数名は<code>putbyte</code>にした。また出力したいデータは<code>r0</code>レジスタにの下位8ビットに入れられているものとした。書き込めるデーターは8ビットだけなので、<code>0xff</code>と論理積をとってから書き込んでいる: +</p> +<pre><code>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 +</code></pre> + +<p> +入力はUART: UARTDRの下位8ビットを読むことで得られる。UARTからの入力は、一時的に入力用FIFOに保存される。このFIFOが空の状態でデータを読んでも意味がないので、FIFOが空でないことを確認する必要がある。これはUART: UARTFRレジスタのRXFEを読むことで確認できる。本来は入力があったときに割り込みを発生させて、それまではCPUを休ませるか別の処理をさせておくべきだが、とりあえずここではループでFIFOの状態を確認し続けている。関数名は<code>getbyte</code>にした。 +読み込んだデータは<code>r0</code>レジスタに保存している:</p> +<pre><code>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 +</code></pre> +<p> +あとはこの2つの関数をループの中で交互に呼び出せば、オウム返しするだけのプログラムが完成する: +</p> +<pre><code>loop: + bl getbyte + bl putbyte + b loop +</code></pre> + +<h2>リング発振回路でUARTは動くんかな?</h2> +<p>UARTの通信には正確なクロックが必要である。その為上では<code>clk_peri</code>として水晶発振子とPLLを用いた。ところがpico-examplesのhello_uartでは<code>main()</code>関数で水晶発振子を設定していない。そこでリング発振回路を用いてみたのだが、どうもうまく通信できない。出力されている正確な周波数も分からないのであきらめることにした。オシロスコープなんていうものは持っていない。</p> + +<h3>pico-sdk</h3> +<p> +ところがどうも調べているとSDKを使った場合、デフォルトではクロック周波数は125MHzになっているらしい。どうやら水晶発振子もPLLも<code>main()</code>が呼ばれる前に設定されているようである。</p> +<p> +pico-examplesのサンプルプログラムはビルドすると自動で逆アセンブリしたファイルを出力してくれる。これを見ると、最初の256バイトは前回説明したboot2のコードで、その後ろにベクターテーブルが続く。ベクターテーブルの最初は初期スタックポインタで、<code>0x20042000</code>になっている。次はエントリーポイントで、<code>0x100001f7</code>である:</p> +<pre><code>10000100 &lt;__VECTOR_TABLE&gt;: +10000100: 20042000 .word 0x20042000 +10000104: 100001f7 .word 0x100001f7 +</code></pre> +<p> +Thumbモードなので実際のエントリーポイントは<code>1</code>引いた、<code>0x100001f6</code>である。この場所ではまず自分のCPUIDを調べて、<code>1</code>であれば待機状態に移行する。RP2040はデュアルコアである。起動直後はCPUIDが<code>0</code>のコアだけで処理をして、CPUIDが<code>1</code>のコアはプログラマが必要に応じて起動することになっている。このためCPUIDが<code>1</code>のコアは起動してすぐに待機状態に入ることがデータシートに書かれている。しかしこの処理はユーザーの書いたプログラムじゃなくて内蔵ROMにある起動用プログラムが担当するみたいに書かれてるんやけど、なんでSDKではユーザープログラムの一部として組み込んでるんかな? +</p> +<pre><code>100001f6 &lt;_reset_handler&gt;: +100001f6: 481d ldr r0, [pc, #116] ; (1000026c &lt;hold_non_core0_in_bootrom+0xe&gt;) +100001f8: 6800 ldr r0, [r0, #0] +100001fa: 2800 cmp r0, #0 +100001fc: d12f bne.n 1000025e &lt;hold_non_core0_in_bootrom&gt; +</code></pre> +<p>上のコードの最初の<code>ldr</code>は、<code>0xd0000000</code>(M0PLUS: CPUIDレジスタ)をロードしている。最後の飛び先<code>0x1000025e</code>はCPUIDが<code>1</code>のCPUを待機させる処理である:</p> +<pre><code>1000025e &lt;hold_non_core0_in_bootrom&gt;: +1000025e: 4809 ldr r0, [pc, #36] ; (10000284 &lt;hold_non_core0_in_bootrom+0x26&gt;) +10000260: f001 fb9c bl 1000199c &lt;rom_func_lookup&gt; +10000264: 4700 bx r0 +10000266: 0000 .short 0x0000 +/* ... */ +10000284: 00005657 .word 0x00005657 +</code></pre> +<p>内蔵フラッシュに書きこまれた関数を呼びだしている。呼びだしに使うコードは<code>0x00005657</code>(<code>'W' | 'V' &lt;&lt; 8</code>)である。データシートを見ると、この関数は<code>_wait_for_vector()</code>という名前で、CPUIDが1のCPUを寝かしつけるのに使われると書いている。この部分のソースコードをpico-sdkで探すと<code>pico-sdk/src/rp2_common/pico_standard_link/crt0.S</code>というのが見付かった:</p> +<pre><code>$ find pico-sdk/src -type f | xargs grep -l _reset_handler +pico-sdk/src/rp2_common/pico_standard_link/crt0.S +</code></pre> +<p>このファイルによると: +</p> +<pre><code> // 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 +</code></pre> +<p>だそうである。やっぱり無駄やん。内蔵フラッシュのプログラムにバグがあってもこのコードのせいで見付かりにくくなってない?知らんけど。</p> + +<p>続いて<code>.data</code>領域と<code>.bss</code>領域のコピー、初期化のようである。多分OSの本かなんかで習ったメモリマップの話:</p> +<pre><code>100001fe: a40d add r4, pc, #52 ; (adr r4, 10000234 &lt;data_cpy_table&gt;) +10000200: cc0e ldmia r4!, {r1, r2, r3} +10000202: 2900 cmp r1, #0 +10000204: d002 beq.n 1000020c &lt;_reset_handler+0x16&gt; +10000206: f000 f812 bl 1000022e &lt;data_cpy&gt; +1000020a: e7f9 b.n 10000200 &lt;_reset_handler+0xa&gt; +1000020c: 4918 ldr r1, [pc, #96] ; (10000270 &lt;hold_non_core0_in_bootrom+0x12&gt;) +1000020e: 4a19 ldr r2, [pc, #100] ; (10000274 &lt;hold_non_core0_in_bootrom+0x16&gt;) +10000210: 2000 movs r0, #0 +10000212: e000 b.n 10000216 &lt;bss_fill_test&gt; + +10000214 &lt;bss_fill_loop&gt;: +10000214: c101 stmia r1!, {r0} + +10000216 &lt;bss_fill_test&gt;: +10000216: 4291 cmp r1, r2 +10000218: d1fc bne.n 10000214 &lt;bss_fill_loop&gt; +</code></pre> + +<p>最後にいろいろ呼びだす:</p> +<pre><code>1000021a &lt;platform_entry&gt;: +1000021a: 4917 ldr r1, [pc, #92] ; (10000278 &lt;hold_non_core0_in_bootrom+0x1a&gt;) +1000021c: 4788 blx r1 +1000021e: 4917 ldr r1, [pc, #92] ; (1000027c &lt;hold_non_core0_in_bootrom+0x1e&gt;) +10000220: 4788 blx r1 +10000222: 4917 ldr r1, [pc, #92] ; (10000280 &lt;hold_non_core0_in_bootrom+0x22&gt;) +10000224: 4788 blx r1 +10000226: be00 bkpt 0x0000 +10000228: e7fd b.n 10000226 &lt;platform_entry+0xc&gt; +/* ... */ +10000278: 10001819 .word 0x10001819 +1000027c: 100002dd .word 0x100002dd +10000280: 10001909 .word 0x10001909 +</code></pre> +<p>一つめの<code>blx</code>は<code>0x10001818</code>(<code>runtime_init</code>)を、二つめは<code>0x100002dc</code>(<code>main</code>)を、最後のは<code>0x10001908</code>(<code>exit</code>)を、それぞれ呼んでいる。この<code>runtime_init</code>はアセンブリでは分かりにくいのでソースコードを探してみると、以下のものが見付かった:</p> +<pre><code>$ 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 +</code></pre> +<p>最後の<code>mutex.h</code>は関係なさそう。二つめの<code>crt0.S</code>は呼びだしてるだけ。一つめの<code>runtime.c</code>が多分探しているものである。これを見るとまず各種周辺機器を一度リセットし、リセット状態を解除している。使わんやつも初期化してない?その後<code>clocks_init()</code>を呼んでいる。この関数は<code>pico-sdk/src/rp2_common/hardware_clocks/clocks.c</code>で定義されている。これを見ると、<code>xosc_init()</code>を呼んで水晶発振子を初期化した後、<code>clk_peri</code>を125MHzに設定している:</p> +<pre><code> clock_configure(clk_peri, + 0, + CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLK_SYS, + 125 * MHZ, + 125 * MHZ); +</code></pre> +<p>やっぱり水晶発振子じゃないとあかんのかな。</p> + +<h2>CMake</h2> +<p>上ではビルドしたバイナリを逆アッセンブルして読んだ。わざわざこんなことをしなくてもMakefile読めばなにがどうなって最終生成物に辿りつくのか分かればいいのだが、そうもいかない。このSDKとpico-examplesにはビルドシステムとしてCMakeなるものが使われている。これがどうも複雑でよく分からない。勉強する気にもならん。上で見た<code>crt0.S</code>や<code>runtime.c</code>といったファイルも<code>hello_uart</code>で本当に使われているものなのかもよく分からない。こんな煩雑なものは本当に必要なのかな。無駄に複雑にしてるだけとちゃうんかな。特に僕は勉強用に使ってるので、ソースコードの依存関係をもっと分かりやすくしてくれないと、内部でなにがどうなってるのか理解しにくい。何度か頑張って読もうとしたが、面白くないのでやめた。数百行のファイルをあっちからこっちから<code>include</code>してるし、大文字ばかりの変数だらけで目が痛い。こんなものを扱えるというのはえらいええ頭してはるんやね。</p> + + +<h2>参考</h2> +<ul> +<li> +<a href="https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf">RP2040 Datasheet.Raspberry Pi Foundation</a> +</li> +<li> +<a href="https://github.com/raspberrypi/pico-sdk">pico-sdk.github</a> +</li> +<li> +<a href="https://developer.arm.com/documentation/ddi0419/c/">ARMv6-M Architecture Reference Manual</a> +</li> +<li> +<a href="https://ja.wikipedia.org/wiki/%E3%83%AA%E3%83%B3%E3%82%B0%E3%83%BB%E3%82%AA%E3%82%B7%E3%83%AC%E3%83%BC%E3%82%BF">リング・オシレータ.Wikipedia</a> +</li> +<li> +<a href="https://www5.epsondevice.com/ja/information/technical_info/osc.html">水晶発振器とは? 原理と仕組み、水晶振動子との違い、選び方のポイントを解説.エプソン水晶デバイス</a> +</li> +</ul> + </article> + + </main> + <footer> + <address>info(at)mtkn(dot)jp</address> + <a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" rel="license noopener noreferrer">CC0 1.0</a> + </footer> +</body> +</html> diff --git a/pub/draft/rp2040_2.html b/pub/draft/rp2040_2.html @@ -1,421 +0,0 @@ -<!DOCTYPE html> -<html> -<head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width,initial-scale=1"> - <link rel="stylesheet" type="text/css" href="/style.css"> - <link rel="icon" type="image/x-icon" href="/pics/favicon.ico"> - <title>RP2040 SDKなし2 Clock, UART</title> -</head> -<body> - <header> - <a href="/">主頁</a> | - <a href="/about.html">自己紹介</a> | - <a href="/journal">日記</a> | - <a href="/farm">農業</a> | - <a href="/kitchen">台所</a> | - <a href="/computer">電算機</a> | - <a href="/poetry">詩</a> | - <a href="/books">本棚</a> | - <a href="/gallery">絵</a> | - <a href="https://git.mtkn.jp">Git</a> - </header> - <main> - <article> -<h1>RP2040 SDKなし2 Clock, UART</h1> -<time>2023-05-10</time> -<p> -今回はClockとUARTを設定してパソコンに繋ぎ、キーボードからの入力をオウム返しするプログラムを作成する。 -<p> -</p> -前回: <a href="rp2040_1.html">RP2040 SDKなしでLチカ</a><br> -ソースコード: <a href="https://git.mtkn.jp/rp2040">git</a>/ex2 -</p> - -<h2>動作環境</h2> -<ul> -<li>Void Linux - <ul> - <li>cross-arm-none-eabi-binutils-2.32_2</li> - <li>GNU Make 4.4.1</li> - <li>minicom version 2.7.1</li> - </ul> -</li> -<li><a href="https://akizukidenshi.com/catalog/g/g108461/">FT234X 超小型USBシリアル変換モジュール</a> -</li> -</ul> - -<h2>Clock</h2> - -<h3>リング発振回路</h3> -<p>RP2040にはリング発振回路というのが内蔵されている。これは自分の出力を反転させようとするもので、不安定だが高速で消費電力の少ないクロックとして用いられる。RP2040は電源を入れると、このリング発振回路を動作用のクロックとして用いている。この発振回路の周波数は、チップの製造過程での誤差、動作時の電圧、動作温度によって変動するので、正確な周波数が必要な用途には向かない。</p> - -<h3>水晶発振子</h3> -<p>秋月電子通商で購入したRP2040マイコンボードには外部クロックとして、12MHzの水晶発振子が付属する。水晶発振子はリング発振回路より電力を消費するが、より正確である。</p> - -<h3>PLL</h3> -<p>水晶振動子を入力として、周波数を数倍にしたものを出力するもの。電気的な話はよく知らない。データシートの「2.18.2. Calcurating PLL parameters」によると、入力周波数を<code>FREF</code>としたときの出力周波数は<code>(FREF / REFDIV) × FBDIV / (POSTDIV1 × POSTDIV2)</code>となる。これらの変数はそれぞれ設定用のレジスタに値を保存することで変更できる。</p> - -<h2>UART</h2> -<p> -Universal Asynchronous Receiver/Transmitterの略。電源と接地の他、2本の線だけで通信できる。プロトコルは詳しく知らないが、rp2040がよしなにやってくれる。rp2040では同時に二個まで利用できる。どのGPIOピンを使うかもある程度自由に選べる。どのピンが使えるかはデータシートの「2.19.2. Function Select」にかかれている。今回はGPIO0とGPIO1を使う。パソコンとの接続には、秋月電子通商で売っている<a href="https://akizukidenshi.com/catalog/g/g108461/">FT234X 超小型USBシリアル変換モジュール</a>を使用した。</p> - -<h2>プログラム</h2> -<h3>初期設定</h3> -<p> -後で見るように、UARTの動作には多分水晶発振子とPLLが必要なので、まずはそれを設定する。起動後、メインのプログラムが読み込まれるまでの<code>boot2</code>は前回と同じものである。<code>main.s</code>ではまず前回と同様に初期スタックポインタとエントリーポイントを定義する: -</p> -<pre><code> .section .vectors -vectors: - .word 0x20040000 // initial SP - .word(reset+1) // entry point -</code></pre> -<p> -続いて利用するサブシステムのリセットを解除する。PLLとUARTが追加されている。今回使うUARTはUART0だけである: -</p> -<pre><code> .section .text -reset: - // unreset gpio, pll_sys, uart0 - ldr r0, =(1 &lt;&lt; 22 | 1 &lt;&lt; 12 | 1 &lt;&lt; 5) // uart0 | pll_sys | io_bank0 - ldr r3, resets_base - ldr r1, atomic_clr - str r0, [r3, r1] // RESETS: RESET -unreset_chk: - ldr r1, [r3, #0x8] // RESETS: RESET_DONE - tst r0, r1 - beq unreset_chk - -/* ... */ - -atomic_clr: - .word 0x00003000 -resets_base: - .word 0x4000c000 -</code></pre> - -<h3>GPIOの設定</h3> -<p> -次にGPIOの役割を設定する。前回はLEDを点滅させるためにGPIO25をSIOに設定したが、今回はGPIO0とGPIO1をUART0にする: -</p> -<pre><code> // 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 -</code></pre> - -<h3>Clockの設定</h3> -<p> -Clockの設定をする。まずは水晶発振子を起動する。推奨発振子は起動してから周波数が安定するまで少し時間がかかるようで、その間待たないといけない。この時間は1msあれば十分だとデータシートに書いている。この待ち時間はXOSC: STARTUPレジスタに、256サイクル単位で記述する。データシートによると初期のリング発振子は最大で12MHzなので、<code>(12 * 10^6 * 1 * 10^-3) / 256 = 47</code>をこのレジスタにセットする。ところでデータシートではこの計算は水晶発振子の周波数で書かれている。起動直後でまだ使えない水晶発振子の周波数を使うのはなんでやろ。SDKでも<code>pico-sdk/src/rp2_common/hardware_xosc/xosc.c</code>で、 -</p> -<pre><code>#define STARTUP_DELAY (((((XOSC_MHZ * MHZ) / 1000) + 128) / 256) * PICO_XOSC_STARTUP_DELAY_MULTIPLIER) -</code></pre> -<p> -と定義されている(PICO_XOSC_STARTUP_DELAY_MULTIPLIERは1)。とりあえず47に設定しているが、試しに0や1にしても動いた。よくわからん。</p> -<p> -待ち時間を設定したら、起動する。XOSC: CTRLに起動用のコマンド的なものを入力し、周波数が安定するのを待つ。</p> -<p> -以上を実装したのが以下のコード: -</p> -<pre><code> // 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 -</code></pre> - -<h3>PLLの設定</h3> -<p> -水晶発振子が起動できたので、次にPLLを設定する。CPUが133MHzまで対応しているので133MHzになるようにした。</p> -<p> -PLLは入力となる振動(ここでは水晶発振子の振動)を加工して周波数を上げたり下げたりする。出力の周波数は以下の式で決まる: -</p> -<pre>(FREF / REFDIV) * FBDIV / (POSTDIV1 * POSTDIV2)</pre> -<p> -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つの値の差が大きい方がいいと書いている)。 -</p> -<p> -PLL設定の手順は、FBDIVの設定、PLLとVCOの起動、VOCが安定するまで待機、POSTDIV1とPOSTDIV2の設定、Post Dividerの起動、そして最後にシステムとUARTのクロックを今設定したPLLに変更、である。以上を実装したのが以下のコード: -</p> -<pre><code> // 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 &lt;&lt; 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 &lt;&lt; 16 | 3 &lt;&lt; 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 &lt;&lt; 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 -</code></pre> - -<h3>UARTの設定</h3> -<p> -データシートによるとUART設定の手順は以下の通り: -</p> -<ul> -<li>リセットの解除</li> -<li>clock_periの設定</li> -<li>UARTの有効化</li> -<li>FIFOの有効化</li> -<li>転送速度の設定</li> -<li>フォーマットの設定</li> -</ul> -<p> -上の2つは既に終えている。残りの部分はこの順番どおりに設定しても動かなかった。C言語で書かれたサンプルを見ると、クロックを設定した後、転送速度の設定、UARTの有効化、FIFOの有効化の順になっている。そのとおりにすると動いた。理由はよく理解していないが、変数を設定してから起動するほうが素直ではある。</p> -<p> -転送速度はminicomのデフォルトである115200 baudに設定する。データシート「4.2.7.1. Baud Rate Calculation」の計算式において、クロック周波数を125MHzから133MHzに変えて計算して、BRDI=72、BDRF=0.157(=10/64)となる。この数値をUART: UARTIBRD、UART: UARTFBRDレジスタにそれぞれ代入する。 -</p> -<p> -UARTの有効化はUART: UARTCRレジスタのUARTENビットをセットすることで行う。C言語のサンプルでは同じレジスタのRXE、TXEビットもセットしているが、この2つはもともと1になっているのでほっといてよさそう。</p> -<p> -FIFOの有効化はUART: UARTLCR_HレジスタのFENビットをセットすることで行う。また、同じレジスタの他のビットで、データーのフォーマットを設定できる。ここではminicomのデフォルトに合わせてWLENを8bitにする。</p> -<p> -以上をまとめると以下のようになる: -</p> -<pre><code> // 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 &lt;&lt; 5 | 1 &lt;&lt; 4) // WLEN = 8, FEN = 1 - str r0, [r3, #0x2c] // UART: UARTLCR_H - -/* ... */ - -atomic_set: - .word 0x00002000 -uart0_base: - .word 0x40034000 -</code></pre> - -<h3>UARTの入出力</h3> -<p> -設定が終わったので実際にUARTの入出力を処理するコードを書く。まずUARTからの出力は、出力したいバイトをUART: UARTDRに書き込むことで行う。その際、書き込まれたデータは一時的に出力用FIFOに保持されるので、このFIFOが満杯でないことを確認する必要がある。FIFOの状態はUART: UARTFRレジスタで確認できる。このレジスタのTXFFの値が1であればデータを書き込めないので、0になるまで待機する。関数名は<code>putchar</code>にした。また出力したいデータは<code>r0</code>レジスタにの下位8ビットに入れられているものとした。書き込めるデーターは8ビットだけなので、<code>0xff</code>と論理積をとってから書き込んでいる: -</p> -<pre><code>putchar: - 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 -</code></pre> - -<p> -入力はUART: UARTDRの下位8ビットを読むことで得られる。UARTからの入力は、一時的に入力用FIFOに保存される。このFIFOが空の状態でデータを読んでも意味がないので、FIFOが空でないことを確認する必要がある。これはUART: UARTFRレジスタのRXFEを読むことで確認できる。本来は入力があったときに割り込みを発生させて、それまではCPUを休ませるか別の処理をさせておくべきだが、とりあえずここではループでFIFOの状態を確認し続けている。関数名は<code>getchar</code>にした。 -読み込んだデータは<code>r0</code>レジスタに保存している:</p> -<pre><code>getchar: - 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 -</code></pre> -<p> -あとはこの2つの関数をループの中で交互に呼び出せば、オウム返しするだけのプログラムが完成する: -</p> -<pre><code>loop: - bl getchar - bl putchar - b loop -</code></pre> - -<h2>リング発振回路でUARTは動くんかな?</h2> -<p>UARTの通信には正確なクロックが必要である。その為上では<code>clk_peri</code>として水晶発振子とPLLを用いた。ところがpico-examplesのhello_uartでは<code>main()</code>関数で水晶発振子を設定していない。そこでリング発振回路を用いてみたのだが、どうもうまく通信できない。出力されている正確な周波数も分からないのであきらめることにした。オシロスコープなんていうものは持っていない。</p> - -<h3>pico-sdk</h3> -<p> -ところがどうも調べているとSDKを使った場合、デフォルトではクロック周波数は125MHzになっているらしい。どうやら水晶発振子もPLLも<code>main()</code>が呼ばれる前に設定されているようである。</p> -<p> -pico-examplesのサンプルプログラムはビルドすると自動で逆アセンブリしたファイルを出力してくれる。これを見ると、最初の256バイトは前回説明したboot2のコードで、その後ろにベクターテーブルが続く。ベクターテーブルの最初は初期スタックポインタで、<code>0x20042000</code>になっている。次はエントリーポイントで、<code>0x100001f7</code>である:</p> -<pre><code>10000100 &lt;__VECTOR_TABLE&gt;: -10000100: 20042000 .word 0x20042000 -10000104: 100001f7 .word 0x100001f7 -</code></pre> -<p> -Thumbモードなので実際のエントリーポイントは<code>1</code>引いた、<code>0x100001f6</code>である。この場所ではまず自分のCPUIDを調べて、<code>1</code>であれば待機状態に移行する。RP2040はデュアルコアである。起動直後はCPUIDが<code>0</code>のコアだけで処理をして、CPUIDが<code>1</code>のコアはプログラマが必要に応じて起動することになっている。このためCPUIDが<code>1</code>のコアは起動してすぐに待機状態に入ることがデータシートに書かれている。しかしこの処理はユーザーの書いたプログラムじゃなくて内蔵ROMにある起動用プログラムが担当するみたいに書かれてるんやけど、なんでSDKではユーザープログラムの一部として組み込んでるんかな? -</p> -<pre><code>100001f6 &lt;_reset_handler&gt;: -100001f6: 481d ldr r0, [pc, #116] ; (1000026c &lt;hold_non_core0_in_bootrom+0xe&gt;) -100001f8: 6800 ldr r0, [r0, #0] -100001fa: 2800 cmp r0, #0 -100001fc: d12f bne.n 1000025e &lt;hold_non_core0_in_bootrom&gt; -</code></pre> -<p>上のコードの最初の<code>ldr</code>は、<code>0xd0000000</code>(M0PLUS: CPUIDレジスタ)をロードしている。最後の飛び先<code>0x1000025e</code>はCPUIDが<code>1</code>のCPUを待機させる処理である:</p> -<pre><code>1000025e &lt;hold_non_core0_in_bootrom&gt;: -1000025e: 4809 ldr r0, [pc, #36] ; (10000284 &lt;hold_non_core0_in_bootrom+0x26&gt;) -10000260: f001 fb9c bl 1000199c &lt;rom_func_lookup&gt; -10000264: 4700 bx r0 -10000266: 0000 .short 0x0000 -/* ... */ -10000284: 00005657 .word 0x00005657 -</code></pre> -<p>内蔵フラッシュに書きこまれた関数を呼びだしている。呼びだしに使うコードは<code>0x00005657</code>(<code>'W' | 'V' &lt;&lt; 8</code>)である。データシートを見ると、この関数は<code>_wait_for_vector()</code>という名前で、CPUIDが1のCPUを寝かしつけるのに使われると書いている。この部分のソースコードをpico-sdkで探すと<code>pico-sdk/src/rp2_common/pico_standard_link/crt0.S</code>というのが見付かった:</p> -<pre><code>$ find pico-sdk/src -type f | xargs grep -l _reset_handler -pico-sdk/src/rp2_common/pico_standard_link/crt0.S -</code></pre> -<p>このファイルによると: -</p> -<pre><code> // 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 -</code></pre> -<p>だそうである。やっぱり無駄やん。内蔵フラッシュのプログラムにバグがあってもこのコードのせいで見付かりにくくなってない?知らんけど。</p> - -<p>続いて<code>.data</code>領域と<code>.bss</code>領域のコピー、初期化のようである。多分OSの本かなんかで習ったメモリマップの話:</p> -<pre><code>100001fe: a40d add r4, pc, #52 ; (adr r4, 10000234 &lt;data_cpy_table&gt;) -10000200: cc0e ldmia r4!, {r1, r2, r3} -10000202: 2900 cmp r1, #0 -10000204: d002 beq.n 1000020c &lt;_reset_handler+0x16&gt; -10000206: f000 f812 bl 1000022e &lt;data_cpy&gt; -1000020a: e7f9 b.n 10000200 &lt;_reset_handler+0xa&gt; -1000020c: 4918 ldr r1, [pc, #96] ; (10000270 &lt;hold_non_core0_in_bootrom+0x12&gt;) -1000020e: 4a19 ldr r2, [pc, #100] ; (10000274 &lt;hold_non_core0_in_bootrom+0x16&gt;) -10000210: 2000 movs r0, #0 -10000212: e000 b.n 10000216 &lt;bss_fill_test&gt; - -10000214 &lt;bss_fill_loop&gt;: -10000214: c101 stmia r1!, {r0} - -10000216 &lt;bss_fill_test&gt;: -10000216: 4291 cmp r1, r2 -10000218: d1fc bne.n 10000214 &lt;bss_fill_loop&gt; -</code></pre> - -<p>最後にいろいろ呼びだす:</p> -<pre><code>1000021a &lt;platform_entry&gt;: -1000021a: 4917 ldr r1, [pc, #92] ; (10000278 &lt;hold_non_core0_in_bootrom+0x1a&gt;) -1000021c: 4788 blx r1 -1000021e: 4917 ldr r1, [pc, #92] ; (1000027c &lt;hold_non_core0_in_bootrom+0x1e&gt;) -10000220: 4788 blx r1 -10000222: 4917 ldr r1, [pc, #92] ; (10000280 &lt;hold_non_core0_in_bootrom+0x22&gt;) -10000224: 4788 blx r1 -10000226: be00 bkpt 0x0000 -10000228: e7fd b.n 10000226 &lt;platform_entry+0xc&gt; -/* ... */ -10000278: 10001819 .word 0x10001819 -1000027c: 100002dd .word 0x100002dd -10000280: 10001909 .word 0x10001909 -</code></pre> -<p>一つめの<code>blx</code>は<code>0x10001818</code>(<code>runtime_init</code>)を、二つめは<code>0x100002dc</code>(<code>main</code>)を、最後のは<code>0x10001908</code>(<code>exit</code>)を、それぞれ呼んでいる。この<code>runtime_init</code>はアセンブリでは分かりにくいのでソースコードを探してみると、以下のものが見付かった:</p> -<pre><code>$ 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 -</code></pre> -<p>最後の<code>mutex.h</code>は関係なさそう。二つめの<code>crt0.S</code>は呼びだしてるだけ。一つめの<code>runtime.c</code>が多分探しているものである。これを見るとまず各種周辺機器を一度リセットし、リセット状態を解除している。使わんやつも初期化してない?その後<code>clocks_init()</code>を呼んでいる。この関数は<code>pico-sdk/src/rp2_common/hardware_clocks/clocks.c</code>で定義されている。これを見ると、<code>xosc_init()</code>を呼んで水晶発振子を初期化した後、<code>clk_peri</code>を125MHzに設定している:</p> -<pre><code> clock_configure(clk_peri, - 0, - CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLK_SYS, - 125 * MHZ, - 125 * MHZ); -</code></pre> -<p>やっぱり水晶発振子じゃないとあかんのかな。</p> - -<h2>CMake</h2> -<p>上ではビルドしたバイナリを逆アッセンブルして読んだ。わざわざこんなことをしなくてもMakefile読めばなにがどうなって最終生成物に辿りつくのか分かればいいのだが、そうもいかない。このSDKとpico-examplesにはビルドシステムとしてCMakeなるものが使われている。これがどうも複雑でよく分からない。勉強する気にもならん。上で見た<code>crt0.S</code>や<code>runtime.c</code>といったファイルも<code>hello_uart</code>で本当に使われているものなのかもよく分からない。こんな煩雑なものは本当に必要なのかな。無駄に複雑にしてるだけとちゃうんかな。特に僕は勉強用に使ってるので、ソースコードの依存関係をもっと分かりやすくしてくれないと、内部でなにがどうなってるのか理解しにくい。何度か頑張って読もうとしたが、面白くないのでやめた。数百行のファイルをあっちからこっちから<code>include</code>してるし、大文字ばかりの変数だらけで目が痛い。こんなものを扱えるというのはえらいええ頭してはるんやね。</p> - - -<h2>参考</h2> -<ul> -<li> -<a href="https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf">RP2040 Datasheet.Raspberry Pi Foundation</a> -</li> -<li> -<a href="https://github.com/raspberrypi/pico-sdk">pico-sdk.github</a> -</li> -<li> -<a href="https://developer.arm.com/documentation/ddi0419/c/">ARMv6-M Architecture Reference Manual</a> -</li> -<li> -<a href="https://ja.wikipedia.org/wiki/%E3%83%AA%E3%83%B3%E3%82%B0%E3%83%BB%E3%82%AA%E3%82%B7%E3%83%AC%E3%83%BC%E3%82%BF">リング・オシレータ.Wikipedia</a> -</li> -<li> -<a href="https://www5.epsondevice.com/ja/information/technical_info/osc.html">水晶発振器とは? 原理と仕組み、水晶振動子との違い、選び方のポイントを解説.エプソン水晶デバイス</a> -</li> -</ul> - </article> - - </main> - <footer> - <address>info(at)mtkn(dot)jp</address> - <a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" rel="license noopener noreferrer">CC0 1.0</a> - </footer> -</body> -</html> diff --git a/pub/index.html b/pub/index.html @@ -33,6 +33,7 @@ <h2>更新履歴</h2> <a href="/rss.xml">RSS</a> <ul> +<li>2024-02-22 <a href="/computer/rp2040_2.html">RP2040 SDKなし2 Clock、UART</a></li> <li>2024-02-15 <a href="/computer/git_server.html">Gitサーバーの設定 on OpenBSD</a></li> <li>2024-02-09 <a href="/kitchen/recipe/miso.html">味噌</a></li> <li>2024-01-19 <a href="/computer/mailserver.html">メールサーバー構築 on OpenBSD with OpenSMTPD and Dovecot</a></li> diff --git a/pub/rss.xml b/pub/rss.xml @@ -5,432 +5,159 @@ <description>ウェブページの更新履歴</description> <language>ja-jp</language> <link>https://www.mtkn.jp</link> -<lastBuildDate>Wed, 21 Feb 2024 17:30:26 +0900</lastBuildDate> -<pubDate>Wed, 21 Feb 2024 17:30:26 +0900</pubDate> +<lastBuildDate>Sat, 24 Feb 2024 08:11:56 +0900</lastBuildDate> +<pubDate>Sat, 24 Feb 2024 08:11:56 +0900</pubDate> <docs>https://www.rssboard.org/rss-specification</docs> <item> -<title>Gitサーバーの設定 on OpenBSD</title> -<link>https://www.mtkn.jp/computer/git_server.html</link> -<guid>https://www.mtkn.jp/computer/git_server.html</guid> -<pubDate>Thu, 15 Feb 2024 00:00:00 +0900</pubDate> -<description><![CDATA[<h1>Gitサーバーの設定 on OpenBSD</h1> -<time>2024-02-15</time> +<title>RP2040 SDKなしでLチカ</title> +<link>https://www.mtkn.jp/computer/rp2040_1.html</link> +<guid>https://www.mtkn.jp/computer/rp2040_1.html</guid> +<pubDate>Sat, 24 Feb 2024 00:00:00 +0900</pubDate> +<description><![CDATA[<h1>RP2040 SDKなしでLチカ</h1> +<time>2023-04-25</time>: 作成<br /> +<time>2024-02-24</time>: ベクターテーブルの修正 <h2>はじめに</h2> <p> -GitHubがMicrosoft傘下になり久しい。MincraftがMicrosoftアカウントなしでは遊べなくなった。このままではGitHubもそのうちMicrosoftアカウントを要求するようになるかもしれない。ということでGitサーバーを自前で持つことにした。</p> +パタヘネのRISC-V<sup>[1]</sup>版を買って一通り読んだらアセンブリ言語で組込のプログラミングがしたくなった。RISC-Vのマイコンボードが欲しかったのだが、安くていい感じのものが見付からなかった。代わりに秋月電子通商でArmのものがあった。RP2040マイコンボードキット<sup>[2]</sup>というものである。ウェブ上の情報も多く、データシート<sup>[3]</sup>もしっかりしていそうなので、とりあえずこれを買ってみた。</p> <p> -ところでOpenBSDの開発者がGotという別のgit実装を作成しているので、この記事は近いうちにいらなくなりそう。 -</p> - -<h2>手順</h2> +一般的にはSDK<sup>[4]</sup>をダウンロードしてあらかじめ用意されたライブラリを使って開発するようだが、これはビルドシステムとしてcmakeというのを使っている。これがOpenBSDでは何かエラーがでて動かなかった。僕はこういう便利ツールが嫌いだ。どうせ使わんからいいんやけど。関係ないけど途中から開発環境がLinuxに替わった。SDKには便利な関数がたくさん用意されているので楽である。ハードウェアの面倒な部分がプログラマから見えないようにしているからである。しかし今回はその面倒な部分に触れてみたくて買ったので、SDKを使うと意味がない。</p> <p> -以下ではssh接続によるpull/push及び、httpsによるpullを設定の上、stagitというウェブフロントエンドを導入する。 +ということでSDKなしで開発してみる。とりあえず定番のLチカをば。</p> +<p> +ソースコード: <a href="https://git.mtkn.jp/rp2040">git</a> </p> -<h3>概要</h3> -<ol> -<li>ドメインの設定(任意)</li> -<li><code>httpd(8)</code>の設定</li> -<li>gitパッケージのインストールとchroot環境の整備</li> -<li>stagitの導入</li> -</ol> +<h2>動作環境</h2> +<ul> +<li>Arch Linux 6.2.12-arch1-1 + <ul> + <li>arm-none-eabi-binutils 2.40-1</li> + <li>GNU Make 4.4.1</li> + </ul> +</li> +<li>OpenBSD 7.3 + <ul> + <li>arm-none-eabi-binutils 2.31.1</li> + <li>make (バージョン?)</li> + </ul> +※<code>make flash</code>は動かん。<code>dmesg</code>でデバイス確認して手動でマウントする必要がある。 +</li> +</ul> -<h3>ドメインの設定(任意)</h3> -<p> -ドメインを設定する。IPアドレスでアクセスできればいいならこの設定はいらない。</p> +<h2>Boot Process</h2> <p> -今回はgitというサブドメインを登録した: +RP2040は電源を入れるといくつかの段階(ここでは関係ないので省略。データシート「2.8.1 Processor Controlled Boot Sequence」に詳しく書いてある)を踏んだあと、外部のフラッシュROMの先頭から256バイトを内部のSRAMにコピーして、フラッシュにプログラムが書き込まれているかどうか確認する。RP2040はフラッシュの先頭252バイトから計算したCRC32チェックサムを、直後の253バイト目から256バイトに記録することになっている。起動時にこのチェックサムを確認することで、フラッシュにプログラムが書き込まれているかどうか確かめている。コピーした最後の4バイトと起動時に最初の252バイトから計算したチェックサムが一致していれば、そのままコピーしてきた256バイトの先頭にPCをセットして実行を開始する。一致しなければUSBデバイスモードに切り替わり、パソコンに接続するとストレージとして認識される。このストレージにUF2という形式に変換したプログラムをコピーするとプログラムがフラッシュROMやSRAMに書き込まれる。 </p> -<table> -<thead> -<tr> -<th>サブドメイン</th> -<th>種別</th> -<th>内容</th> -<th>優先度</th> -</tr> -</thead> -<tbody> -<tr> -<td>git</td> -<td>A</td> -<td><i>サーバーのIPアドレス</i></td> -<td></td> -</tr> -</tbody> -</table> - -<h3><code>httpd(8)</code>の設定</h3> <p> -httpsで接続する場合、<code>acme-client(8)</code>を設定する(IPアドレスで接続するなら自己証明書を発行することになる)。まず<code>/etc/acme-client.conf</code>にドメインを追加する: +以上のことから、プログラムを実行するためにはCRC32を計算し、UF2という形式に変換することが必要である。ソースコードからの流れは以下の通り: </p> -<pre><code>domain git.mtkn.jp { - domain key "/etc/ssl/private/git.mtkn.jp.key" - domain full chain certificate "/etc/ssl/git.mtkn.jp.fullchain.pem" - sign with letsencrypt -} -</code></pre> - -<p> -続いて<code>/etc/httpd.conf</code>を設定する。</p> -<pre><code>server "git.mtkn.jp" { - listen on * port 80 - location "/.well-known/acme-challenge/*" { - root "/acme" - request strip 2 - } - location "*" { - block return 302 "https://$HTTP_HOST$REQUEST_URI" - } -} +<pre>source bin bin with +code ----------> object ------> elf --------> bin -------> with --------> crc32 in + crc32 uf2 format + assemble link objcopy bincrc bin2uf2 +</pre> -server "git.mtkn.jp" { - listen on * tls port 443 - tls { - certificate "/etc/ssl/git.mtkn.jp.fullchain.pem" - key "/etc/ssl/private/git.mtkn.jp.key" - } - location "/.well-known/acme-challenge/*" { - root "/acme" - request strip 2 - } - location "/*/git-receive-pack" { - block - } - location "/*/git-upload-pack" { - fastcgi { - param SCRIPT_FILENAME "/usr/local/libexec/git/git-http-backend" - param GIT_PROJECT_ROOT "/git" - param GIT_HTTP_EXPORT_ALL "true" - } - } - location "/*/info/refs" { - fastcgi { - param SCRIPT_FILENAME "/usr/local/libexec/git/git-http-backend" - param GIT_PROJECT_ROOT "/git" - param GIT_HTTP_EXPORT_ALL "true" - } - } -} -</code></pre> +<h2>CRC(巡回冗長検査)</h2> <p> -<code>location "/*/git-receive-pack"</code>はpushの設定。今回はhttpsでのpushを受付けないので<code>block</code>する。</p> +入力のデータをごにょごにょしてある値を出力する。</p> +<blockquote cite="https://ja.wikipedia.org/wiki/%E5%B7%A1%E5%9B%9E%E5%86%97%E9%95%B7%E6%A4%9C%E6%9F%BB"> <p> -<code>location "/*/git-upload-pack"</code>は手元のパソコンから<code>git clone</code>や<code>git pull</code>したときのもの。<code>location "/*/info/refs"</code>は上記を含むアクセスがあったときのもの。ここではCGIを使って<code>git-http-backend</code>を呼んでいるだけである。<code>SCRIPT_FILENAME</code>は<code>httpd(8)</code>のchroot環境でのパスである。必要なファイルは後でこのchroot環境にコピーする。 +データ転送等に伴う偶発的な誤りの検査によく使われている<sup>[5]</sup>。 </p> +</blockquote> <p> -この後同じURLでフロントエンドをホストしたいので、上記のように必要なURLからのみCGIを実行するようにした。gitのhttpクライアントがどのURLを利用しているのかは[2]に書いていた。</p> - - - -<p> -httpsが必要ない場合は1つ目の<code>server</code>に各<code>location</code>を書くだけでいい。 +らしい。 </p> <p> -<code>httpd(8)</code>と<code>slowcgi(8)</code>を起動する: +入力のビットを一列に並べて、除数で「割り算」していく。この「割り算」が多項式の除算に似ているので、この除数をCRC多項式というらしい。ただし多項式の除算と違い、引き算するところをXORする。CRC32の場合、除数は33ビットである。33ビットで割ると32ビットの余りが残る。この余りがCRC32のチェックサムである。除数は色々あるようだが、標準的なものがWikipedia<sup>[5]</sup>に列挙されている。除数<code>1011</code>を使ったCRC3の計算の手順は以下の通り: </p> -<pre><code># echo httpd_flags= &gt;&gt; /etc/rc.conf.local -# echo slowcgi_flags= &gt;&gt; /etc/rc.conf.local -# rcctl start httpd -# rcctl enable httpd -# rcctl start slowcgi -# rcctl enable slowcgi +<pre><code>1110101011011100110101101101111 入力(適当) +1011 除数(4ビット) +------------------------------- + 101101011011100110101101101111 結果(入力と除数のXOR) + 1011 + ------------------------------ + 00001011011100110101101101111 + 1011 + ------------------------- + 000011100110101101101111 + 1011 + -------------------- + 1010110101101101111 + 1011 + ------------------- + 001110101101101111 + 1011 + ---------------- + 101101101101111 + 1011 + --------------- + 00001101101111 + 1011 + ---------- + 110101111 + 1011 + --------- + 11001111 + 1011 + -------- + 1111111 + 1011 + ------- + 100111 + 1011 + ------ + 01011 + 1011 + ---- + 000 CRC3チェックサム </code></pre> <p> -サーバー証明書の発行: -</p> -<pre><code># acme-client -v git.mtkn.jp -</code></pre> +普通の割り算と基本は同じであるが、引き算の部分だけXORになっている。</p> <p> -<code>crontab(1)</code>にサーバー証明書の自動更新を登録: -</p> -<pre><code>#minute hour mday month wday [flags] command -~ 2 * * * acme-client git.mtkn.jp && rcctl reload httpd -</code></pre> - -<h3>gitパッケージのインストールとchroot環境の整備</h3> +以上の計算をプログラムの先頭252バイトに対して、33ビットの除数を用いて行う。データの並べ方は、上の例において左側を先頭に、フラッシュROM上の0番地から、各バイトは最上位ビットから順に並べる。入力のデータは253バイト目から256バイト目に<code>0</code>をひっつけて計算する。これは多分予め長さが分からないデータでも計算できるようにしたかったからかな。除数は<code>0x104c11db7</code>である(最上位ビットは常に1なのでデータシートでは省略されている)。</p> <p> -gitパッケージをインストールし、ssh接続用のユーザーを作成する。httpsでも公開するので、gitユーザーのホームディレクトリは<code>/var/www</code>下にする: +入力データは1バイトづつ処理したいみたいである。多分通信等で使う都合である。この時XORは結合則が成り立つので1バイト処理した結果と次のバイトとをXORして次の処理の入力として利用することができる: </p> -<pre><code># pkg_add git -# useradd -b /var/www -m -s /usr/local/bin/git-shell git +<pre><code>111000111000000110000110111000111000001010010011111000111000000110010011 入力(適当) +|......| +111000110000000000000000000000000 先頭1バイト +100000100110000010001110110110111 除数 +------------------------------------------------------------------------ +011000010110000010001110110110111 + 100000100110000010001110110110111 + ----------------------------------------------------------------------- + 010000001010000110010011011011001 + 100000100110000010001110110110111 + ---------------------------------------------------------------------- + 000000110010001110101000000000101 +|......| + 110010001110101000000000101000000 1バイト目の結果 + |......| + 10000001 入力の2バイト目 + ---------------------------------------------------------------- + 010010011110101000000000101000000 1バイト目の結果と2バイト目のXOR + 100000100110000010001110110110111 除数 + ---------------------------------------------------------------- + 000100011011010010001111100110111 + . + . + . </code></pre> <p> -ssh接続用の公開鍵を<code>/var/www/git/.ssh/authorized_keys</code>に登録する。ところでこのファイルに公開鍵を書き込むとgitユーザーとしてsshでログインできるので、部外者がこのファイルの編集をできないようにしておく必要がある。一応所有者は<code>git:git</code>、権限は<code>-rw-------</code>なので大丈夫だと思うが、心配なら<code>httpd(8)</code>のchroot環境の外にこのファイルを移動させておいてもいいかもしれない。</p> -<p> -httpsでアクセスするためにchroot環境を整備する。<code>httpd(8)</code>は既定では<code>/var/www</code>にchrootして実行されるので、CGIに必要なファイルをこのディレクトリ以下に用意する必要がある。まずはgitのコマンドをコピー: -</p> -<pre><code># mkdir -p /var/www/usr/local/libexec/git -# cp /usr/local/libexec/git/git-{http-backend,receive-pack,upload-pack} /var/www/usr/local/libexec/git/ -# chown www:www /var/www/usr/local/libexec/git/git-{http-backend,receive-pack,upload-pack} -# chmod 0500 /var/www/usr/local/libexec/git/git-{http-backend,receive-pack,upload-pack} -# mkdir -p /var/www/usr/local/bin -# cp /usr/local/bin/git /var/www/usr/local/bin/ -# chown www:www /var/www/usr/local/bin/git -# chmod 0500 /var/www/usr/local/bin/git -</code></pre> +以上の操作は以下のようなアルゴリズムのループで実装できる。</p> +<ul> +<li>前回の結果と、入力データの次のバイトをXOR</li> +<li> + <ul> + <li>先頭の1ビットが1の場合、除数とXORを取り左シフト</li> + <li>先頭の1ビットが0の場合、そのまま左シフト</li> + </ul> +</li> +</ul> <p> -続いてコマンドの実行に必要なライブラリをコピー: -</p> -<pre><code># mkdir -p /var/www/usr/lib /var/www/usr/local/lib /var/www/usr/libexec -# find /var/www/{bin,usr} -type f | grep git | xargs ldd | awk '{print $7}' | grep -v -e '^/var/www/' -e '^$' -e 'Name' | sort | uniq | awk '{printf &quot;cp %s /var/www%s &amp;&amp; chown www:www /var/www%s &amp;&amp; chmod 0400 /var/www%s\n&quot;, $1, $1, $1, $1}' | sh -s -</code></pre> -<p> -<code>/dev/null</code>をコピーする(<code>mknod(8)</code>参照): -</p> -<pre><code># mkdir /var/www/dev -# mknod -m 666 /var/www/dev/null c 2 2 -</code></pre> -<p> -最後に、<code>/var/www/dev/null</code>を作成するために<code>/etc/fstab</code>の<code>/var</code>エントリーから<code>nodev</code>オプションを削除し、再起動する。 -</p> - -<p> -gitパッケージやシステムの更新後、chroot環境のコマンドやライブラリも更新しないといけないのでそのためのスクリプトを適当に作った: -</p> -<pre><code>#!/bin/sh -xe - -oso=$(find /var/www/usr -type f -name '*.so*') -rm $oso - -bin=$(find /var/www/bin /var/www/usr -type f ! -name '*.so*' | - grep -v bgpctl | - sed 's|^/var/www||' - ) -echo &quot;$bin&quot; | sed 's|.*|cp &amp; /var/www&amp;|' | sh -s -echo &quot;$bin&quot; | sed 's|.*|chown www:www /var/www&amp;|' | sh -s -echo &quot;$bin&quot; | sed 's|.*|chmod 0500 /var/www&amp;|' | sh -s - -nso=$(echo &quot;$bin&quot; | sed 's|^|/var/www|' | - xargs ldd | awk '{print $7}' | - grep -v -e '^/var/www/' -e '^$' -e 'Name' | - sort | uniq - ) -echo &quot;$nso&quot; | sed 's|.*|cp &amp; /var/www&amp;|' | sh -s -echo &quot;$nso&quot; | sed 's|.*|chown www:www /var/www&amp;|' | sh -s -echo &quot;$nso&quot; | sed 's|.*|chmod 0400 /var/www&amp;|' | sh -s -</code></pre> - -<h3><code>stagit(1)</code>の導入</h3> -<p> -ウェブフロントエンドとしてstagitを導入する: -</p> -<pre><code># pkg_add stagit -</code></pre> -<p> -<code>httpd.conf(5)</code>の<code>server "git.mtkn.jp"</code>の中に以下の設定を追加する(<code>location</code>のマッチは上から順番に評価されるので、上で設定したgitのhttpクライアント用の<code>location</code>よりも下に記入する): -</p> -<pre><code> location &quot;/&quot; { - directory index index.html - root &quot;/htdocs/git.mtkn.jp&quot; - } - location &quot;*&quot; { - directory index log.html - root &quot;/htdocs/git.mtkn.jp&quot; - } -</code></pre> -<p> -stagit用のディレクトリを作成して<code>httpd(8)</code>を再読込する: -</p> -<pre><code># mkdir /var/www/htdocs/git.mtkn.jp -# chown git:git /var/www/htdocs/git.mtkn.jp -# rcctl reload httpd -</code></pre> - -<p> -gitリポジトリが更新されたときにウェブページも更新するように設定する。gitリポジトリはなにか更新があった場合、そのリポジトリのディレクトリの中の<code>hooks/post-receive</code>というファイルを自動で実行する。そのためこのファイルに、stagitの更新をするスクリプトを書いておけばいい: -</p> -<pre><code>#!/bin/sh - -git_root=&quot;/var/www/git&quot; -stagit_root=&quot;/var/www/htdocs/git.mtkn.jp&quot; -repo=&quot;$(basename &quot;$(pwd)&quot; | sed 's/\.git$//')&quot; -src=&quot;$(pwd)&quot; -stagit_dst=&quot;$stagit_root/$repo&quot; - -mkdir -p &quot;$stagit_dst&quot; -(cd &quot;$stagit_dst&quot; &amp;&amp; stagit -l 64 &quot;$src&quot;) -(cd &quot;$stagit_root&quot; &amp;&amp; stagit-index $git_root/*.git &gt; index.html) -</code></pre> - -<h2>レポジトリの作成</h2> -<p> -レポジトリを作成するにはサーバーで以下のようにする。</p> -<pre><code>$ cd /var/www/git -$ doas -u git mkdir <i>repo</i>.git -$ cd <i>repo</i>.git -$ doas -u git git init --bare -</code></pre> - -<p> -これで手元のパソコンからクローンできる: -</p> -<pre><code>$ git clone git@git.mtkn.jp:<i>repo</i>.git -</code></pre> -<p> -または -</p> -<pre><code>$ git clone https://git.mtkn.jp/<i>repo</i>.git -</code></pre> - -<h2>参考</h2> -<ul> -<li>[1] <a href="https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols">Git - The Protocols.Git</a></li> -<li>[2] <a href="https://git-scm.com/docs/http-protocol">Git - http-protocol Documentation.Git</a></li> -<li>[3] <a href="https://codemadness.org/stagit.html">Stagit: a static git page generator - Codemadness</a></li> -</ul> -]]></description> -</item> -<item> -<title>RP2040 SDKなしでLチカ</title> -<link>https://www.mtkn.jp/computer/rp2040_1.html</link> -<guid>https://www.mtkn.jp/computer/rp2040_1.html</guid> -<pubDate>Sat, 10 Feb 2024 00:00:00 +0900</pubDate> -<description><![CDATA[<h1>RP2040 SDKなしでLチカ</h1> -<time>2023-04-25</time> - -<h2>はじめに</h2> -<p> -パタヘネのRISC-V<sup>[1]</sup>版を買って一通り読んだらアセンブリ言語で組込のプログラミングがしたくなった。RISC-Vのマイコンボードが欲しかったのだが、安くていい感じのものが見付からなかった。代わりに秋月電子通商でArmのものがあった。RP2040マイコンボードキット<sup>[2]</sup>というものである。ウェブ上の情報も多く、データシート<sup>[3]</sup>もしっかりしていそうなので、とりあえずこれを買ってみた。</p> -<p> -一般的にはSDK<sup>[4]</sup>をダウンロードしてあらかじめ用意されたライブラリを使って開発するようだが、これはビルドシステムとしてcmakeというのを使っている。これがOpenBSDでは何かエラーがでて動かなかった。僕はこういう便利ツールが嫌いだ。どうせ使わんからいいんやけど。関係ないけど途中から開発環境がLinuxに替わった。SDKには便利な関数がたくさん用意されているので楽である。ハードウェアの面倒な部分がプログラマから見えないようにしているからである。しかし今回はその面倒な部分に触れてみたくて買ったので、SDKを使うと意味がない。</p> -<p> -ということでSDKなしで開発してみる。とりあえず定番のLチカをば。</p> -<p> -ソースコード: <a href="https://git.mtkn.jp/rp2040">git</a> -</p> - -<h2>動作環境</h2> -<ul> -<li>Arch Linux 6.2.12-arch1-1 - <ul> - <li>arm-none-eabi-binutils 2.40-1</li> - <li>GNU Make 4.4.1</li> - </ul> -</li> -<li>OpenBSD 7.3 - <ul> - <li>arm-none-eabi-binutils 2.31.1</li> - <li>make (バージョン?)</li> - </ul> -※<code>make flash</code>は動かん。<code>dmesg</code>でデバイス確認して手動でマウントする必要がある。 -</li> -</ul> - -<h2>Boot Process</h2> -<p> -RP2040は電源を入れるといくつかの段階(ここでは関係ないので省略。データシート「2.8.1 Processor Controlled Boot Sequence」に詳しく書いてある)を踏んだあと、外部のフラッシュROMの先頭から256バイトを内部のSRAMにコピーして、フラッシュにプログラムが書き込まれているかどうか確認する。RP2040はフラッシュの先頭252バイトから計算したCRC32チェックサムを、直後の253バイト目から256バイトに記録することになっている。起動時にこのチェックサムを確認することで、フラッシュにプログラムが書き込まれているかどうか確かめている。コピーした最後の4バイトと起動時に最初の252バイトから計算したチェックサムが一致していれば、そのままコピーしてきた256バイトの先頭にPCをセットして実行を開始する。一致しなければUSBデバイスモードに切り替わり、パソコンに接続するとストレージとして認識される。このストレージにUF2という形式に変換したプログラムをコピーするとプログラムがフラッシュROMやSRAMに書き込まれる。 -</p> -<p> -以上のことから、プログラムを実行するためにはCRC32を計算し、UF2という形式に変換することが必要である。ソースコードからの流れは以下の通り: -</p> -<pre>source bin bin with -code ----------> object ------> elf --------> bin -------> with --------> crc32 in - crc32 uf2 format - assemble link objcopy bincrc bin2uf2 -</pre> - -<h2>CRC(巡回冗長検査)</h2> -<p> -入力のデータをごにょごにょしてある値を出力する。</p> -<blockquote cite="https://ja.wikipedia.org/wiki/%E5%B7%A1%E5%9B%9E%E5%86%97%E9%95%B7%E6%A4%9C%E6%9F%BB"> -<p> -データ転送等に伴う偶発的な誤りの検査によく使われている<sup>[5]</sup>。 -</p> -</blockquote> -<p> -らしい。 -</p> -<p> -入力のビットを一列に並べて、除数で「割り算」していく。この「割り算」が多項式の除算に似ているので、この除数をCRC多項式というらしい。ただし多項式の除算と違い、引き算するところをXORする。CRC32の場合、除数は33ビットである。33ビットで割ると32ビットの余りが残る。この余りがCRC32のチェックサムである。除数は色々あるようだが、標準的なものがWikipedia<sup>[5]</sup>に列挙されている。除数<code>1011</code>を使ったCRC3の計算の手順は以下の通り: -</p> -<pre><code>1110101011011100110101101101111 入力(適当) -1011 除数(4ビット) -------------------------------- - 101101011011100110101101101111 結果(入力と除数のXOR) - 1011 - ------------------------------ - 00001011011100110101101101111 - 1011 - ------------------------- - 000011100110101101101111 - 1011 - -------------------- - 1010110101101101111 - 1011 - ------------------- - 001110101101101111 - 1011 - ---------------- - 101101101101111 - 1011 - --------------- - 00001101101111 - 1011 - ---------- - 110101111 - 1011 - --------- - 11001111 - 1011 - -------- - 1111111 - 1011 - ------- - 100111 - 1011 - ------ - 01011 - 1011 - ---- - 000 CRC3チェックサム -</code></pre> -<p> -普通の割り算と基本は同じであるが、引き算の部分だけXORになっている。</p> -<p> -以上の計算をプログラムの先頭252バイトに対して、33ビットの除数を用いて行う。データの並べ方は、上の例において左側を先頭に、フラッシュROM上の0番地から、各バイトは最上位ビットから順に並べる。入力のデータは253バイト目から256バイト目に<code>0</code>をひっつけて計算する。これは多分予め長さが分からないデータでも計算できるようにしたかったからかな。除数は<code>0x104c11db7</code>である(最上位ビットは常に1なのでデータシートでは省略されている)。</p> -<p> -入力データは1バイトづつ処理したいみたいである。多分通信等で使う都合である。この時XORは結合則が成り立つので1バイト処理した結果と次のバイトとをXORして次の処理の入力として利用することができる: -</p> -<pre><code>111000111000000110000110111000111000001010010011111000111000000110010011 入力(適当) -|......| -111000110000000000000000000000000 先頭1バイト -100000100110000010001110110110111 除数 ------------------------------------------------------------------------- -011000010110000010001110110110111 - 100000100110000010001110110110111 - ----------------------------------------------------------------------- - 010000001010000110010011011011001 - 100000100110000010001110110110111 - ---------------------------------------------------------------------- - 000000110010001110101000000000101 -|......| - 110010001110101000000000101000000 1バイト目の結果 - |......| - 10000001 入力の2バイト目 - ---------------------------------------------------------------- - 010010011110101000000000101000000 1バイト目の結果と2バイト目のXOR - 100000100110000010001110110110111 除数 - ---------------------------------------------------------------- - 000100011011010010001111100110111 - . - . - . -</code></pre> -<p> -以上の操作は以下のようなアルゴリズムのループで実装できる。</p> -<ul> -<li>前回の結果と、入力データの次のバイトをXOR</li> -<li> - <ul> - <li>先頭の1ビットが1の場合、除数とXORを取り左シフト</li> - <li>先頭の1ビットが0の場合、そのまま左シフト</li> - </ul> -</li> -</ul> -<p> -これを<code>for</code>ループで回す都合上、最初のバイトもXORを取る。上の例では最初は<code>0x0</code>とXORを取っているが、この値を<code>0x0</code>以外にすることもできる。そうした方がいろいろいいこともあるらしい。RP2040では<code>0xffffffff</code>を使う。更にこの工程を32ビットの<code>int</code>だけで行うことを考える: +これを<code>for</code>ループで回す都合上、最初のバイトもXORを取る。上の例では最初は<code>0x0</code>とXORを取っているが、この値を<code>0x0</code>以外にすることもできる。そうした方がいろいろいいこともあるらしい。RP2040では<code>0xffffffff</code>を使う。更にこの工程を32ビットの<code>int</code>だけで行うことを考える: </p> <pre><code>111000111000000110000110111000111000001010010011111000111000000110010011 入力(適当) @@ -690,313 +417,985 @@ defer: return retnum; } </code></pre> -<p><code>fwrite32l()</code>関数は指定されたファイルに32ビットの整数を下位バイトから順に書き込む関数である。バイトオーダーとかややこしそうなので作っておいたけど必要なのかな?あと名前が気に入らない。</p> +<p><code>fwrite32l()</code>関数は指定されたファイルに32ビットの整数を下位バイトから順に書き込む関数である。バイトオーダーとかややこしそうなので作っておいたけど必要なのかな?あと名前が気に入らない。</p> +<p> +CRC32のチェックサムが書き込まれたバイナリファイルを、このプログラムでUF2に変換し、生成されたファイルをUSBストレージとして接続したRP2040にコピーすればフラッシュROMに書き込まれる。 +</p> + +<h2>Flash Second Stage</h2> +<p> +RP2040に電源を投入し、CRC32のチェックが通った後、フラッシュROMからコピーされたプログラムの先頭から実行が開始される。このコピーされた部分で、その後の動作に必要な各種の設定を行うことになる。RP2040のデータシートには、フラッシュROMとSSIコントローラのXIPを設定するようにと書かれている。XIPはExecute in Placeの略で、フラッシュROMの内容をCPUから直接実行するものである。SSIはSynchronous Serial Interfaceの略で、周辺機器と情報のやりとりをする通信方式である。RP2040はチップに内蔵されたこのSSIコントローラを通して、外部のフラッシュROMと通信しているのだが、このコントローラを適切に設定すればフラッシュROMの内容がCPUから直接アクセスできる<code>0x10000000</code>番地以降にマップされる。これによりフラッシュROMから内部のSRAMにデータをコピーすることなく命令を実行できるので、速くて便利だという。 +</p> +<p> +しかしこのSSIコントローラはSynopsysという会社のDW_apb_ssiというIPを使っているようで、データシートのSSIコントローラの章は多分Synopsysの人が書いている。その他の章はRaspberry Pi財団の書いたブリティッシュイングリッシュだが、この部分だけ多分ネイティブじゃない人の書いたいい加減な英語である。誤植も多い。何日かかけて理解しようとしたがよく分からん。不毛なので一旦諦めた。</p> +<p> +RP2040には内部にもROMがあり、はバージョン情報や電源を投入した時の動作、その他便利な関数が書き込まれている。この関数の中に外部のフラッシュROMとSSIコントローラを設定するものも含まれているので、今回はこれを利用した。ただしこの方法だとフラッシュROMとの通信方式がStandard SPIのままなので少し遅いらしい。詳しくはデータシートの「2.3.8. Bootrom Contents」を参照。 +</p> +<p> +RP2040の内蔵ROMの<code>0x00000018</code>番地に関数を検索するための関数がある。この関数に<code>0x00000014</code>番地の<code>rom_func_table</code>と、各関数に割り当てられた二文字の文字列を渡せば、欲しい関数へのポインタが返ってくる。なお、二文字の文字列はそれぞれASCIIコードで現し、二文字目を8ビットシフトしたものと1文字目のORを取ったものを渡すことになっている。今回欲しい関数はフラッシュROMをXIPに設定するもの(<code>_flash_enter_cmd_xip()</code>)なので、<code>'C', 'X'</code>を渡す。関数のポインタが返ってきて、それを呼び出せばフラッシュROMとSSIはXIPモードになる: +</p> +<pre><code>setup_xip: + ldr r3, rom_base + + ldrh r0, [r3, #0x14] // rom_func_table + ldr r1, =('C' | 'X' &lt;&lt; 8) // _flash_enter_cmd_xip() + ldrh r2, [r3, #0x18] // rom_table_lookup + blx r2 + blx r0 +/* ... */ +rom_base: + .word 0x00000000 +</code></pre> + +<p> +XIPの設定が完了すれば、次はメインのプログラムを実行するための準備である。エントリーポイントの指定、スタックポインタの初期値の設定、ベクターテーブルの設定である。Armのマニュアル<sup>[7]</sup>によると、初期スタックポインタとエントリーポイントはベクターテーブルの<code>0x0</code>バイト目と<code>0x4</code>バイト目に書くことになっている:</p> +<blockquote cite="https://developer.arm.com/documentation/ddi0419/c/System-Level-Architecture/System-Level-Programmers--Model/ARMv6-M-exception-model/Exception-number-definition"> +<table> +<caption> +Table 7.3. Exception numbers +</caption><colgroup><col><col></colgroup><thead><tr><th>Exception number</th><th>Exception</th></tr></thead><tbody><tr><td>1</td><td>Reset</td></tr><tr><td>2</td><td>NMI</td></tr><tr><td>3</td><td>HardFault</td></tr><tr><td>4-10</td><td>Reserved</td></tr><tr><td>11</td><td>SVCall</td></tr><tr><td>12-13</td><td>Reserved</td></tr><tr><td>14</td><td>PendSV</td></tr><tr><td>15</td><td>SysTick, optional</td></tr><tr><td>16</td><td>External Interrupt(0)</td></tr><tr><td>...</td><td>...</td></tr><tr><td>16 + N</td><td>External Interrupt(N)</td></tr></tbody> +</table> +</blockquote> + +<blockquote cite="https://developer.arm.com/documentation/ddi0419/c/System-Level-Architecture/System-Level-Programmers--Model/ARMv6-M-exception-model/The-vector-table"> +<table> +<caption> +Table 7.4. Vector table format +</caption><colgroup><col><col></colgroup><thead><tr><th>Word offset in table</th><th>Description, for all pointer address values</th></tr></thead><tbody><tr><td>0</td><td>SP_main. This is the reset value of the Main stack pointer.</td></tr><tr><td>Exception Number</td><td>Exception using that Exception Number</td></tr></tbody> +</table> +</blockquote> +<p> +RP2040のベクターテーブルはM0PLUS: VTOR(<code>0xe0000000 + 0xed08</code>)というレジスタに書き込むことで設定する。このとき、下位8ビットは0にしないといけないので、ベクターテーブルの位置は256バイトでアラインする必要がある。ベクターテーブルの定義は<code>main.s</code>に書き、<code>boot2.s</code>からはラベルを使ってアクセスすることにする。以上をまとめると以下のコードになる:</p> +<pre><code> ldr r0, =vectors + ldr r1, m0plus_vtor + str r0, [r1, #0] // vector table + ldr r1, [r0, #4] // entry point + ldr r0, [r0, #0] // stack pointer + mov sp, r0 + bx r1 + +/* ... */ + +m0plus_vtor: + .word 0xe0000000 + 0xed08 +</code></pre> +<p>なお以上のコードは<code>.boot2</code>という名前のセクションにしてある。 +</p> + +<h2>メインのコード(<code>main.s</code>)</h2> + +<h3>GPIOの設定</h3> +<p> +電源投入直後、RP2040の周辺機器はリセット状態になっている。まずは今回利用するGPIOのリセット状態を解除する必要がある。データシートの「2.14. Subsystem Resets」には以下のように書かれている: +</p> +<blockquote cite="https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf"> +<p> +Every peripheral reset by the reset controller is held in reset at power-up. +It is up to software to deassert the reset of peripherals it intends to use. +</p> +</blockquote> +<p> +リセット状態を解除するには、RESETS_BASE(<code>0x4000c000</code>)から<code>0x0</code>バイト目のRESETS: RESETレジスタのうち利用したい周辺機器のビットを<code>0x0</code>にすればいい。 +GPIOはIO Bank 0なので(これ明記されてなくない?)、RESETS: RESETレジスタのIO_BANK0(5番ビット)を<code>0x0</code>にする。 +</p> +<h4>レジスタのアトミックなクリア</h4> +<p> +RESETS: RESETレジスタのうち5番ビットだけを<code>0x0</code>にしたい。この時、まずこのレジスタを読み込んでから<code>~(1 &lt;&lt; 5)</code>と論理積を取って同レジスタに書き戻してもいいのだが、RP2040にはこれを一回の<code>str</code>でしかもアトミックにできる機能が用意されている。今回の場合アトミックかどうかは関係ないと思うけど。</p> +<p> +各レジスタには4個のアドレスが割り当てられている。データシートの各章のList of Registersに記載されているアドレスは通常の読み書きができる。そのアドレスに<code>0x1000</code>を足したものにアクセスするとアトミックなXORが、<code>0x2000</code>を足したものはアトミックなセットが、<code>0x3000</code>を足したものはアトミックなクリアができる。つまりレジスタのアドレスに<code>0x3000</code>を足したものに、<code>0x1 &lt;&lt; 5</code>を<code>str</code>すれば5番目のビットだけ<code>0x0</code>にして、他のビットは変更されない。逆に指定したビットだけ立てて他を触らない場合は<code>0x2000</code>を、あるいは指定したビットだけトグルしたい場合は<code>0x1000</code>を足したアドレスにアクセスすればいい。</p> +<h4>リセット状態の確認</h4> +<p>リセットの解除はすぐに完了するわけではないようである。リセットの解除が完了したかどうか確認するにはRESETS: RESET_DONEレジスタ(RESETS_BASEから<code>0x8</code>バイト目)の該当するビット(ここでは5番目のビット)を読む。この値が<code>0x1</code>であればリセットの解除が完了している。<code>0x0</code>であれば処理が進行中なので<code>0x1</code>が返ってくるまで繰り返し読み込んで<code>0x0</code>になるまで待機する。ところでこのレジスタはリセットの解除が完了したかどうか確かめるものなので、RESET_DONEという名前はどうなん? +<p> +以上から、GPIOのリセットを解除するのは以下のコード: +</p> +<pre><code>reset: + // unreset gpio + mov r0, #1 + lsl r0, r0, #5 // io_bank0 + ldr r3, resets_base + ldr r1, atomic_clr + str r0, [r3, r1] // RESETS: RESET +reset_chk: + ldr r1, [r3, #0x8] // RESETS: RESET_DONE + tst r0, r1 + beq reset_chk + +/* ... */ + +atomic_clr: + .word 0x00003000 +resets_base: + .word 0x4000c000 +</code></pre> + +<h3>GPIOの機能の選択</h3> +<p>RP2040のGPIOにはそれぞれ複数の機能が用意されていて、どれを使うかはソフトウェアから選択できる。利用できる機能の一覧と各機能の説明はデータシートの「2.19.2 Function Select」に詳しく書いてある。ここではGPIO25番のピンをSIO(Single-cycle IO)として利用する。同じCPUが載っているRaspberry Pi PicoはGPIO25番にLEDが半田付けされている。25番にしたのはこれに合わせるためである。他のピンでもいい。GPIOに1か0を印加するだけならこのSIOを使うみたいである。Single-cycleはCPUから操作したときに1クロックでその操作が完了するという意味らしい(本当か)。SIOの詳しい説明はデータシートの「2.3.1 SIO」にある。</p> +<p> +GPIO25番の機能を選択するにはIO_BANK0_BASE(<code>0x40014000</code>)から<code>0xcc</code>番目のGPIO25_CTRLレジスタの下位5ビットに、該当する機能の番号を書き込めばいい。データシートの「2.19.2 Function Select」にある表を見ると、GPIO25番のSIOは5である:</p> +<pre><code> // set gpio functions + ldr r3, io_bank0_base + mov r0, #5 // sio + mov r1, #0xcc + str r0, [r3, r1] // IO_BANK0: GPIO25_CTRL + +/* ... */ + +io_bank0_base: + .word 0x40014000 +</code></pre> + +<h3>GPIOの出力を有効化</h3> +<p> +GPIO25番がSIOになったので、次にこのピンからの出力を有効化する。既定値では出力は無効になっている。ハイインピーダンスってことなのかな?出力を有効にするには、SIO_BASE(<code>0xd0000000</code>)から<code>0x24</code>バイト目のSIO: GPIO_OEレジスタの該当するビット(25番のピンなので25番ビット)を<code>0x1</code>にする: +</p> +<pre><code> // enable gpio output + ldr r3, sio_base + mov r0, #1 + lsl r0, r0, #25 // gpio25 + str r0, [r3, #0x24] // SIO: GPIO_OE + +/* ... */ + +sio_base: + .word 0xd0000000 +</code></pre> + +<h3>LEDの点滅</h3> +<p>以上でGPIOの設定は完了したので、あとは実際にLEDに電圧を掛けるだけである。レジスタのアドレスに<code>0x1000</code>を足したものに書き込むとアトミックなレジスタのXORができると書いたが、SIOはこの機能がサポートされていないようである。データシートの「2.1.2 Atomic Register Access」に、 +</p> +<blockquote cite="https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf"> +<p> +The SIO (Section 2.3.1), a single-cycle IO block attached directly to the cores' +IO ports, does <strong>not</strong> support atomic accesses at the bus level, +although some individual registers (e.g. GPIO) have set/clear/xor aliases. +</p> +</blockquote> +<p> +と書かれている。そのかわりここにも書かれている通り、SIOの一部のレジスタにはアトミックなセット/クリア/XORをするためのレジスタが用意されている。ここではLEDを点滅させるためにGPIOの出力をトグルしたいのでXOR用のレジスタを使う。SIO_BASE(<code>0xd0000000</code>)から<code>0x1c</code>バイト目のSIO: GPIO_OUT_XORレジスタがそれである。このレジスタの25番ビットに<code>0x1</code>を書き込めばいい。出力をトグルした後は少し間をおいて同じことを繰り返す。間をおくためにここでは適当な数値を1づつ減らしていって0になったら返る関数<code>delay</code>を作った。タイマーと割り込みを使ったほうが消費電力等で優位なようだが、面倒なのでとりあえずこれで:</p> + +<pre><code> // blink led on gpio25 + ldr r4, sio_base + mov r5, r0 // r0 = 1 &lt;&lt; 25 +loop: + str r5, [r4, #0x1c] // SIO: GPIO_OUT_XOR + bl delay + b loop + +delay: + mov r0, #1 + lsl r0, r0, #20 +delay_loop: + sub r0, r0, #1 + bne delay_loop + bx lr + +/* ... */ + +sio_base: + .word 0xd0000000 +</code></pre> + +<h3>ベクターテーブル</h3> +<p> +メインのコードの最後には上で説明したベクターテーブルを配置する。ここでは割り込みの処理は考えないので、初期スタックポインタとエントリーポイントだけである。初期スタックポインタはSRAMの最後?(<code>0x20040000</code>)、エントリーポイントはエントリーポイントのラベルを用いて設定した。上で説明したように、ベクターテーブルのアドレスは256バイトの境界にないといけないので、<code>.align 8</code>しておく。また、別のファイル(<code>boot2.s</code>)からアクセスしたいので、<code>.global</code>宣言をつけておく: +</p> +<pre><code> .align 8 + .global vectors +vectors: + .word 0x20040000 // initial SP + .word (reset+1) +</code></pre> +<p> +<code>reset</code>ラベルに<code>1</code>を足しているのはRP2040がThumbモードのみに対応しているからである。ArmのCPUはArmモードとThumbモードがあり、Armモードは32ビットの命令で高機能。Thumbモードは16ビットの命令(一部32ビット)でコンパクトである。どちらのモードでも命令は2の倍数のアドレスに並ぶことになる。そのためジャンブ命令のジャンプ先のアドレスの最下位ビットは常に0である。この最下位ビットはジャンプ先のモードを示す為に利用される。両方のモードに対応したCPUではジャンプ先のアドレスの最下位ビットが0ならArmモード、1ならThumbモードに切り替わる。ブランチ命令のオペランド等は多分アセンブラがいい感じにしてくれるので単にラベルを書けば動く。ベクターテーブルのこの部分は自分で足す必要があるみたい。あんまりちゃんと調べてないのでマニュアル読んでや。</p> +<p>なお以上のコードは<code>.text</code>セクションである。</p> + +<h2>リンカスクリプト</h2> +<p> +以上のコードには<code>.boot2</code>、<code>.text</code>の2つのセクションが含まれる。<code>.boot2</code>はフラッシュの先頭から256(<code>0x100</code>)バイト目まで、<code>.text</code>はその後ろに続くように配置する: +<pre><code>MEMORY +{ + FLASH(rx) : ORIGIN = 0x10000000, LENGTH = 2048k +} + +SECTIONS +{ + .boot2 : { + *(.boot2) + . = 0x100; + } > FLASH + + .text : { + *(.text) + } > FLASH +} +</code></pre> + +<h2>Makefile</h2> +<p> +以上のソースコードは以下のように配置している: +</p> +<pre>rp2040 +├── ex1 +│   ├── Makefile +│   ├── boot2.s +│   ├── main.s +│   └── memmap.ld +└── tools + ├── Makefile + ├── bin2uf2.c + └── bincrc.c +</pre> +<p> +toolsディレクトリのMakefileは同じディレクトリのソースファイルを<code>$(CC)</code>でコンパイルするだけのものである(個人的な趣味で<code>tcc</code>を使っている)。ex1ディレクトリのMakefileは以下の通り: +</p> +<pre><code>AS = arm-none-eabi-as +LD = arm-none-eabi-ld +OBJCOPY = arm-none-eabi-objcopy +BINCRC = ../tools/bincrc +BIN2UF2 = ../tools/bin2uf2 + +MCPU = -mcpu=cortex-m0plus +ASFLAGS = $(MCPU) +CFLAGS = $(MCPU) -ffreestanding -nostartfiles -O0 -fpic -mthumb -c +LDFLAGS = --no-relax -nostdlib + +all: tools led.uf2 + +clean: + rm -f *.o *.elf *.uf2 *.bin + cd ../tools &amp;&amp; make clean + +.s.o: + $(AS) $(ASFLAGS) -o $@ $&lt; + +led.elf: boot2.o main.o memmap.ld + $(LD) $(LDFLAGS) -o $@ -T memmap.ld boot2.o main.o + +led.bin: led.elf + $(OBJCOPY) -O binary led.elf $@ + +led.uf2: led.bin + $(BINCRC) led.bin led_crc.bin + $(BIN2UF2) led_crc.bin $@ + +flash: all + mount /dev/disk/by-label/RPI-RP2 /mnt + cp led.uf2 /mnt + +tools: + cd ../tools &amp;&amp; make +</code></pre> + +<p> +RP2040のボードをUSBデバイスモードでLinuxのパソコンに接続し、ex1ディレクトリで</p> +<pre><code>$ make +# make flash +</code></pre> +<p> +とすればプログラムがRP2040のボードに書き込まれて実行が開始される。</p> + +<h2>最後に</h2> +<p> +光あれ。 +</p> + +<h2>参考</h2> +<ul> +<li> +[1] Hennesy, J. L. and Patterson, D. A. 2017. Computer Organization And Design RISC-V Edition. +</li> +<li> +[2] <a href="https://akizukidenshi.com/catalog/g/gK-17542/">RP2040マイコンボードキット.秋月電子通商</a> +</li> +<li> +[3] <a href="https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf">RP2040 Datasheet.Raspberry Pi Foundation</a> +</li> +<li> +[4] <a href="https://github.com/raspberrypi/pico-sdk">pico-sdk.github</a> +</li> +<li> +[5] <a href="https://ja.wikipedia.org/wiki/%E5%B7%A1%E5%9B%9E%E5%86%97%E9%95%B7%E6%A4%9C%E6%9F%BB">巡回冗長検査.Wikipedia</a> +</li> +<li> +[6] <a href="https://github.com/microsoft/uf2">USB Flashing Format (UF2).GitHub</a> +</li> +<li> +[7] <a href="https://developer.arm.com/documentation/ddi0419/c/">ARMv6-M Architecture Reference Manual</a> +</li> +</ul> +]]></description> +</item> +<item> +<title>RP2040 SDKなし2 Clock、UART</title> +<link>https://www.mtkn.jp/computer/rp2040_2.html</link> +<guid>https://www.mtkn.jp/computer/rp2040_2.html</guid> +<pubDate>Thu, 22 Feb 2024 00:00:00 +0900</pubDate> +<description><![CDATA[<h1>RP2040 SDKなし2 Clock、UART</h1> +<time>2024-02-22</time> +<p> +今回はClockとUARTを設定してパソコンに繋ぎ、キーボードからの入力をオウム返しするプログラムを作成する。 +<p> +</p> +前回: <a href="rp2040_1.html">RP2040 SDKなしでLチカ</a><br> +ソースコード: <a href="https://git.mtkn.jp/rp2040">git</a>/ex2 +</p> + +<h2>動作環境</h2> +<ul> +<li>Void Linux + <ul> + <li>cross-arm-none-eabi-binutils-2.32_2</li> + <li>GNU Make 4.4.1</li> + <li>minicom version 2.7.1</li> + </ul> +</li> +<li><a href="https://akizukidenshi.com/catalog/g/g108461/">FT234X 超小型USBシリアル変換モジュール</a> +</li> +</ul> + +<h2>Clock</h2> + +<h3>リング発振回路</h3> +<p>RP2040にはリング発振回路というのが内蔵されている。これは自分の出力を反転させようとするもので、不安定だが高速で消費電力の少ないクロックとして用いられる。RP2040は電源を入れると、このリング発振回路を動作用のクロックとして用いている。この発振回路の周波数は、チップの製造過程での誤差、動作時の電圧、動作温度によって変動するので、正確な周波数が必要な用途には向かない。</p> + +<h3>水晶発振子</h3> +<p>秋月電子通商で購入したRP2040マイコンボードには外部クロックとして、12MHzの水晶発振子が付属する。水晶発振子はリング発振回路より電力を消費するが、より正確である。</p> + +<h3>PLL</h3> +<p>水晶振動子を入力として、周波数を数倍にしたものを出力するもの。電気的な話はよく知らない。データシートの「2.18.2. Calcurating PLL parameters」によると、入力周波数を<code>FREF</code>としたときの出力周波数は<code>(FREF / REFDIV) × FBDIV / (POSTDIV1 × POSTDIV2)</code>となる。これらの変数はそれぞれ設定用のレジスタに値を保存することで変更できる。</p> + +<h2>UART</h2> +<p> +Universal Asynchronous Receiver/Transmitterの略。2本の線だけで通信できる。プロトコルは詳しく知らないが、rp2040がよしなにやってくれる。rp2040では同時に二個まで利用できる。どのGPIOピンを使うかもある程度自由に選べる。どのピンが使えるかはデータシートの「2.19.2. Function Select」に書かれている。今回はGPIO0とGPIO1を使う。パソコンとの接続には、秋月電子通商で売っている<a href="https://akizukidenshi.com/catalog/g/g108461/">FT234X 超小型USBシリアル変換モジュール</a>を使用した。UARTで接続するためのパソコン側のソフトウェアはminicomを使用した。僕の環境ではシリアル変換モジュールをパソコンにUSB接続すると、<code>/dev/ttyUSB0</code>として認識されるので、</p> +<pre><code>$ minicom -D /dev/ttyUSB0 +</code></pre> +<p> +とすると接続できる。 +</p> + +<h2>main.s</h2> +<h3>初期設定</h3> +<p> +後で見るように、UARTの動作には多分水晶発振子とPLLが必要なので、まずはそれを設定する。起動後、メインのプログラムが読み込まれるまでの<code>boot2</code>は前回と同じものである。<code>main.s</code>ではまず前回と同様に初期スタックポインタとエントリーポイントを定義する: +</p> +<pre><code> .section .vectors +vectors: + .word 0x20040000 // initial SP + .word (reset+1) // entry point +</code></pre> +<p> +続いて利用するサブシステムのリセットを解除する。PLLとUARTが追加されている。今回使うUARTはUART0だけである: +</p> +<pre><code> .section .text +reset: + // unreset gpio, pll_sys, uart0 + ldr r0, =(1 &lt;&lt; 22 | 1 &lt;&lt; 12 | 1 &lt;&lt; 5) // uart0 | pll_sys | io_bank0 + ldr r3, resets_base + ldr r1, atomic_clr + str r0, [r3, r1] // RESETS: RESET +unreset_chk: + ldr r1, [r3, #0x8] // RESETS: RESET_DONE + tst r0, r1 + beq unreset_chk + +/* ... */ + +atomic_clr: + .word 0x00003000 +resets_base: + .word 0x4000c000 +</code></pre> + +<h3>GPIOの設定</h3> +<p> +次にGPIOの役割を設定する。前回はLEDを点滅させるためにGPIO25をSIOに設定したが、今回はGPIO0とGPIO1をUART0にする: +</p> +<pre><code> // 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 +</code></pre> + +<h3>Clockの設定</h3> <p> -CRC32のチェックサムが書き込まれたバイナリファイルを、このプログラムでUF2に変換し、生成されたファイルをUSBストレージとして接続したRP2040にコピーすればフラッシュROMに書き込まれる。 +Clockの設定をする。まずは水晶発振子を起動する。水晶発振子は起動してから周波数が安定するまで少し時間がかかるようで、その間待たないといけない。この時間は1msあれば十分だとデータシートに書いている。この待ち時間はXOSC: STARTUPレジスタに、256サイクル単位で記述する。データシートによると初期のリング発振子は最大で12MHzなので、<code>(12 * 10^6 * 1 * 10^-3) / 256 = 47</code>をこのレジスタにセットする。ところでデータシートではこの計算はリング発振子ではなく水晶発振子の周波数で書かれている。起動直後でまだ使えない水晶発振子の周波数を使うのはなんでやろ。SDKでも<code>pico-sdk/src/rp2_common/hardware_xosc/xosc.c</code>で、 +</p> +<pre><code>#define STARTUP_DELAY (((((XOSC_MHZ * MHZ) / 1000) + 128) / 256) * PICO_XOSC_STARTUP_DELAY_MULTIPLIER) +</code></pre> +<p> +と定義されている(PICO_XOSC_STARTUP_DELAY_MULTIPLIERは1)。とりあえず47に設定しているが、試しに0や1にしても動いた。よくわからん。</p> +<p> +待ち時間を設定したら発振子を起動する。XOSC: CTRLに起動用のコマンド的なものを入力し、周波数が安定するのを待つ。</p> +<p> +以上を実装したのが以下のコード: </p> +<pre><code> // 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 -<h2>Flash Second Stage</h2> +/* ... */ + +xosc_base: + .word 0x40024000 +</code></pre> + +<h3>PLLの設定</h3> <p> -RP2040に電源を投入し、CRC32のチェックが通った後、フラッシュROMからコピーされたプログラムの先頭から実行が開始される。このコピーされた部分で、その後の動作に必要な各種の設定を行うことになる。RP2040のデータシートには、フラッシュROMとSSIコントローラのXIPを設定するようにと書かれている。XIPはExecute in Placeの略で、フラッシュROMの内容をCPUから直接実行するものである。SSIはSynchronous Serial Interfaceの略で、周辺機器と情報のやりとりをする通信方式である。RP2040はチップに内蔵されたこのSSIコントローラを通して、外部のフラッシュROMと通信しているのだが、このコントローラを適切に設定すればフラッシュROMの内容がCPUから直接アクセスできる<code>0x10000000</code>番地以降にマップされる。これによりフラッシュROMから内部のSRAMにデータをコピーすることなく命令を実行できるので、速くて便利だという。 +水晶発振子が起動できたので、次にPLLを設定する。CPUが133MHzまで対応しているので133MHzになるようにした。</p> +<p> +PLLは入力となる振動(ここでは水晶発振子の振動)を加工して周波数を上げたり下げたりする。出力の周波数は以下の式で決まる: </p> +<pre>(FREF / REFDIV) * FBDIV / (POSTDIV1 * POSTDIV2)</pre> <p> -しかしこのSSIコントローラはSynopsysという会社のDW_apb_ssiというIPを使っているようで、データシートのSSIコントローラの章は多分Synopsysの人が書いている。その他の章はRaspberry Pi財団の書いたブリティッシュイングリッシュだが、この部分だけ多分ネイティブじゃない人の書いたいい加減な英語である。誤植も多い。何日かかけて理解しようとしたがよく分からん。不毛なので一旦諦めた。</p> +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つの値の差が大きい方がいいと書いている)。 +</p> <p> -RP2040には内部にもROMがあり、はバージョン情報や電源を投入した時の動作、その他便利な関数が書き込まれている。この関数の中に外部のフラッシュROMとSSIコントローラを設定するものも含まれているので、今回はこれを利用した。ただしこの方法だとフラッシュROMとの通信方式がStandard SPIのままなので少し遅いらしい。詳しくはデータシートの「2.3.8. Bootrom Contents」を参照。 +PLL設定の手順は、FBDIVの設定、PLLとVCOの起動、VOCが安定するまで待機、POSTDIV1とPOSTDIV2の設定、Post Dividerの起動、そして最後にシステムとUARTのクロックを今設定したPLLに変更、である。以上を実装したのが以下のコード: </p> +<pre><code> // 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 &lt;&lt; 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 &lt;&lt; 16 | 3 &lt;&lt; 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 &lt;&lt; 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 +</code></pre> + +<h3>UARTの設定</h3> <p> -RP2040の内蔵ROMの<code>0x00000018</code>番地に関数を検索するための関数がある。この関数に<code>0x00000014</code>番地の<code>rom_func_table</code>と、各関数に割り当てられた二文字の文字列を渡せば、欲しい関数へのポインタが返ってくる。なお、二文字の文字列はそれぞれASCIIコードで現し、二文字目を8ビットシフトしたものと1文字目のORを取ったものを渡すことになっている。今回欲しい関数はフラッシュROMをXIPに設定するもの(<code>_flash_enter_cmd_xip()</code>)なので、<code>'C', 'X'</code>を渡す。関数のポインタが返ってきて、それを呼び出せばフラッシュROMとSSIはXIPモードになる: +データシートによるとUART設定の手順は以下の通り: </p> -<pre><code>setup_xip: - ldr r3, rom_base +<ul> +<li>リセットの解除</li> +<li>clock_periの設定</li> +<li>UARTの有効化</li> +<li>FIFOの有効化</li> +<li>転送速度の設定</li> +<li>フォーマットの設定</li> +</ul> +<p> +上の2つは既に終えている。残りの部分はこの順番どおりに設定しても動かなかった。C言語で書かれたサンプルを見ると、クロックを設定した後、転送速度の設定、UARTの有効化、FIFOの有効化の順になっている。そのとおりにすると動いた。理由はよく理解していないが、変数を設定してから起動するほうが素直ではある。</p> +<p> +転送速度はminicomのデフォルトである115200 baudに設定する。データシート「4.2.7.1. Baud Rate Calculation」の計算式において、クロック周波数を125MHzから133MHzに変えて計算して、BRDI=72、BDRF=0.157(=10/64)となる。この数値をUART: UARTIBRD、UART: UARTFBRDレジスタにそれぞれ代入する。 +</p> +<p> +UARTの有効化はUART: UARTCRレジスタのUARTENビットをセットすることで行う。C言語のサンプルでは同じレジスタのRXE、TXEビットもセットしているが、この2つはもともと1になっているのでほっといてよさそう。</p> +<p> +FIFOの有効化はUART: UARTLCR_HレジスタのFENビットをセットすることで行う。また、同じレジスタの他のビットで、データーのフォーマットを設定できる。ここではminicomのデフォルトに合わせてWLENを8bitにする。</p> +<p> +以上をまとめると以下のようになる: +</p> +<pre><code> // 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 &lt;&lt; 5 | 1 &lt;&lt; 4) // WLEN = 8, FEN = 1 + str r0, [r3, #0x2c] // UART: UARTLCR_H - ldrh r0, [r3, #0x14] // rom_func_table - ldr r1, =('C' | 'X' &lt;&lt; 8) // _flash_enter_cmd_xip() - ldrh r2, [r3, #0x18] // rom_table_lookup - blx r2 - blx r0 /* ... */ -rom_base: - .word 0x00000000 + +atomic_set: + .word 0x00002000 +uart0_base: + .word 0x40034000 </code></pre> +<h3>UARTの入出力</h3> <p> -XIPの設定が完了すれば、次はメインのプログラムを実行するための準備である。エントリーポイントの指定、スタックポインタの初期値の設定、ベクターテーブルの設定である。Armのマニュアル<sup>[7]</sup>によると、初期スタックポインタとエントリーポイントはベクターテーブルの<code>0x0</code>バイト目と<code>0x4</code>バイト目に書くことになっている:</p> -<blockquote cite="https://developer.arm.com/documentation/ddi0419/c/System-Level-Architecture/System-Level-Programmers--Model/ARMv6-M-exception-model/Exception-number-definition"> -<table> -<caption> -Table 7.3. Exception numbers -</caption><colgroup><col><col></colgroup><thead><tr><th>Exception number</th><th>Exception</th></tr></thead><tbody><tr><td>1</td><td>Reset</td></tr><tr><td>2</td><td>NMI</td></tr><tr><td>3</td><td>HardFault</td></tr><tr><td>4-10</td><td>Reserved</td></tr><tr><td>11</td><td>SVCall</td></tr><tr><td>12-13</td><td>Reserved</td></tr><tr><td>14</td><td>PendSV</td></tr><tr><td>15</td><td>SysTick, optional</td></tr><tr><td>16</td><td>External Interrupt(0)</td></tr><tr><td>...</td><td>...</td></tr><tr><td>16 + N</td><td>External Interrupt(N)</td></tr></tbody> -</table> -</blockquote> +設定が終わったので実際にUARTの入出力を処理するコードを書く。まずUARTからの出力は、出力したいバイトをUART: UARTDRに書き込むことで行う。その際、書き込まれたデータは一時的に出力用FIFOに保持されるので、このFIFOが満杯でないことを確認する必要がある。FIFOの状態はUART: UARTFRレジスタで確認できる。このレジスタのTXFFの値が1であればデータを書き込めないので、0になるまで待機する。関数名は<code>putbyte</code>にした。また出力したいデータは<code>r0</code>レジスタにの下位8ビットに入れられているものとした。書き込めるデーターは8ビットだけなので、<code>0xff</code>と論理積をとってから書き込んでいる: +</p> +<pre><code>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 +</code></pre> -<blockquote cite="https://developer.arm.com/documentation/ddi0419/c/System-Level-Architecture/System-Level-Programmers--Model/ARMv6-M-exception-model/The-vector-table"> -<table> -<caption> -Table 7.4. Vector table format -</caption><colgroup><col><col></colgroup><thead><tr><th>Word offset in table</th><th>Description, for all pointer address values</th></tr></thead><tbody><tr><td>0</td><td>SP_main. This is the reset value of the Main stack pointer.</td></tr><tr><td>Exception Number</td><td>Exception using that Exception Number</td></tr></tbody> -</table> -</blockquote> <p> -また、ベクターテーブルはメインのプログラムの先頭に置くことにする。メインのプログラムはFlash Second Stageが占有する256バイトの直後、フラッシュROMの257バイト目から配置することにする。RP2040のベクターテーブルはM0PLUS: VTOR(<code>0xe0000000 + 0xed08</code>)というレジスタに書き込むことで設定する。以上をまとめると以下のコードになる:</p> -<pre><code> ldr r0, flash_main - ldr r1, m0plus_vtor - str r0, [r1, #0] // vector table - ldr r1, [r0, #4] // entry point - ldr r0, [r0, #0] // stack pointer - mov sp, r0 - bx r1 +入力はUART: UARTDRの下位8ビットを読むことで得られる。UARTからの入力は、一時的に入力用FIFOに保存される。このFIFOが空の状態でデータを読んでも意味がないので、FIFOが空でないことを確認する必要がある。これはUART: UARTFRレジスタのRXFEを読むことで確認できる。本来は入力があったときに割り込みを発生させて、それまではCPUを休ませるか別の処理をさせておくべきだが、とりあえずここではループでFIFOの状態を確認し続けている。関数名は<code>getbyte</code>にした。 +読み込んだデータは<code>r0</code>レジスタに保存している:</p> +<pre><code>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 /* ... */ -flash_main: - .word 0x10000000 + 0x100 -m0plus_vtor: - .word 0xe0000000 + 0xed08 +uart0_base: + .word 0x40034000 </code></pre> -<p>なお以上のコードは<code>.boot2</code>という名前のセクションにしてある。 +<p> +あとはこの2つの関数をループの中で交互に呼び出せば、オウム返しするだけのプログラムが完成する: </p> +<pre><code>loop: + bl getbyte + bl putbyte + b loop +</code></pre> -<h2>メインのコード(<code>main.s</code>)</h2> -<h3>ベクターテーブル</h3> +<h2>リング発振回路でUARTは動くんかな?</h2> +<p>UARTの通信には正確なクロックが必要である。その為上では<code>clk_peri</code>として水晶発振子とPLLを用いた。ところがpico-examplesのhello_uartでは<code>main()</code>関数で水晶発振子を設定していない。そこでリング発振回路を用いてみたのだが、どうもうまく通信できない。出力されている正確な周波数も分からないのであきらめることにした。オシロスコープなんていうものは持っていない。</p> + +<h3>pico-sdk</h3> <p> -メインのコードの最初には上で説明したベクターテーブルを配置する。ここでは割り込みの処理は考えないので、初期スタックポインタとエントリーポイントだけである。初期スタックポインタはSRAMの最後?(<code>0x20040000</code>)、エントリーポイントはエントリーポイントのラベルを用いて設定した。</p> -<pre><code>vectors: - .word 0x20040000 // initial SP - .word (reset+1) -</code></pre> +ところがどうも調べているとSDKを使った場合、デフォルトではクロック周波数は125MHzになっているらしい。どうやら水晶発振子もPLLも<code>main()</code>が呼ばれる前に設定されているようである。</p> <p> -<code>reset</code>ラベルに<code>1</code>を足しているのはRP2040がThumbモードのみに対応しているからである。ArmのCPUはArmモードとThumbモードがあり、Armモードは32ビットの命令で高機能。Thumbモードは16ビットの命令(一部32ビット)でコンパクトである。どちらのモードでも命令は2の倍数のアドレスに並ぶことになる。そのためジャンブ命令のジャンプ先のアドレスの最下位ビットは常に0である。この最下位ビットはジャンプ先のモードを示す為に利用される。両方のモードに対応したCPUではジャンプ先のアドレスの最下位ビットが0ならArmモード、1ならThumbモードに切り替わる。ブランチ命令のオペランド等は多分アセンブラがいい感じにしてくれるので単にラベルを書けば動く。ベクターテーブルのこの部分は自分で足す必要があるみたい。あんまりちゃんと調べてないのでマニュアル読んでや。</p> +pico-examplesのサンプルプログラムはビルドすると自動で逆アセンブリしたファイルを出力してくれる。これを見ると、最初の256バイトは前回説明したboot2のコードで、その後ろにベクターテーブルが続く。ベクターテーブルの最初は初期スタックポインタで、<code>0x20042000</code>になっている。次はエントリーポイントで、<code>0x100001f7</code>である:</p> +<pre><code>10000100 &lt;__VECTOR_TABLE&gt;: +10000100: 20042000 .word 0x20042000 +10000104: 100001f7 .word 0x100001f7 +</code></pre> <p> -この部分のセクション名は<code>.vectors</code>である。 +Thumbモードなので実際のエントリーポイントは<code>1</code>引いた、<code>0x100001f6</code>である。この場所ではまず自分のCPUIDを調べて、<code>1</code>であれば待機状態に移行する。RP2040はデュアルコアである。起動直後はCPUIDが<code>0</code>のコアだけで処理をして、CPUIDが<code>1</code>のコアはプログラマが必要に応じて起動することになっている。このためCPUIDが<code>1</code>のコアは起動してすぐに待機状態に入ることがデータシートに書かれている。しかしこの処理はユーザーの書いたプログラムじゃなくて内蔵ROMにある起動用プログラムが担当するみたいに書かれてるんやけど、なんでSDKではユーザープログラムの一部として組み込んでるんかな? </p> +<pre><code>100001f6 &lt;_reset_handler&gt;: +100001f6: 481d ldr r0, [pc, #116] ; (1000026c &lt;hold_non_core0_in_bootrom+0xe&gt;) +100001f8: 6800 ldr r0, [r0, #0] +100001fa: 2800 cmp r0, #0 +100001fc: d12f bne.n 1000025e &lt;hold_non_core0_in_bootrom&gt; +</code></pre> +<p>上のコードの最初の<code>ldr</code>は、<code>0xd0000000</code>(M0PLUS: CPUIDレジスタ)をロードしている。最後の飛び先<code>0x1000025e</code>はCPUIDが<code>1</code>のCPUを待機させる処理である:</p> +<pre><code>1000025e &lt;hold_non_core0_in_bootrom&gt;: +1000025e: 4809 ldr r0, [pc, #36] ; (10000284 &lt;hold_non_core0_in_bootrom+0x26&gt;) +10000260: f001 fb9c bl 1000199c &lt;rom_func_lookup&gt; +10000264: 4700 bx r0 +10000266: 0000 .short 0x0000 +/* ... */ +10000284: 00005657 .word 0x00005657 +</code></pre> +<p>内蔵フラッシュに書きこまれた関数を呼びだしている。呼びだしに使うコードは<code>0x00005657</code>(<code>'W' | 'V' &lt;&lt; 8</code>)である。データシートを見ると、この関数は<code>_wait_for_vector()</code>という名前で、CPUIDが1のCPUを寝かしつけるのに使われると書いている。この部分のソースコードをpico-sdkで探すと<code>pico-sdk/src/rp2_common/pico_standard_link/crt0.S</code>というのが見付かった:</p> +<pre><code>$ find pico-sdk/src -type f | xargs grep -l _reset_handler +pico-sdk/src/rp2_common/pico_standard_link/crt0.S +</code></pre> +<p>このファイルによると: +</p> +<pre><code> // 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 +</code></pre> +<p>だそうである。やっぱり無駄やん。内蔵フラッシュのプログラムにバグがあってもこのコードのせいで見付かりにくくなってない?知らんけど。</p> + +<p>続いて<code>.data</code>領域と<code>.bss</code>領域のコピー、初期化のようである。多分OSの本かなんかで習ったメモリマップの話:</p> +<pre><code>100001fe: a40d add r4, pc, #52 ; (adr r4, 10000234 &lt;data_cpy_table&gt;) +10000200: cc0e ldmia r4!, {r1, r2, r3} +10000202: 2900 cmp r1, #0 +10000204: d002 beq.n 1000020c &lt;_reset_handler+0x16&gt; +10000206: f000 f812 bl 1000022e &lt;data_cpy&gt; +1000020a: e7f9 b.n 10000200 &lt;_reset_handler+0xa&gt; +1000020c: 4918 ldr r1, [pc, #96] ; (10000270 &lt;hold_non_core0_in_bootrom+0x12&gt;) +1000020e: 4a19 ldr r2, [pc, #100] ; (10000274 &lt;hold_non_core0_in_bootrom+0x16&gt;) +10000210: 2000 movs r0, #0 +10000212: e000 b.n 10000216 &lt;bss_fill_test&gt; + +10000214 &lt;bss_fill_loop&gt;: +10000214: c101 stmia r1!, {r0} + +10000216 &lt;bss_fill_test&gt;: +10000216: 4291 cmp r1, r2 +10000218: d1fc bne.n 10000214 &lt;bss_fill_loop&gt; +</code></pre> + +<p>最後にいろいろ呼びだす:</p> +<pre><code>1000021a &lt;platform_entry&gt;: +1000021a: 4917 ldr r1, [pc, #92] ; (10000278 &lt;hold_non_core0_in_bootrom+0x1a&gt;) +1000021c: 4788 blx r1 +1000021e: 4917 ldr r1, [pc, #92] ; (1000027c &lt;hold_non_core0_in_bootrom+0x1e&gt;) +10000220: 4788 blx r1 +10000222: 4917 ldr r1, [pc, #92] ; (10000280 &lt;hold_non_core0_in_bootrom+0x22&gt;) +10000224: 4788 blx r1 +10000226: be00 bkpt 0x0000 +10000228: e7fd b.n 10000226 &lt;platform_entry+0xc&gt; +/* ... */ +10000278: 10001819 .word 0x10001819 +1000027c: 100002dd .word 0x100002dd +10000280: 10001909 .word 0x10001909 +</code></pre> +<p>一つめの<code>blx</code>は<code>0x10001818</code>(<code>runtime_init</code>)を、二つめは<code>0x100002dc</code>(<code>main</code>)を、最後のは<code>0x10001908</code>(<code>exit</code>)を、それぞれ呼んでいる。この<code>runtime_init</code>はアセンブリでは分かりにくいのでソースコードを探してみると、以下のものが見付かった:</p> +<pre><code>$ 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 +</code></pre> +<p>最後の<code>mutex.h</code>は関係なさそう。二つめの<code>crt0.S</code>は呼びだしてるだけ。一つめの<code>runtime.c</code>が多分探しているものである。これを見るとまず各種周辺機器を一度リセットし、リセット状態を解除している。使わんやつも初期化してない?その後<code>clocks_init()</code>を呼んでいる。この関数は<code>pico-sdk/src/rp2_common/hardware_clocks/clocks.c</code>で定義されている。これを見ると、<code>xosc_init()</code>を呼んで水晶発振子を初期化した後、<code>clk_peri</code>を125MHzに設定している:</p> +<pre><code> clock_configure(clk_peri, + 0, + CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLK_SYS, + 125 * MHZ, + 125 * MHZ); +</code></pre> +<p>やっぱり水晶発振子じゃないとあかんのかな。</p> -<h3>GPIOの設定</h3> +<h2>CMake</h2> +<p>上ではビルドしたバイナリを逆アッセンブルして読んだ。わざわざこんなことをしなくてもMakefile読めばなにがどうなって最終生成物に辿りつくのか分かればいいのだが、そうもいかない。このSDKとpico-examplesにはビルドシステムとしてCMakeなるものが使われている。これがどうも複雑でよく分からない。勉強する気にもならん。上で見た<code>crt0.S</code>や<code>runtime.c</code>といったファイルも<code>hello_uart</code>で本当に使われているものなのかもよく分からない。こんな煩雑なものは本当に必要なのかな。無駄に複雑にしてるだけとちゃうんかな。特に僕は勉強用に使ってるので、ソースコードの依存関係をもっと分かりやすくしてくれないと、内部でなにがどうなってるのか理解しにくい。何度か頑張って読もうとしたが、面白くないのでやめた。数百行のファイルをあっちからこっちから<code>include</code>してるし、大文字ばかりの変数だらけで目が痛い。こんなものを扱えるというのはえらいええ頭してはるんやね。</p> + + +<h2>参考</h2> +<ul> +<li> +<a href="https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf">RP2040 Datasheet.Raspberry Pi Foundation</a> +</li> +<li> +<a href="https://github.com/raspberrypi/pico-sdk">pico-sdk.github</a> +</li> +<li> +<a href="https://developer.arm.com/documentation/ddi0419/c/">ARMv6-M Architecture Reference Manual</a> +</li> +<li> +<a href="https://ja.wikipedia.org/wiki/%E3%83%AA%E3%83%B3%E3%82%B0%E3%83%BB%E3%82%AA%E3%82%B7%E3%83%AC%E3%83%BC%E3%82%BF">リング・オシレータ.Wikipedia</a> +</li> +<li> +<a href="https://www5.epsondevice.com/ja/information/technical_info/osc.html">水晶発振器とは? 原理と仕組み、水晶振動子との違い、選び方のポイントを解説.エプソン水晶デバイス</a> +</li> +</ul> +]]></description> +</item> +<item> +<title>Gitサーバーの設定 on OpenBSD</title> +<link>https://www.mtkn.jp/computer/git_server.html</link> +<guid>https://www.mtkn.jp/computer/git_server.html</guid> +<pubDate>Thu, 15 Feb 2024 00:00:00 +0900</pubDate> +<description><![CDATA[<h1>Gitサーバーの設定 on OpenBSD</h1> +<time>2024-02-15</time> + +<h2>はじめに</h2> <p> -電源投入直後、RP2040の周辺機器はリセット状態になっている。まずは今回利用するGPIOのリセット状態を解除する必要がある。データシートの「2.14. Subsystem Resets」には以下のように書かれている: -</p> -<blockquote cite="https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf"> +GitHubがMicrosoft傘下になり久しい。MincraftがMicrosoftアカウントなしでは遊べなくなった。このままではGitHubもそのうちMicrosoftアカウントを要求するようになるかもしれない。ということでGitサーバーを自前で持つことにした。</p> <p> -Every peripheral reset by the reset controller is held in reset at power-up. -It is up to software to deassert the reset of peripherals it intends to use. +ところでOpenBSDの開発者がGotという別のgit実装を作成しているので、この記事は近いうちにいらなくなりそう。 </p> -</blockquote> + +<h2>手順</h2> <p> -リセット状態を解除するには、RESETS_BASE(<code>0x4000c000</code>)から<code>0x0</code>バイト目のRESETS: RESETレジスタのうち利用したい周辺機器のビットを<code>0x0</code>にすればいい。 -GPIOはIO Bank 0なので(これ明記されてなくない?)、RESETS: RESETレジスタのIO_BANK0(5番ビット)を<code>0x0</code>にする。 +以下ではssh接続によるpull/push及び、httpsによるpullを設定の上、stagitというウェブフロントエンドを導入する。 </p> -<h4>レジスタのアトミックなクリア</h4> + +<h3>概要</h3> +<ol> +<li>ドメインの設定(任意)</li> +<li><code>httpd(8)</code>の設定</li> +<li>gitパッケージのインストールとchroot環境の整備</li> +<li>stagitの導入</li> +</ol> + +<h3>ドメインの設定(任意)</h3> <p> -RESETS: RESETレジスタのうち5番ビットだけを<code>0x0</code>にしたい。この時、まずこのレジスタを読み込んでから<code>~(1 &lt;&lt; 5)</code>と論理積を取って同レジスタに書き戻してもいいのだが、RP2040にはこれを一回の<code>str</code>でしかもアトミックにできる機能が用意されている。今回の場合アトミックかどうかは関係ないと思うけど。</p> +ドメインを設定する。IPアドレスでアクセスできればいいならこの設定はいらない。</p> <p> -各レジスタには4個のアドレスが割り当てられている。データシートの各章のList of Registersに記載されているアドレスは通常の読み書きができる。そのアドレスに<code>0x1000</code>を足したものにアクセスするとアトミックなXORが、<code>0x2000</code>を足したものはアトミックなセットが、<code>0x3000</code>を足したものはアトミックなクリアができる。つまりレジスタのアドレスに<code>0x3000</code>を足したものに、<code>0x1 &lt;&lt; 5</code>を<code>str</code>すれば5番目のビットだけ<code>0x0</code>にして、他のビットは変更されない。逆に指定したビットだけ立てて他を触らない場合は<code>0x2000</code>を、あるいは指定したビットだけトグルしたい場合は<code>0x1000</code>を足したアドレスにアクセスすればいい。</p> -<h4>リセット状態の確認</h4> -<p>リセットの解除はすぐに完了するわけではないようである。リセットの解除が完了したかどうか確認するにはRESETS: RESET_DONEレジスタ(RESETS_BASEから<code>0x8</code>バイト目)の該当するビット(ここでは5番目のビット)を読む。この値が<code>0x1</code>であればリセットの解除が完了している。<code>0x0</code>であれば処理が進行中なので<code>0x1</code>が返ってくるまで繰り返し読み込んで<code>0x0</code>になるまで待機する。ところでこのレジスタはリセットの解除が完了したかどうか確かめるものなので、RESET_DONEという名前はどうなん? +今回はgitというサブドメインを登録した: +</p> +<table> +<thead> +<tr> +<th>サブドメイン</th> +<th>種別</th> +<th>内容</th> +<th>優先度</th> +</tr> +</thead> +<tbody> +<tr> +<td>git</td> +<td>A</td> +<td><i>サーバーのIPアドレス</i></td> +<td></td> +</tr> +</tbody> +</table> + +<h3><code>httpd(8)</code>の設定</h3> <p> -以上から、GPIOのリセットを解除するのは以下のコード: +httpsで接続する場合、<code>acme-client(8)</code>を設定する(IPアドレスで接続するなら自己証明書を発行することになる)。まず<code>/etc/acme-client.conf</code>にドメインを追加する: </p> -<pre><code>reset: - // unreset gpio - mov r0, #1 - lsl r0, r0, #5 // io_bank0 - ldr r3, resets_base - ldr r1, atomic_clr - str r0, [r3, r1] // RESETS: RESET -reset_chk: - ldr r1, [r3, #0x8] // RESETS: RESET_DONE - tst r0, r1 - beq reset_chk +<pre><code>domain git.mtkn.jp { + domain key "/etc/ssl/private/git.mtkn.jp.key" + domain full chain certificate "/etc/ssl/git.mtkn.jp.fullchain.pem" + sign with letsencrypt +} +</code></pre> -/* ... */ +<p> +続いて<code>/etc/httpd.conf</code>を設定する。</p> +<pre><code>server "git.mtkn.jp" { + listen on * port 80 + location "/.well-known/acme-challenge/*" { + root "/acme" + request strip 2 + } + location "*" { + block return 302 "https://$HTTP_HOST$REQUEST_URI" + } +} -atomic_clr: - .word 0x00003000 -resets_base: - .word 0x4000c000 +server "git.mtkn.jp" { + listen on * tls port 443 + tls { + certificate "/etc/ssl/git.mtkn.jp.fullchain.pem" + key "/etc/ssl/private/git.mtkn.jp.key" + } + location "/.well-known/acme-challenge/*" { + root "/acme" + request strip 2 + } + location "/*/git-receive-pack" { + block + } + location "/*/git-upload-pack" { + fastcgi { + param SCRIPT_FILENAME "/usr/local/libexec/git/git-http-backend" + param GIT_PROJECT_ROOT "/git" + param GIT_HTTP_EXPORT_ALL "true" + } + } + location "/*/info/refs" { + fastcgi { + param SCRIPT_FILENAME "/usr/local/libexec/git/git-http-backend" + param GIT_PROJECT_ROOT "/git" + param GIT_HTTP_EXPORT_ALL "true" + } + } +} </code></pre> - -<h3>GPIOの機能の選択</h3> -<p>RP2040のGPIOにはそれぞれ複数の機能が用意されていて、どれを使うかはソフトウェアから選択できる。利用できる機能の一覧と各機能の説明はデータシートの「2.19.2 Function Select」に詳しく書いてある。ここではGPIO25番のピンをSIO(Single-cycle IO)として利用する。同じCPUが載っているRaspberry Pi PicoはGPIO25番にLEDが半田付けされている。25番にしたのはこれに合わせるためである。他のピンでもいい。GPIOに1か0を印加するだけならこのSIOを使うみたいである。Single-cycleはCPUから操作したときに1クロックでその操作が完了するという意味らしい(本当か)。SIOの詳しい説明はデータシートの「2.3.1 SIO」にある。</p> <p> -GPIO25番の機能を選択するにはIO_BANK0_BASE(<code>0x40014000</code>)から<code>0xcc</code>番目のGPIO25_CTRLレジスタの下位5ビットに、該当する機能の番号を書き込めばいい。データシートの「2.19.2 Function Select」にある表を見ると、GPIO25番のSIOは5である:</p> -<pre><code> // set gpio functions - ldr r3, io_bank0_base - mov r0, #5 // sio - mov r1, #0xcc - str r0, [r3, r1] // IO_BANK0: GPIO25_CTRL +<code>location "/*/git-receive-pack"</code>はpushの設定。今回はhttpsでのpushを受付けないので<code>block</code>する。</p> +<p> +<code>location "/*/git-upload-pack"</code>は手元のパソコンから<code>git clone</code>や<code>git pull</code>したときのもの。<code>location "/*/info/refs"</code>は上記を含むアクセスがあったときのもの。ここではCGIを使って<code>git-http-backend</code>を呼んでいるだけである。<code>SCRIPT_FILENAME</code>は<code>httpd(8)</code>のchroot環境でのパスである。必要なファイルは後でこのchroot環境にコピーする。 +</p> +<p> +この後同じURLでフロントエンドをホストしたいので、上記のように必要なURLからのみCGIを実行するようにした。gitのhttpクライアントがどのURLを利用しているのかは[2]に書いていた。</p> + -/* ... */ -io_bank0_base: - .word 0x40014000 +<p> +httpsが必要ない場合は1つ目の<code>server</code>に各<code>location</code>を書くだけでいい。 +</p> +<p> +<code>httpd(8)</code>と<code>slowcgi(8)</code>を起動する: +</p> +<pre><code># echo httpd_flags= &gt;&gt; /etc/rc.conf.local +# echo slowcgi_flags= &gt;&gt; /etc/rc.conf.local +# rcctl start httpd +# rcctl enable httpd +# rcctl start slowcgi +# rcctl enable slowcgi </code></pre> - -<h3>GPIOの出力を有効化</h3> <p> -GPIO25番がSIOになったので、次にこのピンからの出力を有効化する。既定値では出力は無効になっている。ハイインピーダンスってことなのかな?出力を有効にするには、SIO_BASE(<code>0xd0000000</code>)から<code>0x24</code>バイト目のSIO: GPIO_OEレジスタの該当するビット(25番のピンなので25番ビット)を<code>0x1</code>にする: +サーバー証明書の発行: </p> -<pre><code> // enable gpio output - ldr r3, sio_base - mov r0, #1 - lsl r0, r0, #25 // gpio25 - str r0, [r3, #0x24] // SIO: GPIO_OE - -/* ... */ - -sio_base: - .word 0xd0000000 +<pre><code># acme-client -v git.mtkn.jp +</code></pre> +<p> +<code>crontab(1)</code>にサーバー証明書の自動更新を登録: +</p> +<pre><code>#minute hour mday month wday [flags] command +~ 2 * * * acme-client git.mtkn.jp && rcctl reload httpd </code></pre> -<h3>LEDの点滅</h3> -<p>以上でGPIOの設定は完了したので、あとは実際にLEDに電圧を掛けるだけである。レジスタのアドレスに<code>0x1000</code>を足したものに書き込むとアトミックなレジスタのXORができると書いたが、SIOはこの機能がサポートされていないようである。データシートの「2.1.2 Atomic Register Access」に、 +<h3>gitパッケージのインストールとchroot環境の整備</h3> +<p> +gitパッケージをインストールし、ssh接続用のユーザーを作成する。httpsでも公開するので、gitユーザーのホームディレクトリは<code>/var/www</code>下にする: </p> -<blockquote cite="https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf"> +<pre><code># pkg_add git +# useradd -b /var/www -m -s /usr/local/bin/git-shell git +</code></pre> <p> -The SIO (Section 2.3.1), a single-cycle IO block attached directly to the cores' -IO ports, does <strong>not</strong> support atomic accesses at the bus level, -although some individual registers (e.g. GPIO) have set/clear/xor aliases. +ssh接続用の公開鍵を<code>/var/www/git/.ssh/authorized_keys</code>に登録する。ところでこのファイルに公開鍵を書き込むとgitユーザーとしてsshでログインできるので、部外者がこのファイルの編集をできないようにしておく必要がある。一応所有者は<code>git:git</code>、権限は<code>-rw-------</code>なので大丈夫だと思うが、心配なら<code>httpd(8)</code>のchroot環境の外にこのファイルを移動させておいてもいいかもしれない。</p> +<p> +httpsでアクセスするためにchroot環境を整備する。<code>httpd(8)</code>は既定では<code>/var/www</code>にchrootして実行されるので、CGIに必要なファイルをこのディレクトリ以下に用意する必要がある。まずはgitのコマンドをコピー: </p> -</blockquote> +<pre><code># mkdir -p /var/www/usr/local/libexec/git +# cp /usr/local/libexec/git/git-{http-backend,receive-pack,upload-pack} /var/www/usr/local/libexec/git/ +# chown www:www /var/www/usr/local/libexec/git/git-{http-backend,receive-pack,upload-pack} +# chmod 0500 /var/www/usr/local/libexec/git/git-{http-backend,receive-pack,upload-pack} +# mkdir -p /var/www/usr/local/bin +# cp /usr/local/bin/git /var/www/usr/local/bin/ +# chown www:www /var/www/usr/local/bin/git +# chmod 0500 /var/www/usr/local/bin/git +</code></pre> <p> -と書かれている。そのかわりここにも書かれている通り、SIOの一部のレジスタにはアトミックなセット/クリア/XORをするためのレジスタが用意されている。ここではLEDを点滅させるためにGPIOの出力をトグルしたいのでXOR用のレジスタを使う。SIO_BASE(<code>0xd0000000</code>)から<code>0x1c</code>バイト目のSIO: GPIO_OUT_XORレジスタがそれである。このレジスタの25番ビットに<code>0x1</code>を書き込めばいい。出力をトグルした後は少し間をおいて同じことを繰り返す。間をおくためにここでは適当な数値を1づつ減らしていって0になったら返る関数<code>delay</code>を作った。タイマーと割り込みを使ったほうが消費電力等で優位なようだが、面倒なのでとりあえずこれで:</p> +続いてコマンドの実行に必要なライブラリをコピー: +</p> +<pre><code># mkdir -p /var/www/usr/lib /var/www/usr/local/lib /var/www/usr/libexec +# find /var/www/{bin,usr} -type f | grep git | xargs ldd | awk '{print $7}' | grep -v -e '^/var/www/' -e '^$' -e 'Name' | sort | uniq | awk '{printf &quot;cp %s /var/www%s &amp;&amp; chown www:www /var/www%s &amp;&amp; chmod 0400 /var/www%s\n&quot;, $1, $1, $1, $1}' | sh -s +</code></pre> +<p> +<code>/dev/null</code>をコピーする(<code>mknod(8)</code>参照): +</p> +<pre><code># mkdir /var/www/dev +# mknod -m 666 /var/www/dev/null c 2 2 +</code></pre> +<p> +最後に、<code>/var/www/dev/null</code>を作成するために<code>/etc/fstab</code>の<code>/var</code>エントリーから<code>nodev</code>オプションを削除し、再起動する。 +</p> -<pre><code> // blink led on gpio25 - ldr r4, sio_base - mov r5, r0 // r0 = 1 &lt;&lt; 25 -loop: - str r5, [r4, #0x1c] // SIO: GPIO_OUT_XOR - bl delay - b loop +<p> +gitパッケージやシステムの更新後、chroot環境のコマンドやライブラリも更新しないといけないのでそのためのスクリプトを適当に作った: +</p> +<pre><code>#!/bin/sh -xe -delay: - mov r0, #1 - lsl r0, r0, #20 -delay_loop: - sub r0, r0, #1 - bne delay_loop - bx lr +oso=$(find /var/www/usr -type f -name '*.so*') +rm $oso -/* ... */ +bin=$(find /var/www/bin /var/www/usr -type f ! -name '*.so*' | + grep -v bgpctl | + sed 's|^/var/www||' + ) +echo &quot;$bin&quot; | sed 's|.*|cp &amp; /var/www&amp;|' | sh -s +echo &quot;$bin&quot; | sed 's|.*|chown www:www /var/www&amp;|' | sh -s +echo &quot;$bin&quot; | sed 's|.*|chmod 0500 /var/www&amp;|' | sh -s -sio_base: - .word 0xd0000000 +nso=$(echo &quot;$bin&quot; | sed 's|^|/var/www|' | + xargs ldd | awk '{print $7}' | + grep -v -e '^/var/www/' -e '^$' -e 'Name' | + sort | uniq + ) +echo &quot;$nso&quot; | sed 's|.*|cp &amp; /var/www&amp;|' | sh -s +echo &quot;$nso&quot; | sed 's|.*|chown www:www /var/www&amp;|' | sh -s +echo &quot;$nso&quot; | sed 's|.*|chmod 0400 /var/www&amp;|' | sh -s </code></pre> -<p>なお以上のコードは<code>.text</code>セクションである。</p> -<h2>リンカスクリプト</h2> +<h3><code>stagit(1)</code>の導入</h3> <p> -以上のコードには<code>.boot2</code>、<code>.vectors</code>、<code>.text</code>の3つのセクションが含まれる。<code>.boot2</code>はフラッシュの先頭から256(<code>0x100</code>)バイト目まで、<code>.vectors</code>と<code>.text</code>はその後ろに続くように配置する: -<pre><code>MEMORY -{ - FLASH(rx) : ORIGIN = 0x10000000, LENGTH = 2048k -} - -SECTIONS -{ - .boot2 : { - *(.boot2) - . = 0x100; - } > FLASH - - .text : { - *(.vectors) - *(.text) - } > FLASH -} +ウェブフロントエンドとしてstagitを導入する: +</p> +<pre><code># pkg_add stagit </code></pre> - -<h2>Makefile</h2> <p> -以上のソースコードは以下のように配置している: +<code>httpd.conf(5)</code>の<code>server "git.mtkn.jp"</code>の中に以下の設定を追加する(<code>location</code>のマッチは上から順番に評価されるので、上で設定したgitのhttpクライアント用の<code>location</code>よりも下に記入する): </p> -<pre>rp2040 -├── ex1 -│   ├── Makefile -│   ├── boot2.s -│   ├── main.s -│   └── memmap.ld -└── tools - ├── Makefile - ├── bin2uf2.c - └── bincrc.c -</pre> +<pre><code> location &quot;/&quot; { + directory index index.html + root &quot;/htdocs/git.mtkn.jp&quot; + } + location &quot;*&quot; { + directory index log.html + root &quot;/htdocs/git.mtkn.jp&quot; + } +</code></pre> <p> -toolsディレクトリのMakefileは同じディレクトリのソースファイルを<code>$(CC)</code>でコンパイルするだけのものである(個人的な趣味で<code>tcc</code>を使っている)。ex1ディレクトリのMakefileは以下の通り: +stagit用のディレクトリを作成して<code>httpd(8)</code>を再読込する: </p> -<pre><code>AS = arm-none-eabi-as -LD = arm-none-eabi-ld -OBJCOPY = arm-none-eabi-objcopy -BINCRC = ../tools/bincrc -BIN2UF2 = ../tools/bin2uf2 - -MCPU = -mcpu=cortex-m0plus -ASFLAGS = $(MCPU) -CFLAGS = $(MCPU) -ffreestanding -nostartfiles -O0 -fpic -mthumb -c -LDFLAGS = --no-relax -nostdlib - -all: tools led.uf2 - -clean: - rm -f *.o *.elf *.uf2 *.bin - cd ../tools &amp;&amp; make clean - -.s.o: - $(AS) $(ASFLAGS) -o $@ $&lt; - -led.elf: boot2.o main.o memmap.ld - $(LD) $(LDFLAGS) -o $@ -T memmap.ld boot2.o main.o - -led.bin: led.elf - $(OBJCOPY) -O binary led.elf $@ +<pre><code># mkdir /var/www/htdocs/git.mtkn.jp +# chown git:git /var/www/htdocs/git.mtkn.jp +# rcctl reload httpd +</code></pre> -led.uf2: led.bin - $(BINCRC) led.bin led_crc.bin - $(BIN2UF2) led_crc.bin $@ +<p> +gitリポジトリが更新されたときにウェブページも更新するように設定する。gitリポジトリはなにか更新があった場合、そのリポジトリのディレクトリの中の<code>hooks/post-receive</code>というファイルを自動で実行する。そのためこのファイルに、stagitの更新をするスクリプトを書いておけばいい: +</p> +<pre><code>#!/bin/sh -flash: all - mount /dev/disk/by-label/RPI-RP2 /mnt - cp led.uf2 /mnt +git_root=&quot;/var/www/git&quot; +stagit_root=&quot;/var/www/htdocs/git.mtkn.jp&quot; +repo=&quot;$(basename &quot;$(pwd)&quot; | sed 's/\.git$//')&quot; +src=&quot;$(pwd)&quot; +stagit_dst=&quot;$stagit_root/$repo&quot; -tools: - cd ../tools &amp;&amp; make +mkdir -p &quot;$stagit_dst&quot; +(cd &quot;$stagit_dst&quot; &amp;&amp; stagit -l 64 &quot;$src&quot;) +(cd &quot;$stagit_root&quot; &amp;&amp; stagit-index $git_root/*.git &gt; index.html) </code></pre> +<h2>レポジトリの作成</h2> <p> -RP2040のボードをUSBデバイスモードでLinuxのパソコンに接続し、ex1ディレクトリで</p> -<pre><code>$ make -# make flash +レポジトリを作成するにはサーバーで以下のようにする。</p> +<pre><code>$ cd /var/www/git +$ doas -u git mkdir <i>repo</i>.git +$ cd <i>repo</i>.git +$ doas -u git git init --bare </code></pre> -<p> -とすればプログラムがRP2040のボードに書き込まれて実行が開始される。</p> -<h2>最後に</h2> <p> -光あれ。 +これで手元のパソコンからクローンできる: +</p> +<pre><code>$ git clone git@git.mtkn.jp:<i>repo</i>.git +</code></pre> +<p> +または </p> +<pre><code>$ git clone https://git.mtkn.jp/<i>repo</i>.git +</code></pre> <h2>参考</h2> <ul> -<li> -[1] Hennesy, J. L. and Patterson, D. A. 2017. Computer Organization And Design RISC-V Edition. -</li> -<li> -[2] <a href="https://akizukidenshi.com/catalog/g/gK-17542/">RP2040マイコンボードキット.秋月電子通商</a> -</li> -<li> -[3] <a href="https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf">RP2040 Datasheet.Raspberry Pi Foundation</a> -</li> -<li> -[4] <a href="https://github.com/raspberrypi/pico-sdk">pico-sdk.github</a> -</li> -<li> -[5] <a href="https://ja.wikipedia.org/wiki/%E5%B7%A1%E5%9B%9E%E5%86%97%E9%95%B7%E6%A4%9C%E6%9F%BB">巡回冗長検査.Wikipedia</a> -</li> -<li> -[6] <a href="https://github.com/microsoft/uf2">USB Flashing Format (UF2).GitHub</a> -</li> -<li> -[7] <a href="https://developer.arm.com/documentation/ddi0419/c/">ARMv6-M Architecture Reference Manual</a> -</li> +<li>[1] <a href="https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols">Git - The Protocols.Git</a></li> +<li>[2] <a href="https://git-scm.com/docs/http-protocol">Git - http-protocol Documentation.Git</a></li> +<li>[3] <a href="https://codemadness.org/stagit.html">Stagit: a static git page generator - Codemadness</a></li> </ul> ]]></description> </item> diff --git a/pub/sitemap.xml b/pub/sitemap.xml @@ -1,10 +1,11 @@ <?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> +<url><loc>https://www.mtkn.jp/computer/rp2040_1.html</loc><lastmod>2024-02-24</lastmod></url> +<url><loc>https://www.mtkn.jp/</loc><lastmod>2024-02-22</lastmod></url> +<url><loc>https://www.mtkn.jp/computer/rp2040_2.html</loc><lastmod>2024-02-22</lastmod></url> +<url><loc>https://www.mtkn.jp/computer/</loc><lastmod>2024-02-22</lastmod></url> <url><loc>https://www.mtkn.jp/books/</loc><lastmod>2024-02-17</lastmod></url> -<url><loc>https://www.mtkn.jp/</loc><lastmod>2024-02-15</lastmod></url> -<url><loc>https://www.mtkn.jp/computer/</loc><lastmod>2024-02-15</lastmod></url> <url><loc>https://www.mtkn.jp/computer/git_server.html</loc><lastmod>2024-02-15</lastmod></url> -<url><loc>https://www.mtkn.jp/computer/rp2040_1.html</loc><lastmod>2024-02-10</lastmod></url> <url><loc>https://www.mtkn.jp/kitchen/recipe/miso.html</loc><lastmod>2024-02-09</lastmod></url> <url><loc>https://www.mtkn.jp/kitchen/</loc><lastmod>2024-02-09</lastmod></url> <url><loc>https://www.mtkn.jp/about.html</loc><lastmod>2024-01-23</lastmod></url>