본문 바로가기
임베디드/STM32

4. [STM32] Entry Point

by fuhehe 2024. 7. 12.

Entry Point(진입점)은 프로그램이 최초 실행될 때 제일 처음 실행되는 위치를 의미한다.

우리는 C/C++로 프로그램을 만들고 있고 C/C++의 Entry Point는 main함수이다.

이 장에서 설명할 건 사실 C/C++의 Entry Point가 아니고 MCU가 어떻게 main함수를 호출하는가 즉, main함수 호출 직전까지의 Booting Sequence이다.

 

MCU/CPU등의 프로세서 동작 방식을 이해하는데 도움이 될 거 같아서 이 장을 마련했다.

STM32를 라이트하게 배우는 사람들은 이 장의 내용이 조금 어려울 수 있다. 이런 내용에 관심이 없다면 건너 뛰어도 좋고 나중에 어느 정도 실력이 쌓인 후 봐도 좋다.

 

하지만 앞장에서 배운 디버거를 사용해 MCU의 동작방식을 익히는건 앞으로 STM32를 공부하는데 있어 좋은 지식이 될 것이다.

따라서 조금 어렵더라도 천천히 읽어보고 필요하다면 다른 사이트의 정보도 검색해 보면서 공부하기 바란다.

 

  • 본 블로그는 STM32를 소프트웨어 엔지니어 관점에서 바라본 블로그입니다. 따라서 회로등의 전자공학 관련 내용은 사실과 다를 수 있습니다.
  • 본 블로그에서 사용된 MCU 및 개발보드는 다음과 같습니다.
    1. NUCLEO-F429ZI (STM32F429ZIT LQFP144)
    2. NUCLEO-F439ZI (STM32F439ZIT LQFP144)

 

 

1.  준비물

먼저 이 장을 진행하기 전에 아래 링크된 문서 두개를 다운로드 받자.

소스는 2장에서 작성한 HelloWorld 프로젝트를 그대로 사용한다.

 

<STM32 Reference Manual>

 

이 문서는 1700페이지에 달하는 방대한 자료를 담고 있다. STM32의 메모리 구조와 모든 Peripheral 레지스터 정보를 기술하고 있다.

 

<STM32 Programming Manual>

 

이 문서는 STM32 프로그래밍 매뉴얼이다. 프로그래밍이라고 해서 일반 사용자를 위한 HAL_로 시작하는 함수내역을 설명한 문서는 아니고 컴파일러나 OS 제작등의 프로그래밍에 관련된 문서이다.

이 문서는 STM32의 코어 레지스터, 어셈블리 명령어 등에 대해 기술하고 있다.

 

 

2. STM32의 메모리

[ STM32F429 Memory Map]

 

STM32F429ZI/439에는 다음과 같은 종류의 메모리가 있다.

  • Internal Flash Memory : 2Mb의 크기. 우리가 작성한 코드와 Interrupt Vector Table이 들어가는 메모리이다. 따라서 실행시에는 일반적으로 Read Only 메모리이므로 쓰기를 할 수 없다.
  • Embedded SRAM : 256Kb의 크기. 프로그램에서 사용할 Data, Heap, Stack등이 여기 저장된다. 우리 프로그램을 Flash메모리가 아니라 SRAM에 올릴 수도 있다. SRAM은 다시
    • SRAM1 :112Kb
    • SRAM2 : 16Kb
    • SRAM3 : 64Kb
    • CCM(Core Coupled Memory) : 64Kb
      로 나뉜다.
  • Backup RAM : 4Kb의 크기. 전원이 나간 뒤 Vbat핀의 배터리를 통해 엑세스가 가능한 메모리.
  • System Memory : 30Kb의 크기. ST사에서 만든 Boot Loader가 들어있는 메모리이다. Read Only메모리이므로 쓰기는 할 수 없다.

 

이와 같이 칩 내부에 여러 종류의 메모리가 있는데 위의 그림과 같이 STM32는 이들 메모리를 하나의 선형 메모리(Linear Memory)구조로 관리한다.

따라서 우리는 각 메모리를 엑세스 할때 별도 구분없이 해당 메모리의 시작위치만 알면 동일한 방식으로 엑세스 할 수 있다.

