マイクロコントローラーはどのように起動および起動しますか?


15

Cコードが記述され、コンパイルされ、マイクロコントローラーにアップロードされると、マイクロコントローラーが実行を開始します。しかし、このアップロードと起動プロセスを段階的にスローモーションで実行すると、MCU(メモリ、CPU、ブートローダー)の内部で実際に何が起こっているかについて混乱が生じます。誰かが私に尋ねた場合に私が答えるのは(おそらく間違っている)です:

  1. コンパイルされたバイナリコードはUSB経由でフラッシュROM(またはEEPROM)に書き込まれます
  2. ブートローダーは、このコードの一部をRAMにコピーします。真の場合、ブートローダーはどのようにコピーするか(ROMのどの部分をRAMにコピーするか)を知るのですか?
  3. CPUはROMおよびRAMからコードの命令とデータのフェッチを開始します

これは間違っていますか?

この起動と起動のプロセスを、このフェーズでメモリ、ブートローダー、およびCPUがどのように相互作用するかについての情報で要約することは可能ですか?

BIOSを介してPCが起動する方法について、多くの基本的な説明を見つけました。しかし、私はマイクロコントローラーの起動プロセスにこだわっています。

回答:


29

1)コンパイルされたバイナリはprom / flash yesに書き込まれます。USB、シリアル、i2c、jtagなどは、ブートプロセスを理解するために関係なく、デバイスによってサポートされるものに関してデバイスに依存します。

2)これは通常、マイクロコントローラーには当てはまりません。主な使用例は、ROM /フラッシュに命令を、RAMにデータを入れることです。アーキテクチャに関係なく。非マイクロコントローラー、PC、ラップトップ、サーバーの場合、プログラムは不揮発性(ディスク)からRAMにコピーされ、そこから実行されます。一部のマイクロコントローラーでは、ラムを使用できます。定義に違反しているように見えても、ハーバードを主張するものも同様です。ラムを命令側にマップすることを妨げるハーバードについては何もありません。電源がオンになった後にそこに命令を取得するメカニズムが必要です(これは定義に違反しますが、ハーバードシステムは他の役に立つためにそれを行う必要があります)マイクロコントローラとして)。

3)並べ替え。

各CPUは、設計どおりの決定論的な方法で「起動」します。最も一般的な方法は、電源投入後に実行する最初の命令のアドレスがリセットベクターにあるベクターテーブルです。ハードウェアが読み取ったアドレスは、そのアドレスを使用して実行を開始します。もう1つの一般的な方法は、よく知られたアドレスでベクターテーブルなしでプロセッサの実行を開始することです。チップには「ストラップ」、リセットを解除する前にハイまたはローに接続できるピンがあり、ロジックがさまざまな方法でブートすることがあります。CPU自体、つまりプロセッサコアをシステムの他の部分から分離する必要があります。cpuの動作方法を理解し、cpuアドレス空間の一部がフラッシュと通信できるように、チップ/システム設計者がcpuの外側にアドレスデコーダーをセットアップしていることを理解します。また、RAMを使用するものと周辺機器(uart、i2c、spi、gpioなど)を使用するものもあります。必要に応じて、同じCPUコアを使用して、異なる方法でラップできます。これは、腕やミップをベースにしたものを購入したときに得られるものです。armとmipsはCPUコアを作成します。CPUコアはチップを購入してラップしますが、さまざまな理由で、それらはブランド間で互換性がありません。これが、コア以外の何かに関して一般的なアームの質問をすることがめったにできない理由です。

マイクロコントローラーはチップ上のシステムになろうとするため、その不揮発性メモリー(フラッシュ/ ROM)、揮発性(SRAM)、およびCPUはすべて、周辺機器が混在する同じチップ上にあります。しかし、チップは、そのCPUのブート特性に一致するCPUのアドレス空間にフラッシュがマッピングされるように内部的に設計されています。たとえば、CPUのアドレス0xFFFCにリセットベクトルがある場合、1)を介してプログラムできるアドレスに応答するフラッシュ/ ROMが必要です。また、有用なプログラム用のアドレススペースに十分なフラッシュ/ ROMが必要です。チップ設計者は、これらの要件を満たすために、0xF000から始まる0x1000バイトのフラッシュを選択する場合があります。そしておそらく、彼らはより低いアドレスまたは多分0x0000にいくらかのRAMを置き、周辺機器は中央のどこかに置きます。

CPUの別のアーキテクチャは、アドレス0で実行を開始する可能性があるため、反対のことを行う必要があり、フラッシュを配置して、ゼロ付近のアドレス範囲に応答するようにします。たとえば、0x0000〜0x0FFFのようになります。そして、別の場所にラムを置きます。

