www.mtkn.jp

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

commit b37ddebf5ecf5f131930a222b93e5cb61d427a7f
parent b9597e84c46d4cf3237dd94b6b40b6474866ba2a
Author: Matsuda Kenji <info@mtkn.jp>
Date:   Tue, 25 Apr 2023 18:56:12 +0900

publish

Diffstat:
Aman/computer/rp2040_1.html | 934+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apub/computer/rp2040_1.html | 741+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 1675 insertions(+), 0 deletions(-)

diff --git a/man/computer/rp2040_1.html b/man/computer/rp2040_1.html @@ -0,0 +1,934 @@ +<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> +入力のデータをごにょごにょしてある値を出力する。\ +<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> +入力のビットを一列に並べて、除数で「割り算」していく。\ +この「割り算」が多項式の除算に似ているので、この除数を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>だけで行うことを考える: +</p> +<pre><code>\ +111000111000000110000110111000111000001010010011111000111000000110010011 入力(適当) + +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 +. +. +. +</code></pre> +<p> +これを実装したのが以下のコード:\ +</p> +<pre><code>\ +uint32_t +crc32(uint8_t *idata, size_t len) +{ + uint32_t pol = 0x04C11DB7; + uint32_t c = 0xFFFFFFFF; + uint32_t b; + + 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>UF2(USB Flashing Format)</h2> +<p> +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> +</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; + + +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; +} + +int +main(int argc, char *argv[]) +{ + FILE *src = NULL, *dst = NULL; + size_t sdata = 476; + int retnum = 0; + + 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); + + if (argc != 3) { + fprintf(stderr, "Usage: %s src dst\n", argv[0]); + exit(1); + } + + 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; +} +</code></pre> +<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' << 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>バイト目に書くことになっている: +<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> + +また、ベクターテーブルはメインのプログラムの先頭に置くことにする。\ +メインのプログラムは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 + +/* ... */ + +flash_main: + .word 0x10000000 + 0x100 +m0plus_vtor: + .word 0xe0000000 + 0xed08 +</code></pre> +<p>\ +なお以上のコードは<code>.boot2</code>という名前のセクションにしてある。 +</p> + +<h2>メインのコード(<code>main.s</code>)<h2> +<h3>ベクターテーブル</h3> +<p> +メインのコードの最初には上で説明したベクターテーブルを配置する。\ +ここでは割り込みの処理は考えないので、初期スタックポインタと\ +エントリーポイントだけである。初期スタックポインタはSRAMの最後?\ +(<code>0x20040000</code>)、エントリーポイントはエントリーポイントの\ +ラベルを用いて設定した。\ +</p> +<pre><code>\ +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>.vectors</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> +<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 +</pre></code> + +<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 << 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> +<p>\ +なお以上のコードは<code>.text</code>セクションである。\ +</p> + +<h2>リンカスクリプト</h2> +<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 +} +</code></pre> + +<h2>Makefile</h2> +<p> +以上のソースコードは以下のように配置している: +</p> +<pre><code>\ +rp2040 +├── ex1 +│   ├── Makefile +│   ├── boot2.s +│   ├── main.s +│   └── memmap.ld +└── tools + ├── Makefile + ├── bin2uf2.c + └── bincrc.c +</code></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> diff --git a/pub/computer/rp2040_1.html b/pub/computer/rp2040_1.html @@ -0,0 +1,741 @@ +<!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なしでLチカ</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="https://git.mtkn.jp">Git</a> + </header> + <main> + <article> +<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> +入力のデータをごにょごにょしてある値を出力する。<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> +入力のビットを一列に並べて、除数で「割り算」していく。この「割り算」が多項式の除算に似ているので、この除数を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>だけで行うことを考える: +</p> +<pre><code>111000111000000110000110111000111000001010010011111000111000000110010011 入力(適当) + +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 +. +. +. +</code></pre> +<p> +これを実装したのが以下のコード:</p> +<pre><code>uint32_t +crc32(uint8_t *idata, size_t len) +{ + uint32_t pol = 0x04C11DB7; + uint32_t c = 0xFFFFFFFF; + uint32_t b; + + 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>UF2(USB Flashing Format)</h2> +<p> +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> +</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; + + +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; +} + +int +main(int argc, char *argv[]) +{ + FILE *src = NULL, *dst = NULL; + size_t sdata = 476; + int retnum = 0; + + 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); + + if (argc != 3) { + fprintf(stderr, "Usage: %s src dst\n", argv[0]); + exit(1); + } + + 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; +} +</code></pre> +<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' << 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>バイト目に書くことになっている: +<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> + +また、ベクターテーブルはメインのプログラムの先頭に置くことにする。メインのプログラムは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 + +/* ... */ + +flash_main: + .word 0x10000000 + 0x100 +m0plus_vtor: + .word 0xe0000000 + 0xed08 +</code></pre> +<p>なお以上のコードは<code>.boot2</code>という名前のセクションにしてある。 +</p> + +<h2>メインのコード(<code>main.s</code>)<h2> +<h3>ベクターテーブル</h3> +<p> +メインのコードの最初には上で説明したベクターテーブルを配置する。ここでは割り込みの処理は考えないので、初期スタックポインタとエントリーポイントだけである。初期スタックポインタはSRAMの最後?(<code>0x20040000</code>)、エントリーポイントはエントリーポイントのラベルを用いて設定した。</p> +<pre><code>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>.vectors</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> +<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 +</pre></code> + +<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 << 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> +<p>なお以上のコードは<code>.text</code>セクションである。</p> + +<h2>リンカスクリプト</h2> +<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 +} +</code></pre> + +<h2>Makefile</h2> +<p> +以上のソースコードは以下のように配置している: +</p> +<pre><code>rp2040 +├── ex1 +│   ├── Makefile +│   ├── boot2.s +│   ├── main.s +│   └── memmap.ld +└── tools + ├── Makefile + ├── bin2uf2.c + └── bincrc.c +</code></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> + </article> + + </main> + <footer> + <address>info(at)mtkn(dot)jp</address> + </footer> +</body> +</html>