2013년 11월 11일 월요일

[작업중] stm32f405rg를 위한 startup code 와 링커 스크립트 분석

 임베디드 시스템에서 어셈블리 언어가 아닌 C 언어를 사용하기위해서 사전에 처리해 주어야 하는 작업들이 존재한다. 이 부분에 대해서는 여러가지 복잡한 것들이 많이 존재하지만, 새로운 CPU를 사용하려고 하거나 메모리를 구체적으로 어떻게 관리하는지 알아 보려면 반드시 알아두고 넘어 가야 하는 것이다.

 C언어로 된 바이너리 코드를 실행하려면 이 바이너리 코드가 메모리의 어느 부분에서 실행되는지 그리고 코드에서 사용하는 각종 변수와 스텍 그리고 힙이라고 하는 동적 메모리 할당 영역을 램의 어느 부분에 할당하고 값을 어떻게 초기화 해야하는지 결정해야 한다. 여기에 덧붙여  CPU에서 발생하는 인터럽트를 처리하고자 한다면 인터럽트 벡터의 위치와 각종 인터럽트 벡터의 초기화를 해 주어야한다.

위와 같은 네 가지 과정은 C 언어 코드자체만으로는 구현이 불가능하다. 이 기능들은 원래 시스템 OS에서 관리해 주어야 하지만, 임베디드 시스템의 경우 사용자가 알아서 처리해야한다. 그래서 C언어로 임베디드 시스템 프로그램을 하려고 한다면 불가피하게 startup 코드와 linker script를 작성해야한다. 물론 이런 것을 편리하게 하나의 툴로 판매하는 회사 IAR, Keil 등등이 있지만, 자유소프트웨어인 gcc와 연관된 도구를 사용하고자 한다면 사용자가 작성해야 한다.

 그럼 간단히 startup 코드와 linker script가 하는 일에 대해서 살펴보자. startup 코드는 C 언어를 사용하기 이전에 필요한 스택 레지스터 초기화, 동적할당 영역을 '0'으로 초기화, 클럭 설정, 인터럽트 벡터의 지정 및 cpu에 달려 있는 메모리의 위치지정을 위한 레지스터 설정과 같은 기초적인 cpu의 초기화 작업을 수행한다. 그리고 linker script는 기본적으로 컴파일된 모든 오브젝트 파일들을 하나로 모아서 하나의 실행 가능한 출력파일로 합성하는 기능을 수행한다. 그리고 좀 더 구체적으로는 startup에서 설정한 메모리의 구성에 따라서 컴파일된 모든 오브젝트파일의 각 세그먼트들을 재배치 시키고, 실행 가능한 출력 파일을 어떻게 구성할 것인지에 대한 설정을 수행한다.

 다시 정리해서 위의 과정을 좀 더 기능적으로 구분해 보자면 다음과 같다.

 1. C언어의 바이너리 코드가 적재될 메모리의 위치 지정  
 2. 변수, 스텍, 그리고 동적 메모리 할당 영역의 위치 지정  
 3. 스텍 레지스터 및, 스텍 그리고 동적 메모리 할당 영역의 초기화  
 4. 인터럽트 벡터의 위치 지정  
 5. 인터럽트 벡터 레지스터 초기화 및 인터럽트 벡터의 초기화  


 위의 기능은 컴파일러와 어셈블리 언어에  의존적이기 때문에 이해하기에 쉽지가 않다. 나도 마찬가지이기 때문에 기존에 완성되어 있던 것을 더듬어 가는 방법으로 설명해 보고자 한다. 앞으로 사용할 코드는 gcc 기반의 크로스 컴파일러에 기반하고 그 코드는 다음의 주소에서 가져왔음을 밝혀둔다.

