今回は、STM32CubeIDEのデバッグ機能を使い、プログラムの実行に伴って変化するマイコン内部の状態を、具体的な数値(アドレス)として確認していきます。C言語の構文の裏側にある 「アドレス(住所)」 という物理的な座標を理解することが、このPhaseの目的です。

前回の第0回では、組み込みプログラミングが「場所(メモリ)」と「時間(実行タイミング)」の世界であることを説明しました。今回は、その「場所」を実際にデバッガで覗いてみましょう。


📖 前回の記事

第0回:なぜ組み込みは難しく見えるのか ― 場所と時間の話 ―

📍 連載トップページ

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


1. デバッガの基本操作と役割

1-1. なぜデバッガが必要なのか

通常のPC環境でのプログラミングでは、printf()console.log() といった出力関数を使ってプログラムの状態を確認できます。しかし、マイコンには画面もコンソールもありません。

そこで登場するのがデバッガです。デバッガは、プログラムを一時停止させ、その瞬間のCPU内部の状態(レジスタの値、メモリの内容)を可視化するツールです。

STM32では、ST-Linkという専用のハードウェアデバッガを介して、PCからマイコンの内部状態をリアルタイムに覗くことができます。これは、STM32開発ボードに最初から搭載されているため、追加の機材は不要です。

1-2. デバッガの仕組み

デバッガは、CPUのJTAG(ジェイタグ)やSWD(シリアル・ワイヤ・デバッグ)という専用のデバッグインターフェースを通じて、以下のことができます:

  • プログラムの実行を一時停止(ブレーク)
  • CPUの内部レジスタ(PC、SP、汎用レジスタなど)の読み取り
  • メモリ(Flash、SRAM)の内容の読み書き
  • プログラムを1命令ずつ実行(ステップ実行)

補足:JTAG/SWDとは?
これらは、プログラムを実行しながら内部状態を監視するためのハードウェアインターフェースです。マイコンの特定のピンに接続することで、ST-Linkなどのデバッガツールがマイコンの「神経系」に直接アクセスできるようになります。

1-3. デバッグ操作の基本

マイコン内部の状態をリアルタイムに監視するため、まずSTM32CubeIDEのデバッグ操作(Step実行)を定義します。 IDEのデバッグコンソール

  • ステップオーバー(F6)
    処理を一行ずつ実行します。
    関数の呼び出しがある場合、その内部には入らずに実行結果のみを確認したい場合に使用します。
    例えば、HAL_Delay(1000); という関数呼び出しがあった場合、HAL_Delay()の内部処理には入らず、「1秒待つ」という結果だけを受け取って次の行に進みます。

  • ステップイントゥ(F5)
    関数内部の具体的な処理まで追いかけたい場合に使用します。
    HAL_GPIO_WritePin() の内部でレジスタがどう書き換えられているかを詳しく見たい場合などに使います。
    ただし、HAL(Hardware Abstraction Layer:ハードウェア抽象化層)の内部は複雑なので、最初はステップオーバーで十分です。

  • ブレークポイントとレジューム(F8)
    特定の行(ソースコード左端)をダブルクリックして停止点(ブレークポイント)を設定します。
    「レジューム(F8)」を実行すると、次の停止点まで処理を連続実行します。
    これは、ループの中で特定の条件下だけを確認したい場合に非常に便利です。例えば、while(1) ループの中で変数が変化する瞬間を繰り返し観察できます。

デバッグのコツ:
最初は「ステップオーバー(F6)」で全体の流れを掴み、気になる関数だけ「ステップイントゥ(F5)」で詳しく見る、という使い分けが効果的です。


2. PC(プログラムカウンタ)による現在地の特定

2-1. プログラムカウンタとは?

プログラムカウンタ(PC: Program Counter) は、CPUが「今どのアドレスの命令を実行しているか」を記録する特殊なレジスタです。

C言語でプログラムを書くと、コンパイラがそれを機械語(バイナリ命令)に変換し、Flashメモリに保存します。CPUは、PCが指すアドレスからバイナリ命令を1つ読み込み、それを実行し、PCを更新して次の命令へ進む、というサイクルを延々と繰り返します。

1. PCが指すアドレスから命令を読み込む(Fetch:フェッチ)
2. 命令を解読する(Decode:デコード)
3. 命令を実行する(Execute:エグゼキュート)
4. PCを更新して次の命令へ
5. 1に戻る

このサイクルは、フェッチ・デコード・エグゼキュートサイクルと呼ばれ、すべてのCPUの基本動作です。

