본문 바로가기
Bootloader와 Memory Budget (Mapfile) 어떻게 변수초기화를 할 것인가

Bootloader, Bootloader라는 말을 많이 쓰는데 도대체 Bootloader란 무엇이란 말인가요. Boot를 하는데 뭘 싣는다는 건지(Loader). Booting은 보통 컴퓨터가 동작을 시작하여 정상적으로 사용 가능하기 전까지의 과정을 Booting이라고 부르는데, 원래는 Boot strap이라는 데서 말이 왔다고 하네요. Boot Strap이라는 건 어디서 주워들은 얘기로는 신발에 달린 끈으로 자기를 들어올린다는 말이라는데, 제 생각에는 자 이제 뛰어 나가려고 신발 끈을 묶는 게 아닌가 싶어요. 뛰기 직전에 신발끈을 꽉 묶어야 뛸 준비가 끝나는 것과 같이. 이놈의 짤막한 상상력이란 도대체 어디다 써먹어야 할지요.

어쨌거나, Boot은 그런 의미에서 시작해서 System을 사용할 수 있는 상태까지의 Sequence를 의미하고 있고요, Loader는 뭔가 싣는 거지요. Boot를 하면서 뭔가를 싣는 건데, 실은 ROM에서 RAM으로 뭔가를 싣는 걸 말하는 겁니다. 그러니까, Boot loader는 하는 일이 크게 두 가지 정도 되는 거에요. Hardware를 정상적으로 사용 가능하게 하고요, 실제 동작이 가능하도록 ROM에서 RAM으로 뭔가를 싣는 거죠. 자, 뭘 싣는지도 알아보고, 싣기 위해서 Memory Layout은 어떻게 할 건지 뭐 그런 거 알아보죠 머.

통상 Board를 구성할 때에는 MCP (Multichip Chip Package)를 무엇으로 할 것이냐가 고민될 때가 많습니다. NOR + PSRAM 구성으로 할 것이냐! 아니면, NAND + SDRAM 구성으로 할 것이냐 하는 고민을 많이 하게 되죠. 이건 또 무신 귀신 시나락 까먹는 소리냐. 여기에서 이전에 보았던 Memory종류들이 나왔어요. 이것들의 특징을 다시 한번 정리할 필요가 있겠어요. XIP 개념 기억나지요?

뭐 이런 식이죠. MCP는 Multi chip Package로서, ROM과 RAM을 한 package에 담아서 파는 chip이잖아요. 이중에 combination을 많이 하는 것은 NOR + PSRAM 하고, NAND + SDRAM combination이에요. 이제부터 NOR + PSRAM은 NOR MCP, NAND + SDRAM 은 NAND MCP라고 부를 게요. 계속 길게 쓰려니까, 귀차니즘이.. 오홋, 이런 말을 굳이 지금 꺼내는 이유는 Bootloader가 해야 할 일이 이 combination에 따라 틀리기 때문이지요. Bootloader는 뭔가 싣는다고 했으니까, 그 뭔가에 대해서 알아봐야 할 것 같아요.

일단, 우리 ROM에 담아놔야 되는 것은 무엇이 있는지 파악해 봅시다. ROM에는 당연히 code와 const data인 RO (Read Only)가 있어야 되고요, RW (Read Write)도 당연히 들어가야 합니다. 왜냐고요? RW는 초기값이 있는 Global 변수라 했잖아요. 그러니까, RW는 0이 아닌 초기값들을 가지고 있을 테니, ROM에 초기값을 고이 간직하고 있어야겠죠. 그러면, ZI (Zero Initialized)는? ZI는 당연히 초기값이 모두 Zero니까 굳이 ROM에 0을 가득 담아 놓을 필요가 없지요. 유식한 말로 하면 ZI는 Static하게 생성되지는 않지만, Dynamic하게 생성해줘야 한다. 뭐 그런 식인 거죠. 그럼 RAM에는 뭐가 들어갈까요? 당연히 RW와 ZI가 들어가 주셔야 되겠죠. RW와 ZI는 계속 값을 만져줘야 하는 Variable들이니까 RAM에 들어가 주시죠. ZI는 ROM에는 없지만, RAM에는 들어가는 것이죠~ 여기에서 잠깐! 그럼 RO는? RO는 code가 대부분인데요. 이 부분은 MCP의 종류에 따라 달라지는 겁니다. NOR MCP의 경우에는 RO는 ROM에서 그대로 실행가능 하니까 복사할 필요가 없고요, NAND MCP의 경우에는 RO를 ROM에서 그대로 실행 못하니까, XIP가 가능한 SDRAM에 복사를 하고서 실행시키는 거에요. 오호라, 이제 NAND MCP와 NOR MCP의 차이가 막 나오네요.

