前回の記事では、GPIOやUART、タイマーなどの周辺回路をレジスタによって設定・制御できることを解説しました。
では、CPUはどのようにしてGPIOのレジスタへアクセスしているのでしょうか。
多くのマイコンで使われている仕組みが、メモリマップドI/O(Memory-Mapped I/O:MMIO)です。
メモリマップドI/Oでは、GPIOやUARTなどの周辺回路のレジスタにアドレスを割り当て、CPUから見ると通常のメモリへアクセスするような形で周辺回路を操作できます。
この記事では、メモリ空間とアドレスの基本から、レジスタがどのようにメモリ空間へ配置されるのか、CPUがアドレスを読み書きするとどうしてGPIOが動くのかまで順番に解説します。
メモリマップドI/O(MMIO)とは?
メモリマップドI/O(Memory-Mapped I/O:MMIO)とは、周辺回路のレジスタをCPUのアドレス空間に配置し、メモリアクセスによって周辺回路を制御する仕組みです。
CPUから見ると、RAMのデータを読み書きするときも、GPIOのレジスタを読み書きするときも、「特定のアドレスへアクセスする」という点では似ています。
ただし、そのアドレスが何に接続されているかによって結果が変わります。
- RAMのアドレス → RAMのデータを読み書きする
- GPIOレジスタのアドレス → GPIOの設定や状態を読み書きする
- UARTレジスタのアドレス → UARTの設定や送受信を制御する
上図のように、CPUのアドレス空間にはRAMだけでなく、周辺回路のレジスタへアクセスするための領域も配置されます。
そもそもメモリアドレスとは?
メモリには、それぞれの場所を識別するためのアドレスがあります。
たとえば説明を簡単にするため、次のようなメモリを考えてみます。
アドレス データ 0x1000 0x25 0x1001 0x18 0x1002 0xA0 0x1003 0x03
CPUは「0x1000を読み出す」といった形で、どの場所のデータへアクセスするかを指定します。
アドレスは、メモリ上の住所のようなものだと考えると分かりやすいでしょう。
メモリ空間とは?
CPUがアドレスを使ってアクセスできる範囲を、アドレス空間と呼びます。
マイコンでは、このアドレス空間の中にさまざまな領域が割り当てられています。
アドレス空間 ┌─────────────────┐ │ RAM │ ├─────────────────┤ │ 周辺回路 │ │ GPIO │ │ UART │ │ SPI │ │ I2C │ │ Timer │ ├─────────────────┤ │ Flash など │ └─────────────────┘
実際の配置やアドレス範囲はマイコンによって異なります。
なぜメモリのアドレスでGPIOを操作できるの?
ここがメモリマップドI/Oを理解する重要なポイントです。
たとえば、あるアドレスがGPIOの出力レジスタへ割り当てられているとします。
CPU │ │ 特定アドレスへ書き込み ▼ アドレス空間 │ ▼ GPIO出力レジスタ │ ▼ GPIO周辺回路 │ ▼ 端子の出力が変化
CPUがそのアドレスへ値を書き込むと、RAMへ値が保存されるのではなく、そのアドレスに対応したGPIO周辺回路へアクセスが行われます。
これによってGPIOの出力状態などを変更できます。
RAMへの書き込みとMMIOへの書き込みの違い
プログラムから見ると似たような書き込みでも、そのアドレスがどこへ割り当てられているかによって意味が変わります。
| アクセス先 | 書き込んだ結果 |
|---|---|
| RAM | データがメモリへ保存される |
| GPIOレジスタ | GPIOの設定や出力が変化する |
| UARTレジスタ | UARTの設定や送信などに影響する |
| タイマーレジスタ | タイマーの設定などが変化する |
つまり、同じCPUのロード・ストアという仕組みを利用しながら、アドレスによってアクセス先を切り替えているのがメモリマップドI/Oの基本的な考え方です。
レジスタとの関係
前回学習した周辺回路のレジスタには、それぞれCPUからアクセスするためのアドレスまたはアドレスを決めるためのオフセットがあります。
たとえば説明用として、GPIO出力レジスタが次のアドレスにあるとします。
GPIO_OUTPUT_REG アドレス:0x60000020
CPUがこのアドレスへアクセスすると、GPIO出力レジスタを操作できる、というイメージです。
このアドレスは仕組みを説明するための架空の例です。実際のレジスタアドレスは使用するマイコンの公式資料で確認してください。
レジスタそのものについては「レジスタとは?マイコンのGPIOや周辺回路を制御する仕組みを初心者向けに解説」で詳しく解説しています。
C/C++からMMIOへアクセスするイメージ
C/C++では、アドレスをポインタとして扱うことで特定のメモリアドレスへアクセスできます。
説明用として、次のようなコードを考えてみます。
#define GPIO_REG (*(volatile uint32_t *)0x60000020)
GPIO_REG = 0x00000001;
このコードでは、0x60000020というアドレスを32ビットのレジスタとして扱っています。
もしこのアドレスに実際のGPIOレジスタが割り当てられていれば、値を書き込むことでGPIO周辺回路の動作に影響します。
ここで使用しているアドレスは説明用です。実際のマイコンでそのまま実行するためのコードではありません。
volatileはなぜ必要なの?
レジスタへ直接アクセスするコードでは、volatileというキーワードをよく見かけます。
volatile uint32_t *reg;
volatileは、その値がプログラムから見えない要因によって変化したり、読み書きそのものに意味があったりする可能性があることをコンパイラへ伝えるために使います。
周辺回路のレジスタでは、ハードウェアによって値が変化することがあります。
また、レジスタへの書き込みそのものがハードウェアを動かす操作になる場合があります。
そのためMMIOのレジスタ定義ではvolatileが重要になります。
volatileを付ければ何でも安全になるわけではない
volatileは重要ですが、マルチタスクや割り込みなどで共有するデータを自動的に安全にしてくれる機能ではありません。
複数の処理が同じレジスタや変数へアクセスする場合には、排他制御やアトミック操作など、別の問題も考える必要があります。
まずは「ハードウェアレジスタへのアクセスをコンパイラに勝手に省略・統合されないようにするために使われる」と理解しておくとよいでしょう。
MMIOでもビット演算を使う
MMIOでレジスタへアクセスできるようになると、次に必要になるのがビット単位の操作です。
たとえば説明用のレジスタのビット3を1にしたい場合、次のように書けます。
REG |= (1U << 3);
これまで学習してきた内容が、ここでつながります。
ビット演算
↓
レジスタ
↓
レジスタのアドレス
↓
メモリマップドI/O
↓
GPIOなどのハードウェアが動く
ビット演算については「ビット演算とは?AND・OR・XOR・NOT・シフト演算を初心者向けに解説」もあわせて確認してください。
Read-Modify-Writeにも注意する
レジスタの特定ビットだけを変更するときには、現在値を読み出し、変更し、書き戻すRead-Modify-Writeが行われることがあります。
REG |= (1U << 3);
ただし、すべてのハードウェアレジスタでこの操作が安全とは限りません。
レジスタによっては、読み出した値の意味と書き込む値の意味が異なったり、「1を書き込むとフラグをクリアする」といった特殊な動作をしたりします。
また、ハードウェアや別の処理が同時に値を変更する可能性もあります。
実際のレジスタ操作では、必ず対象マイコンのリファレンスマニュアルを確認します。
set・clear専用レジスタが用意されることもある
マイコンによっては、ビットを安全かつ効率的に変更するために、通常のレジスタとは別にSET用・CLEAR用などのレジスタや機構が用意されている場合があります。
たとえばSET用レジスタへ特定ビットだけが1の値を書き込むことで、対象ビットだけを1にできる、といった仕組みです。
この場合、現在値を読み出してから書き戻す必要がないため、単純なRead-Modify-Writeとは異なる方法で操作できます。
ArduinoではMMIOを意識しなくても使える
Arduinoでは、通常はメモリアドレスを直接指定しなくてもGPIOを操作できます。
pinMode(8, OUTPUT);
digitalWrite(8, HIGH);
Arduino Coreやその下のドライバが、対象マイコンに応じたハードウェア操作を行ってくれるためです。
そのため、通常のアプリケーション開発では無理にMMIOを直接操作する必要はありません。
しかしMMIOを理解すると、Arduinoの関数やドライバのさらに下で、CPUと周辺回路がどのようにつながっているのかを理解できるようになります。
KUMITATE-C3ではどうなっている?
KUMITATE-C3に搭載されているESP32-C3でも、GPIOやUARTなどの周辺回路はレジスタを通して制御されます。
ArduinoでdigitalWrite()を使っている間は、レジスタのアドレスを直接意識する必要はありません。
一方、ESP32-C3のTechnical Reference ManualやESP-IDFのヘッダーファイルを調べると、GPIOをはじめとする周辺回路のレジスタ構成を確認できます。
学習するときは、次の順番で追いかけると理解しやすくなります。
digitalWrite()でGPIOを動かす- Arduino Coreやドライバの実装を見る
- GPIOのレジスタを調べる
- レジスタのアドレスやビットフィールドを確認する
- Technical Reference Manualでハードウェアの動作を確認する
MMIOのアドレスはどこで調べる?
実際の周辺回路レジスタのアドレスを調べる場合は、マイコンメーカーが公開している資料を確認します。
- データシート
- Technical Reference Manual
- リファレンスマニュアル
- SDKのヘッダーファイル
- HALやドライバのソースコード
資料には、レジスタのベースアドレスやオフセット、各ビットの意味、初期値、読み書き属性などが記載されています。
アドレスとオフセットの関係
周辺回路の資料では、すべてのレジスタについて完全なアドレスを書くのではなく、ベースアドレス+オフセットで表現することがあります。
たとえば説明用として、GPIOのベースアドレスが次の値だったとします。
GPIO_BASE = 0x60000000
出力レジスタのオフセットが0x20なら、アドレスは次のように考えられます。
0x60000000 + 0x20 ------------ 0x60000020
このように、1つの周辺回路が持つ複数のレジスタをベースアドレスからの位置として整理できます。
MMIOとポートマップドI/Oの違い
I/Oへのアクセス方法としては、メモリマップドI/O以外にポートマップドI/O(Isolated I/O)という考え方もあります。
メモリマップドI/Oでは、メモリと周辺回路が同じアドレス空間に配置されます。
一方、ポートマップドI/Oでは、メモリとは別のI/O用アドレス空間や専用命令を使用します。
| MMIO | ポートマップドI/O | |
|---|---|---|
| アドレス空間 | メモリと共有 | I/O用に分離 |
| アクセス | 通常のメモリアクセスとして扱う | I/O専用の仕組みを使用 |
マイコンの学習では、まずメモリマップドI/Oを理解しておけば、周辺回路とレジスタの関係を理解しやすくなります。
まとめ
メモリマップドI/Oは、マイコンの周辺回路をCPUのアドレス空間へ配置し、メモリアクセスによって制御する仕組みです。
- MMIOはMemory-Mapped I/Oの略
- CPUはアドレスを指定してデータを読み書きする
- 周辺回路のレジスタにもアドレスが割り当てられる
- 特定のアドレスへ書き込むことでGPIOなどを制御できる
- RAMへのアクセスとMMIOへのアクセスは結果が異なる
- レジスタアクセスではvolatileが重要になる
- ビット演算を使ってレジスタの特定ビットを操作できる
- 実際のアドレスやビットの意味は公式資料で確認する
ここまで理解すると、これまで学んできた内容が一本につながります。
C/C++のプログラム
↓
ビット演算
↓
レジスタ
↓
メモリマップドI/O
↓
GPIO・UART・SPI・I2C・Timer
↓
実際の回路
Arduinoの関数だけを使っていたときには見えにくかった、ソフトウェアから実際のハードウェアが動くまでの流れが、ここまででかなり見えるようになってきます。