STM32는 32bit프로세서이므로 주소는 0x0000 0000 ~ 0xFFFF FFFF까지 총 4Gb의 메모리 엑세스가 가능하다.

 

그런데 이 선형 메모리 구조에서 위 그림의 가운데 SRAM상세 부분을 보면 각 메모리의 주소가

  • Flash Memory : 0x0800 0000 ~ 0x081F FFFF
  • System Memory : 0x1FFF 0000 ~ 0X1FFF 7A0F
  • SRAM(112Kb) : 0x2000 0000 ~ 0x2001 BFFF

로 나와 있는걸 볼 수 있고 제일 아래 부분에 "Alias to Flash, system memory or SRAM depending on the BOOT pins"부분의 주소가 0x0000 0000 ~ 0x001F FFFF로 나와 있다.

 

이 말은 아래 부팅 방식에서 설명할 BOOT1, BOOT0번 핀 상태에 따라 Flash Memory, System Memory, SRAM중 하나의 원래 주소가 0x0000 0000에 Alias방식으로 매핑된다는 의미이다. 

예를 들어 Flash Memory 부팅이면 0x0800 0000이 0x0000 0000으로 Alias되어 두 주소가 동일한 값을 가진다는 의미이다.(Alias이므로 복사가 되는게 아니다. C++에서 참조형 정도로 이해하자.)

 

이를 확인하려면 프로그램을 디버그 모드로 실행한 뒤 main()의 HAL_Init()부분등에서 브레이크 포인트를 설정하자.

그리고 브레이크가 걸린 상태에서 Memory Browser창을 통해 확인해 볼 수 있다. 창의 주소입력란에서 0x0000 0000을 입력해 조회해보고 Flash Memory의 시작주소인 0x0800 0000을 다시 조회해보자.

동일한 값들이 나올 것이다.

 

3. Core Register

Core Register는 MCU가 프로그램을 실행하는데 필요한 가장 중요한 레지스터들의 집합이다.(선형메모리로 관리되는 Peripheral Register와는 다르다.)

이 레지스터는 STM32 Programming Manual의 p18에 자세히 설명되어 있다. 

[Core Register]

크게 보면 R0~R12까지의 범용 레지스터와 SP, LR, PC 그리고 PSR, PRIMASK등의 특수 레지스터들이 보인다.

다른 레지스터는 기회가 되면 설명하기로 하고 이 장에서 봐야될 레지스터는 SP와 PC 두가지이다.

SP(Stack Pointer) 레지스터는 메모리상의 현재 Stack위치를 저장하고 있다. 그리고 PC(Program Counter) 레지스터는 현재 실행될 명령어가 저장된 메모리의 주소가 저장되어 있다.

 

MCU는 PC에 저장된 주소위치에 있는 명령어를 실행하고 PC값을 4Byte(32bit)만큼 증가시킨다. 그리고 다시 그 위치의 명령어를 실행한다. 이걸 무한 반복해서 우리가 작성한 프로그램을 수행한다.(물론 중간에 분기문을 만나면 4Byte증가가 아니라 해당 분기문의 타겟 위치 주소가 된다.)

 

SP의 경우 현재의 Stack Pointer위치를 가지고 있는데 Stack이라는건 LIFO(Last In First Out)형태의 자료구조를 말한다. 즉 이 자료구조에 데이터를 집어넣고 다시 꺼낼 때 제일 마지막에 집어넣은 값을 먼저 꺼내는 구조이다.

예를들어 Stack에 1, 2, 3, 4, 5 라는 숫자를 차례대로 넣었다면 꺼낼때는 차례로 5, 4, 3, 2, 1순서로 가져오는 것이다.

참고로 꺼낼때 들어간 순서대로 1, 2, 3, 4, 5와 같이 꺼내오는 자료구조를 Queue라고 한다. Queue는 FIFO(First In First Out)구조이다.

이 스택이 쓰이는 용도는 여러가지가 있겠지만 대표적으로 MCU에서 함수호출시 사용된다. 그리고 우리가 각종 편집기 등에서 입력하다 Ctrl+Z를 눌러 Undo를 수행할 때도 사용된다.

다음의 Pseudo 코드를 보자.

