組み込みプログラミングは難しいと言われます。

ポインタが怖い、割り込みの意味がわからない、スタックオーバーフローって何?コンパイラの最適化で動作が変わる?DMAってどう使うの?――初めて学ぶ方の多くが、これらの言葉に戸惑います。

でも実は、組み込みプログラミング自体が難しいわけではありません。普段のプログラミングとの「前提の違い」 が理解されていないだけなんです。

組み込みプログラミングの本質は、シンプルに2つにまとめられます:

「メモリという『場所』を意識すること」と「実行の『時間』を管理すること」

今回は、この2つの視点から、組み込みプログラミングの世界を見ていきましょう。


普段のプログラミング環境 ― OSが守ってくれる世界

まず、普段のPC・スマートフォン・サーバー向けのプログラミングを思い出してみてください。

これらの環境では、OS(オペレーティングシステム)が様々な「面倒なこと」を代わりにやってくれています。ハードウェアの複雑な部分は隠されていて、私たちは気にする必要がありません。

OSが守ってくれること

  • 仮想メモリ(バーチャルメモリ)
    各プログラムは「自分専用のメモリがある」と思い込めます。実際の物理メモリの配置はOSが管理してくれるので、気にしなくてOK

  • メモリ保護
    間違ったメモリにアクセスしようとすると、OSが「ダメです!」と止めてくれます(セグメンテーションフォルトなど)

  • エラーが出てもシステム全体は止まらない
    プログラムがクラッシュしても、OSが察知して他のプログラムは動き続けます

  • 複数のプログラムが「同時に」動く
    OSが時間を細かく区切って、各プログラムに順番に実行時間を割り当ててくれます(マルチタスク)

  • ファイル・通信などの標準的な操作
    ファイルを開く、ネットワーク通信する――こういった操作はOSが用意してくれた簡単な命令(APIやシステムコール)で実行できます

これらのおかげで、私たちは 「プログラムのロジックを考える」ことだけに集中 できます。メモリがどこにあるか、いつ処理が実行されるか――そういう細かいことは気にしなくていいんです。


組み込みの世界 ― OSがいない「むき出し」の環境

一方、マイコン(STM32など)を使った組み込みプログラミングでは、状況がガラッと変わります。

多くの組み込みシステムはベアメタル(Bare Metal = 裸の金属)、つまりOSがほぼない状態で動きます。(または最小限のリアルタイムOSだけ)

💡 「ベアメタル」って?
金属(ハードウェア)がむき出しの状態、という意味です。普通のOSのような「保護の層」がなく、ハードウェアを直接触ることになります。

メモリやリソースの物理的な制約

  • メモリ容量が少ない
    RAM(作業用メモリ): 数KB〜数百KB、Flash(プログラム保存用): 数十KB〜数MBくらい
    (PCは数GBあるのと比べると、かなり少ないですね)

  • メモリの配置が固定
    プログラムがどこに配置されるかは、最初に決まっていて変えられません(リンカという仕組みで決定)

  • 仮想メモリがない
    「この住所(アドレス)」と言ったら、それは本当の物理的な場所を指します。ごまかしや変換はありません

ハードウェアを直接触る

最も重要なのが、メモリのある場所に値を書き込むと、ハードウェアが直接反応するということです。

例えばこんなコード:

// GPIO(ピンの出力)を制御するレジスタの例(STM32)
#define GPIOA_BASE    0x40020000UL  // GPIOのアドレス
#define GPIOA_BSRR    (*(volatile uint32_t*)(GPIOA_BASE + 0x18))

// この「メモリの場所」に値を書くと...
GPIOA_BSRR = (1 << 13);  // ← LEDが光る!(ピンがHIGHになる)

普通の変数への代入と見た目は同じですが、実際にはハードウェアが物理的に動きます。

これには特別な性質があります:

  • 書き込むと何かが起きる(副作用):ただの計算じゃなく、電気信号が変わる、LEDが光る、モーターが回るなど
  • 順序が大事:書く順番を間違えると、ハードウェアが誤動作することも
  • 時間制約がある:「この操作は〇〇ミリ秒以内にやらないとダメ」という制限があることも

つまり、組み込みプログラミングは物理的なハードウェアと直結しています。プログラムだけの世界ではなく、電気・時間・物理の世界なんです。


1つ目の軸:メモリという「場所」を意識する

組み込みプログラミングで大切な1つ目のポイントは、メモリ(記憶領域)が「実際のどこか」にあることを意識するということです。

メモリの地図を理解する

組み込みでは、メモリは「この範囲はプログラム用」「この範囲はデータ用」と明確に区切られています。

例えばSTM32F401REというマイコンのメモリ配置:

メモリマップ(地図)の例:
0x0800 0000 - 0x087F FFFF : Flash (512KB, 読み取り専用 = プログラムを保存)
0x2000 0000 - 0x2001 7FFF : SRAM (96KB, 読み書き自由 = データを保存)
0x4000 0000 - 0x5FFF FFFF : 周辺機器のレジスタ(GPIO、タイマなど)
0xE000 0000 - 0xE00F FFFF : CPU内部の機器

