www.mtkn.jp

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

commit 6fac11f282972230d3aa13379166f0736bacb371
parent f9361c9bfb2739751844a44b44ef4a4407a02a70
Author: Matsuda Kenji <info@mtkn.jp>
Date:   Sat, 24 Feb 2024 08:21:40 +0900

fix html bug

Diffstat:
Mdata/weblog | 2++
Mman/computer/rp2040_2.html | 4++--
Mpub/computer/rp2040_2.html | 4++--
Mpub/rss.xml | 1842++++++++++++++++++++++++++++++++++++++++----------------------------------------
Mpub/sitemap.xml | 2+-
5 files changed, 928 insertions(+), 926 deletions(-)

diff --git a/data/weblog b/data/weblog @@ -149,3 +149,5 @@ 1708700400 /computer/rp2040_1.html 1708700400 /computer/rp2040_1.html 1708700400 /computer/rp2040_1.html +1708700400 /computer/rp2040_1.html +1708700400 /computer/rp2040_2.html diff --git a/man/computer/rp2040_2.html b/man/computer/rp2040_2.html @@ -4,7 +4,7 @@ 今回はClockとUARTを設定してパソコンに繋ぎ、\ キーボードからの入力をオウム返しするプログラムを作成する。 <p> -</p> +<p> 前回: <a href="rp2040_1.html">RP2040 SDKなしでLチカ</a><br> ソースコード: <a href="https://git.mtkn.jp/rp2040">git</a>/ex2 </p> @@ -166,7 +166,7 @@ XOSC: CTRLに起動用のコマンド的なものを入力し、周波数が安 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) + ldr r0, =(0xfab &lt;&lt; 12 | 0xaa0) str r0, [r3, #0] // XOSC: CTRL wait_xosc: ldr r0, [r3, #0x4] // XOSC: STATUS diff --git a/pub/computer/rp2040_2.html b/pub/computer/rp2040_2.html @@ -27,7 +27,7 @@ <p> 今回はClockとUARTを設定してパソコンに繋ぎ、キーボードからの入力をオウム返しするプログラムを作成する。 <p> -</p> +<p> 前回: <a href="rp2040_1.html">RP2040 SDKなしでLチカ</a><br> ソースコード: <a href="https://git.mtkn.jp/rp2040">git</a>/ex2 </p> @@ -133,7 +133,7 @@ Clockの設定をする。まずは水晶発振子を起動する。水晶発振 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) + ldr r0, =(0xfab &lt;&lt; 12 | 0xaa0) str r0, [r3, #0] // XOSC: CTRL wait_xosc: ldr r0, [r3, #0x4] // XOSC: STATUS diff --git a/pub/rss.xml b/pub/rss.xml @@ -5,1122 +5,1122 @@ <description>ウェブページの更新履歴</description> <language>ja-jp</language> <link>https://www.mtkn.jp</link> -<lastBuildDate>Sat, 24 Feb 2024 08:11:56 +0900</lastBuildDate> -<pubDate>Sat, 24 Feb 2024 08:11:56 +0900</pubDate> +<lastBuildDate>Sat, 24 Feb 2024 08:20:49 +0900</lastBuildDate> +<pubDate>Sat, 24 Feb 2024 08:20:49 +0900</pubDate> <docs>https://www.rssboard.org/rss-specification</docs> <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> +<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>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> -パタヘネのRISC-V<sup>[1]</sup>版を買って一通り読んだらアセンブリ言語で組込のプログラミングがしたくなった。RISC-Vのマイコンボードが欲しかったのだが、安くていい感じのものが見付からなかった。代わりに秋月電子通商でArmのものがあった。RP2040マイコンボードキット<sup>[2]</sup>というものである。ウェブ上の情報も多く、データシート<sup>[3]</sup>もしっかりしていそうなので、とりあえずこれを買ってみた。</p> +<description><![CDATA[<h1>RP2040 SDKなし2 Clock、UART</h1> +<time>2024-02-22</time> <p> -一般的にはSDK<sup>[4]</sup>をダウンロードしてあらかじめ用意されたライブラリを使って開発するようだが、これはビルドシステムとしてcmakeというのを使っている。これがOpenBSDでは何かエラーがでて動かなかった。僕はこういう便利ツールが嫌いだ。どうせ使わんからいいんやけど。関係ないけど途中から開発環境がLinuxに替わった。SDKには便利な関数がたくさん用意されているので楽である。ハードウェアの面倒な部分がプログラマから見えないようにしているからである。しかし今回はその面倒な部分に触れてみたくて買ったので、SDKを使うと意味がない。</p> +今回はClockとUARTを設定してパソコンに繋ぎ、キーボードからの入力をオウム返しするプログラムを作成する。 <p> -ということでSDKなしで開発してみる。とりあえず定番のLチカをば。</p> <p> -ソースコード: <a href="https://git.mtkn.jp/rp2040">git</a> +前回: <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>Arch Linux 6.2.12-arch1-1 +<li>Void Linux <ul> - <li>arm-none-eabi-binutils 2.40-1</li> + <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>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><a href="https://akizukidenshi.com/catalog/g/g108461/">FT234X 超小型USBシリアル変換モジュール</a> </li> </ul> -<h2>Boot Process</h2> +<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> -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> +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> -以上のことから、プログラムを実行するためには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"> +<h2>main.s</h2> +<h3>初期設定</h3> <p> -データ転送等に伴う偶発的な誤りの検査によく使われている<sup>[5]</sup>。 +後で見るように、UARTの動作には多分水晶発振子とPLLが必要なので、まずはそれを設定する。起動後、メインのプログラムが読み込まれるまでの<code>boot2</code>は前回と同じものである。<code>main.s</code>ではまず前回と同様に初期スタックポインタとエントリーポイントを定義する: </p> -</blockquote> +<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> -入力のビットを一列に並べて、除数で「割り算」していく。この「割り算」が多項式の除算に似ているので、この除数をCRC多項式というらしい。ただし多項式の除算と違い、引き算するところをXORする。CRC32の場合、除数は33ビットである。33ビットで割ると32ビットの余りが残る。この余りがCRC32のチェックサムである。除数は色々あるようだが、標準的なものがWikipedia<sup>[5]</sup>に列挙されている。除数<code>1011</code>を使ったCRC3の計算の手順は以下の通り: +次にGPIOの役割を設定する。前回はLEDを点滅させるためにGPIO25をSIOに設定したが、今回はGPIO0とGPIO1をUART0にする: </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チェックサム +<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> -普通の割り算と基本は同じであるが、引き算の部分だけXORになっている。</p> -<p> -以上の計算をプログラムの先頭252バイトに対して、33ビットの除数を用いて行う。データの並べ方は、上の例において左側を先頭に、フラッシュROM上の0番地から、各バイトは最上位ビットから順に並べる。入力のデータは253バイト目から256バイト目に<code>0</code>をひっつけて計算する。これは多分予め長さが分からないデータでも計算できるようにしたかったからかな。除数は<code>0x104c11db7</code>である(最上位ビットは常に1なのでデータシートでは省略されている)。</p> -<p> -入力データは1バイトづつ処理したいみたいである。多分通信等で使う都合である。この時XORは結合則が成り立つので1バイト処理した結果と次のバイトとをXORして次の処理の入力として利用することができる: +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>111000111000000110000110111000111000001010010011111000111000000110010011 入力(適当) -|......| -111000110000000000000000000000000 先頭1バイト -100000100110000010001110110110111 除数 ------------------------------------------------------------------------- -011000010110000010001110110110111 - 100000100110000010001110110110111 - ----------------------------------------------------------------------- - 010000001010000110010011011011001 - 100000100110000010001110110110111 - ---------------------------------------------------------------------- - 000000110010001110101000000000101 -|......| - 110010001110101000000000101000000 1バイト目の結果 - |......| - 10000001 入力の2バイト目 - ---------------------------------------------------------------- - 010010011110101000000000101000000 1バイト目の結果と2バイト目のXOR - 100000100110000010001110110110111 除数 - ---------------------------------------------------------------- - 000100011011010010001111100110111 - . - . - . +<pre><code>#define STARTUP_DELAY (((((XOSC_MHZ * MHZ) / 1000) + 128) / 256) * PICO_XOSC_STARTUP_DELAY_MULTIPLIER) </code></pre> <p> -以上の操作は以下のようなアルゴリズムのループで実装できる。</p> -<ul> -<li>前回の結果と、入力データの次のバイトをXOR</li> -<li> - <ul> - <li>先頭の1ビットが1の場合、除数とXORを取り左シフト</li> - <li>先頭の1ビットが0の場合、そのまま左シフト</li> - </ul> -</li> -</ul> +と定義されている(PICO_XOSC_STARTUP_DELAY_MULTIPLIERは1)。とりあえず47に設定しているが、試しに0や1にしても動いた。よくわからん。</p> <p> -これを<code>for</code>ループで回す都合上、最初のバイトもXORを取る。上の例では最初は<code>0x0</code>とXORを取っているが、この値を<code>0x0</code>以外にすることもできる。そうした方がいろいろいいこともあるらしい。RP2040では<code>0xffffffff</code>を使う。更にこの工程を32ビットの<code>int</code>だけで行うことを考える: +待ち時間を設定したら発振子を起動する。XOSC: CTRLに起動用のコマンド的なものを入力し、周波数が安定するのを待つ。</p> +<p> +以上を実装したのが以下のコード: </p> -<pre><code>111000111000000110000110111000111000001010010011111000111000000110010011 入力(適当) +<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 &lt;&lt; 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 -11111111111111111111111111111111 0xffffffff -11100011000000000000000000000000 先頭1バイトを24ビットシフト --------------------------------- XOR -00011100111111111111111111111111 -先頭1ビットが0なので1ビットシフト --------------------------------- シフト -00111001111111111111111111111110 -先頭1ビットが0なので1ビットシフト --------------------------------- シフト -01110011111111111111111111111100 -先頭1ビットが0なので1ビットシフト --------------------------------- シフト -11100111111111111111111111111000 -先頭1ビットが1なので1ビットシフトした後、除数の下位32ビットとXOR: -11001111111111111111111111110000 シフト -00000100110000010001110110110111 除数の下位32ビット --------------------------------- XOR -11001011001111101110001001000111 -先頭1ビットが1なので1ビットシフトした後、除数の下位32ビットとXOR: -10010110011111011100010010001110 シフト -00000100110000010001110110110111 除数の下位32ビット --------------------------------- XOR -10010010101111001101100100111001 -先頭1ビットが1なので1ビットシフトした後、除数の下位32ビットとXOR: -00100101011110011011001001110010 シフト -00000100110000010001110110110111 除数の下位32ビット --------------------------------- XOR -00100001101110001010111111000101 -先頭1ビットが0なので1ビットシフト --------------------------------- シフト -01000011011100010101111110001010 -先頭1ビットが0なので1ビットシフト --------------------------------- シフト -10000110111000101011111100010100 1バイト目の結果 +/* ... */ -10000001 入力の2バイト目 --------------------------------- XOR -00000111111000101011111100010100 -先頭1ビットが0なので1ビットシフト --------------------------------- シフト -00001111110001010111111000101000 -. -. -. +xosc_base: + .word 0x40024000 </code></pre> + +<h3>PLLの設定</h3> <p> -これを実装したのが以下のコード:</p> -<pre><code>uint32_t -crc32(uint8_t *idata, size_t len) -{ - uint32_t pol = 0x04C11DB7; - uint32_t c = 0xFFFFFFFF; - uint32_t b; +水晶発振子が起動できたので、次に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 - for (int i = 0; i &lt; len; i++) { - b = idata[i] &lt;&lt; 24; - c ^= b; - for (int j = 0; j &lt; 8; j++) { - c = c &gt;&gt; 31 & 1 ? c &lt;&lt; 1 ^ pol : c &lt;&lt; 1; - } - } + // 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 - return c; -} +/* ... */ + +atomic_clr: + .word 0x00003000 +clocks_base: + .word 0x40008000 +pll_sys_base: + .word 0x40028000 </code></pre> + +<h3>UARTの設定</h3> <p> -<code>main()</code>関数では上の<code>crc32()</code>に、<code>idata</code>として入力となるバイナリデータの先頭を、<code>len</code>として<code>252</code>を渡してCRC32を計算させる。その後、出力先のファイルに入力元のデータをコピーしていき、253バイト目から256バイト目だけ、計算したCRC32に置き換える。入力元のこの場所にデータが書き込まれていないかどうかは確かめていない。 +データシートによるとUART設定の手順は以下の通り: </p> - -<h2>UF2(USB Flashing Format)</h2> +<ul> +<li>リセットの解除</li> +<li>clock_periの設定</li> +<li>UARTの有効化</li> +<li>FIFOの有効化</li> +<li>転送速度の設定</li> +<li>フォーマットの設定</li> +</ul> <p> -Microsoftが開発したフラッシュ書き込み用のファイル形式らしい: -<blockquote cite="https://github.com/microsoft/uf2"> +上の2つは既に終えている。残りの部分はこの順番どおりに設定しても動かなかった。C言語で書かれたサンプルを見ると、クロックを設定した後、転送速度の設定、UARTの有効化、FIFOの有効化の順になっている。そのとおりにすると動いた。理由はよく理解していないが、変数を設定してから起動するほうが素直ではある。</p> <p> -UF2 is a file format, developed by Microsoft for PXT (also known as -Microsoft MakeCode), that is particularly suitable for flashing microcontrollers -over MSC (Mass Storage Class; aka removable flash drive)<sup>[6]</sup>. +転送速度はminicomのデフォルトである115200 baudに設定する。データシート「4.2.7.1. Baud Rate Calculation」の計算式において、クロック周波数を125MHzから133MHzに変えて計算して、BRDI=72、BDRF=0.157(=10/64)となる。この数値をUART: UARTIBRD、UART: UARTFBRDレジスタにそれぞれ代入する。 </p> -</blockquote> <p> -このファイルに変換する上で必要な情報はGitHubのmicrosoft/uf2<sup>[6]</sup>に表として纏められている: -<blockquote cite="https://github.com/microsoft/uf2"> -<table> -<thead><tr> -<th>Offset</th><th>Size</th><th>Value</th> -</tr></thead> -<tbody> -<tr> -<td>0</td> -<td>4</td> -<td>First magic number, <code>0x0A324655</code> (<code>"UF2\n"</code>)</td> -</tr> -<tr> -<td>4</td> -<td>4</td> -<td>Second magic number, <code>0x9E5D5157</code></td> -</tr> -<tr> -<td>8</td> -<td>4</td> -<td>Flags</td> -</tr> -<tr> -<td>12</td> -<td>4</td> -<td>Address in flash where the data should be written</td> -</tr> -<tr> -<td>16</td> -<td>4</td> -<td>Number of bytes used in data (often 256)</td> -</tr> -<tr> -<td>20</td> -<td>4</td> -<td>Sequential block number; starts at 0</td> -</tr> -<tr> -<td>24</td> -<td>4</td> -<td>Total number of blocks in file</td> -</tr> -<tr> -<td>28</td> -<td>4</td> -<td>File size or board family ID or zero</td> -</tr> -<tr> -<td>32</td> -<td>476</td> -<td>Data, padded with zeros</td> -</tr> -<tr> -<td>508</td> -<td>4</td> -<td>Final magic number, <code>0x0AB16F30</code></td> -</tr> -</tbody> -</table> -</blockquote> - +UARTの有効化はUART: UARTCRレジスタのUARTENビットをセットすることで行う。C言語のサンプルでは同じレジスタのRXE、TXEビットもセットしているが、この2つはもともと1になっているのでほっといてよさそう。</p> <p> -RP2040のデータシート<sup>[3]</sup>「2.8.4.2 UF2 Format Details」を見ると、8バイト目のFlagsは、28バイト目にファミリーIDが書き込まれていることを示す<code>0x00002000</code>、12バイト目は、書き込みを行うフラッシュROMの先頭アドレスである<code>0x10000000</code>に、各ブロックの先頭からの位置を足したもの、16バイト目の、各ブロックのデータサイズは256バイト、28バイト目のファミリーIDは<code>0xe48bff56</code>である。あとは表の通り3つのマジックナンバーをセットし、32バイト目以降にデータを書き込み、20バイト目と24バイト目にブロックの通し番号と総数をそれぞれ書き込めばいい。ブロックの通し番号はデータのついでに書き込めるが、総数はデータを全部さばいた後でないと分からないので、最後全てのブロックにまとめて書き込むようにした。できたのが以下のコード: +FIFOの有効化はUART: UARTLCR_HレジスタのFENビットをセットすることで行う。また、同じレジスタの他のビットで、データーのフォーマットを設定できる。ここではminicomのデフォルトに合わせてWLENを8bitにする。</p> +<p> +以上をまとめると以下のようになる: </p> -<pre><code>#include &lt;stdio.h&gt; -#include &lt;stdint.h&gt; -#include &lt;stdlib.h&gt; -#include &lt;string.h&gt; +<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 +/* ... */ -size_t -fwrite32l(uint32_t d, FILE *f) -{ - int i; - uint8_t b; - for (i = 0; i &lt; 32; i += 8) { - b = (uint8_t) (d &gt;&gt; i & 0xff); - fwrite(&amp;b, 1, 1, f); - if (ferror(f)) { - fprintf(stderr, "Fwrite32l: write error.\n"); - return 0; - } - } - return 4; -} +atomic_set: + .word 0x00002000 +uart0_base: + .word 0x40034000 +</code></pre> -int -main(int argc, char *argv[]) -{ - FILE *src = NULL, *dst = NULL; - size_t sdata = 476; - int retnum = 0; +<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 - uint32_t mag1 = 0x0A324655; - uint32_t mag2 = 0x9E5D5157; - uint32_t flags = 0x00002000; // familyID present - uint32_t addr = 0x10000000; - uint32_t nbyte = 256; - uint32_t blk = 0; - uint32_t nblk = 0; - uint32_t famid = 0xe48bff56; - uint8_t data[sdata]; - uint32_t mag3 = 0x0AB16F30; +/* ... */ - memset(data, 0, sdata); +uart0_base: + .word 0x40034000 +</code></pre> - if (argc != 3) { - fprintf(stderr, "Usage: %s src dst\n", argv[0]); - exit(1); - } +<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 - if ((src = fopen(argv[1], "rb")) == NULL) { - fprintf(stderr, "Could not open %s.\n", argv[1]); - retnum = 1; - goto defer; - } - if ((dst = fopen(argv[2], "wb")) == NULL) { - fprintf(stderr, "Could not open %s.\n", argv[2]); - retnum = 1; - goto defer; - } - - while (!feof(src)) { - fwrite32l(mag1, dst); - fwrite32l(mag2, dst); - fwrite32l(flags, dst); - fwrite32l(addr, dst); - fwrite32l(nbyte, dst); - fwrite32l(blk, dst); - fwrite32l(nblk, dst); // dummy - fwrite32l(famid, dst); - - fread(data, 1, nbyte, src); - if (ferror(src)) { - fprintf(stderr, "Read error: %s.\n", argv[1]); - retnum = 1; - goto defer; - } - fwrite(data, 1, sdata, dst); - if (ferror(src)) { - fprintf(stderr, "Write error: %s.\n", argv[2]); - retnum = 1; - goto defer; - } - - fwrite32l(mag3, dst); - - addr += nbyte; - blk++; - nblk++; - } +/* ... */ - for (int i = 0; i &lt; nblk; i++) { - if (i == 0) - if (fseek(dst, 24, SEEK_SET) &lt; 0) { - fprintf(stderr, "Seek error: %s.\n argv[2]"); - retnum = 1; - goto defer; - } - fwrite32l(nblk, dst); - if (i &lt; nblk - 1) - if(fseek(dst, 512 - 4, SEEK_CUR) &lt; 0){ - fprintf(stderr, "Seek error: %s.\n argv[2]"); - retnum = 1; - goto defer; - } - } - -defer: - if (src) - fclose(src); - if (dst) - fclose(dst); - return retnum; -} +uart0_base: + .word 0x40034000 </code></pre> -<p><code>fwrite32l()</code>関数は指定されたファイルに32ビットの整数を下位バイトから順に書き込む関数である。バイトオーダーとかややこしそうなので作っておいたけど必要なのかな?あと名前が気に入らない。</p> <p> -CRC32のチェックサムが書き込まれたバイナリファイルを、このプログラムでUF2に変換し、生成されたファイルをUSBストレージとして接続したRP2040にコピーすればフラッシュROMに書き込まれる。 +あとはこの2つの関数をループの中で交互に呼び出せば、オウム返しするだけのプログラムが完成する: </p> +<pre><code>loop: + bl getbyte + bl putbyte + b loop +</code></pre> -<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> +<h2>リング発振回路でUARTは動くんかな?</h2> +<p>UARTの通信には正確なクロックが必要である。その為上では<code>clk_peri</code>として水晶発振子とPLLを用いた。ところがpico-examplesのhello_uartでは<code>main()</code>関数で水晶発振子を設定していない。そこでリング発振回路を用いてみたのだが、どうもうまく通信できない。出力されている正確な周波数も分からないのであきらめることにした。オシロスコープなんていうものは持っていない。</p> + +<h3>pico-sdk</h3> <p> -しかしこのSSIコントローラはSynopsysという会社のDW_apb_ssiというIPを使っているようで、データシートのSSIコントローラの章は多分Synopsysの人が書いている。その他の章はRaspberry Pi財団の書いたブリティッシュイングリッシュだが、この部分だけ多分ネイティブじゃない人の書いたいい加減な英語である。誤植も多い。何日かかけて理解しようとしたがよく分からん。不毛なので一旦諦めた。</p> +ところがどうも調べているとSDKを使った場合、デフォルトではクロック周波数は125MHzになっているらしい。どうやら水晶発振子もPLLも<code>main()</code>が呼ばれる前に設定されているようである。</p> <p> -RP2040には内部にもROMがあり、はバージョン情報や電源を投入した時の動作、その他便利な関数が書き込まれている。この関数の中に外部のフラッシュROMとSSIコントローラを設定するものも含まれているので、今回はこれを利用した。ただしこの方法だとフラッシュROMとの通信方式がStandard SPIのままなので少し遅いらしい。詳しくはデータシートの「2.3.8. Bootrom Contents」を参照。 -</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> -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モードになる: +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>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 +<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 /* ... */ -rom_base: - .word 0x00000000 +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> -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> +<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; -<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 +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> -m0plus_vtor: - .word 0xe0000000 + 0xed08 +<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>.boot2</code>という名前のセクションにしてある。 -</p> +<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>メインのコード(<code>main.s</code>)</h2> +<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> -<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> + +<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>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> -リセット状態を解除するには、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> +パタヘネのRISC-V<sup>[1]</sup>版を買って一通り読んだらアセンブリ言語で組込のプログラミングがしたくなった。RISC-Vのマイコンボードが欲しかったのだが、安くていい感じのものが見付からなかった。代わりに秋月電子通商でArmのものがあった。RP2040マイコンボードキット<sup>[2]</sup>というものである。ウェブ上の情報も多く、データシート<sup>[3]</sup>もしっかりしていそうなので、とりあえずこれを買ってみた。</p> <p> -RESETS: RESETレジスタのうち5番ビットだけを<code>0x0</code>にしたい。この時、まずこのレジスタを読み込んでから<code>~(1 &lt;&lt; 5)</code>と論理積を取って同レジスタに書き戻してもいいのだが、RP2040にはこれを一回の<code>str</code>でしかもアトミックにできる機能が用意されている。今回の場合アトミックかどうかは関係ないと思うけど。</p> +一般的にはSDK<sup>[4]</sup>をダウンロードしてあらかじめ用意されたライブラリを使って開発するようだが、これはビルドシステムとしてcmakeというのを使っている。これがOpenBSDでは何かエラーがでて動かなかった。僕はこういう便利ツールが嫌いだ。どうせ使わんからいいんやけど。関係ないけど途中から開発環境がLinuxに替わった。SDKには便利な関数がたくさん用意されているので楽である。ハードウェアの面倒な部分がプログラマから見えないようにしているからである。しかし今回はその面倒な部分に触れてみたくて買ったので、SDKを使うと意味がない。</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という名前はどうなん? +ということでSDKなしで開発してみる。とりあえず定番のLチカをば。</p> <p> -以上から、GPIOのリセットを解除するのは以下のコード: +ソースコード: <a href="https://git.mtkn.jp/rp2040">git</a> </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> +<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>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> +<h2>Boot Process</h2> <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> +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> -GPIO25番がSIOになったので、次にこのピンからの出力を有効化する。既定値では出力は無効になっている。ハイインピーダンスってことなのかな?出力を有効にするには、SIO_BASE(<code>0xd0000000</code>)から<code>0x24</code>バイト目のSIO: GPIO_OEレジスタの該当するビット(25番のピンなので25番ビット)を<code>0x1</code>にする: +以上のことから、プログラムを実行するためにはCRC32を計算し、UF2という形式に変換することが必要である。ソースコードからの流れは以下の通り: </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> +<pre>source bin bin with +code ----------> object ------> elf --------> bin -------> with --------> crc32 in + crc32 uf2 format + assemble link objcopy bincrc bin2uf2 +</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"> +<h2>CRC(巡回冗長検査)</h2> <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 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> -と書かれている。そのかわりここにも書かれている通り、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> <p> -メインのコードの最後には上で説明したベクターテーブルを配置する。ここでは割り込みの処理は考えないので、初期スタックポインタとエントリーポイントだけである。初期スタックポインタはSRAMの最後?(<code>0x20040000</code>)、エントリーポイントはエントリーポイントのラベルを用いて設定した。上で説明したように、ベクターテーブルのアドレスは256バイトの境界にないといけないので、<code>.align 8</code>しておく。また、別のファイル(<code>boot2.s</code>)からアクセスしたいので、<code>.global</code>宣言をつけておく: +入力のビットを一列に並べて、除数で「割り算」していく。この「割り算」が多項式の除算に似ているので、この除数をCRC多項式というらしい。ただし多項式の除算と違い、引き算するところをXORする。CRC32の場合、除数は33ビットである。33ビットで割ると32ビットの余りが残る。この余りがCRC32のチェックサムである。除数は色々あるようだが、標準的なものがWikipedia<sup>[5]</sup>に列挙されている。除数<code>1011</code>を使ったCRC3の計算の手順は以下の通り: </p> -<pre><code> .align 8 - .global vectors -vectors: - .word 0x20040000 // initial SP - .word (reset+1) +<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> -<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> +普通の割り算と基本は同じであるが、引き算の部分だけXORになっている。</p> <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> +以上の計算をプログラムの先頭252バイトに対して、33ビットの除数を用いて行う。データの並べ方は、上の例において左側を先頭に、フラッシュROM上の0番地から、各バイトは最上位ビットから順に並べる。入力のデータは253バイト目から256バイト目に<code>0</code>をひっつけて計算する。これは多分予め長さが分からないデータでも計算できるようにしたかったからかな。除数は<code>0x104c11db7</code>である(最上位ビットは常に1なのでデータシートでは省略されている)。</p> <p> -以上のソースコードは以下のように配置している: +入力データは1バイトづつ処理したいみたいである。多分通信等で使う都合である。この時XORは結合則が成り立つので1バイト処理した結果と次のバイトとをXORして次の処理の入力として利用することができる: </p> -<pre>rp2040 -├── ex1 -│   ├── Makefile -│   ├── boot2.s -│   ├── main.s -│   └── memmap.ld -└── tools - ├── Makefile - ├── bin2uf2.c - └── bincrc.c -</pre> +<pre><code>111000111000000110000110111000111000001010010011111000111000000110010011 入力(適当) +|......| +111000110000000000000000000000000 先頭1バイト +100000100110000010001110110110111 除数 +------------------------------------------------------------------------ +011000010110000010001110110110111 + 100000100110000010001110110110111 + ----------------------------------------------------------------------- + 010000001010000110010011011011001 + 100000100110000010001110110110111 + ---------------------------------------------------------------------- + 000000110010001110101000000000101 +|......| + 110010001110101000000000101000000 1バイト目の結果 + |......| + 10000001 入力の2バイト目 + ---------------------------------------------------------------- + 010010011110101000000000101000000 1バイト目の結果と2バイト目のXOR + 100000100110000010001110110110111 除数 + ---------------------------------------------------------------- + 000100011011010010001111100110111 + . + . + . +</code></pre> <p> -toolsディレクトリのMakefileは同じディレクトリのソースファイルを<code>$(CC)</code>でコンパイルするだけのものである(個人的な趣味で<code>tcc</code>を使っている)。ex1ディレクトリのMakefileは以下の通り: +以上の操作は以下のようなアルゴリズムのループで実装できる。</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>だけで行うことを考える: </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 +<pre><code>111000111000000110000110111000111000001010010011111000111000000110010011 入力(適当) -tools: - cd ../tools &amp;&amp; make -</code></pre> +11111111111111111111111111111111 0xffffffff +11100011000000000000000000000000 先頭1バイトを24ビットシフト +-------------------------------- XOR +00011100111111111111111111111111 +先頭1ビットが0なので1ビットシフト +-------------------------------- シフト +00111001111111111111111111111110 +先頭1ビットが0なので1ビットシフト +-------------------------------- シフト +01110011111111111111111111111100 +先頭1ビットが0なので1ビットシフト +-------------------------------- シフト +11100111111111111111111111111000 +先頭1ビットが1なので1ビットシフトした後、除数の下位32ビットとXOR: +11001111111111111111111111110000 シフト +00000100110000010001110110110111 除数の下位32ビット +-------------------------------- XOR +11001011001111101110001001000111 +先頭1ビットが1なので1ビットシフトした後、除数の下位32ビットとXOR: +10010110011111011100010010001110 シフト +00000100110000010001110110110111 除数の下位32ビット +-------------------------------- XOR +10010010101111001101100100111001 +先頭1ビットが1なので1ビットシフトした後、除数の下位32ビットとXOR: +00100101011110011011001001110010 シフト +00000100110000010001110110110111 除数の下位32ビット +-------------------------------- XOR +00100001101110001010111111000101 +先頭1ビットが0なので1ビットシフト +-------------------------------- シフト +01000011011100010101111110001010 +先頭1ビットが0なので1ビットシフト +-------------------------------- シフト +10000110111000101011111100010100 1バイト目の結果 -<p> -RP2040のボードをUSBデバイスモードでLinuxのパソコンに接続し、ex1ディレクトリで</p> -<pre><code>$ make -# make flash +10000001 入力の2バイト目 +-------------------------------- XOR +00000111111000101011111100010100 +先頭1ビットが0なので1ビットシフト +-------------------------------- シフト +00001111110001010111111000101000 +. +. +. </code></pre> <p> -とすればプログラムがRP2040のボードに書き込まれて実行が開始される。</p> +これを実装したのが以下のコード:</p> +<pre><code>uint32_t +crc32(uint8_t *idata, size_t len) +{ + uint32_t pol = 0x04C11DB7; + uint32_t c = 0xFFFFFFFF; + uint32_t b; -<h2>最後に</h2> + for (int i = 0; i &lt; len; i++) { + b = idata[i] &lt;&lt; 24; + c ^= b; + for (int j = 0; j &lt; 8; j++) { + c = c &gt;&gt; 31 & 1 ? c &lt;&lt; 1 ^ pol : c &lt;&lt; 1; + } + } + + return c; +} +</code></pre> <p> -光あれ。 +<code>main()</code>関数では上の<code>crc32()</code>に、<code>idata</code>として入力となるバイナリデータの先頭を、<code>len</code>として<code>252</code>を渡してCRC32を計算させる。その後、出力先のファイルに入力元のデータをコピーしていき、253バイト目から256バイト目だけ、計算したCRC32に置き換える。入力元のこの場所にデータが書き込まれていないかどうかは確かめていない。 </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> +<h2>UF2(USB Flashing Format)</h2> <p> -今回はClockとUARTを設定してパソコンに繋ぎ、キーボードからの入力をオウム返しするプログラムを作成する。 +Microsoftが開発したフラッシュ書き込み用のファイル形式らしい: +<blockquote cite="https://github.com/microsoft/uf2"> <p> +UF2 is a file format, developed by Microsoft for PXT (also known as +Microsoft MakeCode), that is particularly suitable for flashing microcontrollers +over MSC (Mass Storage Class; aka removable flash drive)<sup>[6]</sup>. </p> -前回: <a href="rp2040_1.html">RP2040 SDKなしでLチカ</a><br> -ソースコード: <a href="https://git.mtkn.jp/rp2040">git</a>/ex2 +</blockquote> +<p> +このファイルに変換する上で必要な情報はGitHubのmicrosoft/uf2<sup>[6]</sup>に表として纏められている: +<blockquote cite="https://github.com/microsoft/uf2"> +<table> +<thead><tr> +<th>Offset</th><th>Size</th><th>Value</th> +</tr></thead> +<tbody> +<tr> +<td>0</td> +<td>4</td> +<td>First magic number, <code>0x0A324655</code> (<code>"UF2\n"</code>)</td> +</tr> +<tr> +<td>4</td> +<td>4</td> +<td>Second magic number, <code>0x9E5D5157</code></td> +</tr> +<tr> +<td>8</td> +<td>4</td> +<td>Flags</td> +</tr> +<tr> +<td>12</td> +<td>4</td> +<td>Address in flash where the data should be written</td> +</tr> +<tr> +<td>16</td> +<td>4</td> +<td>Number of bytes used in data (often 256)</td> +</tr> +<tr> +<td>20</td> +<td>4</td> +<td>Sequential block number; starts at 0</td> +</tr> +<tr> +<td>24</td> +<td>4</td> +<td>Total number of blocks in file</td> +</tr> +<tr> +<td>28</td> +<td>4</td> +<td>File size or board family ID or zero</td> +</tr> +<tr> +<td>32</td> +<td>476</td> +<td>Data, padded with zeros</td> +</tr> +<tr> +<td>508</td> +<td>4</td> +<td>Final magic number, <code>0x0AB16F30</code></td> +</tr> +</tbody> +</table> +</blockquote> + +<p> +RP2040のデータシート<sup>[3]</sup>「2.8.4.2 UF2 Format Details」を見ると、8バイト目のFlagsは、28バイト目にファミリーIDが書き込まれていることを示す<code>0x00002000</code>、12バイト目は、書き込みを行うフラッシュROMの先頭アドレスである<code>0x10000000</code>に、各ブロックの先頭からの位置を足したもの、16バイト目の、各ブロックのデータサイズは256バイト、28バイト目のファミリーIDは<code>0xe48bff56</code>である。あとは表の通り3つのマジックナンバーをセットし、32バイト目以降にデータを書き込み、20バイト目と24バイト目にブロックの通し番号と総数をそれぞれ書き込めばいい。ブロックの通し番号はデータのついでに書き込めるが、総数はデータを全部さばいた後でないと分からないので、最後全てのブロックにまとめて書き込むようにした。できたのが以下のコード: </p> +<pre><code>#include &lt;stdio.h&gt; +#include &lt;stdint.h&gt; +#include &lt;stdlib.h&gt; +#include &lt;string.h&gt; -<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> +size_t +fwrite32l(uint32_t d, FILE *f) +{ + int i; + uint8_t b; + for (i = 0; i &lt; 32; i += 8) { + b = (uint8_t) (d &gt;&gt; i & 0xff); + fwrite(&amp;b, 1, 1, f); + if (ferror(f)) { + fprintf(stderr, "Fwrite32l: write error.\n"); + return 0; + } + } + return 4; +} -<h3>リング発振回路</h3> -<p>RP2040にはリング発振回路というのが内蔵されている。これは自分の出力を反転させようとするもので、不安定だが高速で消費電力の少ないクロックとして用いられる。RP2040は電源を入れると、このリング発振回路を動作用のクロックとして用いている。この発振回路の周波数は、チップの製造過程での誤差、動作時の電圧、動作温度によって変動するので、正確な周波数が必要な用途には向かない。</p> +int +main(int argc, char *argv[]) +{ + FILE *src = NULL, *dst = NULL; + size_t sdata = 476; + int retnum = 0; -<h3>水晶発振子</h3> -<p>秋月電子通商で購入したRP2040マイコンボードには外部クロックとして、12MHzの水晶発振子が付属する。水晶発振子はリング発振回路より電力を消費するが、より正確である。</p> + uint32_t mag1 = 0x0A324655; + uint32_t mag2 = 0x9E5D5157; + uint32_t flags = 0x00002000; // familyID present + uint32_t addr = 0x10000000; + uint32_t nbyte = 256; + uint32_t blk = 0; + uint32_t nblk = 0; + uint32_t famid = 0xe48bff56; + uint8_t data[sdata]; + uint32_t mag3 = 0x0AB16F30; -<h3>PLL</h3> -<p>水晶振動子を入力として、周波数を数倍にしたものを出力するもの。電気的な話はよく知らない。データシートの「2.18.2. Calcurating PLL parameters」によると、入力周波数を<code>FREF</code>としたときの出力周波数は<code>(FREF / REFDIV) × FBDIV / (POSTDIV1 × POSTDIV2)</code>となる。これらの変数はそれぞれ設定用のレジスタに値を保存することで変更できる。</p> + memset(data, 0, sdata); -<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> + if (argc != 3) { + fprintf(stderr, "Usage: %s src dst\n", argv[0]); + exit(1); + } -<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 + if ((src = fopen(argv[1], "rb")) == NULL) { + fprintf(stderr, "Could not open %s.\n", argv[1]); + retnum = 1; + goto defer; + } + if ((dst = fopen(argv[2], "wb")) == NULL) { + fprintf(stderr, "Could not open %s.\n", argv[2]); + retnum = 1; + goto defer; + } + + while (!feof(src)) { + fwrite32l(mag1, dst); + fwrite32l(mag2, dst); + fwrite32l(flags, dst); + fwrite32l(addr, dst); + fwrite32l(nbyte, dst); + fwrite32l(blk, dst); + fwrite32l(nblk, dst); // dummy + fwrite32l(famid, dst); + + fread(data, 1, nbyte, src); + if (ferror(src)) { + fprintf(stderr, "Read error: %s.\n", argv[1]); + retnum = 1; + goto defer; + } + fwrite(data, 1, sdata, dst); + if (ferror(src)) { + fprintf(stderr, "Write error: %s.\n", argv[2]); + retnum = 1; + goto defer; + } -/* ... */ + fwrite32l(mag3, dst); -atomic_clr: - .word 0x00003000 -resets_base: - .word 0x4000c000 -</code></pre> + addr += nbyte; + blk++; + nblk++; + } -<h3>GPIOの設定</h3> + for (int i = 0; i &lt; nblk; i++) { + if (i == 0) + if (fseek(dst, 24, SEEK_SET) &lt; 0) { + fprintf(stderr, "Seek error: %s.\n argv[2]"); + retnum = 1; + goto defer; + } + fwrite32l(nblk, dst); + if (i &lt; nblk - 1) + if(fseek(dst, 512 - 4, SEEK_CUR) &lt; 0){ + fprintf(stderr, "Seek error: %s.\n argv[2]"); + retnum = 1; + goto defer; + } + } + +defer: + if (src) + fclose(src); + if (dst) + fclose(dst); + return retnum; +} +</code></pre> +<p><code>fwrite32l()</code>関数は指定されたファイルに32ビットの整数を下位バイトから順に書き込む関数である。バイトオーダーとかややこしそうなので作っておいたけど必要なのかな?あと名前が気に入らない。</p> <p> -次にGPIOの役割を設定する。前回はLEDを点滅させるためにGPIO25をSIOに設定したが、今回はGPIO0とGPIO1をUART0にする: +CRC32のチェックサムが書き込まれたバイナリファイルを、このプログラムでUF2に変換し、生成されたファイルをUSBストレージとして接続したRP2040にコピーすればフラッシュROMに書き込まれる。 </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> +<h2>Flash Second Stage</h2> <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>で、 +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> -<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> +しかしこのSSIコントローラはSynopsysという会社のDW_apb_ssiというIPを使っているようで、データシートのSSIコントローラの章は多分Synopsysの人が書いている。その他の章はRaspberry Pi財団の書いたブリティッシュイングリッシュだが、この部分だけ多分ネイティブじゃない人の書いたいい加減な英語である。誤植も多い。何日かかけて理解しようとしたがよく分からん。不毛なので一旦諦めた。</p> <p> -待ち時間を設定したら発振子を起動する。XOSC: CTRLに起動用のコマンド的なものを入力し、周波数が安定するのを待つ。</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 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 +<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 /* ... */ - -xosc_base: - .word 0x40024000 +rom_base: + .word 0x00000000 </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 +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> - // 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 +<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 /* ... */ -atomic_clr: - .word 0x00003000 -clocks_base: - .word 0x40008000 -pll_sys_base: - .word 0x40028000 +m0plus_vtor: + .word 0xe0000000 + 0xed08 </code></pre> +<p>なお以上のコードは<code>.boot2</code>という名前のセクションにしてある。 +</p> -<h3>UARTの設定</h3> +<h2>メインのコード(<code>main.s</code>)</h2> + +<h3>GPIOの設定</h3> <p> -データシートによるとUART設定の手順は以下の通り: +電源投入直後、RP2040の周辺機器はリセット状態になっている。まずは今回利用するGPIOのリセット状態を解除する必要がある。データシートの「2.14. Subsystem Resets」には以下のように書かれている: </p> -<ul> -<li>リセットの解除</li> -<li>clock_periの設定</li> -<li>UARTの有効化</li> -<li>FIFOの有効化</li> -<li>転送速度の設定</li> -<li>フォーマットの設定</li> -</ul> +<blockquote cite="https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf"> <p> -上の2つは既に終えている。残りの部分はこの順番どおりに設定しても動かなかった。C言語で書かれたサンプルを見ると、クロックを設定した後、転送速度の設定、UARTの有効化、FIFOの有効化の順になっている。そのとおりにすると動いた。理由はよく理解していないが、変数を設定してから起動するほうが素直ではある。</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> -転送速度はminicomのデフォルトである115200 baudに設定する。データシート「4.2.7.1. Baud Rate Calculation」の計算式において、クロック周波数を125MHzから133MHzに変えて計算して、BRDI=72、BDRF=0.157(=10/64)となる。この数値をUART: UARTIBRD、UART: UARTFBRDレジスタにそれぞれ代入する。 +リセット状態を解除するには、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> -UARTの有効化はUART: UARTCRレジスタのUARTENビットをセットすることで行う。C言語のサンプルでは同じレジスタのRXE、TXEビットもセットしているが、この2つはもともと1になっているのでほっといてよさそう。</p> +RESETS: RESETレジスタのうち5番ビットだけを<code>0x0</code>にしたい。この時、まずこのレジスタを読み込んでから<code>~(1 &lt;&lt; 5)</code>と論理積を取って同レジスタに書き戻してもいいのだが、RP2040にはこれを一回の<code>str</code>でしかもアトミックにできる機能が用意されている。今回の場合アトミックかどうかは関係ないと思うけど。</p> <p> -FIFOの有効化はUART: UARTLCR_HレジスタのFENビットをセットすることで行う。また、同じレジスタの他のビットで、データーのフォーマットを設定できる。ここではminicomのデフォルトに合わせてWLENを8bitにする。</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> // 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 +<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_set: - .word 0x00002000 -uart0_base: - .word 0x40034000 +atomic_clr: + .word 0x00003000 +resets_base: + .word 0x4000c000 </code></pre> -<h3>UARTの入出力</h3> +<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> -設定が終わったので実際に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 +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 /* ... */ -uart0_base: - .word 0x40034000 +io_bank0_base: + .word 0x40014000 </code></pre> +<h3>GPIOの出力を有効化</h3> <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 +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 /* ... */ -uart0_base: - .word 0x40034000 +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> -あとはこの2つの関数をループの中で交互に呼び出せば、オウム返しするだけのプログラムが完成する: +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> -<pre><code>loop: - bl getbyte - bl putbyte +</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 -</code></pre> -<h2>リング発振回路でUARTは動くんかな?</h2> -<p>UARTの通信には正確なクロックが必要である。その為上では<code>clk_peri</code>として水晶発振子とPLLを用いた。ところがpico-examplesのhello_uartでは<code>main()</code>関数で水晶発振子を設定していない。そこでリング発振回路を用いてみたのだが、どうもうまく通信できない。出力されている正確な周波数も分からないのであきらめることにした。オシロスコープなんていうものは持っていない。</p> +delay: + mov r0, #1 + lsl r0, r0, #20 +delay_loop: + sub r0, r0, #1 + bne delay_loop + bx lr -<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 +/* ... */ + +sio_base: + .word 0xd0000000 </code></pre> + +<h3>ベクターテーブル</h3> <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ではユーザープログラムの一部として組み込んでるんかな? +メインのコードの最後には上で説明したベクターテーブルを配置する。ここでは割り込みの処理は考えないので、初期スタックポインタとエントリーポイントだけである。初期スタックポインタはSRAMの最後?(<code>0x20040000</code>)、エントリーポイントはエントリーポイントのラベルを用いて設定した。上で説明したように、ベクターテーブルのアドレスは256バイトの境界にないといけないので、<code>.align 8</code>しておく。また、別のファイル(<code>boot2.s</code>)からアクセスしたいので、<code>.global</code>宣言をつけておく: </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 +<pre><code> .align 8 + .global vectors +vectors: + .word 0x20040000 // initial SP + .word (reset+1) </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 +<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> -<p>このファイルによると: + +<h2>Makefile</h2> +<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> +<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 -<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; +MCPU = -mcpu=cortex-m0plus +ASFLAGS = $(MCPU) +CFLAGS = $(MCPU) -ffreestanding -nostartfiles -O0 -fpic -mthumb -c +LDFLAGS = --no-relax -nostdlib -10000214 &lt;bss_fill_loop&gt;: -10000214: c101 stmia r1!, {r0} +all: tools led.uf2 -10000216 &lt;bss_fill_test&gt;: -10000216: 4291 cmp r1, r2 -10000218: d1fc bne.n 10000214 &lt;bss_fill_loop&gt; -</code></pre> +clean: + rm -f *.o *.elf *.uf2 *.bin + cd ../tools &amp;&amp; make clean -<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); +.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>やっぱり水晶発振子じゃないとあかんのかな。</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> +<p> +RP2040のボードをUSBデバイスモードでLinuxのパソコンに接続し、ex1ディレクトリで</p> +<pre><code>$ make +# make flash +</code></pre> +<p> +とすればプログラムがRP2040のボードに書き込まれて実行が開始される。</p> +<h2>最後に</h2> +<p> +光あれ。 +</p> <h2>参考</h2> <ul> <li> -<a href="https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf">RP2040 Datasheet.Raspberry Pi Foundation</a> +[1] Hennesy, J. L. and Patterson, D. A. 2017. Computer Organization And Design RISC-V Edition. </li> <li> -<a href="https://github.com/raspberrypi/pico-sdk">pico-sdk.github</a> +[2] <a href="https://akizukidenshi.com/catalog/g/gK-17542/">RP2040マイコンボードキット.秋月電子通商</a> </li> <li> -<a href="https://developer.arm.com/documentation/ddi0419/c/">ARMv6-M Architecture Reference Manual</a> +[3] <a href="https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf">RP2040 Datasheet.Raspberry Pi Foundation</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> +[4] <a href="https://github.com/raspberrypi/pico-sdk">pico-sdk.github</a> </li> <li> -<a href="https://www5.epsondevice.com/ja/information/technical_info/osc.html">水晶発振器とは? 原理と仕組み、水晶振動子との違い、選び方のポイントを解説.エプソン水晶デバイス</a> +[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> diff --git a/pub/sitemap.xml b/pub/sitemap.xml @@ -1,8 +1,8 @@ <?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_2.html</loc><lastmod>2024-02-24</lastmod></url> <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/computer/git_server.html</loc><lastmod>2024-02-15</lastmod></url>