RAM에 들어가주셔야 하는 부분이 ROM으로부터 copy되는 것. 이것이 바로 loading과정이고요 Bootloader가 ROM으로부터 RAM으로 실어 주는 바로 그것입니다. MCP에 따라 Loader가 loading해 줘야 하는 것들이 달라지죠. Bootloader는 NOR MCP에서는 RW와 ZI를, NAND MCP에서는 RO, RW, ZI를 RAM에 Loading (복사) 해 주는 일을 한답니다. 와하핫! 여기에서 불현듯! Scatter loading이 떠오르지 않나요? Scatter loading은 Load view와 Execution view를 제공하죠. 바로 여기에서 이 두 가지 view가 빛을 보는 거죠. Load view는 Flash (ROM)에 어떻게 담겨 있을 것이냐. Execution view는 RAM에 어떻게 복사되어서 실행 될 것이냐는 걸 의미한답니다. 으랏차.

그러면, Bootloader는 RO, RW, ZI를 어떻게 구분해서 RAM에 Loading을 할까요? 그 부분을 짚고 넘어가겠사옵니다. 우선은 Scatter Loading을 사용하지 않는 Default Memory Model에서의 Bootloader의 copy동작을 설명하겠나이다.

재미있는 건 Linker가 만들어주는 Symbol이 있다는 거에요. 아래처럼 Execution View에서 RO, RW, ZI 세 개 Section에 대해서 각각의 시작 주소와 끝 주소를 Linker가 Symbol로 만들어 주지요. 그러니까 초기값이 있는 전역변수들의 모임인 RW는 Image$$RW$$Base에서 Image$$RW$$Limit 사이에 모여 있어요. (정확히는 Image$$RW$$Limit - 1까지 에요)

이런 걸 이용해서 RO, RW, ZI 영역을 자유자재로 Access할 수 있어요. 예를 들어서 ZI영역을 0으로 가득 채우고 싶다면, Image$$ZI$$Base에서부터 Image$$ZI$$Limit - 1 까지 0으로 마구 써버리면 되겠죠. 실제 Assembly에서 이 Symbol을 접근하는 방법은 양 끝을 |로 막아주면 됩니다.
그 예는!

r0, = |Image$$ZI$$Base|
r1, = |Image$$ZI$$Limit|
r2, = 0

begin
str r2, [r0], #4
cmp r0, r1
bne begin

이렇게 해주면 ZI Section을 마구마구 0으로 채울 수 있겠죠. 별다른 게 아니고 바로 이게 바로 초기값이 없는 전역변수가 Default값으로 0을 갖는 비밀이죠! Linker는 초기값이 없는 전역변수를 ZI에 넣어두고 Bootloader는 이 영역을 0으로 채워버리고.

그런데, 이렇게만 하면 정말 서먹하니까 ADS Manual 중에 나오는 유명한 예제 하나를 들어보고 가시죠. 사실 Default Memory Model은 아주 간단한 시스템 말고는 Embedded System에서 거의 안 쓰여요.

이 예는 1개의 Load Region과 3개의 Execution Region을 가지고 있는 Memory Layout 이에요. 이때! Linker가 만들어 내는 Symbol과 그 값을 볼까요? Table을 잘 봐보세요. Load$$Region_name$$Base(Length), Image$$Region_name$$Base(Length) 라는 형식으로 되어 있죠. Default Memory model과는 차원이 틀립니다. 단어에서 풍기는 냄새처럼 Load는 Load View, Image는 Execution View를 의미합니다.

주소는 잘 봐보세요. 재미있는 숫자놀이 0부터 시작해서 Base는 점점 올라가죠.
그 해석은..

각 Length는 뭘 의미하는지 아시겠죠? Length가 다음에 시작할 Base를 넘어가면 Linker가 Error를 냅니다. - 여기에서의 Length는 예제를 위해서 적당한 값들을 채워 넣었어요 - 여기서 한가지, armlink는 ZI output section을 포함하는 모든 execution region마다 추가적인 $$ZI$$ 를 포함하는 추가적인 symbol들을 생성한다 구요. 왜냐고요? ZI는 실제 Load View에는 없지만, Image에는 생성되어야 하는 부분이니까 굳이 따로 ZI를 넣지 않은 경우를 제외하고는 모든 Region에 $$ZI$$를 init할 수 있도록 Linker가 친절히 만들어 줍니다.

