Arm

レガシーマイコンからの移行時に絶対ハマる16の落とし穴と回避策

<span id="hs_cos_wrapper_name" class="hs_cos_wrapper hs_cos_wrapper_meta_field hs_cos_wrapper_type_text" style="" data-hs-cos-general-type="meta_field" data-hs-cos-type="text" >レガシーマイコンからの移行時に絶対ハマる16の落とし穴と回避策</span>

組み込み開発において、長年親しまれてきた各半導体ベンダーの「オリジナルコア」。しかし近年の市場シェア集約に伴い、「そろそろCortex-Mを触らなければならなくなった」というエンジニアの方も多いのではないでしょうか。

本記事では、レガシーマイコンとの違いで特にハマりやすい「アドレス・メモリ構成」「レジスタと関数呼び出し」「スタック・割り込み」「排他制御」の要点を抽出し、16のポイントにまとめました。

1. Cortex-Mの歴史と「命令セット」の落とし穴

Cortex-Mの祖先は、1994年頃の「ARM7TDMI」(ゲームボーイアドバンス等に搭載)にまで遡ります。ARMはこの時代から、32ビットの「ARM命令」に加えて、コードサイズを削減するための16ビット命令「Thumb(サム)命令」を導入しました。

Cortexシリーズになり、このThumb命令は16/32ビット混合の「Thumb-2命令」へと進化し、Cortex-Mのベースとなっています。

CPU名とアーキテクチャ名の違い

開発者を混乱させるのが「名称」の二重構造です。私たちが普段呼ぶ「Cortex-M0」などはCPUであり、その根底にはARMが定義するアーキテクチャ名が存在します。

CPU

対応アーキテクチャ

特徴・主な新機能

Cortex-M0 / M0+

ARMv6-M

ローエンド・超低消費電力

Cortex-M3 / M4 / M7

ARMv7-M

メインライン、M4/M7は浮動小数点演算(FPU)等に対応

Cortex-M23 / M33 / M55 / M85

ARMv8-M / ARMv8.1-M

最新世代。TrustZone(セキュア/ノンセキュア分離)、スタックリミットチェックなどのセキュリティ強化

開発時のポイント

CPUごとにサポートする命令セットが異なります。CC++での開発では、IAR Embedded WorkbenchEWARM)などのコンパイラオプションで、使用する「デバイス(マイコン名)」を正しく選択すれば、最適な命令を自動生成してくれます。

2. マニュアルが手元にない?!「マイコン構成」の罠

レガシーマイコンから移行した人が最初に驚くのが、「半導体ベンダーのマニュアルを読んでも、CPU命令や割り込みコントローラ(NVIC)の説明が載っていない」という問題です。

Cortex-Mマイコンは、ARM社が設計した「CPUコア+NVICSysTickタイマー」という標準パッケージ(設計図)を、各半導体ベンダー(STNXP、ルネサス、インフィニオン等)がライセンス契約して自社の周辺機能(GPIOADCUART等)と組み合わせることで製品化されています。

そのため、開発時には以下の2種類(細分化すると4種類)のマニュアルを並行して読む必要があります。

    • 半導体ベンダーの資料: データシート(ピン配置・電気特性)、リファレンスマニュアル(周辺機能の使い方)
    • ARM社の資料: アーキテクチャリファレンスマニュアル、コアごとのテクニカルリファレンスマニュアル(命令セットやNVICの詳細)

3. メモリ配置とレジスタの基本(PC / LR / SP

Cortex-Mのアドレス構成

Cortex-Mは、アーキテクチャで大まかなメモリマップ(アドレス構成)が規定されています。

    • 0x0000_0000〜:コード領域(内蔵フラッシュメモリなど)
    • 0x2000_0000〜:SRAM領域(データ用)
    • 0x4000_0000〜:周辺機能(ペリフェラル)領域
    • 0xE000_0000〜:システム領域CPU設定レジスタなど)

高位アドレス(0xFFFF_FFFF付近)の秘密

Cortex-Mでは、アドレスの最上部には絶対にコードを配置しません。この領域は後述する「割り込みからの復帰」を表すマジックナンバーとしてハードウェアに予約されています。

