-ジャックガンスル、「組み込みシステムの設計技術」
こんにちは、Habr!
奇妙に思えるかもしれませんが、私が特に見た「STM32プライマー」の大部分と、一般的なマイクロコントローラーでは、メモリ割り当て、スタック配置、そして最も重要なこととして、メモリオーバーフローの防止など、一般的に何もありません。ある領域は別の領域をほぐし、通常は魅惑的な効果ですべてが崩壊します。
これは、メモリの不足、LEDの点滅が発生する可能性のある、比較的脂っこいマイクロコントローラーを備えたデバッグボードで実行されているトレーニングプロジェクトの単純さによるものもありますが、最近、初心者のアマチュアでさえ、たとえばSTM32F030F4P6タイプのコントローラーへの参照がますます一般的になっています。 、簡単にインストールでき、1ペニーの価値がありますが、メモリユニットはキロバイトです。
このようなコントローラーを使用すると、かなり深刻なことを自分で行うことができます(たとえば、ここでは、STM32F042K6T6で6 KBのRAMを使用して完全に適切な測定を行い、そこから100バイトを少し超える空き容量が残っています)きちんとした。
この精度についてお話したいと思います。 記事は短くなり、専門家は新しいことを何も学びませんが、初心者にはこの知識を強くお勧めします。
Cortex-Mコアに基づくマイクロコントローラーの典型的なプロジェクトでは、RAMは4つのセクションに条件付きで分割されています。
- data-特定の値で初期化されたデータ
- bss-ゼロに初期化されたデータ
- heap-ヒープ(mallocを使用して明示的にメモリが割り当てられる動的領域)
- stack-スタック(メモリがコンパイラによって暗黙的に割り当てられる動的領域)
特定のタスクに割り当てられた他のいくつかの領域でも、まれに、初期化されていない領域(初期化されていない変数-再起動間で値を保持するので便利です)が発生することもあります
それらは物理メモリにかなり具体的な方法で配置されます-実際には、ARMコア上のマイクロコントローラーのスタックは上から下に成長します。 したがって、RAMの最後に、残りのメモリブロックとは別に配置されます。

デフォルトでは、そのアドレスは通常、最新のRAMアドレスと同じであり、そこから成長するにつれて低下します。スタックの非常に不愉快な機能の1つは、そこから成長します:bssに到達してそのトップを書き換えることができ、明示的な方法でそれを知ることはありません
静的および動的メモリ領域
すべてのメモリは、静的に割り当てられた2つのカテゴリに分類されます。 メモリは、プログラムのテキストから明らかであり、その実行順序に依存せず、動的に割り当てられ、その必要量はプログラムの進行状況に依存します。
後者には、ヒープ(mallocを使用してチャンクを取得し、freeを使用して戻る)と、「単独で」拡大および縮小するスタックが含まれます。
一般的に、マイクロコントローラでmallocを使用することは、何をしているのか正確に理解していない限り、 強く推奨されません 。 それらがもたらす主な問題は、メモリの断片化です。10バイトずつ10個ずつ割り当ててから、1秒ごとに解放すると、50バイトは解放されません。 各10バイトの5つの無料の断片を取得します。
さらに、プログラムのコンパイルの段階では、コンパイラーはmallocが必要とするメモリー量を自動的に決定することができません(特に断片化を考慮します。断片化は、要求されたピースのサイズだけでなく、割り当てと解放の順序にも依存します)。したがって、警告することはできません。最後に十分なメモリがない場合。
この問題を回避する方法があります-RAMだけでなく静的に割り当てられた領域内で動作する特別なmalloc実装、mallocの慎重な使用、プログラムロジックレベルで起こりうるフラグメンテーションなどを考慮します。 -しかし、一般的にmallocは触れない方が良いです。
境界とアドレスを持つすべてのメモリ領域は、.LD拡張子を持つファイルに登録されます。これは、プロジェクトをビルドするときにリンカが指向します。
静的に割り当てられたメモリ
したがって、静的に割り当てられたメモリから、bssとdataの2つの領域があります。これらは正式にのみ異なります。 システムが初期化されると、データブロックはフラッシュからコピーされ、そこに必要な初期化値が保存されます。bssブロックは単にゼロで埋められます(少なくともゼロで埋めることは適切な形式と見なされます)。
フラッシュからコピーし、ゼロで埋める-の両方は、明示的な形式でプログラムコードで行われますが、メイン()ではなく、最初に実行される別のファイルで行われ、一度書き込まれ、プロジェクトからプロジェクトにドラッグされます。
ただし、これは今では興味の対象ではありませんが、データがコントローラーのRAMに収まるかどうかをどのように理解するかはわかりません。
これは非常に簡単に認識されます-1つのパラメーターを持つarm-none-eabi-sizeユーティリティにより、プログラムのコンパイル済みELFファイル(多くの場合、Makefileの最後に呼び出しが挿入されるので便利です):