http://git.munts.com/arm-mcu/gcc/stm32f4/

 위의 주소에 있는 파일 중에서 살펴볼 것은 stm32f405rg.S와 stm32f405rg.ld이다. stm32f405rg.S는 소위 말하는 런타임 라이브러리라고 하는 것이고 stm32f405rg.ld는 링크 스크립트라고 하는 바이너리 코드의 코드영역, 데이터 영역, 그리고 스텍영역을 어디에 배치할지를 지정하는 스크립트 코드이다.

stm32f405rg.ld의 분석

 gcc 컴파일러의 링크 기능이 요구하는 기능은 시스템에 메모리의 구성이 어떻게 되며, C언어가 컴파일된후 코드 영역과 데이터 영역, 그리고 스텍 영역그리고 C++언어에서 추가적으로 생성하는 영역을 시스템 메모리의 어느 영역에 배치할 것이지를 요구한다.

메모리의 구성

 먼저, 시스템의 메모리 구성이 어떻게 되는지는 다음과 같이 지정한다.

 MEMORY  
 {  
  flash (rx) : ORIGIN = 0x00000000, LENGTH = 1024K  
  ram (rwx)  : ORIGIN = 0x20000000, LENGTH = 128K  
  ccm (rwx)  : ORIGIN = 0x10000000, LENGTH = 64K  
 }  

 위와 같이 세 종류의 메모리가 지정되고 있다. 각종 메모리를 지정하는 방식은 다음과 같다.

 메모리이름 (특성) : ORIGIN = 시작주소, LENGTH = 메모리의크기  

 메모리 이름은 알아 보기 쉬운 아무 것이든 가능하고 특성은 rwx같은 약자로 지정이 가능하다. 이들은 각각 Readable, Writable, 그리고 eXecutable의 약자로 읽기, 쓰기, 실행가능의 특성을 이야기 한다. 이 부분은 반드시 사용하고자 하는 CPU의 데이터 시트를 살펴보아야 한다.

 stm32f405rg의 데이터 시트를 살펴보면 다음과 같이 메모리의 구성을 알려주고 있다.

STM32F405RG의 내부 메모리 구성

 위의 그림에서 보듯이 stm32f405rg는 32bits로 지정가능한 영역인 4Gbyte영역을 8개의 512Mbyte 단위의 블럭으로 쪼개서 관리한다. 앞의 두 블럭을 제외한 나머지 영역은 외장 메모리 혹은 내장된 모든 주변장치들에 접근하는데 사용된다.

 아무튼 지금 현시점에서 중요한 것은 최하위 주소에 있는 두 블럭이다.

 다시 되돌아 가서 주소 블럭 0의 0x00000000에서 0x000FFFFF에 해당하는 영역은 1024Kbyte의 크기를 가지고 있고 첨삭된 내용을 보자면 Flash로 명칭되고 BOOT 핀을 어떻게 설정하느냐에 따라서 SRAM으로 설정될 수 도 있다고 되어 있다.

 좀 더 자세히 이야기 하자면 기본적으로 해당 영역은 읽고 실행만이 가능한 Flash로 되어 있고 BOOT 핀을 변경하면 SRAM을 위한 영역으로 변경가능하다는 이야기 이다. 이러한 기능이 왜 필요한지는 컴파일과 다운로드로 이어지는 지겨운 개발 과정을 거쳐 보았다면 충분히 이해가 가능할 것이다. 나와 같이 개념없이 개발하는 개발자에게 펌웨어 개발 과정은 컴파일하고 Flash 메모리에 굽는 과정이 솔직히 대부분이라고도 할 수 있다. 그래서 정상적으로 쓰는데 시간이 소요되는 Flash 메모리 보다 SRAM을 위의 위치에 배치 시키면 펌웨어 개발에 많은 도움이 될 수 있다.

 그럼 BOOT 핀들의 기능을 살펴보도록 하자.

stm32f405rg의 데이터 시트에 있는 부트 모드에 대한 설명

