みつきんのメモ

組み込みエンジニアです。Interface誌で「Yocto Projectではじめる 組み込みLinux開発入門」連載中

Pico SDKのリンカスクリプトを眺める

はじめに

リンカスクリプト初心者がPico SDKリンカスクリプトを眺める。今回眺めるのはmemmap_default.ld

大きく分けて下記のようなブロックに別れている

  • MEMORY
  • ENTRY
  • SECTIONS

elfにもリンカスクリプトの詳しくないので間違っている情報があるはず。

MEMORY

メモリ領域のアドレスとサイズ定義している。

MEMORY
{
    FLASH(rx) : ORIGIN = 0x10000000, LENGTH = 2048k
    RAM(rwx) : ORIGIN =  0x20000000, LENGTH = 256k
    SCRATCH_X(rwx) : ORIGIN = 0x20040000, LENGTH = 4k
    SCRATCH_Y(rwx) : ORIGIN = 0x20041000, LENGTH = 4k
}

このあたりはハードウェアに強く依存する。

FLASH-ROMの領域を0x10000000から2048KiB、RAMの領域を0x20000000から256KiBに割り当てている。

SCRATCH_XとSCRATCH_Yがよくわからない。

RP2040 Datasheetによると、RAMは全部で264KiB搭載されていて、6個の独立したバンク(SRAM0-5)で構成されている。

そのうちSRAM0-3の4つ分を1つの領域として使って、残りの2つの領域(SRAM4,5)をそれぞれSCRATCH_X、SCATCH_Yとして割り当てている。ということらしい。

f:id:mickey_happygolucky:20210221170643p:plain
メモリのアドレスマップ

ENTRY

elfのエントリポイントとして_entry_pointを登録している。

SECTIONS

セクション情報を定義している。

.flash_begin

FLASHの先頭の位置を保存している。

    .flash_begin : {
        __flash_binary_start = .;
    } > FLASH

.boot2

.boot2セクションがFLASHの先頭に配置されるようになっている。

    .boot2 : {
        __boot2_start__ = .;
        KEEP (*(.boot2))
        __boot2_end__ = .;
    } > FLASH

    ASSERT(__boot2_end__ - __boot2_start__ == 256,
        "ERROR: Pico second stage bootloader must be 256 bytes in size")

boot2のサイズがチェックサムを含めて256バイトである必要があるので、__boot2_end____boot2_start__の差分でサイズをチェックしている。

.text

プログラム本体が配置される部分。

    .text : {
        __logical_binary_start = .;
        KEEP (*(.vectors))
        KEEP (*(.binary_info_header))
        __binary_info_header_end = .;
        KEEP (*(.reset))
        /* TODO revisit this now memset/memcpy/float in ROM */
        /* bit of a hack right now to exclude all floating point and time critical (e.g. memset, memcpy) code from
         * FLASH ... we will include any thing excluded here in .data below by default */
        *(.init)
        *(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a:) .text*)
        *(.fini)
        /* Pull all c'tors into .text */
        *crtbegin.o(.ctors)
        *crtbegin?.o(.ctors)
        *(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors)
        *(SORT(.ctors.*))
        *(.ctors)
        /* Followed by destructors */
        *crtbegin.o(.dtors)
        *crtbegin?.o(.dtors)
        *(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors)
        *(SORT(.dtors.*))
        *(.dtors)

        *(.eh_frame*)
        . = ALIGN(4);
    } > FLASH

.vectorsベクターテーブルをFLASH上のboot2の直後(0x10000100)に配置する。

.binary_info_headerは後述の.binary_infoを探すための情報のようだ。

残りの部分は一般的なelfのセクションで、各セクションの詳細はここが詳しい。

セクション 概要
.init ELFのコード初期化
.text プログラムの本体
.fini ELFのコード後処理
.ctor コンストラク
.dtor デストラク
.eh_frame バックトレース取得用

.init/.finiと.ctor/.dtorについてはこの記事が詳しい。

.rodata

読み取り専用データが配置される。

    .rodata : {
        *(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a:) .rodata*)
        . = ALIGN(4);
        *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.flashdata*)))
        . = ALIGN(4);
    } > FLASH

特定のライブラリに含まれるセクションは除外しているようだ。また、アライメントや順序に配慮して配置しているようだ。

.ARM.extab、.ARM.exidx

newlibを使用する時に必要になるセクションらしい。ここが詳しい。

    .ARM.extab :
    {
        *(.ARM.extab* .gnu.linkonce.armextab.*)
    } > FLASH

    __exidx_start = .;
    .ARM.exidx :
    {
        *(.ARM.exidx* .gnu.linkonce.armexidx.*)
    } > FLASH
    __exidx_end = .;

例外テーブルと例外インデックスらしい。

.binary_info

.binary_infoはpicoが独自で持っている情報のようだ。

/**
 * Binary info is intended for embedding machine readable information with the binary in FLASH.
 *
 * Example uses include:
 *
 * - Program identification / information
 * - Pin layouts
 * - Included features
 * - Identifying flash regions used as block devices/storage
 */

おそらくpicotool infoで読み取るための情報が入ると思われる。

    /* Machine inspectable binary information */
    . = ALIGN(4);
    __binary_info_start = .;
    .binary_info :
    {
        KEEP(*(.binary_info.keep.*))
        *(.binary_info.*)
    } > FLASH
    __binary_info_end = .;
    . = ALIGN(4);

    /* End of .text-like segments */
    __etext = .;

ここまででtext(実行可能コード)領域が終了。

.ram_vector_table

vectorテーブルをRAM上に構築するするための領域。

   .ram_vector_table (COPY): {
        *(.ram_vector_table)
    } > RAM

.data

初期化済みのデータが配置される。

    .data : {
        __data_start__ = .;
        *(vtable)

        *(.time_critical*)

        /* remaining .text and .rodata; i.e. stuff we exclude above because we want it in RAM */
        *(.text*)
        . = ALIGN(4);
        *(.rodata*)
        . = ALIGN(4);

        *(.data*)

        . = ALIGN(4);
        *(.after_data.*)
        . = ALIGN(4);
        /* preinit data */
        PROVIDE_HIDDEN (__mutex_array_start = .);
        KEEP(*(SORT(.mutex_array.*)))
        KEEP(*(.mutex_array))
        PROVIDE_HIDDEN (__mutex_array_end = .);

        . = ALIGN(4);
        /* preinit data */
        PROVIDE_HIDDEN (__preinit_array_start = .);
        KEEP(*(SORT(.preinit_array.*)))
        KEEP(*(.preinit_array))
        PROVIDE_HIDDEN (__preinit_array_end = .);

        . = ALIGN(4);
        /* init data */
        PROVIDE_HIDDEN (__init_array_start = .);
        KEEP(*(SORT(.init_array.*)))
        KEEP(*(.init_array))
        PROVIDE_HIDDEN (__init_array_end = .);

        . = ALIGN(4);
        /* finit data */
        PROVIDE_HIDDEN (__fini_array_start = .);
        *(SORT(.fini_array.*))
        *(.fini_array)
        PROVIDE_HIDDEN (__fini_array_end = .);

        *(.jcr)
        . = ALIGN(4);
        /* All data end */
        __data_end__ = .;
    } > RAM AT> FLASH