2-2. 実際にPCを確認してみる

デバッグを開始し、命令を一行進めるごとに、CPU内部の「現在地」がどのように推移するかを確認します。

PCレジスタの表示

解説: STM32CubeIDEの「Registers」ビューで pc(プログラムカウンタ)の項目を確認してください。ここでは 0x80004ce という値を指しています。これは、CPUが現在実行している命令が格納されている Flashメモリ上の物理アドレス です。

2-3. アドレスの読み方(16進数)

0x80004ce という表記を見てみましょう。

  • 0x:これは「16進数(Hexadecimal)ですよ」という接頭辞です。
  • 08:Flash領域の開始を示す部分(詳しくは次のセクションで説明)
  • 0004ce:Flash領域内での相対的な位置

16進数は、0〜9の数字と A〜F の文字を使って表現します。1桁で0〜15を表現できるため、メモリアドレスのような大きな数値を短く表記するのに便利です。

10進数 16進数 2進数
0 0x0 0000
10 0xA 1010
15 0xF 1111
255 0xFF 11111111
2316 0x4ce 010011001110

なぜ16進数を使うのか?
コンピュータは2進数で動作しますが、2進数は桁数が多くて人間には読みにくいです。16進数なら、2進数の4桁を1桁で表現できるため、「人間が読みやすく、コンピュータの動作とも対応しやすい」という特徴があります。

2-4. CPUの動作原理

CPUは、このPCが指し示すアドレスから命令(バイナリデータ)を読み込み、実行完了後にPCの値を更新して次の命令へ進む、というサイクルを繰り返す状態機械です。

例えば、以下のようなC言語のコードがあったとします:

int main(void) {
    int counter = 0;       // アドレス 0x08000500 に格納された命令
    counter++;             // アドレス 0x08000504 に格納された命令
    HAL_GPIO_WritePin(...);// アドレス 0x08000508 に格納された命令
}

実際には、コンパイラがこれを機械語に変換し、Flashに書き込みます。デバッガでステップ実行すると、PCが 0x080005000x080005040x08000508 と増えていく様子が観察できます。


3. アドレス空間の構造(メモリマップ)

3-1. すべてはアドレスで管理されている

マイコンのすべてのリソース(メモリ、周辺機器)は、単一のアドレス空間上に配置されています。これは、街の住所のようなものです。

例えば、東京都の住所には「千代田区」「新宿区」「渋谷区」といった区画があるように、マイコンのアドレス空間にも「Flash領域」「SRAM領域」「周辺機器領域」という区画があります。

3-2. STM32のメモリマップ

アドレス範囲 物理領域 主な役割 サイズ例(STM32F4の場合)
0x0800 0000 〜 Flash領域 プログラム命令(読み出し専用)の保存 512KB 〜 2MB
0x2000 0000 〜 SRAM領域 変数などの動的なデータ保持 64KB 〜 512KB
0x4000 0000 〜 周辺機器レジスタ GPIOや通信機能などのハードウェア設定窓口 (機能ごとに配置)

補足:Flash vs SRAM

  • Flash:電源を切っても内容が消えない不揮発性メモリ。プログラムコードや定数データを保存します。書き込み回数に制限があり、書き込み速度も遅いですが、読み出しは高速です。
  • SRAM:電源を切ると内容が消える揮発性メモリ。変数や一時データを保存します。読み書きが非常に高速で、書き込み回数の制限もありません。

3-3. メモリマップドI/O

ハードウェアの制御をメモリ操作として扱うこの仕組みは、メモリマップドI/O(Memory-Mapped I/O) と呼ばれます。

例えば、GPIOポートAのデータ出力レジスタは 0x40020014 というアドレスに配置されています。このアドレスに値を書き込むと、物理的なピンの電圧が変化します。

// 0x40020014 番地に 0x0020 という値を書き込む
*(uint32_t*)0x40020014 = 0x0020;
// → GPIOA の 5番ピンが HIGH になる(LED点灯など)

これは、特定の住所にある「スイッチ」を押すイメージです。その住所にデータを送ると、ハードウェアが反応するのです。

3-4. なぜアドレスで管理するのか?

すべてを「アドレス」という統一的な方法で扱うことで、CPUの設計がシンプルになります。CPUは、「メモリからデータを読む」「メモリにデータを書く」という2つの基本操作だけで、すべてのハードウェアを制御できるのです。

これにより、プログラムコードの読み込みも、変数の更新も、ハードウェアの制御も、すべて同じ命令セットで実現できます。