잠시 해석을 해보자면,
시작시, BOOT 핀들은 다음의 세 가지 부팅 옵션중 하나를 선택하는데 사용된다.
 * Flash로 부터 부팅하기,  
 * System memory로 부터 부팅하기,  
 * 그리고 마지막으로 내장된 SRAM으로 부터 부팅하기  
부트로더가 system memory에 위치하고 있고 USART1(PA9/PA10 핀), USART3(PC10/PC11 핀 혹은 PB10/PB11 핀), CAN2 (PB5/PB13 핀), 그리고 DFU (device firmware upgrade)를 통한 디바이스 모드의 USB OTG FS (PA11/PA12 핀)을 이용해서 Flash 메모리를 재 프로그래밍하는데 사용된다.

 즉, STmicrosystem에서 제공하는 부트로더가 이미 system memory에 내장되어 있고, 이 부트로더는 USART1, USART3, 그리고 CAN2, 디바이스 모드의 USB OTG FS를 통하여 새로운 펌웨어를 다운로드하여 Flash 메모리에 꿉어 주는 기능을 하고 있다는 이야기 이다.

 해당 system memory는 0x1fff000에서 0x1fff7a0f에 위치한다. 그리고 첨삭 내용을 보자면 OTP방식으로 CPU가 제조될때 한번만 기록이 가능한 것으로 추정된다.

그리고 BOOT 핀들의 설정에 따른 모드 변환은 다음과 같이 할 수 있다.

stm32f405rg의 BOOT 핀 설정에 따른 부트 모드의 변화

 어쨌든, "flash (rx)  : ORIGIN = 0x00000000, LENGTH = 1024K" 는 설명이 된 것 같다.

"ccm (rwx)   : ORIGIN = 0x10000000, LENGTH = 64K"에 해당하는 CCM이라는 처음 들어 보는 메모리를 살펴보도록 하자. CCM 메모리는 기본적으로 SRAM과 많은 부분의 기능들을 공유하고 있는 것으로 보여진다. 하지만 CCM(Core Coupled Memory)이라는 이름에서 보듯이 stm32f405rg의 핵심 부분인 arm cortex-m4와 직접적으로 연결되어 있는 것으로 보인다. 이와 같은 사항은 데이터 시트에서 확인 가능하다.

stm32f405rg의 내부 블럭 다이어 그램
 다른 메모리들(SRAM 112KB, SRAM 16KB, 그리고 Flash)과 다르게 AHB라는 버스 제어 블럭을 거치지 않기 때문에 아마도 CCM을 연산용 메모리를 사용하면 좀 더 빠르게 처리가 가능할 것으로 여겨진다. 하지만, 그림에서도 보듯이 D-BUS만 연결되어 있기 때문에 코드가 직접 실행되는 것을 불가능하고 데이터를 저장하고 읽는 것만 가능한 것 같다. 그럼 CCM의 특성에 해당하는 "x"는 제거하는 것이 마땅할 것으로 여겨진다. 다음과 같이...

 ccm (rw)  : ORIGIN = 0x10000000, LENGTH = 64K  

 그리고 마지막으로 SRAM은 하드웨어적으로 두 개로 구분되어 있지만 두 SRAM이 연속된 메모리 공간에 위치해 있기때문에 하나의 SRAM으로 간주하여 지정되어 있다.

 하지만, 위의 메모리 구성 그림에는 있으나, 스크립트에서는 제외된 Flash 메모리가 존재한다. 위치는 0x00800000에서 0x008fffff에 해당하는 1024Kbyte용량의 메모리이다. 추정해 보건데 아마도 BOOT 핀들의 구성에 따라 Flash가 재 배치되는 공간인 것 같다. 광고에도 없는 메모리를 보너스를 넣어 줄 만큼 STMicrosystem이 훈훈하지는 않을 것이다. 아무리 선진국 프랑스의 회사라고 해도 말이다.

