スタックとメモリ管理
スタックは逆向成長するデータ構造であり、アドレスの高い位置にスタック底、低い位置にスタック頂があります。通常、スタックの境界はコンパイル時に確定し、静的領域に隣接して配置されます。
スタックオーバーフローが発生すると、プログラムは未定義の動作を引き起こし、静的領域に侵入してグローバル変数や静的変数を不正に変更する可能性があります。マイクロコントローラでは、関数の実行はスタックに依存しており、関数の実行単位はスタックフレームです。これには、関数の引数、ローカル変数、戻りアドレスが含まれます。
関数内でローカル変数を過剰に定義するとスタックオーバーフローを引き起こす可能性がありますが、ポインタ経由でのデータアクセスや変更はスタックサイズを消費しません。特に、大きな構造体を扱う際は構造体ポインタを使用することを推奨します。
スタックの使用量は動的であり、関数の呼び出しが終了すると対応するスタックフレームは破棄されます。したがって、スタックサイズを設定する際の主な要因は、メイン関数から直接呼び出される個々の関数のスタックサイズ、ローカル変数のサイズ、および引数の数です。
スタック使用の具体例
メイン関数がfunc1を呼び出し、func1がfunc2を呼び出し、その後func3を呼び出す場合のスタック使用状況は以下の通りです:
- メイン関数のスタックフレームが確立される
- func1のスタックフレームが追加される
- func2の呼び出しによりスタックがさらに拡張される
- func2の終了によりスタックが解放される
- func1の終了によりスタックがさらに解放される
- func3の呼び出しにより新しいスタックフレームが確立される
- すべての処理完了後、スタックは初期状態に戻る
共用体と構造体の活用
共用体のベストプラクティス
共用体にアクセスする際は、まず外部名を経由して内部要素にアクセスすることでコードの可読性が向上します。以下にその例を示します:
typedef struct
{
uint8_t id;
union
{
uint8_t status;
struct
{
uint8_t status1:1;
uint8_t status2:1;
uint8_t status3:1;
uint8_t status4:1;
uint8_t status5:1;
uint8_t status6:1;
uint8_t status7:1;
}status_bits;
}status_union;
}device_data_t;
device_data_t device1;
int main()
{
device1.status_union.status_bits.status5 = 1;
printf(" %#x", device1.status_union.status);
return 0;
}
このコードの出力は0x10となります。直接アクセスも可能ですが、大規模プロジェクトでは共用体変数名を経由したアクセス方が構造が明確になります。
周辺デバイスプログラミング
UART通信のベストプラクティス
- デバッグ時の双方向通信: 2つのシリアルポートを同時にデバッグする際は、両方のTXをデバイスに接続し、GNDを共通に接続します。同時にKeilのデバッグモードで受信バッファを監視し、データの整合性を確認します。
- バッファクリア: 受信バッファをクリアする際は、最大値でクリアするのが安全です。現在の書き込みポジションだけをクリアすると、データの不整合が発生することがあります。
- 割り込み設定: UARTを設定する際はNVIC割り込みを有効にし、間違ってUART_IRQNではなくUARTの割り込みを有効にしないように注意します。
- データ通信の問題解決: デバイスが正しく応答しない場合、送信間隔を延長し、受信関数の設定を確認し、両デバイス間のデータ有効性を検証します。
- コールバック関数の活用: データ解析後に特定のアクションを実行する必要があるが、下位レベルで決定したくない場合は、コールバック関数構造体/配列(コールバック関数テーブル)を設定し、上位レベルから関数ポインタを登録します。
周辺デバイスの設定手順
- 周辺デバイスクロックを有効にする
- 周辺デバイスIOクロックを有効にする
- 設定構造体を作成する
- 設定構造体をリセットする
- 設定構造体のパラメータを設定する
- 周辺デバイス設定パラメータを適用する
- 周辺デバイス割り込みを有効にする
- 周辺デバイス自体を有効にする
基本知識とデバッグテクニック
プログラミングに役立つ英語
- local - ローカルな
- direction - 方向
- identifier - 識別子
- wheel - 車輪
- binary - 二進数/二分探索
- kernel - カーネル
デバッグと学び
- 可変長配列: C99標準から可変長配列がサポートされましたが、C++ではサポートされていません。C11ではオプション機能として扱われます。例えば
int a=10; int arr[a];は有効ですが、int arr[a] = {0};は無効です。 - インクルード方法: 標準ライブラリには<>を使用し、自作ライブラリやチップベンダーライブラリには""を使用します。標準ライブラリに""を使用すると、奇妙なバグが発生することがあります。
- 共用体のメモリアライメント: 共用体のメモリ使用は構造体と同様のルールに従います。内部要素のサイズが異なる場合、共用体のサイズは最大要素のサイズ以上になります。
- UART出力設定: UART出力はAF_PPに設定する必要があります。OUT_PPを使用すると出力されません。
- STM32初期化: STM32では構造体を使用した初期化は推奨されません。多くのマクロ定義は強制変換された構造体ポインタであり、この方法での初期化は正しく行われない可能性があります。
- アサートエラー: mircoライブラリの選択を解除、NDEBUGマクロの定義、packにstderrを含めることで解決できます。
- const変数:
const int a = 5;の値が変わる可能性は、スタックオーバーフロー、配列の境界外アクセス、ポインタエラーの3つです。 - 関数設計: MCUの構造上、引数は4つ以下にすることが推奨されます。R0-R3レジスタが引数を受け取るために使用され、それを超えるとオーバーヘッドが増加します。
- アセンブリデバッグ: J-Flash Commanderを使用してアセンブリを確認し、デッドロックの異常を特定します。LRレジスタの値から関数の呼び出し元を追跡できます。
printfと表示更新
バックスペース文字(\b)を使用して、前の表示を上書きできます。例えば、printf("\b\b%2d", value); は最後の2桁を更新します。
uint32_t timCount = (uint32_t)GetSysRunTime();
uint8_t currentValue = 0;
uint8_t lastValue = 0;
while ((timCount < BOOT_DELAY_COUNT) && !GetKeyPressed(&serialKey))
{
timCount = (uint32_t)GetSysRunTime();
currentValue = (BOOT_DELAY_COUNT - timCount) / 1000;
if (currentValue != lastValue)
{
printf("\b\b%2d", currentValue);
lastValue = currentValue;
}
}
lastValueの値は重要で、時間が変化しない場合(1秒未満)は表示を更新する必要がありません。
コード規約
- 多くのコンパイラは31文字以前の名前しか認識しないため、変数名や関数名は3-31文字の範囲に保ちます。
- ppはポインタのポインタ、hはハンドルの接頭辞として使用します。タスク関数はtask_で始めます。
- 関数名は「機能識別子+名詞+動詞」の形式でUnixスタイルを使用し、インデントにはスペースを使用します。
- すべての変数は使用前に初期化し、特に未定義動作のポインタはNULLで初期化します。
- 戻り値の型と関数名の間には1行の空行を入れます。
- 複数の変数を定義する際は、カンマの使用を避け、個別に行を分けます。
- ifとelseのネストは2層以下に留め、必要であれば関数呼び出しやswitch-case文を使用します。
- 実現に影響しない限り、短い文は関数の先頭に配置し、長い文は後方に配置します。
- switch-case文のcaseはbreakとインデントを合わせ、必ずdefaultを含めます。
- 「魔法の数字」の使用を避け、マクロ定義でラップしてロジックを明確にします。
- 代入不可能な内容は判断文の左側に配置し、==と=の混同を避けます。