.dataは初期化済みデータなので、初期値を含んだデータの内容をプログラムのバイナリ、ここではFLASHに格納する必要があるが、書き換え可能である必要があるため実行時にはRAMに配置される必要がある。そのような場合にはRAM AT> FLASHのように指定する。

このあたりの内容はここが詳しい。

.uninitialized_data

    .uninitialized_data (COPY): {
        . = ALIGN(4);
        *(.uninitialized_data*)
    } > RAM

.scratch_x、.scratch_y

おそらく実行時SCRATCH_X、SCRATCH_Yに配置されるデータの初期値を格納する領域。

    /* Start and end symbols must be word-aligned */
    .scratch_x : {
        __scratch_x_start__ = .;
        *(.scratch_x.*)
        . = ALIGN(4);
        __scratch_x_end__ = .;
    } > SCRATCH_X AT > FLASH
    __scratch_x_source__ = LOADADDR(.scratch_x);

    .scratch_y : {
        __scratch_y_start__ = .;
        *(.scratch_y.*)
        . = ALIGN(4);
        __scratch_y_end__ = .;
    } > SCRATCH_Y AT > FLASH
    __scratch_y_source__ = LOADADDR(.scratch_y);

scratch_x、scratch_yマクロを使用してデータを定義するとこれらのセクションに配置されるようになっている。

#define __scratch_x(group) __attribute__((section(".scratch_x." group)))
#define __scratch_y(group) __attribute__((section(".scratch_y." group)))

.bss、.heap

初期値を指定しない、スタック以外のデータを配置する。

    .bss  : {
        . = ALIGN(4);
        __bss_start__ = .;
        *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.bss*)))
        *(COMMON)
        . = ALIGN(4);
        __bss_end__ = .;
    } > RAM

    .heap (COPY):
    {
        __end__ = .;
        end = __end__;
        *(.heap*)
        __HeapLimit = .;
    } > RAM

Stack領域の計算

リンカスクリプトののこりの部分はスタックを配置する領域を計算する。

    /* .stack*_dummy section doesn't contains any symbols. It is only
     * used for linker to calculate size of stack sections, and assign
     * values to stack symbols later
     *
     * stack1 section may be empty/missing if platform_launch_core1 is not used */

    /* by default we put core 0 stack at the end of scratch Y, so that if core 1
     * stack is not used then all of SCRATCH_X is free.
     */
    .stack1_dummy (COPY):
    {
        *(.stack1*)
    } > SCRATCH_X
    .stack_dummy (COPY):
    {
        *(.stack*)
    } > SCRATCH_Y

    .flash_end : {
        __flash_binary_end = .;
    } > FLASH

    /* stack limit is poorly named, but historically is maximum heap ptr */
    __StackLimit = ORIGIN(RAM) + LENGTH(RAM);
    __StackOneTop = ORIGIN(SCRATCH_X) + LENGTH(SCRATCH_X);
    __StackTop = ORIGIN(SCRATCH_Y) + LENGTH(SCRATCH_Y);
    __StackOneBottom = __StackOneTop - SIZEOF(.stack1_dummy);
    __StackBottom = __StackTop - SIZEOF(.stack_dummy);
    PROVIDE(__stack = __StackTop);

    /* Check if data + heap + stack exceeds RAM limit */
    ASSERT(__StackLimit >= __HeapLimit, "region RAM overflowed")

    ASSERT( __binary_info_header_end - __logical_binary_start <= 256, "Binary info must be in first 256 bytes of the binary")
    /* todo assert on extra code */

基本的には、コア0のスタックをSCRATCH_Yに、コア1のスタックをSCRATCH_Xに配置するなっているようだ。

コア0のスタックを後ろに配置することで、コア1が動かない場合にその分の領域を使用できるようにするという意図があるらしい。

最後の2つのASSERTは下記のチェックを行っている。

  1. data + heap + stackがRAMのサイズより大きいとエラーになる
  2. Binary info(header)がboot2を除いたバイナリの先頭の256バイトに配置される必要がある

まとめ

boot2やbinary_infoなどのRP2040/Pico特有の制限に関してリンカスクリプトのASSERTでチェックを行っている。

.textや.dataなどに関しては至ってまともなリンカスクリプトになっているようだ。

scratch_x、scratch_yに関しては、コアごとに分ける必要があるデータ領域はこちらに割り振るようになっているようだ。

リンカスクリプトやelfに関しては初心者なので理解が間違っているところがある可能性は高い。

その場合はコメントやツイッターで私のメンタルがやられない程度にツッコミを入れてくれるとありがたい。

参考

*1:constructor

PicoでベアメタルのLチカ(RAMバージョン) その1

はじめに

picoprobeでRAMに直接プログラムがロードできるようになったので、難しいboot2のことはおいておいて、 ベアメタルでLチカを試みる。

ホントは、コンパイル済みのboot2を組み込んでLチカしようと頑張ったが、現時点では挫折した。

心の師匠の成果物の丸パクリにならないように頑張ったが初期化コードの一部をまるっと拝借した。

あと、SysTickの設定なども手が回らなかったのでチカチカのタイミングは適当。

ディレクトリ構成

下記のようにフラットな構造とした。

├── Makefile
├── main.c
├── memmap_ram.ld
├── pico.gdbinit
├── regs.h
└── start.S

リンカスクリプト

memmap_ram.ldを下記の内容で作成する。

MEMORY
{
      RAM(rwx) : ORIGIN =  0x20000000, LENGTH = 256k
}

ENTRY(reset)

SECTIONS
{
    .text : {
        *(.text*)
    } > RAM
}

単純にtextセクションをRAM上に配置する。

エントリポイントにresetを設定している。

スタートアップ

start.Sを下記の内容で作成する。

 .cpu cortex-m0plus
    .thumb

    .global reset
.thumb_func
reset:
    ldr r0, =0x20001000
    mov sp, r0
    bl main
    b hang
    
.thumb_func
hang:    b .

デバッガで実行すると、ENTRYから開始されるのでresetの先頭から実行される。

resetではスタックポインタを初期化してmain関数を呼び出している。

レジスタ定義

regs.hを下記の内容で作成する。

#ifndef REGS_H
#define REGS_H

