초기값이 0이 아닌 전역·static 변수가 들어가는 섹션.
int counter = 100; // .data
static float gain = 1.5f; // .data
char msg[] = "hello"; // .data (배열이므로 RAM에 복사본이 생긴다)
const char *p = "hello"; // 포인터는 .data, 문자열은 .rodata
왜 두 곳에 존재하는가
초기값 100 은 전원이 꺼져도 보존돼야 한다 → 플래시에 저장
-
실행 중에는 값을 바꿀 수 있어야 한다 — → RAM에 있어야 한다
-
부팅 시 플래시에서 RAM으로 복사한다
저장 위치(LMA)와 실행 위치(VMA)가 다른 대표적인 섹션이다.
startup 코드의 복사
extern uint32_t _sidata; // 플래시의 초기값 시작 (LMA)
extern uint32_t _sdata; // RAM의 목적지 시작 (VMA)
extern uint32_t _edata; // RAM의 목적지 끝
void Reset_Handler(void) {
uint32_t *src = &_sidata;
uint32_t *dst = &_sdata;
while (dst < &_edata) *dst++ = *src++;
…
}
이 루프 이전에는 전역 변수 초기값을 신뢰할 수 없다.
SystemInit() 안에서 초기화된 전역 변수를 쓰면 쓰레기 값을 읽는다 —
디버깅하기 까다로운 함정이다.
플래시와 RAM을 모두 소모한다
uint8_t table[1024] = { 1, 2, 3, … }; // 플래시 1KB + RAM 1KB
const uint8_t table[1024] = { … }; // 플래시 1KB만 ← .rodata
바뀌지 않는 데이터에 const를 빠뜨리면 RAM을 두 배로 쓴다.
RAM이 20KB인 MCU에서 이것이 결정적일 수 있다.
0으로 초기화하면 .bss로 간다
int a = 0; // .bss (초기값 0은 저장할 필요가 없다)
int b; // .bss
int c = 1; // .data
컴파일러가 자동으로 판단한다. 0 초기화는 플래시 공간을 아끼려고
.bss로 보내고 startup에서 memset 한다.
크기 줄이기
✅ 안 바뀌는 데이터는 const → .rodata (플래시만) ✅ 0으로 초기화되는 것은 명시적으로 = 0 하지 않아도 된다 (어차피 .bss) ✅ 큰 배열의 초기값이 반복적이면 런타임에 계산 ✅ 문자열 리터럴은 const char * 로 (배열로 받으면 복사본이 생긴다)
char msg[] = "hello world"; // RAM 12바이트 + 플래시 12바이트
const char *msg = "hello world"; // 플래시 12바이트 + RAM 4바이트(포인터)
const char msg[] = "hello world"; // 플래시 12바이트만 ← 최선
복사 시간
.data 가 크면 부팅이 느려진다
1KB 복사는 무시할 수준이지만, 수십 KB면 체감된다
빠른 부팅이 필요하면 .data 크기를 줄이거나 DMA로 복사하기도 한다.
여러 RAM 영역에 배치
// 링커 스크립트에서 정의한 섹션에 변수를 배치
__attribute__((section(".ccmram"))) uint32_t fast_buffer[256];
CCM처럼 별도 RAM에 두면 버스 경합을 줄일 수 있다. 단, 그 영역도 startup에서 초기화 코드를 추가해야 초기값이 들어간다. 링커 스크립트만 고치고 startup을 안 고쳐 초기값이 쓰레기인 문제가 흔하다.
외부 RAM에 배치할 때
- SDRAM 은 startup 초기 단계에서 아직 초기화되지 않았다
- .data 복사 루프가 SDRAM 영역에 쓰려 하면 실패한다
- SDRAM 초기화를 먼저 하고 별도로 복사해야 한다 링커 스크립트와 startup 순서를 함께 설계해야 하는 지점이다.