最小のARM:レイアウト2、割り込み、こんにちは世界!



別の記事でサイクルを「仕上げる」機会を見つけたので、小さな結果を要約します。 実際、通常はプログラミングを開始できるようになりました。


シリーズの以前の記事:


記事のコード例: https : //github.com/farcaller/arm-demos



前回、コンパイルされたアプリケーションで遭遇する可能性のあるセクションと、その典型的な内容を見つけました。 特に、 .data.bss .dataました。 .dataは、コンパイル時に指定された値を持つグローバル(静的)変数が格納されることを思い出してください。 このセクションは、フラッシュメモリからRAMにコピーする必要があります。 .bssは、グローバル変数をゼロ値で保存します。リセットする必要があります。

典型的な条件では、 crt0.aの手順でcrt0.a (Wikipediaでは、この名前はC RunTime 0を意味することを示唆しています。0はアプリケーションの寿命の始まりです)。 今日は、おもちゃのプラットフォーム用にcrt0のアナログを書きます。

免責事項。 GNU ldでは、構文のバリエーションとレイアウトフラグを使用して、同じことの多くを異なる方法で行うことができます。 以下に説明する方法はすべて、私の想像の産物であり、LPCXpressoのレイアウトスクリプトの影響下で書かれています。 説明されている状況のいずれかを解決するためのより効果的な方法を知っている場合は、私に書いてください!

メモリ内データの初期化


04-helloworld/platform/protoboard/layout.ld確認してください。 一般に、以前のバージョンと比較して大きな変更はありません:いくつかの定数、メモリの説明、セクション。 例として.dataセクションを見てみましょう。
 .data : ALIGN(4) { _data = .; *(SORT_BY_ALIGNMENT(.data*)) . = ALIGN(4); _edata = .; } > ram AT>rom = 0xff 


4バイトのアラインメントを持つ.dataセクションが出力ファイルに書き込まれます(つまり、カーソルがこのセクションの前のアドレス0x00000101を指している場合、 .dataは0x00000104から始まります)。 セクションはRAM( > ram )にありますが、フラッシュメモリからロードされます( AT>rom )。

Construction =0xffは塗りつぶしパターンを定義します。 出力セクションでアドレス指定されていないバイトが生成された場合、それらの値はプレースホルダーバイトに設定されます。 0xffは、消去されたフラッシュメモリがすべてのユニットであるという理由で選択されます。つまり、0xffの書き込みは(たとえば0x00とは異なり)空の操作です。