#define OP_RW  (0x0000)
#define OP_XOR (0x1000)
#define OP_SET (0x2000)
#define OP_CLR (0x3000)

#define RESETS_BASE                 (0x4000C000)

#define RESETS_RESET             (RESETS_BASE+0x0)
#define RESETS_RESET_DONE        (RESETS_BASE+0x8)

#define SIO_BASE (0xD0000000)

#define SIO_GPIO_OUT_SET (SIO_BASE+0x14)
#define SIO_GPIO_OUT_CLR (SIO_BASE+0x18)
#define SIO_GPIO_OUT_XOR (SIO_BASE+0x1C)
#define SIO_GPIO_OE_SET (SIO_BASE+0x24)
#define SIO_GPIO_OE_CLR (SIO_BASE+0x28)

#define PADS_BANK0_BASE (0x4001C000)
#define PADS_BANK0_GPIO (0x68)

#define PADS_GPIO25 (PADS_BANK0_BASE + PADS_BANK0_GPIO)

#define IO_BANK0_BASE (0x40014000)
#define IO_BANK_GPIO25_CTRL (0xCC)

#define IO_GPIO25_CTRL (IO_BANK0_BASE + IO_BANK_GPIO25_CTRL)


#endif //REGS_H

Lチカに必要なレジスタの定義をregs.hに切り出している。

メインプログラム

main.cを下記の内容で作成する。

#include "regs.h"

#include <stdint.h>

uint32_t read_reg(uint32_t addr) {
        return (*((volatile uint32_t *)addr));
}

void write_reg(uint32_t addr, uint32_t value) {
        *((volatile uint32_t *)addr) = value;
}

void write_reg_op(uint32_t addr, uint32_t value, uint32_t op) {
        write_reg(addr | op, value);
}


int main(void) {

        /////////////////////
        // This initialise code is ported from https://github.com/dwelch67/raspberrypi-pico/blob/master/blinker00/notmain.c
        // release reset on IO_BANK0
        write_reg_op(RESETS_RESET, 1<<5, OP_CLR); //IO_BANK0
        //wait for reset to be done
        while(1) {
                if((read_reg(RESETS_RESET_DONE)&(1<<5))!=0) break;
        }
        write_reg_op(RESETS_RESET, (1<<8), OP_CLR); //PADS_BANK0
        while(1) {
                if((read_reg(RESETS_RESET_DONE)&(1<<8))!=0) break;
        }
        /////////////////////

        // GPIO init
        write_reg(SIO_GPIO_OE_CLR, (1ul<<25));
        write_reg(SIO_GPIO_OUT_CLR, (1ul<<25));
        uint32_t ra = read_reg(PADS_GPIO25);
        write_reg_op(PADS_GPIO25, (ra^0x40)&0xC0, OP_XOR);

        write_reg(IO_GPIO25_CTRL, 0x5);
        uint32_t v = read_reg(IO_GPIO25_CTRL);

        // Blink
        write_reg(SIO_GPIO_OE_SET, (1ul<<25));
        while (1) {
                for (int i = 100000; i != 0; i--) ;
                write_reg(SIO_GPIO_OUT_XOR, (1ul<<25));
        }
        return 0;
}

基本的にはpico-examplesのblinkの処理を分解して必要な処理だけにしたが、 試行錯誤の結果、初期化コードだけはdwelch67先生の成果物を丸パクリ参考にした。

Makefile

Makefileを下記の内容で作成する。

CROSS_COMPILE ?= arm-none-eabi-

MCPU = -mcpu=cortex-m0plus

ASMOPT = $(MCPU) -g
COPT = $(MCPU) -ffreestanding -g -O0
LOPT = -nostdlib -nostartfiles

all: ram

clean:
    rm -f *.o
    rm -f *.elf
    rm -f *.list
    rm -f *~

start.o: start.S
    $(CROSS_COMPILE)as $(ASMOPT) start.S -o start.o

main.o: main.c
    $(CROSS_COMPILE)gcc $(COPT) -fpic -mthumb -c main.c -o main.o

ram: start.o main.o
    $(CROSS_COMPILE)ld $(LOPT) start.o main.o -T memmap_ram.ld  -o led.elf 

gdb初期化スクリプト

pico.gdbinitを下記の内容で作成する。

target remote :3333
mon reset init
load

ビルドと実行

ビルド

makeでビルドできる。

$ make

実行

まずopenocdを起動する。

$ cd ${PICO_SDK_PATH}/../openocd
$ src/openocd -f interface/picoprobe.cfg -f target/rp2040.cfg -s tcl

別のターミナルでgdb-multiarchを起動する。

$ gdb-multiarch -x pico.gdbinit led.elf
... (snip) ...
Type "apropos word" to search for commands related to "word"...
Reading symbols from led.elf...
0x20000116 in main () at main.c:83
83                  for (int i = 100000; i != 0; i--) ;
target halted due to debug-request, current mode: Thread 
xPSR: 0xf1000000 pc: 0x000000ee msp: 0x20041f00
target halted due to debug-request, current mode: Thread 
xPSR: 0xf1000000 pc: 0x000000ee msp: 0x20041f00
Loading section .text, size 0x158 lma 0x20000000
Start address 0x20000000, load size 344
Transfer rate: 2752 bits in <1 sec, 344 bytes/write.
(gdb) 

これでPicoにプログラムがロードされるので、下記で実行する。

(gdb) c
Continuing.

LEDがチカチカする。

f:id:mickey_happygolucky:20210221025047g:plain
Lチカ

まとめ

boot2を使わない、RAMで直接するLチカを試した。

SysTickやタイマーを使っていないためタイミングは適当。 Pico SDKで作る場合と違い、ボードの初期化処理は自分でしないといけない。

コアは1個しか動いていないはず。(何もしてないので。)

Ubuntu 20.04のPCでRaspberry Pi Picoを2台使ってOpenOCDによるデバッグ

はじめに

Raspberry Pi Pico(Pico)はRaspberry Pi4を使用して、SWDでデバッグすることが推奨されているようだが、 Picoが2台ある場合、そのうち1台にpicoprobeというプログラムを書き込むことで、デバッガとして使用することができる。

下記のようにPico SDKには必要なものが一式揃っている。

  1. picoprobeのソースコード
  2. picoprobe対応のOpenOCD

実のところ以前やったようにPC上でもpico_setup.shで環境を構築すると必要なものは全て揃った状態になる。

Getting started with Raspberry Pi Picoを参考に作業する。

作業環境はUbuntu 20.04のPC。

HWの準備

Figure.34の図に従って接続する。

f:id:mickey_happygolucky:20210218234957p:plain
2台のPicoの接続