ここで、テキストはフラッシュにあるプログラムデータの量であり、bssとデータはRAMの静的に割り当てられた領域です。 最後の2列は気にしません。これは最初の3列の合計であり、実用的な意味はありません。
合計、静的にRAMにbss +データバイト、この場合-5324バイトが必要です。 コントローラーには6144バイトのRAMがあり、mallocを使用せず、820バイトが残ります。
スタック上に十分あるはずです。
しかし、十分ですか? そうでない場合、スタックは独自のデータに成長し、最初にデータを上書きし、次にデータが上書きしてから、すべてがクラッシュするためです。 さらに、最初のポイントと2番目のポイントの間では、プログラムは処理するデータにゴミがあることに気付かずに動作を継続できます。 最悪の場合、それはすべてがスタックで正常だったときに書き留めたデータであり、現在は読み取り中です(たとえば、一部のセンサーのキャリブレーションパラメーター)。その後、すべてが悪いことを理解する明確な方法がありませんこのプログラムは、何も起こらなかったかのように実行を続け、出力でごみを出します。
動的に割り当てられたメモリ
そして、ここで最も興味深い部分が始まります-物語を1つのフレーズに減らすと、スタックのサイズを事前に決定することはほとんど不可能です。
理論的には 、個々の関数が使用するスタックサイズをコンパイラーに求めてから、プログラムの実行ツリーを返すように求め、そのブランチごとに、このツリーに存在するすべての関数のスタックの合計を計算します。 多かれ少なかれ複雑なプログラムでは、これだけでもかなりの時間がかかります。
その後、いつでも割り込みが発生する可能性があり、そのハンドラーにもメモリが必要であることを思い出してください。
次に、2つまたは3つのネストされた割り込みが発生し、そのハンドラーが...
一般的に、あなたは理解しています。 特定のプログラムのスタックをカウントしようとすることは、エキサイティングで一般的に有用なアクティビティですが、多くの場合、そうしません。
したがって、実際には、私たちの生活のすべてがうまくいくかどうかを少なくとも何らかの形で理解できるようにする1つの手法、いわゆる「記憶絵画」(記憶絵画)が使用されます。
この方法で便利なのは、使用するデバッグツールに依存しないことです。システムに情報を出力する手段が少なくともある場合は、デバッグツールをまったく使用しなくてもかまいません。
その本質は、スタックがまだ正確に小さく、同じ値でプログラム実行の非常に早い段階でbssの終わりからスタックの先頭まで配列全体を埋めることです。
さらに、この値がすでに消失しているアドレスを確認すると、スタックがどこに落ちたのかがわかります。 消去された色自体は復元されないため、チェックは散発的に実行できるため、到達した最大スタックサイズが表示されます。
ペイントの色を定義します-特定の値は重要ではありません。以下では、左手の2本の指でタップしました。 主なことは、0とFFを選択しないことです。
#define STACK_CANARY_WORD (0xCACACACAUL)
- , -, :
volatile unsigned *top, *start; __asm__ volatile ("mov %[top], sp" : [top] "=r" (top) : : ); start = &_ebss; while (start < top) { *(start++) = STACK_CANARY_WORD; }
? top , — ; start — bss (, , *.ld — libopencm3). bss .
:
unsigned check_stack_size(void) { /* top of data section */ unsigned *addr = &_ebss; /* look for the canary word till the end of RAM */ while ((addr < &_stack) && (*addr == STACK_CANARY_WORD)) { addr++; } return ((unsigned)&_stack - (unsigned)addr); }
_ebss , _stack — , , , , .
.
— - check_stack_size() , , , .
.
712 — 6 108 .
Word of caution
— , , 100-% . , , , , , . , , -, 10-20 %, 108 .
, , , .
P.S. RTOS — MSP, , PSP. , — .