メモリ・ストレージ

メモリマップ

各メモリ・周辺をアドレス空間に割り当てた配置。

概要

メモリマップ(Memory Map)とは、マイコン・MPU・SoCが持つアドレス空間において、各メモリ領域(フラッシュ・RAM)や周辺回路(ペリフェラル)がどのアドレスに割り当てられているかを示した配置図です。

組み込みシステムのCPUは物理的なハードウェアにアドレスを割り当て、プログラムはアドレスを介してメモリや周辺機器にアクセスします。たとえばSTM32F4では0x00000000〜0x1FFFFFFFがコード領域(フラッシュ等)、0x20000000〜0x3FFFFFFFがSRAM、0x40000000〜0x5FFFFFFFがペリフェラル(GPIO・UART・SPI等のレジスタ)に割り当てられています。

メモリマップを正確に理解することは、リンカスクリプト作成・デバイスドライバ開発・DMA設定・ブートローダー開発・デバッグなど、組み込み開発のあらゆる場面で必要不可欠な知識です。

歴史・背景

初期のCPU(8ビット時代)は16ビットアドレスバスを持ち、64KB(2^16)のアドレス空間しかありませんでした。Z80・6502はこの64KBに全てのメモリとI/Oを配置していました。一部のCPUはメモリアドレスとI/Oアドレスを分離する「I/O空間」を持ちましたが(Intelアーキテクチャのin/out命令)、多くのアーキテクチャはメモリマップドI/Oを採用しました。

32ビットCPU(ARM・MIPS・PowerPC等)の登場で4GB(2^32)のアドレス空間が使えるようになり、フラッシュ・RAM・ペリフェラルをそれぞれ広大な領域に配置できるようになりました。ARMはCortex-Mで標準メモリマップを定義し、異なるマイコンでも同じアドレス帯の役割(0xE0000000系はシステム制御等)が統一されています。

64ビットアーキテクチャ(ARM Cortex-A64・RISC-V 64ビット)では2^64≒16エクサバイトの仮想アドレス空間を持ち、MMUによる仮想アドレス変換で柔軟なメモリ管理が可能になっています。

技術仕様

ARM Cortex-M の標準メモリマップ

ARM Cortex-Mには以下の標準メモリマップが定義されています(4GBアドレス空間):

アドレス範囲サイズ領域名用途
0x00000000〜0x1FFFFFFF512MBCodeフラッシュ・ROM(XIP)
0x20000000〜0x3FFFFFFF512MBSRAM内蔵SRAM
0x40000000〜0x5FFFFFFF512MBPeripheralペリフェラルレジスタ
0x60000000〜0x7FFFFFFF512MBExternal RAM外部RAM
0x80000000〜0x9FFFFFFF512MBExternal RAM外部RAM(キャッシュ可)
0xA0000000〜0xBFFFFFFF512MBExternal Device外部デバイス(キャッシュ不可)
0xC0000000〜0xDFFFFFFF512MBExternal Device外部デバイス(キャッシュ不可)
0xE0000000〜0xFFFFFFFF512MBSystemCortex-Mシステム領域

STM32F4 メモリマップ例(具体例)

0x00000000 ─ Aliased to Flash/SRAM(起動モード設定による)
0x08000000 ─ Flash(最大1MB: 0x08000000〜0x080FFFFF)
0x1FFF0000 ─ System Memory(Internal Bootloader ROM)
0x1FFF7800 ─ Option Bytes
0x20000000 ─ SRAM1(112KB)
0x2001C000 ─ SRAM2(16KB)
0x40000000 ─ APB1 Peripherals(TIM2〜7, SPI2/3, USART2/3等)
0x40010000 ─ APB2 Peripherals(USART1, SPI1, ADC1〜3等)
0x40020000 ─ AHB1 Peripherals(GPIO, DMA, RCC等)
0x50000000 ─ AHB2 Peripherals(USB OTG FS等)
0x60000000 ─ External Memory(FMC SDRAM等)
0xE0000000 ─ Cortex-M4システム(SCB, NVIC, SysTick等)