Debugger側 Debuggee側 備考
GND GND
GP2 SWCLK
GP3 SWDIO
GP4/UART1 TX GP1/UART0 RX
GP5/UART1 RX GP0/UART0 TX
VSYS VSYS オプション

f:id:mickey_happygolucky:20210218235501j:plain
接続の様子

udevルールの追加

一般ユーザーでopenocd実行時にError: libusb_open() failed with LIBUSB_ERROR_ACCESSが出る場合はudevのルールを追加する。

そのためには/etc/udev/rules.d/99-picoprobe.rulesを下記の内容で作成する。

# Raspberry Pi Pico probe
ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0004", MODE="0666"

下記のコマンドで設定を反映する。

$ sudo udevadm trigger 

picoprobeの書き込み

Debuggerにする方のPicoを転送モードでブートし、/media/${USER}/RPI-RP2がマウントされた状態にする。

picoprobeのプログラムを書き込む。以下はpico_setup.shで構築した環境の場合。

$ cp ${PICO_EXAMPLES_PATH}/../picoprobe/build/picoprobe.uf2 /media/${USER}/RPI-RP2

デバッグの実行

OpenOCD

pico_setup.shで構築した環境の場合、下記のようにするとOpenOCDを起動することができる。

$ cd ${PICO_SDK_PATH}/../openocd
$ src/openocd -f interface/picoprobe.cfg -f target/rp2040.cfg -s tcl

gdb

あらかじめgdb-multiarchパッケージをインストールしておく。

$ sudo apt install gdb-multiarch

例としてblinkを実行する。既にDebuggee側のPicoにblink.uf2が書き込んであるものとする。

$ cd ${PICO_EXAMPLES_PATH}/build/blink
$ gdb-multiarch blink.elf

gdbのシェルで下記のようにするとデバッグを開始できる。

(gdb) target remote :3333
(gdb) mon reset init
(gdb) load

メイン関数にブレークポイントを貼る場合は下記のようにする。

(gdb) b main
(gdb) c 
continuing.
Note: automatically using hardware breakpoints for read-only addresses.
target halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x00000178 msp: 0x20041f00

Thread 1 hit Breakpoint 1, main () at /home/mickey/work/rpi_pico/pico/pico-examples/blink/blink.c:9
9   int main() 

きちんとブレークすることが確認できる。

gdb初期化ファイルの作成

pico.gdbinitを下記の内容で作成する。

target remote :3333
mon reset init
load

gdbを実行する時に下記のようにする。

$ gdb-multiarch -x pico.gdbinit blink.elf

gdbguiの使用

弊ブログでは毎度同じみのgdbguiでもデバッグできる。

$ pip3 install gdbgui --upgrade --user

ユーザーインストールの場合は、${HOME}/.local/binにパスを通す必要がある。

$ cd ${PICO_EXAMPLES_PATH}/build/blink
$ gdbgui -g gdb-multiarch ./blink.elf

pico.gdbinitを使用する場合は下記のようにする。

$ gdbgui -g "gdb-multiarch -x pico.gdbinit blink.elf"

まとめ

picoprobeは十分実用的。

lxd入門 VM編

はじめに

lxdではバージョン4.0から仮想マシン(VM)も扱えるようになった。

コンテナと異なり、CPUやメモリはホストから一部割り当てて使用するためにパフォーマンス的には不利になるが、 カーネル層の機能を使用したい場合など、コンテナでは足りないケースでコンテナと同じコマンド体系で使用できる。

ただし初期ログイン時など少し癖があるのでメモしておく

Running virtual machines with LXD 4.0を参考に作業する。

VMの作成

ubuntu20.04の作成。マシン名は環境変数CONTAINEREに設定しておく。コンテナを作成する場合とほぼ同じ。オプションに--vmを指定する。

$ lxc init ubuntu:20.04 --vm ${CONTAINER}

初期ユーザー設定

LXDがオフィシャルで提供しているOSのイメージではデフォルトユーザーのパスワードなどが未設定であるため、そのままではログインできない。

cloud-initを使用して、OSの初回起動時にのみ実行されるコンフィグレーションを設定しておく必要がある。

$ (
cat << EOF
#cloud-config
apt_mirror: http://us.archive.ubuntu.com/ubuntu/
ssh_pwauth: yes
users:
  - name: ubuntu
    passwd: "\$6\$s.wXDkoGmU5md\$d.vxMQSvtcs1I7wUG4SLgUhmarY7BR.5lusJq1D9U9EnHK2LJx18x90ipsg0g3Jcomfp0EoGAZYfgvT22qGFl/"
    lock_passwd: false
    groups: lxd
    shell: /bin/bash
    sudo: ALL=(ALL) NOPASSWD:ALL
EOF
) | lxc config set ${CONTAINER} user.user-data -
$ lxc config device add ${CONTAINER} config disk source=cloud-init:config

lxd-agentの起動

VMを起動する。

$ lxc start ${CONTAINER}

このままではlxd-agentが動いていないためlxc execは失敗する。

$ lxc exec ${CONTAINER} -- /bin/bash
Error: Failed to connect to lxd-agent

初回起動時はlxc consoleVMに直接ログインして初期化スクリプトを実行する必要がある。

$ lxc console ${CONTAINER}

何も表示されない場合はEnterキーを押すとログイン画面が表示される。下記の情報でログインできる。

ユーザー名 パスワード
ubuntu ubuntu

VMにログインしたらlxd-agentをインストールするのだが、ubuntu 20.04とubuntu 18.04のVMイメージで起動方法が異なっている。

ubuntu 20.04の場合

$ sudo su -
$ cd /run/lxd_config/9p
$ ./install.sh
$ reboot

ubuntu 18.04

$ sudo su -
$ mount -t 9p config /mnt
$ cd /mnt
$ ./install.sh
$ reboot

コンテナの起動

lxd-agentが起動できれば従来どおりlxc execでログインできる。ただしコンテナの開始からlxd-agentが使用可能になるまでは若干のタイムラグがある。

$ lxc exec ${CONTAINER} -- su --login ${USER}

初期パスワードを任意のものに変更する

Running virtual machines with LXD 4.0の手順で紹介されているclaud-initのコンフィグを使用するとパスワードはubuntuになるが、下記のpasswdハッシュ値を変更することで別のパスワードを設定することができる。

$ (
cat << EOF
#cloud-config
apt_mirror: http://us.archive.ubuntu.com/ubuntu/
ssh_pwauth: yes
users:
  - name: ubuntu
    passwd: "\$6\$s.wXDkoGmU5md\$d.vxMQSvtcs1I7wUG4SLgUhmarY7BR.5lusJq1D9U9EnHK2LJx18x90ipsg0g3Jcomfp0EoGAZYfgvT22qGFl/"
    lock_passwd: false
    groups: lxd
    shell: /bin/bash
    sudo: ALL=(ALL) NOPASSWD:ALL
EOF
) | lxc config set ${CONTAINER} user.user-data -
$ lxc config device add ${CONTAINER} config disk source=cloud-init:config

