0. Introduction
Memory-Mapped I/O란 CPU가 입출력 장치와 데이터를 주고받는 방식이다.
이 Memory-Mapped I/O(MMIO)와 함께 I/O-Mapped I/O(PMIO)가 존재한다.
두방식 모두 CPU가 입출력 장치와 데이터를 주고받는 개념은 동일하며 방법의 차이가 있다.
이번 포스팅에서는 두 방식의 개념과 서로다른 차이를 알아보려 한다.
1. MMIO
메모리와 I/O장치가 단일주소공간을 공유하는 방식이다.
쉽게 말하면 "물리주소에 어떤 값을 쓰게 되면 레지스터에 값이 업데이트 된다는 개념"이다.
CPU입장에서 I/O장치의 레지스터는 메모리의 일부 주소처럼 처리된다.

주요 특징
- 하드웨어 간소화: 메모리용 버스와 I/O용 버스를 따로 구분할 필요가 없기 때문이다.
- 주소 공간 감소: I/O장치가 메모리 주소 영역의 일부분이 되므로, 실제 RAM으로 사용될 수 있는 용량은 그만큼 줄어든다.
- volatile: C언어 등에서 컴파일러 최적화로 인해 I/O레지스터 생략이 발생할 수 있기 때문에, 해당 주소 변수 앞에는 volatile 키워드를 지정해줘야 한다.
보통 사용하게 되는 ARM, RISC-V등 대부분의 현대 시스템에서 표준으로 사용되는것이 Memory_Mapped I/O이다.
2. PMIO (I/O-Mapped I/O)
주소공간을 공유하는 MMIO와 달리 PMIO는 주소공간을 완전히 분리하여 다루는 방식이다.
주의할점은 일반 메모리 명령어와 달리 I/O 전용 입출력 명령어를 별도로 사용한다.
예를 들어 x86 아키텍처의 IN/OUT 명령어가 있다.
ARM과 RISC-V는 애초에 PMIO를 지원하지 않는 pure MMIO 아키텍처이다.
(ARM이나 RISC-V에서 입출력을 수행할 때는 PMIO가 아닌 MMIO방식을 활용한다)

주요 특징
- 메모리 용량 보존: MMIO에서는 메모리 주소 공간을 공유하기 때문에, 메모리 용량 감소에 대한 부분이 있었지만, PMIO에서는 I/O장치가 메모리 주소 공간을 침범하지 않으므로 메모리 용량이 감소할 일이 없다.
- 제어 신호선: PMIO에서는 메모리주소공간과 I/O주소공간이 분리되어있기에, 버스에 실은 값이 메모리용인지 I/O용인지 구분하기 위한 신호선이 필요하다.
2. MMIO assembly (ARM64 아키텍처)
제일 간단한 수준에서 코드를 확인해보려고 한다.
키 포인트는 volatile키워드의 유무에 따른 어셈블리 차이를 비교해본다.
#include <stdint.h>
#define MMIO_BASE 0x40021000
// 1. volatile을 사용한 MMIO 제어
void write_mmio_volatile(uint32_t val) {
*(volatile uint32_t *)(MMIO_BASE) = val;
}
uint32_t read_mmio_volatile(void) {
return *(volatile uint32_t *)(MMIO_BASE);
}
// 2. 일반 포인터 (비교용)
void write_mmio_normal(uint32_t val) {
*(uint32_t *)(MMIO_BASE) = val;
}
위의 C코드를 해석하여 생성한 어셈블리는 아래와 같다.
read_mmio_volatile:
mov w0, 4096
movk w0, 0x4002, lsl 16 // w0 = 0x40021000 주소 생성
ldr w0, [x0] // LDR (Load Register): x0 주소의 메모리 값을 읽어 w0 레지스터에 로드
ret
write_mmio_volatile:
mov w1, 4096 // 0x1000 (하위 16비트 주소)
movk w1, 0x4002, lsl 16 // 0x40020000 (상위 16비트 합쳐서 0x40021000 생성)
str w0, [x1] // STR (Store Register): w0 레지스터의 값을 x1 주소(0x40021000)에 저장
ret
write_normal:
mov w1, 4096
movk w1, 0x4002, lsl 16
str w0, [x1] // 단일 쓰기에서는 일반 포인터도 일단 STR을 만듭니다.
ret
위에 write_mmio_volatile과 write_mnormal 어셈블리코드를 보면 차이가 없음을 알 수 있지만,
실제 하드웨어 레지스터에서의 해석은 전혀 다르다.
#define MMIO_REG ((uint32_t *)0x40021000)
#define MMIO_REG_V ((volatile uint32_t *)0x40021000)
// 일반 포인터로 연속 쓰기
void init_normal(void) {
*MMIO_REG = 1; // 첫 번째 설정
*MMIO_REG = 2; // 두 번째 설정
*MMIO_REG = 3; // 최종 설정
}
// volatile 포인터로 연속 쓰기
void init_volatile(void) {
*MMIO_REG_V = 1; // 첫 번째 설정
*MMIO_REG_V = 2; // 두 번째 설정
*MMIO_REG_V = 3; // 최종 설정
}
이때 init_normal을 컴파일러가 해석할때는 "어차피 마지막 값이 3이네"라고 해석하기 때문에 앞의 쓰기명령어(STR) 2개를 완전히 삭제한다.
init_volatile을 컴파일러가 해석할때는 "volatile이 붙어 있으니, 절대 생략하거나 최적화 하면 안된다"라고 인식하고, 3번의 STR명령어를 정확히 모두 수행한다.
요약
일반 포인터는 컴파일러가 메모리를 값을 저장하는 RAM공간으로 간주하기 때문에 중간과정을 생략하는 최적화를 마음대로 수행한다.
반면 volatile 포인터는 하드웨어 레지스터와 연결되어 있어서 접근자체로서 의미를 가지기 때문에 절대 최적화 하지 않는다.
활용 예시
MMIO 주소는 단순 메모리가 아니라 하드웨어 장치이다.
read/write 순간 HW내부에서 센서, 타이머, 패킷 전송 등 실제 동작이 일어나는 상황에서 활용된다.
다음포스트에서는 간단한 디바이스 드라이버를 작성하여 MMIO방식을 활용한 레지스터 제어에 대해서도 알아보려한다.
REFERENCE
https://www.geeksforgeeks.org/computer-organization-architecture/memory-mapped-i-o-and-isolated-i-o/
'Kernel > Memory Management' 카테고리의 다른 글
| Memory Model (0) | 2026.09.02 |
|---|---|
| node & zone (0) | 2026.08.29 |