#はじめに
開発を進めるにあたって毎回フラッシュへの書き込みを行うのは面倒です。
やはり、簡単にお試しができる環境が必要です。
やはり、ブートローダを作ってバイナリをRAMに流し込むしかない。
#開発ターゲット
STM32F303K8
#ブートローダとは
マイコンに電源を入れると、プログラムが起動します。これは目的のプログラムがすぐに起動する場合もあれば、
「目的のプログラムを起動するためのプログラム」が最初に起動して、それが「目的のプログラム」を起動する場合もあります。
この「プログラムを起動するためのプログラム」が、「ブートローダ」と呼ばれるものです。
ブートローダをメインのプログラムと分けておく一番のメリットとしては、
頻繁に書き換える可能性があるプログラムと、一度書きこんだらその後変更しないプログラムを分離できることがあります。
プログラムをROMに書き込まないでネットワーク・ブートを行うことも可能です。
#作りたいもの
プログラムをROMに書き込まないですむ、ネットワークブートを行いたいのですが、このボードにはLAN端子がついていません。
そこで、シリアル通信でバイナリをボードに送り、RAMから起動するブートローダを作成します。
内容は組み込みOS自作入門を参考にしています。
#作る
##メモリ構成を考える
ブートローダを作成するにあたってはブートローダの場所、メインプログラムを保存する場所、
メインプログラムがコピーされる場所など配置を事前に考えておく必要があります。
使用しているメモリ上にメインプログラムをロードすると、ブートローダそのものが動かなくなるなど問題が発生するためです。
今回、メモリ構成は下記のようにしました。
方針としてメモリの内訳をブートローダ用とメインプログラム用に分離し、メモリを破壊しないようにしています。
柔軟性がなくなる気もしますが、簡単のためこの構成で進めます。
実際のメモリマップはSTM32F303K8のデータシートを参照してください。
それぞれの領域は現状のバイナリサイズから適当に指定しているため、今後の開発次第で考え直す必要があるかもしれません。
##リンカスクリプトを作る
設計したメモリ構成でバイナリが作成されるように、リンカスクリプトを書きます。
リンカスクリプトの詳細はSTM32 Nucleo Boardリンカスクリプトを参照してください。
今回はこのリンカスクリプトを元に作成していきます。
###ブートローダのリンカスクリプト
まずメモリ領域の設定をします。上記のメモリマップに従います。
/* Entry Point */
ENTRY(Reset_Handler)
:
/* Specify the memory areas */
MEMORY
{
RAM_BOOT (xrw) : ORIGIN = 0x20000000, LENGTH = 2K <-ブートローダで使用するRAM
RAM_CODE (xrw) : ORIGIN = 0x20000800, LENGTH = 6K <-メインプログラムのコードをコピーする領域
RAM_WORK (rw) : ORIGIN = 0x20002000, LENGTH = 4K <-メインプログラムで使用するRAM+Stack領域
CCMRAM (rw) : ORIGIN = 0x10000000, LENGTH = 4K <-CCMRAM領域
FLASH_BOOT (rx) : ORIGIN = 0x08000000, LENGTH = 8K <-ブートローダをおく領域
FLASH_MAIN (rx) : ORIGIN = 0x08002000, LENGTH = 56K<-メインプログラムを保存しておく領域
}
次に各セクションの配置を設定します。ブートローダ用に確保した領域になるようにします。
/* Define output sections */
SECTIONS
{
/* The startup code goes first into FLASH */
.isr_vector :
{
: 省略
} >FLASH_BOOT
/* The program code and other data goes into FLASH */
.text :
{
: 省略
_etext = .; /* define a global symbols at end of code */
} >FLASH_BOOT
/* Constant data goes into FLASH */
.rodata :
{
: 省略
} >FLASH_BOOT
: 省略
/* Initialized data sections goes into RAM, load LMA copy after code */
.data :
{
: 省略
} >RAM_BOOT AT> FLASH_BOOT
/* CCM-RAM section */
.ccmram :
{
: 省略
} >CCMRAM AT> FLASH_BOOT
/* Uninitialized data section */
. = ALIGN(4);
.bss :
{
: 省略
} >RAM_BOOT
/* User_heap_stack section, used to check that there is enough RAM left */
._user_heap_stack :
{
: 省略
} >RAM_WORK
/* buffer section */
. = ALIGN(4);
.buffer :
{
_buffer_start = .; /* define a global symbol at buffer start */
} >RAM_CODE
_MAIN_CODE_ADDR = ADDR(.buffer);
}
実際には他にもいろいろ書いてあるのですが、簡単にまとめると
/* Entry Point */
ENTRY(main)
:
/* Specify the memory areas */
MEMORY
{
RAM_BOOT (xrw) : ORIGIN = 0x20000000, LENGTH = 2K
RAM_CODE (xr) : ORIGIN = 0x20000800, LENGTH = 6K
RAM_WORK (rw) : ORIGIN = 0x20002000, LENGTH = 4K
CCMRAM (rw) : ORIGIN = 0x10000000, LENGTH = 4K
FLASH_BOOT (rx) : ORIGIN = 0x08000000, LENGTH = 8K
FLASH_MAIN (rx) : ORIGIN = 0x08002000, LENGTH = 56K
}
各セクションはRAM_CODE/RAM_WORKに配置されるようにしておきます。
これにより、メモリの0x20000800番地にメインプログラムのバイナリをロードすれば起動できるはずです。
/* Define output sections */
SECTIONS
{
/* Entry Point */
.entry_point :
{
. = ALIGN(4);
} >RAM_CODE
.text :
{
: 省略
} >RAM_CODE
/* Constant data goes into FLASH */
.rodata :
{
: 省略
} >RAM_CODE
: 省略
.data :
{
: 省略
} >RAM_WORK
_siccmram = LOADADDR(.ccmram);
/* CCM-RAM section */
.ccmram :
{
: 省略
} >CCMRAM
/* Uninitialized data section */
. = ALIGN(4);
.bss :
{
: 省略
} >RAM_WORK
/* User_heap_stack section, used to check that there is enough RAM left */
._user_heap_stack :
{
: 省略
} >RAM_WORK
.ARM.attributes 0 : { *(.ARM.attributes) }
}
こちらも簡単にまとめると
セクション | 位置 |
---|---|
.text | RAM_CODE |
.rodata | RAM_CODE |
.data | RAM_WORK |
.bss | RAM_WORK |
となります。 | |
.textセクションの前に |
.entry_point :
{
. = ALIGN(4);
} >RAM_CODE
で.entry_pointセクションを定義しています。これはエントリポイントをRAM_CODE領域の先頭に配置するために作成しています。
後述しますが、ソースコードでmain関数が.entry_pointに配置されるように設定します。
ここでメインプログラムのELFを確認してみます。
実際に指定したアドレスに配置されています。
##ロード方針
ELFはサイズが大きいため今回のメモリ構成ではロードできません。そこでELFをobjcopyでバイナリに変換します。
バイナリにするとメモリ内の最初のロード領域の開始アドレスがベースアドレスとして使用されます。
ロード領域間はパディングが挿入され、正しい相対オフセットになります。
そのため、作成したバイナリはアドレス00000000hが.entry_pointということになります。
他の領域の相対位置は変わらないため、下記のようにRAM_CODEにバイナリをそのままロードすればOKのはずです。
##プログラムを作る
###ブートローダ
ソースコードはこちら。
過去に作成したスタートアップルーチン、printfを使用しています。
以下がブートローダの処理です。
メインプログラムを起動するときは、エントリポイントのアドレスにある関数を呼び出す形になります。
#include "usart_driver.h"
#include "printf.h"
#include "lib.h"
#include "xmodem.h"
extern void _MAIN_CODE_ADDR(); // リンカスクリプトで定義したエントリポイントのアドレス
int main(void)
{
UsartInit();
static char buf[16];
static long size = -1;
static unsigned char *loadbuf = NULL;
extern int _buffer_start; // リンカスクリプトで定義したバイナリ転送先アドレス
printf("zloader started.\n");
while(1){
printf("zload> ");
gets(buf);
if(!strcmp(buf, "load")){ // ロード処理
loadbuf = (char*)(&_buffer_start);
size = XmodemRecv(loadbuf);
wait();
if(size < 0){
printf("\nXMODEM receive error!\n");
}else{
printf("\nXMODEM receive succeeded.\n");
}
}else if(!strcmp(buf, "dump")){ // バイナリのダンプ
printf("size: %d\n", size);
dump(loadbuf, size);
}else if(!strcmp(buf, "run")){ // 起動処理
_MAIN_CODE_ADDR();
}else{
printf("unknown.\n");
}
}
return 0;
}
以下の機能があります。
・xmodemでRAMにバイナリを転送する
・RAMに転送されたバイナリをダンプする
・プログラムを起動する
###メインプログラム
ソースコードはこちら。
「Main Program booted!!」が表示されるだけのコードです。
main関数の後に__attribute__ ((section (".entry_point")))
をつけて、.entry_pointセクションへの配置を指定しています。
ARMコンパイラではこのように指定できるようです。ほかに#pragma
を使う方法もあるようです。
#include "printf.h"
int main(void)
__attribute__ ((section (".entry_point"))); // mainを.entry_pointに配置する
// Main -----------------------------------------------------------------------
int main(void)
{
printf("Main Program booted!!\n");
while(1);
return 0;
}
初期設定はブートローダで行っているため何もしません。本当はBSS領域の初期化が必要です。
#テスト
まず、ブートローダを焼きこんで電源を入れます。ブートローダが起動しました。
loadでメインプログラムのバイナリを転送します。転送プロトコルはxmodemです。
#おわりに
ブートローダを作るにあたってELFについて色々と知ることができました。
これで楽に試行錯誤が出来そうです。
#参考書籍
12ステップで作る組み込みOS自作入門