04 NDS 홈브루(Homebrew) - NDS 커널(Kernel) 만들기

들어가기 전에...

0.시작하면서...

NDS 커널 플레이 동영상

libnds를 이용해서 프로그램을 작성하는 예제는 충분히 많이 있다. 하지만 이것만 가지고는 무엇인가 부족하다. NDS에서 멀티 태스킹을 지원할 수 는 없을까? NDS는 타이머를 4개나 가지고 있기 때문에 시분할 멀티 태스킹을 하는데 전혀 문제가 없다.

커널이 되기위해서는 빠질 수 없는 기능이 멀티 태스킹이고 하니, NDS에 태스크 스위칭(Task Swithcing) 기능을 넣어보도록 하자. 컨텍스트 저장에 사용되는 곳은 OS 프레임워크와 동일하게 각 태스크 스택의 가장 아래부분(0 쪽에 가까운 부분)에 저장되고 아래의 순서로 총 16개의 값이 저장된다.

스택에 저장되는 레지스터 정보

위의 그림은 아래에서 설명할 소스코드를 이해하는데 도움이 되므로 꼭 봐두도록 하자

1.System/User 모드에서 태스크 스위칭(Task Switching)

유저모드에서 간단히 태스크 스위칭 하는 방법은 스택에다가 레지스터를 전부 넣고 스택을 스위칭 한 뒤에 전부 빼면된다. 저장하는 방식은 스택의 Bottom에서 레지스터의 갯수만큼을 저장하고 다시 복원하면 간단히 해결할 수 있다.

    태스크 스위치를 수행하는 함수  
    SwitchTask: // 0x0000 -> 0xFFFF로 올라가니까 넣고 증가시켜야 한다.  
    STMIA R0, { R0-R14 }  
    LDMIA R1, { R0-R13, pc}

2.타이머 인터럽트(Timer Interrupt)를 통한 태스크 스위칭(Task Swithcing)

2.1 인터럽트 핸들러(Interrupt Handler) 코드와 스택(Stack)

타이머 인터럽트를 통해 태스트스위칭을 할때 User/System 모드와 다른 점은 모드가 User/System 모드에서 IRQ 모드로 바뀐다는 것이다. 프로세서의 모드에 대한 내용은 참고. ARM Processor Overview의 내용을 참고하도록 하고 실질적인 문제에 대해서 알아보자.

가장 큰 문제는 레지스터가 뱅크되어 IRQ 모드의 R13(sp)R14(lr), 그리고 SPSR을 가진다는 것이다. 따라서 정상적인 태스트 스위칭을 위해서는 현재 태스크를 저장할 때 User/System 모드의 레지스터 및 SPSR을 저장해야 하며 복원할 태스크 또한 유저모드의 R13R14를 잘 복원해주고 유저모드의 CPSR에SPSR에 넣어준 다음 리턴해야 한다.

문제는 이것만이 아니다. 더 큰 문제는 NDS BIOS 라이브러리에 있는 인터럽트 처리 루틴을 통과한 후에 내가 제어를 이어받는 다는 것인데, NDS BIOS에서 인터럽트 선행 처리를 위해 무슨 일을 하는지 모르면 컨텍스트(Context)를 정확하게 저장할 수 없다. 상당히 충격적인 내용인데... BIOS와 동작 호환을 위해서는 이 부분에 대한 처리를 해줘야.. ㅜ_ㅜ...

