みつきんのメモ

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

Raspberry Pi Pico Systick割り込みを使ってLチカをする

はじめに

SysTick割り込みを使用してLチカをする。

2021-02-26 Raspberry Pi Pico Systickを使ってLチカを正確にするのコードをベースに作業する。

SysTick割り込み

SysTick割り込みはm0+のコアで用意されている割り込みで、この割り込みが発生するとベクターテーブルに飛んでくる。

ここによるとベクターテーブルは下記のような構造になっている。

ベクターテーブル

ベクターテーブルは通常0x00000000に配置されCPUのリセットがかかったり割り込みが入ったりした場合に参照される様になっているが、VTORレジスタにアドレスを設定することで任意の場所に配置することができる。

今回はPicoのSRAM上にベクターテーブルを配置するようにする。

ベクターテーブルと割り込みハンドラの追加

ベクターテーブルとSysTickの割り込みハンドラとなるisr_systickを追加する。

スタートアップ

start.Sを下記のように変更する。

--- a/blink_ram_int_systick/start.S
+++ b/blink_ram_int_systick/start.S
@@ -2,10 +2,36 @@
    .cpu cortex-m0plus
    .thumb

+/* vector table */
+   .section .vectors, "ax"
+   .align 2
+   .global __vectors
+__vectors:
+.word 0x20001000
+.word reset
+.word hang
+.word hang
+.word hang
+.word hang
+.word hang
+.word hang
+.word hang
+.word hang
+.word hang
+.word hang
+.word hang
+.word hang
+.word hang
+.word isr_systick
+
 /* reset handler */
    .thumb_func
    .global reset
 reset:
+   ldr r1, =0xE000ED08 /* VTOR */
+   ldr r0, =__vectors
+   str r0, [r1]
+
    ldr r0, =0x20001000
    mov sp, r0
    bl main

ベクターテーブルを__vectorsとして定義する。 Systick割り込みハンドラの位置にisr_systickを登録している。他の割り込みは使用しないのでisr_systickまでしか定義していない。

reset関数の先頭でVTORレジスタ__vectorsのアドレスを設定している。

リンカスクリプト

memmap_ram.ldを下記の様に修正する。

--- a/blink_ram_int_systick/memmap_ram.ld
+++ b/blink_ram_int_systick/memmap_ram.ld
@@ -9,6 +9,7 @@ ENTRY(reset)
 SECTIONS
 {
     .text : {
+        *(.vectors)
        *(.text*)
     } > RAM
 }

.vectorセクションがRAMに配置されるようにする。

main

main.cを下記のように修正する。

--- a/blink_ram_int_systick/main.c
+++ b/blink_ram_int_systick/main.c
@@ -18,6 +18,11 @@ void init_systick() {
         write_reg(SYSTICK_CSR, 0x1);
 }

