<목차>
1. Micro-C/OS-II 포팅을 위한 준비 자료들.
1.1 ARM Compiler
1.2 System Board
1.3 Kernel Source
1.4 MicroC/OS-II Book
1.5 ARM DataSheet and Ep7209 DataSheet
2. 포팅을 위해서 알아야 할 ARM의 구조.
2.1 Register
2.2 ALU
2.3 Booth’s Multipler
2.4 Exceptions
2.5 Processor modes
2.6 ARM Instruction set
3. The Real-Time Kernel 용어.
3.1 Critical Section of Code
3.2 Resource
3.3 Shared Resouce
3.4 Multitasking
3.5 Task
3.6 Context Switching
3.7 Kernel
3.8 Scheduler
3.9 Tick
4. Porting MicroC/OS-II
4.1 INCLUDES.H
4.2 OS_CPU.H
4.3 OS_CPU_C.C
4.4 OS_CPU_A.S
5. VxWork VS MicroC/OS-II
6. 참고 자료 및 문헌
1. MicroC/OS-II 포팅을 위한 준비 자료들.
어떤 시스템의 환경을 만든다는 것은 그리 쉬운 일은 아니다. 해당 cpu에 맞는 시스템을 만들어야 할 것이고, 이 시스템을 동작 시키기 위해서는 컴파일러 또한 필요하다. 컴파일러를 사용하기 위해서는 해당 cpu 매뉴얼을 참조해야 될 것이다. 여러가지 외부 디바이스를 붙인다면 그에 따른 여러가지 자료 또한 필요하다. MicroC/OS-II포팅에 관련된 것 뿐만 아니라 일반적으로 ARM을 사용하기 위한 것들은 다음과 같다.
1.1 ARM Compiler
가장 중요하게 갖추어야 할 것이다. 현재까지 나온 최신의 ARM 통합 개발툴은 ADS 1.1버전이다. 정품 가격은 일반 학생이 구매 하기 어려운 가격이라 평가판을 이용한다. 평가판은 컴파일 용량 제한이 없는 게 특징이지만, 날짜 제한이 있고 프로그램 라이센스가 없다. 학생이 이용하기에는 적당한 것 같다. 평가판을 구하는 방법은 www.arm.com에서 평가판 신청하면 2주이내로 국제 우편으로 받을 수 있다.
지금 연구자가 가지고 있는 버전은 ARM SDT 2.5 , ARM Developer Suite 1.01 , ARM Developer Suite 1.1 버전이다. 실제 포팅에 사용한 버전은 ARM Developer Suite 1.01이다. 사용법은 매뉴얼을 참조하면 된다.
1.2 System Board
연구자가 ARM cpu를 구하지 못하는 관계로 일반 회사에서 파는 보드를 사서 사용했습니다. 참고지만 일반 학생이 ARM을 구경하기란 그리 쉽지 않습니다. 공부하고 싶어도 시스템을 고가에 구입해야 하기 때문에 부담이 되는 것이 사실이다. 전 오로지 지식욕으로 인한 충동 구매를 했지만요. 연구자가 구매한 보드는 Cirrus Logic 의 Ep7209를 사용한 보드였다.
1.3 Kernel Source
MicroC/OS-II 커널 소스가 있어야 한다. 해당 소스는 Labrosse의 책에 포함되어 있는 소스로, 책 저자의 재산이므로 책과 함께 구해야 한다. 인터넷 교보문고에서 교재명목으로 구매 할 수 있다. 이책에는 버전 2.0 kernel 및 2.04 kernel이 있다. 또한 8086을 위한 포팅 코드 및 예제들이 포함되어 있다. 이 외에도 www.ucos-ii.com에 가면 다른 cpu에 대한 포팅 소스들을 다운 받을 수 있다. 연구자가 실제 이용한 방법은 제 시스템 보드에 김효준 님 포팅한 소스를 다운 받아 ARM컴파일러에 맞게 고쳐준 방법이다. 실제 처음부터 연구자가 한 것은 아니다. 물론 처음 소스부터 할 수도 있었지만 학과 공부와 함께 병행하면서 해야 했기에 시간 절약을 위해서 이다.
1.4 MicroC/OS-II Book
해당 책에서는 커널의 구조 및 소스들이 공개 되어 있다. 커널을 공부하기에는 적당한 책이라고 생각된다. 리눅스 역시 소스가 공개 되긴 했지만 커널 구조가 매우 복잡하고 제대로 공부할려면 6~12개월 정도 공부해야 제대로 알 수 있다고 한다. 그리고 포팅의 방법 및 8086의 포팅에 관련된 자료 역시 제공 하고 있으므로 일반 pc에서 돌려볼수 있다는 장점이 있다.
1.5 ARM DataSheet and Ep7209 DataSheet
MicroC/OS의 포팅에 있었어 중요하게 생각 되는 것이 해당 CPU에 대한 지식이다. 즉 ARM계열의 cpu에 포팅 하기 위해서는 ARM관련 시스템 정보들을 정확하게 알고 있었야 한다. 일반 상용 OS와 다르게 MicroC/OS-II의 중요 핵심 코드는 어셈블러든 C든 직접 코딩 해야 하기 때문이다. 또한 해당 CPU의 정보도 알고 있어야 할 것이다.. ARM은 CPU Core일뿐이지 직접적으로 CPU라고 말하기는 곤란하다고 생각되어진다. 같은 ARM계열은 cpu라 할지라도 각 인터럽트 소스 및 외부 인터페이스가 틀린다. 제조 회사들은 단지 라이센스를 지불하고 칩을 용도에 맞게 제작하는 것 뿐이다.
ARM DataSheet 는 평가판을 구하면 pdf파일로 있다. 또한 각 컴파일러 사용법도 있으므로 읽어보시기 바랍니다. 현재 국내에서 ARM 관련 서적은 없으므로 www.arm.com에 보면 관련 서적이 있는데, 교보 문고에서 해외 구매하면 구하실 수 있다. 연구자가 구매한 책은 ARM system-on-chip architecture이다. ARM관련 전반적인 내용을 다루고 있으므로 DataSheet에서 얻을 수 없는 자료들도 얻을 수 있다.
2. 포팅을 위해서 알아야 할 ARM의 구조.
실제 포팅을 위해서 알아야 할 내용들이 있다. ARM계열의 cpu를 이용해서 하므로 기본적으로 ARM에 관련된 것들을 알아야 하겠다. 어셈블러로 코딩하기 하기 위해서는 제법 알아야 할 것들이 많이 있지만 기본적인 것만 설명하겠다.
2.1 Register
ARM7에는 31개은 Register가 있다. 우리가 알고 있는 Register 목록은 r0-r15,cpsr의 17개 이다. 나머지는 각 시스템모드 전용으로 사용되는데, 각 시스템 모드의 레지스터들은 리맵팅되어 사용되어 진다. 생각해 보면 아주 커다란 장점이 있다. Exception모드의 하나인 fig모드의 레지스터 7개가 리매핑되는데, 스택에 저장할 필요없이 바로 사용가능 하므로 시간상의 절약을 꿰할수 있다. 각 시스템 모드의 레지스터 개수를 보면 31개이다.
2.2 ALU
32bit 연산이 가능한 ALU가 제공된다. 그러나 다른 cpu와 틀리게 한쪽 입력은 Barrel Shifter와 연결되어 있다. ALU의 인수 하나는 레지스터에서 바로 들어오고 다른 하나는 레지스터나 버스에서 Barrel Shifter라는 것을 거쳐 입력되도록 되어 있다. 이런 이유로 ARM에서 제 2 오퍼런드를 지정할 때, 해당 값을 Shifter시켜서 사용할수 있다. ARM 어셈블러 명령어를 보면 직접적인 Shifter명령은 없다.
…Barrel Shifter에 관한 내용은 Micro Processor 책을 보면 알 수 있다.
2.3 Booth’s Multipler
곱샘 기능을 제공하는 32비트 Booth’s 곱샘기가 있다. 곱셈기는 32비트 연산을 지원하며, 32비트의 두 입력을 받아서 곱하여, 결과가 32비트를 넘더라도 넘는 부분은 버리고 32비트만을 남기게 된다.
…해당 Booth’s 곱샘기 내용은 컴퓨터 구조라는 책의 내용을 보면 알 수 있다.
2.4 Exceptions
우선 Exception이 무슨 의미인가를 정리해 보고자 한다. 인터럽트와 유사한 개념이지만 보다 큰 의미를 지니고 있다. 정확한 정의에 대해서는 연구자도 잘 모르겠습니다. ARM에 관련해서 설명하겠다. ARM에는 FIQ(Fast Interrupt reQuest), IRQ(Interrupt ReQuest), Abort, SoftWare Interrupt, Undefined Instruction Trap의 5가지 Excetpion이 있고, 각각 Exception이 발생하면 Cpu는 대응하는 동작모드로 전환 된다. 여기에 User모드가 추가되어 총 동작 모드는 6개가 있다. MicroC/OS에서 사용하는 동작모드는 각기 틀릴수 있지만, 포팅에서 가장 중요한 모드는 IRQ이다. 왜냐하면 타이머 인터럽트가 여기 있기 때문이다. 그럼 각 Exception에 대해서 설명하겠다.
-IRQ
일반적으로 I/O 장치로부터의 입력이 들어오면 반응을 하는 Exception이다. Ep7209의 소스로는 내부 타이머나 시리얼, 외부 IRQ 입력 등이 있다. 포팅에 이용되는 모드이다.
- FIQ
개념적으로는 IRQ와 유사하지만, 보다 빠른 처리를 할수 있도록 제공된 Exception이다.
FIQ의 소스로는 대부분 critical한 소스가 대부분이다. 즉 외부 FIQ, Battery low interrupt, Media changed interrupt 등이 있다.
- Abort
CPU가 메모리로부터 인스트럭션을 가져오거나, 혹은 인스트럭션을 동작시키면서 데이터를 가져오려고 할 경우, 해당 메모리를 억세스 할 수 없다면 Abort Exception이 발생한다. 위에 제시한 두 가지 경우에 대응하여 Prefetch Abort 와 Data Abort로 구분 할수 있다.
- Software Interrupt
프로그램에서 임의로 인터럽트를 호출하는 경우이다. ARM 인스트럭션 SWI가 이에 해당한다. 이 Exception이 발생하면 CPU동작모드가 Supervisor모드로 바뀌도록 되어 있고, 보통 OS의 시스템 Call을 구현하기 위해 사용된다. DOS의 경우를 보면, 어셈블러로 코딩할시 인터럽트 11h가 화면에 글자를 출력하도록 하는 인터럽트인데, ARM의 SWI와 유사한 경우라 볼수 있다. SWI에 대한 것을 매뉴얼을 참조 하면 된다.
- Undefined Instruction Trap
ARM에 정의되어 있지 않는 명령어를 메모리로부터 가져 왔을 경우 발생하는 Exception이다. 이 기능은 코프로세서를 사용하는 경우와 관련되어 사용된다고 한다.
연구자도 더 연구해야 할 부분이다.
위에서 언급한 5종류의 Exception에 몇가지를 더하여, 해당 처리를 위해 ARM7은 0번지에 벡터 테이블을 유지한다. 즉, 해당 Exception이 발생하면 정해져 있는 벡터 번지로 실행을 옮긴다. 해당 번지는 다음과 같다.
Address Exception Mode on entry
-----------------------------------------------------
0x0000.0000 Reset Supervisor
0x0000.0004 Undefined Instruction Undefined
0x0000.0008 Software Interrupt Supervisor
0x0000.000C Abort(prefetch) Abort
0x0000.0010 Abort(data) Abort
0x0000.0014 --reserved-- --
0x0000.0018 IRQ IRQ
0x0000.001C FIQ FIQ
한가지 주의할 점은 86계열이나 96계열은 해당 벡터에 점프 어드레스를 넣어두어 인터럽트 발생시 해당 주소를 가져와 PC에 넣어 주는 일이 일어나지만, ARM의 경우는 해당 백터로 점프를 한다. 즉, IRQ가 발생하면 프로그램 카운터의 번지는 0000.0018이 된다. 그러므로 해당 번지에는 점프 명령어 같은 것이 들어 있어야 한다.
2.5 Processor modes
Exception과 관련되어 CPU의 동작모드가 있다. ARM에서는 6가지 동작모드가 있다. User, FIQ, IRQ, SVC, Abort, Undefined Mode가 있다. 각 동작 모드에 따라서 사용하는 레지스터가 틀린 점이 몇가지 있는데, 아래 그림을 참조하면 된다.
-----------------------------------------------------
User FIQ Super Abort IRQ Undefined
-----------------------------------------------------
r0 . . . . .
r1 . . . . .
r2 . . . . .
r3 . . . . .
r4 . . . . .
r5 . . . . .
r6 . . . . .
r7 . . . . .
r8 r8_fiq . . . .
r9 r9_fiq . . . .
r10 r10_fiq . . . .
r11 r11_fiq . . . .
r12 r12_fiq . . . .
r13 r13_fiq r13_svc r13_abt r13_irq r13_und
r14 r14_fiq r14_svc r14_abt r14_irq r14_und
r15(PC) . . . . .
-----------------------------------------------------
또한 CPSR과 각 모드에 SPSR있다. CPSR은 각 모드 공통으로 사용된다. 그러나 SPSR은 각 모드에 각각 따로 존재한다. 즉 일반 User 모드에서 다른 모드로 바뀐다면 현재의 CPSR은 다른 모드의 SPSR에 저장 되고 CPSR은 초기화 된다. 여기서 생각해 보면 ARM은 제작되어질 때부터 스택의 사용을 최소화 시킬려고 한 것 같다.
2.6 ARM Instruction set
ARM의 인스트럭션은 크게 두가지로 나눌수 있다. ARM모드에서 돌아가는 32bit 인스트럭션과 Thumb모드에 돌아가는 16비트 인스트럭션이 있다. 또 하나 생각 해 볼 것은 32bit 머신에 왜 굳이 16비트 인스트럭션을 만들었을까 하는 것이다. 이건 상업적인 예기일지도 모르겠습니다만, 메모리를 외부 인터페이스 했을 때 32bit로 돌릴려면 메모리가 커질 뿐더러 pcb공간은 면적 등 가격이 올라갑니다. 그에 비해 16bit 돌리면 메모리 뿐만 아니라 공간상의 절약으로 인해 가격이 내려갈수 있다. 속도 또한 32bit에는 못 미치지만 왠 만큼 따라 가므로 많은 절약을 할수 있다. 이렇듯 ARM이라는 것은 이용가치가 높게 만들어진 CPU이다. 그러나 thumb 모드 16bit는 ARM모드에서 사용되는 일부기능을 사용 할수 없는 단점이 있다. 인스트럭션의 길이로 인해 레지스터를 모두 사용할수 없을 뿐더러 몇가지 ARM모드 인스트럭션을 몇가지 사용할수 없다. 하나의 예를 보면 thumb모드는 모든 인스트럭션이 State Register를 항상 갱신 하는 반면, ARM모드는 모든 인스트럭션에 옵션으로 붙는 s의 추가 및 각 상태 업데이트 옵션을 붙이므로서 State Register를 바꿉니다. 다시 말하면, ARM모드는 사용자가 원할때만 state Register를 바꾼다는 것이다. 또한 알고리즘 방식인데, Thumb모드에서 사용불가능한 산술,비교 알고리즘을 ARM모드에서는 사용 가능 한다. 그래서 ARM모드는 Thumb모드 보다는 빠르다고 볼수 있다. 각 모드에 대한 인스트럭션에 대한 설명은 ARM Architecture Reference Manual을 참조 하면 자세히 나와 있다. 포팅에서 중요하게 보고 넘어가야 할 사항은 스택에 관한 것이다.
==================================================================
동작 Stack Other
----------------------------------------------------------------
pre increment load LDMED LDMIB
post increment load LDMFD LDMIA
pre decrement load LDMEA LDMDB
post decrement load LDMFA LDMDA
pre increment store STMFA STMIB
post increment store STMEA STMIA
pre decrement store STMFD STMDB
post decrement store STMED STMDA
==================================================================
ARM에서는 특이하면서 아주 강력한 인스트럭션을 제공하는데 그 중 하나가 모든 레지스터들을 단 하나의 명령어로서 저장하거나 복구할수 있다는 것이다. 그 각각에 대한 설명은 역시 매뉴얼을 보면 된다.
3. The Real-Time Kernel 용어.
기본적으로 알아야 될 용어들이 있다. 그 외 Real-Time OS가 구체적으로 무엇인지 대해서는 알아야 필요도 있겠습니다. Real-Time OS의 종류는 많으며 그 방식 또한 여러가지가 있지만, 기본적인 틀은 그리 크게 벗어나지 않는 것으로 생각된다.

