참고. 터치스크린(Touch Screen)의 튐 현상 해결방안

들어가기 전에...

0.2007/09/27 터치 문제 해결

  • Z 값을 이용해서 터치 스크린의 문제를 해결함
  • GBA Tek에 있는 소스와 PALib 쪽의 ARM7 함수를 섞어서 해결
    • sPressure = (((( touch.px + 1 ) * touch.z2) >> 6) / touch.z1) - (touch.px >> 6);
    • 위 식의 문제는 펜으로 강하게 터치했을 때 터치 스크린의 좌측의 값은 0이고 우측의 값은 3이 나온다는 것임
    • 터치 스크린의 X 좌표를 이용하게 되어있으므로 좌측의 경우 X 좌표가 0에 가까워져서 분해능이 떨어지는 문제도 발생(좌측의 경우 강약에 따라서 01, 우측의 경우 330 정도의 범위)
    • sPressure1 = ( touch.z2/( touch.z1 - 1 ) );
    • 위 식의 문제는 강하게 터치했을때 터치 스크린의 좌측 일정 범위를 넘어서면 값이 튀는 것임(X좌표의 범위가 0~20 정도일때 값이 급격하게 변함)
    • 그외의 범위에서는 세게 눌렀을 때 값이 우측에서 좌측으로 갈때 3->2->1->0 정도로 변함
  • 결국 두 값의 MIN을 취해서 해결
  • 어느정도 세기의 문제와 X 축 좌표에 따른 압력값 편차의 문제를 해결함
  • 압력값은 5정도 보다 작도록 설정하면 안튀는 것 같음
  • 아래는 사용한 코드
    // 압력계산 코드  
    sPressure = (((( touch.px + 1 ) \* touch.z2) >> 6) / touch.z1) - (touch.px >> 6);  
    sPressure1 = ( touch.z2/( touch.z1 - 1 ) );  
    if( sPressure1 < sPressure )  
    {  
        sPressure2 = sPressure1;  
    }  
    else  
    {  
        sPressure2 = sPressure;  
    }

    // 아래의 Touch와 XY버튼은 특별한 케이스이다.  
    // Touch Down  
    // Down 메시지는 계속 보낸다.  
    if( ( usSpecial & ( 0x01 << 6 ) ) && ( touch.x != 0 ) && ( touch.y != 0 ) && **( sPressure2 < 5 )** )

