main()
関数に入るためにプロセッサで行う必要があることです。 記事の最初の部分は、ビルドおよびデバッグツールについてです。 2番目は例外ベクトルを処理するためのもので、3番目はスタックとメモリを初期化するためのものです。
しかし、最初に、1つ明確化したいと思います。 何らかの理由で、多くの人々は、ARMは必然的に外部メモリ、束縛の束、少なくとも600MHzの周波数で動作するモンスターなどであると考えています。 これは部分的にしか当てはまりません(ARM9以降のファミリについて話す場合)。 私が通常使用するチップ(AT91SAM7X512)は、おなじみのAVRほど複雑ではありません。 彼は働くためにクォーツと食物だけを必要とします(クオーツなしでも可能ですが、それは完全に悲しいでしょう)。 それだけです しかしもちろん、彼にはAVRよりも多くの機会があります。 しかし、それについては後で。 今日の記事は特定のハードウェアに関連するものではありません。
コンパイラー、リンカー、デバッガー
とても心配な質問。 有料(IAR、Keil MDK、CrossWorks)と無料(gcc-arm)があります。 例ではgcc-armを使用します。 Windowsには、WinARM(死んでいるようです)、YAGARTOというアセンブリがあります。 原則として、自分で収集できます。 coLinuxのような楽しいものはまだありますが、それはまったく別の話です。 Linuxでは、通常、クロスコンパイラは通常の配布ツールを使用して組み立てられます。 一般的なドックを読む:)
標準ライブラリのような有用なものがまだあります。
printf, mktime, malloc
、およびCのプログラマーが慣れている他のすべてのような関数を実装するものglibcの使用は、大きすぎるため機能しません。 代わりに、通常は無料のnewlibを使用します。 WinARM / YAGARTOの一部ですが、Linuxユーザーは手動で組み立てる必要があります。 再び-ドキュメントを読んでください:)
デバッガーはもう少し複雑です。 エミュレータを使用できますが、周辺機器に関してはかなりバグがあります。 ここでは経験がありません。 COMポートでデバッグメッセージを使用できます。 私はこれをずっと生涯続けてきました。 99%のケースで十分です。
しかし、最もクールなのはJTAGです。 プロセッサに接続し、ストーン内で直接コードの借方記入を可能にするデバイス(ブレークポイントの設定、トレース、メモリの表示/変更など)。 確かに、お金がかかる一方で、一方では-ボード上では、その下に足を上げる必要があります。
例外ハンドラー
さて、コンパイラがインストールおよび設定されていると仮定します。 今すぐ何かを実行しましょう。 最初から始めましょう:プロセッサがリセットされたときに何が起こるか(たとえば、電源がオンになり、電圧が安定した後)。 ここではすべてが簡単です。プロセッサはアドレス0x0からプログラムの実行を開始します。 このアドレスから初期化コードを配置して、自分で作業できるようです。 しかし、それほど単純ではありません。 実際には、開始アドレスには例外ハンドラーのベクターが格納されています。
たとえば、割り込みが発生すると、プロセッサはアドレス0x18から処理を開始し、例外「不明な命令」はアドレス0x04から処理されます。 一般に、最初の28バイトは例外ハンドラーのテーブル用に予約されています(リセットも例外です)。
図は、これをより明確に示しています。 この図は、各プロセッサ、または1つのプロセッサ命令に4バイトが割り当てられていることを示しています。 (ARMモード。すべてのハンドラーはこの命令モードで呼び出されます。)
したがって、最初に行う必要があるのは、例外ハンドラを作成し、それらを正しく配置することです。 これを行います:
ldr pc, ResetHandlerAddr
ldr pc, UndefHandlerAddr
ldr pc, SWIHandlerAddr
ldr pc, PrefetchAbtHandlerAddr
ldr pc, DataAbtHandlerAddr
nop
ldr pc, IRQHandlerAddr
ldr pc, FIQHandlerAddr
このコードは何をしますか? これらは、実際のハンドラーのアドレスを
pc
レジスターにロードするコマンドです。 このような無条件の移行。 さらにコードに沿って、これらの同じアドレスを格納する変数があります。
ResetHandlerAddr: .word ResetHandler
UndefHandlerAddr: .word UndefHandler
SWIHandlerAddr: .word SWIHandler
PrefetchAbtHandlerAddr: .word PrefetchAbtHandler
DataAbtHandlerAddr: .word DataAbtHandler
IRQHandlerAddr: .word IRQHandler
FIQHandlerAddr: .word FIQHandler
ここでは、割り込み処理を加速するいくつかのトリックを適用することができました。 たとえば、ご覧のとおり、FIQハンドラーはリストの最後にあるため、この割り込みの処理はその場ですぐに開始できます。
AIC(高度な割り込みコントローラー)レジスタを使用して、割り込みハンドラーに直接ジャンプすることもできました。 しかし、私たちの生活が複雑になるまで。 これまでのところ、リセット処理のみが重要です。
ハンドラー自体をできるだけ単純に書きましょう。 プロセッサをハングアップさせます(無条件に自分自身に移行するコマンドを際限なく実行します)。 とにかく、私たちはまだ例外を処理する方法を知らないので、ぶら下がりプロセッサは完全に受け入れられます。
UndefHandler: B UndefHandler
SWIHandler: B SWIHandler
PrefetchAbtHandler: B PrefetchAbtHandler
DataAbtHandler: B DataAbtHandler
IRQHandler: B IRQHandler
FIQHandler: B FIQHandler
B
は無条件分岐コマンドです
次に行う必要があるのは、各動作モードの
sp
スタックポインターを構成することです。 したがって、例外が発生した場合、ハンドラーには既に独自のスタックがあります。 最初にのみ、すべてのスタックのサイズを説明します。
.EQU IRQ_STACK_SIZE, 0x100
.EQU FIQ_STACK_SIZE, 0x100
.EQU ABT_STACK_SIZE, 0x100
.EQU UND_STACK_SIZE, 0x100
.EQU SVC_STACK_SIZE, 0x100
長い間苦労しないように、各モードでスタックごとに256バイトを割り当てます。 実際、これらのモードのほとんどについて-これはたくさんあります。 それはすべてハンドラーに依存しますが。 ご覧のとおり、6つのモードのうち5つのモードのサイズについて説明します。 残りのメモリは、ヒープと第6(ユーザーモード)モードのスタック間で共有されます。
次に、さまざまなモードへの移行を容易にする定数について説明します。 現在のモードはCPSRレジスタです。 また、ステータスレジスタの役割も果たします。
.EQU ARM_MODE_FIQ, 0x11
.EQU ARM_MODE_IRQ, 0x12
.EQU ARM_MODE_SVC, 0x13
.EQU ARM_MODE_ABT, 0x17
.EQU ARM_MODE_UND, 0x1B
.EQU ARM_MODE_USR, 0x10
.EQU I_BIT, 0x80
.EQU F_BIT, 0x40
定数
I_BIT
と
F_BIT
は、それぞれ単純な割り込みと高速な割り込みを禁止するビットです。 これで、スタックを初期化する準備が整いました。 これは単純に行われます。ポインタをレジスタ
r0
スタックの先頭にロードしてから、目的のモードに移動し、
sp
に値
r0
を書き込み、スタックのサイズだけ
r0
を減らして繰り返します。
.RAM_TOP:
.word __TOP_STACK
ResetHandler:
ldr sp, .RAM_TOP
msr CPSR_c, #ARM_MODE_FIQ | I_BIT | F_BIT
mov sp, r0
sub r0, r0, #FIQ_STACK_SIZE
msr CPSR_c, #ARM_MODE_IRQ | I_BIT | F_BIT
mov sp, r0
sub r0, r0, #IRQ_STACK_SIZE
msr CPSR_c, #ARM_MODE_SVC | I_BIT | F_BIT
mov sp, r0
sub r0, r0, #SVC_STACK_SIZE
msr CPSR_c, #ARM_MODE_ABT | I_BIT | F_BIT
mov sp, r0
sub r0, r0, #ABT_STACK_SIZE
msr CPSR_c, #ARM_MODE_UND | I_BIT | F_BIT
mov sp, r0
sub r0, r0, #UND_STACK_SIZE
msr CPSR_c, #ARM_MODE_USR
メモリ初期化
これで、割り込みがオンになり、スタックが構成された非特権モードになりました。 ところで、このモードから抜け出すことは単に不可能です。 例外をスローすることによってのみ。 しかし、それについては次の記事で詳しく説明します。
main()
関数に進む前に、少しだけ残っています。 一部のデータをRAMに転送し、.BSSセグメントにあるメモリをリセットするだけです。 これは、グローバル変数が保存されるメモリです。 実際、C言語標準では、作業の開始時にグローバル変数をゼロにリセットすることを約束しており、ARMはこれを保証していません。 したがって、セグメントを手動でリセットします。
MOV R0, #0 LDR R1, =__bss_start__ LDR R2, =__bss_end__ LoopZI: CMP R1, R2 STRLO R0, [R1], #4 BLO LoopZI
定数
__bss_end__ & __bss_start__
、リンカによって親切に提供されます。
ところで、ここでは、条件付き命令(接尾辞O)の使用を確認できます。 R1!= R2の間に実行されます。
また、以前に初期化された変数(
int x=42
変数)をROMからRAMに転送する必要があります。
LDR R1, =_etext LDR R2, =_data LDR R3, =_edata LoopRel: CMP R2, R3 LDRLO R0, [R1], #4 STRLO R0, [R2], #4 BLO LoopRel
C ++で記述する場合、グローバルオブジェクトのデザイナーを呼び出す必要があります。
LDR r0, =__ctors_start__ LDR r1, =__ctors_end__ ctor_loop: CMP r0, r1 BEQ ctor_end LDR r2, [r0], #4 STMFD sp!, {r0-r1} MOV lr, pc BX r2 LDMFD sp!, {r0-r1} B ctor_loop ctor_end:
まあ、一般的に、そしてすべて。
main()
を呼び出します:
ldr r0,=main bx r0
おめでとう、これで
void main(void)
関数になりました。 周辺機器の初期化を行うことができます。 事実は、それ以前はソフトウェア環境のみを初期化したということです。 したがって、プロセッサは可能な限り低い周波数で動作するようになり、すべての周辺機器が無効になります。 ここを回避することはできません:)
しかし、周辺機器の初期化は特定のハードウェアに依存するものであり、この記事の目的は抽象ARMの実行方法を伝えることです。
さらにいくつかの微妙な違いがあります。このコードは直接コンパイルして実行することはできません。コードが配置されているセクションはここでは説明されていないためです。 また、リンカスクリプトは提供していません(これらのスクリプトは、メモリおよびファームウェアイメージ内のコードとデータのセクションの配置を記述しています)。
しかし、インターネットには、特定のハードウェアを実行するための既製の例がたくさんあります。 スクリプト、メイクファイル、すべてすべて。 メーカーのウェブサイトを見てください:)
次の記事は、明らかに、理論、今度はプロセッサモードと例外的な状況の説明に当てられます。