« GCCでmbedの実行ファイルをコンパイル | トップページ | mbed + GCCでprintfを使う »

GCCでmbedの実行ファイルをコンパイル(2)

前回に引き続き、GCC + mbedネタです。前回、外部変数のSystemCoreClockが正しく初期化されないと書きましたが、その原因が判明しました。変数領域の初期化やリンカーの動作に関わる部分でなかなかディープな世界でした。リンカーの動作やリンカースクリプトの書式はまだまだブラックボックス(先人の成果を拝借している)が多いのですが、分かったことを記載します。

メモリー領域の種別

先ずはCortex-M3(その他のMCUでも同様)のメモリ配置について記載します。コンパイラでC等のソースをコンパイルすると、コードの他に静的な変数を格納する領域が必要になります。組み込みの場合、コードはFlash ROMに配置し、静的変数はRAMに配置する必要があります。ソースを複数モジュールに分割した場合、モジュール単位にコードや変数領域が生成されますが、リンカーはメモリー領域の種別単位に並び替えを行ってプログラムやデーターのメモリ割付を行います。

リンカーが識別するメモリー領域(セクションと呼ばれる)には以下の種別があります。

  • .text: ROMに配置するプログラムコード
  • .data: 初期値付きの静的変数
  • .bss: 初期化を行わない変数(値を代入しない外部変数とか)
  • .heap: mallocで動的に確保するデーター領域
  • .stack: スタック

次に、データーの宣言と格納領域の関係を考えてみます。

以下の文字列定義を行うと、データーは.textセクションに配置されROMに格納されます。
static const char[] msg = "Hello World\n";
この場合、文字列は定数(const指定)ですので、ROMに配置しても問題ありません。

では、以下のように定義すると、初期値付き変数として、.dataセクションに配置されます。
static char[] msg = "Hello World\n";
この場合文字列はRAMに配置されます。

.dataセクションの初期化

ここで、.dataセクションに配置した文字列などのデーターをどうやって初期化するかという問題が出てきます。リンカーは、.dataセクションに設定すべき初期値をROMの最後(コード領域の次)に格納してくれるので、スタートアップコードの中でこの領域をRAMの.dataセクションにコピーする必要があります。コードの中で、以下の静的変数を定義したとします。

/*----------------------------------------------------------------------------
  Clock Variable definitions
*----------------------------------------------------------------------------*/
uint32_t SystemCoreClock = __CORE_CLK;/*!< System Clock Frequency (Core Clock)*/

/************************** PRIVATE VARIABLES *************************/
static uint8_t menu1[] = "Hello NXP Semiconductors \n\r";
static const uint8_t menu2[] = "UART polling mode demo \n\r\t MCU LPC17xx - ARM Cortex-M3 \n\r\t UART0 - 9600bps \n\r";
static const uint8_t menu3[] = "\r\nUART demo terminated!\r\n";

上記のコードでは、SystemCoreClockとmenu1が.dataセクションに配置されます。コンパイル・リンクしたbinファイルをダンプすると。末尾が以下となっています。

  1. 0x002604~のエリアにmenu2, menu3で定義した文字列が格納されていることが分かります
  2. 0x002690に格納されているワードデーター(0x05B8D800)がSystemCoreClock変数の初期値で、96Mになります
  3. 0x002694~のエリアにmenu1で定義した文字列が格納されています

CodoAllocation

2)3)のデータを、リセット時に.dataセクションにコピーしないと正しい変数として認識されません。前回使ったスタートアップコードにはこのコピー処理が入っていなかったため、SystemCoreClock変数が初期化されず、クロック周波数設定が正しくできなかったという顛末でした。

前回使ったスタートアップコードはCMSISライブラリのexampleに入っていたのですが、何で.dataセクションの初期化が入っていないんじゃ!(と、自分の無知を棚に上げて文句を言ってみる・・)

セクションの開始番地情報

