이번에 CUBEMX 를 업데이트하고 나서, USB CDC 를 테스트 해 봤습니다.


예전에, USBD_CDC.h 파일에서 수정해야 동작하던 상수값이, 
이번 버전(Ver 4.23.0)의 CUBE MX 에서는 제대로 생성해 주는 것을 확인했고,
몇가지 간단하게 수정하면 USB CDC 를 바로 쓸 수가 있었습니다.




usbd_cdc.h  예전의 설정 (USB COM Port 장치 에러 발 생)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** @defgroup usbd_cdc_Exported_Defines
  * @{
  */ 
#define CDC_IN_EP                                   0x81  /* EP1 for data IN */
#define CDC_OUT_EP                                  0x01  /* EP1 for data OUT */
#define CDC_CMD_EP                                  0x82  /* EP2 for CDC commands */
 
/* CDC Endpoints parameters: you can fine tune these values depending on the needed baudrates and performance. */
#define CDC_DATA_HS_MAX_PACKET_SIZE                 512  /* Endpoint IN & OUT Packet size */
#define CDC_DATA_FS_MAX_PACKET_SIZE                 64  /* Endpoint IN & OUT Packet size */
#define CDC_CMD_PACKET_SIZE                         8  /* Control Endpoint Packet size */ 
 
#define USB_CDC_CONFIG_DESC_SIZ                     67
#define CDC_DATA_HS_IN_PACKET_SIZE                  CDC_DATA_HS_MAX_PACKET_SIZE
#define CDC_DATA_HS_OUT_PACKET_SIZE                 CDC_DATA_HS_MAX_PACKET_SIZE
 
#define CDC_DATA_FS_IN_PACKET_SIZE                  CDC_DATA_FS_MAX_PACKET_SIZE
#define CDC_DATA_FS_OUT_PACKET_SIZE                 CDC_DATA_FS_MAX_PACKET_SIZE
cs


usbd_cdc.h  새로 수정된 내용(CDC_DATA_HS_MAX_PACKET_SIZE 값이 256 으로 초기설정됨)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** @defgroup usbd_cdc_Exported_Defines
  * @{
  */ 
#define CDC_IN_EP                                   0x81  /* EP1 for data IN */
#define CDC_OUT_EP                                  0x01  /* EP1 for data OUT */
#define CDC_CMD_EP                                  0x82  /* EP2 for CDC commands */
 
/* CDC Endpoints parameters: you can fine tune these values depending on the needed baudrates and performance. */
#define CDC_DATA_HS_MAX_PACKET_SIZE                 256  /* Endpoint IN & OUT Packet size */
#define CDC_DATA_FS_MAX_PACKET_SIZE                 64  /* Endpoint IN & OUT Packet size */
#define CDC_CMD_PACKET_SIZE                         8  /* Control Endpoint Packet size */ 
 
#define USB_CDC_CONFIG_DESC_SIZ                     67
#define CDC_DATA_HS_IN_PACKET_SIZE                  CDC_DATA_HS_MAX_PACKET_SIZE
#define CDC_DATA_HS_OUT_PACKET_SIZE                 CDC_DATA_HS_MAX_PACKET_SIZE
 
#define CDC_DATA_FS_IN_PACKET_SIZE                  CDC_DATA_FS_MAX_PACKET_SIZE
#define CDC_DATA_FS_OUT_PACKET_SIZE                 CDC_DATA_FS_MAX_PACKET_SIZE
cs

그래서 수정할 내용은, 
1. #include 에서 usbd_cdc_if.h 추가하고,
1
2
#include "string.h"
#include "usbd_cdc_if.h"
cs

2. 저는 보통 cdc를 디버그 출력용으로만 사용해서 Txd 기능만 주로 사용합니다.
  - 그리고, 쓰기 편하게 printf() 함수를 사용하다 보니, string.h 파일을 include 해 줍니다.
    USB_FS 로  printf() 함수를 사용하기 위한 추가 코드는 다음과 같습니다. main.c 파일의 User code 에 추가해 주면 됩니다.
 main.c 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* USER CODE BEGIN 0 */
#ifdef __GNUC__
 #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
 #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
 
PUTCHAR_PROTOTYPE
{
 while(CDC_Transmit_FS((uint8_t *)&ch, 1== USBD_BUSY);
 return ch;
}
 
/* USER CODE END 0 */
cs
  - JTAG 나 SSTLINK 디버거용 핀을 보드에 배치하면 보드 크기가 커져서, 저는 USB CDC를 디버깅 기능으로 사용합니다.
  - USB 만 연결해서, 프로그램 다운로드는 USB DFU 를 사용하고, 디버깅이 필요할 시 USB CDC 를 사용하면 편리합니다. ^^

3. 위 설정만 완료하면, USB COM Port 로 PC 와 통신하면 됩니다.




다음은 간단한 USB CDC 출력 테스트 프로그램입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "stm32f4xx_hal.h"
#include "usb_device.h"
 
/* USER CODE BEGIN Includes */
#include "string.h"
#include "usbd_cdc_if.h"
 
/* USER CODE END Includes */
 
/* Private variables ---------------------------------------------------------*/
I2C_HandleTypeDef hi2c1;
 
/* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*/
 
/* USER CODE END PV */
 
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_I2C1_Init(void);
 
/* USER CODE BEGIN PFP */
/* Private function prototypes -----------------------------------------------*/
 
/* USER CODE END PFP */
 
/* USER CODE BEGIN 0 */
#ifdef __GNUC__
 #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
 #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
 
PUTCHAR_PROTOTYPE
{
 while(CDC_Transmit_FS((uint8_t *)&ch, 1== USBD_BUSY);
 return ch;
}
 
/* USER CODE END 0 */
 
int main(void)
{
 
  /* USER CODE BEGIN 1 */
 
  /* USER CODE END 1 */
 
  /* MCU Configuration----------------------------------------------------------*/
 
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();
 
  /* USER CODE BEGIN Init */
 
  /* USER CODE END Init */
 
  /* Configure the system clock */
  SystemClock_Config();
 
  /* USER CODE BEGIN SysInit */
 
  /* USER CODE END SysInit */
 
  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_I2C1_Init();
  MX_USB_DEVICE_Init();
 
  /* USER CODE BEGIN 2 */
 
  /* USER CODE END 2 */
 
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
        printf("USB CDC Test ^^\r\n");
        HAL_Delay(500);
  /* USER CODE END WHILE */
 
  /* USER CODE BEGIN 3 */
 
  }
  /* USER CODE END 3 */
 
}
cs


터미날 프로그램으로 테스트한 내용입니다.



alpu_v100.zip


[2017.04.19] - 내용 추가 
최근에 겪었던 내용을 추가합니다.
얼마전에 여러 기능을 갖고 있는 APP에 UART Rx Interrupt 코드를 추가했는데, 천천이 데이터를 받으면 괜찮은데, 빨리 데이터를 받으면 UART RX Interrupt 기능이 동작하지 않았습니다. 
고전적인 방법을 쓰지 않고 HAL 라이브러리에 의해 프로그램을 만들어서 그런지느리고 무거운 느낌이 들었습니다.
HAL 드라이버가 다양한 기능을 한 번에 처리하고 , 사용하기 쉽다고 하지만 인터럽트 처리에 있어서는 효율이 떨어지는 것 같습니다.

위와 같이 죽었던 문제점은 HAL 드라이버를 사용하지 않았더니 없어졌습니다.
다음의 글을 참고로 코드를 작성했습니다. 설명이 아주 작 되어 있으니 참고하시기 바랍니다.


어쩌면 USB-to-UART 프로그램으로 ESP8266 에 다운로드가 안되는 문제도 위와 같이 수정하면, 잡힐 것도 같습니다.
----------------------------------------------------------------------------------------------

안녕하세요,

글을 쓸 소재들이 요즘 많아 졌는데, 올릴 시간이 없군요.

얼마 전에 PSOC5로, 
1. 일반 TR(MMBT3904)의 내부 다이오드에 정전류를 흘려서 전압을 재서 온도를 측정한 것도 있고,
2. 열전대로 온도 측정 및 열전대 측정값 디지탈 필터로 노이즈 제거,
3. PSOC5 의 새로운 콤포넌트인 LED Driver 로 7-SEGMENT 7개와 8개의 LED 다이나믹 스캔 제어(DMA로 구성되어 CPU에 부하를 주지 않음)
4. 다이오드 상온 측정값과 열전대의 목표지점의 온도를 측정하면서, PID로 전열기를 제어,
5. STM32F4xx 로 USB VCP 를 통해서 UART로 ESP-8266의 프로그램을 Upload (테스트 완료 예정)
6. ESP-8266으로 FTP 서버 만들기(SPIFF:완료,SD-CARD:테스트 중,SOFT AP FTP Server:테스트 중)의 글을 올릴 생각입니다.

이번에는 5. STM32F4xx 로 USB CDC 를 통해서 UART로 ESP-8266의 프로그램을 Upload (테스트 완료 예정) 을 
테스트하기위해 우선, STM32F4xx로 USB-to-UART 통신 테스트를 했습니다.


예전에 USB CDC 로 Tx 테스트를 해서 글을 올려 놓았는데 (http://cafe.naver.com/cortexsh/997),
USB CDC Rx(데이터를 USB Serial Port로 받는) 테스트는 정말 구현하기 힘들었습니다.

여기저기 인터넷을 뒤지고 뒤져서 겨우 단서를 하나 찾았습니다. (http://www.openstm32.org/forumthread2250)




다음의 사이트에서 STM32Cube_FW_F4_Vxx 라는 펌웨어 예제(링크)들이 있는데, 
다운 받은 zip 파일을 풀어, 
그 중에서 STM324xG EVAL/Application/USB_Device/CDC_Standalone 의 프로젝트를 참고하라고 되어 있었습니다.
이 예제를 많이 참고해서 STM32F4xx 보드로 USB-to-UART 를 구현할 수 있었습니다.

STM32F4xx 에서 사용한 내부 기능은 다음과 같습니다.
1. UART2 : UART TX (DMA 전송) , UART RX (RX Interrupt)
2. TIM2 : 5ms 마다 USB Rx로 수신된 데이터를 UART2 로 DMA 전송
3. USB Device Only : USB CDC(Communication Device Class) 초기화 함수를 실행하면,
 데이터가 자동으로(인터럽트에 의해) 수신되면서 CDC_Receive_FS() 함수가 실행됨.




[사용하지 않는 핀 설명]
위의 그림에서 
- ESP_RST(GPIO_OUT) , ESP_FLASH(GPIO_OUT) 은 나중에 ESP-8266 에 프로그램을 업로드하기 위해,
모드를 RUN-MODE,FLASH-MODE 로 변경하기 위해 설정해 놓았고 현재는 사용하지 않는다.
- 또, UART1 Rx/Tx 핀은 ESP-8266 에 프로그램을 Upload 할 때 사용하는 UART 핀으로 설정한 것으로,
현재는 사용하지 않는다.


개략적으로 프로그램 동작을 설명하자면, 
1. STM32F4xx Nucleo 보드에 UART2 와 USB CDC, TIM2 Update Interrupt 기능을 
   CUBEMX 툴로 사용하도록 설정하고 KEIL MDK 5.0 소스코드를 생성.



2. UART2 , USB CDC , TIM2 초기화.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// usb_device.c
/* init function */                        
void MX_USB_DEVICE_Init(void)
{
  /* Init Device Library,Add Supported Class and Start the library*/
  USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS);
 
  USBD_RegisterClass(&hUsbDeviceFS, &USBD_CDC);
 
  USBD_CDC_RegisterInterface(&hUsbDeviceFS, &USBD_Interface_fops_FS);
 
  USBD_Start(&hUsbDeviceFS);
 
}
 
------------------------------------------------------------------------------
 
// main.c 
/* USART2 init function */
static void MX_USART2_UART_Init(void)
{
 
  huart2.Instance = USART2;
  huart2.Init.BaudRate = 115200;
  huart2.Init.WordLength = UART_WORDLENGTH_8B;
  huart2.Init.StopBits = UART_STOPBITS_1;
  huart2.Init.Parity = UART_PARITY_NONE;
  huart2.Init.Mode = UART_MODE_TX_RX;
  huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart2.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart2) != HAL_OK)
  {
    Error_Handler();
  }
 
}
 
static void MX_DMA_Init(void
{
  /* DMA controller clock enable */
  __HAL_RCC_DMA1_CLK_ENABLE();
 
  /* DMA interrupt init */
  /* DMA1_Stream6_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA1_Stream6_IRQn, 00);
  HAL_NVIC_EnableIRQ(DMA1_Stream6_IRQn);
 
}
 
/* TIM2 init function */
static void MX_TIM2_Init(void)
{
 
  TIM_ClockConfigTypeDef sClockSourceConfig;
  TIM_MasterConfigTypeDef sMasterConfig;
 
  htim2.Instance = TIM2;
  htim2.Init.Prescaler = 0;
  htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim2.Init.Period = 960000/2;
  htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  if (HAL_TIM_Base_Init(&htim2) != HAL_OK)
  {
    Error_Handler();
  }
 
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
 
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
 
}
 
cs

3. UART2 RX인터럽트에 의해 데이터 수신시 USB CDC TX 큐버퍼에 데이터 저장. (in main.c)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  /* Prevent unused argument(s) compilation warning */
  /* Increment Index for buffer writing */
  UserTxBufPtrIn++;
  
  /* To avoid buffer overflow */
  if(UserTxBufPtrIn == APP_RX_DATA_SIZE)
  {
    UserTxBufPtrIn = 0;
  }
  
  /* Start another reception: provide the buffer pointer with offset and the buffer size */
  HAL_UART_Receive_IT(huart, (uint8_t *)(UserTxBufferFS + UserTxBufPtrIn), 1);
  /* NOTE: This function Should not be modified, when the callback is needed,
           the HAL_UART_TxCpltCallback could be implemented in the user file
   */
}
cs

4. 5ms 마다 TIM2 Update 인터럽트가 걸리면 UART2 RX로 받은 데이터가 있는지 체크해서  
USB CDC TX 큐버퍼에 있는 데이터를 USB CDC Tx 핀으로 모두 송신. (in main.c)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    uint32_t buffptr;
    uint32_t buffsize;
 /* Prevent unused argument(s) compilation warning */
    if (htim->Instance == TIM2)
    {
  
  if(UserTxBufPtrOut != UserTxBufPtrIn)
  {
    if(UserTxBufPtrOut > UserTxBufPtrIn) /* Rollback */
    {
      buffsize = APP_TX_DATA_SIZE - UserTxBufPtrOut;
    }
    else 
    {
      buffsize = UserTxBufPtrIn - UserTxBufPtrOut;
    }
    
    buffptr = UserTxBufPtrOut;
    
    USBD_CDC_SetTxBuffer(&hUsbDeviceFS, (uint8_t*)&UserTxBufferFS[buffptr], buffsize);
    
    if(USBD_CDC_TransmitPacket(&hUsbDeviceFS) == USBD_OK)
    {
      UserTxBufPtrOut += buffsize;
      if (UserTxBufPtrOut == APP_RX_DATA_SIZE)
      {
        UserTxBufPtrOut = 0;
      }
    }
  }
    }
    else if (htim->Instance == TIM1)
    {
    }
  /* NOTE : This function Should not be modified, when the callback is needed,
            the __HAL_TIM_PeriodElapsedCallback could be implemented in the user file
   */
}
cs

5. USB CDC Rx로 인터럽트에 의해 Rx 큐버퍼에 데이터가 수신되면,
 바로 UART2 로 데이터를 DMA 방식으로 전송. (in usbd_cdc_if.c)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
  * @brief  CDC_Receive_FS
  *         Data received over USB OUT endpoint are sent over CDC interface 
  *         through this function.
  *           
  *         @note
  *         This function will block any OUT packet reception on USB endpoint 
  *         untill exiting this function. If you exit this function before transfer
  *         is complete on CDC interface (ie. using DMA controller) it will result 
  *         in receiving more data while previous ones are still not sent.
  *                 
  * @param  Buf: Buffer of data to be received
  * @param  Len: Number of data received (in bytes)
  * @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL
  */
static int8_t CDC_Receive_FS (uint8_t* Buf, uint32_t *Len)
{
  /* USER CODE BEGIN 6 */
 
  USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
  USBD_CDC_ReceivePacket(&hUsbDeviceFS);
    HAL_UART_Transmit_DMA(&huart2, Buf, *Len);
  return (USBD_OK);
  /* USER CODE END 6 */ 
}
 
cs

6. 1~5번 항목을 무한 반복.


다음은 실제 Nucleo 보드와 USB 케이블 연결된 사진 입니다.





다음 그림은 프로그램을 실행함에 따라서, 장치관리자로 COM 포트가 생성된 모습입니다.




다음은 테라텀 프로그램으로 각각의 COM 포트에 연결해서 송수신 테스트를 해 본 사진입니다.




다음은 실시간 테스트 영상입니다.


마지막으로 이 프로젝트의 소스 파일을 첨부합니다.


이번에는 STM32F446에서 USB CDC(Communication Device Class) = USB Virtual Comport Serial Driver 기능을 테스트해 보겠습니다. 


회사에서 제작한 보드에는 WLCSP 타입의 STM32F446 IC를 사용해서 CUBEMX 툴에서 패키지를 바꿨습니다.

[회사에서 제작한 보드]



아이고 그런데, 설계를 USB HS 포트에 연결을 해놨네.. @@
이번에 처음 테스트를 했는데, 결과적으로 USB DFU 와 CDC 모두 잘 됐습니다.

USB CDC 는 정말 간단합니다. CUBEMX 툴에서 설정만 제대로 하면 거의 다 된 것입니다.
하지만 아무리 해도 CDC가 UART 포트로 잘 안잡혔는데, 유튜브 동영상 보고 수정하니까 잘 됩니다.
이 내용은 나중에 말씀드리겠습니다.

먼저 CUBEMX 툴로 PINOUT 설정에서 필요한 기능들을 정의합니다.

1. USB_HS 포트에 internal FS Phy 를 Device Only 로 설정.



2. RCC 에서 HSE 를 Crystal 로 설정. MiddleWares 의 USB_DEVICE 에 CDC로 설정.



3. 클럭 설정에서 외부 클락을 24MHz로 수정하고 HCLK 를 180MHz로 수정하고 엔터치면 오랜 시간이 걸린 후에 168MHz로 자동으로 계산해 주는데, 자주 하다 보니 그냥 처음부터 168MHz를 써 주면 금방 설정이 완료 됩니다.



4. 마무리로 프로젝트 이름 넣어주고 Toolchain / IDE를 Keil로 설정하고 Generate code 를 클릭하면 Keil 코드가 만들어 집니다.



5. 그냥 이대로 KEIL 에서 컴파일해서 다운로드해 주면 다음과 같이 PC 장치관리자에서 USB CDC 는 보이는데,
오류가 발생합니다.




다음의 Youtube 영상의 2:36 부분부터 유심히 보시면 저와 같은 결과에 어떻게 대응하는지 잘 나와 있습니다.
저도 이 영상보고 수정해서 성공했습니다.






6. 수정할 코드는 usbd_cdc.h 의 CDC_DATA_HS_MAX_PACKET_SIZE 의 값이 잘 못 되어 있습니다. (^^ 저는 원리도 모르겠고 그냥 따라했는데 잘 되요.)
아뭏든 512 를 256 으로 고치면 정상적으로 PC에서 COM PORT가 잡힙니다.



7. PC장치관리자에서 포트 확인




8. 이렇게까지 하면 포트만 생성됐지 터미날 연결하면 아무짓도 안하는게 당연하겠죠? ^^
USB UART 출력 함수는 usbd_cdc_if.c 파일 안에 uint8_t CDC_Transmit_HS(uint8_t* Buf, uint16_t Len) 함수를 사용하면 됩니다.

간단한 예로,
CDC_Transmit_HS("Try..!!\n\r",9); 하면 
터미날에 ..
Try..!!

라고 출력 됩니다.

그런데 printf 함수가 참 편하므로 이것도 printf() 함수 쓰는 방법을 알아보겠습니다.
예전에 UART에서 사용하던 것과 같이 tx 함수만 CDC_Transmit_HS() 함수로 수정해 주면 됩니다.

main.c 파일에 다음과 같이 추가해 주시면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
/* USER CODE BEGIN 0 */
 
#ifdef __GNUC__
 #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
 #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
 
PUTCHAR_PROTOTYPE
{
 while(CDC_Transmit_HS((uint8_t *)&ch, 1== USBD_BUSY);
 return ch;
}
cs



그런 다음에 필요한 곳에서 printf() 함수를 사용하시면 됩니다.
끝...

이런 한가지 빼 먹었습니다. #include 에 다음과 같이 추가해 주세요.
1
2
3
4
5
6
7
8
9
10
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "stm32f4xx_hal.h"
#include "usb_device.h"
 
/* USER CODE BEGIN Includes */
#include "string.h"
#include "usbd_cdc_if.h"
/* USER CODE END Includes */
cs

이것은 따로 프로젝트 파일 첨부하지 않겠습니다.
너무 간단하니까요. ^^


+ Recent posts