명령어와 데이터를 별도의 메모리·버스로 분리한 컴퓨터 구조.
폰 노이만 구조와의 대비
폰 노이만 (Von Neumann)
┌─────────────────┐
│ CPU │
└──┬──────────────┘
│ 하나의 버스
┌──┴──────────────┐
│ 명령어 + 데이터 │
└─────────────────┘
→ 명령어를 읽을 때와 데이터를 읽을 때 버스를 나눠 써야 한다
(폰 노이만 병목)
하버드 (Harvard)
┌─────┐
┌────┤ CPU ├────┐
│ └─────┘ │
┌──┴────┐ ┌───┴───┐
│ 명령어│ │데이터 │
└───────┘ └───────┘
→ 동시에 접근 가능. 명령어 인출과 데이터 접근이 병렬
이름의 유래
1944년 하버드 대학의 Mark I 컴퓨터가 프로그램을 천공 테이프에, 데이터를 별도 저장소에 둔 데서 왔다.
장점
- ① 명령어 인출과 데이터 접근이 동시에 → 처리량 향상
- ② 명령어 폭과 데이터 폭을 다르게 할 수 있다
- (예: 명령어 14비트, 데이터 8비트 — PIC 마이크로컨트롤러)
- ③ 명령어 메모리를 읽기 전용으로 두어 안전성 확보
- ④ 파이프라인 구성이 쉽다
②가 임베디드에서 실질적이다. 명령어 폭을 데이터와 독립적으로 정해 코드 밀도를 최적화할 수 있다.
순수 하버드의 불편함
- 프로그램 메모리에 있는 상수 테이블을 읽으려면?
- 별도의 특수 명령이 필요하다
// AVR — 플래시의 데이터를 읽으려면 PROGMEM과 전용 함수
const char msg[] PROGMEM = "Hello";
char c = pgm_read_byte(&msg[0]); // 일반 포인터로는 못 읽는다
이것이 AVR(아두이노) 프로그래밍이 번거로운 이유다. 문자열을 플래시에 두면 RAM은 아끼지만 읽는 코드가 달라진다.
수정 하버드 구조 — 현실의 타협
대부분의 현대 MCU가 쓰는 방식이다.
- 버스는 분리되어 있다 (I-Bus, D-Bus) → 병렬 접근의 이득
- 주소 공간은 통합되어 있다 → 같은 포인터로 접근 가능
ARM Cortex-M이 정확히 이 구조다.
Cortex-M3/M4 의 버스
├─ ICode Bus : 플래시에서 명령어 인출 (0x00000000~0x1FFFFFFF)
├─ DCode Bus : 플래시에서 상수 데이터 읽기 (같은 영역)
└─ System Bus: SRAM, 주변장치 (0x20000000~)
같은 플래시 영역을 두 버스로 동시에 접근한다.
그래서 코드 실행 중에 .rodata 상수를 읽어도 버스 경합이 없다.
const uint8_t table[256] = { … }; // 플래시에 있지만
uint8_t v = table[i]; // 일반 포인터로 그냥 읽힌다 ← 통합 주소 공간
AVR과 달리 특수 함수가 필요 없는 이유가 여기에 있다.
실무에서 왜 알아야 하나
① 성능 이해
- SRAM에서 코드를 실행하면?
- System Bus 하나로 명령어와 데이터를 모두 처리 → 경합 발생 → 느려질 수 있다
- 반대로 플래시가 느린 경우(웨이트 스테이트)에는 SRAM 실행이 빠르다
"RAM 실행이 항상 빠르다"는 것은 오해다. 버스 구조와 플래시 대기 사이클을 함께 봐야 한다.
② 부트로더와 플래시 쓰기
- 플래시에 쓰는 동안에는 그 플래시에서 명령어를 인출할 수 없다
- 플래시 쓰기 루틴은 반드시 RAM에서 실행해야 한다
__attribute__((section(".ramfunc"))) void flash_write(...) { … }
링커 스크립트에서 이 섹션을 RAM에 배치하고, startup에서 복사한다. OTA 펌웨어 업데이트를 구현할 때 반드시 마주치는 문제다.
③ 캐시 구조
I-Cache와 D-Cache가 분리된 것도 하버드 발상의 연장이다. 자기 수정 코드나 OTA 후에는 I-Cache를 무효화해야 새로 쓴 코드가 실행된다.