vpos 를 gcc-3.3.4 로 포팅하는 방법과 포팅할 때 고려해야할 것들에 대해서 설명한다.
기본적으로 vpos 는 2.95.3 에서 컴파일 되었고, 그 상위 컴파일러로 컴파일 시에 어셈 파일(.S) 에서 문법 오류가 발생했다. 문법오류를 고치더라도, 쓰레드를 생성하는 도중에 시스템이 죽어버리는 문제가 있었다.
이번에 gcc-3.3.4 로 포팅하면서, 컴파일러에 대한 특성과 이에 대한 문제점들에 대해 알 수 있다.
현재 새롭게 포팅한 vpos 로 divx 까지 확인한 상태다.
먼저 포팅을 했던 환경 설정에 대해서 설명하겠다.

준비 운동 하기

컴파일러는 http://www.falinux.com FALINUX 와 http://www.aesop-embedded.org AESOP 에서 제공하는 컴파일러를 사용했다. 둘다 같은 gcc 버전을 가지고 있으면서, 둘 중에 어느 것으로 컴파일해도 문제가 없다. 각 툴체인에 대한 설치와 설정은 생략하기로 한다.
포팅할 vpos 소스코드는 나중에 테스트를 위해 최근 버전인 divx 가 포함된 것으로 한다. 이제부터는 포팅하면서 수정한 사항들을 중점으로 설명하겠다.

수정 사항

vpos 의 압축을 풀고 컴파일을 한다.

#cd /opt
#tar xzf vpos.tar.gz
#cd vpos
#make

아래와 같은 에러가 발생한다.