일단 NDS의 인터럽트 디스패처(Interrupt Dispatcher) 소스는 \devkitPro\libnds\source\source\common 폴더의 interruptDispatcher.s 파일에서 찾을 수 있고 아래와 같다.

    #ifdef ARM7  
    .text  
    #endif

    #ifdef ARM9  
    .section .itcm,"ax",%progbits  
    #endif

    .extern irqTable  
    .code 32

    .global IntrMain

    @---------------------------------------------------------------------------------  
    IntrMain:
    @---------------------------------------------------------------------------------  
    mov r3, #0x4000000 @ REG_BASE

    ldr r1, [r3, #0x208] @ r1 = IME  
    str r3, [r3, #0x208] @ disable IME  
    mrs r0, spsr  
    stmfd sp!, {r0-r1,r3} @ {spsr, IME, REG_BASE}

    ldr r1, [r3,#0x210] @ REG_IE  
    ldr r2, [r3,#0x214] @ REG_IF  
    and r1,r1,r2

    ldr r0,=__irq_flags @ defined by linker script

    ldr r2,[r0]  
    orr r2,r2,r1  
    str r2,[r0]

    ldr r2,=irqTable

    @---------------------------------------------------------------------------------  
    findIRQ:  
    @---------------------------------------------------------------------------------  
    ldr r0, [r2, #4]  
    cmp r0,#0  
    beq no_handler  
    ands r0, r0, r1  
    bne jump_intr  
    add r2, r2, #8  
    b findIRQ

    @---------------------------------------------------------------------------------  
    no_handler:  
    @---------------------------------------------------------------------------------  
    str r1, [r3, #0x0214] @ IF Clear  
    ldmfd sp!, {r0-r1,r3} @ {spsr, IME, REG_BASE}  
    str r1, [r3, #0x208] @ restore REG_IME  
    mov pc,lr

    @---------------------------------------------------------------------------------  
    jump_intr:  
    @---------------------------------------------------------------------------------  
    ldr r1, [r2] @ user IRQ handler address  
    cmp r1, #0  
    bne got_handler  
    mov r1, r0  
    b no_handler

    @---------------------------------------------------------------------------------  
    got_handler:  
    @---------------------------------------------------------------------------------

    mrs r2, cpsr  
    bic r2, r2, #0xdf @ __  
    orr r2, r2, #0x1f @ / --> Enable IRQ & FIQ. Set CPU mode to System.  
    msr cpsr,r2

    str r0, [r3, #0x0214] @ IF Clear 
    push {lr}

    adr lr, IntrRet  
    bx r1

    @---------------------------------------------------------------------------------  
    IntrRet:  
    @---------------------------------------------------------------------------------  
    pop {lr}  
    mov r3, #0x4000000 @ REG_BASE  
    str r3, [r3, #0x208] @ disable IME

    mrs r3, cpsr  
    bic r3, r3, #0xdf @ __  
    orr r3, r3, #0x92 @ / --> Disable IRQ. Enable FIQ. Set CPU mode to IRQ.  
    msr cpsr, r3**

    ldmfd sp!, {r0-r1,r3} @ {spsr, IME, REG_BASE}

    str r1, [r3, #0x208] @ restore REG_IME  
    msr spsr, r0 @ restore spsr  
    mov pc,lr

    .pool  
    .end

코드의 중요부분은 IRQ Table에서 해당 처리 루틴을 찾아서 IRQ 모드에서 System 모드로 변경한 다음 함수를 호출하는 부분이다. 즉 타이머 인터럽트가 발생하면 내가 만든 타미어 핸들러를 호출하고 이 타이머 핸들러에서 리턴을 하면 정상적인 루트를 통해 다시 복구하도록 되어있다.

그렇다면 IntrMain 이라는 코드는 NDS의 BIOS가 불러준다는 이야긴데... 어떻게 찾아서 저 함수를 불러주는 것일까?

답은 아래는 \devkitPro\libnds\source\source\common 폴더에 있는 Interrupts.c 소스에서 찾을 수 있다. irqInit() 함수에서 위의 IntrMain 함수를 BIOS에서 호출해 줄 핸들러로 등록한다.

     ........ 생략 ........

    //---------------------------------------------------------------------------------  
    void irqInit() {  
    //---------------------------------------------------------------------------------  
    int i;

    // Set all interrupts to dummy functions.  
    for(i = 0; i < MAX_INTERRUPTS; i ++)  
    {  
    irqTable[i].handler = irqDummy;  
    irqTable[i].mask = 0;  
    }

    **IRQ_HANDLER = IntrMain;**

    REG_IE = 0; // disable all interrupts  
    REG_IF = IRQ_ALL; // clear all pending interrupts  
    REG_IME = 1; // enable global interrupt

    }

    ........ 생략 ........

여기서 잠깐.... 뭔가 이상한 점이 느껴지지 않는가? 보통 인터럽트가 발생하면 컨텍스트(Context)를 다 저장해 주고 인터럽트 처리 함수를 호출하여 인터럽트에 대한 처리를 한다음 다시 컨텍스트를 복구하는 절차를 거친다. 그런데 위의 InterruptDispatcher.s 소스에서는 그런 루틴이 보이지 않는다.

ARM 같은 경우라면 ldmfd sp!, {r0-r13} 등과 같은 코드가 있어야 할터인데... 이로보아 IntrMain 코드는 NDS의 BIOS에 의해 불려지는 코드임을 추측할 수 있다.

결국 BIOS의 인터럽트 핸들러 함수(인터럽트 처리의 최고 앞단)에 대해서 알아야 태스크 스위칭같은 문제를 해결할 수 있다는 것인데... ARM9 BIOS의 인터럽트 처리 코드는 어떤 형태일까? 한참을 뒤적이다가 http://www.bottledlight.com/ds/index.php/Main/Interrupts 에서 그 내용을 찾았다.

ARM7 Interrupt handler:

  1. stmdb sp!, {r0-r3, r12, lr}mov r0, #0x04000000
    add lr, pc, #0x0
    ldr pc, [r0, #-0x4]

    ldmia sp!, {r0-r3, r12, lr}

    subs pc, lr, #0x4

The user ARM7 interrupt vector is thus 0x03FFFFFC (mirrors down into ARM7 work RAM)

ARM9 Interrupt handler:

  • stmdb sp!, {r0-r3, r12, lr}
    mrc p15, 0, r0, c9, c1 @ r0 = DTCM_BaseAddress + 0x4000
    mov r0, r0, lsr #12
    mov r0, r0, lsl #12
    add r0, r0, #0x4000
    add lr, pc, #0x0
    ldr pc, [r0, #-0x4] @ bl [DTCM_BaseAddress + 0x3FFC]
    ldmia sp!, {r0-r3, r12, lr}
    subs pc, lr, #0x4

The user ARM9 interrupt vector is thus at DTCM+0x3FFC

In both cases, the BIOS flag word used in swi 0x4 and 0x5 is 4 bytes before the interrupt vector.

위에서 보면 알 수 있듯이 R0, R1, R2, R3, R12, lr을 저장하고 핸들러를 부른다음 IntrMain(붉은 색으로 표시된 부분)을 부르는 것을 알 수 있다. 그 뒤 다시 스택에서 레지스터를 다 복원한 다음 리턴한다. 모드 전환과 같은 코드가 없는 걸 봐서 libnds의 interruptDispatcher.s 파일에 있는 IntrMain 함수를 호출할때는 IRQ 스택을 사용하는 상태이다.

System 모드의 스택은 태스크마다 분리되어있기 때문에 저장해도 별 문제가 없지만 IRQ 레벨의 스택은 공통적으로 사용하기 때문에 만약 타이머 IRQ에서 다시 다른 IRQ가 발생하여 IRQ 스택에 데이터를 PUSH나 POP하면 문제가 발생한다.

일단 위험에 대해서는 위에서 많이 설명했으니 Interrupt가 발생하여 BIOS -> libnds의 IntrMain -> MyTimerHandler 순서로 호출되었을 때 System모드의 스택과 IRQ 모드의 스택 내용을 한번 보자.


<사용자 인터럽트 처리 함수까지 왔을 때의 스택의 모습>

위에서 IRQ 스택과 System 스택 2개로 나누어 진 것은 libndsHandler함수(IntrMain)에 의해서 System 모드로 전환되기 때문이다.

사용자 핸들러 함수(My Handler)가 불린 시점은 이미 System 모드로 전환된 상태이며 스택에 System 모드의 LR(R14) 레지스터의 값이 스택에 저장되어있다. 태스크 스위칭을 이 상황에서 수행하려면 저장하는 컨택스트는 IRQ 스택에서 SPSR과 R0~R3, R12, IRQ_LR을 빼서 저장해야 하고 System 스택에서는 SYSEM_LR을 빼서 저장해야 한다.

2.2 ARM 모드와 THUMB 모드

그런데 이것이 끝이 아니었다. 근 3일 정도를 스택을 계속 보면서 고민했는데, 그냥 리턴을 해서는 정상적으로 동작하지 않았다.

뭐가 문제일까? 그렇게 3일을 고민한 끝에 ARM 모드와 THUMB 모드의 전환... 이라는 내용이 머리를 스쳤다. 컴파일 옵션을 보면 우리가 C로 만든 태스크는 컴파일 옵션에 의해 THUMB 모드로 동작하게 되어있다. 그리고 ARM 모드로 컴파일된 코드를 호출할 수 있도록 Interwork 라는 옵션도 같이 들어있다. 이 말은 ARM 모드와 THUMB 모드를 넘나들며 함수를 호출할 때 중간에 Proxy 함수를 이용해서 호출해서 자동으로 처리해 준다는 이야긴데.... 일반적인 함수 호출에는 큰 문제가 없는 것 같았다. 하지만... 컨텍스트 스위칭의 경우에는 다르다. 레지스터 하나라도 복구가 잘못되면 그냥 크래쉬.. ㅜ_ㅜ...

주의할 점을 하나하나 집어보도록 하자.

2.2.1 CPSR의 설정

C 코드로 태스크 함수를 작성할 경우는 THUMB 모드 코드가 나오므로 태스크를 실행할때 당연히 CPU가 THUMB 모드로 설정되어야 한다. 따라서 복원할 CPSR의 상태를 THUMB Bit1을 키고 SYSTEM 모드(0x1F)로 설정해야 한다. 그런데 만약 어셈블리어 코드로 태스크 함수를 작성하는 경우는 어셈블리어 코드의 생성 옵션(.ARM or .THUMB) 같은 옵션에 맞게 설정해 주어야 한다.

2.2.2 ARM 모드 코드와 THUMB 모드 코드

C 코드(THUMB 코드)에서 어셈블리어 코드(ARM 코드)를 호출하면 어떻게 될까? 컴파일러가 자동적으로 Proxy 함수를 생성하여 C 함수 코드(THUMB 코드) -> THUMB/ARM 변경 코드 -> 어셈블리어 코드(ARM 코드)의 순서로 호출되게 된다. 여기서 잠깐 생각해 볼 것이 Proxy 함수를 거치면 상태 값이 살짝 바뀐다는 것이다. 여기에 대한 자세한 내용은 참고. THUMB 코드와 ARM 코드 및 상호 호출(Interwork) 부분을 참고 하도록 하자. 따라서 이 부분에 대한 처리도 해줘야 하므로 상당히 복잡해 진다.

어셈블리어 코드는 .ARM 모드에서 작성되어 모든 레지스터를 다 저장하고 복구하도록 되어있다. 하지만 타이머 인터럽트에 의해 불리어지는 C 함수는 THUMB 모드로 컴파일 되었고, 이 C 함수를 불러주는 인터럽트 함수는 ARM 모드에서 컴파일 된 핸들러 루틴이다.

이쯤되면 골치가 지끈 아파지고 쉽지 않을꺼라는 생각이 들텐데... 이 문제 때문에 3일을 고생했다. 결국은 요령으로 해결하긴 했지만... ARM 모드와 THUMB 모드를 번갈아가며 사용할 때 태스크 스위칭이 이렇게 힘든 줄 몰랐다.

2.2.3 태스크 스위칭(Task Switching) 코드

한참을 고생한 뒤에 나온 소스가 아래의 태스크 스위칭 코드이다.

@---------------------------------------------------------------------------------

.section ".text"  
.global SwitchTask  
.global SwitchTask2  
.global isrTimerInAsm  
.extern isrTimerInC  
.global g_dwCurTask  
.global g_dwNextTask  

@---------------------------------------------------------------------------------

.align  4  
.arm

@ Timer Interrupt에서 바로 호출해 주는 함수  
isrTimerInAsm:

PUSH { LR }  
BL isrTimerInC      

 LDR R0, g_dwCurTask

LDR R1, g_dwNextTask

 CMP R1, #0

BEQ TIMEREND  
    ADD R2, SP, #0x04  
    bl SwitchTask2

 TIMEREND:

    POP { LR }  
    BX LR   

/*

Switch Task  
    SYSTEM_R0-R14(LR), IRQ_R14, SPSR 순서로 저장한다.  
    IME가 불가된 상태에서 호출하면 안된다.  
    이 함수가 끝나면 가능상태로 바뀌기 때문이다.    

*/  
SwitchTask2:

// 인터럽트 불가 설정  
MOV R3, #0x4000000  
STR R3, [ R3, #0x208 ]    

MOV SP, R2  
POP { LR }  

// System 모드 레지스터를 저장한다.  
// R4-R14 저장  
ADD R3, R0, #16  
STMIA R3, { R4-R14 }^

MRS R3, CPSR  
BIC R3, R3, #0xDF  
ORR R3, R3, #0x92 // ISR 모드로 변경  
MSR CPSR, R3

LDMFD SP!, { R5-R6, R7 }  
//STR R6, [ R7, #0x208 ]  
// SPSR 저장  
//MSR SPSR, R0  
STR R5, [ R0, #64 ]

// R0-R3, R12, IRQ_LR 저장  
LDMFD SP!, { R5-R8, R12, LR }  
STMIA R0, { R5-R8 }  
STR R12, [ R0, #48 ]  
SUB LR, LR, #0x04  
STR LR, [ R0, #60 ]  
// 여기까지 오면 저장 끝...

// 여기서 부터는 복원 시작..

// SPSR 복원  
LDR R5, [ R1, #64 ]  
MSR SPSR, R5  
LDR LR, [ R1, #60 ]

// 인터럽트 가능 설정  
MOV R3, #0x4000001  
STR R3, [ R3, #0x207 ]

 LDMIA R1, { R0-R14 }^

//SUBS PC, LR, #0x00  
 MOVS PC, LR

/*  
Task Switch용 데이터를 저장하는 공간  
*/  
g_dwCurTask: nop  
nop  
g_dwNextTask:  
nop  
nop

간단한 아이디어는 아래와 같다.

  • libnds에 의해 호출되는 타이머 인터럽트 핸들러ARM 코드로 작성한다.
  • 스케줄링을 통해 스위칭할 태스크의 변수를 설정하고 기타 처리를하는 함수는 C로 작성한다.(THUMB 코드가 된다.)
  • 스케줄링 함수에서는 전역 변수 g_dwCurTask, g_dwNextTask 변수에 태스크 스위칭할 태스크 구조체를 넣어서 리턴한다.
  • 타이머 핸들러 함수(ARM 코드)에서에 전역 변수 g_dwCurTask, g_dwNextTask를 이용하여** 태스크 스위칭 함수(ARM 코드)**를 호출한다.
    • 태스크 스위칭 함수로 들어오면 스택 구조는 위 그림에서 보는 것과 같은 형태로 되어있으므로 IRQ 모드 및 SYSTEM 모드를 오가면서 태스크를 저장하고 복원한다.
    • THUMB 모드에서 이 함수를 호출 할 경우에는 Proxy 함수(THUMB에서 ARM 모드로 변경하는 함수)를 통해 호출되므로 모드가 변경되어 호출된다. 따라서 이 THUMB 모드 함수에서 태스크 스위칭 함수(ARM 코드)를 부르게 되면 모드 전환같은 곤란한 문제가 생기므로 약간 곤란하다. 이를 피하기 위해 내가 만든 타이머 인터럽트 핸들러 함수(ARM 코드)에서 태스크 스위칭 함수를 호출하여준다.
  • 만약 스케줄러가 NULL 값을 전역 변수 g_dwNextTask로 설정하면 태스크 스위칭을 호출하지 않고 정상적인 루트로 리턴을 하여 돌아간다. THUMB 모드에서 ARM 모드로 이동했을때 Proxy를 사용한다는 것은 알고 있었지만... 그게 이렇게 큰 문제를 일으킬줄은 몰랐다. 단순한 함수의 호출같은 문제는 크게 관계 없지만... 컨텍스트 스위칭의 경우는 세세한 부분까지 신경을 써줘야 하는 부분인데... 이렇게 되니 완전 눈물이 나는... ㅜ_ㅜ

3.스케줄러(Scheduler) 및 태스크(Task) 함수들...

스케줄러의 기본형태는 OS 프레임워크의 소스를 그대로 따르고 있다. 스케줄링 알고리즘은 라운드 로빈의 형태를 띄고 있고 지금 최대 5개까지 생성 가능하도록 되어있다. 스케줄러에 대한 자세한 내용은 Part14. Tutorial2-멀티 태스킹(Multi Tasking) 기능을 추가해 보자의 내용을 살펴보면 된다.

3.1 태스크 등록 및 타이머의 처리

ARM9의 main.cpp 파일을 보면 타이머를 설정하고 타이머 핸들러를 등록하는 부분이 있다.

// Timer의 Tick Interval 20ms  
#define TIMER_TICKINTERVAL_MS 20
/**
Timer의 핸들러  
*/  
extern "C" DWORD isrTimerInC( void )  
{
    char vcBuffer[ 2 ];  

    REG_IF |= IRQ_TIMER0;  
    VBLANK_INTR_WAIT_FLAGS |= IRQ_TIMER0;  

    g_uiTimerCount++;

    vcBuffer[ 1 ] = 0;  
    vcBuffer[ 0 ] = g_uiTimerCount & 0x7F;  
    // 타이머가 돌아가고 있다는 것을 표시한다.  
    SUBPRINT( 41, 0, vcBuffer );

    // 스케줄러를 호출한다.
    Scheduler();  

    return 0;  
}

/**  
IRQ를 설정한다.
  신버전.. libnds의 루틴을 사용하도록 수정  
*/  
void SetIrq( void )  
{  
    // 인터럽트 불가 설정
    REG_IME = 0;

    irqInit();
    irqSet( IRQ_VBLANK, isrVBlank );
    irqEnable( IRQ_VBLANK );  
    irqSet( IRQ_TIMER0, isrTimerInAsm );  
    irqEnable( IRQ_TIMER0 ); 

    // 20 ms 마다 한번씩 튀도록 한다.
    TIMER0_DATA = TIMER_FREQ_256( 1000 / TIMER_TICKINTERVAL_MS );  

    // 테스트 용으로 분주를 좀 늘려서 천천히 튀게 했다. 
    TIMER0_CR = TIMER_ENABLE | TIMER_IRQ_REQ | TIMER_DIV_256;
    REG_IF |= 0xFFFFFFFF;  
    REG_IME = 1;  
}

단순히 타이머를 20ms 마다 튀도록 설정해 놓고 isrTimerInC 함수에서 스케줄러 함수를 부르는 것을 볼 수 있다. 스케줄러 함수는 g_dwCurTask 및 g_dwNextTask 에 스위칭 할 태스크들을 설정해서 리턴한다. isrTimerInC 함수가 리턴되고 나면 위에서 설명했던 isrTimerInAsm 함수에서 실제로 스위칭하는 함수를 부르게 된다.

3.2 Snake 게임

원래 Snake 게임은 꼬리가 길어지고 점점 사과가 늘어나고 그래야 하지만.... 내가 만든 Snake 게임은... 꼬리도 안 길어지고 사과만 먹으면 된다. 사과(녹색 $ 표시)를 다 먹으면 먹은 사과의 개수와 Play하는데 걸린 시간이 표시된다. 사과를 다 먹기 전에 벽(하얀색 #)에 부딪히게되면 게임이 종료되므로 주의해야 한다. 게임이 종료되면 A 버튼을 누르면 다시 게임을 제개할 수 있다.

제일 주의할 점은.... 중독성이 있으니 자제를 해야한다는 것이다. 시간 갱신에 힘쓰다보면 큰일이 생기니 적당히 하도록 하자. @0@)/~~

3.3 기타 테스크

다른 테스크들은 매우 간단한 일만 하는 테스크이므로 굳이 설명을 하지 않겠다. 소스를 보면 쉽게 이해가 될터이니...(하는 일도 별로 없고.. ㅡ_ㅡ;;;)

4.실행화면


<NDS 커널의 실행화면>

NDS 커널을 실행하면 위와 같은 화면이 나온다. 총 5개의 태스크가 돌아가는 화면이며 각 태스크는 아래와 같은 위치에 있다.

NDS 커널의 테스트 동작 화면

위의 붉은 색 사각형 하나하나가 다 개별적인 태스크로 동작하며 시분할 스케줄링 기법을 이용하여 동시(??)에 동작한다. 특히 Task3번의 경우는 2초마다 사운드를 출력하도록 되어있기 때문에 주기적으로 울리는 소리를 들을 수 있다.

5.추가사항

5.1 2007/08/29 추가 사항

  • Snake 게임에 보글보글 배경음악 추가
  • Task3은 배경음악을 반복하는 주기를 50초로 설정함. 보글 보글 배경음악이 47초 가량이라서 2~3초 Delay가 생김
  • Snake가 움직일 때 움직이는 소리 추가
  • Snake가 죽을 때 죽는 소리 추가

5.첨부

186343_NDSKernel.zip
2.5 MB

+ Recent posts