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