みつきんのメモ

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

Pico ベアメタルをClang(LLVM)でビルド

はじめに

ここまで作ってきたベアメタルのLチカはGCCでビルドしてきたが、それをclangでビルドしてみる。

clang-10のインストール

Ubuntu 20.04環境でClangをインストールする。バージョンは10。

$ sudo apt install -y clang-10 lld-10 make

Makefile

ベースはLチカのFLASH + コンパイル済みBoot Stage2バージョン

まずはMakefileを下記のように変更する。

LLVM_OPT = --target=armv6-m-unknown-none-eabi -mcpu=cortex-m0plus 

ASMOPT = $(LLVM_OPT) -c -g
COPT = $(LLVM_OPT) -c -mthumb -ffreestanding -g -O0
LOPT = $(LLVM_OPT) -nostdlib

all: led.uf2

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

start.o: start.S
    clang-10 $(ASMOPT) start.S -o start.o

boot2.o: boot2/bs2_default_padded_checksummed.S
    clang-10 $(ASMOPT) boot2/bs2_default_padded_checksummed.S -o boot2.o

main.o: main.c
    clang-10 $(COPT) -fpic -mthumb -c main.c -o main.o

led.elf: start.o boot2.o main.o
    clang-10 -fuse-ld=lld $(LOPT) start.o boot2.o main.o -T memmap.ld  -o led.elf 

led.uf2: led.elf
    elf2uf2 led.elf led.uf2

GCC環境とのコマンドは下記のように異なっている。

機能 GCC Clang
アセンブラ as clang
コンパイラ gcc clang
リンカ ld clang

clangは全ての機能においてclangから呼び出されるようになっている

ポイントはアセンブルコンパイルの時にリンクまでしたくない場合は-cをつけること。

clangではクロスコンパイルのためにそれぞれのバイナリがあるのではなくLLVMのバックエンドを切り替えるだけなので、すべてオプションで指定する。

基本的にはRaspberry Pi Pico向けの場合下記のようになる。

LLVM_OPT = --target=armv6-m-unknown-none-eabi -mcpu=cortex-m0plus 

それぞれの機能で渡したいオプションが違うので下記のようにする。

ASMOPT = $(LLVM_OPT) -c -g
COPT = $(LLVM_OPT) -c -mthumb -ffreestanding -g -O0
LOPT = $(LLVM_OPT) -nostdlib

リンカスクリプト

そのままでは下記のようなエラーになる。

ld.lld: error: no memory region specified for section '.ARM.exidx'
clang: error: ld.lld command failed with exit code 1 (use -v to see invocation)
make: *** [Makefile:26: led.elf] エラー 1

これは.ARM.exidxセクションが無いというエラー。

前にリンカスクリプトを眺めた時はnewlibを使用する時に必要になるセクションらしい。とのことだったが、LLVMでバイナリを吐き出す場合にも必要になるようだ。

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

ENTRY(reset)

SECTIONS
{
    /* 
       boot2 section is for embed the precompiled boot2.
       this code from memmap_default.ld in pico-sdk
    */
    .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")
    .text : {
        KEEP (*(.vectors))
        KEEP(*(.text*))
    } > FLASH

    .ARM.exidx :
    {
        *(.ARM.exidx* .gnu.linkonce.armexidx.*)
    } > FLASH
}

これでエラーは回避できるようになった。

$ make
clang-10 -fuse-ld=lld --target=armv6-m-unknown-none-eabi -mcpu=cortex-m0plus  -nostdlib start.o boot2.o main.o -T memmap.ld  -o led.elf 
elf2uf2 led.elf led.uf2

まとめ

アセンブラコンパイラ、リンカの呼び出しが全てclangだとは思わなかった。

.ARM.exidxセクションは必要になる。

PicoでベアメタルのLチカ(FLASH + コンパイル済みBoot Stage2バージョン) その1

はじめに

Raspberry Pi Pico SDKでのFlashからのブートについて調べたでは、Flashからブートするための仕組みを調査した。

Boot Stage2に相当する部分を自作するのは結構面倒だということがわかったので、コンパイル済み(CRC計算済み)のBoot Stage2である、 bs2_default_padded_checksummed.Sをリンクしてプログラムをつくってみる。

