主頁 | 自己紹介 | 日記 | 農業 | 台所 | 電算機 | | 本棚 | | Git

RP2040 SDKなしでLチカ

: 作成
: ベクターテーブルの修正

はじめに

パタヘネのRISC-V[1]版を買って一通り読んだらアセンブリ言語で組込のプログラミングがしたくなった。RISC-Vのマイコンボードが欲しかったのだが、安くていい感じのものが見付からなかった。代わりに秋月電子通商でArmのものがあった。RP2040マイコンボードキット[2]というものである。ウェブ上の情報も多く、データシート[3]もしっかりしていそうなので、とりあえずこれを買ってみた。

一般的にはSDK[4]をダウンロードしてあらかじめ用意されたライブラリを使って開発するようだが、これはビルドシステムとしてcmakeというのを使っている。これがOpenBSDでは何かエラーがでて動かなかった。僕はこういう便利ツールが嫌いだ。どうせ使わんからいいんやけど。関係ないけど途中から開発環境がLinuxに替わった。SDKには便利な関数がたくさん用意されているので楽である。ハードウェアの面倒な部分がプログラマから見えないようにしているからである。しかし今回はその面倒な部分に触れてみたくて買ったので、SDKを使うと意味がない。

ということでSDKなしで開発してみる。とりあえず定番のLチカをば。

ソースコード: git

動作環境

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
	 .
	 .
	 .

以上の操作は以下のようなアルゴリズムのループで実装できる。

これを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]に表として纏められている:

OffsetSizeValue
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 numberException
1Reset
2NMI
3HardFault
4-10Reserved
11SVCall
12-13Reserved
14PendSV
15SysTick, optional
16External Interrupt(0)
......
16 + NExternal Interrupt(N)
Table 7.4. Vector table format
Word offset in tableDescription, for all pointer address values
0SP_main. This is the reset value of the Main stack pointer.
Exception NumberException 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 << 5strすれば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のボードに書き込まれて実行が開始される。

最後に

光あれ。

参考