💡 メモリマップって?
メモリの「地図」です。「この住所にはプログラム」「この住所にはGPIOのスイッチ」というように、何がどこにあるか示しています。

プログラムを実行すると、自動的にこんな風に配置されます:

  • プログラムコード:Flash領域(.textという名前)に保存
  • 初期値つきの変数:Flash → RAMへコピー(.dataという名前)
  • 初期値なしの変数:RAMに配置して0で埋める(.bssという名前)
  • スタック:関数を呼ぶときの情報、ローカル変数など(RAMの上の方から使う)
  • ヒープ:動的にメモリを確保する場所(RAMの下の方から使う)

ポインタって何?

C言語のポインタは、シンプルに言うと 「メモリの住所を持つ変数」 です。

uint32_t value = 0x12345678;  // 変数valueを作る
uint32_t *ptr = &value;       // ptrにvalueの「住所」を入れる

// ptrの値 = valueが置いてある場所(例:0x20000100番地)
// *ptr と書くと、その場所の中身を読み書きできる

組み込みの怖いところは、このポインタがどんな場所でも指せてしまうこと。OSが守ってくれないので、こんな問題が起きます:

  • 存在しない場所にアクセス(OSがいないので誰も止めてくれない → 暴走)
  • スタックとヒープがぶつかる(上から伸びるスタックと下から伸びるヒープが衝突)
  • 配列の外を触る(隣の変数を壊してしまう)
  • もう使ってない場所を触る(ダングリングポインタ = 宙ぶらりんの住所)

メモリという「場所」を正確に理解していないと、こういうバグを見つけるのはとても大変です。


2つ目の軸:実行の「時間」を管理する

組み込みプログラミングで大切な2つ目のポイントは、「いつ」「どの順番で」処理が実行されるかを意識するということです。

CPUは1つずつしか処理できない

CPUの動きはシンプルです:

  1. 「次にやること」が書いてある場所(アドレス)から命令を読む
  2. その命令を実行する
  3. 次の命令に進む

同時に複数のことはできません。 すべて順番に、1つずつ処理していきます。

「割り込み」で流れが変わる

でも、割り込み(Interrupt) という仕組みがあります。これは 「今やってることを中断して、別の処理をする」 機能です:

volatile uint32_t tick_count = 0;

// メインループ(優先度:低)
void main(void) {
    while(1) {
        process_data();  // 実行中...
    }
}

// タイマ割り込みハンドラ(優先度:高)
void TIM2_IRQHandler(void) {
    tick_count++;  // メインループを中断して実行
    TIM2->SR &= ~TIM_SR_UIF;
}

この仕組みによって、以下のような問題が発生します:

1. データの取り合い(データ競合)

// 割り込みとメインループで共有している変数
volatile uint32_t sensor_value = 0;

// 割り込みが起きたときの処理(センサーの値を読む)
void ADC_IRQHandler(void) {
    sensor_value = ADC1->DR;  // センサー値を書き込む
}

// メインの処理
void main(void) {
    uint32_t val = sensor_value;  // 読み取り中に割り込みが起きたら?
}

⚠️ 何が問題?
メインの処理がsensor_valueを読んでいる最中に割り込みが起きて、値が書き換わってしまうかもしれません。

解決策volatileというキーワードを使う、「ちょっと待って」と処理を止める(クリティカルセクション)など

2. 割り込み処理は短く!

割り込みの処理(ISR = Interrupt Service Routineと呼びます)は、できるだけ短時間で終わらせる必要があります。理由:

  • 他の割り込みが起きても対応できなくなる
  • メインの処理がいつ実行されるか予測できなくなる
  • スタック(一時的なメモリ)を使いすぎてしまう

3. **「〇〇秒以内に処理しないとダメ!」という制約

組み込みシステムには、デッドライン(締め切り) があります:

  • モーター制御:PWM(モーター速度制御)の周期内に計算を終わらせる必要がある
  • 通信:相手が決めた時間内に返事を返さないといけない
  • センサー:一定の間隔で必ず測定しないといけない

こういったタイミングの制約を守るために、実行時間を測って最適化することが必要になります。


2つの軸を組み合わせて理解する

メモリという「場所」と、実行の「時間」――この2つを一緒に考えると、組み込みプログラミングの色々な概念がわかりやすくなります。

例1:DMA(Direct Memory Access = 直接メモリアクセス)

DMAは 「CPUの手を借りずに、自動的にメモリをコピーする仕組み」 です。これも2つの視点で見られます:

場所の視点(メモリ)

  • 「どこから」「どこへ」データをコピーするか指定する
  • 例:周辺機器(UARTなど)のレジスタ → メモリ、またはメモリ → メモリ
  • コピーするサイズ(1バイト、2バイト、4バイトなど)も指定