また、大規模なコア(Cortex-M7など)では、フラッシュや通常のSRAMの遅さをカバーするため、コア直結の超高速メモリ領域であるTCMTightly-Coupled Memoryが用意されており、命令用のITCM、データ用のDTCMに分かれています。バスの渋滞(コンテンション)を防ぐためにも、「命令はコード領域(またはITCM)、データはSRAM(またはDTCM)」と明確に分離して配置するのが性能向上の基本です。

押さえておくべき3つの特殊レジスタ

Cortex-Mには16本の汎用レジスタ(R0R15)がありますが、下位の4本には特殊な役割があります。

    • PC(プログラムカウンター / R15): 現在実行しているプログラムのアドレスを指します。
    • LR(リンクレジスタ / R14): 関数の「戻り先アドレス」を保持します。Thumb命令の特性上、実際の戻り先アドレスの最下位ビットに1が立った値(Thumb状態を示すフラグ)が格納されます。
    • SP(スタックポインター / R13): スタック領域の最上部を指します。

4. 関数呼び出しのルール(ABI)とスタックの挙動

引数と返り値のルール(ABI

C/C++関数がアセンブラレベルでどう処理されるかは、ABIApplication Binary Interface)という規約で決まっています。

    • 引数: 最初の4つまではレジスタ R0, R1, R2, R3 を使って高速に渡されます。5つ目以降はスタックに積まれるため、関数設計時は「引数は4つ以下」に抑えるのが効率的です。
    • 返り値: 基本的に R0 レジスタに入れて返されます。

関数から戻る命令が毎回違う?

レガシーマイコンでは関数から戻る命令は一意であることが多いですが、Cortex-Mではコンパイラの最適化によって変化します。単純な関数では BX LRLRのアドレスへジャンプ)が使われますが、関数の中でさらに別の関数を呼ぶ(ネストする)場合は、LRの値を一度スタックに退避させます。その復帰時に、スタックからLRに戻さず、直接PC(プログラムカウンター)に値をポップする POP {R4, PC} のような命令に置き換わることで、1命令で関数を終了させる工夫がなされています。

FPU(浮動小数点演算)を使うとスタックを大量消費する?

スタック(CSTACK領域)は、ローカル変数、関数のネスト時のレジスタ退避、そして「割り込み時のレジスタ退避」に使用されます。ここで注意が必要なのがFPU(ハードウェア浮動小数点演算ユニット)の有無です。FPUを有効にしていると、割り込み発生時に通常の整数レジスタ(R0-R3, R12, LR, PC, xPSR)だけでなく、FPUレジスタ(S0-S15)やステータスレジスタもハードウェアによって自動的にスタックへ退避されます。

これにより、FPU使用時は割り込み1回あたりのスタック消費量が劇的に増加するため、スタックオーバーフロー(暴走やBusFaultの原因)を引き起こしやすくなります。

MSPPSP2つのスタックポインター

Cortex-Mには、実はスタックポインターが2種類あります。

    • MSP(メインスタックポインター): リセット直後や割り込みハンドラー実行時に使用。
    • PSP(プロセススタックポインター): OSのタスク(アプリケーション)実行時に使用。

これらを切り替えて運用することで、万が一アプリケーション(PSP側)でスタックオーバーフローが発生しても、OSのスケジューラーや割り込み(MSP側)の領域を破壊せず、システムを安全に復旧(リライアブル化)させることが可能になります。

5. 「割り込み」の常識を覆す3つのポイント

レガシーマイコンの常識が通用しない、Cortex-Mの割り込み仕様における最大の注意点は以下の3つです。

リセット直後から「割り込み許可」状態である

多くのレガシーマイコンは、リセット直後は割り込み禁止状態になっています。しかし、Cortex-Mはリセットがかかった段階で、CPUの割り込みマスクレジスタ(PRIMASK)が 0(割り込み可能状態)になっています。周辺機能やNVICを設定した瞬間に割り込みが飛び込んでくる可能性があるため、初期化順序には注意が必要です。

スタックポインターの初期化は「ハードウェア」が行う

