일반적으로 지역변수는 함수 호출 시 스택에 동적으로 할당되고 함수 종료 시 해제된다. 하지만 Keil C51 컴파일러에선 8051의 매우 작은 내부 RAM을 효율적으로 사용하기 위해, 지역변수를 다음과 같은 단계로 처리한다.
1. 레지스터 할당
먼저 지역변수를 CPU 레지스터(R0~R7 등)에 배치할 수 있는지 확인하고, 가능한 경우 우선적으로 배치한다.
2. 메모리에 오버레이하여 정적 배치
레지스터만으로 부족한 경우에는 _DATA_GROUP_ 세그먼트에 정적으로 배치한다. (Default 메모리 모델인 Small 모델에서 DATA_GROUP은 Low 128B of Internal RAM을 사용함)
다만 지역변수마다 고정된 고유 주소를 독접하는 것은 아니고, 컴파일러/링커가 프로그램의 Call Tree를 분석해서 동시에 실행될 일이 없는 함수의 지역변수끼리는 같은 RAM 주소에 겹쳐서 배치(Overlay)한다.
즉, 지역변수가 정적 주소를 사용한다고 해서 함수가 끝난 뒤에도 값을 보존하는 것은 아니며, 동일한 주소가 다른 함수의 지역변수로 재사용될 수 있으므로, 함수 종료 후에도 값을 유지해야 하는 지역변수라면 static으로 선언해 고유 주소공간을 할당해야 한다.
참고:
https://developer.arm.com/documentation/ka002474/1-0/
https://developer.arm.com/documentation/ka004006/1-0/
✓ Overlay가 적용되지 않는 경우
모든 지역변수가 Overlay 대상이 되는 것은 아니다. 호출 관계를 정확히 분석할 수 없거나, 실제로 동시에 실행될 가능성이 있는 경우에는 Overlay를 하지 않고 각 변수마다 독립적인 메모리를 할당한다.
대표적인 경우는 다음과 같다.
- 함수 포인터를 통해 호출되는 함수의 지역변수
- 인터럽트(ISR) 내부의 지역변수
- 메인 코드와 ISR에서 동시에 호출될 수 있는 함수의 지역변수
- reentrant(재진입) 키워드를 붙여 선언된 함수의 지역변수
예를 들어 다음과 같은 상황을 가정해보자.
void calc(void)
{
int temp;
// ...
}
void main(void)
{
while (1)
{
calc();
}
}
void timer0_isr(void) interrupt 1
{
calc();
}
메인에서 calc()가 실행되는 도중 인터럽트가 발생하면 ISR에서도 calc()가 실행된다. 이때 calc()의 지역변수 temp는 DATA_GROUP의 동일한 정적 주소를 사용하므로, ISR의 재진입 호출이 메인에서 사용 중인 지역변수를 덮어써 데이터가 손상될 수 있다.
이런 경우 빌드는 성공하지만 링커가 다음과 같은 경고를 출력한다.
*** WARNING L15: MULTIPLE CALL TO SEGMENT
해결방법 1. reentrant(재진입) 키워드 사용
void calc(void) reentrant
{
int temp;
// ...
}
reentrant로 선언하면 해당 함수의 지역변수는 DATA_GROUP를 사용하지 않고 Reentrant Stack에 동적으로 할당된다. 따라서 함수가 중첩 호출되거나 ISR과 메인에서 동시에 호출되어도 각각 독립적인 지역변수를 사용하게 된다.
단, 공식문서에도 reentrant 함수는 비효율적인 스택 시뮬레이션 방식을 사용하므로 꼭 필요한 경우만 제한적으로 쓰라고 설명한다. (https://www.keil.com/support/man/docs/c51/c51_le_reentrantfuncs.asp)
해결방법 2. 동시에 호출되지 않도록 설계
volatile bit calc_req;
void timer0_isr(void) interrupt 1
{
calc_req = 1;
}
void main(void)
{
while (1)
{
if (calc_req)
{
calc_req = 0;
calc();
}
}
}
ISR에서는 플래그만 설정하고 calc()는 항상 메인에서만 호출되도록 구성하는 방식이다.
해결방법 3. 인터럽트를 잠시 비활성화
EA = 0;
calc();
EA = 1;
메인 코드에서 함수 실행 중 인터럽트가 발생하지 않도록 하는 방법이다. 단, 인터럽트 처리가 늦어져 실시간성이 떨어질 수 있다.
표준 라이브러리 사용 시 주의
Keil C51의 런타임 라이브러리 함수 역시 reentrant를 고려하지 않고 작성되었다. 대표적인 예가 printf()이다. 메인에서 printf()가 실행되는 도중 ISR에서도 printf()를 호출하면 내부 변수가 덮어써져 출력이 깨지거나 오동작할 수 있으니, 표준 라이브러리 함수는 ISR에서 호출하지 않도록 주의해야 한다.
참고: https://community.arm.com/support-forums/f/keil-forum/20291/persistent-c51-printf-curiosities/47944
'임베디드 개발 > 8051' 카테고리의 다른 글
| C51 인터럽트 (0) | 2026.08.06 |
|---|---|
| 8051 메모리 구조 (0) | 2026.07.30 |
| 8051 MCU의 1T, 6T, 12T의 의미 (0) | 2026.07.23 |