STM32用リンカスクリプトを書く

出典: Wikimura

オブジェクトコードに具体的なアドレスを割り当てる際、実際に存在するメモリの情報と、どこに何を配置するかという情報を与えなくてはならない。 例えば、ベクタテーブルは決まった番地に配置しなくてはならず、プログラムはROMに書かれなくてはならず、変数はRAM上に確保されなくてはならない。 これらの情報をリンカに与えるためのファイルをリンカスクリプトという。

リンカスクリプトの書き方は参考文献[1][2]を参照されたい。


目次

リンカスクリプトを書くにあたって

リンカスクリプトを書くためには...

  • メモリの情報を記述するために、まずマイコンのメモリについて知る必要がある
  • エントリポイントを指定するために、スタートアップルーチンの名前を知る必要がある
  • ベクタテーブルを配置するために、決まった場所にセクションを配置する必要がある
  • スタートアップルーチンではCプログラムが動作する下準備を行うために、いくつかのシンボルを用意する必要がある
    • スタックポインタを設定するためのRAMの最後尾アドレス
    • 初期値付き静的変数の初期値代入のための、VMAとLMA
    • 初期値なし静的変数のゼロクリアのためのアドレス

メモリ配置

STM32-H103(P103も同様)で使用されているSTM32F103RBT6のメモリ配置は以下のようになっている。

名前 開始アドレス サイズ
Flash ROM 0x0800 0000 128kByte
RAM  0x2000 0000 20kByte

なお、Flash ROMブートモードでは、Flashの内容が「0x0000 0000」からもアクセスできるようになる。 そのため、先頭の「0x0800 0000 ~ 0x0800 0130」は、「0x0000 0000 ~ 0x0000 0130」に対応する。 これは割り込みベクタを配置するためのスペースなので、必ず先頭から割り込みベクタを配置するスクリプトを書く必要がある。

ブートモードでのFlash ROMアドレスのエイリアスについては、[4]の「2.4 Boot Configuration」を参照されたい。割り込みベクタについては、[4]の「8.1 Nested vectored interrupt controller (NVIC)」を参照されたい。


割り込みベクタのセクション

割り込みベクタのセクション名はプログラマが付ける。 STM32の場合、スタートアップルーチンが提供されている。 GCC用のCソースコードに、ベクタテーブルの構造体が定義されている。 このベクタテーブルは「.isr_vector」というセクションに配置するよう指定されている。 また、ベクタテーブル内のシンボルは、GCCのpragmaにより「シンボルが未定義ならばDefault_Handlerにする」と指定されている。

gcc用Medium-Density対応スタートアップファイル: startup_stm32f10x_md.c

__attribute__ ((section(".isr_vector")))
void (* const g_pfnVectors[])(void) =
{       
    &_estack,                   /* The initial stack pointer */
    Reset_Handler,              /* Reset Handler */
    NMI_Handler,                /* NMI Handler */
    HardFault_Handler,          /* Hard Fault Handler */
    ......

    USBWakeUp_IRQHandler,       /* USB Wakeup from suspend */  
    0,0,0,0,0,0,0,
    (void *)0xF108F85F          /* @0x108. This is for boot in RAM mode for 
                                   STM32F10x Medium Density devices. */
};

    ....

#pragma weak MMI_Handler = Default_Handler
#pragma weak MemManage_Handler = Default_Handler
#pragma weak BusFault_Handler = Default_Handler
    ....

エクスポートするシンボル

リンカスクリプトは、リンカが「オブジェクトコードを配置する際に決まる値」をシンボルとして定義できる。このシンボルを使うことで、メモリ配置に関する情報をCやアセンブリコードから利用することができるようになる。

特にスタートアップルーチンでは、Cプログラムが動作する下準備として、静的変数の初期化を行う。 (他にもROM上のプログラムをRAMに配置しなおすことで、高速化を図ることもある...ARMの場合ROMが16bitアクセスなので遅いため)

初期値のある静的変数の初期化では、ROM上にある初期値をRAMへコピーする必要がある。これを行うために、コピー元と、コピー先のアドレスがわからなくてはならない。これはリンカがシンボルをエクスポートすることで得られる。

STM32マイコンでは、以下に示すようなスタートアップルーチンが用意されている。

gcc用Medium-Density対応スタートアップファイル: startup_stm32f10x_md.c

void __Init_Data(void)
{
  unsigned long *pulSrc, *pulDest;

  /* Copy the data segment initializers from flash to SRAM */
  pulSrc = &_sidata;

  for(pulDest = &_sdata; pulDest < &_edata; )
  {
    *(pulDest++) = *(pulSrc++);
  }
  /* Zero fill the bss segment. */
  for(pulDest = &_sbss; pulDest < &_ebss; )
  {
    *(pulDest++) = 0;
  }
}