時間の視点(実行タイミング)

  • CPUと同時に動く(本当の意味での並列処理!)
  • 転送が終わったら割り込みで知らせてくれる
  • CPUと同時にメモリを使おうとして競合することもある

例2:スタックオーバーフロー

「スタックオーバーフロー」という怖そうな言葉も、2つの視点で理解できます:

場所の視点(メモリ)

  • スタック:メモリの上の方から下に向かって伸びていく
  • ヒープ:メモリの下の方から上に向かって伸びていく
  • この2つがぶつかると大事故(未定義動作 = 何が起こるかわからない)

時間の視点(実行タイミング)

  • 関数をたくさん呼ぶ(特に再帰関数)とスタックが伸びる
  • 割り込みが何重にも起きる(ネスト)とスタックが伸びる
  • ローカル変数が大きいとスタックが伸びる
  • プログラムを書いている時点ではわからず、実行して初めてわかる

例3:volatileというキーワード

volatile「コンパイラに『勝手に最適化するな』と伝えるキーワード」 ですが、なぜ必要なのかも2軸で理解できます:

場所の視点(メモリ)

  • ハードウェアのレジスタ(GPIOなど)にアクセスするとき
  • ハードウェアやDMAが勝手に値を変えるかもしれない変数

時間の視点(実行タイミング)

  • メインの処理と割り込みの両方で使う変数
  • コンパイラが「これ無駄だから消しちゃおう」と勝手に最適化するのを防ぐ
// volatile がないと、コンパイラが「2回書いてるけど1回でいいんじゃない?」
// と勝手に判断してしまうかもしれない
volatile uint32_t *gpio_odr = (volatile uint32_t*)0x40020014;

*gpio_odr = 0x01;  // LEDをON(必ず実行される)
*gpio_odr = 0x00;  // LEDをOFF(必ず実行される)

本連載で扱う技術要素

本連載では、メモリ空間と実行時間の2軸に基づき、以下の技術要素を段階的に解説します。

Phase 1: メモリ空間の理解(第1〜6回)

  • メモリマップの読解:Flash/SRAM/周辺機器レジスタの配置
  • リンカスクリプトとセクション:.text/.data/.bss/スタック/ヒープ
  • C言語のメモリモデル:変数、配列、構造体の物理配置
  • ビット操作とレジスタアクセス:RMW問題、BSRR機構
  • ポインタの体系的理解:型付きアドレス、キャスト、メモリマップドI/O
  • ポインタ関連の不具合パターン:NULL、ダングリング、境界外、UB

Phase 2: 実行時間の理解(第7〜9回)

  • クロックと実行時間計測:DWT CYCCNTによるサイクル計測
  • 割り込み機構:NVIC、ベクタテーブル、コンテキストスイッチ
  • 割り込み処理の設計パターン:ISRの制約、共有変数、クリティカルセクション
  • 実行時間の最悪値解析:デッドライン、優先度、ジッタ

Phase 3: システム統合(第10〜12回)

  • DMAによる並列処理:CPUオフロード、バス競合
  • リンカの動作とメモリ配置:mapファイル解析、メモリ使用量最適化
  • コンパイラ最適化:-O0/-O2の違い、アセンブラ出力の確認、UBと最適化の関係

これらすべてが、メモリ空間の物理的理解実行時間の厳密な制御という2つの基礎の上に成り立ちます。


まとめ

組み込みプログラミングが「難しく見える」理由は、シンプルにこの2つです:

  1. メモリという「場所」を意識する必要がある
    仮想メモリやメモリ保護がなく、物理的な「住所」を直接操作します

  2. 実行の「時間」を管理する必要がある
    割り込み、DMA、リアルタイム制約など、「いつ」「どの順番で」を厳密にコントロールします

これらは「難しい」というより、OSという「保護の層」がないだけなんです。

逆に言えば、ハードウェアの動きが直接見えるので、何が起きているか明確です。

本連載では、STM32マイコン(ARM Cortex-M4)を使いますが、目的は「STM32の使い方」ではなく、どんな組み込みシステムにも通用する考え方を身につけることです。


次回予告

第1回:マイコンは"アドレスの世界"

次回は、メモリという「場所」の理解の第一歩として、こんなことをやります:

  • CPUは何をしているのか:命令を読んで→解釈して→実行する、このサイクル
  • 「すべてはアドレス」とはどういうこと?:メモリマップドI/Oの仕組み
  • 実際に見てみよう:デバッガでメモリの中身やレジスタを覗く体験

組み込みの本質である「アドレス(住所)」の世界を、一緒に理解していきましょう。

公開予定:2026年2月20日(木)


📍 連載トップページに戻る

【全13回連載】ポインタの先にある組み込みの世界

📚 次回予告

第1回「マイコンは"アドレスの世界"」
CPUは状態機械。メモリから命令を読み、レジスタに書く。
アドレスだけが現実。デバッガでその世界を覗こう。

📅 公開予定: 2026年2月20日(木)


コードの向こう側にあるハードウェアを見る目を養う、それが組み込みエンジニアの第一歩です。