bool func2(int a, int b) {	
    return result > 100 ? true : false;
}

int func1(int x) {
    int result = 0;
    	
    if(func2(x, 10))
    	return 1;
    else
    	return 0;
}

void main() {
    int a = 0;
    int x = 1;
    int y = 2;
    int z = 0;
    
    z = x + y;
    
    a = func1(x);
    
    z += a;
}

 

코드 내용은 아무 의미없는 내용이므로 분석할 필요는 없다. 다만 여기서 함수 호출 순서를 중점적으로 봐야 하는데 함수 호출 순서를 보면

main()->func1()->func2() 와 같다. 

그런데 func2()함수까지 실행되고 func2()에서 return될 때 어느 함수로 와야하는가? func1()에서 func2()를 호출한 부분으로 돌아와야 한다. 그리고 func1()이 실행되어 return될 때 main()함수에서 func1()이 호출된 위치로 와야 한다.

즉 하나의 함수가 완료되어 리턴될 때 가장 마지막에 그 함수를 호출한 위치부터 재개되어야 하는데 해당 함수로 이동하기전 Stack에 현재 함수에서 사용한 레지스터와 현재 함수의 PC위치등을 저장해 놓으면 호출된 함수가 완료되어 return될 때 Stack에서 마지막에 입력된 데이터부터 꺼내와서 호출순서를 역행해 다시 재개할 수 있다.

이런 목적으로 Stack을 사용하는데 이때의 현재 Stack위치를 저장하는 레지스터가 SP이다. 따라서 SP가 없으면 함수 호출과 같은 분기를 할 때 원래 위치로 돌아와 원래 하던 작업을 다시 진행할 수 가 없다.

 

다음은 우리가 작성한 프로그램이 어떤 형태로 선형 메모리에 올라가는지를 설명한 그림이다. 이 그림은 STM32뿐 아니라 우리가 사용하는 Windows프로그램도 마찬가지다.

[프로그램의 메모리 구조]

제일 아래 코드 영역이 위치한다. 여기에는 우리가 작성한 프로그램이 기계어 형태로 저장되어 있다. Vector 테이블 또한 이 영역에 있다. Flash Memory로 부팅했다면 0x0800 0000영역이 0x0000 0000으로 Aliasing되어 위치할 것이다.

그 위로 Data영역이 존재한다. 이 영역은 소스상에서 선언된 전역변수, static변수 그리고 각 리터럴값(C의 const변수등)들이 위치한다. 이들 값들은 프로그램이 최초 실행될 때 메모리에 만들어 지고 종료될 때 까지 사라지지 않는다.

이 두 영역은 컴파일시 그 크기가 정해진다. 컴파일러는 코드의 크기와 소스상에서 사용한 전역변수, static변수의 종류와 갯수를 알수 있기 때문이다.

 

다시 그 위에는 Heap영역이 나온다.  이 영역은 프로그램 실행중 임의로 할당되고 해제되는 메모리 영역이다. 

최상위 영역이 Stack영역인데 위에 설명한대로 함수 호출시 이전 레지스터 값들을 저장해 놓는 영역이다. 또한 우리가 작성한 함수의 매개변수, 함수내의 지역변수들도 이 영역에 위치한다.

그림에서 보듯 Stack은 최초 0x2003 0000이고 값을 추가(Push)할 수록 아래로 내려간다. 즉 push r0 라는 어셈블리어로 Stack영역에 r0 레지스터 값을 저장하면 StackPointer는 -4Byte되어 0x2002 FFFC가 된다.

Stack과 Heap영역은 컴파일러가 컴파일시 크기를 알 수 없다. 이 두영역은 실행시 그 크기가 동적으로 변경된다.

 

void func1() {
    int *p = (int*)malloc(sizeof(int) * 10);
    
    ...
    
    free(p);
}

 

위의 함수에서 포인터 변수 p는 Stack에, malloc으로 할당된 40Byte는 Heap영역에 위치하고 포인터 변수 p는 이 Heap영역에 할당된 40Byte의 첫번째 주소값을 가지고 있다.

 

 

4. 부팅 방식

STM32에는 3가지 부팅방법이 있다. 레퍼런스 매뉴얼 69페이지 Boot configuration항목을 보면