ペリフェラルレジスタのアドレス

ペリフェラル(GPIO・UART等)のレジスタはメモリマップドI/Oとして特定アドレスに配置されます:

// STM32F4 GPIOA のベースアドレスとレジスタ
#define GPIOA_BASE    0x40020000UL
#define GPIOA_MODER   (*(volatile uint32_t*)(GPIOA_BASE + 0x00))
#define GPIOA_ODR     (*(volatile uint32_t*)(GPIOA_BASE + 0x14))
#define GPIOA_IDR     (*(volatile uint32_t*)(GPIOA_BASE + 0x10))

// GPIO出力設定(PA5: Output)
GPIOA_MODER |= (1U << 10);  // MODER5[1:0] = 01 (Output)
GPIOA_ODR   |= (1U << 5);   // ODR5 = 1 (High)

Linux(ARM Cortex-A)の仮想メモリマップ例

組み込みLinux(ARM64)ではMMUによる仮想アドレス変換が行われ、物理メモリマップと仮想メモリマップが異なります:

0x0000000000000000〜0x0000FFFFFFFFFFFF : ユーザー空間(128TB)
0xFFFF000000000000〜0xFFFFFFFFFFFFFFFF : カーネル空間(128TB)
  0xFFFF800000000000 〜 : 直接マップ領域(物理メモリ直接参照)
  0xFFFFFF8000000000 〜 : カーネルコード・データ

動作原理

メモリマップドI/O

組み込みシステムのほとんどはメモリマップドI/Oを採用しています。CPUのアドレスバスに接続された各デバイス(メモリ・ペリフェラル)はアドレスデコーダを持ち、自分に割り当てられたアドレス範囲のアクセスにのみ応答します。

                  アドレスバス
CPU ─────────────────────────────────┐
                                     ├─ Flash (0x08000000〜)
                                     ├─ SRAM  (0x20000000〜)
                                     ├─ GPIO  (0x40020000〜)
                                     └─ UART  (0x40011000〜)

各デバイスはアドレスデコーダで自分のアドレスか判定し、
一致した場合のみデータバスに応答する

volatileキーワードとメモリマップドI/O

ペリフェラルレジスタへのポインタには必ずvolatileを付ける必要があります。コンパイラの最適化でレジスタアクセスが省略されると、ペリフェラル制御が正常に動作しなくなります。

// NG: volatile なし → コンパイラが最適化で省略する可能性あり
uint32_t *reg = (uint32_t*)0x40020014;
*reg = 0x01;
*reg = 0x00;  // この書き込みが省略されるかもしれない

// OK: volatile あり → 必ず書き込みが実行される
volatile uint32_t *reg = (volatile uint32_t*)0x40020014;
*reg = 0x01;
*reg = 0x00;  // 必ず実行される

リンカスクリプトとメモリマップ

GCCのリンカスクリプト(.ld/.lds)でプログラムの各セクションをメモリマップの適切な場所に配置します:

