키보드를 만듭시다. 어때요~ 참 쉽죠? (3)

나빌레라의 이미지

나빌로스 멀티 플랫폼 설계를 반만 생각해서 삽질한 이야기. Cortex-m3에 나빌로스를 포팅할 줄은 몰랐던 이야기.

키보드 펌웨어에 RTOS가 필수는 아니다. 하지만 내가 만든 RTOS(나빌로스: https://github.com/navilera/Navilos)가 이미 있으니 한번 실전 경험을 해야 하지 않겠는가하는 생각이 들었다. 그래서 직전에 만든 베이스 코드를 나빌로스에 포팅하기로 결정했다. 나빌로스를 만들 때부터 멀티 플랫폼을 고려했다. 그런데 미쳐 생각치 못했던게 있었다. 멀티 플랫폼을 고려하긴 했는데 Cortex-AR만 고려한 멀티 플랫폼 설계였다. Cortex-M 계열은 아예 생각조차 안했다. 이제부터 그 얘기를 시작한다.

첫 번째 난관, HAL

나는 늘 HAL까지 자체 제작하는 프로젝트만 경험했다. 그래서 나빌로스 HAL은 제조사 HAL을 가져다 쓰는 상황을 고려하지 않았다. 나의 실책이다. 실제로 제조사 HAL을 가져다 쓰는 상황이 더 많을 텐데 그걸 고려하지 않은 것이다. 그러나 나빌로스 커널은 공용 HAL API만 사용하기 때문에 디자인 조정이 필요했다. 제조사 HAL을 그대로 쓰려면 기능이 중복되는 나빌로스 공용 HAL API 인터페이스에서 제조사 HAL로 bypass하는 코드를 작성해야 한다. 그저 인터페이스만 맞추는 레이어가 되 버리는 셈이다. 나빌로스 공용 HAL API를 제대로 사용하려면 제조사 HAL 코드를 분석해서 나빌로스 HAL API에 맞춰 코드를 재작성해야 한다. 쉽게 만들려고 작정했는데 HAL을 재작성하는 일은 심각하게 귀찮은 일이다. 그래서 다소 마음에 들지는 않지만 제조사 HAL을 그대로 사용하고 나빌로스 커널에서 사용하는 공용 HAL API만 만들기로 결정했다. 이게 만약 회사 프로젝트였다면 설계 원칙을 깨는 이런 판단을 절대 용납하지 않았을 테지만 취미 아닌가? 즐거운것이 중요하다.

설계 원칙을 깨서 그런지 실제 HAL을 호출하는 코드는 제조사 HAL을 직접 호출하는 코드와 나빌로스 공용 HAL API를 호출하는 코드가 섞여 버렸다. 글을 쓰고 있는 지금도 마음에 들지 않는다. 나중에 기회되면 꼭 정리해야지.

원래 나빌로스 HAL 디렉토리 구조는 아래와 같다. rvpb는 RealViewPlatformBaseboard로 예제로 만든 QEMU 하드웨어다. 책에 넣을 예제로 작성한 것이라 UART, 인터럽트, 타이머만 있다. 내 원래 설계는 이런식으로 하드웨어가 추가로 필요하면 공용 인터페이스를 새로 정의하고 구현 코드를 개별 플랫폼 디렉터리에 배치하는 것이었다.

           hal
      │   ├── HalInterrupt.h
      │   ├── HalTimer.h
      │   ├── HalUart.h
      │   └── rvpb
      │       ├── Interrupt.c
      │       ├── Interrupt.h
      │       ├── Regs.c
      │       ├── Timer.c
      │       ├── Timer.h
      │       ├── Uart.c
      │       └── Uart.h
 

그러나 STM32F103 제조사 HAL을 그대로 쓰기로 결정하면서 디렉터리 구조를 아래처럼 고칠 수 밖에 없었다.

    ├── hal
    │   ├── stm32f103
    │   │   ├── drivers
    │   │   │   ├── stm32f103xb.h
    │   │   │   ├── stm32f1xx.h
    │   │   │   ├── stm32f1xx_hal.c
    │   │   │   ├── stm32f1xx_hal_conf.h
    │   │   │   ├── stm32f1xx_hal_cortex.c
    │   │   │   ├── stm32f1xx_hal_cortex.h
    │   │   │   ├── stm32f1xx_hal_def.h
    │   │   │   ├── stm32f1xx_hal_dma.c
    │   │   │   ├── stm32f1xx_hal_dma_ex.h
    │   │   │   ├── stm32f1xx_hal_dma.h
    │   │   │   ├── stm32f1xx_hal_flash.c
    │   │   │   ├── stm32f1xx_hal_flash_ex.c
    │   │   │   ├── stm32f1xx_hal_flash_ex.h
    │   │   │   ├── stm32f1xx_hal_flash.h
    │   │   │   ├── stm32f1xx_hal_gpio.c
    │   │   │   ├── stm32f1xx_hal_gpio_ex.c
    │   │   │   ├── stm32f1xx_hal_gpio_ex.h
    │   │   │   ├── stm32f1xx_hal_gpio.h
    │   │   │   ├── stm32f1xx_hal.h
    │   │   │   ├── stm32f1xx_hal_msp.c
    │   │   │   ├── stm32f1xx_hal_pcd.c
    │   │   │   ├── stm32f1xx_hal_pcd_ex.c
    │   │   │   ├── stm32f1xx_hal_pcd_ex.h
    │   │   │   ├── stm32f1xx_hal_pcd.h
    │   │   │   ├── stm32f1xx_hal_pwr.c
    │   │   │   ├── stm32f1xx_hal_pwr.h
    │   │   │   ├── stm32f1xx_hal_rcc.c
    │   │   │   ├── stm32f1xx_hal_rcc_ex.c
    │   │   │   ├── stm32f1xx_hal_rcc_ex.h
    │   │   │   ├── stm32f1xx_hal_rcc.h
    │   │   │   ├── stm32f1xx_hal_tim.c
    │   │   │   ├── stm32f1xx_hal_tim_ex.c
    │   │   │   ├── stm32f1xx_hal_tim_ex.h
    │   │   │   ├── stm32f1xx_hal_tim.h
    │   │   │   ├── stm32f1xx_hal_uart.c
    │   │   │   ├── stm32f1xx_hal_uart.h
    │   │   │   ├── stm32f1xx_it.c
    │   │   │   ├── stm32f1xx_it.h
    │   │   │   ├── stm32f1xx_ll_usb.c
    │   │   │   ├── stm32f1xx_ll_usb.h
    │   │   │   ├── stm32_hal_legacy.h
    │   │   │   ├── system_stm32f1xx.c
    │   │   │   └── system_stm32f1xx.h
    │   │   ├── Gpio.c
    │   │   ├── Interrupt.c
    │   │   ├── MemoryMap.h
    │   │   ├── Timer.c
    │   │   └── Uart.c
    │   ├── HalGpio.h
    │   ├── HalInterrupt.h
    │   ├── HalTimer.h
    │   └── HalUart.h

hal 밑에 stm32f103 디렉터리를 새로 만들었다. 그리고 그 밑에 drivers 디렉터리를 만들어 그 안에 제조사 제공 HAL 코드를 넣었다. stm32f1xx_hal로 시작하는 파일들이 모두 제조사에서 배포하는 HAL 코드다. 내가 구상했던 원래 디자인을 살린다면 아마 이런 구조여야 했을 거다.

           hal
      │   ├── HalInterrupt.h
      │   ├── HalTimer.h
      │   ├── HalUart.h
      │   ├── HalGpio.h
      │   ├── HalUsb.h
      │   ├── HalFlash.h
      │   ├── HalSystem.h
      │   └── stm32f103
      │       ├── Interrupt.c
      │       ├── Interrupt.h
      │       ├── Regs.c
      │       ├── Timer.c
      │       ├── Timer.h
      │       ├── Gpio.c
      │       ├── Gpio.h
      │       ├── Usb.c
      │       ├── Usb.h
      │       ├── Flash.c
      │       ├── Flash.h
      │       ├── System.c
      │       ├── System.h
      │       ├── Uart.c
      │       └── Uart.h
 

그러나 HAL을 다시 만들고 싶지 않았기에, 그냥 아래에 레이어를 하나 더 깔아 버리는 것으로 해결했다. 이놈의 귀찮음이란.. 그래서 정작 공용 HAL API 구현은 모두 bypass로 작성했다. 이렇게.

    void Hal_uart_put_char(uint8_t ch)
    {
        HAL_UART_Transmit(&huart1, &ch, 1, 1000);
    }
 
    uint8_t Hal_uart_get_char(void)
    {
        uint8_t ch;
        HAL_UART_Receive(&huart1, &ch, 1, 1000);
        return ch;
    }
 

매우 마음에 들지 않지만, 그냥 두기로 했다. 일단 잘 동작하는 펌웨어를 만드는게 우선이니까.

두 번째 난관, 어라 Cortex-M은 컨택스트 스위칭이 다르네..

HAL이야 처음부터 멀티 플랫폼을 고려했다 쳐도, 나빌로스 커널 코드에 속하는 태스크 컨택스트 스위칭은 아예 멀티 플랫폼을 고려하지 않았다. 왜냐면 멀티 플랫폼이라 할 지라도 다 같은 ARM을 쓰지, ARM이 아닌 시스템에 나빌로스를 쓸 생각은 없었으니까. 뭐 맞는말이다. 내가 AVR이나 80x86이라면 아예 나빌로스를 쓸 생각을 안했을테니. 그런데 Cortex-M은 ARM이다. 그래서 아무 생각없이 포팅 작업을 시작했다가 난관을 만났다. 이런... Cortex-M은 스택 포인터와 상태 레지스터, 동작모드 등 몇가지가 Cortex-AR과 달랐다. 그래서 Cortex-AR 기준으로 작성한 나빌로스 커널의 컨택스트 스위칭 코드는 Cortex-M에서 동작하지 않는다. 이것을 빌드할 때 컴파일 에러를 보고서야 깨달았다.

예를 들어 IRQ를 켜고 끄는 코드를 보면 Cortex-AR은 코드가 아래와 같다.

    void enable_irq(void)
    {
        __asm__ ("PUSH {r0, r1}");
        __asm__ ("MRS  r0, cpsr");
        __asm__ ("BIC  r1, r0, #0x80");
        __asm__ ("MSR  cpsr, r1");
        __asm__ ("POP {r0, r1}");
    }
    void disable_irq(void)
    {
        __asm__ ("PUSH {r0, r1}");
        __asm__ ("MRS  r0, cpsr");
        __asm__ ("ORR  r1, r0, #0x80");
        __asm__ ("MSR  cpsr, r1");
        __asm__ ("POP {r0, r1}");
    }
 

그러나 Cortex-M은 제어하는 레지스터 이름이 다르다. 이름이 다르니까 제어하는 코드도 다르다.

    void enable_irq(void)
    {
        __asm__ ("cpsie i");
    }
    void disable_irq(void)
    {
        __asm__ ("cpsid i");
    }
 

IRQ를 제어하는 방법 자체는 더 단순하다. 단순해서 좋고 나쁘고를 떠나서 일단 Cortex-AR과 다르다는 것이 중요하다. 다르기 때문에 어떤 방식으로든 코드를 플랫폼에 따라 분리해야 한다.

게다가 Cortex-M은 FIQ가 없으므로 만약 어디선가 FIQ를 사용한다면 찾아서 고쳐야 한다. 다행히 나빌로스는 FIQ를 사용하지 않는다.

여기까지는 애교다. 진짜 문제는 태스크 컨택스트 스위칭 코드다. Cortex-AR만 고려한 설계덕에 컨텍스트 스위칭 코드가 플랫폼 별로 다를 것이라는 가정을 하지 않아서 컨택스트 스위칭 코드가 커널 코드안에 있는 것이 문제다. 그래서 우선 컨택스트 스위칭 코드를 분리해야 했다.

Cortex-AR 용 컨택스트 스위칭 코드는 아래와 같다.

    __attribute__ ((naked)) void Kernel_task_context_switching(void)
    {
        __asm__ ("B Save_context");
        __asm__ ("B Restore_context");
    }
 
    static __attribute__ ((naked)) void Save_context(void)
    {
        // save current task context into the current task stack
        __asm__ ("PUSH {lr}");
        __asm__ ("PUSH {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}");
        __asm__ ("MRS   r0, cpsr");
        __asm__ ("PUSH {r0}");
        // save current task stack pointer into the current TCB
        __asm__ ("LDR   r0, =sCurrent_tcb");
        __asm__ ("LDR   r0, [r0]");
        __asm__ ("STMIA r0!, {sp}");
    }
 
    static __attribute__ ((naked)) void Restore_context(void)
    {
        // restore next task stack pointer from the next TCB
        __asm__ ("LDR   r0, =sNext_tcb");
        __asm__ ("LDR   r0, [r0]");
        __asm__ ("LDMIA r0!, {sp}");
        // restore next task context from the next task stack
        __asm__ ("POP  {r0}");
        __asm__ ("MSR   cpsr, r0");
        __asm__ ("POP  {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}");
        __asm__ ("POP  {pc}");
    }

나는 나빌로스의 스케줄링을 비선점형으로 만들었다. 그래서 컨택스트 스위칭 코드에서 인터럽트를 호출하지 않는다. 현재 태스크 스택에 컨택스트를 백업한다. 그리고 스케줄링 받을 태스크의 스택 포인터를 커널 TCB에서 받아온다. 스택 포인터 레지스터를 갱신한 다음 스택에서 컨택스트를 복구해서 컨택스트 스위칭을 완료한다. 이 코드는 커널 소스 코드의 task.c 파일에 작성했는데 이 것을 적당한 위치로 바꿔야 했다. 직관적으로 HAL에 위치하는 것이 옳으나, 같은 아키텍처에 다른 SoC 혹은 다른 개발 보드로 HAL을 구분하기로 했으므로 아키텍처 구분은 HAL이 아닌 다른 곳에 두는 것이 맞다고 생각했다. 그래서 lib 디렉터리 밑에 cortexM과 cortexAR을 만들고 여기에 각각 컨택스트 스위칭 코드와 기타 아키텍처 의존적인 코드를 넣는 것으로 결정했다.

    lib
    ├── cortexM
    │   ├── armcpu.c
    │   ├── armcpu.h
    │   ├── switch.c
    │   └── switch.h
    ├── cortexAR
    │   ├── armcpu.c
    │   ├── armcpu.h
    │   ├── switch.c
    │   └── switch.h
    ├── stdio.c
    ├── stdio.h
    ├── stdlib.c
    └── stdlib.h
 

이 디자인 변경은 마음에 들었다. 실전을 치루면서 진화하는 느낌이랄까. 중요하진 않지만 내가 구현한 Cortex-M 용 나빌로스 컨택스트 스위칭 코드는 아래와 같다.

    __attribute__ ((naked)) void Arch_context_switching(void)
    {
        __asm__ volatile ("B Arch_Save_context");
        __asm__ volatile ("B Arch_Restore_context");
    }
 
    __attribute__ ((naked)) void Arch_Save_context(void)
    {
        // save current task context into the current task stack
        __asm__ ("PUSH {lr}");
        __asm__ ("PUSH {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}");
        __asm__ ("MRS  r0, PSR");
        __asm__ ("PUSH {r0}");
        // save current task stack pointer into the current TCB
        __asm__ ("LDR   r0, =gCurrent_tcb");
        __asm__ ("LDR   r0, [r0]");
        __asm__ ("MRS      r1, PSP");
        __asm__ ("STMIA r0!,{r1}");
    }
 
    __attribute__ ((naked)) void Arch_Restore_context(void)
    {
        // restore next task stack pointer from the next TCB
        __asm__ ("LDR   r0, =gNext_tcb");
        __asm__ ("LDR   r0, [r0]");
        __asm__ ("LDMIA r0!,{r1}");
        __asm__ ("MSR      PSP,r1");
        // restore next task context from the next task stack
        __asm__ ("POP  {r0}");
        __asm__ ("MSR  PSR_nzcvq, r0");
        __asm__ ("POP  {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}");
        __asm__ ("POP  {pc}");
    }

전체적인 구조는 Cortex-AR 용 컨택스트 스위칭 코드와 거의 같다. 다만 프로그램 상태 레지스터를 사용하는 방법이 Cortex-AR과 M이 다르다. 기본 동작은 동일하다. 스택에 백업하고 스택 포인터 바꾸고 스택에서 복구하고...

그래서 아래처럼 두 단계였던 호출 구조를

태스크 -> 커널 (task.c)

이렇게 세 단계로 바꾸었다.

태스트 -> 커널 (task.c) -> 아키텍처 (lib/arch 이름/switch.c)

이러면 논리적으로는 나빌로스를 x86에도 포팅할 수 있다. 적어도 디자인적으로는.

boot와 build 분리

어셈블리어 코드를 최소화 하기 위해서 ARM 벡터 테이블과 main() 함수를 작성해 놓은 main.c를 boot 디렉터리에 넣어 놨다. 그리고 makefile과 링커 스크립트는 따로 디렉터리를 만들지 않고 그냥 소스 트리 최상위 디렉터리에 넣어 놨었다.

    ├── boot
    │   ├── Entry.S
    │   ├── Handler.c
    │   └── Main.c
    ├── Makefile
    ├── navilos.ld

원래 이런 구조였다. Entry.S에 벡터 테이블이, Handler.c에 IRQ 핸들러들이, Main.c에 main() 함수와 샘플 태스크 함수 구현이 있다. 이 구조를 아래처럼 바꿨다.

    ├── boot
    │   ├── stm32f103
    │   |   ├── main.c
    │   |   └── startup_stm32f103xb.S
    │   └── rvpb
    │       ├── main.c
    │       └── Entry.S
    │       └── Handler.c
    ├── build
    │   ├── stm32f103
    │   |   ├── Makefile
    │   |   └── stm32f10c8.ld
    │   └── rvpb
    │       ├── Makefile
    │       └── navilos.ld
 

makefile을 수정해서 개발 보드 구성에 맞는 파일을 찾아 빌드하도록 만들면 된다. boot와 build 밑에 있는 stm32f10c8.ld와 startup_stm32f103xb.S 파일은 SDK에 있는 파일을 그대로 사용한다.

키보드 펌웨어 만들다가 나빌로스 개선

키보드 펌웨어에 나빌로스를 적용한 것은 정말 잘한 선택이었다. 덕분에 나빌로스를 개선할 수 있었다. 또한 나빌로스가 실전에서도 쓸만한 RTOS라는것을 직접 검증할 수 있었다.

댓글

궁금해요~의 이미지

안녕하세요. 블로그/책을 보면서 공부하다가 궁금한 것이 있는데요..

stm32f103으로 변환하면서 해당 코드가 왜 이렇게 변한지 이해가 안되는데 설명해주실 수 있을까요>>?? ㅠ

    static __attribute__ ((naked)) void Restore_context(void)
    {
        // restore next task stack pointer from the next TCB
        __asm__ ("LDR   r0, =sNext_tcb");
        __asm__ ("LDR   r0, [r0]");
        __asm__ ("LDMIA r0!, {sp}");

위의 코드가 아래 처럼 왜 바뀌나요 ?

    static __attribute__ ((naked)) void Restore_context(void)
    {
        // restore next task stack pointer from the next TCB
        __asm__ ("LDR   r0, =gNext_tcb");
        __asm__ ("LDR   r0, [r0]");
        __asm__ ("LDMIA r0!,{r1}");
        __asm__ ("MSR      PSP,r1");
나빌레라의 이미지

ARM Cortex-AR 아키텍처와 Cortex-M 아키텍처가 다르기 때문입니다. Cortex-M 의 컨택스트 스위칭 관련 내용을 검색해서 공부해 보고 코드와 비교해 보세요. 그러면 이해 될겁니다.

----------------------
얇은 사 하이얀 고깔은 고이 접어서 나빌레라

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.