더욱 복잡한 예제 하나만 더 해보면 확실하겠죠. 이것도 유명한 ADS Manual의 예제 라니깐요. 특별하게 더 좋은 예제를 만들어 볼라고 했는데, 역시나 ADS Manual이 왕짱입니다. 간단하고요.

어때요? 이제 감 오시나요? 자세히 보시면 Load는 Base만 있고, Length가 없지요? 왜 그럴까요? Load의 Base는 Image의 Base와 다를 수 있지만, Length는 항상 같겠지요? 그러니까, Load에는 따로 Length가 없고요, 또 한가지 Load에는 ZI가 없죠? 이건 이미 언급했듯이 Load View에는 ZI가 존재하지 않기 때문이에요. ROM에 저장할 이유가 전혀 없는 거죠. Image의 Base와 Length만 있으면 되는 거지요. 이런 Symbol들은 아까 ||Image$$RAM$$Base|| 이런 식으로 Assembly에서 접근 가능하다고 했는데, 그럼 C file에서도 그럴까요? 문제는 C file에서 Symbol로 $$를 쓸 수 없다는 문제가 있어요. 그래서 편법으로 어떻게 하느냐!

Assembly file에서는 이 Symbol을 Access 가능하니까, 적당한 Label로 하나 만들어서 C file에서도 접근 가능하게 만들 수 있어요. 예를 들어 SRAM region의 영역을 Access하고 싶으면,

Load__SRAM__Base
    DCD |Load$$SRAM$$Base|

Image__SRAM__Base
    DCD |Image$$SRAM$$Base|

Image__SRAM__Length
     DCD |Image$$SRAM$$Length|

Image__SRAM__ZI__Base
    DCD |Image$$SRAM$$ZI$$Base|

Image__SRAM__ZI__Length
    DCD |Image$$SRAM$$ZI$$Length|

이런 식으로 Assembly file에서 Label을 하나 정해서 그 Label에다가 이 Symbol의 값을 넣어두면C file에서는

extern byte *Image__SRAM__Base;
extern byte *Image__SRAM__Length;
extern byte *Load__SRAM__Base;
extern byte *Image__SRAM__ZI__Base;
extern byte *Image__SRAM__ZI__Length;

요런 식으로 extern 선언해서 가져다 쓸 수 있겠죠. 일단 byte를 가리키는 Pointer Type으로 선언해서 가져다 쓰면 주소를 가리키는 Pointer로 쓸 수 있는 거죠. 다른 크기를 가리키고 싶으면 casting이라는 우리의 슈퍼맨이 있지요. 캬~

위에서 Default Memory Model에서의 ZI를 initial 하는 예를 들었었는데, 이렇게 하면 C에서도 직접 가져다 쓸 수 있겠죠. SRAM의 초기값이 있는 전역변수의 초기 값들인 RW를 초기화 하는 방법은

end_point = (dword *) ( (dword) Image__SRAM__Base + (dword) Image__SRAM__Length);

for ( src = (dword *) Load__SRAM__Base,
     dst = (dword *) Image__SRAM__Base;
     dst < end_point;
     src++, dst++ )
{
     *dst = *src;
}

요렇게 하면 Load View의 RW가 Execution View의 RW로 변환되겠죠. Boot loader가 해주는 일은 이렇듯 Load View (ROM)에서 Execution View (RAM)으로 변환 (복사)를 해준다고 보시면 틀림없습니다. 냐하.

결국에 Bootloader가 해야 할 일을 정리하면, - System이 제대로 동작하기 위하여, 기본적인 Hardware 초기화 작업을 설정을 해주는 작업이 필요하며, Reset신호가 입력되면, 시스템초기화 작업 (Clock, Memory Control initialization, Watch dog timer I/O, MMU, 각 mode stack initialization)을 기본적으로 해주고, main() 과 같은 C로 구성된 함수를 호출하게 되는 거지요.

천천히 아이템별로 배열해 보면,
1) Power-on 또는 reset switch에 의하여 시스템 reset
2) Reset handler로 pc 이동.
3) Interrupt를 Disable하여 Interrupt가 걸리지 않게 막는다.
4) Watch-dog Timer를 initialization
5) System clock을 initialization and setting
6) Memory controller initialization
7) 각 mode들, 즉, SVC, USR, ABT, IRQ, FIQ, Undef 등의 stack을 initialization
8) NAND MCP의 경우에 RO/ RW를 Execution 주소에 맞게 제대로 복사하는 일을 해야 하고,
NOR MCP의 경우에는 RW를 Execution 주소에 맞게 제대로 복사해 줘야 하고요.
8) ZI 영역을 0으로 채우는 일을 해주고요.
9) main함수 등의 C함수로 branch하여 System을 Start합니다.

