2008. 4. 21. 23:04
     

01 따라하는 PSP 홈브루 개발 - Hello World 출력

원문 :http://kkamagui.springnote.com/pages/1075736

 

들어가기 전에...

 

0.시작하면서...

 이 글은 PSP 홈브루(Homebrew)를 Step by Step 형식으로 개발할 수 있도록 도와주는 문서이다. PSP SDK를 사용하는 방법만 단순히 설명하기보다 PSP의 전체적인 동작 방식과 흐름과 함께 설명하는데 목적을 두고 있다. 이번 회에서는 Hello World를 출력하는  간단한 홈브루를 구현해 볼 것이며, 더불이 PSP Kernel과 PSP 응용프로그램의 연결고리도 함께 살펴볼 것이다.

 

1.예제 소스코드

 아래는 처음으로 작성하는 "Hello World" 프로그램이다. 아래의 코드를 DevkitPSP로 컴파일 및 링크하면 EBOOT.PBP 파일을 얻을 수 있다. 이 파일을 메모리스틱에 PSP->GAME->TEST 폴더로 복사하면 PSP Home Menu의 Game 항목에서 볼 수 있다.

  1. #include <pspkernel.h>
    #include <pspdebug.h>

    // 매크로 정의
    #define TRUE 1
    #define FALSE 0
  2. #define printf pspDebugScreenPrintf

    /* Define the module info section */
    PSP_MODULE_INFO("KKAMAGUI", 0, 1, 1);

    /* Define the main thread's attribute value (optional) */
    PSP_MAIN_THREAD_ATTR(THREAD_ATTR_USER | THREAD_ATTR_VFPU);

    // 종료 Flag
    int g_bExit = FALSE;

    int ExitCallBack(int arg1, int arg2, void *common)
    {
        printf( "%X %X", arg1, arg2 );
        g_bExit = TRUE;
        return 0;
    }
     
    int main(int argc, char *argv[])
    {
        int cbid;

        pspDebugScreenInit();

        // Call Back 설정
        cbid = sceKernelCreateCallback("Exit Callback", ExitCallBack, NULL);
        sceKernelRegisterExitCallback(cbid);
       
        printf("Hello World\n");

        while( g_bExit == FALSE )
        {
            ;
        }
        sceKernelExitGame();
        return 0;
    }

 