Figure. Mutiple tasks
3.1 Critical Section of Code
커널의 핵심 코드라고 생각하면 된다. Real-Time OS에서 타이머 인터럽트는 아주 중요하면서도 매번 항상 주기적으로 발생하는데, 커널의 핵심 코드가 실행되고 있는 과정중에 이 인터럽트가 발생할 수도 있다. 그럼, 중요 작업 중이던 레지스터들이 다른 값을 가질수 있다. 그런 이유로 Critical Section of Code는 인터럽트를 금지 시켜 주어야 그 코드가 제대로 실행 될 것이다.
3.2 Resource
Task에서 사용되는 그 어떤 자원이라고 정의 되어 있다. 그 자원이라 함은 I/O디바이스, 프린터, 키보드, 또는 디스플레이에 사용되는 데이터라고 할 수도 있다..
3.3 Shared Resouce
Shared Resource는 하나 이상의 Task에서 사용할수 있는 Resource라고 정의되어져 있다.
3.4 Multitasking
Multitasking이라 함은 여러가지 Task 사이에서 Cpu를 스위칭하고 스케줄하는 과정으로 정의 되어져 있다. SingleTask와 Multitask와 비교를 해 보겠습니다.SingleTask는 단 하나은 프로그램으로 구성되어져 있다. 가끔식 인터럽트가 걸리면 잠시 한던일을 멈추고 인터럽트 처리를 해준 다음 복귀하는 방식이 대표적이라 할수 있다. 그래서 여러가지 프로그램들을 실행할수 없다. 단 그 실행속도는 빠릅니다. 오로지 지정된 일만 하면 되니까 그렇습니다. MultiTask는 여러가지 프로그램들을 실행시킬수 있다. 시스템 자원 활용면에서는 뛰어나다고 할수 있다. 각 Task마다 스택 공간 및 TCB가 존재한다. 각 Task는 지정된 스택 공간을 가지고 작업을 하게 된다. TCB(Task Control Block)은 각 타스크의 복구 되어야 할 레지스터 내용 및 State등이 있는 구조체 자료형이다.
3.5 Task
Task라 함은 일종의 간단한 프로그램이라고 정의되어져 있다. Process나 Thread와도 비슷한 개념이다. 어째든 동시에 실행 될수 있는 어떤 프로그램 혹은 CPU흐름 정도로 생각해도 좋을 것이다.Real Time OS에서 Task는 다섯가지 동작모드가 있다.

