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

나빌레라의 이미지

처음 키보드를 만들려고 했을 땐 펌웨어까지 만들 생각은 없었다. 공개된 키보드 펌웨어 중엔 널리 쓰이고 검증된 펌웨어가 있었으니까. 대표적인 오픈 소스 키보드 펌웨어는 QMK(https://github.com/qmk/qmk_firmware)와 TMK(https://github.com/tmk/tmk_keyboard)가 있다. QMK도 TMK 기반에 부가 기능을 추가한 것이다. 많은 커스텀 키보드들이 QMK나 TMK를 쓴다. 나도 당연히 아무 고민 없이 QMK를 쓰려했다.

막상 쓰려고 마음 먹고 포팅 작업을 하려 했더니 몇 가지 마음에 들지 않는 부분이 보였다. 우선 QMK나 TMK 모두 AVR을 기본으로 개발된 펌웨어다. 물론 지금은 STM32 등 ARM 계열 프로세서도 몇 개 지원하긴 한다. 그러나 ARM을 지원하려고 chobios라는 덩어리 큰 RTOS를 가져다 쓴다. 물론 STM32 포팅 작업조차 누군가 해 놨기 때문에 사실 포팅 자체가 목적이라면 작업은 쉬웠을 것이다. 키맵 일부만 수정하고 하드웨어 설정 값 정도만 바꾸면 아마 됐을 것이다. 해보진 않아서 모르겠지만 됐겠지 뭐.

포팅 작업을 시작하기 전에 펌웨어가 어떤 식으로 동작하는지 파악하고 싶었다. 그냥 쓰면 될 것을 꼭 이런 호기심 때문에 삽질을 하게 된다. 구조를 파악하려고 코드를 보는데, 아니 무슨 키보드 펌웨어가 이토록 복잡하단 말인가. 물론 USB도 제어하고 LED 드라이버도 제어하는 등 기능이 많으니까 HAL 쪽 코드가 많은건 이해하겠다. 그런데 일단 기본적으로 스위치 닫힘을 인식해서 이 신호가 HID scancode로 변경되어 USB로 나가는 flow 자체가 너무 복잡했다. 물론 남이 만든 프로그램이니 내 맘에 안드는 것이 당연 할 테지만 굳이 이렇게까지 복잡해야 하나라는 생각이 들었다. 더불어 지금 QMK를 메인테인하는 개발자들이 손 떼면 누가 이어서 작업 할 순 있을까 하는 생각도 들었다. 세상에 똑똑하고 실력 좋은 사람들은 많으니 당연히 잘 굴러가겠지만 그런 생각이 들 정도로 구조를 파악하기 어려웠다는 말이다.

사실 지금 생각해보면 다 핑계고 그냥 키보드 펌웨어 정도 새로 만들고 싶었는지도 모른다. 생각해 보면 정말 쉬운 펌웨어 아닌가. 스위치 입력 인식해서 어떤 키가 눌렸는지 판단하고 그 키를 HID 스펙에 맞춰서 USB로 보내면 끝이다. 실제로도 쉽고 즐거운 작업이었다. 그래서 이렇게 글을 쓰고 있다. 글로 설명할 수 있는 수준으로 쉬우니까. 어려운 부분은 HID 스펙에 맞춰서 USB 디스크립터 코드 작성하고 STM32의 USB 컨트롤러 사용 방법에 맞춰 레지스터 설정하는 내용이다. 고맙게도 이 부분은 STM32 제조사인 ST micronics에서 SDK로 만들어서 배포한다. 나는 단지 그 코드를 가져다가 썻다.

그래도 펌웨어 시작은 UART부터

어떤 디바이스에 펌웨어 개발 환경을 세팅하고 가장 먼저 돌려보는 펌웨어는 UART 출력 펌웨어다. 일단 UART로 글자가 찍히는 것을 봐야 펌웨어 빌드, 다운로드, 동작이 모두 제대로 됐다고 확신 할 수 있기 때문이다.

내가 고른 키보드 컨트롤러는 STM32F103 칩이다. 이 칩을 쓴 보드는 알리익스프레스에서 $1.x에 판다. 정말 싸다. 저렇게 팔아도 남으니 파는 것이겠지만 어떻게 저런 가격이 나올 수 있는지 신기할 따름이다. 똑같은 제품을 아마존에서는 $7.x에 판다. 몇 배에 파는거냐.. 이 칩을 쓴 보드를 고른 이유는 딱 하나다. 싸니까. 그냥 제일 싼 것 중에서 GPIO가 많은 것을 골랐다. 키보드 스위치 매트릭스를 연결하려면 GPIO가 많아야 하니까. 고르고 나서 보니 이 보드를 쓰는 사람이 많았다. 그래서 예제도 많았다. 아마도 싸서 많이 쓰는 것 같다.

어차피 나중에 USB도 쓸거니까 github에서 "stm32f103 uart usb"로 검색해서 나오는 프로젝트 중에 아무거나 받았다. 다들 기본 SDK에서 조금씩 변형한 것들이라 코드 구성이 다 비슷하다. 사실 이 글을 쓰는 시점에서 내가 어떤 걸 다운 받아서 시작했는지 기억이 안난다. 나중에 HAL을 제외한 부분을 모두 바꿔 버려서... 아무튼 중요한 것은 아무거나 받아도 된다는 것이다. 다운 받기 전에 main.c 정도 훑어보고 코드가 마음에 드는 걸 골라도 된다. 그리고 나는 소스 코드에 makefile이 있는 것을 골랐다. 어떤 사람들은 SDK 툴로만 빌드 할 수 있는 코드를 그대로 github에 올려놔서 직접 makefile을 만들지 않으면 빌드 할 수 없는 것들도 많다. 물론 나는 makefile을 만들 수 있으나 귀찮아서 남이 만든 것을 쓰기로 했다. 어차피 나중에 다시 다 만들거니까. makefile도 있고 main.c 코드도 마음에 드는 프로젝트를 골랐으면 다운받자.

검색어가 "uart usb"라서 그런지 검색 결과는 거의다 USBtoUART bridge를 STM32F103으로 구현한 것들이다. 물론 이것마져도 SDK 예제중 하나다. 내 생각에 USBtoUART bridge 구현하는 것이 STM32 교육 커리컬럼 중 하나가 아닐까 싶을 정도로 인터넷에서 구한 소스 코드가 모두 비슷했다. 아무렴 어떠리오.

일단 아무런 수정 하지 않고 빌드해 본다. 잘 되어야 한다. 빌드가 잘 안되면 빌드 잘 되는 코드로 다른 것을 골라서 다운받는다. arm-gcc 같은 패키지 설치 문제라면 설치하기 바란다. 빌드가 되면 보드에 다운로드한다. 보드에 다운로드 하려면 CP2102나 FT232R 같은 칩을 쓴 USBtoUART bridge 보드가 필요하다. 이것도 알리익스프레스에서 $3.x 정도에 살 수 있다. 나는 가지고 있는 것이 있어서 있는 것을 썻다. STM32F103는 PA9, PA10이 각각 UART1의 Tx와 Rx다. Tx와 Rx를 USBtoUART bridge 보드의 Rx, Tx에 연결하고 접지를 연결하면 된다. 필요에 따라 3.3v 혹은 5v 전원을 연결해야 할 수도 있다. 이것은 각자 구매한 보드의 매뉴얼을 보면 설명이 있을 것이다. 보드에 따라 다르기 때문에 내가 어떻게 하라고 말할 수 없는 부분이다. 다운로드해서 동작하는지 확인해 본다. 나는 처음 받은 코드가 USBtoUART bridge 펌웨어라서 아래 명령으로 USB가 연결되는지 확인해 봤다.

sudo dmesg -w

USB가 제대로 연결되지 않으면 딱 봐도 에러 같은 메시지가 잔뜩 지나간다.

잘 되는것을 확인 했으니 이제 UART만 살리고 나머지 코드를 제거한다. 목적은 베이스 코드를 얻기 위함이니 나중에 다시 추가 하더라도 일단 지금 필요없는 코드는 과감히 지워버린다.

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();
 
  /* Configure the system clock */
  SystemClock_Config();
 
  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART3_UART_Init();
  MX_USB_DEVICE_Init();
 
  /* USER CODE BEGIN 2 */
 
  /* USER CODE END 2 */
 
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
  /* USER CODE END WHILE */
 
  /* USER CODE BEGIN 3 */
    if(uartIn != uartOut)
    {      
      CDC_Transmit_FS(&uartBuf[uartOut],1);    
      uartOut++;
      if(uartOut>=250)
        uartOut=0;
    }
 
  }
  /* USER CODE END 3 */
 
}