arm-linux-gcc    -c -o HAL_arch_startup.o HAL_arch_startup.S
HAL_arch_startup.S: Assembler messages:
HAL_arch_startup.S:118: Error: invalid pseudo operation -- `str sp,=vk_save_swi_mode_stack_ptr'
HAL_arch_startup.S:119: Warning: writeback of base register is UNPREDICTABLE
HAL_arch_startup.S:125: Error: invalid pseudo operation -- `str sp,=vk_save_swi_current_tcb_bottom'
HAL_arch_startup.S:132: Warning: writeback of base register is UNPREDICTABLE
HAL_arch_startup.S:138: Error: invalid pseudo operation -- `str sp,=vk_save_irq_mode_stack_ptr'
HAL_arch_startup.S:139: Warning: writeback of base register is UNPREDICTABLE
HAL_arch_startup.S:142: Error: invalid pseudo operation -- `str sp,=vk_save_irq_current_tcb_bottom'
HAL_arch_startup.S:148: Warning: writeback of base register is UNPREDICTABLE
HAL_arch_startup.S:159: Warning: writeback of base register is UNPREDICTABLE
make[1]: *** [HAL_arch_startup.o] Error 1
make[1]: Leaving directory `/opt/vpos/hal/cpu'
make: *** [all] Error 2

HAL_arch_startup.S 파일을 아래와 같이 수정한다. 여기서 숫자는 라인넘버를 뜻한다.

vh_software_interrupt:
vh_entering_swi:
  str sp, vk_save_swi_mode_stack_ptr     // 수정
  stmfd sp!, {r0-r14}^
  mrs r0, spsr_all
  stmfd sp!, {r0, lr}
  mrs   r0, cpsr_all
  orr r0, r0, #0x80
  msr cpsr_all, r0
  str sp, vk_save_swi_current_tcb_bottom      // 수정
  ldr r0, [sp, #8]
  bl  vk_swi_classifier
 
vh_leaving_swi:
  ldmfd sp!, {r0, lr}
//  msr spsr_all, r0
  ldmfd sp!, {r0-r14}^
  movs  pc, lr
 
vh_irq:
vh_entering_interrupt:
  sub lr, lr, #4
  str sp, vk_save_irq_mode_stack_ptr    // 수정
  stmfd sp!, {r0-r14}^
  mrs r0, spsr_all
  stmfd sp!, {r0, lr}
  str sp, vk_save_irq_current_tcb_bottom     // 수정
  bl  vh_hwi_classifier
 
vh_leaving_interrupt:
  ldmfd sp!, {r0, lr}
  msr spsr_all, r0
  ldmfd sp!, {r0-r14}^
  movs  pc, lr
 
vh_restore_thread_ctx:
  mrs r1, cpsr_all
  bic r1, r1, #0xfffffff0
        mov r2, #0x2
  cmps     r1, r2
  mov sp, r0
  ldmfd sp!, {r0, lr}
  msr spsr_all, r0
  ldmfd sp!, {r0-r14}^
  ldreq sp, vk_save_irq_mode_stack_ptr    // 수정
  ldrne sp, vk_save_swi_mode_stack_ptr    // 수정
  movs  pc, lr
 
vh_save_thread_ctx:
  mrs r1, cpsr_all
  bic r1, r1, #0xfffffff0
        mov r2, #0x2
  cmps     r1, r2
        ldreq r2, vk_save_irq_current_tcb_bottom     // 수정
  ldrne r2, vk_save_swi_current_tcb_bottom          // 수정
  .rept 17
  ldr r1, [r2], #4
  str r1, [r0], #4  .endr
  mov pc, lr
 
vk_save_swi_mode_stack_ptr:      ------ 1
        .long 0
vk_save_swi_current_tcb_bottom:    ------ 2
        .long 0
vk_save_irq_mode_stack_ptr:       ------ 3
        .long 0
vk_save_irq_current_tcb_bottom:   -------- 4
        .long 0

위의 수정한 코드를 보면, 기본의 코드에서 모두 '=' 를 제거한 것을 알 수 있다.

왜 저렇게 코드를 수정하는지 궁금할 것이다.
예전의 경우, 컴파일 에러가 발생하는 부분의 '=' 만 제거를 했기 때문에 결과적으로 컨텍스트 스위칭이 제대로 일어나지 않아서 abort 가 발생하는 문제가 있었다.
코드의 아래 부분에 보이는 1 ~ 4 까지는 컨텍스트 스위칭이 발생해서 진입한 모드에서의 가장 처음 sp 와 가장 마지막으로 저장한 tcb 의 주소를 저장하는 곳이다. 이 주소에 저장된 값들은 컨텍스트 스위칭이 끝나고 돌아오거나 다시 재진입할 때, 이 값을 읽어들어서 저장하거나 복구한다. 따라서 이 값들이 제대로 설정되어 있지 않으면 컨텍스트 스위칭에 문제가 생기게 된다. 이것은 t32 같은 툴들을 통해서 디스어셈블 해서 보는 것이 훨씬 이해하기 편하다. 나중에 수정할 때를 대비해서 아래의 표를 추가한다. 총 4 개의 주소를 사용한다.

vh_entering_swi 0x30000204
0x30000208
vh_entering_interrupt 0x3000020c
0x30000210
vh_restore_thread_ctx 0x3000020c
0x300000204
vh_save_thread_ctx 0x30000210
0x300000208

T32 로 본 각 주소 영역에 정보이다.

addr/line code label mnemonic
SR:30000204 00000000 vk_save_swi_mode_stack_ptr andeq r0,r0,r0
SR:30000208 00000000 vk_save_swi_current_tcb_bottom andeq r0,r0,r0
SR:3000020c 00000000 vk_save_irq_mode_stack_ptr andeq r0,r0,r0
SR:30000210 00000000 vk_save_irq_current_tcb_bottom andeq r0,r0,r0

SCV/OS 디버깅일지 에서 언급했듯이, 나는 처음에 저런 방식으로 꽁수(?)를 사용해서 컨텍스트 스위칭을 하게끔 했다. 하지만 여러가지 문제들이 발견되었다.

  1. printf 문이 t32 에서 실행시에 문자가 깨진다는 점
  2. vpos_shell 에서 입력문자를 계속해서 출력한다는 점(디버거로 확인결과, uart 루틴이 예상과는 다르게 동작하고 있었음)
  3. 이외 등등(data abort 발생)

내가 생각해볼때, 미묘한 타이밍 문제인 듯 보였다. 내가 추가한 어셈코드가 기존의 코드보다 몇 라인 더 많았기도 했기 때문에 이에 걸리는 오버헤드 때문인 듯 하다. 내가 생각하는 대로 적어보면, 다음과 같다.

  1. 컨텍스트 스위칭에 사용하는 타이머의 값 조절
  2. r3 레지스터의 사용으로 인한 문제

메모리를 할당하는 루틴에서 수정해야할 부분이 있다. 2.95.3 버전에서는 전역 변수가 0 으로 초기화 되지만, 3.4.4 버전에서는 0xffffffff 으로 초기화 된다.

 void* vk_malloc(unsigned nbytes)
 {
        Header *p, *prevp;
        Header *heap_memory_alloc(void);
        unsigned nunits;
        nunits = (nbytes+sizeof(Header)-1)/sizeof(Header) + 1;
 
         if ((prevp = freep) == 0xffffffff) {   // 수정
            /* no free list yet */
            base.s.ptr = freep = prevp = &base;
            base.s.size = 0;
        }
        for (p = prevp->s.ptr; ; prevp = p, p = p->s.ptr) {
            if (p->s.size >= nunits) {  /* big enough */
                if (p->s.size == nunits)  /* exactly */
                    prevp->s.ptr = p->s.ptr;
                else {              /* allocate tail end */
                    p->s.size -= nunits;
                    p += p->s.size;
                    p->s.size = nunits;
                }
                freep = prevp;
                return (void *)(p+1);
            }
            if (p == freep)  /* wrapped around free list */
                if ((p = heap_memory_alloc()) == NULL)
            return NULL;    /* none left */
        }
 }

아직 이유가 밝혀지진 않았지만, 전역변수를 선언할 때 0 으로 초기화를 시켜도 0xffffffff 으로 초기화되는 현상 때문에, 코드를 수정했다.

flash write 에 제대로 안되는 문제의 원인은 바로 전역변수가 0xffffffff 으로 초기화되는 문제였다. 그래서 역시 flash_write.c 에 있는 모든 targetOffset 을 모두 지운다.

#include "stdio.h"
#include "stdlib.h"
#include "printf.h"
#include "pthread.h"
#include "swi_handler.h"
#include "debug.h"
#include "linux/string.h"
#include "linux/delay.h"
#include "serial.h"
#include "vh_cpu_hal.h"
#include "vh_io_hal.h"
 
// Flash
#define _WR(addr,data)	*((volatile unsigned int *)(addr))=(unsigned int)data 
#define _RD(addr)		( *((volatile unsigned int *)(addr)) )       
#define _RESET()		_WR(targetAddress,0x00ff00ff)
 
#define BIT_ALLMSK    (0xffffffff)
#define rUTRSTAT0   (*(volatile unsigned *)0x50000010) //UART 0 Tx/Rx status
#define RdURXH0()   (*(volatile unsigned char *)0x50000024)
 
////////////////////////////////////////////////////////////////////////////
static int  Strata_ProgFlash(unsigned int realAddr,unsigned int data);
static void Strata_EraseSector(int targetAddr);
static int  Strata_CheckID(int targetAddr);
static int  Strata_CheckDevice(int targetAddr);
//static int  Strata_CheckBlockLock(int targetAddr);
static int  Strata_BlankCheck(int targetAddr,int targetSize);
//static int  _WAIT(void);
 
static unsigned int srcAddress;
static unsigned int targetOffset=0;        // 0 으로 초기화함에도 불구하고 0xffffffff 으로 초기화됨
static unsigned int targetAddress; ㅜ
static unsigned int targetSize; 
 
static int error_erase=0;
static int error_program=0;
 
 
///////////////////////////////////////////////////////////////////
unsigned int downloadAddress; 
unsigned int downloadProgramSize;
unsigned int downloadImageOK=0;
 
 
/////////////////////////////////////////////////////////////////////////////////
int Strata_CheckID(int targetAddr) 
{
    _WR(targetAddr, 0x00900090); 
	udelay(40);
    return _RD(targetAddr);
}
 
int Strata_CheckDevice(int targetAddr) 
{
    _WR(targetAddr, 0x00900090);
	udelay(40);
    return _RD(targetAddr+0x4);
}
 
#if 0
int Strata_CheckBlockLock(int targetAddr) 
{
    _WR(targetAddr, 0x00900090);
    return _RD(targetAddr+0x8);
}
#endif
 
void Strata_EraseSector(int Address) 
{
    unsigned long ReadStatus;
    unsigned long bSR5;
    unsigned long bSR5_2;
    unsigned long bSR7;
    unsigned long bSR7_2;
 
    _WR(Address, 0x00200020);
	udelay(100);
    _WR(Address, 0x00d000d0);
	udelay(100);
 
    _WR(Address, 0x00700070);
	udelay(100);
    ReadStatus=_RD(Address);
    bSR7=ReadStatus & (1<<7);
    bSR7_2=ReadStatus & (1<<(7+16));
    while(!bSR7 | !bSR7_2) {
        _WR(Address, 0x00700070);
		udelay(100);
        ReadStatus=_RD(Address);
        bSR7=ReadStatus & (1<<7);
        bSR7_2=ReadStatus & (1<<(7+16));
    }
 
    _WR(Address, 0x00700070);
	udelay(100);
    ReadStatus=_RD(Address);  
    bSR5=ReadStatus & (1<<5);
    bSR5_2=ReadStatus & (1<<(5+16));
    if (bSR5==0 && bSR5_2==0) {
        printf("Block_%x Erase O.K. \n",Address);
    } else {
        printf("Error in Block Erasure!! = 0x%x\n", ReadStatus);
        _WR(Address, 0x00500050);
		udelay(100);
        error_erase=1;
    }
 
    _RESET();
}
 
int Strata_BlankCheck(int targetAddr,int targetSize) 
{
    int i,j;
    for (i=0; i<targetSize; i+=4) {
        j=*((volatile unsigned int *)(i+targetAddr));
        if (j!=0xffffffff) {
            printf("E : 0x%x = 0x%x\n", (i+targetAddr), j);
            return 0;
        }
    }
    return 1;
}
 
int Strata_ProgFlash(unsigned int realAddr,unsigned int data) 
{
    volatile unsigned int *ptargetAddr;
    unsigned long ReadStatus;
    unsigned long bSR4;
    unsigned long bSR4_2;
    unsigned long bSR7;
    unsigned long bSR7_2;
 
    ptargetAddr = (volatile unsigned int *)realAddr;
 
    _WR(realAddr, 0x00400040);
	udelay(10);
    *ptargetAddr=data;
 
    _WR(realAddr, 0x00700070);
	udelay(10);
    ReadStatus=_RD(realAddr);
    bSR7=ReadStatus & (1<<7);
    bSR7_2=ReadStatus & (1<<(7+16));
    while(!bSR7 || !bSR7_2) 
    {
        _WR(realAddr, 0x00700070);
		udelay(5);
        ReadStatus=_RD(realAddr);
        bSR7=ReadStatus & (1<<7);
        bSR7_2=ReadStatus & (1<<(7+16));
    }
 
    _WR(realAddr, 0x00700070); 
	udelay(10);
    ReadStatus=_RD(realAddr);
    bSR4=ReadStatus & (1<<4);
    bSR4_2=ReadStatus & (1<<(4+16));
 
    if (bSR4==0 && bSR4_2==0) {
//        printf("Successful Program!!\n");
		;
    } else {
        printf("Error Program!!\n");
        _WR(realAddr, 0x00500050);
		udelay(10);
        error_program=1;
    }
 
    _RESET();
    return 0;
}
 
void Program28F128J3A (void)
{
    int i,tmp=0;
 
    printf("\n[ 28F128J3A Flash Writing Program ]\n\n");
 
    if (!downloadImageOK) {
    	printf("Error! Write image not found!\n");
		return;
    }
 
    vh_rINTMSK = vh_Interrupt_All_Disable;   
    targetSize=downloadProgramSize;
 
    if(targetSize==0)
    {
        printf("\nThe data must be downloaded using ICE or USB from 0x31000000\n");
        srcAddress=downloadAddress; 
    } else { 
        srcAddress=downloadAddress+4;
    }
 
#ifndef MYSEO
AGAIN:
#endif
    printf("Source size [0x?] : 0h~%xh\n\n",downloadProgramSize);
    printf("Source base address = 0x%x\n",srcAddress);
    printf("Target base address = 0x%x\n",targetAddress);
    printf("Target offset       = 0x%x\n",targetOffset);          // 굳이 안 지워도 된다
    printf("Target size         = 0x%x\n",targetSize);
 
#if 0 // MYSEO
	tmp = _RD(vh_SRAM_CTRL_REG);
	tmp = tmp | 0x0406;
	_WR(vh_SRAM_CTRL_REG, tmp);
 
	_RESET();
#endif
 
    tmp = Strata_CheckID(targetAddress) & 0xffff;
    printf(" - Identification check (0x0089) = 0x%04x\n", tmp);
 
    if ( tmp != 0x0089 )       // ID number = 0x0089
    {
        printf("Identification check error !!\n");
        return;
    }
 
    tmp = Strata_CheckDevice(targetAddress) & 0xffff;
    printf(" - Device check (0x0018) = 0x%04x\n", tmp);
    if ( tmp != 0x0018 )   // Device number=0x0018
    {
        printf("Device check error !!\n");
        return;
    }
 
    printf("\nErase the sector : 0x%x.\n", targetAddress);
 
    for(i=0;i<targetSize;i+=0x20000)
    {
        Strata_EraseSector(targetAddress+i);      // 수정
    }
 
    if(!Strata_BlankCheck(targetAddress,targetSize))     // 수정
    {
#ifdef MYSEO
        printf("Blank Check Error!!!\n");
        return;
#else
        printf("\n\nagain flash write");
		goto AGAIN;
#endif
    }
 
    printf("\nStart of the data writing...\n");
 
    for (i=0; i<targetSize; i+=4) 
    {
        Strata_ProgFlash(i+targetAddress, *((unsigned int *)(srcAddress+i)));      // 수정
        if(i%0x10000==0xfffc)
            printf("[0x%x]",(i+4)/0x10000);
    }
    printf("\nEnd of the data writing \n");
 
    _RESET();
	udelay(10);
 
    printf("Verifying Start...\n");
    for (i=0; i<targetSize; i+=4) 
    {
        if (*((unsigned int *)(i+targetAddress)) !=*((unsigned int *)(srcAddress+i)))      // 수정
        {
            printf("verify error  src %08x = %08x\n", srcAddress+i, *((unsigned int *)(srcAddress+i)));
            printf("verify error  des %08x = %08x\n", i+targetAddress, *((unsigned int *)(i+targetAddress)));    // 수정
#ifdef MYSEO
{
			int  in;
 
            printf("\n\nagain flash write (y/n)? ");
			in = getc();
            printf("\n\n");
			if (in == 'y')
				goto AGAIN;
}
#else
            printf("\n\nagain flash write");
			goto AGAIN;
#endif
            return;
        }
    }
    printf("Verifying End!!!");
}
 
 
////////////////////////////////////////////////////////////////////////////
static int DownloadData(void)
{
	int i,tmp;
	unsigned short checkSum=0,dnCS;
	unsigned int fileSize=10;
	unsigned char *downPt;
 
	downPt=(unsigned char *)downloadAddress;
 
	printf("\nDownload Memoey Address = 0x%x\n",downloadAddress);
	printf("\nFlash write Address = 0x%x\n",targetAddress);
 
	printf("\nSTATUS : ");
	vh_rINTMSK = BIT_ALLMSK;
 
	tmp=RdURXH0(); //To remove overrun error state.
 
	i=0;    
	while(i<fileSize) {
		while(!(rUTRSTAT0&0x1));
			*(downPt+i)=RdURXH0();
		if(i==3) {
			fileSize=*((unsigned char *)(downloadAddress+0))+
			(*((unsigned char *)(downloadAddress+1))<<8)+
			(*((unsigned char *)(downloadAddress+2))<<16)+
			(*((unsigned char *)(downloadAddress+3))<<24);
		}
 
		if((i%4000)==0)
			putc('#');
		i++;
	}
 
	downloadProgramSize=fileSize-6;
 
	for(i=4;i<(fileSize-2);i++)
		checkSum+=*((unsigned char *)(i+downloadAddress));
 
	dnCS=*((unsigned char *)(downloadAddress+fileSize-2))+ (*((unsigned char *)(downloadAddress+fileSize-1) )<<8);
 
	if(checkSum!=dnCS) {
		printf("Checksum Error!!! MEM : 0x%x  DN : 0x%x\n",checkSum,dnCS);
		return 0;
	}
 
	printf("\nDownload O.K.\n");
	return downloadProgramSize;
}
 
void *flash_write(void *arg)
{
	int cnt=0;
	char *argv[1];
	char **str;
	int start_addr;
 
	str = (char **)arg;
	argv[0] = str[0];
	start_addr = atoi(argv[0]);
 
	if (!start_addr || start_addr < 0x100000 || start_addr > 0x2000000)
    	targetAddress=vh_VPOS_FLASH_BASE;
	else
    	targetAddress=start_addr;
 
	downloadAddress= vh_SDRAM_BASE + 0x00f00000;
	cnt = DownloadData();
 
	if (cnt) {
		downloadImageOK=1;
		Program28F128J3A ();
	}
 
 
	return (void *)0;
}

컴파일시에

GNU ld version 2.14.90.0.6 20030820
arm-linux-ld: ERROR: /usr/local/arm/lib/gcc-lib/arm-linux/2.95.3/libgcc.a(_divsi3.o) uses hardware FP, whereas vpos_kernel-elf32 uses software FP
Success: failed to merge target specific data of file /usr/local/arm/lib/gcc-lib/arm-linux/2.95.3/libgcc.a(_divsi3.o)
arm-linux-ld: ERROR: /usr/local/arm/lib/gcc-lib/arm-linux/2.95.3/libgcc.a(_dvmd_lnx.o) uses hardware FP, whereas vpos_kernel-elf32 uses software FP
Success: failed to merge target specific data of file /usr/local/arm/lib/gcc-lib/arm-linux/2.95.3/libgcc.a(_dvmd_lnx.o)
arm-linux-ld: ERROR: /usr/local/arm/lib/gcc-lib/arm-linux/2.95.3/libgcc.a(_modsi3.o) uses hardware FP, whereas vpos_kernel-elf32 uses software FP
Success: failed to merge target specific data of file /usr/local/arm/lib/gcc-lib/arm-linux/2.95.3/libgcc.a(_modsi3.o)
arm-linux-ld: ERROR: /usr/local/arm/lib/gcc-lib/arm-linux/2.95.3/libgcc.a(_udivsi3.o) uses hardware FP, whereas vpos_kernel-elf32 uses software FP
Success: failed to merge target specific data of file /usr/local/arm/lib/gcc-lib/arm-linux/2.95.3/libgcc.a(_udivsi3.o)
arm-linux-ld: ERROR: /usr/local/arm/lib/gcc-lib/arm-linux/2.95.3/libgcc.a(_umodsi3.o) uses hardware FP, whereas vpos_kernel-elf32 uses software FP
Success: failed to merge target specific data of file /usr/local/arm/lib/gcc-lib/arm-linux/2.95.3/libgcc.a(_umodsi3.o)
arm-linux-ld: ERROR: /usr/bin/../arm-linux/lib/libc.a(memmove.o) uses hardware FP, whereas vpos_kernel-elf32 uses software FP
Success: failed to merge target specific data of file /usr/bin/../arm-linux/lib/libc.a(memmove.o)
arm-linux-ld: ERROR: /usr/bin/../arm-linux/lib/libc.a(memset.o) uses hardware FP, whereas vpos_kernel-elf32 uses software FP
Success: failed to merge target specific data of file /usr/bin/../arm-linux/lib/libc.a(memset.o)
arm-linux-ld: ERROR: /usr/bin/../arm-linux/lib/libc.a(memcpy.o) uses hardware FP, whereas vpos_kernel-elf32 uses software FP
Success: failed to merge target specific data of file /usr/bin/../arm-linux/lib/libc.a(memcpy.o)
arm-linux-ld: ERROR: /usr/bin/../arm-linux/lib/libc.a(wordcopy.o) uses hardware FP, whereas vpos_kernel-elf32 uses software FP
Success: failed to merge target specific data of file /usr/bin/../arm-linux/lib/libc.a(wordcopy.o)
make[1]: *** [vpos_kernel-elf32] Error 1
make[1]: Leaving directory `/opt/vpos/objs'
make: *** [all] Error 2

위와 같은 에러가 난다면, makefile 을 수정해야 한다.

...
CFLAGS  = -O0 -Wall -Wstrict-prototypes -fPIC -mshort-load-bytes -nostdinc -nostartfiles -nostdlib -march=armv4 -mtune=arm9tdmi -fomit-frame-pointer -fno-builtin -mapcs-32 $(INCLUDE)    // 수정
OCFLAGS = -O binary -R .note -R .comment -R .stab -R .stabstr -S
...

'-msoft-float' 를 제거하면 된다.

컴파일 및 수행

이제 컴파일 해보자! 제대로 수행되었다면, 생성된 bin 파일을 가지고 제대로 동작하는 지 테스트해보자!

  • computer/rtcclab/vpos_gcc3_으로_포팅하기.txt
  • Last modified: 3 years ago
  • by likewind