바이너리 코드의 섹션 배치용 변수 지정

 위와 같이 구성된 메모리에 C언어로 컴파일된 바이너리 코드를 배치하기 위해서 링커스크립트는 다음과 같은 참조용 변수를 지정한다. 이것은 어디 까지나 사용자에 따라 무한히 변경이 가능하기 때문에 용도에 따라서 얼마든지 변경 가능하다.

 __rom_start__     = ORIGIN(flash);  
 __rom_size__     = LENGTH(flash);  
 __ram_start__     = ORIGIN(ram);  
 __ram_size__     = LENGTH(ram);  
 __ram_end__     = __ram_start__ + __ram_size__;  
 __stack_end__     = __ram_end__;          /* Top of RAM */  
 __stack_size__     = 16K;  
 __stack_start__     = __stack_end__ - __stack_size__;  
 __heap_start__     = __bss_end__;          /* Between bss and stack */  
 __heap_end__     = __stack_start__;  
 __ccm_start__     = ORIGIN(ccm);  
 __ccm_size__     = LENGTH(ccm);  

 위의 변수들은 이름에서 보듯이 전통적으로 실행 코드만 위치하게 되는 rom 영역의 시작주소와 크기, 데이터만 들어 가게 되는 ram 영역의 시작주소 및 크기, 그리고 램의 마지막주소에서 16Kbyte영역에 해당하는 stack을 위한 영역 시작 주소와 크기, 그리고 마지막 주소에서 부터 사용하는 stack 의 초기 주소를 heap영역을 bss영역의 마지막 주소에서 stack의 시작주소에 해당하는 영역으로 지정한다. 그리고 마지막으로 특수 영역인 ccm의 시작 주소와 크기를 지정한다. 이와 같은 변수들은 startup 코드인 stm32f405rg.S에서 나중에 참조하여 레지스터를 설정하고 영역 초기화하는데 사용할 것이다. 하지만 위의 변수 중 __bss_end__라는 것은 정의 되지 않고 참조만 하고 있다. 이것은 나중에 다시 나올 것이다. 여담 이지만 이와 같은 코드는 상호 참조가 너무 많아서 어디에서 정의 되는지 아니면 컴파일러에서 만들어 주는 것인지 판단하기가 쉽지 않다. 아무튼 어디엔가 있을 것이다.

 SECTIONS  
 {  
  . = 0;  
  .text : {  
  KEEP(*(.startup))               /* Startup code */  
  *(.text*)                    /* Program code */  
  KEEP(*(.rodata*))               /* Read only data */  
  *(.glue_7)  
  *(.glue_7t)  
  *(.eh_frame)  
  . = ALIGN(4);  
  __ctors_start__ = .;  
  KEEP(*(.init_array));               /* C++ constructors */  
  KEEP(*(.ctors));               /* C++ constructors */  
  __ctors_end__ = .;  
  . = ALIGN(16);  
  __text_end__ = .;  
  } >flash  
  .data : ALIGN(16) {  
  __data_beg__ = .;               /* Used in crt0.S */  
  *(.data)                    /* Initialized data */  
  __data_end__ = .;               /* Used in crt0.S */  
  } >ram AT > flash  
  .bss (NOLOAD) : ALIGN(16) {  
  __bss_beg__ = .;               /* Used in crt0.S */  
  *(.bss)                    /* Uninitialized data */  
  *(COMMON)                    /* Common data */  
  __bss_end__ = .;               /* Used in crt0.S */  
  } >ram  
 /* C++ exception unwinding stuff--needed by some toolchains */  
  .ARM.extab : { *(.ARM.extab* .gnu.linkonce.armextab.*) } >flash  
  __exidx_start = .;  
  .ARM.exidx : { *(.ARM.exidx* .gnu.linkonce.armexidx.*) } >flash  
  __exidx_end = .;  
 }  


 위의 섹션 지정을 이해 하려면 linker command language를 약간 알아 두어야 한다. 먼저 섹션은 크게 3가지로 재배치 되어 진다. text, data 그리고 bss로 관리되며, 그 이름에서도 알 수 있듯이 각각 프로그램 코드, 변수의 데이터, 그리고 동적할당과 Stack을 위한 메모리 공간으로 이름 붙여 졌음을 알 수 있다. 그리고 그 해당 영역은 flash, ram, ram으로 할당되어 있다. 

 

 다음은 C언어를 컴파일러가 메모리 영역을 관리하는 각 섹션에 대한 설명이다.