다운받은 코드를 보면 거의 다 위 코드와 비슷한 모습으로 main() 함수가 작성되어 있다. 조금 달라도 상관없다. 어차피 UART만 남기고 다 지울거니까. 내가 일단 다운받아서 빌드하고 보드에서 돌려본 이유는 기본적으로 작업해야 할 시스템 초기화나 HAL 초기화 코딩을 하고 싶지 않아서다. 공부하기도 귀찮고.

위 코드를 우선 이렇게 고쳤다.

int main(void)
{
  HAL_Init();
  SystemClock_Config();
 
  MX_GPIO_Init();
  MX_USART3_UART_Init();
 
  while (1)
  {
  }
}

이렇게 불필요한 코드 다 지우고 빌드 한 다음 다시 보드에서 돌려본다. 이러면 당연히 이제 USB 인식은 안된다. 뭐 그래도 아까는 돌았으니 내부에서는 잘 되고 있을 것이라 믿고 다음 작업을 진행한다.

UART를 초기화하는 함수 이름을 보면 UART3을 쓰는 것 같다. 나는 UART1을 쓸 거니까 일단 이 함수를 UART1을 쓰는 코드로 바꾼다.

static void MX_USART3_UART_Init(void)
{
  huart3.Instance = USART3;
  huart3.Init.BaudRate = 115200;
  huart3.Init.WordLength = UART_WORDLENGTH_8B;
  huart3.Init.StopBits = UART_STOPBITS_1;
  huart3.Init.Parity = UART_PARITY_NONE;
  huart3.Init.Mode = UART_MODE_TX_RX;
  huart3.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart3.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart3) != HAL_OK)
  {
    Error_Handler();
  }
}