チップ設計者は、CPUがどのようにブートするかを知っており、そこに不揮発性ストレージ(フラッシュ/ ROM)を配置しています。そのCPUのよく知られた動作に合わせてブートコードを記述するのは、ソフトウェアの担当者次第です。リセットベクタにリセットベクタアドレスを配置し、リセットベクタで定義したアドレスにブートコードを配置する必要があります。ツールチェーンはここで非常に役立ちます。時々、ポイントアンドクリックideまたは他のサンドボックスを使用して、ほとんどの作業を行うことができます。あなたがすることは、高レベル言語(C)でAPIを呼び出すことだけです。

ただし、フラッシュ/ ROMにロードされるプログラムは、CPUのハードワイヤードブート動作と一致する必要があります。プログラムmain()のC部分の前、およびエントリポイントとしてmainを使用する場合は、いくつかのことを行う必要があります。ACプログラマーは、初期値を持つ変数を宣言するときに、実際に機能すると期待していると想定しています。まあ、const以外の変数はRAMにありますが、初期値を持つ変数がある場合、その初期値は不揮発性RAMになければなりません。したがって、これは.dataセグメントであり、Cブートストラップは.dataをフラッシュからRAMにコピーする必要があります(通常、ツールチェーンによって決定されます)。初期値なしで宣言するグローバル変数は、プログラムが開始する前にゼロであると想定されますが、実際には想定しないでください。ありがたいことに、一部のコンパイラは初期化されていない変数について警告し始めています。これは.bssセグメントであり、CブートストラップのゼロはRAMにあり、その内容のゼロは不揮発性メモリに保存する必要はありませんが、開始アドレスとその量は保存します。繰り返しますが、ツールチェーンはここで大いに役立ちます。そして最後に最低限必要なことは、Cプログラムがローカル変数を持ち、他の関数を呼び出すことができることを期待するため、スタックポインターをセットアップする必要があるということです。その後、他のチップ固有の処理が行われるか、Cでチップ固有の処理が残ります。不揮発性メモリに保存する必要はありませんが、開始アドレスと量は保存します。繰り返しますが、ツールチェーンはここで大いに役立ちます。そして最後に最低限必要なことは、Cプログラムがローカル変数を持ち、他の関数を呼び出すことができることを期待するため、スタックポインターをセットアップする必要があるということです。その後、他のチップ固有の処理が行われるか、Cでチップ固有の処理が残ります。不揮発性メモリに保存する必要はありませんが、開始アドレスと量は保存します。繰り返しますが、ツールチェーンはここで大いに役立ちます。そして最後に最低限必要なことは、Cプログラムがローカル変数を持ち、他の関数を呼び出すことができることを期待するため、スタックポインターをセットアップする必要があるということです。その後、他のチップ固有の処理が行われるか、Cでチップ固有の処理が残ります。

armのcortex-mシリーズコアがこれを行います。スタックポインターはベクターテーブルにあり、リセット後に実行されるコードを指すリセットベクターがあります。 (とにかく通常asmを使用する)ベクターテーブルを生成するには、asmなしで純粋なCを使用します。今では、.dataをコピーしたり、.bssをゼロにしたりしないため、cortex-mベースの何かにasmを使用しない場合は、自分でそれを行う必要があります。大きな機能はリセットベクトルではなく、ハードウェアが推奨されるCの呼び出し規約に従ってレジスタを保持する割り込みベクトルであり、そのベクトルの正しいリターンを使用するため、各ハンドラーの周りに正しいasmをラップする必要はありません(または、ターゲットにツールチェーン固有のディレクティブを使用して、ツールチェーンでラップするようにします)。

たとえば、マイクロコントローラはバッテリーベースのシステムでよく使用されるため、一部の周辺機器の電源がオフになっていると一部のリセットが解除されるため、これらのサブシステムをオンにする必要があります。 。Uarts、gpiosなど。しばしば、水晶または内部発振器から直接、低めのクロック速度が使用されます。また、システム設計により、より高速のクロックが必要であることが示される場合があるため、それを初期化します。クロックがフラッシュまたはRAMに対して速すぎるため、クロックをアップする前に待機状態を変更する必要がある場合があります。uart、USB、またはその他のインターフェイスをセットアップする必要がある場合があります。その後、アプリケーションはそのことを実行できます。

コンピューターのデスクトップ、ラップトップ、サーバー、およびマイクロコントローラーは、起動/動作方法に違いはありません。それらがほとんど1つのチップ上にないことを除いて。BIOSプログラムは、CPUとは別のチップフラッシュ/ ROMにあることがよくあります。最近、x86 cpusはサポートチップであったものを同じパッケージ(pcieコントローラーなど)に引き寄せていますが、RAMとROMのほとんどはチップから離れていませんが、それでもシステムであり、正確に動作します高レベルでも同じです。CPUブートプロセスはよく知られています。ボード設計者は、CPUがブートするアドレス空間にフラッシュ/ ROMを配置します。そのプログラム(x86 pcのBIOSの一部)は上記のすべてを実行し、さまざまな周辺機器を起動し、dramを初期化し、pcieバスを列挙します。多くの場合、ユーザーがBIOS設定またはcmos設定と呼んでいたものに基づいて非常に設定可能です。これは当時の技術が使用されていたためです。問題ではありませんが、BIOSブートコードに何を変更するかを指示するために移動して変更できるユーザー設定があります。