レガシーマイコンのスタートアップルーチン(アセンブラコード)の最初には、必ずスタックポインターの初期値を代入する記述(例:MOV SP, #INIT_SP)がありました。しかしCortex-Mでは、ベクターテーブルの「1番目のワード」にスタック初期値、「2番目のワード」にリセットベクター(PCの初期値)を配置しておけば、リセット時にハードウェアが自動でSPPCに値をロードしてくれます。そのため、スタートアップコード内にSP初期化の記述がないケースが一般的です。

割り込みハンドラーは「普通の関数」でよい

通常、割り込み関数には専用の修飾子(__interruptなど)をつけ、割り込み復帰専用命令(RETIなど)を出力させる必要がありました。

しかしCortex-Mでは、引数なし・返り値なし(void func(void))の普通の関数として割り込みハンドラーを記述できます。

なぜこれで正常に戻れるのでしょうか?

割り込みが発生すると、ハードウェアが自動的にレジスタをスタックに退避させ、LR(リンクレジスタ)に 0xFFFF_FFFx という特殊なマジックナンバー(EXC_RETURN)をセットします。関数終了時にこのアドレス(0xFFF...領域)へジャンプしようとすると、CPUが「あ、これは割り込みからの復帰要求だな」と判断し、ハードウェアがスタックから元のレジスタ(PC等)を自動復帰させる仕組みになっているからです。

6. 最も難解なテーマ:「排他制御」のスマートな使い分け

複数のタスクや割り込みハンドラーから、同じ共有資源(グローバル変数や周辺レジスタ)にアクセスしてビットを書き換える際、ロード・演算・ストアの間に処理が割り込まれるとデータの一貫性が壊れます(競合状態)。これを防ぐのが排他制御です。

Cortex-Mにおける排他制御にはいくつかの手法があり、コアやマイコンの世代によって実装状況が異なります。

排他制御の手法

メカニズム

メリット / デメリット

割り込み禁止



(__disable_interrupt)

一時的にCPU全体の割り込みを止め、タスク切り替えを防ぐ。

一番シンプルだが、割り込み応答性が悪化する。



マルチコアマイコンの場合、別のコアの動作は止められない。

ビットバンド

1ビットの操作を、専用の別アドレス(32ビット空間)へのアクセスにマッピングし、ハードウェアが排他を保証する。

1命令で安全にビット操作ができる。



近年の新しいマイコン(キャッシュ搭載コア等)では、一貫性保持の観点からほぼ廃止されている。

アクワイア・リリース命令



(LDREX / STREX)

排他アクセス用のロード(LDREX)を行い、書き込み(STREX)を行うまでの間に他からアクセスがなかったかをハードウェアで監視する。成否がレジスタに返るため、失敗時はリトライする。

近年のメインラインCortex-Mにおける標準的な排他制御。割り込みを止めない。



Cortex-M0 / M0+ / M1などのローエンドコアには命令自体が実装されていない。

マイコンによる「制限事項」の罠

アーキテクチャ(CPUコア)としては LDREX / STREX 命令に対応していても、マイコン(半導体ベンダーの実装)によっては、内部に排他監視用の「グローバルモニター」が搭載されておらず、これらの命令が常に「失敗」のステータスを返す(実質使えない)制限事項を持つマイコンが存在します(例:ルネサス RA2シリーズなど)。CPU仕様を鵜呑みにせず、必ず各マイコンのハードウェアマニュアルや制限事項(エラッタ)を確認してください。

まとめ:フレッシャーズ&移行エンジニアの皆様へ

Cortex-Mは非常に合理的で使いやすい優秀なアーキテクチャですが、レガシーマイコンの「当たり前」を引きずったまま開発に入ると、スタートアップや排他制御、スタックの挙動で思わぬ落とし穴にハマることがあります。

    • 「多分こうだろう」という思い込みは危険です。
    • 半導体ベンダーのマニュアルと、ARMのマニュアル、双方を見る癖をつけましょう。
    • IAR Embedded Workbench(EWARM)などのデバッカーを活用し、実機でレジスタやスタックの動きをステップ実行しながら目で確認することが、理解への一番の近道です。

しっかりと基礎を押さえ、安全で高品質な組み込みシステム開発を進めていきましょう!