4. 実践:レジスタ書き換えによるハードウェアの有効化

4-1. レジスタとは何か?

レジスタ(Register) という言葉は、組み込みプログラミングで頻繁に登場しますが、文脈によって2つの意味があります:

  1. CPUレジスタ:CPU内部の超高速な一時記憶領域(R0、R1、PC、SPなど)
  2. 周辺機器レジスタ:周辺機器(GPIO、UART、タイマーなど)の設定や状態を管理するメモリ上の特定アドレス

ここでは、周辺機器レジスタについて説明します。

周辺機器レジスタは、特定のメモリアドレスに配置された「設定窓口」です。このアドレスに値を書き込むことで、ハードウェアの動作を制御できます。

4-2. GPIOレジスタの種類

例えば、GPIOA(General Purpose Input/Outputポート A)には、以下のようなレジスタがあります:

レジスタ名 アドレス 役割
MODER 0x40020000 ピンのモード設定(入力/出力/代替機能/アナログ)
OTYPER 0x40020004 出力タイプ設定(プッシュプル/オープンドレイン)
OSPEEDR 0x40020008 出力速度設定
PUPDR 0x4002000C プルアップ/プルダウン設定
ODR 0x40020014 出力データレジスタ(ピンのON/OFF)
IDR 0x40020010 入力データレジスタ(ピンの状態読み取り)

これらのレジスタに適切な値を書き込むことで、ピンを「出力モード」に設定したり、ピンの電圧を HIGH/LOW に切り替えたりできます。

4-3. 実際に初期化関数を実行してみる

MX_GPIO_Init(); などの初期化関数をステップオーバー(F6) で実行し、周辺機器レジスタの値の変化を観察します。

初期化後の周辺機器レジスタ

解説: 関数実行に伴い、GPIOAの基底アドレス(0x40020000 付近)にある各レジスタに設定値が書き込まれます。この書き込みにより、特定のピンが「出力モード」に設定されるなど、ハードウェアの物理的な状態が確定します。数値が確定し、黄色くハイライトされている状態を確認できます。

4-4. 何が起こっているのか?

MX_GPIO_Init() の内部では、以下のような処理が行われています:

// GPIOA クロック有効化(電源ON)
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

// PA5 を出力モードに設定
GPIOA->MODER &= ~(0x3 << (5 * 2));  // 既存の設定をクリア
GPIOA->MODER |= (0x1 << (5 * 2));   // 01 = 出力モード

// PA5 をプッシュプル出力に設定
GPIOA->OTYPER &= ~(0x1 << 5);       // 0 = プッシュプル

// PA5 を低速出力に設定
GPIOA->OSPEEDR &= ~(0x3 << (5 * 2)); // 00 = 低速

これらの命令は、すべて「特定のアドレスに特定の値を書き込む」という操作です。つまり、ハードウェアの設定は、メモリへの書き込みで行われるのです。


5. 実践:SRAM上でのデータ更新(インクリメント)

5-1. 変数はメモリ上のどこにある?

次に、while(1) ループ内で変数 counter を更新し、SRAM(メモリ)の特定アドレスが書き換わる様子を確認します。効率的に確認するため、counter++ の行にブレークポイントを設置し、F8(レジューム) で繰り返し停止させます。

以下のようなコードを想定します:

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();

    uint32_t counter = 0;  // 32ビット符号なし整数(4バイト)

    while(1) {
        counter++;         // ← ここにブレークポイント
        HAL_Delay(1000);
    }
}

5-2. 変数の変化を観察する

VariablesビューとMemoryビューの比較

解説: 変数 counter は、SRAM上のアドレス 0x20017FF4 に配置されています。

左側(初期状態): プログラム開始直後、counter = 0 の状態です。Variablesビューで値が 0 と表示され、Memoryビューで同アドレス 0x20017FF4 を参照すると、格納されている生データは 00 00 00 00 です。

右側(10回ループ後): ブレークポイントで10回停止させた後、counter = 10 に更新されています。Variablesビューで値が 10 と表示され、Memoryビューでは同アドレスの内容が 0A 00 00 00 へと書き換わっていることが実測値として確認できます(0A は16進数で、10進数の 10 を表します)。

このように、C言語の変数の値の変化は、物理メモリの特定アドレスに格納されたバイト列の変化として、デバッガで直接観察できます。

5-3. リトルエンディアンとは?

ここで注目すべきは、値 10 がメモリ上では 0A 00 00 00 と表現されている点です。

