rp2040_1.html (36578B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width,initial-scale=1"> 6 <link rel="stylesheet" type="text/css" href="/style.css"> 7 <link rel="icon" type="image/x-icon" href="/pics/favicon.ico"> 8 <title>RP2040 SDKなしでLチカ</title> 9 </head> 10 <body> 11 <header> 12 <a href="/">主頁</a> | 13 <a href="/about.html">自己紹介</a> | 14 <a href="/journal">日記</a> | 15 <a href="/farm">農業</a> | 16 <a href="/kitchen">台所</a> | 17 <a href="/computer">電算機</a> | 18 <a href="/poetry">詩</a> | 19 <a href="/books">本棚</a> | 20 <a href="/gallery">絵</a> | 21 <a href="https://git.mtkn.jp">Git</a> 22 </header> 23 <main> 24 <article> 25 <h1>RP2040 SDKなしでLチカ</h1> 26 <time>2023-04-25</time>: 作成<br /> 27 <time>2024-02-25</time>: ベクターテーブルの修正 28 29 <h2>はじめに</h2> 30 <p> 31 パタヘネのRISC-V<sup>[1]</sup>版を買って一通り読んだらアセンブリ言語で組込のプログラミングがしたくなった。RISC-Vのマイコンボードが欲しかったのだが、安くていい感じのものが見付からなかった。代わりに秋月電子通商でArmのものがあった。RP2040マイコンボードキット<sup>[2]</sup>というものである。ウェブ上の情報も多く、データシート<sup>[3]</sup>もしっかりしていそうなので、とりあえずこれを買ってみた。</p> 32 <p> 33 一般的にはSDK<sup>[4]</sup>をダウンロードしてあらかじめ用意されたライブラリを使って開発するようだが、これはビルドシステムとしてcmakeというのを使っている。これがOpenBSDでは何かエラーがでて動かなかった。僕はこういう便利ツールが嫌いだ。どうせ使わんからいいんやけど。関係ないけど途中から開発環境がLinuxに替わった。SDKには便利な関数がたくさん用意されているので楽である。ハードウェアの面倒な部分がプログラマから見えないようにしているからである。しかし今回はその面倒な部分に触れてみたくて買ったので、SDKを使うと意味がない。</p> 34 <p> 35 ということでSDKなしで開発してみる。とりあえず定番のLチカをば。</p> 36 <p> 37 ソースコード: <a href="https://git.mtkn.jp/rp2040">git</a> 38 </p> 39 40 <h2>動作環境</h2> 41 <ul> 42 <li>Arch Linux 6.2.12-arch1-1 43 <ul> 44 <li>arm-none-eabi-binutils 2.40-1</li> 45 <li>GNU Make 4.4.1</li> 46 </ul> 47 </li> 48 <li>OpenBSD 7.3 49 <ul> 50 <li>arm-none-eabi-binutils 2.31.1</li> 51 <li>make (バージョン?)</li> 52 </ul> 53 ※<code>make flash</code>は動かん。<code>dmesg</code>でデバイス確認して手動でマウントする必要がある。 54 </li> 55 </ul> 56 57 <h2>Boot Process</h2> 58 <p> 59 RP2040は電源を入れるといくつかの段階(ここでは関係ないので省略。データシート「2.8.1 Processor Controlled Boot Sequence」に詳しく書いてある)を踏んだあと、外部のフラッシュROMの先頭から256バイトを内部のSRAMにコピーして、フラッシュにプログラムが書き込まれているかどうか確認する。RP2040はフラッシュの先頭252バイトから計算したCRC32チェックサムを、直後の253バイト目から256バイトに記録することになっている。起動時にこのチェックサムを確認することで、フラッシュにプログラムが書き込まれているかどうか確かめている。コピーした最後の4バイトと起動時に最初の252バイトから計算したチェックサムが一致していれば、そのままコピーしてきた256バイトの先頭にPCをセットして実行を開始する。一致しなければUSBデバイスモードに切り替わり、パソコンに接続するとストレージとして認識される。このストレージにUF2という形式に変換したプログラムをコピーするとプログラムがフラッシュROMやSRAMに書き込まれる。 60 </p> 61 <p> 62 以上のことから、プログラムを実行するためにはCRC32を計算し、UF2という形式に変換することが必要である。ソースコードからの流れは以下の通り: 63 </p> 64 <pre>source bin bin with 65 code ----------> object ------> elf --------> bin -------> with --------> crc32 in 66 crc32 uf2 format 67 assemble link objcopy bincrc bin2uf2 68 </pre> 69 70 <h2>CRC(巡回冗長検査)</h2> 71 <p> 72 入力のデータをごにょごにょしてある値を出力する。</p> 73 <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"> 74 <p> 75 データ転送等に伴う偶発的な誤りの検査によく使われている<sup>[5]</sup>。 76 </p> 77 </blockquote> 78 <p> 79 らしい。 80 </p> 81 <p> 82 入力のビットを一列に並べて、除数で「割り算」していく。この「割り算」が多項式の除算に似ているので、この除数をCRC多項式というらしい。ただし多項式の除算と違い、引き算するところをXORする。CRC32の場合、除数は33ビットである。33ビットで割ると32ビットの余りが残る。この余りがCRC32のチェックサムである。除数は色々あるようだが、標準的なものがWikipedia<sup>[5]</sup>に列挙されている。除数<code>1011</code>を使ったCRC3の計算の手順は以下の通り: 83 </p> 84 <pre><code>1110101011011100110101101101111 入力(適当) 85 1011 除数(4ビット) 86 ------------------------------- 87 101101011011100110101101101111 結果(入力と除数のXOR) 88 1011 89 ------------------------------ 90 00001011011100110101101101111 91 1011 92 ------------------------- 93 000011100110101101101111 94 1011 95 -------------------- 96 1010110101101101111 97 1011 98 ------------------- 99 001110101101101111 100 1011 101 ---------------- 102 101101101101111 103 1011 104 --------------- 105 00001101101111 106 1011 107 ---------- 108 110101111 109 1011 110 --------- 111 11001111 112 1011 113 -------- 114 1111111 115 1011 116 ------- 117 100111 118 1011 119 ------ 120 01011 121 1011 122 ---- 123 000 CRC3チェックサム 124 </code></pre> 125 <p> 126 普通の割り算と基本は同じであるが、引き算の部分だけXORになっている。</p> 127 <p> 128 以上の計算をプログラムの先頭252バイトに対して、33ビットの除数を用いて行う。データの並べ方は、上の例において左側を先頭に、フラッシュROM上の0番地から、各バイトは最上位ビットから順に並べる。入力のデータは253バイト目から256バイト目に<code>0</code>をひっつけて計算する。これは多分予め長さが分からないデータでも計算できるようにしたかったからかな。除数は<code>0x104c11db7</code>である(最上位ビットは常に1なのでデータシートでは省略されている)。</p> 129 <p> 130 入力データは1バイトづつ処理したいみたいである。多分通信等で使う都合である。この時XORは結合則が成り立つので1バイト処理した結果と次のバイトとをXORして次の処理の入力として利用することができる: 131 </p> 132 <pre><code>111000111000000110000110111000111000001010010011111000111000000110010011 入力(適当) 133 |......| 134 111000110000000000000000000000000 先頭1バイト 135 100000100110000010001110110110111 除数 136 ------------------------------------------------------------------------ 137 011000010110000010001110110110111 138 100000100110000010001110110110111 139 ----------------------------------------------------------------------- 140 010000001010000110010011011011001 141 100000100110000010001110110110111 142 ---------------------------------------------------------------------- 143 000000110010001110101000000000101 144 |......| 145 110010001110101000000000101000000 1バイト目の結果 146 |......| 147 10000001 入力の2バイト目 148 ---------------------------------------------------------------- 149 010010011110101000000000101000000 1バイト目の結果と2バイト目のXOR 150 100000100110000010001110110110111 除数 151 ---------------------------------------------------------------- 152 000100011011010010001111100110111 153 . 154 . 155 . 156 </code></pre> 157 <p> 158 以上の操作は以下のようなアルゴリズムのループで実装できる。</p> 159 <ul> 160 <li>前回の結果と、入力データの次のバイトをXOR</li> 161 <li> 162 <ul> 163 <li>先頭の1ビットが1の場合、除数とXORを取り左シフト</li> 164 <li>先頭の1ビットが0の場合、そのまま左シフト</li> 165 </ul> 166 </li> 167 </ul> 168 <p> 169 これを<code>for</code>ループで回す都合上、最初のバイトもXORを取る。上の例では最初は<code>0x0</code>とXORを取っているが、この値を<code>0x0</code>以外にすることもできる。そうした方がいろいろいいこともあるらしい。RP2040では<code>0xffffffff</code>を使う。更にこの工程を32ビットの<code>int</code>だけで行うことを考える: 170 </p> 171 <pre><code>111000111000000110000110111000111000001010010011111000111000000110010011 入力(適当) 172 173 11111111111111111111111111111111 0xffffffff 174 11100011000000000000000000000000 先頭1バイトを24ビットシフト 175 -------------------------------- XOR 176 00011100111111111111111111111111 177 先頭1ビットが0なので1ビットシフト 178 -------------------------------- シフト 179 00111001111111111111111111111110 180 先頭1ビットが0なので1ビットシフト 181 -------------------------------- シフト 182 01110011111111111111111111111100 183 先頭1ビットが0なので1ビットシフト 184 -------------------------------- シフト 185 11100111111111111111111111111000 186 先頭1ビットが1なので1ビットシフトした後、除数の下位32ビットとXOR: 187 11001111111111111111111111110000 シフト 188 00000100110000010001110110110111 除数の下位32ビット 189 -------------------------------- XOR 190 11001011001111101110001001000111 191 先頭1ビットが1なので1ビットシフトした後、除数の下位32ビットとXOR: 192 10010110011111011100010010001110 シフト 193 00000100110000010001110110110111 除数の下位32ビット 194 -------------------------------- XOR 195 10010010101111001101100100111001 196 先頭1ビットが1なので1ビットシフトした後、除数の下位32ビットとXOR: 197 00100101011110011011001001110010 シフト 198 00000100110000010001110110110111 除数の下位32ビット 199 -------------------------------- XOR 200 00100001101110001010111111000101 201 先頭1ビットが0なので1ビットシフト 202 -------------------------------- シフト 203 01000011011100010101111110001010 204 先頭1ビットが0なので1ビットシフト 205 -------------------------------- シフト 206 10000110111000101011111100010100 1バイト目の結果 207 208 10000001 入力の2バイト目 209 -------------------------------- XOR 210 00000111111000101011111100010100 211 先頭1ビットが0なので1ビットシフト 212 -------------------------------- シフト 213 00001111110001010111111000101000 214 . 215 . 216 . 217 </code></pre> 218 <p> 219 これを実装したのが以下のコード:</p> 220 <pre><code>uint32_t 221 crc32(uint8_t *idata, size_t len) 222 { 223 uint32_t pol = 0x04C11DB7; 224 uint32_t c = 0xFFFFFFFF; 225 uint32_t b; 226 227 for (int i = 0; i < len; i++) { 228 b = idata[i] << 24; 229 c ^= b; 230 for (int j = 0; j < 8; j++) { 231 c = c >> 31 & 1 ? c << 1 ^ pol : c << 1; 232 } 233 } 234 235 return c; 236 } 237 </code></pre> 238 <p> 239 <code>main()</code>関数では上の<code>crc32()</code>に、<code>idata</code>として入力となるバイナリデータの先頭を、<code>len</code>として<code>252</code>を渡してCRC32を計算させる。その後、出力先のファイルに入力元のデータをコピーしていき、253バイト目から256バイト目だけ、計算したCRC32に置き換える。入力元のこの場所にデータが書き込まれていないかどうかは確かめていない。 240 </p> 241 242 <h2>UF2(USB Flashing Format)</h2> 243 <p> 244 Microsoftが開発したフラッシュ書き込み用のファイル形式らしい: 245 <blockquote cite="https://github.com/microsoft/uf2"> 246 <p> 247 UF2 is a file format, developed by Microsoft for PXT (also known as 248 Microsoft MakeCode), that is particularly suitable for flashing microcontrollers 249 over MSC (Mass Storage Class; aka removable flash drive)<sup>[6]</sup>. 250 </p> 251 </blockquote> 252 <p> 253 このファイルに変換する上で必要な情報はGitHubのmicrosoft/uf2<sup>[6]</sup>に表として纏められている: 254 <blockquote cite="https://github.com/microsoft/uf2"> 255 <table> 256 <thead><tr> 257 <th>Offset</th><th>Size</th><th>Value</th> 258 </tr></thead> 259 <tbody> 260 <tr> 261 <td>0</td> 262 <td>4</td> 263 <td>First magic number, <code>0x0A324655</code> (<code>"UF2\n"</code>)</td> 264 </tr> 265 <tr> 266 <td>4</td> 267 <td>4</td> 268 <td>Second magic number, <code>0x9E5D5157</code></td> 269 </tr> 270 <tr> 271 <td>8</td> 272 <td>4</td> 273 <td>Flags</td> 274 </tr> 275 <tr> 276 <td>12</td> 277 <td>4</td> 278 <td>Address in flash where the data should be written</td> 279 </tr> 280 <tr> 281 <td>16</td> 282 <td>4</td> 283 <td>Number of bytes used in data (often 256)</td> 284 </tr> 285 <tr> 286 <td>20</td> 287 <td>4</td> 288 <td>Sequential block number; starts at 0</td> 289 </tr> 290 <tr> 291 <td>24</td> 292 <td>4</td> 293 <td>Total number of blocks in file</td> 294 </tr> 295 <tr> 296 <td>28</td> 297 <td>4</td> 298 <td>File size or board family ID or zero</td> 299 </tr> 300 <tr> 301 <td>32</td> 302 <td>476</td> 303 <td>Data, padded with zeros</td> 304 </tr> 305 <tr> 306 <td>508</td> 307 <td>4</td> 308 <td>Final magic number, <code>0x0AB16F30</code></td> 309 </tr> 310 </tbody> 311 </table> 312 </blockquote> 313 314 <p> 315 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バイト目にブロックの通し番号と総数をそれぞれ書き込めばいい。ブロックの通し番号はデータのついでに書き込めるが、総数はデータを全部さばいた後でないと分からないので、最後全てのブロックにまとめて書き込むようにした。できたのが以下のコード: 316 </p> 317 <pre><code>#include <stdio.h> 318 #include <stdint.h> 319 #include <stdlib.h> 320 #include <string.h> 321 322 323 size_t 324 fwrite32l(uint32_t d, FILE *f) 325 { 326 int i; 327 uint8_t b; 328 for (i = 0; i < 32; i += 8) { 329 b = (uint8_t) (d >> i & 0xff); 330 fwrite(&b, 1, 1, f); 331 if (ferror(f)) { 332 fprintf(stderr, "Fwrite32l: write error.\n"); 333 return 0; 334 } 335 } 336 return 4; 337 } 338 339 int 340 main(int argc, char *argv[]) 341 { 342 FILE *src = NULL, *dst = NULL; 343 size_t sdata = 476; 344 int retnum = 0; 345 346 uint32_t mag1 = 0x0A324655; 347 uint32_t mag2 = 0x9E5D5157; 348 uint32_t flags = 0x00002000; // familyID present 349 uint32_t addr = 0x10000000; 350 uint32_t nbyte = 256; 351 uint32_t blk = 0; 352 uint32_t nblk = 0; 353 uint32_t famid = 0xe48bff56; 354 uint8_t data[sdata]; 355 uint32_t mag3 = 0x0AB16F30; 356 357 memset(data, 0, sdata); 358 359 if (argc != 3) { 360 fprintf(stderr, "Usage: %s src dst\n", argv[0]); 361 exit(1); 362 } 363 364 if ((src = fopen(argv[1], "rb")) == NULL) { 365 fprintf(stderr, "Could not open %s.\n", argv[1]); 366 retnum = 1; 367 goto defer; 368 } 369 if ((dst = fopen(argv[2], "wb")) == NULL) { 370 fprintf(stderr, "Could not open %s.\n", argv[2]); 371 retnum = 1; 372 goto defer; 373 } 374 375 while (!feof(src)) { 376 fwrite32l(mag1, dst); 377 fwrite32l(mag2, dst); 378 fwrite32l(flags, dst); 379 fwrite32l(addr, dst); 380 fwrite32l(nbyte, dst); 381 fwrite32l(blk, dst); 382 fwrite32l(nblk, dst); // dummy 383 fwrite32l(famid, dst); 384 385 fread(data, 1, nbyte, src); 386 if (ferror(src)) { 387 fprintf(stderr, "Read error: %s.\n", argv[1]); 388 retnum = 1; 389 goto defer; 390 } 391 fwrite(data, 1, sdata, dst); 392 if (ferror(src)) { 393 fprintf(stderr, "Write error: %s.\n", argv[2]); 394 retnum = 1; 395 goto defer; 396 } 397 398 fwrite32l(mag3, dst); 399 400 addr += nbyte; 401 blk++; 402 nblk++; 403 } 404 405 for (int i = 0; i < nblk; i++) { 406 if (i == 0) 407 if (fseek(dst, 24, SEEK_SET) < 0) { 408 fprintf(stderr, "Seek error: %s.\n argv[2]"); 409 retnum = 1; 410 goto defer; 411 } 412 fwrite32l(nblk, dst); 413 if (i < nblk - 1) 414 if(fseek(dst, 512 - 4, SEEK_CUR) < 0){ 415 fprintf(stderr, "Seek error: %s.\n argv[2]"); 416 retnum = 1; 417 goto defer; 418 } 419 } 420 421 defer: 422 if (src) 423 fclose(src); 424 if (dst) 425 fclose(dst); 426 return retnum; 427 } 428 </code></pre> 429 <p><code>fwrite32l()</code>関数は指定されたファイルに32ビットの整数を下位バイトから順に書き込む関数である。バイトオーダーとかややこしそうなので作っておいたけど必要なのかな?あと名前が気に入らない。</p> 430 <p> 431 CRC32のチェックサムが書き込まれたバイナリファイルを、このプログラムでUF2に変換し、生成されたファイルをUSBストレージとして接続したRP2040にコピーすればフラッシュROMに書き込まれる。 432 </p> 433 434 <h2>Flash Second Stage</h2> 435 <p> 436 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にデータをコピーすることなく命令を実行できるので、速くて便利だという。 437 </p> 438 <p> 439 しかしこのSSIコントローラはSynopsysという会社のDW_apb_ssiというIPを使っているようで、データシートのSSIコントローラの章は多分Synopsysの人が書いている。その他の章はRaspberry Pi財団の書いたブリティッシュイングリッシュだが、この部分だけ多分ネイティブじゃない人の書いたいい加減な英語である。誤植も多い。何日かかけて理解しようとしたがよく分からん。不毛なので一旦諦めた。</p> 440 <p> 441 RP2040には内部にもROMがあり、はバージョン情報や電源を投入した時の動作、その他便利な関数が書き込まれている。この関数の中に外部のフラッシュROMとSSIコントローラを設定するものも含まれているので、今回はこれを利用した。ただしこの方法だとフラッシュROMとの通信方式がStandard SPIのままなので少し遅いらしい。詳しくはデータシートの「2.3.8. Bootrom Contents」を参照。 442 </p> 443 <p> 444 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モードになる: 445 </p> 446 <pre><code>setup_xip: 447 ldr r3, rom_base 448 449 ldrh r0, [r3, #0x14] // rom_func_table 450 ldr r1, =('C' | 'X' << 8) // _flash_enter_cmd_xip() 451 ldrh r2, [r3, #0x18] // rom_table_lookup 452 blx r2 453 blx r0 454 /* ... */ 455 rom_base: 456 .word 0x00000000 457 </code></pre> 458 459 <p> 460 XIPの設定が完了すれば、次はメインのプログラムを実行するための準備である。エントリーポイントの指定、スタックポインタの初期値の設定、ベクターテーブルの設定である。Armのマニュアル<sup>[7]</sup>によると、初期スタックポインタとエントリーポイントはベクターテーブルの<code>0x0</code>バイト目と<code>0x4</code>バイト目に書くことになっている:</p> 461 <blockquote cite="https://developer.arm.com/documentation/ddi0419/c/System-Level-Architecture/System-Level-Programmers--Model/ARMv6-M-exception-model/Exception-number-definition"> 462 <table> 463 <caption> 464 Table 7.3. Exception numbers 465 </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> 466 </table> 467 </blockquote> 468 469 <blockquote cite="https://developer.arm.com/documentation/ddi0419/c/System-Level-Architecture/System-Level-Programmers--Model/ARMv6-M-exception-model/The-vector-table"> 470 <table> 471 <caption> 472 Table 7.4. Vector table format 473 </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> 474 </table> 475 </blockquote> 476 <p> 477 RP2040のベクターテーブルはM0PLUS: VTOR(<code>0xe0000000 + 0xed08</code>)というレジスタに書き込むことで設定する。このとき、下位8ビットは0にしないといけないので、ベクターテーブルの位置は256バイトでアラインする必要がある。ベクターテーブルの定義は<code>main.s</code>に書き、<code>boot2.s</code>からはラベルを使ってアクセスすることにする。以上をまとめると以下のコードになる:</p> 478 <pre><code> ldr r0, =vectors 479 ldr r1, m0plus_vtor 480 str r0, [r1, #0] // vector table 481 ldr r1, [r0, #4] // entry point 482 ldr r0, [r0, #0] // stack pointer 483 mov sp, r0 484 bx r1 485 486 /* ... */ 487 488 m0plus_vtor: 489 .word 0xe0000000 + 0xed08 490 </code></pre> 491 <p>なお以上のコードは<code>.boot2</code>という名前のセクションにしてある。 492 </p> 493 494 <h2>メインのコード(<code>main.s</code>)</h2> 495 496 <h3>ベクターテーブル</h3> 497 <p> 498 上で説明したように、ベクターテーブルのアドレスは256バイトの境界にないといけないが、<code>boot2.s</code>をフラッシュの最初の256バイトに配置しており、<code>main.s</code>はその直後から始まるようにリンクする。そのためメインのコードの最初にベクターテーブルを配置すればいい。ここでは割り込みの処理は考えないので、初期スタックポインタとエントリーポイントだけである。初期スタックポインタはSRAMの最後?(<code>0x20040000</code>)、エントリーポイントはエントリーポイントのラベルを用いて設定した。また、別のファイル(<code>boot2.s</code>)からアクセスしたいので、<code>.global</code>宣言をつけておく: 499 </p> 500 <pre><code> .global vectors 501 vectors: 502 .word 0x20040000 // initial SP 503 .word (reset+1) 504 </code></pre> 505 <p> 506 <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> 507 508 <h3>GPIOの設定</h3> 509 <p> 510 電源投入直後、RP2040の周辺機器はリセット状態になっている。まずは今回利用するGPIOのリセット状態を解除する必要がある。データシートの「2.14. Subsystem Resets」には以下のように書かれている: 511 </p> 512 <blockquote cite="https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf"> 513 <p> 514 Every peripheral reset by the reset controller is held in reset at power-up. 515 It is up to software to deassert the reset of peripherals it intends to use. 516 </p> 517 </blockquote> 518 <p> 519 リセット状態を解除するには、RESETS_BASE(<code>0x4000c000</code>)から<code>0x0</code>バイト目のRESETS: RESETレジスタのうち利用したい周辺機器のビットを<code>0x0</code>にすればいい。 520 GPIOはIO Bank 0なので(これ明記されてなくない?)、RESETS: RESETレジスタのIO_BANK0(5番ビット)を<code>0x0</code>にする。 521 </p> 522 <h4>レジスタのアトミックなクリア</h4> 523 <p> 524 RESETS: RESETレジスタのうち5番ビットだけを<code>0x0</code>にしたい。この時、まずこのレジスタを読み込んでから<code>~(1 << 5)</code>と論理積を取って同レジスタに書き戻してもいいのだが、RP2040にはこれを一回の<code>str</code>でしかもアトミックにできる機能が用意されている。今回の場合アトミックかどうかは関係ないと思うけど。</p> 525 <p> 526 各レジスタには4個のアドレスが割り当てられている。データシートの各章のList of Registersに記載されているアドレスは通常の読み書きができる。そのアドレスに<code>0x1000</code>を足したものにアクセスするとアトミックなXORが、<code>0x2000</code>を足したものはアトミックなセットが、<code>0x3000</code>を足したものはアトミックなクリアができる。つまりレジスタのアドレスに<code>0x3000</code>を足したものに、<code>0x1 << 5</code>を<code>str</code>すれば5番目のビットだけ<code>0x0</code>にして、他のビットは変更されない。逆に指定したビットだけ立てて他を触らない場合は<code>0x2000</code>を、あるいは指定したビットだけトグルしたい場合は<code>0x1000</code>を足したアドレスにアクセスすればいい。</p> 527 <h4>リセット状態の確認</h4> 528 <p>リセットの解除はすぐに完了するわけではないようである。リセットの解除が完了したかどうか確認するにはRESETS: RESET_DONEレジスタ(RESETS_BASEから<code>0x8</code>バイト目)の該当するビット(ここでは5番目のビット)を読む。この値が<code>0x1</code>であればリセットの解除が完了している。<code>0x0</code>であれば処理が進行中なので<code>0x1</code>が返ってくるまで繰り返し読み込んで<code>0x0</code>になるまで待機する。ところでこのレジスタはリセットの解除が完了したかどうか確かめるものなので、RESET_DONEという名前はどうなん? 529 <p> 530 以上から、GPIOのリセットを解除するのは以下のコード: 531 </p> 532 <pre><code>reset: 533 // unreset gpio 534 mov r0, #1 535 lsl r0, r0, #5 // io_bank0 536 ldr r3, resets_base 537 ldr r1, atomic_clr 538 str r0, [r3, r1] // RESETS: RESET 539 reset_chk: 540 ldr r1, [r3, #0x8] // RESETS: RESET_DONE 541 tst r0, r1 542 beq reset_chk 543 544 /* ... */ 545 546 atomic_clr: 547 .word 0x00003000 548 resets_base: 549 .word 0x4000c000 550 </code></pre> 551 552 <h3>GPIOの機能の選択</h3> 553 <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> 554 <p> 555 GPIO25番の機能を選択するにはIO_BANK0_BASE(<code>0x40014000</code>)から<code>0xcc</code>番目のGPIO25_CTRLレジスタの下位5ビットに、該当する機能の番号を書き込めばいい。データシートの「2.19.2 Function Select」にある表を見ると、GPIO25番のSIOは5である:</p> 556 <pre><code> // set gpio functions 557 ldr r3, io_bank0_base 558 mov r0, #5 // sio 559 mov r1, #0xcc 560 str r0, [r3, r1] // IO_BANK0: GPIO25_CTRL 561 562 /* ... */ 563 564 io_bank0_base: 565 .word 0x40014000 566 </code></pre> 567 568 <h3>GPIOの出力を有効化</h3> 569 <p> 570 GPIO25番がSIOになったので、次にこのピンからの出力を有効化する。既定値では出力は無効になっている。ハイインピーダンスってことなのかな?出力を有効にするには、SIO_BASE(<code>0xd0000000</code>)から<code>0x24</code>バイト目のSIO: GPIO_OEレジスタの該当するビット(25番のピンなので25番ビット)を<code>0x1</code>にする: 571 </p> 572 <pre><code> // enable gpio output 573 ldr r3, sio_base 574 mov r0, #1 575 lsl r0, r0, #25 // gpio25 576 str r0, [r3, #0x24] // SIO: GPIO_OE 577 578 /* ... */ 579 580 sio_base: 581 .word 0xd0000000 582 </code></pre> 583 584 <h3>LEDの点滅</h3> 585 <p>以上でGPIOの設定は完了したので、あとは実際にLEDに電圧を掛けるだけである。レジスタのアドレスに<code>0x1000</code>を足したものに書き込むとアトミックなレジスタのXORができると書いたが、SIOはこの機能がサポートされていないようである。データシートの「2.1.2 Atomic Register Access」に、 586 </p> 587 <blockquote cite="https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf"> 588 <p> 589 The SIO (Section 2.3.1), a single-cycle IO block attached directly to the cores' 590 IO ports, does <strong>not</strong> support atomic accesses at the bus level, 591 although some individual registers (e.g. GPIO) have set/clear/xor aliases. 592 </p> 593 </blockquote> 594 <p> 595 と書かれている。そのかわりここにも書かれている通り、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> 596 597 <pre><code> // blink led on gpio25 598 ldr r4, sio_base 599 mov r5, r0 // r0 = 1 << 25 600 loop: 601 str r5, [r4, #0x1c] // SIO: GPIO_OUT_XOR 602 bl delay 603 b loop 604 605 delay: 606 mov r0, #1 607 lsl r0, r0, #20 608 delay_loop: 609 sub r0, r0, #1 610 bne delay_loop 611 bx lr 612 613 /* ... */ 614 615 sio_base: 616 .word 0xd0000000 617 </code></pre> 618 <p>なお以上のコードは<code>.text</code>セクションである。</p> 619 620 <h2>リンカスクリプト</h2> 621 <p> 622 以上のコードには<code>.boot2</code>、<code>.text</code>の2つのセクションが含まれる。<code>.boot2</code>はフラッシュの先頭から256(<code>0x100</code>)バイト目まで、<code>.text</code>はその後ろに続くように配置する: 623 <pre><code>MEMORY 624 { 625 FLASH(rx) : ORIGIN = 0x10000000, LENGTH = 2048k 626 } 627 628 SECTIONS 629 { 630 .boot2 : { 631 *(.boot2) 632 . = 0x100; 633 } > FLASH 634 635 .text : { 636 *(.text) 637 } > FLASH 638 } 639 </code></pre> 640 641 <h2>Makefile</h2> 642 <p> 643 以上のソースコードは以下のように配置している: 644 </p> 645 <pre>rp2040 646 ├── ex1 647 │ ├── Makefile 648 │ ├── boot2.s 649 │ ├── main.s 650 │ └── memmap.ld 651 └── tools 652 ├── Makefile 653 ├── bin2uf2.c 654 └── bincrc.c 655 </pre> 656 <p> 657 toolsディレクトリのMakefileは同じディレクトリのソースファイルを<code>$(CC)</code>でコンパイルするだけのものである(個人的な趣味で<code>tcc</code>を使っている)。ex1ディレクトリのMakefileは以下の通り: 658 </p> 659 <pre><code>AS = arm-none-eabi-as 660 LD = arm-none-eabi-ld 661 OBJCOPY = arm-none-eabi-objcopy 662 BINCRC = ../tools/bincrc 663 BIN2UF2 = ../tools/bin2uf2 664 665 MCPU = -mcpu=cortex-m0plus 666 ASFLAGS = $(MCPU) 667 CFLAGS = $(MCPU) -ffreestanding -nostartfiles -O0 -fpic -mthumb -c 668 LDFLAGS = --no-relax -nostdlib 669 670 all: tools led.uf2 671 672 clean: 673 rm -f *.o *.elf *.uf2 *.bin 674 cd ../tools && make clean 675 676 .s.o: 677 $(AS) $(ASFLAGS) -o $@ $< 678 679 led.elf: boot2.o main.o memmap.ld 680 $(LD) $(LDFLAGS) -o $@ -T memmap.ld boot2.o main.o 681 682 led.bin: led.elf 683 $(OBJCOPY) -O binary led.elf $@ 684 685 led.uf2: led.bin 686 $(BINCRC) led.bin led_crc.bin 687 $(BIN2UF2) led_crc.bin $@ 688 689 flash: all 690 mount /dev/disk/by-label/RPI-RP2 /mnt 691 cp led.uf2 /mnt 692 693 tools: 694 cd ../tools && make 695 </code></pre> 696 697 <p> 698 RP2040のボードをUSBデバイスモードでLinuxのパソコンに接続し、ex1ディレクトリで</p> 699 <pre><code>$ make 700 # make flash 701 </code></pre> 702 <p> 703 とすればプログラムがRP2040のボードに書き込まれて実行が開始される。</p> 704 705 <h2>最後に</h2> 706 <p> 707 光あれ。 708 </p> 709 710 <h2>参考</h2> 711 <ul> 712 <li> 713 [1] Hennesy, J. L. and Patterson, D. A. 2017. Computer Organization And Design RISC-V Edition. 714 </li> 715 <li> 716 [2] <a href="https://akizukidenshi.com/catalog/g/gK-17542/">RP2040マイコンボードキット.秋月電子通商</a> 717 </li> 718 <li> 719 [3] <a href="https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf">RP2040 Datasheet.Raspberry Pi Foundation</a> 720 </li> 721 <li> 722 [4] <a href="https://github.com/raspberrypi/pico-sdk">pico-sdk.github</a> 723 </li> 724 <li> 725 [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> 726 </li> 727 <li> 728 [6] <a href="https://github.com/microsoft/uf2">USB Flashing Format (UF2).GitHub</a> 729 </li> 730 <li> 731 [7] <a href="https://developer.arm.com/documentation/ddi0419/c/">ARMv6-M Architecture Reference Manual</a> 732 </li> 733 </ul> 734 </article> 735 736 </main> 737 <footer> 738 <address>info(at)mtkn(dot)jp</address> 739 <a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" rel="license noopener noreferrer">CC0 1.0</a> 740 </footer> 741 </body> 742 </html>