異なる人々は異なる用語を使用します。チップブート、つまり実行される最初のコード。ブートストラップとも呼ばれます。ローダーという単語を含むブートローダーは、多くの場合、干渉することを何もしないと、一般的なブートからアプリケーションまたはオペレーティングシステムへの一般的なブートから起動するブートストラップであることを意味します。ただし、ローダー部分は、ブートプロセスを中断して、他のテストプログラムをロードできることを意味します。たとえば、組み込みLinuxシステムでubootを使用したことがある場合は、キーを押して通常のブートを停止し、テストカーネルをramにダウンロードして、フラッシュ上のカーネルの代わりにブートするか、またはまたは、新しいカーネルをダウンロードしてからブートローダーにフラッシュに書き込み、次回ブートするときに新しいものが実行されるようにすることができます。

CPU自体に関しては、周辺機器からのフラッシュからのRAMを知らないコアプロセッサです。ブートローダー、オペレーティングシステム、アプリケーションの概念はありません。CPUに送られて実行される命令のシーケンスです。これらは、異なるプログラミングタスクを互いに区別するためのソフトウェア用語です。互いにソフトウェアのコンセプト。

一部のマイクロコントローラには、チップベンダーが提供する個別のブートローダーがあり、別のフラッシュまたはフラッシュの別の領域にありますが、これらは変更できない場合があります。この場合、多くの場合、リセットが解除される前にピンをハイまたはローに接続すると、ロジックまたはブートローダーに何をすべきかを伝えているピンまたはピンのセット(私はストラップと呼びます)がありますチップにそのブートローダーを実行するよう指示し、データがフラッシュにプログラムされるのをuartで待ちます。ストラップを他の方法で設定すると、チップベンダーのブートローダーではなくプログラムが起動し、チップのフィールドプログラミングやプログラムのクラッシュからの回復が可能になります。フラッシュをプログラムできるのは、純粋なロジックだけである場合があります。これは最近よくあることですが、

ほとんどのマイクロコントローラがRAMよりもはるかに多くのフラッシュを持っている理由は、主な使用例がフラッシュから直接プログラムを実行し、スタックと変数をカバーするのに十分なRAMしか持っていないからです。場合によっては、RAMからプログラムを実行して、適切にコンパイルしてフラッシュに保存し、呼び出しの前にコピーする必要があります。

編集

フラッシュ

.cpu cortex-m0
.thumb

.thumb_func
.global _start
_start:
stacktop: .word 0x20001000
.word reset
.word hang
.word hang
.word hang

.thumb_func
reset:
    bl notmain
    b hang

.thumb_func
hang:   b .

notmain.c

int notmain ( void )
{
    unsigned int x=1;
    unsigned int y;
    y = x + 1;

    return(0);
}

flash.ld

MEMORY
{
    bob : ORIGIN = 0x00000000, LENGTH = 0x1000
    ted : ORIGIN = 0x20000000, LENGTH = 0x1000
}
SECTIONS
{
    .text : { *(.text*) } > bob
    .rodata : { *(.rodata*) } > bob
    .bss : { *(.bss*) } > ted
    .data : { *(.bss*) } > ted AT > bob
}

したがって、これはcortex-m0の例であり、cortex-msはすべて、この例の範囲内で同じように機能します。この例の特定のチップには、アームアドレス空間のアドレス0x00000000にアプリケーションフラッシュがあり、RAMは0x20000000にあります。

cortex-mの起動方法は、アドレス0x0000の32ビットワードで、スタックポインターを初期化するアドレスです。この例では多くのスタックは必要ないので、0x20001000で十分です。明らかに、そのアドレスの下にRAMが必要です(アームがプッシュする方法です。最初に減算してからプッシュします。 0x2000FFFCを使用する必要はありません)。アドレス0x0004の32ビットワードは、リセットハンドラのアドレスであり、基本的にはリセット後に実行される最初のコードです。次に、その皮質mコアとチップに固有の割り込みハンドラとイベントハンドラがさ​​らにありますが、おそらく128または256です。それらを使用しない場合、テーブルをセットアップする必要はありません、私はデモのためにいくつかを投げました目的。

この例では、コードを見るとこれらのセグメントには何も存在しないことがわかっているため、.dataや.bssを扱う必要はありません。もしあれば、私はそれに対処し、すぐにします。