では、セクションの開始番地情報をどうやって取得するかというと、リンカーがexportしているシンボル情報を使います。上記のbinファイルを生成したプロジェクトのmapファイルは以下となります。

                0x00002690                _etext = . ← .textエリアが終了し、以後.dataにコピーすべきデーターを配置する
                0x00080000                __cs3_region_size_rom = 0x80000
                0x00000001                __cs3_region_num = 0x1
                0x10000000                _data = ADDR (.data) ← .dataセクションの開始(RAMの先頭番地)

.data           0x10000000       0x20 load address 0x00002690
                0x10000000                __cs3_region_start_ram = .

                0x10000020                . = ALIGN (0x8)
                0x10000020                _edata = .  ← .dataセクションの終了番地
.bss            0x10000020       0x50 load address 0x000026b0

従ってスタートアップコードの中で、_etextから始まるROM領域を、_data~_edataにコピーすればよいことになります。加えて、.bssセクションは0でクリアしておきます。

上記のアドレス情報をリンカースクリプトで設定します。リンカースクリプトの詳細は末尾の参考情報を見てください、、基本はCMSISライブラリのexampleに入っていたスクリプトを使っています(一部修正)。一部抜粋を示します。

ENTRY(_start)    ← スタートアップコートの開始ラベル

SECTIONS
{
  .text :
{

} ← .textセクションが終了

.text.align :
{
  . = ALIGN(8);
_etext = .; ← 引き続き、.dataセクションの初期値を配置し、_etextのラベルをexport
} >rom

_data = ADDR(.data);  ← データーセクションの開始位置をexport
.data :
{

_edata = .;
} >ram AT>rom ← データーはramに配置

サンプルプログラム

今回のスタートアップコード修正を盛り込んだサンプルプログラムをここに置きました。SysTick割り込みを使って、LEDを1秒周期で点滅しながら、UART0にて文字の入出力を行います。UARTの初期化と入出力はCMSISライブラリを使っています。

最後に

サンプルを動かした際に、出力が文字化けしてしまうので散々悩みました。最初はCMSISライブラリを使ったボーレート設定がうまくいっていないのかと思い、インタフェース付録基板(LPC2388)のサンプルを参考に、DLL/DLM, FDRレジスタを直接設定すると文字化けが解消したため、ライブラリが原因と半分思いかけていました。実はこの時文字列定義にconstを付けたので、.dataセクションが初期化されていなくても「たまたま」うまく動いていたんですね。const宣言を抜くと、レジスタ直打ち設定でも文字化けすることが分かり、そこから色々と調べて、.dataセクションの初期化が必要なことにたどり着きました。

STM32 Primer2のスタートアップコードを見ると、なにやらデーターのコピー処理を行っているので、「何のためにこんなことやってるの?」と疑問に思ったのですが深く考えていなかったんですね。今となってはRAMデーターを初期化するためには至極当たり前の処理ですが・・

.etextから.dataセクションに初期値をコピーする処理はアセンブラで書いているのですが、STM32 Primer2のコードは何だが冗長に思えて(ループの先頭で毎回同じ値をレジスタにロードしたりしている)、自前で最適化版を作ったのですが、これがまた動かず。結局、STM32のコードをパクッています。

なんとかUARTまで動くようになったので、次はnewlibをリンクして、printfを使えるようにする予定です。

参考情報

  1. CMSISライブラリ
  2. GNUリンカーの使い方
  3. Building Bare-Metal ARM Systems with GNU
  4. STM32用リンカスクリプトを書く
  5. ねむいさんのブログ - 今頃LPC2388基板(CQ- FRK-NXPARM)とかいぢってみる .. LPC2388用のコードで検証することで開発環境要因がないことを切り分けることができ、大変重宝しました

« GCCでmbedの実行ファイルをコンパイル | トップページ | mbed + GCCでprintfを使う »

mbed」カテゴリの記事

コメント

コメントを書く

コメントは記事投稿者が公開するまで表示されません。

(ウェブ上には掲載しません)

« GCCでmbedの実行ファイルをコンパイル | トップページ | mbed + GCCでprintfを使う »

2017年2月
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28        
無料ブログはココログ