メモリマップドI/O(MMIO)とは?アドレスでマイコンの周辺回路を制御する仕組みを解説

組み込み基礎

前回の記事では、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の設定や送受信を制御する
メモリマップドI/Oの仕組み。CPUのアドレス空間にRAM、フラッシュ、GPIOなどの周辺回路レジスタが配置され、アドレスへの読み書きで周辺回路を制御する図
メモリマップドI/OによってCPUのアドレス空間から周辺回路のレジスタへアクセスする仕組み

上図のように、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をはじめとする周辺回路のレジスタ構成を確認できます。

学習するときは、次の順番で追いかけると理解しやすくなります。

  1. digitalWrite()でGPIOを動かす
  2. Arduino Coreやドライバの実装を見る
  3. GPIOのレジスタを調べる
  4. レジスタのアドレスやビットフィールドを確認する
  5. 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の関数だけを使っていたときには見えにくかった、ソフトウェアから実際のハードウェアが動くまでの流れが、ここまででかなり見えるようになってきます。

KUMITATE

読むだけでなく、
実際に動かして学びませんか?

クミタテは、組み込み開発や電子工作を 実践しながら学べる学習プラットフォームです。 ESP32を使ったプログラミングから、電子回路、センサー、 通信、基板設計まで、自分のペースで学習できます。

  • 無料で学べる実践的な教材を掲載
  • すぐに試せるサンプルプログラム付き
  • Googleアカウントですぐに登録可能
クミタテに無料登録する
組み込み基礎
スポンサーリンク