[Boot modes]

와 같이 설명되어 있다. BOOT1, BOOT0 핀의 상태에 따라 부팅방식이 달라지는데 각각 어느 위치의 메모리에 있는 코드로 부팅을 할지 결정할 수 있다.

우리는 현재 NUCLEO-F429/439 보드를 사용하고 있으므로 보드의 매뉴얼을 보자.

[NUCLEO Pin]
[NUCLEO보드 Solder bridge]

먼저 위 핀상태를 보면 7번핀이 BOOT0이고 주석에 디폴트는 0이고, 5번핀(VDD)과 연결해서 1로 설정할 수 있다고 나와있다. 


그리고 그 아래 표(NUCLEO보드 Solder bridge)에 보면 뒷면 Solder Bridge의 상태에 따라 BOOT1번 핀의 상태를 결정할 수 있다. NUCLEO-F429/439는 BOOT1번 핀으로 PB2를 사용한다. 따라서 이 핀을 Booting용도로 사용한다면 PB2는 다른 목적으로는 사용할 수 없다.

현재 보드 디폴트는 둘다 연결되어 있지 않으므로 첫번째 OFF, OFF여서 BOOT1로 사용하지 않는다.

 

따라서 현재 보드를 커스터마이즈하지 않았다면 보드의 Boot Mode는 Main flash memory방식이다.

 

 

위와 같이 Pin의 상태에 따라 우리는 부팅방식을 변경할 수 있다.

  • Main flash memory 부팅 : 우리가 이번장에서 보게될 기본적인 Booting 방식이다. 이 방식은 우리가 작성한 C프로그램을 Flash Memory에 올리고 이 내용대로 부팅을 시키는 방법이다.
  • System memory 부팅 : ST사에서 만들어서 System Memory에 넣어놓은 Boot Loader로 부팅을 하는 방식이다. 이 방식이 선택되면 Boot Loader는 UART나 USB OTG등으로부터 데이터 수신을 기다리고 데이터가 들어오면 이를 Flash Memory에 기록한 뒤 그 내용대로 부팅을 시작한다. 즉 펌웨어 업데이트 등의 용도에 사용되는 부팅방식이다.
  • Embedded SRAM 부팅 : SRAM에 저장된 내용대로 부팅하는 방식이다. 디버그나 특수용도로 사용한다고 되어 있는데 정확히 어떤 상황에서 하는건지는 구글링을 해봐도 정확한 내용을 찾을 수 없었다. 그래서 이 부팅방식은 더 이상 설명하지 않겠다. 모르는걸 설명할 수 는 없으니까...

아무튼 우리가 이 장에서 보고자 하는 부팅방식은 가장 일반적인 Main flash memory로 부팅하는 것이다. 위의 설명에서 System memory부팅시에 ST사의 Boot Loader가 실행된다고 하였는데 사실 이 방식도 최초에는 이 Boot Loader가 실행되어야 한다.

아래는 STM32F42xxx/43xxx의 V9.x버전의 Boot Loader순서도이다. 참고만 하자.

[Boot Loader 순서도]

 

 

5. Boot Sequence

지금부터는 이 장의 주제인 Boot Sequence를 알아보고 어떻게 main()함수가 실행되는지 보도록 하자.(Main flash Memory방식의 부팅 모드로 설명한다.)

 

  • 최초 전원이 인가되면 MCU는 System Memory의 Boot Loader를 실행한다. 이 Boot Loader는 BOOT1, BOOT0 Pin의 상태에 따라 각각의 Boot Mode동작을 수행하게 된다.
  • Main flash Memory부팅이므로 0x0800 0000번지를 0x0000 0000에 Alias시킨다. 지금부터는 0x0000 0000번지를 엑세스하는건 0x0800 0000번지를 엑세스하는것과 동일하다.
  • PC(Program Counter) 레지스터 값을 0x0으로 설정한다.
  • PC에 0x0이 들어가 있으므로 지금부터 MCU는 메모리의 0x0000 0000위치의 명령을 실행하게 된다.
  • 현재 0x0000 0000주소는 Flash Memory의 주소인 0x0800 0000이 Aliasing되어 있고 Flash Memory는 우리가 만든 프로그램이 컴파일되어 저장되어 있다.
  • 따라서 지금부터는 우리가 만든 프로그램이 동작되기 시작한다.