uint32_t は32ビット(4バイト)の整数なので、メモリ上では4バイト分の領域を使います。数値 10 を32ビットで表現すると、以下のようになります:

10進数:10
2進数:00000000 00000000 00000000 00001010
16進数:0x0000000A

これを4バイトに分けると:

バイト位置 値(16進) 値(10進)
最下位バイト 0x0A 10
下から2番目 0x00 0
下から3番目 0x00 0
最上位バイト 0x00 0

STM32(ARMアーキテクチャ)は、リトルエンディアン(Little Endian) という方式を採用しており、最下位バイトから順に格納します。

したがって、メモリ上では以下のように配置されます:

アドレス 格納値
0x20017FF4 0x0A(最下位バイト)
0x20017FF5 0x00
0x20017FF6 0x00
0x20017FF7 0x00(最上位バイト)

Memoryビューでは、これらが連続して 0A 00 00 00 と表示されます。

補足:ビッグエンディアン vs リトルエンディアン

  • リトルエンディアン:最下位バイトを先頭アドレスに格納(STM32、x86など)
  • ビッグエンディアン:最上位バイトを先頭アドレスに格納(一部のネットワーク機器など)

どちらが優れているということはありませんが、データをやり取りする際には統一する必要があります。

5-4. 変数操作の本質

組み込みにおいて「変数の値を操作する」とは、物理メモリの特定のアドレスに格納されたビット列をCPUが更新するという物理現象に他なりません。

C言語の counter++; という1行は、コンパイルされると以下のような機械語命令になります:

1. アドレス 0x20017FF4 から値を読み込む(LOAD命令)
2. その値に 1 を加算する(ADD命令)
3. アドレス 0x20017FF4 に結果を書き戻す(STORE命令)

このように、高級言語の抽象的な操作は、最終的にすべて「アドレスを指定してメモリを読み書きする」という基本操作に変換されます。


6. デバッグの実践的なTips

6-1. Memoryビューの開き方

STM32CubeIDEで特定のアドレスのメモリ内容を確認するには:

  1. デバッグモードで停止中に、WindowShow ViewMemory を選択
  2. Memoryビューの「+」ボタンをクリック
  3. アドレス(例:0x20017FF4)を入力するか、変数名(例:&counter)を入力
  4. Enterキーを押すと、そのアドレスの内容が表示される

6-2. レジスタビューの活用

Registersビューでは、CPU内部の全レジスタと周辺機器レジスタが確認できます:

  • Core Registers:PC、SP、LR、汎用レジスタ(R0〜R15)など
  • Peripheral Registers:GPIOA、USART1、TIM2など(プロジェクトによって表示内容が変わる)

周辺機器レジスタを展開すると、個々のビットフィールド(例:MODER、OTYPER)も確認できます。

6-3. データブレークポイント

通常のブレークポイントは「特定の行に到達したら停止」ですが、データブレークポイント(Watchpoint) は「特定のメモリアドレスの値が変化したら停止」という機能です。

使い方:

  1. Variablesビューで変数を右クリック
  2. Add Watchpoint (C/C++) を選択
  3. アドレスが変化すると自動的に停止する

これにより、「どこで変数が書き換えられたか不明」という問題を解決できます。

6-4. Expressionsビュー(式の監視)

デバッグ中に特定の式や変数を継続的に監視するには、Expressionsビューが便利です:

  1. WindowShow ViewExpressions を選択
  2. Add new expression(緑の+アイコン)をクリック
  3. 監視したい式を入力(例:counter * 2counter > 100など)

プログラムが停止するたびに、登録した式が自動的に評価され、結果が表示されます。

注意:スコープの制限
Expressionsビューで評価できるのは、現在のスコープ内にある変数のみです。例えば:

  • ローカル変数 counter は、その関数内で実行が停止している場合のみ評価可能
  • 関数を抜けるとスコープ外になり、評価できなくなります
  • グローバル変数や static 変数は、どこで停止していても評価可能

ローカル変数を常時監視したい場合は、グローバル変数static変数として宣言するか、Variablesビューで直接確認する方が確実です。

6-5. SFRビュー(周辺機器レジスタの監視)

周辺機器レジスタを細かく監視するには、SFR(Special Function Registers) ビューを使います:

  1. WindowShow ViewSFRs を選択
  2. 周辺機器(例:GPIOA、TIM2)を展開すると、各レジスタの値とビットフィールドが表示される
  3. プログラムが停止するたびに、値が自動的に更新される(変更があれば黄色でハイライト)

これは、Registersビューよりも見やすく、ビット単位での変化が確認しやすいです。