원래 코드는 이러했는데, 아래처럼 고쳤다.

static void MX_USART1_UART_Init(void)
{
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }
}

이게 무슨 개발이냐라고 말할 정도로 아무것도 안했다. 그냥 UART3을 UART1로 바꿨다. 이쯤되면 그냥 날로 먹는 거다. 그리고 뭔가 동작을 눈으로 확인하려면 UART로 출력을 해야 하는데, 어떻게 출력하는지 모르겠다. 이럴땐 제조사 SDK API를 먼저 보는게 답이다. 제조사에서 예제를 다 만들어 놨으니 분명 HAL 어딘가에 UART 출력 API가 있을 것이다.

UART HAL 구현 보기

찾아보니 stm32f1xx_hal_uart.h라는 파일이 있다. 딱 봐도 UART 관련 API가 정의된 헤더 파일일것 같지 않은가? 열어보면 각종 define과 struct들이 정의되 있고 위 코드에서 사용한 HAL_UART_Init() 함수도 있다. 조금 더 내려가 보면

HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);

이렇게 UART 입출력 관련 API가 있다. 함수 이름 뒤에 IT가 붙은 것과 DMA가 붙은 것은 다른 기능이 있는 것 같다. 그냥 기본 기능만 쓸 땐 HAL_UART_Transmit()과 HAL_UART_Receive()를 쓰면 될 것 같다. 지금 나는 UART 출력만 필요하므로 HAL_UART_Transmit()이 어떻게 생겼는지 봤다. 헤더 파일이 있으니 당연히 구현 파일도 있을 터. 찾아 보니 stm32f1xx_hal_uart.c 파일이 있다. 이 파일에서 HAL_UART_Transmit() 함수 구현을 찾는다.

HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
  uint16_t* tmp;
  uint32_t tmp_state = 0;
 
  tmp_state = huart->State;
  if((tmp_state == HAL_UART_STATE_READY) || (tmp_state == HAL_UART_STATE_BUSY_RX))
  {
    if((pData == NULL) || (Size == 0))
    {
      return  HAL_ERROR;
    }
 
    /* Process Locked */
    __HAL_LOCK(huart);
 
    huart->ErrorCode = HAL_UART_ERROR_NONE;
    /* Check if a non-blocking receive process is ongoing or not */
    if(huart->State == HAL_UART_STATE_BUSY_RX) 
    {
      huart->State = HAL_UART_STATE_BUSY_TX_RX;
    }
    else
    {
      huart->State = HAL_UART_STATE_BUSY_TX;
    }
 
    huart->TxXferSize = Size;
    huart->TxXferCount = Size;
    while(huart->TxXferCount > 0)
    {
      huart->TxXferCount--;
      if(huart->Init.WordLength == UART_WORDLENGTH_9B)
      {
        if(UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, Timeout) != HAL_OK)
        {
          return HAL_TIMEOUT;
        }
        tmp = (uint16_t*) pData;
        huart->Instance->DR = (*tmp & (uint16_t)0x01FF);
        if(huart->Init.Parity == UART_PARITY_NONE)
        {
          pData +=2;
        }
        else
        { 
          pData +=1;
        }
      } 
      else
      {
        if(UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, Timeout) != HAL_OK)
        {
          return HAL_TIMEOUT;
        }
        huart->Instance->DR = (*pData++ & (uint8_t)0xFF);
      }
    }
 
    if(UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TC, RESET, Timeout) != HAL_OK)
    { 
      return HAL_TIMEOUT;
    }
 
    /* Check if a non-blocking receive process is ongoing or not */
    if(huart->State == HAL_UART_STATE_BUSY_TX_RX) 
    {
      huart->State = HAL_UART_STATE_BUSY_RX;
    }
    else
    {
      huart->State = HAL_UART_STATE_READY;
    }
 
    /* Process Unlocked */
    __HAL_UNLOCK(huart);
 
    return HAL_OK;
  }
  else
  {
    return HAL_BUSY;
  }
}