0.2007/09/22 터치 문제 발견

  • 아직 확실하게 터치 스크린 튀는 문제가 해결이 안됬음
    • 아무래도 확실하게 해결하기는 어려운 문제인듯 함... 약간의 기교로 처리할 수는 있는데... 그것 또한 완전하지 않음
  • 결국 원래의 Touch 함수를 사용하고 Z축 값을 입력 받아서 사용하도록 만듬
  • PALib 쪽의 ARM7 의 Main 함수를 참고
  • 아래는 수정된 VcountHandler() 함수
    /**  
        VCount Handler 함수  
            템플릿의 기본 소스를 아주 간단한 로직으로 수정  
    */  
    void VcountHandler()  
    {  
        touchPosition tempPos;  
        unsigned short usButton;  
        unsigned long ulTemp;  
        int a, b;  

        ulTemp = \_touchReadTemperature( &a, &b );  
        usButton = REG\_KEYXY;  

        // 만약 Touch가 되었으면 좌표를 읽어서 설정한다.  
        if( ( usButton & ( 1 << 6 ) ) == 0x00 )  
        {  
            tempPos = touchReadXY();
            if( tempPos.x == 0 || tempPos.y == 0 )  
            {  
                IPC->mailBusy = 1;  
                IPC->buttons = usButton | (1 << 6);  
                IPC->temperature = ulTemp;  
                IPC->mailBusy = 0;  
            }  
            else  
            {     
                IPC->mailBusy = 1;  
                IPC->touchX         = tempPos.x;  
                IPC->touchY   = tempPos.y;  
                IPC->touchXpx  = tempPos.px;  
                IPC->touchYpx  = tempPos.py;  
                IPC->touchZ1  = tempPos.z1;  
                IPC->touchZ2  = tempPos.z2;  
                IPC->temperature    = ulTemp;  
                IPC->buttons        = usButton;  
                IPC->mailBusy = 0;  
            }  
        }  
        else  
        {  
            IPC->mailBusy = 1;  
            IPC->buttons = usButton;  
            IPC->temperature = ulTemp;  
            IPC->mailBusy = 0;  
        }  

        g\_iVCount ^= (80 ^ 130);  
        SetYtrigger(g\_iVCount);  
    } 
  • 아래는 사용하는 소스다
        // Button의 상태를 읽는다.  
        // Mail이 Busy이면 대기한다.  
        while( IPC->mailBusy );  
        usSpecial = ~( IPC->buttons );  
        touch.x = IPC->touchX;  
        touch.y = IPC->touchY;  
        touch.px = IPC->touchXpx;  
        touch.py = IPC->touchYpx;  
        touch.z1 = IPC->touchZ1;  
        touch.z2 = IPC->touchZ2;  
        sPressure = (((IPC->touchXpx \* IPC->touchZ2) >> 6) / IPC->touchZ1) - (IPC->touchXpx >> 6);  

        // Z축 테스트 용  
        DrawBox( ( void\* ) 0x06000000, 16, 16, 256, 32, BIT( 15 ), TRUE );  
        sprintf( vcBuffer, "%d %d %d", touch.z1, touch.z2, sPressure );  

        HanPutStr(( void\* ) 0x06000000, 16, 16, RGB15( 0x1F, 0x1F, 0x1F ) | BIT( 15 ), vcBuffer );   
        ... 
        if( ( usSpecial & ( 0x01 << 6 ) ) &&  
            ( touch.x != 0 ) && ( touch.y != 0 ) && **( sPressure < 9 )** )

0.시작하면서... 2007/09/22 이전...

문쉘의 터치 스크린 관련 소스를 분석해보면 아주 간단하게 되어있음을 알 수 있다. 문서를 읽어보면 터치 스크린에서 값을 읽는 데는 어느정도 시간이 걸리고 또한 정확한 값을 읽기 위해서는 여러번 반복해서 읽어줘야 한다는 것을 알 수 있다. 문쉘의 터치스크린 소스 분석 부분은 07 문쉘(Moon shell)의 터치스크린(Touch Screen) 소스를 참고하고 터치스크린 제어에 대한 부분은 06 키패드(KeyPad) 및 터치스크린(Touch Screen) 제어 부분을 참고하도록 하자.

왜 터치스크린의 값이 이토록 튀는 것일까? 아래와 같은 문제점때문이 아닐까 추측하고 이것을 해결하려 노력했다.

  • X의 좌표값을 읽고 Y의 좌표값을 읽었을 때 SPI를 통해서 값을 읽고 이것을 몇변 반복하는 동안 시간이 걸린다.
  • 터치스크린으로 부터 값을 읽는 동안 언제든지 사용자의 동작에 따라 펜이 Release 될 수 있다.
  • ARM7에서 값을 쓰고 ARM9에서 값을 읽을 때 동기화의 문제

이제 위의 문제에 대해서 하나하나 살펴보도록 하자.

1.X/Y 좌표 값을 읽는 타이밍(Timing) 문제

터치스크린쪽 스펙을 보면 터치스크린의 값이 유효하지 않을 수 있으니, 여러번 읽어서 사용하라고 되어있다. 실제 코드도 그것을 증명하듯 libnds도 그렇고 문쉘의 소스도 여러번 읽게 되어있다. 실제로 유효하지 않은 값인지를 판단하는 부분은 양쪽이 다 다르지만...