오홋.

각 mode별 stack initialization은 "Stack의 정체와 자세히 보기 - initialization까지" 에서 어떻게 하는지 함 보시고요. 그럼 몇 가지 연습문제 하고 지나갈까요? 이번엔 Memory Budget입니다.어떤 Embedded system에 올릴 image의 compile된 내용을 보니 다음과 같더라.
RO, RW, ZI를 각기 전부 다 더했더니 이렇게 되더라.

Code RO Data RW Data ZI Data Debug
6373664 12805833 569538 15028594 171448836 Grand Totals
================================================================
Total RO Size(Code + RO Data) 19179497 (18729.98kB)
Total RW Size(RW Data + ZI Data) 15598132 (15232.55kB)
Total ROM Size(Code + RO Data + RW Data) 19749035 (19286.17kB)
=================================================================

라고 한다면, 어떻게 우리가 필요한 Memory Size를 산정할 수 있을까요 옆에 제대로 그 용도들이 나와 있네요. Total ROM Size는 Code + RO data + RW data니까, NOR MCP를 쓰던 NAND MCP를 쓰던 ROM이 꼭 필요한 양 입니다. (19749035Byte 네요, 18.83MB정도 됩니다.) 그럼 필요한 RAM의 양은요? NOR MCP의 경우에는 Total RW size라고 쓰여져 있는 RW Data + ZI Data 부분이 필요합니다. 그러니까 15598132Byte 정도 필요하고요. 14.87MB정도 되네요. NAND MCP의 경우에는요? 모두 다 SDRAM에 올려주어야 합니다. 그러니까 Code + RO data + RW data + ZI data가 모두 올라가 주셔야 합니다. 필요한 SDRAM size는 Total RO + Total RW 라고 보시면 되겠네요. 크기는 19179497 byte + 15598132 byte = 34777629 byte고요, 적당히 33.16MB 정도 됩니다.

오호 정리하면
NOR MCP의 경우
PSRAM : 14.87MB (RW+ZI), NOR : 18.83MB (RO+RW)필요하고요.
NAND MCP의 경우에는
SDRAM : 33.16MB (RO+RW+ZI), NAND : 18.83MB (RO+RW) 필요합니다.

- 여기에 대충~잡긴 했는데, RO data는 NOR의 경우에는 NOR에, NAND의 경우에는 SDRAM에 올려줘야겠어요.- 호, NAND MCP가 SDRAM을 훨씬 더 잡아 먹네요. 그런데, 요즘 NAND MCP가 더 많이 사용되는 이유는 NOR MCP보다 NAND MCP가 같은 크기라면 더 싸다는 거~ 그래서 대용량 NAND MCP가 더 많이 사용된답니다.

사족 : 계속 용어가 헷갈리시겠지만, 이렇게 이해 하시면 됩니다.
Load = Load View = ROM,
Image = Execution View = RAM
초 간단하지요?

사족 : Bootloader가 하는 일에는 Loading하는 일만 하는 게 아니라, 메인시스템이 동작하기 전에 기본적인 하드웨어의 초기화 작업을 설정을 해주는 작업을 해주고요,
Reset신호가 입력되면, 시스템초기화 작업(Clock configuration, Memory configuration I/O initialization, MMU configuration, 각 Mode Stack 초기화 등)을 수행하는 것도 기본적으로 해주지요.

1) Power-on 에 의하여 Reset Vector로 PC가 설정됨
2) Reset handler로 branch
3) 외부의 모든 인터럽트를 받아들이지 않도록 한다.
4) Clock 주파수, PLL등을 초기화 한다.
5) 메모리 제어기를 초기화
6) SVC, USR, ABT, IRQ, FIQ, Undef 등의 stack을 초기화
7) BSS 영역을 초기화 한다.
8) C 함수로 분기하여 메인 프로그램을 시작.

뭐, 이런 일들을 해주는 셈이지요.

 
Linked at 임베디드 시스템 개발자 되기 .. at 2009/07/14 22:37

... 진실 ⓘ Coprocessor Assembly ⓙ Bootloader와 Memory Budget (Mapfile) - 어떻게 변수를 초기화 할 것인가 ⓚ Reset Handelr에서 main (Entry point)까지 & ... more

Commented by 히언 at 2009/08/10 21:13
어디가 바뀌었을까요~?
※ 이 포스트는 더 이상 덧글을 남길 수 없습니다.
친절한 임베디드 시스템 개발자 되기 강좌 글 전체 리스트 (링크) -



댓글





친절한 임베디드 개발자 되기 강좌 글 전체 리스트 (링크) -