드럽게 길다. 무슨 UART 전송 구현이 이리 긴가.. 아무래도 제조사에서 만든거라 STM32F103의 UART 하드웨어가 지원하는 기능을 모두 사용하는 코드를 만들어 놓아서 그런것 같다. 코드를 쭉 읽어보면 사실 별거 없다. 파라메터로 전달하는 것들을 보면 아래 4개다.

UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout

huart는 UART1~3 중 하나다. 앞에 main.c에서 HAL_UART_Init() 함수에 넘겨 줬던 그 구조체 변수를 쓰면 된다. pData는 문자열 포인터이고 Size는 문자열 길이, Timeout은 말 그대로 타임아웃. 그래서 긴 HAL_UART_Transmit() 함수의 동작을 후려쳐 보면,

Size 개수 만큼 한 글자씩 루프 돌며

    huart->TxXferSize = Size;
    huart->TxXferCount = Size;
    while(huart->TxXferCount > 0)
    {
        huart->TxXferCount--;

UART의 Tx Enable 레지스터가 reset인지 확인해보고

    if(UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, Timeout) != HAL_OK)
    {
        return HAL_TIMEOUT;
    }

물론 타임아웃이 지날 때까지 Tx Enable 레지스터가 풀리지 않으면 Timout error 리턴~

pData 문자열 포인터를 하나씩 증가해서 UART의 Data 레지스터에 넣는다.

    huart->Instance->DR = (*pData++ & (uint8_t)0xFF);

이게 핵심이다. 다른 코드 다 지우고 위 코드만 남겨도 아마 동작할 것이다. 하지만 나는 건들이지 않겠다. 잘 동작하는 코드는 그냥 쓰는게 진리다. 특히 HAL은...

main() 함수 수정

그래서 main() 함수는 최종적으로 아래 코드처럼 고쳤다.

int main(void)
{
  HAL_Init();
  SystemClock_Config();
 
  MX_GPIO_Init();
  MX_USART3_UART_Init();
 
  while (1)
  {
    HAL_UART_Transmit(huart1, "Hello..\n", 8, 10000);
  }
}

Timeout은 단위가 밀리세컨드인데 대충 10000 정도 넣었다. 통상적인 UART 동작이라면 즉각 반응해야 하므로 아무 값이나 넣어도 된다. UART_WaitOnFlagUntilTimeout() 함수 구현을 보니 0만 아니면 되는 것 같다. 저렇게 고치고 빌드해서 보드에서 동작해 보면 Hello...가 계속 찍힌다. 어디에 찍히냐면 시리얼 터미널 프로그램에 찍힌다. 나는 리눅스에서 cutecom이라는 시리얼 터미널 프로그램을 쓴다. 예전에는 micom이런걸 썻던것 같은데, 몇 개 써본 결과 100% 만족스럽진 않아도 cutecom이 그나마 쓸만했다. 아니면 그냥 콘솔에서 telnet 명령을 써도 될듯한데 해 보진 않았다.

HAL과 main() 함수에서 호출하지 않는 함수와 소스 파일을 모두 지우고 다시 빌드해 본다. makefile 구현에 따라 소스 코드를 지우면 파일을 찾을 수 없다는 에러가 나기도 하니 makefile에서 해당 파일 이름을 찾아 지운다. 빌드가 될 때까지 반복한다.

베이스 코드

이 번편에서 특정 URL을 지칭하지 않았다. 왜냐면 인터넷에서 구할 수 있는 코드는 다들 비슷하기 때문이다. 그래서 차라리 큰 흐름과 방법을 쓰는 편이 구조를 파악하는데 더 도움이 되리라 생각해서다. 혹시 내 생각이 틀려서 이번 편이 아무런 도움이 되지 않았더라도 날 욕하지 말아 달라. 다음편 보면 되니까. 이번 편은 베이스 코드를 어떻게 만드는지를 약간 정석이 아닌 방법으로 설명한 글이다. 정석 대로라면 바닥부터 짜거나 정식으로 SDK를 다운 받아서 개발 환경을 구성해야 한다. 나는 그냥 쉬운 방법을 택했다.

댓글

lsk2017의 이미지

오늘 나빌렐라님 처음 접했는데,

강좌가 너무 좋아서 댓글 남겨요.

책도 벌써 두권 구매했습니다. (임베디드os, 컴퓨터 아나토미)

키보드 강좌(펌웨어 강좌)도 제가 찾던 강좌입니다

잘부탁드립니다 (--)(__)

나빌레라의 이미지

감사합니다.

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

AustinKim의 이미지

저도 이미 두권 책 샀습니다. (임베디드os, 컴퓨터 아나토미)

이렇게 유익한 블로그 글을 올려주셔서 감사합니다.

(개인블로그)
http://rousalome.egloos.com

나빌레라의 이미지

감사합니다.

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

댓글 달기

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
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.