ハッシュの作成

パスワードからハッシュ値を生成するにはopensslコマンドを使用する。

$ openssl passwd -6 <パスワード>

例えばsampleをパスワードにする場合は下記のようにする。

$ openssl passwd -6 sample
$6$ePQ6Km2XVOh7iM9b$zWu/hGbOCa2kxEk7XMNBu6HV63L42lE3mmh8558RWdxsDym8KoklKDNMWM.S8Bj4fdj288sQksmaPnQcTlbEy.

lxc configにこのハッシュ値を渡す場合、$エスケープする必要がある ので注意が必要。

下記のハッシュ値の場合は

$6$ePQ6Km2XVOh7iM9b$zWu/hGbOCa2kxEk7XMNBu6HV63L42lE3mmh8558RWdxsDym8KoklKDNMWM.S8Bj4fdj288sQksmaPnQcTlbEy.

この様にする必要がある。

\$6\$ePQ6Km2XVOh7iM9b\$zWu/hGbOCa2kxEk7XMNBu6HV63L42lE3mmh8558RWdxsDym8KoklKDNMWM.S8Bj4fdj288sQksmaPnQcTlbEy.

生成したハッシュ値を登録する

$ (
cat << EOF
#cloud-config
apt_mirror: http://us.archive.ubuntu.com/ubuntu/
ssh_pwauth: yes
users:
  - name: ubuntu
    passwd: "\$6\$ePQ6Km2XVOh7iM9b\$zWu/hGbOCa2kxEk7XMNBu6HV63L42lE3mmh8558RWdxsDym8KoklKDNMWM.S8Bj4fdj288sQksmaPnQcTlbEy."
    lock_passwd: false
    groups: lxd
    shell: /bin/bash
    sudo: ALL=(ALL) NOPASSWD:ALL
EOF
) | lxc config set ${CONTAINER} user.user-data -
$ lxc config device add ${CONTAINER} config disk source=cloud-init:config

正しく登録されているかは下記のコマンドで確認できる。

$ lxc config show ${CONTAINER}
architecture: x86_64
config:
  image.architecture: amd64
  image.description: ubuntu 18.04 LTS amd64 (release) (20210129)
  image.label: release
  image.os: ubuntu
  image.release: bionic
  image.serial: "20210129"
  image.type: disk1.img
  image.version: "18.04"
  user.user-data: |
    #cloud-config
    apt_mirror: http://us.archive.ubuntu.com/ubuntu/
    ssh_pwauth: yes
    users:
      - name: ubuntu
        passwd: "$6$ePQ6Km2XVOh7iM9b$zWu/hGbOCa2kxEk7XMNBu6HV63L42lE3mmh8558RWdxsDym8KoklKDNMWM.S8Bj4fdj288sQksmaPnQcTlbEy."
        lock_passwd: false
        groups: lxd
        shell: /bin/bash
        sudo: ALL=(ALL) NOPASSWD:ALL
  volatile.apply_template: create
  volatile.base_image: dc0a7d127e63e27063638e413cf6f2b83094e60924190f07a12b13a1bbd93a2a
  volatile.eth0.hwaddr: 00:16:3e:bd:de:28
devices: {}
ephemeral: false
profiles:
- default
stateful: false
description: ""

ハッシュが正しく登録されていることが確認できた。

初期パスワードの確認

lxc consoleでログインしてみる。

$ lxc start ${CONTAINER}
$ lxc console ${CONTAINER}

下記でログインすることができる。

ユーザー名 パスワード
ubuntu sample

まとめ

lxdでVMを使用することができる。

下記の2点、あらかじめ知っていないと面を食らう。

  1. 初期パスワードの設定
  2. lxd-agentのインストール

lxd入門

はじめに

コンテナ技術としてはDockerが有名だが、こちらはアプリケーションコンテナとなっている。 LXDはシステムコンテナという位置づけになっているが、これらがどう違っているのかわからなかった。

DockerとLXDの大きな違いはセッションのライフサイクルとなる。

Dockerでもbashを実行することでコンテナにログインすることができる。しかし、シェルからexitすると、コンテナそのものが終了してしまう。

LXDはシステムコンテナなのでbashでログインして作業し、そこからexitしてもコンテナそのものは起動したままになる。

つまり、Dockerはあるアプリケーションを実行するための実行環境としてコンテナを使用するもので、 LXDはVMのような仮想的な環境としてコンテナを使用するものという違いがある。

LXDでも下記のようにlxc execで特定のコマンドを実行することもできるが、コンテナ自体はずっと可動している。

$ lxc exec <container name> -- ls

lxdのインストール

Ubuntu 20.04環境ではsnapで導入するのが無難

$ sudo snap install lxd

コンテナ環境の初期化

$ sudo lxd init

初期のストレージプールの作成などが行われる。

グループの追加

root以外でlxcを実行可能にする。

$ sudo gpasswd -a ${USER} lxd

UID/GIDマッピングの準備

LXDコンテナ中でホストのホームディレクトリを読み書きする方法

ホーム以外にも、ホストのディレクトリをコンテナ内にマッピングしてアクセス可能にするために必要。

ホスト環境の/etc/subuid/etc/subgidマッピング情報が必要になる。

$ echo "root:$(id -u):1" | sudo tee -a /etc/subuid
$ echo "root:$(id -g):1" | sudo tee -a /etc/subgid

IDのマッピングを許可する範囲を指定する。書式は下記。

USER:START:COUNT

上記の例ではマッピングを許可するのはコンテナのrootで、マッピングを許可されるのはホストの$(id -u)のUIDを持つユーザーから1個と指定している。

コンテナの作成

手順を汎用化するために環境変数CONTAINERにコンテナ名を設定しておく。 ${CONTAINER}の部分をコンテナ名直打ちにしても問題ない。

$ lxc init ubuntu:18.04 ${CONTAINER}

ubuntu:18.04はインターネット経由で提供されているベースイメージ。

使用可能なものは下記のコマンドで確認できる。

$ lxc image list images:

下記のようにするとディストリビューションを絞って表示できる。

$ lxc image list images:ubuntu

コンテナの起動

$ lxc start ${CONTAINER}
$ lxc exec ${CONTAINER} -- /bin/bash

一般ユーザーの作成

ユーザー名は任意、 コマンドの例では環境変数USERNAMEに設定してある。 ホストPCの${USER}と併せておくといろ都合がよい。

# adduser ${USERNAME}