そのためにはBoot Stage2が何をやっているかを調べる必要があったが、既に調べている人がいたので参考にする。(ありがたや!)

また、FLASHやメモリ上にどのように配置しなければならないかを調べるためにPico SDKのリンカスクリプトを眺めた

下準備

elf2uf2

PicoのFLASHに書き込む場合、ビルドしたelf形式のファイルをuf2形式に変換する必要がある。ここではPico SDKに含まれるelf2uf2というツールを使用する。

そのためには、あらかじめPATHの通った場所にファイルをコピーしておく。

$ cp ${PICO_EXAMPLES_PATH}/build/elf2uf2/elf2uf2 ~/bin

~/binにパスを通すためには下記のように.bashrcなどに設定しておく。

$ echo 'export PATH=${HOME}/bin:${PATH}' >> ~/.bashrc
$ source ~/.bashrc

Boot Stage2

PicoのデフォルトのBoot Stage2である、bs2_default_padded_checksummed.SをPico SDKからコピーする。

$ mkdir boot2
$ cp ${PICO_EXAMPLES_PATH}/build/pico-sdk/src/rp2_common/boot_stage2/bs2_default_padded_checksummed.S ./boot2

ディレクトリ構成

Boot Stage2をboot2ディレクトリに格納してそれ以外はフラットな構造。自分で作るファイルはRAM版とほぼ同じ。

├── Makefile
├── boot2
│   └── bs2_default_padded_checksummed.S
├── main.c
├── memmap.ld
├── pico.gdbinit
├── regs.h
└── start.S

違っているのは下記の3つのファイルなので、これ以外のものはRAM版)の記事を参照してほしい。

リンカスクリプト

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

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

ENTRY(reset)

SECTIONS
{
    /* 
       boot2 section is for embed the precompiled boot2.
       this code from memmap_default.ld in pico-sdk
    */
    .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")
    .text : {
        KEEP (*(.vectors))
        KEEP(*(.text*))
    } > FLASH
}

スタックポインタもRAM上においてしまうのでSCRATCH_XやSCRATCH_Yは定義しない。

FLASHの先頭256バイトの位置にBoot Stage2を配置する。

Boot Stage2によって、その直後の0x10000100の位置にあるはずのベクターテーブルのリセットベクタを読みに来るので、.vectors.boot2の直後に配置されるようにする。 プログラム本体である.textベクターテーブルの後ろに配置されるようにしておく。

スタートアップ

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

 .cpu cortex-m0plus
    .thumb

/* vector table */
    .section .vectors, "ax"
    .align 2
    .global __vectors
__vectors:
.word 0x20001000
.word reset

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

ベクターテーブルは先頭の位置とresetベクタの位置が合っていればとりあえずプログラムは起動できる。 ただし割り込みなどを使用する場合は、きちんと定義する必要がある。

リセットベクタとしてreset関数を登録しておく。

resetはスタックポインタを設定してmainにジャンプするだけ。

Makefile

Makefileを下記の名前で作成する。

CROSS_COMPILE ?= arm-none-eabi-

MCPU = -mcpu=cortex-m0plus

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

all: led.uf2

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

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

boot2.o: boot2/bs2_default_padded_checksummed.S
    $(CROSS_COMPILE)as $(ASMOPT) boot2/bs2_default_padded_checksummed.S -o boot2.o

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

led.elf: start.o boot2.o main.o
    $(CROSS_COMPILE)ld $(LOPT) start.o boot2.o main.o -T memmap.ld  -o led.elf 

led.uf2: led.elf
    elf2uf2 led.elf led.uf2

makeコマンドでuf2形式に変換できるようにした。

書き込み

ボード側のBOOTSELボタンを押しながら、USBケーブルでPCと接続すると自動的に/media/${USER}/RPI-RP2にマウントされる。

その状態でled.uf2をボードに書き込む。

$ cp led.uf2 /media/${USER}/RPI-RP2

書き込み終了時点からLEDがチカチカした!

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

まとめ

軽い気持ちでBoot Stage2を組み込んだらベアメタル簡単につくれるのでは?と作業を始めたが、結構大変だった。

リンカスクリプトについても、最低限起動に必要なルールを踏襲するだけで割と自由に作れることがわかった。

おそらくその2はない。

参考

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を実行することもできる。