/* STM32F4 リンカスクリプト例 */
MEMORY {
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 1024K
    SRAM  (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

SECTIONS {
    .text : {              /* コード・定数 → フラッシュ */
        KEEP(*(.isr_vector))
        *(.text*)
        *(.rodata*)
    } > FLASH

    .data : {              /* 初期値ありグローバル変数 → SRAM (フラッシュからコピー) */
        _sdata = .;
        *(.data*)
        _edata = .;
    } > SRAM AT > FLASH

    .bss : {               /* 初期値なしグローバル変数 → SRAM (0クリア) */
        _sbss = .;
        *(.bss*)
        _ebss = .;
    } > SRAM
}

デバイスツリーでのメモリマップ記述

Linux組み込みシステムでは、デバイスツリー(Device Tree)でハードウェアのメモリマップを記述します:

/ {
    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x40000000>;  /* DRAM: 1GB at 0x80000000 */
    };

    soc {
        uart0: serial@40011000 {
            compatible = "st,stm32-usart";
            reg = <0x40011000 0x400>;   /* UARTレジスタ: 0x40011000, 1KB */
            interrupts = <37>;
        };
    };
};

用途・ユースケース

ベアメタル開発でのペリフェラル制御

HALライブラリを使わずに直接ペリフェラルレジスタを操作する場合、メモリマップが必須の知識です。データシートのメモリマップ章でベースアドレスとレジスタオフセットを確認して実装します。

// RCC(クロック制御)レジスタへの直接アクセス
#define RCC_BASE        0x40023800UL
#define RCC_AHB1ENR     (*(volatile uint32_t*)(RCC_BASE + 0x30))

// GPIOA クロック有効化
RCC_AHB1ENR |= (1U << 0);  // bit0: GPIOAEN

デバッガでのメモリ確認

JTAGデバッガ・OpenOCDを使ってメモリアドレスを直接読み書きし、ペリフェラルの状態を確認できます:

# OpenOCD + telnetでレジスタ確認
> mdw 0x40020014 1    # GPIOA ODRを読み出す
0x40020014: 00000020  # bit5がHigh(PA5がHigh)

> mww 0x40020014 0x00000000  # GPIOA ODRを全Lowに

DMA設定

DMAの転送元・転送先アドレスを設定する際も、メモリマップに基づいてアドレスを指定します。ペリフェラルのデータレジスタアドレスとRAMバッファアドレスを設定します。

実装・開発のポイント

メモリバリア

CPUやコンパイラがメモリアクセス順序を最適化することで、ペリフェラル制御が期待通りに動作しないことがあります。ARM Cortex-Mでは__DSB()(Data Synchronization Barrier)・__DMB()(Data Memory Barrier)を使います。

// ペリフェラル設定後のバリア
UART->CR1 |= USART_CR1_UE;   // UARTを有効化
__DSB();                        // この書き込みが完了するのを保証
UART->CR1 |= USART_CR1_TE;   // 送信有効化

XIPの考慮

フラッシュからコードを実行(XIP)する場合、フラッシュアクセス時間がCPUクロックより遅いため、待機サイクル(Flash Wait States)の設定が必要です。STM32F4の場合、168MHzではFlash Latencyを5に設定する必要があります。

// STM32F4 Flashアクセス速度設定
FLASH->ACR |= FLASH_ACR_LATENCY_5WS;  // 5ウェイトステート(168MHz, 3.3V時)

アドレスアライメント

ARM Cortex-Mはアドレスのアライメント要件があります。4バイトアクセスは4バイト境界(アドレスが4の倍数)、2バイトアクセスは2バイト境界に合わせる必要があります。非アライメントアクセスはハードフォルト(Hard Fault)例外を引き起こすことがあります(設定次第で許可も可能)。

// アライメントに注意
uint32_t *ptr = (uint32_t*)0x20000001;  // 危険: 4バイト非アライメント
*ptr = 0x12345678;  // ハードフォルトの可能性

uint32_t *ptr = (uint32_t*)0x20000004;  // 安全: 4バイトアライメント

他技術との比較

メモリマップドI/O vs ポートI/O

x86アーキテクチャはメモリアドレスとは独立したI/Oポートアドレス空間(64KB)を持ち、in/out命令でアクセスします。ARM・MIPS・RISC-V等のほとんどの組み込みアーキテクチャはポートI/Oを持たず、メモリマップドI/Oで統一されています。メモリマップドI/Oはアドレス空間を消費しますが、通常のロード・ストア命令でアクセスできるため、高速・シンプルです。

物理アドレス vs 仮想アドレス

マイコン(Cortex-M等、MMUなし)は物理アドレス直接です。Linux等を動かすMPU/SoC(Cortex-A等、MMUあり)はMMUによって仮想アドレスを物理アドレスに変換します。デバイスドライバではioremap()を使って物理ペリフェラルアドレスを仮想アドレス空間にマップしてアクセスします。

// Linuxカーネルドライバでのioremapの例
void __iomem *base = ioremap(UART_PHYS_BASE, 0x1000);
writel(0x01, base + UART_CR_OFFSET);  // レジスタ書き込み
iounmap(base);

関連用語

参考リンク