Raspberry Pi Picoでアセンブラ

Raspberry Pi Picoでアセンブラを最低限触れる環境を作って簡単な関数をアセンブラで作ってみます。環境はLinuxMint20ですが、Ubuntuでもだいたい同じでしょう。

1.とにかくRaspberry Pi Picoを動かしてみる。

いきなり接続してみます。Pico上のスイッチを押しながら、Raspberry Pi PicoをPCに接続します。

すると、USBストレージに「RPI-RP2」というデバイスが現れて、INDEX.HTMというファイルと、INFO_UF2.TXTというファイルが見えます。

後者の内容は、

UF2 Bootloader v1.0
Model: Raspberry Pi RP2
Board-ID: RPI-RP2

でボードの仕様が記述されています。前者はダブルクリックするとドキュメントのページでリダイレクトされます。

この中の、

をクリックします。環境セットアップについて記載してあるページに飛ぶのですが、この中の

のDownload UF2 Fileを右クリックして、blink.uf2をダウンロードします。

ダウンロードしたら、これを先のINDEX.HTMがあったフォルダにドラッグ&ドロップすると、フォルダが閉じて、LEDが点滅を始めます。(もう一度行う場合は、一度Raspberry Pi Picoを抜いて、スイッチを押しながら再度接続します)

2.環境の構築

今度こそ環境の構築をします。先の

をクリックした際に表示されたページに戻ります。

setup scriptを右クリックしてホームディレクトリに保存します。保存したら、スクリプトを実行します。

$ . pico_setup.sh

で実行します。「Not running on a Raspberry Pi. Use at your own risk!」という警告が出て、sudo実行のためにパスワードを聞いてきますので入力すると、クロスコンパイラなど不足しているパッケージをインストールして、OpenOCDをビルドしたり、VScodeもインストールします。いろいろとインストールしますが、ターミナルウインドウは勝手に閉じてしまいますので、細部で何をしているかはスクリプトを読むしかありません。

ちなみに、SDKをアップデートする際には、こちらの2.3. Updating the SDKによると、

$ cd pico-sdk
$ git pull
$ git submodule update

とするようです。

3.Lチカソースのコピーとビルド

適当なフォルダで

$ git clone -b master https://github.com/raspberrypi/pico-examples.git

としてサンプルソースをダウンロードします。

.../RaspiPico$ cd pico-examples
.../pico-examples$ mkdir build
.../pico-examples$ cd build
.../pico-examples/build$ export PICO_SDK_PATH=~/pico/pico-sdk
.../pico-examples/build$ cmake ..
Using PICO_SDK_PATH from environment ('/home/user/pico/pico-sdk')
PICO_SDK_PATH is /home/user/pico/pico-sdk
Defaulting PICO_PLATFORM to rp2040 since not specified.
Defaulting PICO platform compiler to pico_arm_gcc since not specified.
-- Defaulting build type to 'Release' since not specified.
PICO compiler is pico_arm_gcc
-- The C compiler identification is GNU 9.2.1
-- The CXX compiler identification is GNU 9.2.1
-- The ASM compiler identification is GNU
-- Found assembler: /usr/bin/arm-none-eabi-gcc
Defaulting PICO target board to pico since not specified.
Using board configuration from /home/user/pico/pico-sdk/src/boards/include/boards/pico.h
-- Found Python3: /usr/bin/python3.8 (found version "3.8.10") found components: Interpreter 
TinyUSB available at /home/user/pico/pico-sdk/lib/tinyusb/src/portable/raspberrypi/rp2040; enabling build support for USB.
-- Configuring done
-- Generating done
-- Build files have been written to: .../pico-examples/build
.../pico-examples/build$ 

として、ビルド環境を生成します。

/pico-examples/build$ cd blink/
/pico-examples/build/blink$ make -j

としてビルドすると、同じフォルダに blink.uf2 が生成されるので、これを先ほどと同様にドラッグ&ドロップするとLEDが点滅します。

4.アセンブラでいじってみる環境を構築

サンプルディレクトリのblink.cをベースに参考にアセンブラでいじってみる環境を構築してみます。

ディレクトリ構成は以下のようにします。

pico-blink
├── CMakeLists.txt        ...①
├── build        ...(空のディレクトリ ②)
├── example_auto_set_url.cmake        ...③
├── pico_sdk_import.cmake        ...④
└── src        ...(ディレクトリ)
    ├── CMakeLists.txt        ...⑤
    └── blink.c        ...⑥