Figure. Task Status
DORMANT---Task가 Program Space(ROM, RAM)상에 있으나 Real-Time OS에서 사용되지 않는 상태. Task가 Delete되면 DORMANT상태가 된다.
READY---처음 TASK가 생성되어지거나, 이벤트가 발생되어진 상태이다. Scheduling된 상태를 말한다.
RUNNING---현재 TASK가 실행되고 있는 상태이다.
WAITING---EVENT,DELAY가 발생된 상태 및 세마포어, 메일박스, 큐등을 기다릴때의 상태이다.
ISR---TASK가 실행되어지고 있는 과정중에서 타이머 인터럽트 발생시 인터럽트 서비스 루틴으로 들어간 상태이다.
아래 그림을 참조하시기 바랍니다.
3.6 Context Switching
Context Switching이라 함은 커널에서 현재 실행되고 있는 Task에서 스케줄러에 의해서 다음 Task를 실행시키기 위해서 현재의 Task의 작업 내용 및 스택 포인터 등을 현재의 Task TCB에 저장하는 과정이라 할수 있다. 약간은 이해 하기 어려울 수도 있지만 곰곰히 생각해 보면 잘 아실수 있다.
아래 그림을 참조하시기 바랍니다.
3.7 Kernel
Task를 관리와 각 Task간의 Communication을 책임지는 Multitasking System의 일부분이라고 정의하고 있다. 커널의 가장 핵심은 Context Switching이다.
3.8 Scheduler
다음에 어떤 Task를 실행시킬건지 결정하도록 책임지는 커널의 일부분이다. 그 자세한 사항은 더 연구해야 한다.
3.9 Tick
Tick은 RTOS에서 말하는 시간 개념으로, 보통 1회의 Timer 인터럽트를 의미한다. 따라서 1/100초로 인터럽트 주기를 맞췄다면, 100 Tick이 지나면 1초가 지난 것이 된다.
4. Porting MicroC/OS-II
이제부터 실제 포팅에 관한 내용을 다루도록 하겠다. MicroC/OS책의 8장을 보면 구체적으로 포팅의 순서가 나와 있다. 책이 있으시다면 한번쯤 읽어 보는 것도 좋을 것이다. 실제 포팅에서 순수 코딩 해야 할 부분은 그리 많지 않을 수도 있다. 하지만 실제 어셈블러로 직접 코딩해야 하기 때문에 어려움이 따를수 있다.
아래그림을 참조 하시기 바랍니다.
Application SoftWare | |
|
MicroC/OS-II (Processor-Independent Code) OS_CORE.C OS_MBOX.C OS_Q.C OS_SEM.C OS_TASK.C OS_TIME.C uCOS_II.C uCOS_II.H |
MicroC/OS-II Configuration (Application-Specific Code) OS_CFG.H INCLUDE.H |
|
MicroC/OS-II Port (Processor-Specific Code) OS_CPU.H OS_CPU_A.ASM OS_CPU_C.C | |
SOFTWARE
HARDWARE
CPU |
TIMER |
4.1 INCLUDES.H
이 파일은 커널 및 여러가지 .C를 INCLUDE하는 마스터 Include file이다. 실제 속을 들여다 보면 커널 및 Cpu 셋팅해주는 파일만 Include해 놓고 있다. 그 경로는 사용환경에 따라 바꾸면 된다.
#include <string.h>
#include <stdio.h>
#include "..\Ep7209\os_cpu.h"
#include "os_cfg.h"
#include "..\source\ucos_ii.h"
4.2 OS_CPU.H
16비트 cpu와 32bit cpu에서 int ,short ,long 형의 bit수가 틀린다. 즉 변수 형을 ARM에 맞게 설정해 주어야 한다. ARM 컴파일러 C관련 매뉴얼을 참조하셔서 수정하면 된다.
typedef unsigned char BOOLEAN;
typedef unsigned char INT8U; /* Unsigned 8 bit quantity */
typedef signed char INT8S;/* Signed 8 bit quantity */
typedef unsigned short INT16U;/*Unsigned 16 bit quantity */
typedef signed short INT16S;/*Signed 16 bit quantity */
typedef unsigned long INT32U;/*Unsigned 32 bit quantity */
typedef signed long INT32S;/*Signed 32 bit quantity */
typedef float FP32; /* Single precision floating point */
typedef double FP64; /* Double precision floating point */
각각의 데이터 타입을 ARM에 맞게 해준 것이다. 한가지 특이한 점은 unsigned long과 unsigned int는 같은 비트수를 가지지만 unsigned int로 바꾸면 컴파일은 되나 실행은 되지 않는 현상이 있었습니다. 매뉴얼도 같은 걸로 취급합니다만, 정확히는 연구자도 잘 모르겠습니다.
다음으로 해 주어야 할 것은 스택이 어떻게 자라나는가를 설정해주는 것이다. 대부분의 cpu가 스택은 아래로 자라나는 형태이다. 여기서도 마찬가지로 스택이 아래로 자라나는 형태를 가지게 셋팅했습니다.
#define OS_STK_GROWTH 1
옵션은 책에 나와 있다. 0은 위로 자라나는 형태이고 1은 아래로 자라나는 형태이다.ARM인스트럭션에서 스택의 어떻게 자라나게 할건지에 대해서 언급했습니다. 중요한 사항이다. 이 값이 잘못되면, IDLE TASK생성시 스택 공간 지정의 문제점이 발생한다.
OS_ENTER_CRITICAL()과 OS_EXIT_CRITICAL()을 선언 해주어야 한다. OS_ENTER_CRITICAL()은 CRITICAL SECTION OF CODE를 실행하기 위해서 인터럽트를 디스에이블 시켜주는 함수이고, OS_EXIT_CRITICAL()은 CRITICAL SECTION OF CODE 실행을 끝내고 인터럽트를 다시 인에이블 시켜주는 함수이다.
인터럽트 금지를 시키는 방법은 여러가지 있다. 대표적인 방법은 인터럽트를 완전 디스에이블 시키고 다시 살리 방법이다. 또한 인터럽트 소스 마스크 비트를 이용한 방법도 있을 수 있다. 96계열에서 어셈 명령어로서 PUSHF,POPF를 통한 방법도 있다.단 ARM에서는 CPSR의 I 비트를 1로 만들어서 금지하는 방법을 사용하였다. 그리고 해당 역할을 하는 함수를 splx() 라고 어셈 모듈에 만들어 포함시켰놓았습니다. 이렇게 한 이유는 CRITICAL SECTION OF CODE를 실행 과정중 다른 인터럽트가 발생하므로서 벌어지는 현상을 막기 위해서이다. 만약 마스크 비트를 통한 방법을 이용시에는 다른 인터럽트 발생시 그 전의 작업 내용인 모든 레지스터들은 저장되어야 할 것이다..
4.3 OS_CPU_C.C
여기 집고 넘어가야 할 사항은 TCB상의 스택모양을 결정해 주어야 한다. 일종의 레지스터 저장 복구하는 약속이라 할수도 있다. 그 모양은 많은 형태들이 있지만 ARM에서의 어셈블러의 간단한 명령어로 처리하기 위해서 아래와 같이 정의한다.
----------------------------------------
Stack (High Memory)
----------------------------------------
15 r15(pc)
14 r14(lr)
13 r12
12 r11
11 r10
10 r9
9 r8
8 r7
7 r6
6 r5
5 r4
4 r3
3 r2
2 r1
1 r0
0 CPSR <- pTCB->OSTCBStkPtr
----------------------------------------
Stack (Low Memory)
----------------------------------------
위에서 보면 r13이 없음을 알수 있다. 이 이유는 각 TASK상의 TCB내에 스택포인터 저장 공간이 따로 있으므로 저장할 필요는 없다. 여기서 보면 ARM에서 스택의 저장과 복구하는 어셈블러 명령어의 특징을 살펴볼 필요가 있다.
Stmfd sp!,{r0,r1,r2,r3,r4,r5,r6,,r7,r8,r9,r10,r11,r12,r14,15}와
Stmfd sp!,{r15,r14,r12,r11,r10,r9,r8,r7,r6,r5,r4,r3,r2,r1,r0}와 차이점은 순서에서 볼수 있지만, 스택에 저장되는 방식은 같은 방식으로 저장된다. 이것은 인스트럭션 코드를 살펴 보아야 합니다만, 그 세세한 내용은 ARM관련 매뉴얼을 보면 알수 있다.
또한 ldmfd 또한 같은 방식으로 어떤한 순서라도 같은 방식으로 꺼내오게 된다. 그러한 이유로 아주 강력한 cpu가 될수 있겠습니다.
잘 보면 r15는 pc로서 이것은 pc에 리턴 값이나 점프할 번지 주소 값을 넣어주면 바로 그 번지나 리턴 번지로 점프 한다는 것이다. 즉 리턴 명령어 없이 레지스터 복구시키면 바로 리턴 된다는 예기이다.
OSTaskStkInit 함수
이름을 풀었어 해석해 보면 Task스택을 초기화하는 함수이다. 타스크를 초기화 한다는 는 의미를 생각해 보겠습니다. 앞서 설명한 것 처럼 각 Task는 해당 TCB가 존재하고 스택공간이 존재한다. 즉 context switching이 일어날 때 TCB를 이용해서 스택포인터를 복구하고 해당 레지스터들을 복구한다. 이러한 것을 볼 때, 처음 TASK를 생성했을 시 스택을 초기화 시키지 않고 그 포인터 역시 저장하지 않는다면, context switching이 발생시 잘못된 스택 포인터와 레지스터값을 가져오므로 오동작을 할수도 있다.
Os_task.c의 파일의 내용을 보면 OSTaskCreate라는 함수가 있다. 이 함수가 바로 Task생성시 사용되는 함수이다. 그럼 OSTaskCrete를 호출할 때 넘겨주는 인수를 보도록 하겠다.
INT8U OSTaskCreate (void (*task)(void *pd), -> 1
void *pdata, -> 2
OS_STK *ptos, -> 3
INT8U prio) -> 4
첫번째 인수는 Task의 시작 번지를 의미한다. 두번째 인수는 Task 시작시에 건내어지게 될 인수를 의미한다. 세번째 인수가 바로 Task가 사용하게 될 스택 공간의 포인터이다.마지막 인수가 Task의 우선 순위이다. 즉, Task생성시 단순히 비어있는 어떤 공간을 넘겨 줌으로서 이 공간을 스택 공간으로 사용하게 하는 것이다. 이 함수 내부에 있는
psp = (void *)OSTaskStkInit(task, pdata, ptos, 0); /* Initialize the task's stack */
보면 *OSTaskStkInit()함수가 바로 이 비어있는 스택공간을 Task스택으로 사용하겠다고 정의하는 것이다. 실제 코딩에 있었어 필요한 것은 스택의 모양이다. 이 모양에 대해서는 앞서 설명했습니다. 아래 그림을 보시기 바랍니다.
----------------------------------------
Stack (High Memory)
----------------------------------------
15 r15(pc)
14 r14(lr)
13 r12
12 r11
11 r10
10 r9
9 r8
8 r7
7 r6
6 r5
5 r4
4 r3
3 r2
2 r1
1 r0
0 CPSR <- pTCB->OSTCBStkPtr
----------------------------------------
Stack (Low Memory)
----------------------------------------
이 그림을 토대로 OSTaskStkInit함수를 코딩하거나 바꾸어주면 된다.
아래는 실제 소스 코딩이다.
01: void * OSTaskStkInit(void (*task)(void *pd),void *pdata,
02: void *ptos,INT16U opt)
03: {
04: INT32U *stk;
05: opt = opt;
06: stk = (INT32U *)ptos;
07: *(--stk) = (INT32U)task; // r15
08: *(--stk) = (INT32U)0; // r14
09: *(--stk) = (INT32U)0; // r12
10: *(--stk) = (INT32U)0; // r11
11: *(--stk) = (INT32U)0; // r10
12: *(--stk) = (INT32U)0; // r9
13: *(--stk) = (INT32U)0; // r8
14: *(--stk) = (INT32U)0; // r7
15: *(--stk) = (INT32U)0; // r6
16: *(--stk) = (INT32U)0; // r5
17: *(--stk) = (INT32U)0; // r4
18: *(--stk) = (INT32U)0; // r3
19: *(--stk) = (INT32U)0; // r2
20: *(--stk) = (INT32U)0; // r1
21: *(--stk) = (INT32U)pdata; // r0
22: *(--stk) = (INT32U)0; // CPSR
23: return ((void *)stk);
24: }
인수를 살펴보면, 첫번째가 Task의 시작 번지이고, 두번째가 Task의 인수, 세번째가 Stack의 위치, 마지막은 현재 사용하지 않는 옵션이다. *(--stk)라고 한 이유는 Full & Decrement Stack을 사용했기 때문이다. ARM 컴파일러는 적은 개수의 인수에 대해서는 레지스터를 사용해 인수를 넘긴다. 즉, 첫째 인수는 r0를 통해 넘기게 된다. 이러한 이유는 c코드의 인수들이 어셈으로 변환시 어떻게 변하는지 알아야 할 필요가 있다. ARM 해당 매뉴얼을 참조 하시기 바랍니다. 자세히 나와 있다. 또한 c로 코딩한 다음 디버깅시 arm 어셈 변환하는데 이걸 통해서 보아도 괜찮습니다. 약간의 머리 회전이 필요한 부분이다. 어셈블러도 컴파일시 컴파일러가 다시 해석해서 코딩한 다음 컴파일 한다. 이러한 이유는 ARM 아키텍처 구조적인 부분이다. 여기서는 설명을 자제 하겠다.
4.4 OS_CPU_A.S
이 부분은 모두 어셈블리어로 코딩 되어 있다. Ep7209라는 데이터 시트가 필요한다. 이러한 이유는 ARM코어를 이용한 칩이 모두 같은 것이 아니고 단지 코어만 같기 때문이다. 각기 인터럽트 소스도 틀리고 I/O 외 여러가지 틀리기 때문이다.
먼저 scat_c.scf파일의 내용을 살펴 볼 필요가 있다. 아래 내용이다.
MEMORY 0x0
{ROM 0x0
{Vectors.o (Vector,+First)
* (+RO)
}
RAM 0x00100000
{* (+RW,+ZI)
}
}
이 파일은 메모리 영역을 나누어 주는 중요한 기능을 가지는 파일이다. 일반 ARM 컴파일러 옵션에 있지만, 코드를 제대로 생성시키지 않아 강제적으로 이렇게 만들었습니다. 즉 ARM 벡터를 코드 메모리 0 번지에 강제적으로 넣어주게 만들었습니다. 위에서 보면 롬 번지는 0번지부터이고, 코드는 Vectors.o 의 AREA 영역 Vector를 시작으로 하라는 것이다. 램 번지는 0x00100000이고 모든 Read/Write 영역과 Zero Initial영역을 포함하도록 했습니다. 이러한 이유는 Zero Initial영역이 롬번지를 이용하지 못하게 하기 위해서 이다. 각 영역에 대한 설명 ARM Linker부분을 참조 하면 된다. 메모리 부분은 MMU를 통해서 변경된 것이다. 물리적인 외부 버스를 바꾸었습니다.
Vectors.s를 살펴보도록 하겠다.
아래는 실제 코딩부분이다.
;''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''';
; Program Start!!! ;
;.......................................................................;
AREA Vector, CODE, READONLY
ENTRY
;''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''';
; Vector Table ;
;.......................................................................;
b ResetHandler
ldr pc, UndefV
ldr pc, SWIV
ldr pc, PAbortV
ldr pc, DAbortV
ldr pc, UnusedV
ldr pc, IRQV
ldr pc, FIQV
IMPORT ResetHandler
IMPORT UndefHandler
IMPORT SWIHandler
IMPORT PAbortHandler
IMPORT DAbortHandler
IMPORT UnusedHandler
IMPORT IRQHandler
IMPORT FIQHandler
ResetV
DCD ResetHandler
UndefV
DCD UndefHandler
SWIV
DCD SWIHandler
PAbortV
DCD PAbortHandler
DAbortV
DCD DAbortHandler
UnusedV
DCD UnusedHandler
IRQV
DCD IRQHandler
FIQV
DCD FIQHandler
END
각 부분에 대한 세세한 설명은 자제하겠다. 여기서의 기능은 단지 벡터 들의 주소들을 메모리에 저장한 다음 이 점프 주소 메모리를 이용해서 해당 벡터로 점프하는 기능뿐이다. 여러가지가 있지만 모든 것은 ARM매뉴얼을 참조하면 알수 있다.
ResetHandler에서 해주어야 할 일은 칩 클럭을 지정해 주어야 하고, 각각의 모드에 대한 스택 포인터를 지정해 주어야 한다. 또한 코프로세서 셋팅, MMU설정, 메모리 영역 복구도 있다. 그리고 가장 중요한 타이머 기능 또한 셋팅해주어야 한다. 이러한 일련의 과정은 모두 ARM 어셈블리어로 코딩하고, 데이터시트를 참조하면서 해야한다. 물론 C로도 할수 있지만, 연구자가 C라는 언어를 별로 좋아하지 않으므로 부득이한 경우가 아니라면 어셈으로 모두 코딩한다. C로 만든 다음 어셈으로 바뀐 코드를 보고 줄이는 방법을 쓰기도 합니다만..
아래는 실제 코딩된 부분들이다.
ResetHandler
ldr r0, =0x80002200 ; Clock Speed Setting
ldr r1, [r0]
orr r1, r1, #CLKCTL_73
strb r1, [r0]
;------------------------------------------------------------------------
; Stack Point Setting Each CPU Mode
;
ldr r0, =0x000000d2 ; IRQ Mode sp Setting
msr cpsr_cf, r0
ldr r13, =IRQStackEnd
ldr r0, =0x000000d1 ; FIQ Mode sp Setting
msr cpsr_cf, r0
ldr r13, =FIQStackEnd
ldr r0, =0x000000d3 ; SVC Mode sp Setting
msr cpsr_cf, r0
ldr r13, =SVCStackEnd
;------------------------------------------------------------------------
; MMU Configuration
;
ldr r0, =0x00000000
mcr p15, 0, r0, c5, c0
mcr p15, 0, r0, c7, c0
ldr r0, =0x55555555
mcr p15, 0, r0, c3, c0
IMPORT PageTable
ldr r0, =PageTable
mcr p15, 0, r0, c2, c0
ldr r0, =0x0000007d
mcr p15, 0, r0, c1, c0
;
; There should always be two NOP instructions following the enable or
; disable of the MMU.
;
mov r0, r0
mov r0, r0
;
; Delay for MMU
;
ldr r1, =0x0
ldr r2, =0x100
delay
cmp r1, r2
ldrne r3, [r1], #4
bne delay
;------------------------------------------------------------------------
; Variable Area Initialization
;
IMPORT |Image$$RO$$Limit| ; End of ROM code (=start of ROM data)
IMPORT |Image$$RW$$Base| ; Base of RAM to initialise
IMPORT |Image$$ZI$$Base| ; Base and limit of area
IMPORT |Image$$ZI$$Limit| ; to zero initialise;
ldr r0, =|Image$$RO$$Limit| ; Get pointer to ROM data
ldr r1, =|Image$$RW$$Base| ; and RAM copy
ldr r3, =|Image$$ZI$$Base| ; Zero init base => top of initialised data
cmp r0, r1 ; Check that they are different
beq %1
0 cmp r1, r3 ; Copy init data
ldrcc r2, [r0], #4
strcc r2, [r1], #4
bcc %0
1 ldr r1, =|Image$$ZI$$Limit| ; Top of zero init segment
mov r2, #0
2 cmp r3, r1 ; Zero init
strcc r2, [r3], #4
bcc %2
;------------------------------------------------------------------------
; Serial Port Initialization
; 115200 BPS, Data bit 8 Bit, No Parity, Stop Bit 1 Bit
;
ldr r12, =REGISTER_BASE
mov r0, #HwControlUartEnable
str r0, [r12, #HwControl]
ldr r1, =HwStatus2
add r1,r1,r12
ldr r2, [r1]
tst r2, #CLKMOD
ldreq r0, =UartValue
ldrne r0, =UartValue_13
str r0, [r12, #HwUartControl]
;------------------------------------------------------------------------
; Timer1 Setting = 10ms period
;
mov r1,#REGISTER_BASE
ldr r2,=SYSCON1
ldr r0,[r1,r2]
bic r0,r0,#0x20
orr r0,r0,#0x10
str r0,[r1,r2]
ldr r2,=TC1D
ldr r0,=21 ; 2115 -> 1 Second, 21 -> 10ms
str r0,[r1,r2]
ldr r2,=INTMR1
ldr r0,[r1,r2]
orr r0,r0,#0x100
str r0,[r1,r2]
ldr r2,=TC1EOI
mov r0,#0
str r0,[r1,r2]
위의 코드들을 일일이 설명하기 위해서는 많은 시간이 필요하므로 넘어가겠습니다. 모든 것은 ep7209의 칩의 설정에 맞게 세팅 되었고, 또한 다른 기능을 하는 것을 셋팅할수도 있다. Mpeg 1,2및 그래픽 LCD, GPIO도 셋팅할수도 있겠습니다. 이러한 것들은 ep7209의 고유한 기능으로 데이터시트 참조 바랍니다..
다음으로 IRQ Handler 대해서 살펴 보도록 하겠다. 가장 까다로운 부분이면서 중요한 부분이다. ARM 관해서 어느정도의 지식이 필요합니다……
먼저 ARM에서 일반 모드에서 IRQ Interrupt가 발생되면 어떤 일련의 과정을 통해서 IRQ 모드로 들어 갑니다. 아래는 그 순서이다.
R14_irq= address of next instruction to be executed +4
SPSR_irq= CPSR
CPSR[4:0]= 0b10010
CPSR[5]= 0
/* CPSR[6] is unchanged */
CPSR[7]=1
If high vectors configured then
PC= 0xffff0018
Else
PC= 0x00000018
차근 차근 살펴 보도록 하겠다. IRQ가 발생하면 r13과 r14는 IRQ 전용 레지스터로 맵핑된다. 자 그럼 첫번째 줄에서는 현재 실행되고 있는 프로그램의 복귀번지+4의 값이 R14_irq에 저장 된다는 것이다. 이것은 ARM의 파이프라인의 구조적인 문제이다. 이러한 이유로 다시 복귀할때는 SUBS PC,R14,#4로 해주어야 한다. 두번째 줄을 현재 실행되고 있는 프로그램의 CPSR을 IRQ전용 SPSR_irq에 저장된다. 복귀할때 그전에 실행되던 프로그램의 상태를 복구한다. 세번째 줄은 IRQ모드로 전환된다는 의미이다. CPSR의 각 비트에 대해서는 매뉴얼을 참조 하시구요. 상위 4비트는 상태필드 하위 5비트는 모드셋 비트,그외 명령어 선택 비트, IRQ 및 FIQ INEABLE AND DISABLE비트가 있다. 네번째 줄이 ARM모드로 전환 된다는 의미이다. 즉 현재 프로그램이 THUMB모드 였다면 IRQ진입시는 무조전 ARM모드로 전환 된다는 의미이다. 여섯번째줄은 IRQ INTERRUPT를 금지 셋이다. 그 다음 줄 부터는 벡터의 위치 문제로 상위냐 하위냐에 따라 점프 번지가 틀려 진다. 여기서 생각 볼건 처음 전원이 들었갔을때에는 벡터 0번지로 갑니다. 그러나 프로그램 실행시 벡터번지를 상위에 놓을 수 있다는 것이다. 즉 상위를 램으로 한다면 좀 더 빠른 실행 속도를 볼수 있다. 롬 보다는 램이 빠르죠. 이정도 하겠다.
자 이제 우리를 타이머에 대한 것을 생각 해 볼 필요가 있다. REAL-TIME OS에서는 시간 개념이 중요하기 때문이다. 약 1/100 초 마다 커널의 특정 함수를 호출해 줄 필요가 있다. 이함수가 OSTimeTick 이라는 함수이다. 즉, 1/100초 마다 타이머 인터럽트가 발생하도록 칩의 타이머를 초기화 시키고, 인터럽트가 발생하면 OSTimeTick 함수를 한번 호출해 주는 것이다.
Os_cfg.h의 소스 내용을 보면 아래와 같은 것이 있다.
#define OS_TICKS_PER_SEC 100 /* Set the number of ticks in one second */
위의 셋팅은 100Tick이 1초라는 것이다. 그래서 타이머 인터럽트를 설정해 줄 때 1/100초의 주기로 셋팅한 것이다. 즉 1초마다 context switching이 일어난다는 것을 의미한다.
여기서 생각해 볼 것이 있다. Task A가 있고 Task B가 있다고 가정할 때, Task A은
두개의 Task가 1초이내( 100Tick이내)에 실행가능 한 프로그램이라면 단지 스케줄러에 의해서 실행 될수 있다. 만약 Task A가 1초 이상의 실행시간을 가진다면 어떻게 될까 생각해 보아야 한다. 즉 Task A가 실행되다가 100Tick의 타이머 인터럽트가 발생하면 context switching이 일어나야 하는다는 것이다. 타이머 인터럽트에도 현재 작업 레지스터들을 저장해 줄 필요가 있다. 레지스터를 저장 한다는 것은 현재의 실행중이었던 Task의 작업 register들을 해당 Task을 스택에 저장한다는 것이죠. 그럼 스택 모양을 만들었던 것과 똑 같이 해주어하겠다.
아래는 실제 코딩된 내용이다. 책에서 보면 인터럽트 부문은 반드시 어셈블리어로 코딩해야 된다고 나와 있다. 그 이유는 C는 레지스터들을 다이렉트로 억세스하지 못하기 때문이라고 되어 있다.
01: IRQHandler
02: stmfd sp!,{r0-r3}
03: mov r1,#REGISTER_BASE
04: ldr r2,=INTSR1
05: ldr r0,[r1,r2]
06: tst r0,#0x100
07: bne TimerIRQ ; Check Timer IRQ
08: ldmfd sp!,{r0-r3}
09: subs pc,lr,#4
10: TimerIRQ
11: ldr r2,=TC1EOI ; Timer 1 Interrupt Clear
12: str r0,[r1,r2]
13:
14: mov r2,sp ; copy IRQ's sp -> r2
15: add sp,sp,#16 ; recover IRQ's sp
16: sub r3,lr,#4 ; copy return address -> r3
17:
18: LDR r0,=IRQ_2
19: MOVS pc,r0
20: IRQ_2
21: stmfd sp!,{r3} ; push SVC's pc
22: stmfd sp!,{r4-r12,lr} ; push SVC's r14, r12-r4
23: mov r4,r2
24: ldmfd r4!,{r0-r3}
25: stmfd sp!,{r0-r3} ; push SVC's r3-r0
26: mrs r5,cpsr
27: stmfd sp!,{r5} ; push SVC's PSR
28: B OSTickISR ; Real Body...
위 소스들을 분석하기 위해서는 ARM 어셈블리어에 대한 지식이 필요하다. 먼저 IRQ로 들어 가게 되면 현재의 인터럽트가 타이머 인터럽트 인지 판단해 주어야 한다. 그런 구문이 맨 처음 들어가야 할 것이다.. 만약 타이머 인터럽트가 아니라면 다른 서비스 루틴으로 점프하던지 아님 그냥 리턴 해야겠지요. 그 다음의 트릭을 써야 한다. 이러한 이유는 IRQ모드에서는 sp,lr가 독립적이기 때문이다. 그전의 모드의 sp,lr을 저장해야 하므로 트릭을 써야한다. 이러한 트릭은 단순히 모드의 전환과 레지스터의 이용으로 할수 있다.
14번 줄에서 16번 줄까지는 IRQ모드의 sp, lr를 레지스터로 저장하는 것이다. 18번과 19번 줄은 IRQ모드를 벗어나는 구문이다. 그 아래 부분이 스택 모양 대로 현 Task의 스택에 작업 내용을 저장하는 구문들이다.
OSTickISR()를 보겠습니다.
책을 보면 이함수의 코딩 방법있다.
Void OSTickISR(void)
{
Save Processor registers;
Call OSIntEnter() or increment OSIntNesting;
Call OSTimeTick();
Call OSIntExit();
Restore processor registers;
Execute a return from interrupt instruction;
}
(1) 레지스터들을 저장한다.
(2) OSIntEnter() 함수를 호출하거나, OSIntNesting변수를 증가시킨다.
(3) OSTimeTick() 함수를 호출한다.
(4) OSIntExit() 함수를 호출한다.
(5) 레지스터들을 복구한다.
(6) 인터럽트 종료...
처음 부분은 IRQ Handler 시작부분에 처리한 내용이고, 나머지 부분은 코딩해 주면 된다.
아래는 실제 코딩된 내용이다.
01: OSTickISR
02: LDR r0,=OSIntNesting ; Notify uC/OS-II of ISR
03: LDRB r1,[r0]
04: ADD r1,r1,#1
05: STRB r1,[r0]
06: BL OSTimeTick ; Process system tick
07: BL OSIntExit ; Notify uC/OS-II of end of ISR
08: LDMFD sp!,{r0}
09: MSR CPSR_xsf,r0
10: LDMFD sp!,{r0 - r12, lr , pc}
간단한 함수 이므로 쉽게 이해할 수 있다. 소스 내용을 보면 첫 부분에 증가시켜주었고 함수들로 Call 처리하였다. 나머지 부분은 레지스터 복구의 과정이다.
OSStartHighRdy() 함수
Context Switching함수는 현재 어떤 Task가 실행되고 있는 시점에서 호출되는 함수인데 반해, OSStartHighRdy() 함수가 실행되는 시점에서는 어떤 Task도 실행되고 있지 않은 상태이다. 즉 현재 상태의 레지스터들을 저장하지 않아도 된다는 뜻이다.
책의 순서를 보겠습니다.
Void OSStartHighRdy(void)
{
Call user definable OSTaskSwHook();
Get the stack pointer of the task to resume;
Stack pointer = OSTCBHighRdy->OSTCBStkPtr;
OSRunning = TRUE;
Restore all processor registers from the new task’s stack;
Execute a return from interrupt instruction;
}
(1) OSTaskSwHook() 함수 호출
(2) 실행시키고자 하는 Task의 Stack Point를 얻어와서 sp에 설정
(3) OSRunning 값을 TRUE로 설정
(4) 설정된 sp에서 레지스터 값을 모두 꺼내옴
(5) 새로운 Task를 실행
아래는 실제 코딩된 내용이다.
01: OSStartHighRdy
02: BL OSTaskSwHook
03: LDR r0,=OSRunning
04: MOV r1,#1
05: STRB r1,[r0]
06: LDR r0,=OSTCBHighRdy
07: LDR r0,[r0]
08: LDR sp,[r0]
09: LDMFD sp!,{r0}
10: MSR CPSR_xsf,r0
11: LDMFD sp!,{r0 - r12, lr , pc}
위 함수 역시 쉽게 이해할 수 있다. 별반 틀린 점이 없다. 단 순서가 좀 틀리긴 하지만 책 저자의 요구하는 것을 모두 만족한다.
OSCtxSw() 함수
OSCtxSw함수는 시스템 서비스를 수행하다가 context switching이 일어날 경우 호출되는 함수이다. 내부적인 일은 현재 작업 내용인 레지스터들을 저장하고 새로운 Task 정보를 가져와 실행시켜 주는 부분까지이다.
어떻든... 중요한 점은 OSCtxSw함수는 시스템 서비스를 수행하다가 Context Switching 이 일어날 경우 호출되는 함수라는 점이다.
아래는 책의 내용이다.
Void OSCtxSw(void)
{
Save processor registers;
Save the current task’s stack pointer intor the current task’s OS_TCB;
OSTCBCur->OSTCBStkPtr = Stack pointer;
Call user definable OSTaskSwHook();
OSTCBCur= OSTCBHighRdy;
OSPrioCur = OSPrioHighRdy;
Get the stack pointer of the task to resume;
Stack pointer = OSTCBHighRdy->OSTCBStkPtr;
Restore all processor registers from the new task’s stack;
Execute a return from interrupt instruction;
}
(1) 사용중인 Register들을 Task의 Stack에 저장
(2) 현재 Stack Pointer를 TCB의 OSTCBStkPtr 변수에 저장
(3) OSTaskSwHook() 호출
(4) OSTCBCur = OSTCBHighRdy
(5) OSPrioCur = OSPrioHighRdy
(6) 실행될 Task의 Stack Pointer를 sp로 설정
(7) 해당 스택에서 레지스터들을 복구
(8) 새로운 Task 실행
순서들을 살펴보면 (1),(2)번 과정이 현재 작업 내용을 마무리 하는 부분이다. 즉 현재 실행중이던 Task의 작업 내용들을 저장한다는 것이다. (3)번 과정은 User 의 Hook함수를 호출해 주는 부분이다. (4)번과 (5)번은 전역변수 값을 조정하는 부분이다.
위의 내용을 보면 (1),(2)번이 현재 작업 내용을 마무리 하는 부분이다. 깍던
사과를 쟁반위에 갈무리하는 부분이다. (3)번은 User 의 Hook함수를 호출해 주
는 부분이고, (4)번과 (5)번은 전역변수 값을 조정하는 부분이다. 전역변수 OSTCBCur은 현재 실행되고 있는 Task의 TCB를 가리키는 포인터이고 OSTCBHighRdy는 ReadyQ내의 가장 높은 우선순위의 TCB를 카리키는 포인터이다.
아래는 실제 코딩된 내용이다.
01: OSCtxSw
02: STMFD sp!,{lr}
03: STMFD sp!,{r0 - r12, lr}
04: MRS r0,CPSR
05: STMFD sp!,{r0}
06: LDR r0,=OSTCBCur
07: LDR r0,[r0]
08: STR sp,[r0]
09: BL OSTaskSwHook
10: LDR r0,=OSTCBCur
11: LDR r1,=OSTCBHighRdy
12: LDR r2,[r1]
13: STR r2,[r0]
14: LDR r0,=OSPrioCur
15: LDR r1,=OSPrioHighRdy
16: LDRB r3,[r1]
17: STRB r3,[r0]
18: LDR sp,[r2]
19: LDMFD sp!,{r0}
20: MSR CPSR_xsf,r0
21: LDMFD sp!,{r0 - r12, lr , pc}
위 소스를 보면 간단히 이해 할 수 있다. 차근 차근 소스들을 따라가 보면 책의 저자가 원한 내용을 만족함을 알 수 있다.
OSIntCtxSw() 함수
MicroC/OS에서 context switching을 수행하는 함수는 두개가 있다. OSCtxSw와 OSIntCtxSw이다. OSCtxSw함수는 시스템 서비스 내부에서 호출되고, OSIntCtxSw함수는 항상 인터럽트 종료 시점에서 호출 된다는 차이가 있다.
아래는 책의 내용이다.
Void OSIntCtxSw(void)
{
Adjust the stack pointer to remove calls to;
OSIntExit();
OSIntCtxSw(), and possibly the push of the processor status word;
Save the current task’s stack pointer into the current task’s OS_TCB;
OSTCBCur->OSTCBStkPtr = Stack pointer;
Call user-definable OSTaskSwHook();
OSTCBCur= OSTCBHighRdy;
OSPrioCur =OSPrioHighRdy;
Get the stack pointer of the task to resume;
Stack pointer =OSTCBHighRdy->OSTCBStkPtr;
Restore all processor registers from the new task’s stack;
Execute a return from interrupt instruction;
}
(1) 스택 포인터를 조정(필요없는 부분을 삭제)
(2) 현재 Stack Pointer를 TCB의 OSTCBStkPtr 변수에 저장
(3) OSTaskSwHook 함수를 호출
(4) OSTCBCur = OSTCBHighRdy
(5) OSPrioCur = OSPrioHighRdy
(6) 실행될 Task의 Stack Pointer를 sp로 설정
(7) 해당 스택에서 레지스터들을 복구
(8) 새로운 Task 실행
아래는 코딩된 소스이다.
01: OSIntCtxSw
02: ADD sp,sp,#4
03: LDR r0,=OSTCBCur
04: LDR r0,[r0]
05: STR sp,[r0]
06: BL OSTaskSwHook
07: LDR r0,=OSTCBCur
08: LDR r1,=OSTCBHighRdy
09: LDR r2,[r1]
10: STR r2,[r0]
11: LDR r0,=OSPrioCur
12:; LDR r1,=OSPrioHighRdy
13: LDRB r3,[r1]
14: STRB r3,[r0]
15: LDR sp,[r2]
16: LDMFD sp!,{r0}
17: MSR CPSR_xsf,r0
18: LDMFD sp!,{r0 - r12, lr , pc}
OSIntCtxSw()함수가 호출 되는 시점은 인터럽트의 OsintExit()함수 내부에 있다.
OSTickISR
LDR r0,=OSIntNesting
LDRB r1,[r0]
ADD r1,r1,#1
STRB r1,[r0]
BL OSTimeTick
BL OSIntExit <- 이 부분에서 OSIntCtxSw를 호출하게 됩니다
LDMFD sp!,{r0}
MSR CPSR_xsf,r0
LDMFD sp!,{r0 - r12, lr , pc}
생각해 보면 다시 Call하므로 리턴 번지가 깨지는 현상이 발생할 수도 있다. 참고 사항이지만 OSIntExit의 스택의 사용은 단지 리턴 번지를 저장만 할 뿐이다.
그래서 첫째줄에 ADD sp,sp,#4를 해주고 원래 스택포인터를 복구시키는 것이다.
이것으로 실제 포팅에 대한 사항은 마무리가 된다.
5. VxWork VS MicroC/OS-II
상용 OS의 대표적인 VxWork와 비교를 해 보겠습니다…

6. 참고 자료 및 문헌
MicroC/OS-II The Real-Time Kernel ( 저자 JEAN J. LABROSSE)
ARM System on chip Architecture(저자 Steve Furber)
ARM Developer Suite 포함 매뉴얼( 저자 ARM Cop.)
Ep7209 Data sheet
ARM 720T Data sheet
제 5회 리눅스 공동체 세미나 강의록
ARM 강의록 (저자 김효준)
ARM 포팅소스 (원 저자 김효준)
ARM 포팅 강의록(저자 김효준)
Computer Organization & Design (저자 David A. Patterson and John L. Hennessy)
이상 참고 자료이다.
그 다음으로 DOS처럼 만들어 보는 소스는 생략한다. 100페이지 분량 이상이 된다.