2.예제 소스코드 분석

 그럼 지금부터 위의 소스를 하나하나 분석해보자. 우선 가장 처음에 나오는 PSP_MOUDLE_INFO() 매크로는 pspmoduleinfo.h 파일에 정의되어있으며, 아래와 같이 특수한 섹션(.lib.ent.XX, .lib.stub.XX)에 데이터를 삽입하도록 되어있다. 주석을 보면 Source 파일에 반드시 있어야 한다고하니 필수 요소인 듯 하며, 나중에 PSP Kernel이 Load할 때 참고하는 정보임을 추측할 수 있다. 그냥 필수적으로 타이핑해야 하는 부분이라고 생각하면 된다.

 
  1. // pspmoduleinfo.h
  2. /* Declare a module.  This must be specified in the source of a library or executable. */
    #define PSP_MODULE_INFO(name, attributes, major_version, minor_version) \
        __asm__ (                                                       \
        "    .set push\n"                                               \
        "    .section .lib.ent.top, \"a\", @progbits\n"                 \
        "    .align 2\n"                                                \
        "    .word 0\n"                                                 \
        "__lib_ent_top:\n"                                              \
        "    .section .lib.ent.btm, \"a\", @progbits\n"                 \
        "    .align 2\n"                                                \
        "__lib_ent_bottom:\n"                                           \
        "    .word 0\n"                                                 \
        "    .section .lib.stub.top, \"a\", @progbits\n"                \
        "    .align 2\n"                                                \
        "    .word 0\n"                                                 \
        "__lib_stub_top:\n"                                             \
        "    .section .lib.stub.btm, \"a\", @progbits\n"                \
        "    .align 2\n"                                                \
        "__lib_stub_bottom:\n"                                          \
        "    .word 0\n"                                                 \
        "    .set pop\n"                                                \
        "    .text\n"                                                    \
        );                                                              \
        extern char __lib_ent_top[], __lib_ent_bottom[];                \
        extern char __lib_stub_top[], __lib_stub_bottom[];              \
        SceModuleInfo module_info                                       \
            __attribute__((section(".rodata.sceModuleInfo"),        \
                       aligned(16), unused)) = {                \
          attributes, { minor_version, major_version }, name, 0, _gp,  \
          __lib_ent_top, __lib_ent_bottom,                              \
          __lib_stub_top, __lib_stub_bottom                             \
        }

 

 그 다음에 오는 PSP_MAIN_THREAD_ATTR() 매크로는 pspmoduleinfo.h에 정의되어있으며 변수에 값을 설정하는 간단한 역할만 한다. Optional한 부분이므로 굳이 쓰지 않아도 되나, 만약의 사태를 대비해서 일단 추가해 두자.

  1. // pspmoduleinfo.h 파일에 포함된 내용
  2. /* Define the main thread's attributes. */
    #define PSP_MAIN_THREAD_ATTR(attr) \
        unsigned int sce_newlib_attribute = (attr)

 Thread의 Attribute는 pspthreadman.h 파일에 있으며 아래와 같이 정의되어있다. PspThreadAttributes에 나와있는 주석을 보면 Thread의 종류에따라 Device에 접근할 수 있는 범위가 제한됨을 알 수 있다. 아직은 USB나 WLAN을 접근할 일이 없지만, 추후에 접근할 일이 생기면 Thread의 목적에 맞게 Type을 설정해야 하는 듯 하다. 이런 것도 있다는 것만 알고 넘어가자.

  1. /** Attribute for threads. */
    enum PspThreadAttributes
    {
        /** Enable VFPU access for the thread. */
        PSP_THREAD_ATTR_VFPU = 0x00004000,
        /** Start the thread in user mode (done automatically
          if the thread creating it is in user mode). */
        PSP_THREAD_ATTR_USER = 0x80000000,
        /** Thread is part of the USB/WLAN API. */
        PSP_THREAD_ATTR_USBWLAN = 0xa0000000,
        /** Thread is part of the VSH API. */
        PSP_THREAD_ATTR_VSH = 0xc0000000,
        /** Allow using scratchpad memory for a thread, NOT USABLE ON V1.0 */
        PSP_THREAD_ATTR_SCRATCH_SRAM = 0x00008000,
        /** Disables filling the stack with 0xFF on creation */
        PSP_THREAD_ATTR_NO_FILLSTACK = 0x00100000,
        /** Clear the stack when the thread is deleted */
        PSP_THREAD_ATTR_CLEAR_STACK = 0x00200000,
    };

 