したがって、スタックはセットアップ、チェック、.dataの処理、check、.bss、checkであるため、Cブートストラップ処理が行われ、Cのエントリ関数に分岐できます。一部のコンパイラは、関数を参照すると余分なジャンクを追加するためmain()そしてmainに向かう途中で、私はその正確な名前を使用しません。ここでは、Cエントリポイントとしてnotmain()を使用しました。そのため、リセットハンドラはnotmain()を呼び出し、notmain()が返された場合は、ハングすることになります。

私はツールをマスターすることを固く信じていますが、多くの人はそうではありませんが、あなたが見つけるのは、あなたがアプリやウェブページを作るのと同じくらい遠く、完全に近い自由のために、各ベアメタル開発者が自分のことをするということです。彼らは再び自分のことをします。独自のブートストラップコードとリンカスクリプトが必要です。他の人はツールチェーンに依存するか、ほとんどの作業が他の誰かによって行われるベンダーのサンドボックスでプレイします(何かが壊れると、怪我の世界にあり、ベアメタルのものが頻繁に劇的に壊れます)。

だから私が得るGNUツールでアセンブル、コンパイル、リンクする:

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f802   bl  1c <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <notmain>:
  1c:   2000        movs    r0, #0
  1e:   4770        bx  lr

それで、ブートローダーはどのようにして物がどこにあるのかを知るのです。コンパイラが仕事をしたからです。最初のケースでは、アセンブラーがflash.sのコードを生成し、そうすることでラベルの場所がわかります(ラベルは関数名や変数名などの単なるアドレスです)。したがって、バイトをカウントしてベクターに入力する必要はありませんでした。テーブルを手動で、ラベル名を使用し、アセンブラーがそれを行いました。リセットがアドレス0x14である場合、アセンブラーがベクターテーブルに0x15を挿入した理由を尋ねます。さて、これはcortex-mであり、起動してサムモードでのみ実行されます。Thumbモードに分岐する場合、ARMモードでリセットする場合、アドレスに分岐するときにlsbitを設定する必要があります。そのため、常にそのビットセットが必要です。ラベルがベクターテーブル内でそのまま使用される場合、またはブランチへの分岐などに使用される場合、ツールのことを知っています。ツールチェーンはlsbitを設定することを知っています。したがって、ここでは0x14 | 1 = 0x15です。ハングについても同様です。これで、逆アセンブラはnotmain()の呼び出しに対して0x1Dを表示しませんが、ツールが命令を正しく構築したことを心配しないでください。

コードがnotmainにあるため、これらのローカル変数は使用されず、デッドコードです。コンパイラーは、yが設定されているが使用されていないということで、その事実についてもコメントします。

アドレス空間に注意してください。これらはすべてアドレス0x0000から始まり、そこからベクトルテーブルが適切に配置されます。.textまたはプログラム空間も適切に配置されます。notmain.cのコードの前にflash.sを取得した方法はツールを知っていると、よくある間違いは、それを正しく行わずにクラッシュして激しく焼くことです。IMOでは、最初に起動する直前に物を配置するために分解する必要があります。適切な場所に物を置いたら、毎回確認する必要はありません。新しいプロジェクトのために、またはそれらがハングする場合。

一部の人々を驚かせるのは、2つのコンパイラが同じ入力から同じ出力を生成することを期待する理由がないということです。または、設定が異なる同じコンパイラーです。clangを使用して、llvmコンパイラー最適化ありとなしでこれら2つの出力を取得します

llvm / clang最適化

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f802   bl  1c <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <notmain>:
  1c:   2000        movs    r0, #0
  1e:   4770        bx  lr

