RP2040 SDKなしでLチカ
: 作成: ベクターテーブルの修正
はじめに
パタヘネのRISC-V[1]版を買って一通り読んだらアセンブリ言語で組込のプログラミングがしたくなった。RISC-Vのマイコンボードが欲しかったのだが、安くていい感じのものが見付からなかった。代わりに秋月電子通商でArmのものがあった。RP2040マイコンボードキット[2]というものである。ウェブ上の情報も多く、データシート[3]もしっかりしていそうなので、とりあえずこれを買ってみた。
一般的にはSDK[4]をダウンロードしてあらかじめ用意されたライブラリを使って開発するようだが、これはビルドシステムとしてcmakeというのを使っている。これがOpenBSDでは何かエラーがでて動かなかった。僕はこういう便利ツールが嫌いだ。どうせ使わんからいいんやけど。関係ないけど途中から開発環境がLinuxに替わった。SDKには便利な関数がたくさん用意されているので楽である。ハードウェアの面倒な部分がプログラマから見えないようにしているからである。しかし今回はその面倒な部分に触れてみたくて買ったので、SDKを使うと意味がない。
ということでSDKなしで開発してみる。とりあえず定番のLチカをば。
ソースコード: git
動作環境
- Arch Linux 6.2.12-arch1-1
- arm-none-eabi-binutils 2.40-1
- GNU Make 4.4.1
- OpenBSD 7.3
- arm-none-eabi-binutils 2.31.1
- make (バージョン?)
make flash
は動かん。dmesg
でデバイス確認して手動でマウントする必要がある。
Boot Process
RP2040は電源を入れるといくつかの段階(ここでは関係ないので省略。データシート「2.8.1 Processor Controlled Boot Sequence」に詳しく書いてある)を踏んだあと、外部のフラッシュROMの先頭から256バイトを内部のSRAMにコピーして、フラッシュにプログラムが書き込まれているかどうか確認する。RP2040はフラッシュの先頭252バイトから計算したCRC32チェックサムを、直後の253バイト目から256バイトに記録することになっている。起動時にこのチェックサムを確認することで、フラッシュにプログラムが書き込まれているかどうか確かめている。コピーした最後の4バイトと起動時に最初の252バイトから計算したチェックサムが一致していれば、そのままコピーしてきた256バイトの先頭にPCをセットして実行を開始する。一致しなければUSBデバイスモードに切り替わり、パソコンに接続するとストレージとして認識される。このストレージにUF2という形式に変換したプログラムをコピーするとプログラムがフラッシュROMやSRAMに書き込まれる。
以上のことから、プログラムを実行するためにはCRC32を計算し、UF2という形式に変換することが必要である。ソースコードからの流れは以下の通り:
source bin bin with code ----------> object ------> elf --------> bin -------> with --------> crc32 in crc32 uf2 format assemble link objcopy bincrc bin2uf2
CRC(巡回冗長検査)
入力のデータをごにょごにょしてある値を出力する。
データ転送等に伴う偶発的な誤りの検査によく使われている[5]。
らしい。
入力のビットを一列に並べて、除数で「割り算」していく。この「割り算」が多項式の除算に似ているので、この除数をCRC多項式というらしい。ただし多項式の除算と違い、引き算するところをXORする。CRC32の場合、除数は33ビットである。33ビットで割ると32ビットの余りが残る。この余りがCRC32のチェックサムである。除数は色々あるようだが、標準的なものがWikipedia[5]に列挙されている。除数1011
を使ったCRC3の計算の手順は以下の通り:
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チェックサム
普通の割り算と基本は同じであるが、引き算の部分だけXORになっている。
以上の計算をプログラムの先頭252バイトに対して、33ビットの除数を用いて行う。データの並べ方は、上の例において左側を先頭に、フラッシュROM上の0番地から、各バイトは最上位ビットから順に並べる。入力のデータは253バイト目から256バイト目に0
をひっつけて計算する。これは多分予め長さが分からないデータでも計算できるようにしたかったからかな。除数は0x104c11db7
である(最上位ビットは常に1なのでデータシートでは省略されている)。
入力データは1バイトづつ処理したいみたいである。多分通信等で使う都合である。この時XORは結合則が成り立つので1バイト処理した結果と次のバイトとをXORして次の処理の入力として利用することができる:
111000111000000110000110111000111000001010010011111000111000000110010011 入力(適当)
|......|
111000110000000000000000000000000 先頭1バイト
100000100110000010001110110110111 除数
------------------------------------------------------------------------
011000010110000010001110110110111
100000100110000010001110110110111
-----------------------------------------------------------------------
010000001010000110010011011011001
100000100110000010001110110110111
----------------------------------------------------------------------
000000110010001110101000000000101
|......|
110010001110101000000000101000000 1バイト目の結果
|......|
10000001 入力の2バイト目
----------------------------------------------------------------
010010011110101000000000101000000 1バイト目の結果と2バイト目のXOR
100000100110000010001110110110111 除数
----------------------------------------------------------------
000100011011010010001111100110111
.
.
.
以上の操作は以下のようなアルゴリズムのループで実装できる。
- 前回の結果と、入力データの次のバイトをXOR
-
- 先頭の1ビットが1の場合、除数とXORを取り左シフト
- 先頭の1ビットが0の場合、そのまま左シフト
これをfor
ループで回す都合上、最初のバイトもXORを取る。上の例では最初は0x0
とXORを取っているが、この値を0x0
以外にすることもできる。そうした方がいろいろいいこともあるらしい。RP2040では0xffffffff
を使う。更にこの工程を32ビットのint
だけで行うことを考える:
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
.
.
.
これを実装したのが以下のコード:
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 < len; i++) {
b = idata[i] << 24;
c ^= b;
for (int j = 0; j < 8; j++) {
c = c >> 31 & 1 ? c << 1 ^ pol : c << 1;
}
}
return c;
}
main()
関数では上のcrc32()
に、idata
として入力となるバイナリデータの先頭を、len
として252
を渡してCRC32を計算させる。その後、出力先のファイルに入力元のデータをコピーしていき、253バイト目から256バイト目だけ、計算したCRC32に置き換える。入力元のこの場所にデータが書き込まれていないかどうかは確かめていない。
UF2(USB Flashing Format)
Microsoftが開発したフラッシュ書き込み用のファイル形式らしい:
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)[6].
このファイルに変換する上で必要な情報はGitHubのmicrosoft/uf2[6]に表として纏められている:
Offset Size Value 0 4 First magic number, 0x0A324655
("UF2\n"
)4 4 Second magic number, 0x9E5D5157
8 4 Flags 12 4 Address in flash where the data should be written 16 4 Number of bytes used in data (often 256) 20 4 Sequential block number; starts at 0 24 4 Total number of blocks in file 28 4 File size or board family ID or zero 32 476 Data, padded with zeros 508 4 Final magic number, 0x0AB16F30
RP2040のデータシート[3]「2.8.4.2 UF2 Format Details」を見ると、8バイト目のFlagsは、28バイト目にファミリーIDが書き込まれていることを示す0x00002000
、12バイト目は、書き込みを行うフラッシュROMの先頭アドレスである0x10000000
に、各ブロックの先頭からの位置を足したもの、16バイト目の、各ブロックのデータサイズは256バイト、28バイト目のファミリーIDは0xe48bff56
である。あとは表の通り3つのマジックナンバーをセットし、32バイト目以降にデータを書き込み、20バイト目と24バイト目にブロックの通し番号と総数をそれぞれ書き込めばいい。ブロックの通し番号はデータのついでに書き込めるが、総数はデータを全部さばいた後でないと分からないので、最後全てのブロックにまとめて書き込むようにした。できたのが以下のコード:
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
size_t
fwrite32l(uint32_t d, FILE *f)
{
int i;
uint8_t b;
for (i = 0; i < 32; i += 8) {
b = (uint8_t) (d >> i & 0xff);
fwrite(&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 < nblk; i++) {
if (i == 0)
if (fseek(dst, 24, SEEK_SET) < 0) {
fprintf(stderr, "Seek error: %s.\n argv[2]");
retnum = 1;
goto defer;
}
fwrite32l(nblk, dst);
if (i < nblk - 1)
if(fseek(dst, 512 - 4, SEEK_CUR) < 0){
fprintf(stderr, "Seek error: %s.\n argv[2]");
retnum = 1;
goto defer;
}
}
defer:
if (src)
fclose(src);
if (dst)
fclose(dst);
return retnum;
}
fwrite32l()
関数は指定されたファイルに32ビットの整数を下位バイトから順に書き込む関数である。バイトオーダーとかややこしそうなので作っておいたけど必要なのかな?あと名前が気に入らない。
CRC32のチェックサムが書き込まれたバイナリファイルを、このプログラムでUF2に変換し、生成されたファイルをUSBストレージとして接続したRP2040にコピーすればフラッシュROMに書き込まれる。
Flash Second Stage
RP2040に電源を投入し、CRC32のチェックが通った後、フラッシュROMからコピーされたプログラムの先頭から実行が開始される。このコピーされた部分で、その後の動作に必要な各種の設定を行うことになる。RP2040のデータシートには、フラッシュROMとSSIコントローラのXIPを設定するようにと書かれている。XIPはExecute in Placeの略で、フラッシュROMの内容をCPUから直接実行するものである。SSIはSynchronous Serial Interfaceの略で、周辺機器と情報のやりとりをする通信方式である。RP2040はチップに内蔵されたこのSSIコントローラを通して、外部のフラッシュROMと通信しているのだが、このコントローラを適切に設定すればフラッシュROMの内容がCPUから直接アクセスできる0x10000000
番地以降にマップされる。これによりフラッシュROMから内部のSRAMにデータをコピーすることなく命令を実行できるので、速くて便利だという。
しかしこのSSIコントローラはSynopsysという会社のDW_apb_ssiというIPを使っているようで、データシートのSSIコントローラの章は多分Synopsysの人が書いている。その他の章はRaspberry Pi財団の書いたブリティッシュイングリッシュだが、この部分だけ多分ネイティブじゃない人の書いたいい加減な英語である。誤植も多い。何日かかけて理解しようとしたがよく分からん。不毛なので一旦諦めた。
RP2040には内部にもROMがあり、はバージョン情報や電源を投入した時の動作、その他便利な関数が書き込まれている。この関数の中に外部のフラッシュROMとSSIコントローラを設定するものも含まれているので、今回はこれを利用した。ただしこの方法だとフラッシュROMとの通信方式がStandard SPIのままなので少し遅いらしい。詳しくはデータシートの「2.3.8. Bootrom Contents」を参照。
RP2040の内蔵ROMの0x00000018
番地に関数を検索するための関数がある。この関数に0x00000014
番地のrom_func_table
と、各関数に割り当てられた二文字の文字列を渡せば、欲しい関数へのポインタが返ってくる。なお、二文字の文字列はそれぞれASCIIコードで現し、二文字目を8ビットシフトしたものと1文字目のORを取ったものを渡すことになっている。今回欲しい関数はフラッシュROMをXIPに設定するもの(_flash_enter_cmd_xip()
)なので、'C', 'X'
を渡す。関数のポインタが返ってきて、それを呼び出せばフラッシュROMとSSIはXIPモードになる:
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
XIPの設定が完了すれば、次はメインのプログラムを実行するための準備である。エントリーポイントの指定、スタックポインタの初期値の設定、ベクターテーブルの設定である。Armのマニュアル[7]によると、初期スタックポインタとエントリーポイントはベクターテーブルの0x0
バイト目と0x4
バイト目に書くことになっている:
Table 7.3. Exception numbers Exception number Exception 1 Reset 2 NMI 3 HardFault 4-10 Reserved 11 SVCall 12-13 Reserved 14 PendSV 15 SysTick, optional 16 External Interrupt(0) ... ... 16 + N External Interrupt(N)
Table 7.4. Vector table format Word offset in table Description, for all pointer address values 0 SP_main. This is the reset value of the Main stack pointer. Exception Number Exception using that Exception Number
RP2040のベクターテーブルはM0PLUS: VTOR(0xe0000000 + 0xed08
)というレジスタに書き込むことで設定する。このとき、下位8ビットは0にしないといけないので、ベクターテーブルの位置は256バイトでアラインする必要がある。ベクターテーブルの定義はmain.s
に書き、boot2.s
からはラベルを使ってアクセスすることにする。以上をまとめると以下のコードになる:
ldr r0, =vectors
ldr r1, m0plus_vtor
str r0, [r1, #0] // vector table
ldr r1, [r0, #4] // entry point
ldr r0, [r0, #0] // stack pointer
mov sp, r0
bx r1
/* ... */
m0plus_vtor:
.word 0xe0000000 + 0xed08
なお以上のコードは.boot2
という名前のセクションにしてある。
メインのコード(main.s
)
ベクターテーブル
上で説明したように、ベクターテーブルのアドレスは256バイトの境界にないといけないが、boot2.s
をフラッシュの最初の256バイトに配置しており、main.s
はその直後から始まるようにリンクする。そのためメインのコードの最初にベクターテーブルを配置すればいい。ここでは割り込みの処理は考えないので、初期スタックポインタとエントリーポイントだけである。初期スタックポインタはSRAMの最後?(0x20040000
)、エントリーポイントはエントリーポイントのラベルを用いて設定した。また、別のファイル(boot2.s
)からアクセスしたいので、.global
宣言をつけておく:
.global vectors
vectors:
.word 0x20040000 // initial SP
.word (reset+1)
reset
ラベルに1
を足しているのはRP2040がThumbモードのみに対応しているからである。ArmのCPUはArmモードとThumbモードがあり、Armモードは32ビットの命令で高機能。Thumbモードは16ビットの命令(一部32ビット)でコンパクトである。どちらのモードでも命令は2の倍数のアドレスに並ぶことになる。そのためジャンブ命令のジャンプ先のアドレスの最下位ビットは常に0である。この最下位ビットはジャンプ先のモードを示す為に利用される。両方のモードに対応したCPUではジャンプ先のアドレスの最下位ビットが0ならArmモード、1ならThumbモードに切り替わる。ブランチ命令のオペランド等は多分アセンブラがいい感じにしてくれるので単にラベルを書けば動く。ベクターテーブルのこの部分は自分で足す必要があるみたい。あんまりちゃんと調べてないのでマニュアル読んでや。
GPIOの設定
電源投入直後、RP2040の周辺機器はリセット状態になっている。まずは今回利用するGPIOのリセット状態を解除する必要がある。データシートの「2.14. Subsystem Resets」には以下のように書かれている:
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.
リセット状態を解除するには、RESETS_BASE(0x4000c000
)から0x0
バイト目のRESETS: RESETレジスタのうち利用したい周辺機器のビットを0x0
にすればいい。
GPIOはIO Bank 0なので(これ明記されてなくない?)、RESETS: RESETレジスタのIO_BANK0(5番ビット)を0x0
にする。
レジスタのアトミックなクリア
RESETS: RESETレジスタのうち5番ビットだけを0x0
にしたい。この時、まずこのレジスタを読み込んでから~(1 << 5)
と論理積を取って同レジスタに書き戻してもいいのだが、RP2040にはこれを一回のstr
でしかもアトミックにできる機能が用意されている。今回の場合アトミックかどうかは関係ないと思うけど。
各レジスタには4個のアドレスが割り当てられている。データシートの各章のList of Registersに記載されているアドレスは通常の読み書きができる。そのアドレスに0x1000
を足したものにアクセスするとアトミックなXORが、0x2000
を足したものはアトミックなセットが、0x3000
を足したものはアトミックなクリアができる。つまりレジスタのアドレスに0x3000
を足したものに、0x1 << 5
をstr
すれば5番目のビットだけ0x0
にして、他のビットは変更されない。逆に指定したビットだけ立てて他を触らない場合は0x2000
を、あるいは指定したビットだけトグルしたい場合は0x1000
を足したアドレスにアクセスすればいい。
リセット状態の確認
リセットの解除はすぐに完了するわけではないようである。リセットの解除が完了したかどうか確認するにはRESETS: RESET_DONEレジスタ(RESETS_BASEから0x8
バイト目)の該当するビット(ここでは5番目のビット)を読む。この値が0x1
であればリセットの解除が完了している。0x0
であれば処理が進行中なので0x1
が返ってくるまで繰り返し読み込んで0x0
になるまで待機する。ところでこのレジスタはリセットの解除が完了したかどうか確かめるものなので、RESET_DONEという名前はどうなん?
以上から、GPIOのリセットを解除するのは以下のコード:
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
GPIOの機能の選択
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」にある。
GPIO25番の機能を選択するにはIO_BANK0_BASE(0x40014000
)から0xcc
番目のGPIO25_CTRLレジスタの下位5ビットに、該当する機能の番号を書き込めばいい。データシートの「2.19.2 Function Select」にある表を見ると、GPIO25番のSIOは5である:
// 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
GPIOの出力を有効化
GPIO25番がSIOになったので、次にこのピンからの出力を有効化する。既定値では出力は無効になっている。ハイインピーダンスってことなのかな?出力を有効にするには、SIO_BASE(0xd0000000
)から0x24
バイト目のSIO: GPIO_OEレジスタの該当するビット(25番のピンなので25番ビット)を0x1
にする:
// 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
LEDの点滅
以上でGPIOの設定は完了したので、あとは実際にLEDに電圧を掛けるだけである。レジスタのアドレスに0x1000
を足したものに書き込むとアトミックなレジスタのXORができると書いたが、SIOはこの機能がサポートされていないようである。データシートの「2.1.2 Atomic Register Access」に、
The SIO (Section 2.3.1), a single-cycle IO block attached directly to the cores' IO ports, does not support atomic accesses at the bus level, although some individual registers (e.g. GPIO) have set/clear/xor aliases.
と書かれている。そのかわりここにも書かれている通り、SIOの一部のレジスタにはアトミックなセット/クリア/XORをするためのレジスタが用意されている。ここではLEDを点滅させるためにGPIOの出力をトグルしたいのでXOR用のレジスタを使う。SIO_BASE(0xd0000000
)から0x1c
バイト目のSIO: GPIO_OUT_XORレジスタがそれである。このレジスタの25番ビットに0x1
を書き込めばいい。出力をトグルした後は少し間をおいて同じことを繰り返す。間をおくためにここでは適当な数値を1づつ減らしていって0になったら返る関数delay
を作った。タイマーと割り込みを使ったほうが消費電力等で優位なようだが、面倒なのでとりあえずこれで:
// 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
なお以上のコードは.text
セクションである。
リンカスクリプト
以上のコードには.boot2
、.text
の2つのセクションが含まれる。.boot2
はフラッシュの先頭から256(0x100
)バイト目まで、.text
はその後ろに続くように配置する:
MEMORY
{
FLASH(rx) : ORIGIN = 0x10000000, LENGTH = 2048k
}
SECTIONS
{
.boot2 : {
*(.boot2)
. = 0x100;
} > FLASH
.text : {
*(.text)
} > FLASH
}
Makefile
以上のソースコードは以下のように配置している:
rp2040 ├── ex1 │ ├── Makefile │ ├── boot2.s │ ├── main.s │ └── memmap.ld └── tools ├── Makefile ├── bin2uf2.c └── bincrc.c
toolsディレクトリのMakefileは同じディレクトリのソースファイルを$(CC)
でコンパイルするだけのものである(個人的な趣味でtcc
を使っている)。ex1ディレクトリのMakefileは以下の通り:
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 && make clean
.s.o:
$(AS) $(ASFLAGS) -o $@ $<
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 && make
RP2040のボードをUSBデバイスモードでLinuxのパソコンに接続し、ex1ディレクトリで
$ make
# make flash
とすればプログラムがRP2040のボードに書き込まれて実行が開始される。
最後に
光あれ。
参考
- [1] Hennesy, J. L. and Patterson, D. A. 2017. Computer Organization And Design RISC-V Edition.
- [2] RP2040マイコンボードキット.秋月電子通商
- [3] RP2040 Datasheet.Raspberry Pi Foundation
- [4] pico-sdk.github
- [5] 巡回冗長検査.Wikipedia
- [6] USB Flashing Format (UF2).GitHub
- [7] ARMv6-M Architecture Reference Manual