今回は、組み込み開発で頻繁に登場するメモリマップトI/Oについて解説します。
マイコンを使った開発では、GPIOのON/OFF、UARTの設定、タイマ開始など、多くの操作をレジスタ経由で行います。このとき重要になるのが、メモリ空間上の特定アドレスを通じて周辺機能を制御する考え方です。
ただし、単にアドレスへ値を書けばよいわけではありません。ビット演算のやり方を誤ると、意図しないビットを壊したり、設定が反映されなかったりします。特に実務では、Read-Modify-Writeやvolatileの扱いでつまずくことが多いです。
この記事では、メモリマップトI/Oの基本から、レジスタ操作の典型例、ビット演算でハマりやすいポイントまで整理していきます。
メモリマップトI/Oとは
周辺機能をメモリのように扱う仕組み
メモリマップトI/Oとは、周辺機能の制御レジスタをメモリ空間の一部に割り当て、通常のメモリアクセスと同じ形で読み書きする仕組みです。
たとえば、あるGPIO出力レジスタが 0x40020014 に割り当てられている場合、そのアドレスへアクセスすることでピンの出力状態を変更できます。
イメージとしては以下のような形です。
- RAM: 変数や配列を格納する領域
- Flash: プログラムを格納する領域
- Peripheral Register: GPIO, UART, Timerなどの制御用領域
CPUから見ると、これらは同じアドレス空間上に並んでいます。そのため、C言語ではポインタを使ってアクセスできます。
どんな場面で使われるのか
メモリマップトI/Oは、組み込みではほぼ常に使われます。たとえば以下です。
- GPIOの入力/出力設定
- タイマの開始・停止
- UARTの送受信制御
- 割り込み許可ビットの設定
- ADC変換開始
普段HALやSDKを使っていても、その内部では多くの場合レジスタアクセスが行われています。
レジスタ操作の基本
1つのレジスタを読む・書く
C言語では、レジスタアドレスをポインタに変換してアクセスする形が基本です。
#define GPIO_OUT (*(volatile unsigned int *)0x40020014)
GPIO_OUT = 0x00000001;この例では、0x40020014 にあるレジスタへ値を書いています。volatile を付けているのは、コンパイラに「この値は最適化で省略してはいけない」と伝えるためです。
ビット単位で制御する
実際のレジスタは、1つのビットごとに意味を持つことが多いです。
たとえば、bit0 を1にするとLED点灯、bit1 を1にすると別機能有効、というように定義されています。そのため、単純な代入だけでなく、ビットを立てる・落とす・反転する操作が重要になります。
代表的な操作は以下です。
- ビットを立てる:
reg |= (1U << n); - ビットを落とす:
reg &= ~(1U << n); - ビットを読む:
if (reg & (1U << n)) - 複数ビットをまとめて設定する: マスクを使う
ビット演算の実践例
特定ビットを1にする
GPIO_OUT |= (1U << 3);これはbit3だけを1にします。他のビットは維持されます。
特定ビットを0にする
GPIO_OUT &= ~(1U << 3);この書き方でbit3だけを0にできます。
特定ビットの状態を確認する
if (GPIO_OUT & (1U << 3)) {
/* bit3が1のときの処理 */
}フラグ確認や入力状態判定でよく使います。
詰まりやすいポイント
volatile を付け忘れる
レジスタは、プログラムの外側で値が変化する可能性があります。たとえば受信完了フラグや割り込み要因などです。
このとき volatile がないと、コンパイラが「値は変わらない」と判断し、読み直しを省略することがあります。結果として、無限ループやフラグ未検出の原因になります。
while ((STATUS_REG & 0x01) == 0) {
/* 完了待ち */
}このような待機処理では特に重要です。
代入で他ビットを壊してしまう
初心者がやりがちなのが、ビット1つだけ変更したいのにレジスタ全体へ代入してしまうことです。
GPIO_OUT = (1U << 3);この書き方だと、bit3以外はすべて0になります。もし別ビットに重要な設定が入っていた場合、それを消してしまいます。
ビット単位で変更したいなら、|= や &= ~ を使うのが基本です。
Read-Modify-Write の副作用
reg |= mask; のような処理は内部的に、
- レジスタを読む
- 値を変更する
- 書き戻す
という流れになります。これをRead-Modify-Writeと呼びます。
問題は、読み取りと書き戻しの間に別の要因でレジスタ内容が変わる可能性があることです。割り込み処理やハードウェア更新が絡むと、競合の原因になります。
対策としては以下があります。
- 割り込み禁止区間で更新する
- 専用のSET/CLEARレジスタを使う
- データシートで書き込み仕様を確認する
マイコンによっては、出力レジスタ本体ではなく「ビットセット用レジスタ」「ビットクリア用レジスタ」が用意されていることがあります。これを使うと安全に更新しやすくなります。
予約ビットを不用意に変更する
レジスタ仕様書には、予約ビット(Reserved) が含まれていることがあります。これらは「0を書け」「変更しないこと」とされる場合があります。
何も考えずにレジスタ全体へ固定値を書き込むと、将来の互換性や意図しない動作に影響することがあります。実務では、必要なビットだけをマスクして更新するのが基本です。
実務で意識したいポイント
マクロや構造体で可読性を上げる
アドレス直書きは動作確認には便利ですが、保守性は高くありません。
#define UART_CR (*(volatile unsigned int *)0x40011000)
#define UART_CR_TE (1U << 3)このように名前を付けておくと、コードの意味が追いやすくなります。さらに、ベンダ提供ヘッダやCMSISを使うと、レジスタ定義を統一しやすくなります。
データシートを前提に読む
レジスタ操作は、Cの文法だけ分かっていても不十分です。重要なのは、各ビットが何を意味し、どう書くべきかをデータシートやリファレンスマニュアルで確認することです。
特に以下は要チェックです。
- 読み取り専用か書き込み可能か
- 1を書いてクリアするビットか
- 書き込み順序に制約があるか
- 初期値は何か
まとめ

メモリマップトI/Oは、周辺機能をメモリのように扱う組み込み開発の基本です。GPIOやUARTを操作するときも、その本質はレジスタへの読み書きにあります。
一方で、実務では単純な代入だけでは済まず、ビット演算、volatile、Read-Modify-Write、予約ビットの扱いなど、細かな注意点が多くあります。
特に重要なのは以下です。
- レジスタは
volatile付きで扱う - 必要なビットだけを変更する
- レジスタ全体を書き換える影響を意識する
- データシートの仕様を必ず確認する
レジスタ操作は地味ですが、組み込みの安定動作を支える基礎です。ライブラリ任せにせず、下の仕組みを理解しておくと、デバッグや不具合解析がかなりやりやすくなります。