最適化されていない

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f802   bl  1c <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <notmain>:
  1c:   b082        sub sp, #8
  1e:   2001        movs    r0, #1
  20:   9001        str r0, [sp, #4]
  22:   2002        movs    r0, #2
  24:   9000        str r0, [sp, #0]
  26:   2000        movs    r0, #0
  28:   b002        add sp, #8
  2a:   4770        bx  lr

それはコンパイラが追加を最適化した嘘ですが、これらは変数にスタック上に2つのアイテムを割り当てました。これらはローカル変数であり、固定アドレスではなくスタック上にあるため、グローバルでそれを見るでしょう変更。しかし、コンパイラはコンパイル時にyを計算できることを認識し、実行時に計算する理由がないため、xに割り当てられたスタックスペースに1を、yに割り当てられたスタックスペースに2を単に配置しました。コンパイラは、内部テーブルを使用してこのスペースを「割り当て」ます。変数yにスタックプラス0、変数xにスタックプラス4を宣言します。コンパイラーは、実装するコードがC標準またはCプログラマーの期待に準拠している限り、何でもできます。コンパイラーが関数の実行中にxをスタック+ 4のままにしなければならない理由はありません。

アセンブラでダミー関数を追加した場合

.thumb_func
.globl dummy
dummy:
    bx lr

そしてそれを呼び出す

void dummy ( unsigned int );
int notmain ( void )
{
    unsigned int x=1;
    unsigned int y;
    y = x + 1;
    dummy(y);
    return(0);
}

出力が変わる

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f804   bl  20 <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <dummy>:
  1c:   4770        bx  lr
    ...

00000020 <notmain>:
  20:   b510        push    {r4, lr}
  22:   2002        movs    r0, #2
  24:   f7ff fffa   bl  1c <dummy>
  28:   2000        movs    r0, #0
  2a:   bc10        pop {r4}
  2c:   bc02        pop {r1}
  2e:   4708        bx  r1

ネストされた関数があるので、notmain関数は戻りアドレスを保持する必要があるため、ネストされた呼び出しの戻りアドレスを上書きできます。これは、x86などのスタックを適切に使用した場合、アームがリターンにレジスタを使用するためです。さて、なぜr4をプッシュしたのでしょうか?さて、呼び出し規約は、スタックを32ビット、1ワード境界ではなく64ビット(2ワード)境界に揃えておくように変更されました。そのため、スタックを整列させるために何かをプッシュする必要があるため、コンパイラは何らかの理由でr4を任意に選択しました。理由は関係ありません。このターゲットの呼び出し規約に従って、r4にポップするのはバグになりますが、関数呼び出しではr4を上書きせず、r0からr3を上書きできます。r0は戻り値です。多分テールの最適化を行っているように見えますが、

しかし、xとyの数学は、ダミー関数に渡されるハードコードされた値2に最適化されていることがわかります(ダミーは個別のファイル、この場合はasmに具体的にコーディングされているため、コンパイラーは関数呼び出しを完全に最適化しないため、 notmain.cのCで単純に返されるダミー関数がある場合、オプティマイザはx、y、およびダミー関数呼び出しを削除します。これらはすべてデッド/役に立たないコードであるためです)。

また、flash.sコードが大きくなったため、notmainはelsehwereであり、ツールチェーンがすべてのアドレスにパッチを適用してくれたため、手動で行う必要はありません。

参照用に最適化されていないclang

00000020 <notmain>:
  20:   b580        push    {r7, lr}
  22:   af00        add r7, sp, #0
  24:   b082        sub sp, #8
  26:   2001        movs    r0, #1
  28:   9001        str r0, [sp, #4]
  2a:   2002        movs    r0, #2
  2c:   9000        str r0, [sp, #0]
  2e:   f7ff fff5   bl  1c <dummy>
  32:   2000        movs    r0, #0
  34:   b002        add sp, #8
  36:   bd80        pop {r7, pc}

最適化されたclang

00000020 <notmain>:
  20:   b580        push    {r7, lr}
  22:   af00        add r7, sp, #0
  24:   2002        movs    r0, #2
  26:   f7ff fff9   bl  1c <dummy>
  2a:   2000        movs    r0, #0
  2c:   bd80        pop {r7, pc}

そのコンパイラの作成者は、スタックを揃えるためにダミー変数としてr7を使用することを選択しました。また、スタックフレームに何もない場合でも、r7を使用してフレームポインタを作成しています。基本的に、命令は最適化されている可能性があります。しかし、ポップを使用して3つの命令を返さなかったので、おそらく正しいコマンドラインオプション(プロセッサを指定)でgccを実行できると思いました。

これは主に残りの質問に答えるはずです

void dummy ( unsigned int );
unsigned int x=1;
unsigned int y;
int notmain ( void )
{
    y = x + 1;
    dummy(y);
    return(0);
}

私は今グローバルを持っています。そのため、最適化されていない場合は.dataまたは.bssのいずれかになります。

最終出力を見る前に、itermediateオブジェクトを見てみましょう

00000000 <notmain>:
   0:   b510        push    {r4, lr}
   2:   4b05        ldr r3, [pc, #20]   ; (18 <notmain+0x18>)
   4:   6818        ldr r0, [r3, #0]
   6:   4b05        ldr r3, [pc, #20]   ; (1c <notmain+0x1c>)
   8:   3001        adds    r0, #1
   a:   6018        str r0, [r3, #0]
   c:   f7ff fffe   bl  0 <dummy>
  10:   2000        movs    r0, #0
  12:   bc10        pop {r4}
  14:   bc02        pop {r1}
  16:   4708        bx  r1
    ...

Disassembly of section .data:
00000000 <x>:
   0:   00000001    andeq   r0, r0, r1

現在、これには情報がありませんが、何が起こっているかのアイデアを提供します。リンカーは、オブジェクトを取得し、提供された情報(この場合はflash.ld)でリンクしますデータなどが行きます。コンパイラはそのようなことを知らず、提示されたコード、リンカが接続を満たすための穴を残さなければならない外部のコードにのみ焦点を合わせることができます。データをリンクする方法を残す必要があるため、コンパイラとこの逆アセンブラーが知らないという理由だけで、すべてのアドレスはゼロに基づいています。ここには示されていないが、リンカが物を配置するために使用する他の情報があります。ここのコードは位置に依存しないので、リンカーはその仕事をすることができます。

その後、少なくともリンクされた出力の逆アセンブリが表示されます

00000020 <notmain>:
  20:   b510        push    {r4, lr}
  22:   4b05        ldr r3, [pc, #20]   ; (38 <notmain+0x18>)
  24:   6818        ldr r0, [r3, #0]
  26:   4b05        ldr r3, [pc, #20]   ; (3c <notmain+0x1c>)
  28:   3001        adds    r0, #1
  2a:   6018        str r0, [r3, #0]
  2c:   f7ff fff6   bl  1c <dummy>
  30:   2000        movs    r0, #0
  32:   bc10        pop {r4}
  34:   bc02        pop {r1}
  36:   4708        bx  r1
  38:   20000004    andcs   r0, r0, r4
  3c:   20000000    andcs   r0, r0, r0

Disassembly of section .bss:

20000000 <y>:
20000000:   00000000    andeq   r0, r0, r0

Disassembly of section .data:

20000004 <x>:
20000004:   00000001    andeq   r0, r0, r1

コンパイラは、基本的にRAMで2つの32ビット変数を要求しています。1つは.bssにあります。初期化していないため、初期化はゼロと見なされます。もう1つは.dataです。宣言時に初期化したためです。

これらはグローバル変数であるため、他の関数がそれらを変更できると想定されています。コンパイラーは、notmainをいつ呼び出すことができるかについて何も想定していないため、表示されるもの(y = x + 1の数学)で最適化できないため、そのランタイムを実行する必要があります。RAMから2つの変数を読み取って追加し、保存し直す必要があります。

今、明らかにこのコードは機能しません。どうして?ここに示すブートストラップはnotmainを呼び出す前にRAMを準備しないため、チップが起動したときに0x20000000と0x20000004にあったゴミがyとxに使用されます。

ここでは表示しません。.dataと.bssについてのさらに長い巻き線を読んだり、ベアメタルコードでそれらを必要としない理由を読んだりできますが、他の誰かが正しくやったことを望んでいるのではなく、ツールをマスターする必要があると感じた場合。 。

https://github.com/dwelch67/raspberrypi/tree/master/bssdata

リンカスクリプトとブートストラップは多少コンパイラ固有であるため、1つのコンパイラの1つのバージョンについて学んだことはすべて、次のバージョンまたは他のコンパイラで放り出される可能性がありますが、.dataおよび.bssの準備に多大な労力を費やさない別の理由この怠け者になるために:

unsigned int x=1;

私はむしろこれをやりたい

unsigned int x;
...
x = 1;

コンパイラにそれを.textに入れてもらいます。フラッシュをそのように保存することもあれば、より多く書き込むこともあります。ツールチェーンバージョンまたは1つのコンパイラから別のコンパイラへのプログラミングおよび移植は、間違いなくはるかに簡単です。はるかに信頼性が高く、エラーが発生しにくい。はい、C標準に準拠していません。

これらの静的グローバルを作成したらどうなるでしょうか?

void dummy ( unsigned int );
static unsigned int x=1;
static unsigned int y;
int notmain ( void )
{
    y = x + 1;
    dummy(y);
    return(0);
}

上手

00000020 <notmain>:
  20:   b510        push    {r4, lr}
  22:   2002        movs    r0, #2
  24:   f7ff fffa   bl  1c <dummy>
  28:   2000        movs    r0, #0
  2a:   bc10        pop {r4}
  2c:   bc02        pop {r1}
  2e:   4708        bx  r1

明らかに、これらの変数は他のコードでは変更できないため、コンパイラはコンパイル時に、以前と同様にデッドコードを最適化できます。

最適化されていない

00000020 <notmain>:
  20:   b580        push    {r7, lr}
  22:   af00        add r7, sp, #0
  24:   4804        ldr r0, [pc, #16]   ; (38 <notmain+0x18>)
  26:   6800        ldr r0, [r0, #0]
  28:   1c40        adds    r0, r0, #1
  2a:   4904        ldr r1, [pc, #16]   ; (3c <notmain+0x1c>)
  2c:   6008        str r0, [r1, #0]
  2e:   f7ff fff5   bl  1c <dummy>
  32:   2000        movs    r0, #0
  34:   bd80        pop {r7, pc}
  36:   46c0        nop         ; (mov r8, r8)
  38:   20000004    andcs   r0, r0, r4
  3c:   20000000    andcs   r0, r0, r0

ローカル用にスタックを使用したこのコンパイラは、グローバル用にramを使用します。このデータは、.dataと.bssを適切に処理しなかったため、記述されたとおりに壊れています。

そして、分解で見ることができない最後の1つです。

:1000000000100020150000001B0000001B00000075
:100010001B00000000F004F8FFE7FEE77047000057
:1000200080B500AF04480068401C04490860FFF731
:10003000F5FF002080BDC046040000200000002025
:08004000E0FFFF7F010000005A
:0400480078563412A0
:00000001FF

xを0x12345678でpre-initに変更しました。私のリンカースクリプト(これはgnu ld用です)には、これがありません。これにより、最終的な場所をtedアドレス空間に配置したいが、tedアドレス空間のバイナリに保存し、誰かがあなたのためにそれを移動します。そして、それが起こったことがわかります。これはインテルの16進形式です。そして、我々は0x12345678を見ることができます

:0400480078563412A0

バイナリのフラッシュアドレス空間にあります。

readelfもこれを示しています

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  EXIDX          0x010040 0x00000040 0x00000040 0x00008 0x00008 R   0x4
  LOAD           0x010000 0x00000000 0x00000000 0x00048 0x00048 R E 0x10000
  LOAD           0x020004 0x20000004 0x00000048 0x00004 0x00004 RW  0x10000
  LOAD           0x030000 0x20000000 0x20000000 0x00000 0x00004 RW  0x10000
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10

仮想アドレスが0x20000004で物理が0x48であるLOAD行


最初に私は物事の2
つのぼやけた

1.)「主な使用例は、ROM /フラッシュに命令を、RAMにデータを入れることです。」「ここのRAMのデータ」と言うとき、プログラムのプロセスで生成されたデータを意味しますか。または、初期化されたデータも含めますか。つまり、コードをROMにアップロードすると、コードには既に初期化されたデータがあります。たとえば、oodeで次のような場合:int x = 1; int y = x +1; 上記のコードには命令があり、初期データは1です(x = 1)。このデータもRAMにコピーされるか、ROMのみに残りますか。
-user16307

12
ハハ、私はスタック交換の答えの文字制限を知っています!
old_timer

2
Uはそのような概念を初心者に説明する本を書くべきです。「githubには膨大な例があります」-いくつかの例を共有することは可能ですか
AlphaGoku

1
今やりました。役に立つことは何もしませんが、それでもマイクロコントローラーのコードの例です。そして、私が共有したもの、良いもの、悪いもの、その他のすべてを見つけることができるgithubリンクを配置しました。
old_timer

8

この答えは、起動プロセスにもっと焦点を合わせます。まず、修正-フラッシュへの書き込みは、MCU(または少なくともその一部)が既に起動した後に行われます。一部のMCU(通常はより高度なMCU)では、CPU自体がシリアルポートを操作してフラッシュレジスタに書き込む場合があります。したがって、プログラムの作成と実行は異なるプロセスです。プログラムは既にフラッシュに書き込まれていると仮定します。

基本的な起動プロセスは次のとおりです。いくつかの一般的なバリエーションに名前を付けますが、大部分はこれをシンプルにしています。

  1. リセット: 2つの基本的なタイプがあります。1つ目はパワーオンリセットで、電源電圧が上昇している間に内部で生成されます。2つ目は外部ピンの切り替えです。とにかく、リセットにより、MCU内のすべてのフリップフロップが強制的に所定の状態になります。

  2. 追加のハードウェア初期化: CPUの実行を開始する前に、追加の時間やクロックサイクルが必要になる場合があります。たとえば、私が取り組んでいるTI MCUには、ロードされる内部構成スキャンチェーンがあります。

  3. CPUブート: CPUは、リセットベクトルと呼ばれる特別なアドレスから最初の命令をフェッチします。このアドレスは、CPUの設計時に決定されます。そこからは、通常のプログラム実行です。

    CPUは3つの基本的な手順を繰り返し繰り返します。

    • フェッチ:プログラムカウンター(PC)レジスタに格納されているアドレスから命令(8、16、または32ビット値)を読み取り、PCをインクリメントします。
    • デコード:バイナリ命令をCPUの内部制御信号の値のセットに変換します。
    • 実行:命令を実行します-2つのレジスタを追加し、メモリからの読み取りまたはメモリへの書き込み、ブランチ(PCの変更)などを行います。

    (実際にはこれよりも複雑です。CPUは通常パイプライン化されています。つまり、上記の各ステップを異なる命令で同時に実行できます。上記の各ステップには複数のパイプラインステージがあります。 、およびこれらのIntel CPUを設計するのに10億個のトランジスタを必要とするすべての派手なコンピューターアーキテクチャのもの。

    フェッチがどのように機能するか疑問に思うかもしれません。CPUには、アドレス(出力)およびデータ(入力/出力)信号で構成されるバスがあります。フェッチを行うために、CPUはそのアドレス行をプログラムカウンターの値に設定し、バスを介してクロックを送信します。アドレスをデコードして、メモリを有効にします。メモリはクロックとアドレスを受け取り、データライン上のそのアドレスに値を置きます。CPUはこの値を受け取ります。データの読み取りと書き込みは似ていますが、アドレスが命令から取得されるか、PCではなく汎用レジスタの値が取得される点が異なります。

    フォンノイマンアーキテクチャの CPUには、命令とデータの両方に使用される単一のバスがあります。ハーバードアーキテクチャの CPUには、命令用とバス用の1つのバスがあります。実際のMCUでは、これらのバスは両方とも同じメモリに接続されている可能性があるため、多くの場合(常にではありませんが)心配する必要はありません。

    ブートプロセスに戻ります。リセット後、PCにはリセットベクトルと呼ばれる開始値がロードされます。これは、ハードウェアに組み込むことができます。または(ARM Cortex-M CPUで)メモリから自動的に読み取ることができます。CPUはリセットベクトルから命令をフェッチし、上記のステップをループし始めます。この時点で、CPUは正常に実行されています。

  4. ブートローダー:多くの場合、MCUの残りの部分を動作させるために行う必要がある低レベルのセットアップがいくつかあります。これには、RAMのクリアやアナログコンポーネントの製造トリム設定のロードなどが含まれます。シリアルポートや外部メモリなどの外部ソースからコードをロードするオプションもあります。MCUには、これらのことを行う小さなプログラムを含むブートROMが含まれている場合があります。この場合、CPUリセットベクトルはブートROMのアドレス空間を指します。これは基本的に通常のコードであり、製造元によって提供されているため、自分で作成する必要はありません。:-) PCでは、BIOSはブートROMと同等です。

  5. C環境のセットアップ: Cは、スタック(関数呼び出し中に状態を保存するためのRAM領域)とグローバル変数用の初期化されたメモリ位置を持つことを期待しています。これらは、Dwelchが話している.stack、.data、および.bssセクションです。初期化されたグローバル変数の初期化値は、このステップでフラッシュからRAMにコピーされます。初期化されていないグローバル変数は、互いに近いRAMアドレスを持っているため、メモリブロック全体を非常に簡単にゼロに初期化できます。スタックを初期化する必要はありません(可能ですが)-CPUのスタックポインターレジスタを設定して、RAM内の割り当てられた領域を指すようにするだけです。

  6. メイン関数:C環境がセットアップされると、Cローダーはmain()関数を呼び出します。それがアプリケーションコードの通常の始まりです。必要に応じて、標準ライブラリを除外し、C環境のセットアップをスキップして、main()を呼び出す独自のコードを作成できます。一部のMCUでは、独自のブートローダーを作成できます。その後、すべての低レベルのセットアップを自分で行うことができます。

その他:多くのMCUでは、パフォーマンスを向上させるためにRAMからコードを実行できます。これは通常、リンカー構成で設定されます。リンカは、すべての関数に2つのアドレスを割り当てます。コードが最初に格納されるロードアドレス(通常はフラッシュ)と、関数を実行するためにPCにロードされるアドレス(フラッシュまたはRAM)である実行アドレスです。RAMからコードを実行するには、CPUが関数コードをフラッシュのロードアドレスからRAMの実行アドレスにコピーし、実行アドレスで関数を呼び出すようにするコードを記述します。リンカは、これを支援するグローバル変数を定義できます。ただし、MCUではRAMからのコードの実行はオプションです。通常は、本当に高いパフォーマンスが必要な場合、またはフラッシュを書き換えたい場合にのみ行います。


1

Von Neumannアーキテクチャの概要はほぼ正しいです。通常、初期コードはブートローダーを介してRAMにロードされますが、(通常)この用語が一般的に参照するソフトウェアブートローダーではありません。これは通常、「シリコンに焼き付けられた」動作です。このアーキテクチャでのコード実行には、プロセッサがコードの実行時間を最大化し、コードがRAMにロードされるのを待たないように、ROMからの命令の予測キャッシングが含まれます。MSP430がこのアーキテクチャの例であることをどこかで読んだことがあります。

ハーバードアーキテクチャデータ・メモリ(RAM)が別のバスを介してアクセスされている間デバイス、命令は、ROMから直接実行されます。このアーキテクチャでは、コードはリセットベクトルから実行を開始するだけです。PIC24とdsPIC33はこのアーキテクチャの例です。

これらのプロセスを開始する実際のビット反転については、デバイスごとに異なり、デバッガー、JTAG、独自のメソッドなどが含まれます。


しかし、いくつかのポイントをすばやくスキップしています。スローモーションにしましょう。「最初の」バイナリコードがROMに書き込まれたとしましょう。OK ..その後、「データメモリにアクセスします」と書きます。しかし、「RAMに」データは最初にどこから来るのですか?それは再びROMから来ますか?もしそうなら、ブートローダーはどのようにしてROMのどの部分が最初にRAMに書き込まれるのかを知るのですか?
user16307

あなたは正しいです、私はたくさんスキップしました。他の人はより良い答えを持っています。あなたが探していたものを手に入れてうれしいです。
わずか
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.