- Code 섹션: 순수한 실행 코드만 가지고 있는 섹션, 일반적으로 rom과 같은 매체에 위치하게 된다.
- Data 섹션 : 값이 초기화된 전역변수와 값이 초기화된 static 타입의 함수 내에서 정의된 변수를 위해 할당된 메모리 공간
- Bss 섹션 : 값이 초기화 되지 않은 전역변수와 값이 초기화 되지 않은 static 타입의 함수 내에서 정의된 변수를 위해 할당된 메모리 공간
- Heap 섹션 : 동적 메모리 할당(malloc, realloc 등의 기능)을 위해 확보된 메모리 공간
- Stack 섹션 : 말 그대로 스텍 영역 - 함수 호출등으로 발생하는 변수 넘기기 혹은 각 함수의 지역 변수를 위해 확보되는 메모리 공간

 

 text 섹션을 살펴보면, 우선 첫 번째 문장은 location counter를 0으로 설정하는 부분으로 혹시나 해서 넣어 둔 것 같다. 자동적으로 0이 될 것은 자명하기 때문이다(".=0"). 그리고 다음 줄은 출력 파일 즉, 컴파일 과정에서 수집된 모든 실행 파일의 text section을 채워 넣는 부분으로 위치는 위에서 설정한 flash의 영역이 된다(".text : {"). 이 text section에는 순수하게 프로그램을 수행하기 위한 코드들을 위한 메모리 영역이고 프로그램이 수행되는 기간 동안에 내용이 변경될 경우를 고려하지 않는 영역이다.  첫 번째로 채워질 내용은 "KEEP(*(.startup))"으로  startup section의 그 내용이 다른 곳에서 참조되어 지지않거나 사용되지 않아도 강제적으로 넣어 두도록 KEEP 옵션을 추가해 두었다. 그리고 본래의 목적인 프로그램을 수행하는 내용을 담고 있는 text section 을 추가하고 ("*(.text*)"),  통상 인터럽트 테이블 같은 참조 목적으로만 생성된 데이터를 위한 rodata section을 추가한다("KEEP(*(.rodata*))"). 여기까지가 가장 핵심이 되는 재 배치될 text section의 내용이고, 그 내용은 startup 코드 내용, 본래의 프로그램 코드 내용, 그리고 인터럽트 벡터 테이블 같은 참조용 데이터 들이 차지하고 있다. ". = ALIGN(4);"을 통해서 이후의 내용의 경계를 4바이트 단위로 관리한다. .glue_7과 .glue_7t는 각각 Arm 컴파일러가 생성해 내는 중간 코드를 위한 영역으로 glue_7섹션은 ARM Mode에서 동작할때의 중간 코드, glue_7t는 Thumb mode에서 동작할 때의 중간 코드를 가지고 있을 것이라 추정된다. 그리고 나머지 부분들은 C++코드를 수행하는데 필요한 생성자의 시작(__ctors_start), 끝(__ctors_end__)을 지정하는 환경 변수 지정, 그리고 그 안에 생성자(ctor 섹션), 그리고 init_array 색션을 놓아 두게 된다.

  data 섹션은 모든 바이너리 파일들의 data 섹션을 한데 모아 넣어 두고 그 영역의 시작(__data_beg__), 끝(__data_end__)에 해당하는 환경 변수도 잊지 않고 만들어 준다. 그리고 data 섹션의경계는 16바이트 단위로 관리한다("ALIGN(16)"). 이 내용은 모두 ram에 적재되지만, 그 복사본을 flash에 담아 둔다(">ram AT > flash").  이 부분은 코드 내용과 같이 보아야만 이해 할 수 있다.

 마지막으로 bss 섹션은 모든 바이너리 파일의 초기화 되지 않은 데이터를 위한 bss 섹션과 COMMON 섹션을 담아 두고 그 영역의 시작(__bss_beg__)과 끝(__bss_end__)을 환경 변수로 등록한다. 여기서 NOLOAD는 솔직히 나도 정확히 어떤 역할을 하는지 모르겠다.