次に、 _dataに現在のカーソル位置が保存されます。 セクションはメインメモリ内にあるため、 _dataはその先頭(この場合は_dataを示します。

すべての入力ファイルの.data始まる名前を持つすべてのソースセクションが1つずつセクションにコピーされ、サイズで並べ替えられます。 ソートは非常に重要な役割を果たします。例を挙げて検討してください。
 uint16_t static_int = 0xab; uint8_t static_int2 = 0xab; uint16_t static_int3 = 0xab; uint8_t static_int4 = 0xab; 


ここでは、 .dataセクションに対して4つの変数が定義されています。 最終ファイルはどうなりますか?
 .data 0x0000000010000000 0xc load address 0x00000000000007b0 0x0000000010000000 _data = . *(.data*) .data.static_int2 0x0000000010000000 0x1 build/d0f0154f60ed1a9c2083183e7c731846451d2bdb_helloworld.o 0x0000000010000000 static_int2 *fill* 0x0000000010000001 0x3 ff .data.static_int3 0x0000000010000004 0x4 build/d0f0154f60ed1a9c2083183e7c731846451d2bdb_helloworld.o 0x0000000010000004 static_int3 .data.static_int4 0x0000000010000008 0x1 build/d0f0154f60ed1a9c2083183e7c731846451d2bdb_helloworld.o 0x0000000010000008 static_int4 *fill* 0x0000000010000009 0x1 ff .data.static_int 0x000000001000000a 0x2 build/d0f0154f60ed1a9c2083183e7c731846451d2bdb_helloworld.o 0x000000001000000a static_int 0x000000001000000c . = ALIGN (0x4) 0x000000001000000c _edata = . 

単語の境界に変数を揃える*fill*バイトに注意してください。 順序が悪いため、そのように4バイトを失いました。 今回はSORT_BY_ALIGNMENTを使用して操作を繰り返します。
 .data 0x0000000010000000 0x8 load address 0x00000000000007b0 0x0000000010000000 _data = . *(SORT(.data*)) .data.static_int3 0x0000000010000000 0x4 build/d0f0154f60ed1a9c2083183e7c731846451d2bdb_helloworld.o 0x0000000010000000 static_int3 .data.static_int 0x0000000010000004 0x2 build/d0f0154f60ed1a9c2083183e7c731846451d2bdb_helloworld.o 0x0000000010000004 static_int .data.static_int2 0x0000000010000006 0x1 build/d0f0154f60ed1a9c2083183e7c731846451d2bdb_helloworld.o 0x0000000010000006 static_int2 .data.static_int4 0x0000000010000007 0x1 build/d0f0154f60ed1a9c2083183e7c731846451d2bdb_helloworld.o 0x0000000010000007 static_int4 0x0000000010000008 . = ALIGN (0x4) 0x0000000010000008 _edata = . 

変数はきちんとソートされており、大量(33%)のメモリを節約しました!

カーソルに戻ります。カーソルはすべての.data終わりをすぐに示します。 建設 . = ALIGN(4)は、カーソルを(入力セクションのデータが完全な位置合わせに不十分な場合)単語の境界に沿って位置合わせします。 最終値は_edata書き込まれ_edata

メモリ内のアドレスに加えて、セクションがフラッシュメモリ内のどこにあるかを知る必要があります。そのために、シンボルはスクリプトの先頭で宣言されます: _data_load = LOADADDR(.data)LOADADDRは、セクションのロードアドレスを返す関数です。 それに加えて、さらに興味深い関数がいくつかあります。ADDRは、「仮想」アドレスSIZEOF-バイト単位のセクションサイズを返します。

.dataセクションの初期化コード04-hello-world/platform/common/platform.cください:
 uint32_t *load_addr = &_data_load; for (uint32_t *mem_addr = &_data; mem_addr < &_edata;) { *mem_addr++ = *load_addr++; } 

ループでは、値をload_addrからload_addrにコピーします。

通常、この初期化は、可能な場合はできるだけ早く、最初のタスクの1つとして実行されます。 これには非常に合理的な説明があります。初期化の前に、Cからグローバル変数にアクセスすると「ガーベッジ」が返されます。 私たちの場合、この関数は.data / .bssデータに依存しないため、 platform_init呼び出しの後に初期化が実行され、その実行により後続のコードがより高速に実行され、結果としてパフォーマンスが向上します。 欠点は、別のplatform_init_postが出現したことplatform_init_post 。この場合、グローバル変数はシステムバスの周波数で初期化されます。

最後のセクション- /DISCARD/ -は特別で、一種の/ dev / nullリンカーです。 すべての着信セクションは単純に破棄されます(覚えているように、セクションが明示的に指定されていない場合、適切なメモリ領域に自動的に追加されます)。 ARMv6-M0の場合の入力セクションは空であることが保証されているため、このセクションは明確にするためにさらに説明されています。

さまざまな割り込みについて


2つの新しいセクション.isr_vector.isr_vector_nvicれる、わずかに変更された最初のセクション.textに注意して.isr_vector_nvic 。 どちらもKEEP命令でラップされているため、リンカが不必要に「最適化」するのを防ぎます。 .isr_vectorは、Cortex-Mの共通割り込みテーブルが含まれており、 platform/common/isr.c

 __attribute__ ((weak)) void isr_nmi(); __attribute__ ((weak)) void isr_hardfault(); __attribute__ ((weak)) void isr_svcall(); __attribute__ ((weak)) void isr_pendsv(); __attribute__ ((weak)) void isr_systick(); __attribute__ ((section(".isr_vector"))) void (* const isr_vector_table[])(void) = { &_stack_base, main, // Reset isr_nmi, // NMI isr_hardfault, // Hard Fault 0, // CM3 Memory Management Fault 0, // CM3 Bus Fault 0, // CM3 Usage Fault &_boot_checksum, // NXP Checksum code 0, // Reserved 0, // Reserved 0, // Reserved isr_svcall, // SVCall 0, // Reserved for debug 0, // Reserved isr_pendsv, // PendSV isr_systick, // SysTick }; 


ご覧のとおり、アセンブラーファイルでテーブルを宣言してCの用語で説明することから離れ、独立した割り込みハンドラーも導入されました(1つの一般的なhang代わりに)。 これらのハンドラーはすべてデフォルトで無限ループを実行します( isr_hardfaultでは記事の例を書いている間にデバッグLEDを数回スリップしましたが)が、 weak属性で宣言されているため、他のファイルで再定義できます。 たとえば、 timer.ctimer.c独自の実装があり、最終的なイメージに分類されます。

テーブルの継続は、特定のプロセッサに既に依存しているため、同様の構造isr_vector_table_nvic配置されますが、本質は同じままです。

そして中断について


割り込みについてもう少し言いましょう。 割り込みの一般的な本質は、外部イベントへの反応としてのハンドラーの呼び出しです(イベントの時点で実行されるコードに対して)。 Cortex-Mの優れた機能:プロセッサ自体がレジスタ値をパック/アンパックするため、Cの通常の関数のように割り込みを書き込むことができます。さらに、割り込みのネストも自動的に行われます。

NVIC-ネストされたベクター割り込みコントローラーは、ARMコアの背後にある周辺機器からの割り込みを処理します。 これにより、異なる優先度の異なる割り込みを設定したり、それらを一元的に無効にしたり、プログラムで割り込みを生成したりできます。

新しいsystickベースのタイマーの実装を見てください。
 static volatile uint32_t systick_10ms_ticks = 0; void platform_delay(uint32_t msec) { uint32_t tenms = msec / 10; uint32_t dest_time = systick_10ms_ticks + tenms; while(systick_10ms_ticks < dest_time) { __WFI(); } } // override isr_systick from isr.c void isr_systick(void) { ++systick_10ms_ticks; } 

スタンバイサイクルは、システムカウンターが必要な値を超えるまで、プロセッサを割り込みスタンバイモード(スリープモード)にします。 同時に、10 msごとにSysTickがオーバーフローし、 isr_systickがカウンターを1インクリメントする割り込みを生成しますsystick_10ms_ticks volatileとして宣言されていることに注意してください。メインメモリ(割り込みハンドラが変更する場所)から毎回再読み込みする必要があります。

libgcc


このコードでは、除算演算を初めて使用します。 これは複雑に思えますが、Cortex-M0には除算のためのハードウェア命令はありません:-)。 コンパイラーはこれを認識しており、除算命令の代わりに__aeabi_uidiv関数の呼び出しを挿入し、プログラムで数値を除算します。 この関数(およびいくつかの類似の関数)は、コンパイラサポートライブラリlibgcc.aに実装されています。 残念ながら、リンカはそれについて何も知らず、不快なエラーに遭遇します。
 build/5a3e7023bbfde5552a4ea7cc57c4520e0e458a53_timer.o: In function `platform_delay': timer.c:(.text.platform_delay+0x4): undefined reference to `__aeabi_uidiv' 

正しい解決策は、リンカ呼び出しを直接gcc呼び出しに置き換えることです。これにより、リンク先がすでにわかります。 -nostartfiles 、gccはそれをやややり-nostartfiles可能性があるため、 -nostartfilesを介して初期化コードがあることを通知し、 -nostartfilesを介してアプリケーションが独立しており、OSに依存しないことを-ffreestandingします。

最後に、こんにちはhabr!


このバージョンはUARTドライバーを備えているため、いくぶん重要です。つまり、点滅するLEDだけでなく、コードの実際の動作を確認できます。 しかし、最初に、ドライバー:
platform/protoboard/uart.c
 extern uint32_t platform_clock; void platform_uart_setup(uint32_t baud_rate) { NVIC_DisableIRQ(UART_IRQn); 
まず、NVICの割り込みが有効になっている場合、それをオフにします。
  LPC_SYSCON->SYSAHBCLKCTRL |= (1<<16); LPC_IOCON->PIO1_6 &= ~0x07; LPC_IOCON->PIO1_6 |= 0x01; LPC_IOCON->PIO1_7 &= ~0x07; LPC_IOCON->PIO1_7 |= 0x01; 
次に、ピンを構成するマイクロコントローラーブロックをオンにし、TXD / RXD UARTモードで構成します。 再起動後にUARTが動作しない理由を理解しようとしたときに、このコードは私の血を流しました。 注意してください、時には明白なものはデフォルトでオフになります!
  LPC_SYSCON->SYSAHBCLKCTRL |= (1<<12); LPC_SYSCON->UARTCLKDIV = 0x1; 
これで、UART自体をオンにし、同時に入力分周器を設定できます。
  LPC_UART->LCR = 0x83; uint32_t Fdiv = platform_clock //   / LPC_SYSCON->SYSAHBCLKDIV //      / LPC_SYSCON->UARTCLKDIV //    UART / 16 //   16,   / baud_rate; // , ,   LPC_UART->DLM = Fdiv / 256; LPC_UART->DLL = Fdiv % 256; LPC_UART->FDR = 0x00 | (1 << 4) | 0; LPC_UART->LCR = 0x03; 
従来の8N1モードに加えて、ビットレートを設定する出力ディバイダーへのアクセスを開きます。 除数を計算し、レジスタに書き込みます。 好奇心For盛な人のために、公式はマニュアルのセクション13.5.15にあります。 さらに、より正確なボディレートのための追加のディバイダーについて説明します。 私のテストでは、9580は非常にうまく機能しました:-)
  LPC_UART->FCR = 0x07; volatile uint32_t unused = LPC_UART->LSR; while(( LPC_UART->LSR & (0x20|0x40)) != (0x20|0x40) ) ; while( LPC_UART->LSR & 0x01 ) { unused = LPC_UART->RBR; } 
FIFOをオンにしてリセットし、いくつかの奇妙なデータがレジスタに埋もれていないことを確認します。
  // NVIC_EnableIRQ(UART_IRQn); // LPC_UART->IER = 0b101; 
受信時の中断も含まれます(実際にはそうではありません)。 この例には割り込みハンドラがないため、割り込みも必要ありません。

LPC1768の場合、コードは非常に似ているため、解析しません。 ロード時にすべての周辺機器がオンになり、状況が簡単になることに注意してください。

重要な点:mbedには、外部に3つのUARTが表示され、それぞれにいくつかのピンオプションがあります。 USB通信はかなり多くのコードを必要とするため、FTDIケーブルをUARTにフックする必要があります。この例では、これらはピンP13 / P14です。

まとめると


リンカを見つけました。データベースを拡張してドライバを作成できる既製のバックボーンがあります。 または、メーカーからCMSISとデモを入手することもできます(コードを読むだけで、LPCXpressoの例にはさまざまな悲しみのタイプミスがあります)。

さらに記事を書くのに十分なアイデアはありますが、時間が足りなかったため、あまりにも多くの興味深いものがまだプログラムされていません! それにもかかわらず、私はオフィスデイの「マクロコスモス」の後に埋め込みの「マイクロワールド」に戻ることを試みます。

PSいつものように、テキストを校正してくれたpfactumに感謝します。

クリエイティブコモンズライセンス この作品は、Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unportedの下ライセンスされています。 例のプログラムテキストは、 ライセンスライセンスの下で使用できます(ファイルヘッダーで明示的に指定されている場合を除く)。 この作品は、教育目的のみのために書かれており、著者の現在または以前の雇用主とは一切関係ありません。

Source: https://habr.com/ru/post/J194816/


All Articles