ubuntu:18.04のコンテナは初期状態でubuntuユーザーがいるためここで作るユーザーのUIDは1001になる。

sudo可能にする

一般ユーザーにsudo権限を追加する。

# gpasswd -a ${USERNAME} sudo

ubuntu 16.04や18.04ではこのままではsudo実行時に下記のエラーになる。

sudo: no tty present and no askpass program specified

/etc/sudoers.d/にファイルを作成して、NOPASSWDを設定する。

$ echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers.d/${USERNAME}

2回目以降

一度抜けたあとに再度コンテナに入る場合は下記のようにする。

$ lxc exec ${CONTAINER} -- /bin/bash

上記では毎回rootでログインすることになるので、作成した一般ユーザーでログインしたい場合は下記のようにする。

$ lxc exec ${CONTAINER} -- su --login ${USER}

上記はコンテナの一般ユーザー名がホストの${USER}と一致していることを想定している。

コンテナの一般的な操作

起動

$ lxc start ${CONTAINER}

終了

$ lxc stop ${CONTAINER}

作成済みのコンテナの確認

$ lxc list

削除

$ lxc delete ${CONTAINER}

ホストのディレクトリのバインド

ホストとコンテナのUID/GIDを紐付ける

lxc config set <container name> raw.imapで実際の紐付けを行う。

書式は下記のようになっている。

"(uid|gid|both) <ホスト側のID> <コンテナ側のID>"

UIDとGIDをどちらも紐付けしたい場合はbothそれぞれどちらかの場合はその値を指定する。

コンテナで一般ユーザーを追加した場合は1001になるので、ホストの実行中のユーザーとコンテナの追加した一般ユーザーを紐付ける場合下記のようになる。

$ lxc config set ${CONTAINER} raw.idmap "both $(id -u) 1001"

この例ではホストのUIDとGIDが一致している場合を想定している。

ディレクトリのバインド

ディレクトリのバインドはlxc config device addで行う。

書式は下記のようになっている。

lxc config device add <コンテナ名> <名前> disk source=<ホスト上のパス> path=<コンテナ上のパス>

コンテナの/mnt/hdd/work/home/${USER}/workに紐付ける場合下記のようにする。

$ lxc config device add ${CONTAINER} work disk \
    source="/mnt/hdd/work" \
    path="/home/${USER}/work"

有効化するにはコンテナを再起動する必要がある。

$ lxc restart ${CONTAINER}

まとめ

Dockerでもbashを実行してコンテナの中にログインすることができるため、LXDを使用するモチベーションがわからなかったが、 コンテナにログインして、ホストのリソースをフルに間借りできる仮想環境として利用するにはLXDのほうが使いやすいということがわかった。

ちなみに、LXD自体はコンテナの他にも同じコマンド体系でVMを実行することもできる。

USB per-port power switching対応ハブガチャを引いてみた

はじめに

USBハブにはper-port power switching(PPPS)という機能に対応しているものがある。 これはポート毎に給電を制御するものであって、これが使えるとそのハブに接続されたUSB機器の電源をPCから制御することができる。

USBハブのポートが供給する電力量は小さいので、直接制御できるのはせいぜい小型のライトとかファンとかのようなものになるのだが、 USB連動電源タップなどと組み合わせると、それなりの機器の電源制御にも使用することができる。

Linuxが使える環境ではuhubctlというツールがあり、コマンドラインで制御することができる。

ただし、全てのUSBハブがPPPSに対応しているわけではない。 むしろ、PPPSに関してはUSBハブを提供するメーカーは全然気にしていないので、どのハブが対応しているかは買ってみるまでわからない。

今回は、ヨドバシカメラなどの量販店で3000程度で購入できるUSBハブをいくつか買ってみて、対応しているものがあるかを検証してみた。

結果を最初にいうと、1つ以外全部ハズレ。

その1つのアタリも4ポートのうち2ポートしか制御できないという微妙な結果となった。

検証方法

下記の手順で検証した。

  1. LinuxPCにUSBハブを接続
  2. uhubctlでUSBの制御チップが対応しているか確認
  3. 対応している場合は各ポートの電源を制御
  4. USBケーブルから電源線を引っ張り出し、テスタで電圧を計測

まず、制御チップが電源制御に対応しているかが鍵となる。そもそもこれに対応していないとPPPSは使用できない。

次に、制御チップが対応していても回路設計上ポートのVBUSが5Vに接続されているなどの場合は、 いくらチップを制御しても接続されている機器には5Vが供給されることになる。

PPPSはポートごとの給電を制御するため、ハブ自体の電源が落ちるわけではないのでこういうことがあり得る。 こればかりは USBハブの基板の設計次第なので本当に実際電圧を測ってみるまでわからない のだ。

uhubctlの対応状況

uhubctlの対応状況はリンク先の表に記載されている。

これはユーザーからの報告によって更新されている。この表には実際に制御してみて電圧が変化することが確認できたものしか追加されない。 つまり、この表に記載があるものは確実に使用できる。

しかし先述の通り、制御チップがPPPS対応でも基板の設計によって非対応のケースがあるため、VID:PIDが同じ別の機種が必ず対応しているとは言えない。

表を見てもらえばわかるが、記載があるハブは下記の点から入手性が高いとは言い難い。

  • 古いものが多い
  • 海外の製品が多い

今回の戦果

筆者がもともと持っていたものもついでに記載する。それ以外はヨドバシなどの量販店Amazonなどで購入した物。

もともと持っていたものは古いものが多かったので備考に手持ち品と記載し、今回の検証用の購入したものとは区別できるようにする。

ブランド 型番 ポート USB VID:PID チップ対応 ポート制御 備考
Digio2 UH-2404BL 4 2.0 05e3:0608 × ×
Buffalo BSH4U050U2 4 2.0 05e3:0608 × ×
Buffalo BSH4A110U3 4 3.0 0bda:0411 ×
Buffalo BSH4U06 4 2.0 05e3:0608 × × 手持ち品
Buffalo BSH4U01BK 4 2.0 05e3:0608 × × 手持ち品
Digio2 UH-3034W 4 3.0 05e3:0612 × ×
ELECOM U3H-A422BX 4 3.0 05e3:0626 × ×
Anker A7516 4 3.0 0bda:0411 ×
Buffalo BSH4A01 4 2.0 05e3:0608 × × 手持ち品
System TALKS USB2-HUB4X(SUGOI-HUB) 4 2.0 0409:005a Port1、2のみ
100均 ???? 4 1.0 0a05:7211 × ×

寸評

手持ち品も含めて何故かBuffaloに偏ってしまった。

