1. RTOS 학습을 위한 선결 조건
RTOS(Real-Time Operating System)는 하드웨어 제어보다는 소프트웨어 알고리즘과 구조에 집중합니다. 따라서 기초적인 마이크로컨트롤러(MCU) 조작 능력이 있다면 바로 시작할 수 있습니다. 과정 진행 중 ARM 아키텍처의 핵심 내용을 다루므로, 하드웨어에 대한 깊은 지식이 부족하더라도 소프트웨어 관점에서 접근이 가능합니다.
2. CPU의 메모리 및 주변장치 접근 방식
CPU는 RAM, Flash, GPIO 등을 서로 다른 주소(Address)를 가진 장치로 인식합니다. CPU 입장에서는 이들 모두가 '주소'를 통해 데이터를 주고받는 대상일 뿐입니다.
- RAM: 읽기와 쓰기가 자유로운 영역으로, 변수가 저장됩니다.
- Flash: 프로그램 코드가 저장되는 곳으로, 읽기는 빠르지만 쓰기(수정)를 위해서는 삭제(Erase)나 잠금 해제와 같은 복잡한 과정이 필요합니다. 전원이 꺼져도 데이터가 유지됩니다.
- GPIO: 특정 주소에 값을 쓰면 하이/로우 레벨 출력이 나가는 등 물리적인 핀 상태를 제어하는 특수한 기능을 수행합니다.
3. 변수 할당과 메모리 주소의 관계
C언어에서 var = 100;과 같은 코드를 작성하면, 컴파일러와 링커가 해당 변수를 RAM의 특정 주소에 할당합니다. 실제 하드웨어 수준에서는 다음과 같은 흐름으로 동작합니다.
// 어셈블리 관점의 예시
LDR R2, =0x20000004 // 변수 var의 RAM 주소
MOV R3, #100 // 저장할 값 100
STR R3, [R2] // R2 주소에 R3 값을 저장
이 과정에서 값 '100'은 처음에 Flash의 코드 영역(상수 데이터)에 존재하다가, 명령어가 실행되면서 RAM의 0x20000004 주소로 복사되는 것입니다.
4. 포인터와 volatile 키워드
주변장치 제어를 위해 주소를 직접 참조할 때 volatile 키워드를 사용합니다. 예를 들어 다음과 같은 코드를 보겠습니다.
uint32_t *ctrl_reg = (volatile uint32_t *)(0x40021000 + 0x18);
여기서 volatile을 생략하면 컴파일러가 최적화 과정에서 해당 주소로의 접근을 생략하거나 레지스터에 캐싱할 수 있습니다. 하드웨어 상태는 예고 없이 변할 수 있으므로, 항상 메모리에서 직접 읽도록 volatile을 명시하는 것이 권장됩니다.
5. 코드 실행 위치: Flash vs RAM
임베디드 시스템(Cortex-M 등)에서 코드는 보통 Flash에 저장된 상태로 직접 실행됩니다. 리눅스와 같은 범용 OS가 실행 파일을 RAM으로 모두 복사해 실행하는 것과는 차이가 있습니다. MCU의 RAM 용량은 제한적이기 때문에 Flash에서 코드를 읽어 CPU 내부에서 처리하는 방식을 주로 사용합니다. 물론 고속 실행이 필요한 특정 루틴은 RAM에 배치하여 실행 속도를 높이기도 합니다.
6. 메모리 정렬(Memory Alignment)과 성능
32비트 CPU는 한 번의 연산으로 32비트(4바이트) 데이터를 읽어오는 것이 가장 효율적입니다. 만약 구조체 멤버가 정렬되지 않아 데이터가 메모리 경계에 걸쳐 있다면, CPU는 두 번에 걸쳐 데이터를 읽고 이를 조합해야 하므로 성능이 저하됩니다.
struct DataPacket {
char id; // 1바이트
// 3바이트 패딩 발생
int value; // 4바이트
};
위 구조체에서 char 뒤에 패딩 바이트가 추가되는 이유는 int value가 4의 배수 주소에 오도록 하여 읽기 효율을 최적화하기 위함입니다. #pragma pack(1) 명령을 사용해 패딩을 제거할 수 있지만, 이는 CPU의 접근 횟수를 늘려 속도를 떨어뜨릴 수 있습니다.
7. RTOS의 스택 할당 방식
FreeRTOS와 같은 RTOS에서 태스크 스택은 정적(Static) 또는 동적(Dynamic)으로 할당될 수 있습니다.
- 동적 할당: 유연성이 높지만 힙 메모리 관리가 필요하며, 런타임에 메모리 부족 현상이 발생할 가능성이 있습니다.
- 정적 할당: 컴파일 타임에 메모리가 확정되므로 예측 가능성이 높고 안정적인 시스템에 적합합니다.
8. 32비트 시스템에서의 주소 체계
32비트 CPU는 32개의 주소선을 가집니다. 따라서 표현 가능한 주소의 범위는 0부터 232-1까지이며, 모든 포인터 변수의 크기는 어떤 데이터 타입을 가리키든 관계없이 4바이트(32비트)가 됩니다. sizeof(int *)나 sizeof(char *)가 모두 4를 반환하는 이유가 여기에 있습니다.