このファイルによれば、以下のシンボルが定義されなくてはならない。

  • _sidata: 初期値付き静的変数の初期値(ROM)の先頭アドレス
  • _sdata: 初期値付き静的変数の転送先(RAM)先頭アドレス
  • _edata: 初期値付き静的変数の転送先(RAM)終了アドレス
  • _sbss: 初期値なし(0クリアされる)静的変数の先頭アドレス
  • _ebss: 初期値なし静的変数の終了アドレス
  • _estack: スタックの開始点(ベクタテーブルに書かれている)

リンカスクリプトでは、これらのシンボルに適当なアドレスを入れてあげる必要がある。

ロード時アドレス(LMA)と実行時アドレス(VMA)

初期値付き静的変数は、ROMに書き込む時のアドレスと、実行時に参照するためのアドレスは異なる。 書き込み先のアドレスをLMA(Load Memory Address)、実行時のアドレスをVMA(Virtual Memory Address)という。詳しくは[2]の「3.1 基本的なリンカスクリプトの考え方」を参照されたい。

実際に書く

STM32F103RBT6用リンカスクリプトを書く。

MEMORY
{
    rom (rx)  : ORIGIN = 0x08000000, LENGTH = 128K
    ram (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}

SECTIONS
{
    .vector : {
        *(.isr_vector)  /* Vector table   */
    } > rom

    .text : {
        *(.text)        /* Program code   */
        *(.rodata)      /* Read only data */
    } > rom

    .data : {
        *(.data)        /* Data memory */
    } > ram AT > rom

    .bss : {
        *(.bss)         /* Zero-filled run time allocate data memory */
    } > ram

    _sidata = ABSOLUTE( LOADADDR( .data));
    _sdata  = ABSOLUTE( ADDR( .data));
    _edata  = _sdata + SIZEOF( .data);
    _sbss   = ABSOLUTE( ADDR( .bss));
    _ebss   = _sbss + SIZEOF( .bss);
    _estack = 0x02005000;
}

リンカスクリプトの読み方

メモリ領域romramを定義する。romは「0x0800 0000」番地から128kByteの領域を指す。 romは「0x2000 0000」番地から20kByteの領域を指す。

MEMORY
{
    rom (rx)  : ORIGIN = 0x08000000, LENGTH = 128K
    ram (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}


セクションの対応付けを行う。入力セクションをどのように出力セクションにまとめるかを記述する。

SECTIONS
{
    ....
}

出力セクション.vectorはメモリ領域romに配置する。 出力セクション.vectorには、全ての入力ファイル内の.isr_vectorセクションを配置する。 このセクションは普通1つしか作らないので、こういう書き方でOK。

    .vector : {
        *(.isr_vector)  /* Vector table   */
    } > rom

出力セクション.textはメモリ領域romに配置する。 出力セクション.textには、全ての入力ファイル内の.textセクションを配置し、その後に全ての入力ファイルの.rodataセクションを配置する。

    .text : {
        *(.text)        /* Program code   */
        *(.rodata)      /* Read only data */
    } > rom

出力セクション.dataは、ロード時に領域romに配置し、実行時はramに配置する。 出力セクション.dataには、全ての入力ファイル内の.dataセクションを配置する。 ちなみにこれは、初期値付き静的変数用のセクション。スタートアップルーチンでromにある初期値が転送される。

    .data : {
        *(.data)        /* Data memory */
    } > ram AT > rom

出力セクション.bssは、領域ramに配置する。 出力セクション.bssには、全ての入力ファイル内の.bssセクションを配置する。 ちなみにこれは、初期値なし静的変数用のセクション。スタートアップルーチンでゼロクリアされる。

    .bss : {
        *(.bss)         /* Zero-filled run time allocate data memory */
    } > ram

各種シンボルの定義。ABSOLUTEが付いていないと、セクションに対する相対アドレスになってしまうので注意。

  • _sidata: セクション.dataのLMA開始番地(つまり領域rom内の初期値本体)
  • _sdata: セクション.dataのVMA開始番地(つまり領域ram内の変数になる場所)
  • _edata: セクション.dataのVMA終了番地
  • _sbss: セクション.bssのVMA開始番地(つまり領域ram内の変数になる場所)
  • _ebss: セクション.bssのVMA終了番地
  • _estack: スタックの基底アドレス
    _sidata = ABSOLUTE( LOADADDR( .data));
    _sdata  = ABSOLUTE( ADDR( .data));
    _edata  = _sdata + SIZEOF( .data);
    _sbss   = ABSOLUTE( ADDR( .bss));
    _ebss   = _sbss + SIZEOF( .bss);
    _estack = 0x02005000;

参考文献

  1. GNUリンカLDの使い方
  2. リンカスクリプト
  3. STM32シリーズ関連ファイル
  4. STM32シリーズリファレンスマニュアルRM0008
個人用ツール