05e3:0608Genesys Logic製のチップで、今回買った価格帯のUSBハブには、このチップが多い気がした。 これはチップ自体がPPPSに対応していないのでlsusbなどでこれが出たら確実にハズレ。 USB2.0だし仕方ないかな?と思いつつ、USB3.005e3:0626もPPPS非対応。 VID:05e3はuhubctlの対応表にも出てくるのでGenesys Logic=ハズレというわけではないだろうが、今回の検証結果のせいで印象が悪い。

いくつかのハブに搭載されていた0bda:0411Realtek製のチップで、チップとしてはアタリ。 しかし今回買ったハブの基板設計上の問題でハズレ扱いに。ちょっと不遇。

今回唯一のアタリのSUGOI-HUBは0409:005aNEC製のチップが搭載されている。安心の日本製! 「SUGOI-HUB(スゴイハブ)なんて仰々しい名前ついてんな。」くらい思っていたが、本当にスゴイやつだった。

100均のハブはVIDからしてUnknownというキレっぷり。いつ火を吹いてもおかしくない感じがやばい。

まとめ

今回は、外れた時の精神的&経済的なダメージが大きいので高額のハブは避けたが、低価格帯はことごとくハズレという結果になった。 結局買ってみるまでわからないっていうのは結構イタイ話だ。

中には高額なUSBハブを持っているひともいるだろうし、 Linuxが使える人は、ぜひともuhubctlで対応状況を調べて見てほしい。 そして対応しているものがあれば積極的にuhubctlのコミュニティに報告してほしい。 ツイッターなんかでつぶやくだけでも嬉しい。

USBハブのベンダの人たちはPPPS対応している製品に関しては積極的に宣伝してほしい。 購入するかどうかの決め手になってもおかしくないぐらい使えると便利な機能のはず。 そしてもっと対応製品作ってくれ。。。

USBハブで機器の電源制御ができたらいちいちリレーくまなくて良くなるしね。

Raspberry Pi Pico SDKでのFlashからのブートについて調べた

はじめに

私のベアメタルの心の師匠であるdwelch67さんが、さっそく何か作っていたのでパクる勉強するべく中身を眺めていた。

リンカスクリプトの下記の部分のLENGTH = 0xFCが引っかかったので調べてみた。

MEMORY
{
    flash : ORIGIN = 0x10000000, LENGTH = 0xFC
}

0xFCということは252バイトで、プログラムのサイズがこれしか書けないのだろうか?

READMEを読むと、252に関しては言及している。

This leaves 252 bytes for the second stage bootloader.

このサイズはthe second stage bootloaderがそうでなければならないということらしい。

最初の方をきちんと読むと下記のことがわかる。

  1. PicoはFlash-ROMから起動する場合先頭の256バイトをSRAMにコピーする。
  2. CRC32でデータをチェックし、正しい値が読めるまで繰り返す。
  3. 最終的に正しくCRCチェックが通らない場合は起動に失敗する。

RP2040のFlash-ROMブートシーケンス

RP2040 DatasheetFigure 15にブートシーケンスのチャートがある。

この部分が先ほどの記述の部分だとわかる。

f:id:mickey_happygolucky:20210210004214p:plain
CRCチェックのフロー

2.8.2.3.1. Checksumの下記の部分にチェックサムについて記述がある。

The last four bytes of the image loaded from flash (which we hope is a valid flash second stage) are a CRC32 checksum of the first 252 bytes.

CRCが最後の4バイトで、検査対象が先頭の252バイトということらしい。

dwelch67さんのこのコードはこのthe second stage bootloaderの部分でLチカをしてしまおうということっぽい(すごい)。

Pico SDKでの扱い

pico-examplesのblinkのように、Pico SDKでビルドされるアプリはどの様になっているのだろうか。

blinkのCMakeLists.txt

blinkのCMakeLists.txtを見てみる。

add_executable(blink
        blink.c
        )

# Pull in our pico_stdlib which pulls in commonly used features
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)

なるほど。わからん。

そもそもスタートアップルーチンやらリンカスクリプトなどはこの層には公開されていない。

SDKリンカスクリプト

Pico SDKリンカスクリプトがないか探してみる。

$ $ find -name '*.ld' | head
./src/rp2_common/boot_stage2/boot_stage2.ld
./src/rp2_common/pico_standard_link/memmap_copy_to_ram.ld
./src/rp2_common/pico_standard_link/memmap_blocked_ram.ld
./src/rp2_common/pico_standard_link/memmap_default.ld
./src/rp2_common/pico_standard_link/memmap_no_flash.ld

関係ありそうなのはこの5つ。boot_stage2.ldthe second stage bootloaderのことのようだ。つまりSDK自体はこのローダのコードを持っている。

boot_stage2.ld

boot_stage2.ldの中身を見てみる。LENGTH = 252で、これがFlashの先頭に書かれるプログラムと見て良さそう。

MEMORY {
    /* We are loaded to the top 256 bytes of SRAM, which is above the bootrom
       stack. Note 4 bytes occupied by checksum. */
    SRAM(rx) : ORIGIN = 0x20041f00, LENGTH = 252
}

SECTIONS {
    . = ORIGIN(SRAM);
    .text : {
        *(.entry)
        *(.text)
    } >SRAM
}

memmap_default.ld

memmap_default.ldを見てみる。

SDKでアプリを作る時にデフォルトで使用されるリンカスクリプトはこれっぽい。

...(snip)...

MEMORY
{
    FLASH(rx) : ORIGIN = 0x10000000, LENGTH = 2048k
    RAM(rwx) : ORIGIN =  0x20000000, LENGTH = 256k
    SCRATCH_X(rwx) : ORIGIN = 0x20040000, LENGTH = 4k
    SCRATCH_Y(rwx) : ORIGIN = 0x20041000, LENGTH = 4k
}

ENTRY(_entry_point)