여러번 읽어서 유효한 값을 찾는 것도 중요하지만, 여기에는 큰 문제가 있다. 일단 SPI를 통해서 읽기 때문에 메모리에서 바로 읽는 것 보다는 더 큰 시간이 걸리고(스펙을 보라... 클럭이 1~3MHz 정도다.. ㅡ_ㅡ;;;) X의 좌표를 여러번 읽게 되므로 Y의 좌표를 읽을 때 쯤에는 상당한 시간이 지나있다. 반복하는 회수에 따라서 차이가 있긴 하겠지만 많이 읽으면 읽을 수록 갭은 커친다.

문쉘의 경우에는 5번을 반복하되 읽은 값이 처음 읽은 값과 비교하여 오차범위가 30 내외이면 유효한 값이라 판단하고 그 값을 사용하게 된다. 5번 반복하는 것도 좋지만 어차피 유효한 값을 체크하는 것이라면 3번 정도도 충분하고 오차범위도 30은 큰편이라 생각되어 10으로 줄였다.

물론 오차범위를 줄이면 유효한 값이라 판단되는 범위가 줄어들어서 펜이 Release가 되었다고 인식될 확률이 높지만, 펜이 자주 릴리즈되는 문제는 타이머를 사용하면 해결 할 수 있기 때문에 될 수 있으면 최대한 유효한 정보를 얻는데 초첨을 두었다.

2.펜 릴리즈(Pen Release) 문제

값을 반복해서 읽다 보면 도중에 펜이 릴리즈되어 값이 유효하지 않는 경우가 생긴다. 이때 값은 문서에 보면 X 축의 유효하지 않은 값은 0으로 송신한다. 하지만 Y의 경우는 0이 아니라 0xFFF로 온다. Y 값 같은 경우 0xFFF가 수신되면 0으로 변경하여 값을 넣게 하고 응용 프로그램에서 터치스크린의 Touch.X, Touch.Y의 값이 둘다 0이 아닐때만 사용하도록 하여 정확도를 높였다.

3. ARM9과 ARM7의 동기화(Syncronization) 문제

사실 이 부분은 가상 IPC 구조체에 Busy 플래그를 1로 설정하는 것으로 처리하고 굳이 신경쓰지 않았다. 그 이유는 ARM7에서 터치스크린에 값을 읽는 시간은 디스플레이의 V Count를 이용하여 80번째 스캔라인에서 읽도록 했기 때문이다. ARM9의 경우 가상 IPC에서 값을 읽는 시간은 VBlank 때이므로 시간적 차이가 좀 있다. 따라서 크게 신경쓰지 않았다. 추후 더 자주 읽어야하거나 어떤 문제가 발생하면 좀 더 동기화에 세밀한 신경을 써야 할지도 모르겠다.

4.구현

아래는 실제 코드인 _touch.c 파일의 내부이다. 첨부 파일로도 추가해 놓았다.

#include "_touch.h"
#include <nds.h>  
#include <nds/jtypes.h>  
#include <nds/system.h>  
#include <nds/arm7/touch.h>  
#include <nds/registers_alt.h>
#include <stdlib.h>

// 글로벌 변수들  
static bool gs_bTouchInit = false;  
static long gs_lXScale, gs_lYScale;  
static long gs_lXOffset, gs_lYOffset;
/**  
    문쉘에서 사용하는 touchRead 함수  
        libnds 함수랑 크게 차이 없음  
*/  
__attribute__((noinline)) static uint16 _touchRead(uint32 command) {  
 uint16 result;  
 SerialWaitBusy();
 // Write the command and wait for it to complete  
 REG_SPICNT = SPI_ENABLE | SPI_BAUD_2MHz | SPI_DEVICE_TOUCH | SPI_CONTINUOUS; //0x0A01;  
 REG_SPIDATA = command;  
 SerialWaitBusy();
 // Write the second command and clock in part of the data  
 REG_SPIDATA = 0;  
 SerialWaitBusy();  
 result = REG_SPIDATA;
 // Clock in the rest of the data (last transfer)  
 REG_SPICNT = SPI_ENABLE | 0x201;  
 REG_SPIDATA = 0;  
 SerialWaitBusy();
 // Return the result  
 return ((result & 0x7F) << 5) | (REG_SPIDATA >> 3);  
}