+static uint32_t tick_count = 0;
+void isr_systick() {
+        ++tick_count;
+}
+
 // us is up to max of 24bit (0xffffff)
 void delay_us(uint32_t us) {
         write_reg(SYSTICK_RVR, us);

Systickの割り込みハンドラとなるisr_systickを追加する。

割り込みが発生するたびにtick_countをインクリメントする。

ディレイ処理の改造

前回はマイクロ秒単位で作成したディレイ関数をミリ秒単位に変更する。

main.c

diff --git a/blink_ram_int_systick/main.c b/blink_ram_int_systick/main.c
index 830f1e4..77d1309 100644
--- a/blink_ram_int_systick/main.c
+++ b/blink_ram_int_systick/main.c
@@ -15,7 +15,9 @@ void write_reg_op(uint32_t addr, uint32_t value, uint32_t op) {
 }

 void init_systick() {
-        write_reg(SYSTICK_CSR, 0x1);
+        write_reg(SYSTICK_RVR, 1000-1); /* Systick interrupt will issue per 1ms. */
+        write_reg(SYSTICK_CVR, 0);
+        write_reg(SYSTICK_CSR, 0x3); /* TICKINT=1, ENABLE=1 */
 }

 static uint32_t tick_count = 0;
@@ -23,16 +25,10 @@ void isr_systick() {
         ++tick_count;
 }

-// us is up to max of 24bit (0xffffff)
-void delay_us(uint32_t us) {
-        write_reg(SYSTICK_RVR, us);
-        write_reg(SYSTICK_CVR, 0);
-
-        // wait for 0 to RVR
-        while (!read_reg(SYSTICK_CVR)) __asm__ __volatile__("");
-
-        // wait for RVR counting down to 0.
-        while (read_reg(SYSTICK_CVR)) __asm__ __volatile__("");
+void delay_ms(uint32_t ms) {
+        uint32_t expire = tick_count + ms;
+        while (tick_count != expire)
+                __asm__ __volatile__("");
 }

 int main(void) {
@@ -66,7 +62,7 @@ int main(void) {
         // Blink
         write_reg(SIO_GPIO_OE_SET, (1ul<<25));
         while (1) {
-                delay_us(100*1000); //100ms
+                delay_ms(100); //100ms
                 write_reg(SIO_GPIO_OUT_XOR, (1ul<<25));
         }
         return 0;

RVRを1000-1に設定することで、割り込みの周期を1ミリ秒にしている。

BSS領域を使えるようにする

変数tick_countはスタティックな広域変数となっている。0で初期化しているため、コンパイラによって.bssセクションに配置される様になっている。

$ readelf -s led.elf | grep tick_count
25:    21: 20000224     4 NOTYPE  LOCAL  DEFAULT    2 tick_count
$ readelf -S led.elf | grep bss
7:  [ 2] .bss              NOBITS          20000224 000278 000004 00  WA  0   0  4

余談だが初期値を0以外にすると.dataセクションに配置される。

現在は.bssセクションはリンカスクリプトでもケアしていないので、下記のようにCのコード上0に初期化していても実際にはおかしな値が設定されている。

static uint32_t tick_count = 0;

これを正しく動作させるためには下記の様な修正をする必要がある。

  • リンカスクリプトを修正し.bssセクションが正しくRAM上に配置されるようにする。
  • mainが実装されるまでに0で初期化する。

リンカスクリプト

リンカスクリプトを下記のように修正する。

diff --git a/blink_ram_int_systick/memmap_ram.ld b/blink_ram_int_systick/memmap_ram.ld
index 97530d9..c16e1ea 100644
--- a/blink_ram_int_systick/memmap_ram.ld
+++ b/blink_ram_int_systick/memmap_ram.ld
@@ -12,4 +12,11 @@ SECTIONS
         *(.vectors)
        *(.text*)
     } > RAM
+
+    .bss : {
+        __bss_start__ = .;
+       *(.bss*)
+       . = ALIGN(4);
+       __bss_end__ = .;
+    } > RAM
 }

__bss_start____bss_end__はロケーションカウンタと呼ばれるもので、リンク後にその位置をプログラムから参照できるようにするもの。ここでは.bssセクションの先頭と末尾の位置を保存している。

main

一般的にはBSS領域の初期化などの処理はスタートアップのアセンブリで実装されることが多いが、ここではC言語からロケーションカウンタを参照し0で初期化する。

diff --git a/blink_ram_int_systick/main.c b/blink_ram_int_systick/main.c
index 77d1309..19539f9 100644
--- a/blink_ram_int_systick/main.c
+++ b/blink_ram_int_systick/main.c
@@ -14,6 +14,16 @@ void write_reg_op(uint32_t addr, uint32_t value, uint32_t op) {
         write_reg(addr | op, value);
 }
 
+void init_bss() {
+        extern uint32_t __bss_start__;
+        extern uint32_t __bss_end__;
+        uint32_t *p = &__bss_start__;
+        uint32_t *end = &__bss_end__;
+        while (p != end) {
+                *p++ = 0;
+        }
+}
+
 void init_systick() {
         write_reg(SYSTICK_RVR, 1000-1); /* Systick interrupt will issue per 1ms. */
         write_reg(SYSTICK_CVR, 0);

スタートアップ

スタートアップからinit_bssを呼び出す。

diff --git a/blink_ram_int_systick/start.S b/blink_ram_int_systick/start.S
index 793d991..8419f1b 100644
--- a/blink_ram_int_systick/start.S
+++ b/blink_ram_int_systick/start.S
@@ -34,6 +34,7 @@ reset:
 
        ldr r0, =0x20001000
        mov sp, r0
+       bl init_bss
        bl main
        b hang

波形を見る

f:id:mickey_happygolucky:20210227132845p:plain
Lチカ

こちらも100msのパルス幅になっている。

まとめ

Systickの割り込みを使用してLチカを行った。

ベクターテーブルの場所はVTORレジスタによって適切に設定する必要がある。 また今回は実装の都合上、tick_countをBSSに置いたためBSSの初期化なども行う必要があった。

今回の成果物はここに置いてある。

参考

*Cortex-M0+ Devices Generic User Guide

Raspberry Pi Pico Systickを使ってLチカを正確にする

はじめに

「PicoでベアメタルのLチカ(RAMバージョン) その2」として。

これまで作ってきたLチカは、適当な値をforでビジーループするだけだったので、タイミングはいい加減だった。

Cortex-M0+ではコアにSysTickを持っていて、これは難しい設定することもなく使用可能なので、これを使ってLチカのタイミングを正確にする。

SysTickはPico SDKの中では基本的には使用されていない。

SysTick

RP2040 Datasheetの「2.4.5.1.1. SysTick timer」によると、

The SysTick timer uses a 1μs pulse as a clock enable. This is generated in the watchdog block as timer_tick.

watchdogのtimer_tickで生成される1usのパルスを使用するとのこと。

SYST_CSRCLKSOURCEの初期値が0になっており外部のクロックを使用するとのこと。これがtimer_tickのことかと思われる。

f:id:mickey_happygolucky:20210226084257p:plain

f:id:mickey_happygolucky:20210226084308p:plain

WATCHDOGのTICKレジスタRUNNINGENABLEが動いているので、timer_tickの1usのパルスは初期状態で動作している。

f:id:mickey_happygolucky:20210226084323p:plain

f:id:mickey_happygolucky:20210226084334p:plain

Lチカ(RAMバージョン)を改造

Lチカ(RAMバージョン)をSysTickを使用するように変更してみる。

regs.h

regs.hにSYSTICK関連のレジスタ定義を追加する。

diff --git a/blink_ram/regs.h b/blink_ram/regs.h
index dc0b11b..2d650ec 100644
--- a/blink_ram/regs.h
+++ b/blink_ram/regs.h
@@ -30,5 +30,9 @@
 
 #define IO_GPIO25_CTRL (IO_BANK0_BASE + IO_BANK_GPIO25_CTRL)
 
+#define SYSTICK_CSR (0xE000E010)
+#define SYSTICK_RVR (0xE000E014)
+#define SYSTICK_CVR (0xE000E018)
+
 
 #endif //REGS_H

main.c

main.cinit_systick()delay_us()を追加する。

初期状態ではSysTickでは動作していないので、init_systick()で動かすようにする。その際CLKSOURCEは0に設定し、timer_tickを使用するようにする。

delay_us()では、RVRに任意のディレイ時間をus単位で設定する。次にCVRに0を設定しRVRにリセットされるまで待つ。 次に、CVRが0になるまで待つことで設定した時間の分だけビジーループする。

diff --git a/blink_ram/main.c b/blink_ram/main.c
index 47f4935..f4b4135 100644
--- a/blink_ram/main.c
+++ b/blink_ram/main.c
@@ -14,6 +14,22 @@ void write_reg_op(uint32_t addr, uint32_t value, uint32_t op) {
         write_reg(addr | op, value);
 }
 
+void init_systick() {
+        write_reg(SYSTICK_CSR, 0x1);
+}
+
+// us is up to max of 24bit (0xffffff)
+void delay_us(uint32_t us) {
+        write_reg(SYSTICK_RVR, us);
+        write_reg(SYSTICK_CVR, 0);
+
+        // wait for 0 to RVR
+        while (!read_reg(SYSTICK_CVR)) __asm__ __volatile__("");
+
+        // wait for RVR counting down to 0.
+        while (read_reg(SYSTICK_CVR)) __asm__ __volatile__("");
+}
+
 int main(void) {
 
         /////////////////////
@@ -39,10 +55,13 @@ int main(void) {
         write_reg(IO_GPIO25_CTRL, 0x5);
         uint32_t v = read_reg(IO_GPIO25_CTRL);
 
+        // SysTick init
+        init_systick();
+
         // Blink
         write_reg(SIO_GPIO_OE_SET, (1ul<<25));
         while (1) {
-                for (int i = 100000; i != 0; i--) ;
+                delay_us(100*1000); //100ms
                 write_reg(SIO_GPIO_OUT_XOR, (1ul<<25));
         }
         return 0;

波形を見る

f:id:mickey_happygolucky:20210226091039p:plain
Lチカ

ちゃんと100ms幅になっている。

まとめ

今回のような単純な使い方では、SysTickは24ビットしかないのであまり長い時間待たせられない。

例えば割り込みを使用してTickカウンタを作って、その差分で待つようにすれば24ビットの壁は超えられると思うが、ちゃんとしたタイマー処理を使用したい場合は、やはりタイマーを使用するほうが良いと思われる。

Raspberry Pi Pico J-LlinkでOpenOCD

はじめに

J-Link EDUが手元にあるので、OpenOCDでPicoのデバッグができないか試してみた。

接続

下記のように接続する。

Pico ARM-JTAG-SWD
SWDIO SWDIO(7)
SWDCLK SWDCLK(9)
GND GND(4,6,8,10,12,14,16,18,20)
VSYS VCC(1)

PlatformIOのJ-LINK

設定ファイル

${PICO_SDK_PATH}/../openocd/tcl/pico-jlink.cfgを下記の内容で作成する。

source [find interface/jlink.cfg]
transport select swd

source [find target/rp2040.cfg]
adapter speed 4000

アダプタをSWDモードに設定し、アダプタのスピードを設定する。

スピードを設定する際にadapter_khzを使うとadapter speedを使えと怒られる。

OpenOCDの実行(失敗)

下記のコマンドでOpenOCDを実行する。

$ cd ${PICO_SDK_PATH}/../openocd
$ src/openocd -f pico-jlink.cfg -s tcl

下記のようなエラーになる。

Open On-Chip Debugger 0.10.0+dev-geb22ace-dirty (2021-02-24-18:16)
Licensed under GNU GPL v2
For bug reports, read
    http://openocd.org/doc/doxygen/bugs.html
Info : Hardware thread awareness created
Info : Hardware thread awareness created
Info : RP2040 Flash Bank Command
DEPRECATED! use 'adapter speed' not 'adapter_khz'
adapter speed: 4000 kHz

Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : J-Link V10 compiled Oct 22 2019 16:28:15
Info : Hardware version: 10.10
Info : VTarget = 4.808 V
Info : clock speed 4000 kHz
Error: Sequence 4 not supported.
Info : DAP init failed


Error: Sequence 3 not supported.
Error: Sequence 4 not supported.

エラーの箇所

src/jtag/drivers/jlink.cの下記の関数でエラーになっている。

static int jlink_swd_switch_seq(enum swd_special_seq seq)
{
    const uint8_t *s;
    unsigned int s_len;

    switch (seq) {
        case LINE_RESET:
            LOG_DEBUG("SWD line reset");
            s = swd_seq_line_reset;
            s_len = swd_seq_line_reset_len;
            break;
        case JTAG_TO_SWD:
            LOG_DEBUG("JTAG-to-SWD");
            s = swd_seq_jtag_to_swd;
            s_len = swd_seq_jtag_to_swd_len;
            break;
        case SWD_TO_JTAG:
            LOG_DEBUG("SWD-to-JTAG");
            s = swd_seq_swd_to_jtag;
            s_len = swd_seq_swd_to_jtag_len;
            break;
        default:
            LOG_ERROR("Sequence %d not supported.", seq);
            return ERROR_FAIL;
    }

    jlink_queue_data_out(s, s_len);

    return ERROR_OK;
}

このseqが3、4の場合にエラーになっている。

enum swd_special_seq {
    LINE_RESET,
    JTAG_TO_SWD,
    SWD_TO_JTAG,
    SWD_TO_DORMANT,
    DORMANT_TO_SWD,
}

SWD_TO_DORMANTDORMANT_TO_SWDに未対応ということらしい。

調べてみると、既にPR(Rp2040 jlink #19)は出されている。

patchは2つあり、下記の修正はドンピシャの内容となっている。

From ef234fa045dfae30e4557db0867d4720f46bbbd3 Mon Sep 17 00:00:00 2001
From: Liam Fraser <liam@raspberrypi.com>
Date: Sun, 24 Jan 2021 08:59:02 +0000
Subject: [PATCH] Add DORMANT_TO_SWD and SWD_TO_DORMANT sequences to jlink
 driver

---
 src/jtag/drivers/jlink.c | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/jtag/drivers/jlink.c b/src/jtag/drivers/jlink.c
index ae8ce49c6..5b2c1ea89 100644
--- a/src/jtag/drivers/jlink.c
+++ b/src/jtag/drivers/jlink.c
@@ -2149,6 +2149,16 @@ static int jlink_swd_switch_seq(enum swd_special_seq seq)
            s = swd_seq_swd_to_jtag;
            s_len = swd_seq_swd_to_jtag_len;
            break;
+       case DORMANT_TO_SWD:
+           LOG_DEBUG("DORMANT-to-SWD");
+           s = swd_seq_dormant_to_swd;
+           s_len = swd_seq_dormant_to_swd_len;
+           break;
+       case SWD_TO_DORMANT:
+           LOG_DEBUG("SWD-to-DORMANT");
+           s = swd_seq_swd_to_dormant;
+           s_len = swd_seq_swd_to_dormant_len;
+           break;
        default:
            LOG_ERROR("Sequence %d not supported.", seq);
            return ERROR_FAIL;

もう一つの方はack待ちが不要なコマンドに対応するものらしい。

From 7e5ea1861a118120a78dac1fd812f1bdcaedc0cc Mon Sep 17 00:00:00 2001
From: graham sanderson <graham.sanderson@gmail.com>
Date: Mon, 25 Jan 2021 12:09:03 -0600
Subject: [PATCH] Add handling of no-ack commands to jlink driver

Change-Id: I4f7b0dad37bc68cde168962d86e53d7f5ea1cad7
---
 src/jtag/drivers/jlink.c | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/jtag/drivers/jlink.c b/src/jtag/drivers/jlink.c
index 5b2c1ea89..e2537b78f 100644
--- a/src/jtag/drivers/jlink.c
+++ b/src/jtag/drivers/jlink.c
@@ -2002,6 +2002,8 @@ struct pending_scan_result {
    void *buffer;
    /** Offset in the destination buffer */
    unsigned buffer_offset;
+   /** true if the command has nmo acknowledgement */
+   bool no_ack;
 };
 
 #define MAX_PENDING_SCAN_RESULTS 256
@@ -2195,7 +2197,9 @@ static int jlink_swd_run_queue(void)
    }
 
    for (i = 0; i < pending_scan_results_length; i++) {
-      int ack = buf_get_u32(tdo_buffer, pending_scan_results_buffer[i].first, 3);
+       int ack = pending_scan_results_buffer[i].no_ack ?
+               SWD_ACK_OK :
+               buf_get_u32(tdo_buffer, pending_scan_results_buffer[i].first, 3);
 
        if (ack != SWD_ACK_OK) {
            LOG_DEBUG("SWD ack not OK: %d %s", ack,
@@ -2259,6 +2263,9 @@ static void jlink_swd_queue_cmd(uint8_t cmd, uint32_t *dst, uint32_t data, uint3
 
        jlink_queue_data_out(data_parity_trn, 32 + 1);
    }
+   pending_scan_results_buffer[pending_scan_results_length].no_ack =
+           (0 == ((cmd ^ swd_cmd(false, false, DP_TARGETSEL)) &
+                         (SWD_CMD_APnDP|SWD_CMD_RnW|SWD_CMD_A32)));
 
    pending_scan_results_length++;

Checkに引っかかっておりマージされないらしい。

パッチの適用

ローカルのソースコードにこれらの変更を適用してみる。手元ではとりあえず手で修正した。

$ make -j $(nproc)

ビルドが通った。

OpenOCDの実行(成功)

再度実行してみる

$ cd ${PICO_SDK_PATH}/../openocd
$ src/openocd -f pico-jlink.cfg -s tcl
Open On-Chip Debugger 0.10.0+dev-geb22ace-dirty (2021-02-24-18:16)
Licensed under GNU GPL v2
For bug reports, read
    http://openocd.org/doc/doxygen/bugs.html
Info : Hardware thread awareness created
Info : Hardware thread awareness created
Info : RP2040 Flash Bank Command
DEPRECATED! use 'adapter speed' not 'adapter_khz'
adapter speed: 4000 kHz

Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : J-Link V10 compiled Oct 22 2019 16:28:15
Info : Hardware version: 10.10
Info : VTarget = 4.825 V
Info : clock speed 4000 kHz
Info : SWD DPIDR 0x0bc12477
Info : SWD DLPIDR 0x00000001
Info : SWD DPIDR 0x0bc12477
Info : SWD DLPIDR 0x10000001
Info : rp2040.core0: hardware has 4 breakpoints, 2 watchpoints
Info : rp2040.core1: hardware has 4 breakpoints, 2 watchpoints
Info : starting gdb server for rp2040.core0 on 3333
Info : Listening on port 3333 for gdb connections

問題なく起動した。

blinkを実行

pico-examplesのblinkを実行してみる。

OpenOCDとは別のターミナルで下記を実行する。

$ cd ${PICO_EXAMPLES_PATH}/build/blink
$ gdb-multiarch blink.elf
(gdb) target remote :3333
(gdb) mon reset init
(gdb) load
(gdb) c

LEDがチカチカした。

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

まとめ

J-Link対応のためのPRは既に出されているが 2021/2/24時点ではマージされていない。

修正内容自体は正しそうなので、自分でこれらの修正を取り込むか、PR用のブランチを使用すればJ-Link自体は使用できそう。

CIがしくじっているだけのような気もするので、もうすぐ取り込まれるかも。

Raspberry Pi Pico ARM-USB-TINY-H + ARM-JTAG-SWDでデバッグ

はじめに

ツイッターでRaspberryPi4が無いケースでOpenOCDでデバッグするにはどういう手段があるか。という話題が出た。

ARM-USB-TINY-H + ARM-JTAG-SWD

ARMのMCUデバッグで安価で使い勝手が良いものとしてはARM-USB-TINY-Hが挙げられる。

しかしこのデバイスはSWDには対応していない。JTAG-SWDは乗っかる信号線はTMSとTCLKだが、JTAGしか理解できないデバッガはTMSにSWDIOが送られてきても理解できない。

ARM-JTAG-SWDはSWDIOで扱う信号をJTAGのTMS、TDI、TDOに適宜振り分けるアダプタとなっている(はず)。

f:id:mickey_happygolucky:20210224070715p:plain
回路図

接続

下記のように接続する。

Pico ARM-JTAG-SWD
SWDIO SWDIO(7)
SWDCLK SWDCLK(9)
GND GND(4,6,8,10,12,14,16,18,20)
VSYS VCC(1)

PlatformIOのOlimex ARM-USB-TINY-H

デバッグ

OpenOCD

Pico SDKのOpenSDKを使用する。

${PICO_SDK_PATH}/../openocd/tcl/pico-jtag-swd.cfgを下記の内容で作成する。

source [find interface/ftdi/olimex-arm-usb-tiny-h.cfg]
source [find interface/ftdi/olimex-arm-jtag-swd.cfg]
source [find target/rp2040.cfg]
adapter speed 1000

クロック設定を忘れるとエラーになる。1000khzに得に根拠は無いので適宜最適値を探してほしい。

また、クロックを設定する際にadapter_khzを使うとadapter speedを使えと怒られる。

下記のコマンドで起動する。

$ cd ${PICO_SDK_PATH}/../openocd
$ src/openocd -f pico-jtag-swd.cfg -s tcl

blinkを実行

pico-examplesのblinkを実行してみる。

OpenOCDとは別のターミナルで下記を実行する。

$ cd ${PICO_EXAMPLES_PATH}/build/blink
$ gdb-multiarch blink.elf
(gdb) target remote :3333
(gdb) mon reset init
(gdb) load
(gdb) c

LEDがチカチカした。

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

まとめ

ARM-USB-TINY-HはARM-JTAG-SWDアダプタを使えばPicoのデバッグができる。

JLinkはまだ対応中のようだ

参考

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