다음은 ExitCallBack() 함수인데, 이 부분은 뒤에 sceKernelRegisterExitCallback() 함수를 설명하면서 같이 진행하기로 하고 main() 함수를 먼저 보자. main() 함수에서 제일 먼저 우리를 반기는 것은 pspDebugScreenInit() 함수다.  pspDebugScreenInit() 함수는 과연 무엇을 하는 함수일까? 함수가 psp라는 Prefix로 시작하는 것을 보아 PSP Library 함수임을 알 수 있으며 실제 함수의 구현부를 보면 아래와 같이 되어있다.

  1. // scr_printf.c 파일의 일부
  2. #define PSP_SCREEN_WIDTH 480
    #define PSP_SCREEN_HEIGHT 272
    #define PSP_LINE_SIZE 512
    #define PSP_PIXEL_FORMAT 3
  3. ... 생략 ...
  4. void pspDebugScreenInit()
    {
       X = Y = 0;
       /* Place vram in uncached memory */
       g_vram_base = (void *) (0x40000000 | (u32) sceGeEdramGetAddr());
       g_vram_offset = 0;
       sceDisplaySetMode(0, PSP_SCREEN_WIDTH, PSP_SCREEN_HEIGHT);
       sceDisplaySetFrameBuf((void *) g_vram_base, PSP_LINE_SIZE, PSP_PIXEL_FORMAT, 1);
       clear_screen(bg_col);
       init = 1;
    }

 코드에서 Display Mode를 408x272로 설정하고 Display할 데이터가 있는 주소 즉 Graphic Buffer를 FrameBuffer로 설정하는데, g_vram_base를 시작 주소로 설정하는 간단한 코드이다. PSP_LINE_SIZE는 확실치는 않지만 한 라인에 대응하는 Pixel의 수인 듯 하다. SCREEN_WIDTH가 480이고 PSP_LINE_SIZE가 512인 것으로 보아, 여유있게 잡았거나 Align하는 단위의 문제 때문에 좀 더 크게 잡은 것이 아닐까 추측한다. pspDebugScreenPrintf() 함수는 printf() 함수와 하는 역할이 거의 비슷하니 다음 함수로 넘어가겠다. 혹시 세부 구현이 궁금하다면 scr_printf.c 파일에 나와있으므로 관심있으면 참고하자.

 

 pspDebugScreenInit() 함수 아래를 자세히 보면 sceKernelXXX로 시작하는 함수를 볼 수 있다. 이 함수는 sceXXX Prefix로 시작되며, 이것은 PSP의 Kernel Service(Function)임을 나타낸다. sceXXX 로 시작하는 함수는 PSP Kernel로부터 Import 되는 함수로서, PSP Kernel이 프로그램을 로딩하면서 해당 영역에 실제 함수 주소를 연결해 주는 것 같다. 실제로 sceKernelCreateCallback() 함수의 정의를 따라가보면 구현은 없고 아래와 같은 Dummy Table만 볼 수 있다.

  1. // ThreadManForKernel.S 파일의 일부분
  2.     .set noreorder

    #include "pspimport.s"

    #ifdef F_ThreadManForKernel_0000
        IMPORT_START    "ThreadManForKernel",0x00010000
    #endif
    #ifdef F_ThreadManForKernel_0001
        IMPORT_FUNC    "ThreadManForKernel",0x0C106E53,sceKernelRegisterThreadEventHandler
    #endif
    #ifdef F_ThreadManForKernel_0002
        IMPORT_FUNC    "ThreadManForKernel",0x72F3C145,sceKernelReleaseThreadEventHandler
    #endif
    #ifdef F_ThreadManForKernel_0003
        IMPORT_FUNC    "ThreadManForKernel",0x369EEB6B,sceKernelReferThreadEventHandlerStatus
    #endif
    #ifdef F_ThreadManForKernel_0004
        IMPORT_FUNC    "ThreadManForKernel",0xE81CAF8F,sceKernelCreateCallback
    #endif
  3. ... 생략 ...

 IMPORT_FUNC는 pspimport.s 에 포함된 매크로로서 pspimport.s는 아래와 같이 어셈블리어 코드로 치환해주는 역할만 한다. 아래의 IMPORT_FUNC의 가운데 정도에 위치한 .ent 부분이 Loading 후에 실제로 Function Address로 치환되며, 이 위치의 Function Address를 사용하는 것으로 추측된다.


  1. .macro IMPORT_START module, flags_ver

        .set push
        .section .rodata.sceResident, "a"
        .word   0
    __stub_modulestr_\module:
        .asciz  "\module"
        .align  2

        .section .lib.stub, "a", @progbits
        .global __stub_module_\module
    __stub_module_\module:
        .word   __stub_modulestr_\module
        .word   \flags_ver
        .word   0x5
        .word   __executable_start
        .word   __executable_start

        .set pop
    .endm

    .macro IMPORT_FUNC module, funcid, funcname

        .set push
        .set noreorder

        .extern __stub_module_\module
        .section .sceStub.text, "ax", @progbits
        .globl  \funcname
        .type   \funcname, @function
        .ent    \funcname, 0
    \funcname:
        .word   __stub_module_\module
        .word   \funcid
        .end    \funcname
        .size   \funcname, .-\funcname

        .section .rodata.sceNid, "a"
        .word   \funcid

        .set pop
    .endm

    .macro IMPORT_FUNC_WITH_ALIAS module, funcid, funcname, alias

        .set push
        .set noreorder

        .extern __stub_module_\module
        .section .sceStub.text, "ax", @progbits
        .globl  \alias
        .type   \alias, @function
    \alias:
        .globl  \funcname
        .type   \funcname, @function
        .ent    \funcname, 0
    \funcname:
        .word   __stub_module_\module
        .word   \funcid
        .end    \funcname
        .size   \funcname, .-\funcname

        .section .rodata.sceNid, "a"
        .word   \funcid

        .set pop
    .endm

 

 예제의 하부에 있는 sceKernelCreateCallback() 함수는 CallBack Function의 Name과 Function Address, 그리고 Parameter를 받아서 Calback ID를 생성하여 반환한다. 이 반환된 ID를 등록하면 Callback 함수가 동작하는데, 테스트 프로그램에서는 sceKernelRegisterExitCallback() 함수를 사용해서 PSP의 Home 버튼과 연결하였다. sceKernelRegisterExitCallback() 함수는 Program이 끝났을 때, 즉 PSP의 Home 버튼을 눌러서 "예"를 선택했을 때 Callback을 불러주도록 등록하는 역할을 한다.

 Callback 함수의 원형은 아래와 같이 총 3개의 파라메터를 받도록 되어있으며, ExitCallBack() 함수의 생김새와 동일함을 알 수 있다.

  1. // pspthreadman.h 파일의 일부
  2. /** Callback function prototype */
    typedef int (*SceKernelCallbackFunction)(int arg1, int arg2, void *arg);

 ExitCallBack() 함수는  sceKernelRegisterExitCallback()에 의해 등록되었으므로,PSP의 Home 버튼을 눌렀을 때 XMB로 돌아가기를 선택하는 경우 불려진다. 혹시 Home 버튼을 눌렀을때 뜨는 선택 화면에서 호출되는 것이 아닌지 궁금해 할지도 몰라서, 테스트 해보기 위해 아래의 printf() 기능을 넣었다. 만약 Home 버튼을 누른 후 표시되는 메뉴에서 CallBack이 호출된다면 "아니오"를 선택하고 빠져나왔을때 화면에 무엇인가 출력되었어야 한다. 하지만 아무런 값이 출력되지 않은 것을 보아 메뉴에서 "예"를 누른 후 호출되는 것을 알 수 있다.

  1. int ExitCallBack(int arg1, int arg2, void *common)
    {
        printf( "%X %X", arg1, arg2 );
        //g_bExit = TRUE;
        return 0;
    }

 

 마지막 함수는 sceKernelExitGame() 함수이다. sce Prefix가 붙어있으므로 PSP Kenrel Service 임을 알 수 있으며, 이 함수를 호출하면 다시 PSP 메뉴(XMB)로 이동하게 된다. 이 함수를 ExitCallback()에서 부르게 되면 충돌이 발생하여 메뉴로 돌아가지 못하고 멈추므로 주의해야 한다.

 