/**  
    온도를 읽는 함수  
        libnds와 크게 차이 없음  
*/  
uint32 _touchReadTemperature(int * t1, int * t2) {  
 *t1 = _touchRead(TSC_MEASURE_TEMP1);  
 *t2 = _touchRead(TSC_MEASURE_TEMP2);  
 return 8490 * (*t2 - *t1) - 273*4096;  
}
 /**  
    TOUCH의 값을 읽어서 유효성을 체크하여 값을 리턴하는 함수  
*/  
__attribute__((noinline)) static s32 readTouchValue(int measure, int retry, int range)  
{  
    int i;  
    s32 this_value=0, this_range;
    s32 last_value = _touchRead(measure | 1);
    for ( i=0; i < retry; i++)  
    {  
        this_value = _touchRead(measure | 1);
        this_range = abs(last_value - this_value);  
        if (this_range <= range) break;  
    }  

   if ( i >= retry ) this_value = 0;
   return this_value;  
}

/**  
    터치 스크린 사용에 관련된 변수값을 설정하고 초기화 하는 함수  
*/  
void _touchReadXY_AutoDetect(void)  
{  
    gs_lXScale = ((PersonalData->calX2px - PersonalData->calX1px) << 19) / ((PersonalData->calX2) - (PersonalData->calX1));
    gs_lYScale = ((PersonalData->calY2px - PersonalData->calY1px) << 19) / ((PersonalData->calY2) - (PersonalData->calY1));

    gs_lXOffset = ((PersonalData->calX1 + PersonalData->calX2) * gs_lXScale  - ((PersonalData->calX1px + PersonalData->calX2px) << 19) ) / 2;
    gs_lYOffset = ((PersonalData->calY1 + PersonalData->calY2) * gs_lYScale  - ((PersonalData->calY1px + PersonalData->calY2px) << 19) ) / 2;
    gs_bTouchInit = true;  
}

/**  
    외부에서 사용하는 함수  
        실제 Pixel 좌표와 Raw 좌표를 리턴  
*/  
touchPosition _touchReadXY()  
{
    touchPosition touchPos; 
    if(gs_bTouchInit==false)
    {
        REG_IME=0;
        while(1);
    }

    // 매크로를 사용하지 않고 그냥 hex 값으로 사용함
    touchPos.x = readTouchValue(0xD4 | 1, 2, 10);
    touchPos.y = readTouchValue(0x94 | 1, 2, 10);
    touchPos.z1 = readTouchValue(0xB4 | 1, 2, 30);
    touchPos.z2 = readTouchValue(0xC4 | 1, 2, 30);  

    // x의 값은 0이면 Release이지만 y는 0xfff이면 Release다. 따라서 처리 로직 추가  
    if( touchPos.y == 0xFFF )  
    {  
        touchPos.y = 0;  
    }  

    s16 px = ( touchPos.x * gs_lXScale - gs_lXOffset + gs_lXScale/2 ) >>19;
    s16 py = ( touchPos.y * gs_lYScale - gs_lYOffset + gs_lYScale/2 ) >>19;
    if ( px < 0) px = 0;
    if ( py < 0) py = 0;
    if ( px > (SCREEN_WIDTH -1)) px = SCREEN_WIDTH -1;
    if ( py > (SCREEN_HEIGHT -1)) py = SCREEN_HEIGHT -1;
    touchPos.px = px;
    touchPos.py = py;
    return touchPos;  
}

5.마치며...

실제 위의 코드를 적용하여 새로운 버전의 KKAMAGUI Notepad를 구현하였는데, 튀는 문제가 없어졌다. 앞으로 저 소스를 libfat 대신 이용해야 겠다.

6.첨부

179533__touch.h
0.00MB
180156__touch.c
0.01MB

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