①は pico-examples/CMakeLists.txt をベースに不要な部分を削って以下の内容にします。

cmake_minimum_required(VERSION 3.12)

# Pull in SDK (must be before project)
include(pico_sdk_import.cmake)

project(pico_examples C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

if (PICO_SDK_VERSION_STRING VERSION_LESS "1.3.0")
    message(FATAL_ERROR "Raspberry Pi Pico SDK version 1.3.0 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}")
endif()

set(PICO_EXAMPLES_PATH ${PROJECT_SOURCE_DIR})

# Initialize the SDK
pico_sdk_init()

include(example_auto_set_url.cmake)
# Add src dir
add_subdirectory(src)

add_compile_options(-Wall
        -Wno-format          # int != int32_t as far as the compiler is concerned because gcc has int32_t as long int
        -Wno-unused-function # we have some for the docs that aren't called
        -Wno-maybe-uninitialized
        )

③④は pico-examples にある同名のファイルをコピーしてきます。
⑤⑥は pico-examples/blink にある同名のファイルをコピーしてきます。
これで、②の pico-blink/build ディレクトリで、

.../pico-blink/build$ export PICO_SDK_PATH=~/pico/pico-sdk
.../pico-blink/build$ cmake ..

とすると、Makefileが生成されるので、

.../pico-blink/build$ make -j

で…/pico-blink/build/srcの下にblink.uf2が生成されます。…/pico-blink/src/blink.c のsleep_ms()のパラメータを変えて、 make -j してから blink.uf2 をドラッグ&ドロップすると、周期が変わることがわかります。

5.アセンブラのコードを追加してみる

…/pico-blink/src の下にアセンブラのソースを asm.s として追加します。まずは何もせずに無限ループに陥るコードです。

.global loop

loop:
    b loop

blink.cの方は以下のハイライト行の部分、無限ループをするloop()の宣言と呼び出しの追加をします。(コメントや必要ないプリプロセッサの処理も削除してあります)

#include "pico/stdlib.h"

extern void loop();

int main() {
    const uint LED_PIN = PICO_DEFAULT_LED_PIN;
    gpio_init(LED_PIN);
    gpio_set_dir(LED_PIN, GPIO_OUT);
    while (true) {
        gpio_put(LED_PIN, 1);
        sleep_ms(500);
        loop();
        gpio_put(LED_PIN, 0);
        sleep_ms(500);
    }
}

同じフォルダのCMakelist.txtも修正して、asm.sがソースコードに追加されたことを記述します。

add_executable(blink
        blink.c
        asm.s
        )

# pull in common dependencies
target_link_libraries(blink pico_stdlib)

# create map/bin/hex file etc.
pico_add_extra_outputs(blink)

# add url via pico_set_program_url
example_auto_set_url(blink)

これで、

.../pico-blink/build$ cmake ..
.../pico-blink/build$ make -j

として生成された blink.uf2 を実行すると、LEDが点灯したままになり、無限ループに入っていることがわかります。

6.アセンブラでLEDをON/OFFしてみる。

…/pico-blink/build/src の下の blink.dis を見ると、この部分のアセンブラコードが見えます。

1000035c <main>:
1000035c:	b570      	push	{r4, r5, r6, lr}
1000035e:	25d0      	movs	r5, #208	; 0xd0
10000360:	2480      	movs	r4, #128	; 0x80
10000362:	2019      	movs	r0, #25
10000364:	062d      	lsls	r5, r5, #24
10000366:	04a4      	lsls	r4, r4, #18
10000368:	f000 f810 	bl	1000038c <gpio_init>
1000036c:	626c      	str	r4, [r5, #36]	; 0x24
1000036e:	20fa      	movs	r0, #250	; 0xfa
10000370:	616c      	str	r4, [r5, #20]
10000372:	0040      	lsls	r0, r0, #1
10000374:	f000 fb24 	bl	100009c0 <sleep_ms>
10000378:	f000 f806 	bl	10000388 <loop>
1000037c:	20fa      	movs	r0, #250	; 0xfa
1000037e:	61ac      	str	r4, [r5, #24]
10000380:	0040      	lsls	r0, r0, #1
10000382:	f000 fb1d 	bl	100009c0 <sleep_ms>
10000386:	e7f2      	b.n	1000036e <main+0x12>

10000388 <loop>:
10000388:	e7fe      	b.n	10000388 <loop>
	...

C言語のソースコードと比較して、RP2040のデータシートのP.46辺りとCortex M0+のインストラクションセットをみながら整理すると、

1000035e:	25d0      	movs	r5, #208	; 0xd0
10000364:	062d      	lsls	r5, r5, #24
  この2行でr5レジスタに 0xd0000000 を代入

10000360:	2480      	movs	r4, #128	; 0x80
10000366:	04a4      	lsls	r4, r4, #18
  この2行でr4レジスタに 0x02000000 を代入(0x02000000 = bit25が1)

10000362:	2019      	movs	r0, #25
10000368:	f000 f810 	bl	1000038c <gpio_init>
  この2行がgpio_init(LED_PIN)の呼び出しに相当

1000036c:	626c      	str	r4, [r5, #36]	; 0x24
  0xd0000024番地(GPIO_OE_SETレジスタ)に0x02000000を代入(書き込み)
  値が1のビットのみ意味を持つ。
  つまりGPIO25の出力をenableするgpio_set_dir(LED_PIN, GPIO_OUT)に相当

10000370:	616c      	str	r4, [r5, #20]   ; 0x14
  0xd0000014番地(GPIO_OUT_SETレジスタ)に0x02000000を代入(書き込み)
  値が1のビットのみ意味を持つ。つまりGPIO25の出力をHにする。  

1000037e:	61ac      	str	r4, [r5, #24]   ; 0x18
  0xd0000014番地(GPIO_OUT_CLRレジスタ)に0x02000000を代入(書き込み)
  値が1のビットのみ意味を持つ。つまりGPIO25の出力をLにする。  

1000037c:	20fa      	movs	r0, #250	; 0xfa
10000382:	f000 fb1d 	bl	100009c0 <sleep_ms>
  この2行でsleep_ms(250)に相当する。

です。データシートを見ていると、GPIO_OUT_XOR Registerという指定のビットの出力を反転させるレジスタ(オフセット 0x1c)があることがわかります。

そこで、GPIO25の出力を反転させる関数をアセンブラで記述してみます。

まず、asm.sを以下のように修正します。

.global reverse

reverse:
    cpsid   if
    push	{r4, r5, lr}
    movs	r5, #208	    ;# 0xd0
    lsl	    r5, r5, #24
    movs	r4, #128	    ;# 0x80
    lsl	    r4, r4, #18
    str     r4, [r5, #28]	;# 0x1c
    cpsie   if
    pop     {r4, r5, pc}

次に、blink.cを以下のように修正します。

#include "pico/stdlib.h"

extern void reverse();

int main() {
    const uint LED_PIN = PICO_DEFAULT_LED_PIN;
    gpio_init(LED_PIN);
    gpio_set_dir(LED_PIN, GPIO_OUT);
    while (true) {
        reverse();
        sleep_ms(250);
    }
}

これで make して、blink.uf2 を書き込むとLEDがやはり点滅しました。

7.アセンブラ部分の中身について

アセンブラ部分の

.global reverse

reverse:
    cpsid   if
    push	{r4, r5, lr}
    movs	r5, #208	    ;# 0xd0
    lsl	    r5, r5, #24
    movs	r4, #128	    ;# 0x80
    lsl	    r4, r4, #18
    str     r4, [r5, #28]	;# 0x1c
    cpsie   if
    pop     {r4, r5, pc}

についてです。

cpsid ifとcpsie ifは割り込みの禁止と許可です。まあ、この関数ではハードウェアを叩くと言っても、GPIO_OUT_XOR Registerという一回の処理で所定のビットを反転するハードウェアがあるので、割り込み禁止は不要です。

push/popはr4,r5,lrレジスタの退避と復帰です。対象レジスタを列記していますが、lrは戻す時はpcに戻してリターンを兼ねています。ここではr0,r1を使えばpushは不要になって、popのところは bx lr で行けるはずです。

ですので、asm.s は

.global reverse

reverse:
    movs	r0, #208	    ;# 0xd0
    lsl	    r0, r0, #24
    movs	r1, #128	    ;# 0x80
    lsl	    r1, r1, #18
    str     r1, [r0, #28]	;# 0x1c
    bx      lr

としても大丈夫です。(実際に動作しました)

8.おまけ

ARMアーキテクチャにおけるABIgithubにありました。この中のABI for the Arm 32-bit ArchitectureのProcedure Call Standard for the Arm Architectureによれば、レジスタの割付は

ということで、r0-r3を引数および戻り値渡しとスクラッチレジスタに使用できるようです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)