그럼 위의 각 섹션이 stm32f405rg의 메모리 구성에 비추어 어떻게 배치되는지 그림을 그려보자.


stm32f405rg.S의 분석

해당 코드의 첫 번째 내용은 다음과 같다.

		.syntax 	unified
		.thumb
		.section	.startup, "x"

위에서 보듯이 Thumb  모드에서 동작하는 코드이며, 차지하게될 영역의 센셕을 startup으로 지정한다. "x"는 실행하는 목적으로 생성되는 영역이라는 지시어 같다.

 그리고 다음은 다른 소스코드 파일에서 참조할 수 있도록 전역 변수를 지정하고 있다.

		.global		_start
		.global		_vectors

그 변수는 _start와 _vectors로 초기화 코드 내용의 시작지점 그리고 인터럽트 벡터의 시작 위치를 지정하고 있다.



------------------------------------------------------------------------------------------------------------
참고 사항: FreeRTOS에서는 자체적인 동적 메모리 할당 메카니즘을 가지고 있다. 메모리를 할당하는 기능은 pvPortMalloc()이 하고, vPortFree()로 할당된 메모리를 해제한다. 그리고 내부적으로 할당하는 기능을 여러가지로 지원하는데 ./FreeRTOS/Source/portable/MemMang/ 밑에 총 네 가지의 동적 메모리 관리 기능을 heap_?.c등의 파일로 지원하고 관리하게 될 heap 영역의 크기는 각 Demo 디렉토리 밑에 두어야 할 FreeRTOSConfig.h에 다음과 같이 지정한다.

 #define configMINIMAL_STACK_SIZE          ( ( unsigned short ) 130 )  
 #define configTOTAL_HEAP_SIZE               ( ( size_t ) ( 75 * 1024 ) )  
------------------------------------------------------------------------------------------------------------



............작업중..........

댓글 6개:

  1. 포스팅 잘보앗습니다.. cortex-m4 스택초기화 함수가 어디에 있나요.. cortex-m3 같은 경우는 macro.h에 위치한 msr msp를 이용했는데 말이죠..

    답글삭제
    답글
    1. 예전에는 STmicrosystem 사가 관리하였던 표준 라이브러리에서 주변부 라이브러리(Stdperiph)와 코어 라이브러리(CMSIS)를 같이 관리하였었는데 요즘은 이게 분리 되어 버려서 크게 변화가 생긴 것 같습니다. 이제는 코어 라이브러리는 Arm에서 직접 관리하는 쪽으로 가고 코어 CPU에 딸려 있는 주변기기를 위한 라이브러리는 제조사에서 관리 하는 방향으로 갈 것 같습니다. 어떤 버전의 표준 라이브러리를 사용하시는 지 알길이 없으나 Arm사가 관리하는 CMSIS라는 독립된 코어 라이브러리에 찾아 보시면 찾으 실 수 있으실 겁니다. 예를 들면 CMSIS/Include/core_cm4.h. 나 CMSIS/Include/core_cmFunc.h.

      삭제
  2. 감사합니다 큰 도움이 되었습니다

    답글삭제
  3. 고마워요~~ 참고가 많이 되었습니다.

    답글삭제
  4. 좋은글 잘 읽었습니다.

    답글삭제