SECTIONS
{
    /* Second stage bootloader is prepended to the image. It must be 256 bytes big
       and checksummed. It is usually built by the boot_stage2 target
       in the Raspberry Pi Pico SDK
    */

    .flash_begin : {
        __flash_binary_start = .;
    } > FLASH

    .boot2 : {
        __boot2_start__ = .;
        KEEP (*(.boot2))
        __boot2_end__ = .;
    } > FLASH

    ASSERT(__boot2_end__ - __boot2_start__ == 256,
        "ERROR: Pico second stage bootloader must be 256 bytes in size")

...(snip)...

Flashの先頭に.boot2セクションが置かれるようになっていて、このセクションの先頭から末尾までのサイズが256バイトじゃないとエラーになる。

ビルドしてelfを作った時に、.boot2セクションにthe second stage bootloaderのコードが入るようになっているようす。

.boot2セクションについて

この.boot2セクションはどこから来るのか。

$ grep -r '\.boot2' .
./src/rp2_common/boot_stage2/pad_checksum:50:        ofile.write(".section .boot2, \"ax\"\n\n")
./src/rp2_common/pico_standard_link/memmap_copy_to_ram.ld:45:    .boot2 : {
./src/rp2_common/pico_standard_link/memmap_copy_to_ram.ld:47:        KEEP (*(.boot2))
./src/rp2_common/pico_standard_link/memmap_blocked_ram.ld:45:    .boot2 : {
./src/rp2_common/pico_standard_link/memmap_blocked_ram.ld:47:        KEEP (*(.boot2))
./src/rp2_common/pico_standard_link/memmap_default.ld:45:    .boot2 : {
./src/rp2_common/pico_standard_link/memmap_default.ld:47:        KEEP (*(.boot2))

pad_checksumで埋め込んでいるっぽい。これの中身を確認する。

#!/usr/bin/env python3

...(snip)...
try:
    with open(args.ofile, "w") as ofile:
        ofile.write("// Padded and checksummed version of: {}\n\n".format(args.ifile))
        ofile.write(".cpu cortex-m0plus\n")
        ofile.write(".thumb\n\n")
        ofile.write(".section .boot2, \"ax\"\n\n")
        for offs in range(0, len(odata), 16):
            chunk = odata[offs:min(offs + 16, len(odata))]
            ofile.write(".byte {}\n".format(", ".join("0x{:02x}".format(b) for b in chunk)))

pythonスクリプトCRCチェックサムの計算をしている。ついでに、対象ファイルに.boot2セクションとしてローダのコードとCRCを埋めているようだ。

このスクリプトはどこから実行されるのか。

$ grep -r 'pad_checksum'  .
./src/rp2_common/boot_stage2/CMakeLists.txt:48:            COMMAND ${Python3_EXECUTABLE} ${PICO_BOOT_STAGE2_DIR}/pad_checksum -s 0xffffffff ${ORIGINAL_BIN} ${PADDED_CHECKSUMMED_ASM}

ローダのCMakeLists.txtで下記のようになっている。

...(snip)...

    set(PADDED_CHECKSUMMED_ASM ${CMAKE_CURRENT_BINARY_DIR}/${NAME}_padded_checksummed.S)

...(snip)...

    add_custom_command(OUTPUT ${PADDED_CHECKSUMMED_ASM} DEPENDS ${ORIGINAL_BIN}
            COMMAND ${Python3_EXECUTABLE} ${PICO_BOOT_STAGE2_DIR}/pad_checksum -s 0xffffffff ${ORIGINAL_BIN} ${PADDED_CHECKSUMMED_ASM}
            )

...(snip)...

一度ビルドして、そのバイナリデータとCRCを含んだ.boot2のセクションの情報を${NAME}_padded_checksummed.Sとして出力している。

この出力結果を確認する。

// Padded and checksummed version of: /home/mickey/work/rpi_pico/pico/picoprobe/build/pico-sdk/src/rp2_common/boot_stage2/bs2_default.bin

.cpu cortex-m0plus
.thumb

.section .boot2, "ax"

.byte 0x00, 0xb5, 0x32, 0x4b, 0x21, 0x20, 0x58, 0x60, 0x98, 0x68, 0x02, 0x21, 0x88, 0x43, 0x98, 0x60
.byte 0xd8, 0x60, 0x18, 0x61, 0x58, 0x61, 0x2e, 0x4b, 0x00, 0x21, 0x99, 0x60, 0x02, 0x21, 0x59, 0x61
.byte 0x01, 0x21, 0xf0, 0x22, 0x99, 0x50, 0x2b, 0x49, 0x19, 0x60, 0x01, 0x21, 0x99, 0x60, 0x35, 0x20
.byte 0x00, 0xf0, 0x44, 0xf8, 0x02, 0x22, 0x90, 0x42, 0x14, 0xd0, 0x06, 0x21, 0x19, 0x66, 0x00, 0xf0
.byte 0x34, 0xf8, 0x19, 0x6e, 0x01, 0x21, 0x19, 0x66, 0x00, 0x20, 0x18, 0x66, 0x1a, 0x66, 0x00, 0xf0
.byte 0x2c, 0xf8, 0x19, 0x6e, 0x19, 0x6e, 0x19, 0x6e, 0x05, 0x20, 0x00, 0xf0, 0x2f, 0xf8, 0x01, 0x21
.byte 0x08, 0x42, 0xf9, 0xd1, 0x00, 0x21, 0x99, 0x60, 0x1b, 0x49, 0x19, 0x60, 0x00, 0x21, 0x59, 0x60
.byte 0x1a, 0x49, 0x1b, 0x48, 0x01, 0x60, 0x01, 0x21, 0x99, 0x60, 0xeb, 0x21, 0x19, 0x66, 0xa0, 0x21
.byte 0x19, 0x66, 0x00, 0xf0, 0x12, 0xf8, 0x00, 0x21, 0x99, 0x60, 0x16, 0x49, 0x14, 0x48, 0x01, 0x60
.byte 0x01, 0x21, 0x99, 0x60, 0x01, 0xbc, 0x00, 0x28, 0x00, 0xd0, 0x00, 0x47, 0x12, 0x48, 0x13, 0x49
.byte 0x08, 0x60, 0x03, 0xc8, 0x80, 0xf3, 0x08, 0x88, 0x08, 0x47, 0x03, 0xb5, 0x99, 0x6a, 0x04, 0x20
.byte 0x01, 0x42, 0xfb, 0xd0, 0x01, 0x20, 0x01, 0x42, 0xf8, 0xd1, 0x03, 0xbd, 0x02, 0xb5, 0x18, 0x66
.byte 0x18, 0x66, 0xff, 0xf7, 0xf2, 0xff, 0x18, 0x6e, 0x18, 0x6e, 0x02, 0xbd, 0x00, 0x00, 0x02, 0x40
.byte 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x07, 0x00, 0x00, 0x03, 0x5f, 0x00, 0x21, 0x22, 0x00, 0x00
.byte 0xf4, 0x00, 0x00, 0x18, 0x22, 0x20, 0x00, 0xa0, 0x00, 0x01, 0x00, 0x10, 0x08, 0xed, 0x00, 0xe0
.byte 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x74, 0xb2, 0x4e, 0x7a

なるほど。これをアプリを作る時にリンクしている。

まとめ

Raspberry Pi PicoでFlashから起動する際の仕組みを調べた。

  • RP2040はFlashからプログラムを起動する場合、最初に先頭の256バイトを読み込む
  • その際にCRCチェックされる
  • 通常では、CRCを除く先頭の252バイトのプログラムが残りをロードするためのローダであることが期待される
  • Pico SDKではboot_stage2として実装されている
  • Flashから起動するアプリケーションをビルドする時に.boot2セクションにこのコードが埋め込まれる