3.컴파일 및 실행

 DevkitPro가 설치된 폴더의 PSP Sample Code 폴더에 보면 elf_template 폴더가 있다(D:\devkitPro\devkitPSP\psp\sdk\samples\template\elf_template). 그 폴더의 main.c 파일에 예제 소스를 덮어쓰고 cmd.exe를 실행하여 make를 입력하면 EBOO 파일이 생성될 것이다. 이 파일을 PSP 메모리스틱에 H:\PSP\GAME\Test 폴더에 넣은 후, PSP 메뉴의 게임->Memory Stick에 가면 실행할 수 있다. 화면 우측 상단에 하얀색으로 빛나는 멋진 Hello World가 표시될 것이다. @0@)/~

 

 

4.마치면서...

 이것으로 Hello World를 화면에 출력하는 PSP 홈브루(Homebrew)를 완성하였다. 첫시간이니만큼 API의 사용법보다는 전체적인 소스의 구성 및 구조에 더 치중하려고 노력하였다. 다음은 PSP의 키입력 처리에 대해서 알아보도록 하자.

 

 

이 글은 스프링노트에서 작성되었습니다.


Android App

Posted by 호기심 많은 kkamagui(까마귀, 한승훈)

댓글을 달아 주세요

  1. Favicon of http://www.filewiki.net BlogIcon 김상규 2008.04.22 01:18  댓글주소  수정/삭제  댓글쓰기

    크 역시 횽은 존내 대단해염 ㅠ_ㅠ
    감동의 도가니탕

    • Favicon of http://kkamagui.tistory.com BlogIcon kkamagui 2008.04.22 09:14  댓글주소  수정/삭제

      뭘 또 도가니탕이여 ㅜ_ㅜ...
      글고보니 예제 프로그램 잘못된 거 있네. 오늘가서 고쳐야겠어 ㅠㅠ

  2. Favicon of http://www.hizstory.net BlogIcon 마로 2008.04.22 09:54  댓글주소  수정/삭제  댓글쓰기

    오오.. 정말 멋지군요.. 뭘 하셔도 이렇게 뚝딱뚝딱 해치우시는 능력에 감탄스럽습니다 :)

  3. Favicon of http://grampus.tistory.com BlogIcon grampus 2008.04.22 16:11  댓글주소  수정/삭제  댓글쓰기

    캬~역쉬~쵝오~~

  4. Favicon of http://www.darkhorse.pe.kr BlogIcon darkhorse 2008.04.25 16:12  댓글주소  수정/삭제  댓글쓰기

    안녕하세요 덕분에 홈브류 혼자 만들어 볼 수 있게 됐어요. 감사합니다.

  5. 애구애구 2008.04.30 11:23  댓글주소  수정/삭제  댓글쓰기

    kkamagui님 덕분에 HelloWorld를 제 psp에 새겨봤네요.. 감사땡큐요 ^^
    근데 make를 실행해보니 TRUE가 선언이 어디서인가 안되서인지 에러가 나더군요
    그래서 TRUE를 0 으로 바꾸었더니 에러가 없었습니다..
    다른분들은 이런 에러 없으셨는지 궁금합니다..^^

  6. 애구애구 2008.04.30 11:34  댓글주소  수정/삭제  댓글쓰기

    음 또 있네요
    HOME키로 XMB메뉴로 빠져 나가기 시도하면 '잠시 기다려 주십시오' 이러고 HANG이 걸리네요
    베터리 빼기 전까지는 아무것도 안되네요.. TRUE를 0으로 바꾸어서 그런거 같아요

    • Favicon of http://kkamagui.tistory.com BlogIcon kkamagui 2008.04.30 22:43  댓글주소  수정/삭제

      헛~ 정말 그런 문제가 있군요. 조만간 해결해서 올리겠습니다. ^^;;;;
      그리고 Home 키를 눌러서 XMB로 돌아가지지 않을 때는 전원 버튼을 위로 한동안 올리고 있으면 꺼진답니다. ^^

  7. ⓒⓚHacker™ 2008.08.15 17:08  댓글주소  수정/삭제  댓글쓰기

    PSP 홈브류개발을 해보고싶었는데 한국어로 된 문서가 거의 없어
    힘들어하고 있었는데 kkamagui님 덕분에 많은것을 배우고 갑니다.
    감사합니다^^

  8. tseug 2009.12.13 20:23  댓글주소  수정/삭제  댓글쓰기

    정펌이라 좌절을 했으나.. 홈브류제작을 시도하고 있는 한 유저입니다.^^:
    현제 장난삼아 콘솔 게임을 제작하고 있는데...
    혹시, dvkitPro 에 정의된 psp 관련 함수 및 예제를 볼 수 있는 사이트 없을까요?..
    의미를 모르겠는 함수가 몇개 있어서 그렇습니다...;
    감사합니다.^^:

  9. a 2010.03.02 20:38  댓글주소  수정/삭제  댓글쓰기

    헬로월드 뿌리는데 참 어렵게도 설명하셨네요. 원래 자잘하고 없어도 되는 optional한 것들은 이런 처음 강의에는 설명하는게 아닙니다. 라이브러리를 선언된 파일까지 찾아가서 설명하고있으니 뭐 별로 모르는 사람들은 중간쯤보다 때려칠만하게 구성된 강의네요

    • Favicon of http://www.mint64os.pe.kr BlogIcon kkamagui 2010.03.28 01:16  댓글주소  수정/삭제

      죄송합니다. ㅠㅠ
      제가 필요해서 남긴 예제라 너무 자세한 내용까지 적어놨군요. ㅎㅎ
      나중에 업데이트 한번 하겠습니다.