위의 동작까지 진행했으면 그 다음부터는 우리가 만든 프로그램이 실행되므로 소스를 보면서 설명하겠다.

 

먼저 Project Explorer에서 STM32F429ZITX_FLASH.ld 파일을 찾아 열어보자.(사용하는 MCU에 따라 이름이 바뀔 수 있다.) 프로젝트 트리의 아래부분에 있다.

이 파일은 소스 컴파일시 프로그램이 메모리상에 어떻게 위치하는지를 정의해 놓은 파일이고 LinkerScript파일이라고 부른다.

이 파일은 컴파일러(정확히는 링커)에게 컴파일 시 실행파일이 메모리에 어떻게 적재되어야 하는지를 기술한 파일이다.

 

우선 첫부분을 보자.

/* Entry Point */
ENTRY(Reset_Handler)

/* Highest address of the user mode stack */
_estack = ORIGIN(RAM) + LENGTH(RAM); /* end of "RAM" Ram type memory */

_Min_Heap_Size = 0x200; /* required amount of heap */
_Min_Stack_Size = 0x400; /* required amount of stack */

/* Memories definition */
MEMORY
{
  CCMRAM    (xrw)    : ORIGIN = 0x10000000,   LENGTH = 64K
  RAM    (xrw)    : ORIGIN = 0x20000000,   LENGTH = 192K
  FLASH    (rx)    : ORIGIN = 0x8000000,   LENGTH = 2048K
}

제일 처음 나오는 항목이 Entry Point이다. 현재 Entry Point는 Reset_Handler로 정의되어 있다.

이 Entry Point는 컴파일된 파일인 .elf파일에 보면 Entry Point address로 지정되어 있다.

우리가 사용하는 컴파일러는 gcc로 GNU C컴파일러이다. 주로 Unix/Linux에서 사용되는데 이 컴파일러의 컴파일 결과물이 elf파일이다. 

우리가 만든 HelloWorld.elf파일을 Linux의 readelf명령어로 보면 아래와 같이 Entry Point의 주소가 0x0800 0E19로 되어 있다. 

[ELF파일 헤더]

 

elf파일의 제일 처음에는 Vector Table이 정의되어 있고 이 Vector Table의 시작주소는 0x0000 0000이다.

이 Table에는 각 인터럽트 핸들러의 주소 리스트가 나열되어 있다.

아래 프로그래밍 매뉴얼에 나와 있는 Vector Table을 보자.

[Vector Table]

그림에서 보듯 제일 처음 SP 레지스터 초기값 설정이 나오고 그다음 Reset 인터럽트가 나온다.

그 위로 앞장에서 살펴봤던 stm32f4xx_it.c 파일에 정의된 각 인터럽트 이름이 나온다.

이 각 Vector Table에는 이들 인터럽트에 대한 처리 핸들러 시작주소가 기록되어 있다.

elf파일에서 Entry Point가 0x0800 0E19로 되어 있는데 디버거로 0x0000 0000위치의 메모리를 보면 첫번째에 0x2003 0000, 그리고 두번째에 0x0800 0E19가 기록되어 있는걸 볼 수 있다. 첫번째가 초기 SP값, 두번째가 Reset인터럽트의 핸들러 주소값이다. 그 뒤로 NMI, Hard fault핸들러에 대한 주소값도 연속적으로 나온다.

[0x0000 0000 메모리 값]

따라서 MCU는 PC 레지스터의 현재 값에 따라 0x0000 0000의 값으로 SP 레지스터 값을 설정하고 그 다음 0x0000 0004에 있는 Reset Handler의 주소로 이동해 Reset Handler를 실행한다.

그러면 이 Reset_Handler는 어디에 정의되어 있을까?

다시 Project Explorer에서 /Core/Startup/startup_stm32f429zitx.s 파일을 찾아 열어보자.

어셈블리어로된 초기 설정 프로그램 소스가 열린다. 어셈블리어를 몰라도 상관없으므로 중요한 부분만 살펴보자.

이 소스의 60라인쯤 보면 Reset_Handler:라는 라벨이 보이는데 이 부분이 Entry Point가 되는 코드의 시작부분이다. 즉, 우리가 만든 프로그램이 Flash Memory에 올라갈 때 첫번째 실행될 부분이다.

[Reset Handler]

위 그림은 Reset Handler안에서 SystemInit으로 분기하는 루틴에 브레이크 포인트를 잡은 상태이다. 이 상태에서 Disassembly창을 보면 Reset_Handler의 시작 주소가 0x0800 0E19이고 현재 0x0800 0E1C의 SystemInit분기직전에 브레이크가 걸린걸 볼 수 있다.

 

다시 Linker Script로 돌아가자.( STM32F429ZITX_FLASH.ld ) 

그 밑으로 _estack 값을 설정하는 부분이 나오는데 ORIGIN(RAM) + LENGTH(RAM)은 그 밑에 MEMORY부분을 보면 RAM의 ORIGIN은 0x2000 0000, LENGTH는 192K즉 192*1024 = 0x30000 이므로 _estack의 값은 0x2000 0000 + 0x0003 0000 = 0x2003 0000이 된다.

이 값이 우리가 SP를 초기화 할 때 사용하는 값이되는데 이 값은 startup_stm32f429zitx.s파일의 Reset_Handler:부분의 첫번째 명령어로

ldr sp, =_estack /* set stack pointer */

와 같이 해당 값으로 SP를 설정하는걸 볼 수 있다.

 

MEMORY 다음으로 SECTIONS부분이 나온다. 여기서 부터 실제 이 실행파일이 메모리상에 어떤 순서로 저장될 것인지가 정의된다. 

간략하게 살펴보면

SECTIONS
{
  /* The startup code into "FLASH" Rom type memory */
  .isr_vector :
  {
    . = ALIGN(4);
    KEEP(*(.isr_vector)) /* Startup code */
    . = ALIGN(4);
  } >FLASH

  /* The program code and other data into "FLASH" Rom type memory */
  .text :
  {
    . = ALIGN(4);
    *(.text)           /* .text sections (code) */
    *(.text*)          /* .text* sections (code) */
    *(.glue_7)         /* glue arm to thumb code */
    *(.glue_7t)        /* glue thumb to arm code */
    *(.eh_frame)

    KEEP (*(.init))
    KEEP (*(.fini))

    . = ALIGN(4);
    _etext = .;        /* define a global symbols at end of code */
  } >FLASH

  /* Constant data into "FLASH" Rom type memory */
  .rodata :
  {
    . = ALIGN(4);
    *(.rodata)         /* .rodata sections (constants, strings, etc.) */
    *(.rodata*)        /* .rodata* sections (constants, strings, etc.) */
    . = ALIGN(4);
  } >FLASH
  
  <중략>
  
    /* User_heap_stack section, used to check that there is enough "RAM" Ram  type memory left */
  ._user_heap_stack :
  {
    . = ALIGN(8);
    PROVIDE ( end = . );
    PROVIDE ( _end = . );
    . = . + _Min_Heap_Size;
    . = . + _Min_Stack_Size;
    . = ALIGN(8);
  } >RAM

 

첫번째로 .isr_vector가 나오는데 이 부분이 Interrupt Service Routine이고 이걸 가장 먼저 위치시겠다는 의미이다. 위의 Vector Table이 0x0000 0000 (실제 주소는 Flash메모리인 0x0800 0000)부터 시작하는 것은 이렇게 SECTIONS부분에서 .isr_vector가 제일 처음 나오기 때문이다.

그 다음으로 .text에는 실제 컴파일된 코드가 들어간다. 그리고 제일 밑으로 내려가면 Heap과 Stack에 대한 정의도 나온다.

 

그렇다면 제일처음에 .isr_vector가 나와야 한다는건 알겠는데 실제 이 Vector Table이 정의된 곳은 어디일까?

실제 Vector Table이 정의된 곳은 아까 Reset_Handler가 정의되어 있던 startup_stm32f429zitx.s파일이다.

/******************************************************************************
*
* The minimal vector table for a Cortex M3. Note that the proper constructs
* must be placed on this to ensure that it ends up at physical address
* 0x0000.0000.
* 
*******************************************************************************/
   .section  .isr_vector,"a",%progbits
  .type  g_pfnVectors, %object
   
g_pfnVectors:
  .word  _estack
  .word  Reset_Handler

  .word  NMI_Handler
  .word  HardFault_Handler
  .word  MemManage_Handler
  .word  BusFault_Handler
  .word  UsageFault_Handler
  .word  0
  .word  0
  .word  0
  .word  0
  .word  SVC_Handler
  .word  DebugMon_Handler
  .word  0
  .word  PendSV_Handler
  .word  SysTick_Handler

이렇게 정의된 부분이 있는데 .section 문에 의해 g_pfnVectors: 부분을 Insruction으로 정의하겠다는 의미다.

g_pfnVectors의 첫번째 부분에 보면 제일 처음에 _estack값을 설정하는걸 볼 수 있다. 그리고 바로 Reset_Handler가 나오고 그 밑으로 HelloWorld 프로그램 작성시 인터럽트 관련 파일(stm32f4xx_it.c)에서 잠시 봤던 NMI_Handler, HardFault_Handler등이 나온다.

이 C소스에 정의된 핸들러들은 사실 stm32f4xx_it.c에서 유저가 재정의해서 쓰라고 만든 함수들이고,

startup_stm32f429zitx.s 파일의 g_pfnVectors: 아래쪽으로 쭉 내려보면

/*******************************************************************************
*
* Provide weak aliases for each Exception handler to the Default_Handler. 
* As they are weak aliases, any function with the same name will override 
* this definition.
* 
*******************************************************************************/
   .weak      NMI_Handler
   .thumb_set NMI_Handler,Default_Handler
  
   .weak      HardFault_Handler
   .thumb_set HardFault_Handler,Default_Handler
  
   .weak      MemManage_Handler
   .thumb_set MemManage_Handler,Default_Handler
  
   .weak      BusFault_Handler
   .thumb_set BusFault_Handler,Default_Handler

   .weak      UsageFault_Handler
   .thumb_set UsageFault_Handler,Default_Handler

   .weak      SVC_Handler
   .thumb_set SVC_Handler,Default_Handler

이와 같이 각 핸들러를 weak형태로 정의하고 있다.

 

그래서, 첫번째로 _estack값을 0x0000 0000에 설정하고 그 다음 Reset_Handler의 주소를 0x0000 0004에 넣는다. 그뒤로 각 Interrupt Handler 주소값을 넣는다.

 

이 상태에서 Entry Pointer가 Reset_Handler이므로 Reset_Handler가 최초 실행된다.

다시 파일의 60라인부터 정의된 Reset_Handler를 보자.

Reset_Handler: 
  ldr   sp, =_estack       /* set stack pointer */

/* Call the clock system initialization function.*/
  bl  SystemInit   
 
/* Copy the data segment initializers from flash to SRAM */  
  ldr r0, =_sdata
  ldr r1, =_edata
  ldr r2, =_sidata
  movs r3, #0
  b LoopCopyDataInit

CopyDataInit:
  ldr r4, [r2, r3]
  str r4, [r0, r3]
  adds r3, r3, #4

LoopCopyDataInit:
  adds r4, r0, r3
  cmp r4, r1
  bcc CopyDataInit
  
/* Zero fill the bss segment. */
  ldr r2, =_sbss
  ldr r4, =_ebss
  movs r3, #0
  b LoopFillZerobss

FillZerobss:
  str  r3, [r2]
  adds r2, r2, #4

LoopFillZerobss:
  cmp r2, r4
  bcc FillZerobss
  
/* Call static constructors */
    bl __libc_init_array
/* Call the application's entry point.*/
  bl  main
  bx  lr    
.size  Reset_Handler, .-Reset_Handler

먼저 SP 레지스터를 0x0000 0000에 있는 _estack값으로 설정한다.

그 아래에는 SystemInit이라는 함수를 호출하고 있다.

 

다시 bl SystemInit 명령어 부분에 브레이크 포인트를 걸고 디버그 모드로 실행한 뒤 몇가지를 확인해보자.

 

Register창을 보면 SP에 아까 계산된 0x2003 0000 값이 들어가 있는걸 볼 수 있고 현재 PC레지스터의 값은 0x0800 0E1C인걸 확인할 수 있다. 이 주소가 Reset_Handler가 정의되어 있는 주소값이다.

 

현재 SystemInit함수 호출직전에 브레이크가 걸려있다. 위에서 SP에 값을 할당했으므로 이제부터는 함수 호출(정확히는 push/pop Instruction실행)을 할 수 있는 것이다.

현재 위치에서 F5를 눌러 Step Into를 실행해 SystemInit 함수내부로 들어가보자.

system_stm32f4xx.c 파일이 열리면서 그 파일에 정의된 SystemInit()함수내부로 진입한다.

첫부분에 CPACR(Coprocessor Access Control Register)라는 FPU관련 레지스터 설정을 한다. 그 아래는 External Memory와 사용자 정의 인터럽트 벡터 테이블을 위한 구문이 있지만 현재 설정으로는 이 항목을 사용하지는 않으므로 넘어가자.

 

다시 Reset Handler로 돌아와 그 밑으로 Reset을 위한 각종 작업을 수행하고 마지막에 

bl main

명령으로 우리가 작성하는 main함수로 분기하도록 코딩이 되어 있는걸 볼 수 있다.

 

 

지금까지 MCU에 전원이 인가되고 main함수를 호출하기 까지의 과정을 살펴봤다.

끝내기 전에 몇가지만 더 살펴보자.

 

우리는 Flash Memory로 부팅하는 모드를 사용했으므로 MCU가 0x0800 0000번지를 0x0000 0000에 Aliasing해서 사용한다고 설명했다.

위에서 확인한 바와 같이 0x0800 0000과 0x0000 0000의 내용이 동일한지 다시 한번 확인하자.

[0x0000 0000번지]

또 첫번째(0x0000 0000)에는 _estack으로 계산한 0x2003 0000값, 즉 SP의 초기값이 들어있는것도 볼 수 있으며 57번째 word값이 0x08000B83인것도 확인할 수 있다.

이 값은 startup_stm32f429zitx.s의 g_pfnVectors에 정의된 각 인터럽트 핸들러의 주소중 EXTI15_10_IRQHandler의 주소이다.

g_pfnVectors에 정의된 각 word형 핸들러 중 57번째를 보면 EXTI15_10_IRQHandler인것을 알 수 있다.

그렇다면 HelloWorld 프로그램을 실행시킨 뒤 main.c에 정의해놓은 HAL_GPIO_EXTI_Callback()함수의 첫번째 행인 char buf선언에 브레이크 포인트를 걸고 User Button을 눌러서 이 부분에서 브레이크를 걸어보자.

다음과 같이 Debug창의 Call Stack에서 최초 호출이 EXTI_15_10_IRQHandler()이고 이 함수의 주소가 0x0800 0B83이라는걸 알 수 있다.

다른 방법으로는 Disassembly창의 주소 입력란에서 0x0800 0B83을 입력하고 엔터키를 눌러보자. 바로 EXTI15_10_IRQHandler로 이동할 수 있다.

 

 

다음으로 메모리의 0x0000 0008 부분은 Reset Handler다음 Handler인 NMI Handler의 주소가 있다.

0x0800 0B25로 기록되어 있는데 이 주소를 Disassembly창의 주소란에 입력해보자.

NMI_Handler의 위치로 이동하는데 이 내용은 stm32f4xx_it.c 파일의 NMI_Handler()의 내용이다.

 

 

6. 마무리

이로써 STM32의 Flash Memory 부팅 시퀀스에 대해 개략적으로 살펴보았다. MCU가 동작하는 방식이 조금더 명확해 졌을 것이다.

또한 앞 시간에 배운 디버거의 활용법도 공부가 되었을 것이다. 

설명한 이외에도 각자 디버거를 사용해 내부 동작을 확인해 보기 바라면서 이번 장을 마친다.

 

'임베디드 > STM32' 카테고리의 다른 글

6. [STM32] GPIO 기초  (0) 2024.07.25
5. [STM32] NUCLEO F429/439 보드 Pinmap  (0) 2024.07.22
3. [STM32] 디버깅  (0) 2024.07.12
2. [STM32] Hello World!  (0) 2024.07.09
1. [STM32] STM32 MCU  (0) 2024.07.08