Arduino UNO R4 Minima 用のブートローダーのソースコードを読む ( アプリケーション起動編 )
1. はじめに
前回の記事では、Arduino UNO R4 Minima (以下、Minima) 用ブートローダーのビルド手順の一例を記載しました。今回の記事は、このブートローダーがアプリケーション (スケッチ) を起動するまでの流れについて確認したときのメモです。
2. ソースコードを読む前に
2.1. makeログの作成
今回の記事は、前回の記事で作成したビルド環境が手元にあると読みやすいかと思います。また、どのファイルをコンパイルしたのかを把握しやすくするために、下記コマンドによってブートローダーの再ビルドをおこない、makeのログを残すと良いかもしれません。
cd ~/arduino-r4-bootloader/arduino-renesas-bootloader/
rm -rf ./_build
TINYUSB_ROOT=$PWD/../tinyusb make -f Makefile.minima --debug=v --print-data-base >& build.log
2.2. ブートローダー自体の起動について
ブートローダーのソースコードを読む前に、マイコンが内蔵フラッシュに書き込まれたプログラム (今回はブートローダー) を起動する流れについて確認します。
Arduino UNO R4 には、ルネサス エレクトロニクス社の RA4M1 MCU グループに属するマイコン「R7FA4M1AB3CFM#AA0」が搭載されています。このマイコンは Cortex-M4 コアを採用し、256 kB の内蔵フラッシュメモリを備えています[1]。
このマイコンには、大きく分けて2種類の起動モードがあります。ひとつは内蔵フラッシュに書き込まれたプログラムを起動する「シングルチップモード」、もうひとつは内蔵フラッシュのプログラムを書き換える「SCI/USBブートモード」です。[2]。ブートローダーの起動では「シングルチップモード」が使用されます。一方の「SCI/USBブートモード」ですが、これは前回の記事でブートローダーを書き込んだ際に使用しました。
シングルチップモードで起動するプログラムは、Cortex-M4 コアが起動できる構造で作成する必要があります。Cortex-M4 は Armv7E-M アーキテクチャですが、この場合、プログラムのイメージにはその開始アドレス (今回の場合は内蔵フラッシュの先頭である 0000 0000h [3]) にベクタテーブルを配置する必要があります。
ベクタテーブルには以下の情報が格納されています[4] :
- 先頭の4バイト: 初期スタックポインタ
- 次の4バイト : 初期プログラムカウンタ (リセットベクタ)
Armv7E-M アーキテクチャでは、リセット時の処理の中で下記の操作が行われます[5] :
- ベクタテーブルから初期スタックポインタを読み取り、その値をメインスタックポインタ (MSP) に設定する。
- ベクタテーブルからリセットベクタを読み取り、その値をプログラムカウンタ (PC) に設定する。
3. リンカスクリプト
前述の通り、シングルチップモードで起動するプログラムは、今回のケースではベクタテーブルを内蔵フラッシュの先頭に配置する必要があります。ベクタテーブルだけでなく、変数や関数を特定の場所 (アドレス) に配置する処理はリンク処理で行われ、通常はリンカスクリプトを使用します。そのため、まずリンカスクリプトを確認し、ベクタテーブルがどのように配置されているかを調べます。
その前に、リンカスクリプトについて簡単に記述します。C言語のソースコードをコンパイルすると、プログラムは例えば以下のようなセクションに分類されます:
-
.text
セクション:関数やプログラムの実行コード。 -
.data
セクション:初期値が設定されたグローバル変数や静的変数。 -
.bss
セクション:初期値が設定されていないグローバル変数や静的変数。 -
.rodata
セクション:定数や読み取り専用のデータ。
また、特定のデータを独自のセクションに分類することも可能です。例として、以下のコードを GCC でコンパイルすると、app_ver
という定数が .app_ver
というセクションに分類されます:
const uint32_t app_ver __attribute__((section(".app_ver"))) = 0x00010000;
これらのセクションがメモリ上のどの位置に配置されるかは、リンク時にリンカが決定します。その際に使用されるのがリンカスクリプトです。リンカスクリプトは、各セクションを具体的にメモリのどこに配置するかを定義します。
前回の記事で作成したビルド環境では、リンカスクリプトは下記の場所にあります。
~/arduino-r4-bootloader/tinyusb/hw/bsp/ra/linker/gcc/fsp.ld
~/arduino-r4-bootloader/tinyusb/hw/bsp/ra/linker/gcc/ra4m1.ld
(以降、パスの先頭部分「~/arduino-r4-bootloader/」は省略します)
fsp.ld では、FLASH_START
や FLASH_LENGTH
といった定数を利用して、メモリ領域 (例えば FLASH
や RAM
など) の配置を抽象的に指定しています。一方、ra4m1.ld ではこれらの定数に具体的な値を割り当てています。たとえば、FLASH_START = 0x00000000
、FLASH_LENGTH = 0x40000
という設定です。
リンカスクリプトでは、MEMORY ブロックでプログラムが使用するメモリ領域を定義します。
内蔵フラッシュはFLASH
として定義しています。ORIGIN
は開始アドレス、LENGTH
はメモリのサイズ (ここではバイト単位) です。ORIGIN = FLASH_ORIGIN + NS_IMAGE_OFFSET
、LENGTH = LIMITED_FLASH_LENGTH
となっていますが、今回ビルドしたブートローダーではORIGIN = 0
、LENGTH = 0x40000
となります。
ここから下に読み進めていくと、SECTIONS ブロックが出てきます。このブロック内で、.text
セクションをFLASH
の先頭に配置する設定が記述されています。
さらに、この.text
セクションの先頭にKEEP(*(.fixed_vector*))
とKEEP(*(.application_vector*))
といった記述があります。この*
はワイルドカードで、一番目の*
は任意のファイル名 (例えばstartup.cをコンパイルしてできたstartup.o) を表し、二つ目は任意の文字列を表します。もし、startup.oの中に.fixed_vector123というセクションに分類されたデータがある場合、そのデータはKEEP(*(.fixed_vector*))
の場所に保持されます。
ここでベクタテーブルの構造を確認します。ベクタテーブルは、Renesas のマニュアルに記載されています[6] 。今回のソースコードでは、表中の例外番号が0-15 (発生元が Arm) であるベクタテーブルと、16番以降のベクタテーブルを別々に定義しています。これらはそれぞれ、.fixed_vector
と.application_vector
に配置されています。初期プログラムカウンタであるリセットベクタはベクタテーブルの先頭から2番目 (例外番号1) に登録されます。そのため、次は.fixed_vector
セクションに配置しているベクタテーブルの定義を探します。
4. ベクタテーブル
.fixed_vector
に配置するベクタテーブルは下記の場所で定義されています。
tinyusb/hw/mcu/renesas/fsp/ra/fsp/src/bsp/cmsis/Device/RENESAS/Source/startup.c
BSP_PLACE_IN_SECTION(BSP_SECTION_FIXED_VECTORS)
で__Vector
を.fixed_vector
に配置しています。この配列の先頭から二番目に登録されたReset_Handlerがリセットベクタです。なお、これらのマクロの定義はここにあります。
tinyusb/hw/mcu/renesas/fsp/ra/fsp/src/bsp/mcu/all/bsp_compiler_support.h
5. Reset_Handler
Reset_Handler
もstartup.c内に定義があります。
6. SystemInit
SystemInit
関数は、システムが正しく動作するための初期設定を行います。具体的には、.bss や .data セクションの変数初期化、クロック設定、MPU(メモリ保護ユニット)の設定、FPU(浮動小数点演算ユニット)の有効化などが含まれます。ただ、これらの詳細はブートローダーがアプリケーションを起動する流れを確認した後でも問題ないかと思います。そのため、SystemInit関数内の処理は別の記事に記述し、本記事では割愛いたします。
7. main
main
関数は arduino-renesas-bootloader のソースコードで定義されています。
arduino-renesas-bootloader/src/main.c
7.1. board_init
まず、179行目のboard_init関数ですが、この定義は tinyusb に戻り下記の場所にあります。
tinyusb/hw/bsp/ra/family.c
__enable_irq()
による割り込みの有効化とR_IOPORT_Open(&port_ctrl, &family_pin_cfg);
でピンの機能設定を行っています。family_pin_cfg
は board.h で定義されています。
tinyusb/hw/bsp/ra/linker/gcc/board.h
41行目は、Minima ではシルクでL
と書かれた LED 用の設定です。43-45行目は USB のピン設定となります。42行目では D12 を入力ピンに設定してますが、このブートローダーでは使用していないようです。
board_init
関数に戻ります。定数TRACE_ETM
は未定義のため108-116行目はコードから除外されます。
また、定数CFG_TUSB_OS
はarduino-renesas-bootloader/src/tusb_config.hでOPT_OS_NONE
として定義されています。
そのため、board_init
関数の残りのコードは、127行目のSysTick_Config
関数と、130行目のboard_led_write
関数の実行処理となります。後者は LED の消灯処理です。前者のSysTick_Config
関数は tinyusb/lib/CMSIS_5/CMSIS/Core/Include/core_cm4.h にあります。
SysTickについてはRenesasおよびArmのマニュアルに記載されています[7],[8]。
CLKSOURCE
ビットを1としているため、クロックソースはprocessor clock (ICLK
) が指定されています。ICLK
の動作周波数の値は、SystemInit
関数内でSystemCoreClock
変数に格納されています。
SysTick->LOAD
(SYST_RVR
レジスタのRELOAD
フィールド) に(SystemCoreClock / 1000) - 1
を設定することで、SysTick 割り込みの周期を1ミリ秒に設定しています [9] 。なお、SysTick 割り込みハンドラは下記の場所で定義されています。
tinyusb/hw/bsp/ra/family.c
1ミリ秒ごとに変数system_ticks
をカウントアップしています。この値はboard_millis
関数で取得できます。このあと、500ミリ秒待機する処理が出てくるのですが、そこでこの関数が使われています。
7.2. ブートローダーモードについて
main
関数に戻ります。board_init
関数の実行後、ブートローダーモードに入るか、アプリケーションを起動するかを選択する分岐処理が行われます。ブートローダーモードは、USB 通信ができなくなるようなアプリケーションを書き込んでしまった場合に、システムを復旧する際に役立ちます。このモードは Minima の場合、電源投入後にリセットボタンをダブルタップすることで起動できます[10]。このモードにすることで、アプリケーションを起動せずにプログラムを書き換えることができます。なお、本記事では少し主題を広げ、このブートローダーモードの起動手順についても触れたいと思います。
まず、下記のif文を確認します。
このBOOT_DOUBLE_TAP_DATA
の定義は下記のようになっています。
このVBTBKR
レジスタはバックアップレジスタで、VCC 端子または VBATT 端子から電源が供給されている限り、リセットボタンを押してもデータは保持されます[11] [12] [13]。
このブートローダーでは、バックアップレジスタの値を読みブートローダーモードに入るかを判断します。
ブートローダーモードの処理はmain
関数内のbootloader
ラベル以降に実装されています。このif文内に入るとbootloader
ラベルに飛んでブートローダーモードに入りますが、電源投入時はバックアップレジスタにマジック値が書かれていないため、この条件には入りません。
次のif文を確認します。
if文の条件式に書かれているR_SYSTEM->RSTSR0_b.PORF
(パワーオンリセット検出フラグ) は、パワーオンリセット時 (リセットボタンを押さずに電源を投入したとき) にセットされます[14]。
このフラグは、RES端子リセット (リセットボタンを押したとき) によって値がクリアされます[15] [16] [17]。
if文の条件式は!R_SYSTEM->RSTSR0_b.PORF
ですので、パワーオンリセット検出フラグがクリアされている場合 (リセットボタンを押してブートローダーを起動したとき) に成立します。このif文に入るとバックアップレジスタにマジック値を書き込みます。なお、バックアップレジスタはプロテクトレジスタによって書き込みの許可・禁止を切り替えられるため[18]、バックアップレジスタへの書き込み処理の前後でプロテクトレジスタの操作を行っています。
次につづくmain.cの190-195行目のコードは Minima の場合は除外されます[19]。そのため、196行目以降を確認します。
500ミリ秒間待機したあとにバックアップレジスタの値を0クリアする処理があります。
待機中にリセットボタンを押すとバックアップレジスタのデータは保持したままブートローダーが再起動されます。このとき、バックアップレジスタにマジック値がかかれていると、182行目のif文内の処理に入り、ブートローダーモードに入ります。
電源投入後にリセットボタンをダブルタップしたときの動作を以下にまとめます:
- 電源を投入してブートローダーを起動したときは、バックアップレジスタにマジック値は書かれていなため、182行目のif文には入らない。また、パワーオンリセット検出フラグがセットされているため、187行目のif文にも入らない。
- リセットボタンを押してブートローダーを再起動すると、パワーオンリセット検出フラグがクリアされるため、187行目のif文に入りバックアップレジスタにマジック値を書き込む。
- 後続の500ミリ秒待機処理中に再びリセットボタンを押すと、バックアップレジスタにマジック値が書かれた状態でブートローダーが再起動するため、182行目のif文に入りブートローダーモードに入る。
500ミリ秒待機中にリセットボタンを押さない場合、バックアップレジスタの値をゼロクリアした後、アプリケーションの起動処理に移ります。
なお、このブートローダーモードはリセットボタンのダブルタップ以外の方法でも起動可能です。このモードは、Arduino IDE でスケッチ (アプリケーション) を書き換える際にも利用されています。具体的には、Arduino IDE でスケッチの書き込みを実行すると、USB通信経由で要求を受け取ったアプリケーションがバックアップレジスタにマジック値を書き込み、その後再起動してブートローダーモードを起動します。
7.3. アプリケーションイメージのチェック
アプリケーションの起動処理を行う前に、アプリケーションイメージの構造をチェックする処理があります。
アプリケーションプログラムは、内蔵フラッシュの先頭からSKETCH_FLASH_OFFSET
バイト分離れた位置以降に書き込まれます。この値は下記の場所で定義されています。
arduino-renesas-bootloader/src/flash.h
今回はBSP_FEATURE_FLASH_LP_VERSIONは3である (0ではない) ため[20]、SKETCH_FLASH_OFFSET
は7行目か9行目の値となります。また、DFU_LOADER
はMakefile.minimaで-DDFU_LOADER
としてCFLAGSに追加していますが、BOSSA_LOADER
については未定義です。そのため、SKETCH_FLASH_OFFSET
の値は9行目の(16 * 1024) = 0x4000
になります。
アプリケーションプログラムのイメージも、ブートローダーと同様にイメージの先頭にベクタテーブルを配置するように作成されています。実際に、Blink を Minima 向けにビルドした際に生成された map ファイルを確認したところ、下図の通りでした。
そのため、イメージの先頭4バイトには、アプリケーションプログラムの初期スタックポインタが格納されています。main.c の201行目は、この初期スタックポインタの値を簡単にチェックする処理かと思います。具体的には、スタックポインタが内蔵 SRAM 内に配置されていることを確認しているのだと思います。RA4M1 グループの場合、内蔵 SRAM はアドレス範囲0x20000000
から0x20008000
に割り当てられています[21]。そのため、SRAM 上のデータのアドレスに0xFF000000
でアンドをとると0x20000000
になります。
この条件式が成立する場合は、アプリケーションイメージの先頭にベクタテーブルが正しく配置されていると判断して、アプリケーションの起動処理であるboot5
関数の実行に進みます。成立しない場合は、正常なアプリケーションが書き込まれていないと判断し、ブートローダーモードに移行するようにしているのだと思います。
7.4. アプリケーションの起動
最後にboot5
関数の内容を確認します。
まずR_BSP_MODULE_STOP
マクロによって、USBFSに対してモジュールストップを実行しています。
tinyusb/hw/mcu/renesas/fsp/ra/fsp/src/bsp/mcu/all/bsp_module_stop.h
ただ、USBFSはリセット時はモジュールストップ状態であり[22]、ここまでの処理のながれではモジュール状態を解除する処理は見当たりませんでした。もしかするとこの処理は保険的な意味合いが強いかもしれません[23]。また USBFS 以外にも USBHS についてもモジュールストップ処理が実行されていますが、RA4M1 シリーズのマイコンには USBHS は非搭載です。そのため MSTPCRB レジスタの対応フィールドは予約扱いとなっています。ただ、この予約フィールドに1を書き込むことは問題ないようです[24]。
次の処理を確認します。ここでは SystemInit
関数内で設定した MPU によるスタックポインタの監視処理を無効化しています。
Minimaで使用しているマイコンには TrustZone は非搭載のため、無効化処理は96行目で行われます。R_MPU_SPMON
は下記のヘッダに定義されています。
tinyusb/hw/mcu/renesas/fsp/ra/fsp/src/bsp/cmsis/Device/RENESAS/Include/R7FA4M1AB.h
最後の処理を確認します。
__disable_irq()
によって割り込みを禁止した後、__DSB()
と __ISB()
がこの順番で実行されています。__DSB()
は、この命令より前に行われたメモリやレジスタへの書き込み操作が完了するまで待機する処理です[25]。一方、__ISB()
はプロセッサのパイプラインをフラッシュする命令です[26]。
割り込み禁止後に、Systick割り込みの無効化とベクタテーブルの変更とアプリケーションの起動処理が続くため、ブートローダー側での処理の影響を極力なくした状態でアプリケーションの起動処理を行う意図で__DSB()
と __ISB()
が記述されているのかもしれません。
Systick割り込みの無効化を行った後は、参照するベクタテーブルをアプリケーション側のベクタテーブルに変更する処理が続きます。参照するベクタテーブルの変更は、VTOR レジスタに設定することで行えます[27]。
アプリケーションのベクタテーブルはイメージの先頭アドレス (SKETCH_FLASH_OFFSET
) に配置されているため、VTOR にこのアドレスを登録しています。そして、アプリケーション側のベクタテーブルにあるスタックポインタの初期値をロードして、メインスタックポインタに設定します。最後に、ベクタテーブルの先頭から4バイト離れた場所に格納されているリセットベクタへのアドレスをロードし、戻り値引数なしの関数として実行することでアプリケーションを起動しています。
なお、アーキテクチャは異なりますが、TrustZone を搭載した Armv8-M の資料によると、VTOR (ベクタテーブルオフセットレジスタ) の値を変更した後に、__DSB()
と __ISB()
を実行することを推奨しているようです[28]。新しいベクタテーブルが VTOR に書き込まれた状態でアプリケーションを起動することを保証したい場合は、VTOR 変更後にも __DSB() と __ISB() を記述しても良いのかもしれません。
-
Arduino "Arduino® UNO R4 Minima Product Reference Manual SKU: ABX00080", p1, "Description" ↩︎
-
Renesas Electronics Corporation, "Renesas RA4M1グループ ユーザーズマニュアル ハードウェア編 Rev.1.10", 2023.09, p.92, "3. 動作モード" ↩︎
-
Renesas Electronics Corporation, "Renesas RA4M1グループ ユーザーズマニュアル ハードウェア編 Rev.1.10", 2023.09, p.94, "図 4.1 メモリマップ" ↩︎
-
Renesas Electronics Corporation, "Renesas RA4M1グループ ユーザーズマニュアル ハードウェア編 Rev.1.10", 2023.09, p.280, "表13.3 割り込みベクタテーブル" ↩︎
-
Arm "ARMv7-M Architecture Reference Manual Version: E.e", p.B1-530, "B1.5.5 Reset behavior" ↩︎
-
Renesas Electronics Corporation, "Renesas RA4M1グループ ユーザーズマニュアル ハードウェア編 Rev.1.10", 2023.09, p.280, "表13.3 割り込みベクタテーブル" ↩︎
-
Renesas Electronics Corporation, "Renesas RA4M1グループ ユーザーズマニュアル ハードウェア編 Rev.1.10", 2023.09, p.87, "2.9 SysTickシステムタイマ" ↩︎
-
Arm "ARMv7-M Architecture Reference Manual Version: E.e", p.B3-621, "B3.3.3 SysTick Control and Status Register, SYST_CSR", ソースコード内の
SysTick->CTRL
はSYST_CSR
に対応します ↩︎ -
1秒間に
SystemCoreClock
回のクロックが発生するため、SystemCoreClock / 1000
回のカウントダウンに要する時間は1ミリ秒。 ↩︎ -
Arduino "Arduino® UNO R4 Minima Product Reference Manual SKU: ABX00080", p.17, "12.5 Board Recovery" ↩︎
-
Renesas Electronics Corporation, "Renesas RA4M1グループ ユーザーズマニュアル ハードウェア編 Rev.1.10", 2023.09, p.237, "11.1.5 バックアップレジスタ" ↩︎
-
Renesas Electronics Corporation, "Renesas RA4M1グループ ユーザーズマニュアル ハードウェア編 Rev.1.10", 2023.09, p.243, "11.2.6 VBATT バックアップレジスタ (VBTBKRn) (n = 0 ~ 511)" ↩︎
-
Renesas Electronics Corporation, "Renesas RA4M1グループ ユーザーズマニュアル ハードウェア編 Rev.1.10", 2023.09, p.256, "11.3.4 VBATT バックアップレジスタの使用法" ↩︎
-
Renesas Electronics Corporation, "Renesas RA4M1グループ ユーザーズマニュアル ハードウェア編 Rev.1.10", 2023.09, p.105, "5.3.2 パワーオンリセット" ↩︎
-
Renesas Electronics Corporation, "Renesas RA4M1グループ ユーザーズマニュアル ハードウェア編 Rev.1.10", 2023.09, p.95, "表 5.1 リセットの名称と要因" ↩︎
-
Renesas Electronics Corporation, "Renesas RA4M1グループ ユーザーズマニュアル ハードウェア編 Rev.1.10", 2023.09, p.96, "表 5.2 リセット要因ごとの初期化対象リセット検出フラグ" ↩︎
-
Renesas Electronics Corporation, "Renesas RA4M1グループ ユーザーズマニュアル ハードウェア編 Rev.1.10", 2023.09, p.99, "5.2.1 リセットステータスレジスタ 0(RSTSR0)" ↩︎
-
Renesas Electronics Corporation, "Renesas RA4M1グループ ユーザーズマニュアル ハードウェア編 Rev.1.10", 2023.09, p.262, "12.2.1 プロテクトレジスタ(PRCR)" ↩︎
-
RENESAS_CORTEX_M23
は未定義です。またTURN_OFF_CHARGER_LED
はarduino-renesas-bootloader/src/flash.hよりBSP_FEATURE_FLASH_HP_VERSION
が0以外のときに定義されますが、この定数はtinyusb/hw/mcu/renesas/fsp/ra/fsp/src/bsp/mcu/ra4m1/bsp_feature.hで0となっています。 ↩︎ -
BSP_FEATURE_FLASH_LP_VERSIONの定義は以下の場所にあります:
tinyusb/hw/mcu/renesas/fsp/ra/fsp/src/bsp/mcu/ra4m1/bsp_feature.h ↩︎ -
Renesas Electronics Corporation, "Renesas RA4M1グループ ユーザーズマニュアル ハードウェア編 Rev.1.10", 2023.09, p.94, "図 4.1 メモリマップ" ↩︎
-
Renesas Electronics Corporation, "Renesas RA4M1グループ ユーザーズマニュアル ハードウェア編 Rev.1.10", 2023.09, p.821, "27.4.1 モジュールストップ状態の設定" ↩︎
-
ソースコードを見ると、USBFS のモジュールストップ状態は、ブートローダーモード内で実行される
tud_init
関数内のdcd_init
関数を通じて、rusb2_module_start
関数で解除されています。
tud_init
: tinyusb/src/device/usbd.c
dcd_init
: tinyusb/src/portable/renesas/rusb2/dcd_rusb2.c
rusb2_module_start
: tinyusb/src/portable/renesas/rusb2/rusb2_ra.h
そのため、ブートローダーモードに入った後にboot5
関数を呼ぶ場合は、このモジュールストップ処理が必要になると思います。実際、main.c を見ると、以前はブートローダーモード中にboot5
関数を実行することを検討していた形跡がありました。ただ、現在はコメントアウトされています。 ↩︎ -
Renesas Electronics Corporation, "Renesas RA4M1グループ ユーザーズマニュアル ハードウェア編 Rev.1.10", 2023.09, p.201, "10.2.3 モジュールストップコントロールレジスタ B(MSTPCRB)" ↩︎
-
Arm "ARMv7-M Architecture Reference Manual Version: E.e", p.A3-94, "Data Synchronization Barrier (DSB)" ↩︎
-
Arm "ARMv7-M Architecture Reference Manual Version: E.e", p.A3-95, "Instruction Synchronization Barrier (ISB)" ↩︎
-
Arm "ARMv7-M Architecture Reference Manual Version: E.e", p.B1-525, "B1.5.3 The vector table" ↩︎
-
Arm "Armv8-M Memory Model and Memory Protection User Guide Version 1.1", p.17, "2.3.4 When do you need a DSB followed by an ISB?" ↩︎
Discussion