7. よくある疑問(FAQ)

Q1. なぜFlashとSRAMに分かれているの?

A: 役割が違うためです。

  • Flashは不揮発性なので、電源を切ってもプログラムが消えません。しかし、書き込みが遅く、書き込み回数にも制限があります。
  • SRAMは揮発性ですが、読み書きが高速で、書き込み回数に制限がありません。一時的なデータ保存に最適です。

この2種類を使い分けることで、効率的なシステムを構築できます。

Q2. 変数のアドレスはどうやって決まるの?

A: コンパイラとリンカが自動的に決定します。

コンパイル時に、変数のサイズと配置ルールに従って、リンカがアドレスを割り当てます。通常、グローバル変数は固定アドレスに配置され、ローカル変数はスタック上に動的に配置されます(この詳細は次回解説します)。

Q3. レジスタアドレスはどこで確認できる?

A: STマイクロエレクトロニクスが提供するリファレンスマニュアルに記載されています。

例えば、STM32F446の場合は「RM0390 Reference Manual」で、すべての周辺機器レジスタのアドレスとビット定義が詳細に解説されています。STの公式サイトから無料でダウンロードできます。

Q4. デバッガで見える値と、実際の動作が違うことがある?

A: コンパイラの最適化が原因の可能性があります。

デバッグビルド(-O0)では最適化が無効化されているため、変数とメモリが1対1対応しています。しかし、リリースビルド(-O2-O3)では、コンパイラが変数をレジスタに保持したり、不要な処理を削除したりするため、ソースコードとメモリの対応が崩れることがあります。

デバッグ時は、必ずDebugビルド設定を使いましょう。

Q5. 間違ったアドレスにアクセスするとどうなる?

A: マイコンによって動作が異なります。

  • メモリ保護機能(MPU)がある場合:例外(HardFault)が発生し、プログラムが停止します。
  • メモリ保護機能がない場合:未定義の動作となり、予期しない値が読み書きされたり、システムが暴走したりします。

STM32の多くのシリーズにはMPU(Memory Protection Unit)が搭載されており、適切に設定すれば不正アクセスを検出できます。


第1回のまとめ

今回は、デバッガを使ってマイコン内部の「アドレスの世界」を覗いてみました。重要なポイントをおさらいしましょう:

  • CPUの実行制御:PC(プログラムカウンタ)によってFlash上のアドレスを順次参照する。CPUは「フェッチ・デコード・エグゼキュート」のサイクルを繰り返す状態機械。

  • ハードウェア設定:周辺機器レジスタという特定アドレスに値を書き込み、回路を有効化する。メモリマップドI/Oにより、ハードウェア制御もメモリ操作として統一的に扱える。

  • データ管理:SRAM上の特定アドレスの数値を更新することで変数を表現する。リトルエンディアン形式で、最下位バイトから順に格納される。

すべてが「アドレス」という座標系で管理されており、C言語の構文はその座標操作を抽象化したものに過ぎません。これが組み込みの世界の本質です。

「変数」も「ハードウェア制御」も「プログラムの実行」も、すべて 「特定のアドレスに対する読み書き」 という統一的な操作で実現されています。

今回学んだこと

✅ デバッガの基本操作(ステップオーバー、ステップイントゥ、ブレークポイント)
✅ プログラムカウンタ(PC)の役割と、CPUの命令実行サイクル
✅ メモリマップの構造(Flash、SRAM、周辺機器レジスタ)
✅ 周辺機器レジスタへの書き込みによるハードウェア制御
✅ SRAM上の変数の実体と、リトルエンディアン形式
✅ デバッグツールの実践的な使い方(Memoryビュー、Registersビュー)

次のステップ

次回は、これらの膨大なアドレス空間が、リンカによってどのように区画整理(セクション分け)されているのかを解説します。

  • .textセクション:プログラムコードが格納される領域
  • .dataセクション:初期化済みグローバル変数の領域
  • .bssセクション:未初期化グローバル変数の領域
  • Stack(スタック):関数呼び出しとローカル変数の領域
  • Heap(ヒープ):動的メモリ確保の領域

これらの配置を理解することで、「スタックオーバーフロー」や「メモリ不足」といった問題の原因と対策が見えてきます。


次回予告:メモリ地図を読む(Flash/RAM/Stack/Heap)

次回は、これらの膨大なアドレス空間が、リンカによってどのように区画整理(セクション分け)されているのかを解説します。
メモリの物理配置と、スタック・ヒープの領域管理の仕組みを詳解します。


連載目次: