티스토리 뷰
이번 글에서는 최초 보드에 전원이 on 되는 시점에 어디에서 어떻게 프로그램이 동작되는지 알아보고, 실제 프로그램을 만들어서 보드에서 동작 시켜 보도록 하겠다.
일반적으로 ARM에서는 memory map을 규정하지 않고, 실제 칩을 만드는 반도체 회사에서 칩의 구조에 맞게 memory map을 설정한다.
하지만, Cortex-M에 대해서만은 ARM에서 memory map을 한가지 포맷으로 규정하였다.
ARMv7-M Architecture Reference Manual과 ARM Cortex-M3 Processor Technical Reference Manual을 읽어 보면 ARM에서 정의한 자세한 memory map을 알 수 있다.
위의 그림에 보이는 것이 Cortex-M3의 기본 memory map이다. 위의 그림에서 중점적으로 봐야 할것은 Code, SRAM, Peripheral 영역이 어디에서부터 시작되는지와 0xe0000000에서 시작되는 주소에는 어떤 것들이 들어있는지이다.
그럼, 실제로 STM32F103의 memory map은 어떻게 생겼는지 확인해 보도록 하겠다.
STM32F103 datasheet 문서에 나와있는 memory map은 다음과 같다.
위의 그림에서 0번에 해당되는 영역이 Code 영역이다. 즉, 다시말해서 ROM(Flash memory) 영역인 것이다. 좀더 자세히 보면 다음과 같다.
위의 그림에서 알아두어야 할 것은 0x08000000에서 시작되는 Flash memory영역과 0x1FFFF000에서 시작되는 System memory 영역이다. Flash 영역은 프로그램을 빌드한 실행 파일이 저장되는 영역이라 할 수 있고, System memory 영역은 bootloader 프로그램이 저장되어 있는 영역이다. 위의 그림에도 표현되어 있는것처럼 0x00000000 번지의 영역은 BOOT 핀의 설정에 따라 Flash memory나 system memory가 보여지도록 되어 있다. BOOT0 핀이 0이면 flash memory 영역이 0번지에 나타나게 되고, BOOT0 핀이 1로 설정이 되면 System memory 영역이 0번지에 보여지게 되는것이다.
그럼 왜 0번지의 영역에는 두 영역이 BOOT0 핀에 따라 다르게 보여지도록 하였을까? 그 이유는 칩이 reset 상태에서 처음 읽어오는 명령어가 0x00000004에 있기 때문이다 기록되어 있는 주소에서 가져오기 때문이다. 이렇게 reset 상태에서 첫번째로 실행되는 명령어가 있는 위치는 미리 하드웨어적으로 정해져 있는데 이를 reset vector라고 한다.
0x20000000번지에서 시작되는 영역은 RAM영역에 해당되며 read/write가 가능하므로 주로 data, bss, heap, stack 용도로 사용된다.
0x40000000에서부터는 위의 그림에도 나오는 것처럼 주변 장치들(peripheral)을 제어하기 위한 레지스터들이 있다. 임베디드 시스템 프로그램은 주로 이러한 주변장치들을 잘 제어하도록 만든 프로그램이라고 보면 된다.
이번 글에서는 이정도만 알고 첫번째 프로그램을 만들어 보기로 하겠다.
먼저 할 일은 위에서 설명한 memory map에 따른 linker script 파일을 작성하는 것이다.
Linker Script - 링커 스크립트 글에서 이미 관련 내용을 자세히 설명을 하였으므로 여기에서는 몇가지 내용만 추가 설명하도록 하겠다.
6번줄의 rom은 flash memory 영역의 시작 주소와 크기를 표현하였다.
7번줄은 SRAM 영역에 해당되는 주소와 크기를 표현하였다.
14,15번 줄은 reset vector를 ROM 영역의 제일 앞에 놓여지기 위해서 사용하였다.
47번줄의 end는 나중에 heap 영역 설정을 위하여 사용될 예정이다.
48번줄의 _ram_end는 stack의 시작 주소를 알려주기 위하여 사용된다.
Linker script 파일 작성이 완료되면, 다음으로는 Makefile을 작성하도록 한다.
1~10번줄은 프로젝트를 빌드하는 사용되는 컴파일러를 지정해 주기 위해 사용된다.
15번줄은 프로젝트의 최종 결과물인 실행파일 이름을 알려주는 것이다. 이 프로젝트에서는 "startup"이라는 실행파일을 만들어 볼 것이다.
19,20번 줄은 프로젝트를 빌드하는데 필요한 소스코드와 오브젝트 파일명을 자동으로 생성해 주도록 한다. 즉, *.c로 되어 있는 모든 C파일을 컴파일하게 되므로 프로젝트에 들어가지 말아야 할 소스코드가 있다면 확장자를 *.c 가 아닌 다른 이름으로 변경하여야 한다.
23번 줄은 컴파일 할때 사용되는 공통 옵션을 지정해 준다.
24번 줄은 architecture 의존적은 컴파일 옵션을 지정해 준다. Target CPU가 Cortex-M3 기반이고, 따라서 thumb2 명령어로 빌드 되도록 지정해 주는 것이다.
30~33이 실행 파일을 만들기 위한 절차이다. 여기에서는 startup이라는 이름의 실행 파일을 만듦과 동시에 binary 파일과 dis-assemble된 코드도 같이 만들어 준다.
35~40번 줄은 C파일이나 H파일이 수정되었을 때 이를 참조하는 모든 파일이 다시 컴파일 되도록 dependency 파일을 자동으로 만들어서 include 되도록 하는 코드이다.
다음으로는 reset vector code를 만들어 보도록 하겠다.
4~8번 줄은 vector table을 만들기 위하여 배열을 사용하였다. 이 배열은 ROM 영역의 시작 부분에 놓여야 되므로 .vectors.table이라는 섹션을 지정해 주고, linker가 linker script 파일을 참조하여 가장 앞 부분에 놓여지도록 해준다.
위의 그림이 Cortex-M3의 vector table을 표현한 것이다. 그림에서도 알수 있듯이 vector table에는 ARM 자체적으로 지정한 16개의 vector와 더불어 칩제조사에서 정의한 68개의 vector가 더 있는것을 볼 수 있다. 이 table은 0x00000000 번지에 놓여지게 되나 VTOR 레지스터를 이용하면 다른 영역으로 옮길수가 있다.
그림을 잘 보면 가장 아래쪽에 Initial SP value라고 되어 있는데, 즉 Stack pointer 레지스터에 들어갈 메모리 주소를 제공하여야 한다. 프로그램에서는 _ram_end 심볼이 가리키는 주소를 사용하도록 하였다. 그림의 아래에서 두번째로 놓이는 벡터가 Reset vector의 주소가 된다. 이 프로그램에서는 Reset_handler라는 함수의 주소를 제공하도록 하였다. 칩에 전원이 들어가게되면 Reset_handler 함수부터 실행되어야 한다.
SP 주소와 reset vector 이외의 나머지 vector들은 당장 필요한 내용이 아니기 때문에 굳이 지금 정의해줄 필요는 없다. 필요한 시기에 따로 지정해 주면 되기때문에 여기에서는 SP와 Reset vector만 사용하는것으로 하였다.
10~15까지가 Reset_handler() 코드이다. Reset_handler가 하는 일이라고는 main()함수를 바로 점프하도록 만들어 두었다.
main() 함수는 다음과 같이 만든다.
이 프로젝트에서는 main() 함수는 아무일도 하지 않고 무한 루프를 수행하도록 되어 있다.
여기까지 코드를 만든다음 프로젝트를 빌드해 보도록 하겠다.
make를 수행한 결과 에러없이 startup 파일을 만들어 준것을 확인할 수 있다. 부수적으로 bin 파일과 dis, map 파일도 같이 만들어 진것을 확인할 수 있다.
startup.dis 파일을 열어보면 다음과 같은 내용을 볼 수 있다.
2번줄에 보이는 0x08000000에 SRAM 영역의 마지막 주소인 0x20004fff가 쓰여진것을 볼 수 있다. 이 값이 SP 레지스터에 들어가게 된다. SP 레지스터는 항상 4바이트 align되도록 되어 있기 때문에 실제로는 0x20004ffc가 쓰여지게 될 것이다.
3번줄은 Reset_handler 함수의 주소값이 들어가게 된다. 그런데 실제 Reset_handler의 주소는 0x08000008이지만 vector table에는 0x08000009라고 되어있다. 그이유는 함수주소의 마지막 비트값이 1이 되어 있으면 thumb 모드로 전환되도록 하는 의미를 가지고 있다. Cortex-M은 thumb2 명령어 체계를 가지고 있으므로 이러한 현상이 발생하게 되는것이다.
6,7번 줄은 Reset_handler에서 main()함수를 부를때 argument로 0,0을 넣어주는 동작이다. 큰 의미는 없다.
8번줄은 main() 함수로 점프하는 명령어이다.
11번줄은 main() 함수 내에서 무한루프를 수행하는 코드이다.
위에 보이는 것이 startup 프로젝트에서 만들어진 최종 결과물이다. 자세히 보면 모두 20바이트로 만들어진 코드인것을 알 수 있다. bin 파일의 크기를 보면 20바이트인것을 확인할 수 있다. 이렇게 만들어진 bin 파일을 ST-Link/V2를 이용해서 보드에 내려 보도록 하겠다.
BOOT0핀이 0으로 되어 있기 때문에 0x00000000 번지에서도 위와 똑같은 데이터를 읽어 오게 된다.
이제 보드를 실행 시킨 후 실제 CPU 내부 레지스터 값이 어떻게 읽히는지 확인해 보도록 하겠다.
메뉴에서 Target > MCU core를 선택하면 CPU 내부 레지스터를 볼 수 있다.
빨간 박스에 있는 버튼중에서 System Reset, Run, Halt 순서대로 실행하면 위의 결과를 볼 수 있다.
MSP 레지스터의 값이 0x20004ffc로 되어 있는것을 볼 수 있고, PC 레지스터가 main() 함수의 while(1) 루프를 수행하는 명령어가 있는 주소인 0x08000010으로 되어 있는것을 볼 수 있다.
이로써 최소한의 개발환경 설정이 제대로 구성되어 main() 함수까지 잘 실행되고 있음을 확인할 수 있게 되었다.
==============
펌린이님의 질문에 대한 댓글
==============
보드에 전원이 들어가는 순간을 생각해 봅시다.
칩 내부에서는 전원이 여러 블럭들에 차례대로 공급이 되어지면서 어느 순간 CPU 코어에 클럭이 공급됩니다.
일반적으로 코어에서는 최소 몇클럭 이상이 들어올때까지 카운트하다가 어느 시점에 이르면 Power on Reset 동작을 수행하게 됩니다.
이미 본문에도 어느정도 설명하였지만 다시 한번 더 설명해 보겠습니다.
위의 그림은 ARMv7-M Architecture Reference Manual에 나오는 reset 동작중 일부분을 캡쳐한 것입니다.
중요한 몇가지 내용에 대해서 순서대로 설명해 보겠습니다.
vectortable의 위치는 VTOR레지스터 31:7번째까지의 값에 나머지 0으로 이루어진 곳을 가리키게 됩니다. VTOR 레지스터의 reset value는 0이므로 자연히 vectortable 주소는 0이 됩니다.
SP_main(MSP) 레지스터는 vectortable의 위치에서 4바이트 값을 읽은 다음에 0xfffffffc로 AND한 결과값, 즉, 마지막 2비트값이 0인(4바이트 align)된 주소를 넣어줍니다.
SP_process(PSP)값은 의미 없는 랜덤한 값이 들어갑니다. 이 레지스터는 나중에 프로그램 안에서 설정해 주어야 합니다.
LR 레지스터값은 0xffffffff로 초기화 됩니다.
그 다음으로는 vectortable+4번지에서 4바이트를 읽어서 tmp에 저장합니다. 실제로는 tmp라는 변수는 없을거 같네요. 어쨋든, 읽어온 값의 마지막 bit값을 tbit라는곳에 저장해 둡니다. 이 tbit값을 EPSR의 T값에 넣어줍니다. 다시말해서 이 값이 1이면 Thumb모드로 동작하게 되는것이지요. 이것이 무엇을 의미하는가 하면 읽어 와야할 명령어의 길이가 16비트라는것을 알려주는 것입니다. 프로그램을 컴파일 할 때 thumb 옵션을 주었기 때문이지요.
마지막 줄에 보면 tmp(vectortable의 4번지에 쓰여져 있던 값)를 0xfffffffe와 AND 합니다. 마지막 1비트값은 사용하지 않겠다는 것입니다. 마지막 비트값은 Thumb 모드 여부를 알려주기 위하여 사용되는 것이지 명령어의 위치와는 무관한 값이기 때문입니다. AND 한 결과값으로 branch 하게 됩니다. 즉, vectortable의 4번지에 쓰여진 주소에서 첫번째로 실행될 명령어를 읽어와서 실행하게 되는것입니다. 이번 프로젝트에서는 Reset_Handler 함수의 주소가 들어가 있게 만들어 두었으므로 첫번째로 실행되는 함수가 Reset_Handler가 됩니다.
BOOT0의 값이 0으로 설정되면 Flash memory(0x08000000)에 들어 있는 값들이 0번지에 보여지게 되므로 자연히 0x08000000 번지에 있는 값이 MSP에 들어가게 되고, 0x08000004번지에 쓰여진 주소에서 첫번째로 수행될 명령어를 가져오게 됩니다.
이 글에서는 0x08000004번지에 0x08000009가 쓰여져 있습니다. 질문에서는Reset_Handler의 주소가 0x08000004라고 하셨는데 본문을 자세히 보면 0x08000008로 되어 있음을 알수 있을겁니다.
질문에서는 Reset_Handler가 실행되도록 해주는 코드가 있는게 아닌가 하셨는데, 그런 코드는 없고 vectortable + 4번지에 Reset_Handler 함수의 주소를 넣어 놓으면 자동으로 Reset_Handler 함수가 첫번째로 수행됩니다.
SP 에 대한 질문도 하셨는데, 일반적으로 CPU안에는 범용 레지스터, ARM의 경우 r0 ~r12가 있습니다. 이 레지스터들은 필요에 따라 수시로 임시 메모리에 저장해 두었다가 다시 읽어 오는 동작을 수행하는데, 이때 사용하는 임시 메모리 영역이 스택영역입니다. push 명령어로 레지스터에 들어 있는 값을 스택에 저장하고 pop 명령어로 스택에 저장된 값을 레지스터로 읽어 오게 됩니다.
예를 들어, push r1 한 다음에 pop r2를 수행하게 되면 r1의 값을 스택을 통해서 r2로 복사하는 동작을 수행하게 됩니다. 이 동작을 좀더 자세히 살펴보면 다음과 같습니다.
현재 MSP에 0x100이 있다고 가정해 봅시다. 그리고 r1에는 0x1234가 들어 있다고 가정합니다.
push r1을 하면 먼저 MSP의 값을 4 감소 시킵니다. 그럼 0xFC가 되겠죠. 이 주소에 r1에 들어 있던 0x1234를 넣어줍니다.
다음에, pop r2를 하게되면 현재 MSP가 가리키고 있는 주소에서 값을 읽어 와서 r2에 복사합니다. 즉, 이제 r2는 0x1234가 되겠죠. 그런 다음 MSP 값을 4증가 시킵니다. 당연히 이제는 0x100 이 됩니다.
결론적으로 스택은 높은 주소에서 낮은 주소쪽으로 자라나게 됩니다. 그러므로 최초 MSP에는 SRAM의 마지막 주소를 써주도록 하고 있습니다.
이 프로젝트에서는 마지막 주소 - 1 해 주었는데 이렇게 한 이유는 4바이트의 여유 공간을 마련해두는 역할을 수행하는것과 같습니다.
어느정도 질문에 대한 답변이 되었는지 모르겠습니다. 글솜씨가 부족해서 제대로 설명이 안될것 같기도 한데, 본문 내용을 꼼꼼히 여러차례 읽어보시면 어떤 내용을 설명하려고 하는지 이해하실수 있을것 같습니다. 부족한 글에 관심 가져 주셔서 고맙습니다. 또다른 궁금한 내용있으시면 다시 댓글로 질문 주시기 바랍니다.
==============
펌린이님의 질문에 대한 추가 댓글
==============
1. Reset 동작과 startup code와는 별개의 것입니다. Reset 동작은 reset 상황이 되면 CPU 내부에서 자동적으로 수행됩니다. 하드웨어적인 문제이지 소프트웨어 하고는 전혀 무관한 동작입니다. 따라서 reset 동작이 startup 코드보다 먼저 동작될 수 도 있고, 나중에 동작될수도 있습니다. 상황에 따라 달라집니다. Reset 상황이 되는 이유는 여러가지가 있는데, 대표적으로 Power on reset, H/W reset, S/W reset, Watchdog reset, Brown-out detect reset 등이 있습니다. 이러한 조건이 되면 무조건 동작합니다.
Reset 동작에 들어가게 되면 무조건 0번지에 들어 있는 값을 MSP레지스터에 복사합니다. 그리고 그다음에 4번지에 들어 있는 값을 PC 레지스터에 복사합니다. 이때 4번지에는 어떤 함수의 주소가 들어가도 관계없습니다. 굳이 Reset_Handler가 꼭 들어가야할 이유가 전혀 없습니다. 4번지에 main() 함수의 주소를 넣어 놓게 되면 main 함수부터 프로그램이 시작됩니다.
2. Cortex-A CPU와 Cortex-M CPU는 동작 방법이 완전히 다릅니다. Cortex-A CPU에서 reset이 되면 PC값이 자동으로 0이 됩니다. 그 얘기는 0번지에 instruction이 들어가 있어야 합니다. 그래야 정상적으로 프로그램이 실행이 되니까요. 대부분 0번지에는 초기화 절차를 수행하는 함수로 점프하는 branch instruction이 들어갑니다. 하지만 Cortex-M의 경우에는 이와 다르게 바로 앞에서 설명한것과 같이 4번지에 들어있는 값을 PC값으로 설정합니다. 즉, Cortex-A는 PC값이 하드웨어적으로 고정되지만, Cortex-M은 소프트웨어적으로 첫번째 수행되는 명령어가 있는 PC값을 설정해주도록 하는것입니다.
3. 어셈블러의 레이블이나 C에서의 함수나 컴파일러 입장에서는 모두 동일하게 처리됩니다. 이 프로젝트에서는 본문에 잘 나와있듯이 vectors.code 섹션을 vectors.table 섹션바로 다음에 놓이도록 해놓았기 때문에 주소가 연달아 할당된것 뿐입니다. 의도적으로 그렇게 한것이지 특별한 의미는 없습니다.
4. main 함수의 위치는 컴파일 후 링크 단계에서 결정됩니다. 이번 프로젝트에서 사용된 함수가 Reset_Handler와 main 밖에 없기 때문에 main이 Reset_Handler 바로 뒤에 위치하게 되는겁니다. 뒤에 나오는 다른 프로젝트에서는 main이 전혀 다른데 위치하게 됩니다. 특히 인터럽트를 사용하게 되면 말씀하신것처럼 vector table이 크게 잡히게 되고 그 뒤로 임의의 함수들이 위치하게 됩니다.
'ARM Cortex-M' 카테고리의 다른 글
GPIO를 통한 LED 제어 (0) | 2017.02.12 |
---|---|
Start up 2 (6) | 2017.02.12 |
디버거/프로그래머 연결 (0) | 2017.02.11 |
프로그램 개발 환경 설정 (0) | 2017.02.11 |
하드웨어 준비 (6) | 2017.02.11 |