안경잡이개발자

728x90
반응형

  Teensy는 USB 장치(Device) 개발용 키트로, 새로운 USB 장치를 개발하고자 할 때 아두이노(Arduino) IDE를 기반으로 하여 개발을 진행할 수 있습니다. Teensy는 USB 장치 개발용 키트라는 점에서 원하는 USB Type으로 해당 보드를 이용할 수 있습니다. 저는 Teensy 3.6 보드를 사용하고 있는데, Teensyduino를 설치한 뒤에 다음과 같이 다양한 기본적인 USB 타입들을 확인할 수 있습니다.

 

 

  Teensy는 동시에 여러 개의 인터페이스를 제공하기에 용이하다는 점에서 다목적의 USB 장치를 만들 수 있습니다. 예를 들어 동시에 마우스이면서 키보드인 USB 장치를 만들 수 있습니다. 다만 기본적으로 제공하는 USB Type이 아니라, 자신만의 USB Type을 만들고자 한다면 어떻게 해야 할까요? 새로운 보드를 추가하고 개발하는 방법을 소개합니다.

 

※ 새로운 USB 타입 정의하기 ※

 

  자신만의 USB Type을 정의하고자 할 때는 가장 먼저 boards.txt 파일을 수정해야 합니다. 일반적으로 boards.txt 파일을 열어서 수정할 때는 에디터(Editor) 프로그램을 관리자 권한으로 실행하셔야 합니다.

 

  ▶ Boards.txt 파일 경로: C:\Program Files (x86)\Arduino\hardware\teensy\avr\cores\teensy3

 

 

  boards.txt를 열면 각 Teensy 보드 버전에 따라서 다양한 보드 타입과 기타 내용들이 정의되어 있는 것을 확인할 수 있습니다. 가볍게 다음과 같은 내용을 추가하여 새로운 USB Type으로 "USB_TEST"를 추가할 수 있습니다.

 

teensy36.menu.usb.test=Test
teensy36.menu.usb.test.build.usbtype=USB_TEST
teensy36.menu.usb.test.fake_serial=teensy_gateway

 

  그러면 다음과 같은 형태로 들어가게 될 겁니다.

 

 

  이제 boards.txt를 저장한 뒤에 아두이노 IDE를 관리자 권한으로 실행합니다. 그러면 다음과 같이 새로운 USB Type으로 "Test"가 추가되어 있는 것을 확인할 수 있습니다.

 

 

※ USB Description 정의하기 ※

 

  (참고: 아두이노 IDE를 관리자 권한으로 실행했다면, 소스코드 편집기도 관리자 권한으로 실행해야 합니다.)

 

  USB 장치는 기본적으로 우리의 컴퓨터 (Host)에 꽂았을 때 기본적인 Description 정보를 보냅니다. "나는 어떤 기능을 제공하는 USB 장치야."라는 의미라고 보시면 됩니다. 당연히 새로운 USB 장치를 개발한다면 Description 정보를 수정할 필요가 있을 것입니다.

 

  Teensy 3.6 버전에서는 usb_desc.h 파일에서 그러한 정보를 담고 있습니다. 이 소스코드를 수정하면 알아서 Teensy 보드가 연결될 때 호스트(Host)에게 자신의 수정된 Description 정보를 보냅니다.

 

 

  예를 들어 다음과 같이 작성할 수 있습니다. 저는 간단히 키보드와 마우스의 기능만을 포함하고 있는 새로운 USB Type을 정의했습니다. 참고로 KEYBOARD_INTERFACE나 MOUSE_INTERFACE 등은 usb_keyboard.h와 usb_mouse.h 코드를 사용할 수 있도록 해주는 전처리 구문입니다. 다시 말해, 일단 저는 이미 Teensy 코어(Core)에 정의되어 있는 키보드와 마우스 기능을 그대로 사용하고자 합니다. (만약 이러한 코어 코드까지 다 새롭게 만드시고 싶을 수 있는데요. 이 또한 원하신다면 처음부터 다 만드실 수 있습니다.)

 

#elif defined(USB_TEST)
  /* USB 장치의 기본적인 ID와 클래스 정보 */
  #define VENDOR_ID 0x16C0
  #define PRODUCT_ID 0x0707
  #define DEVICE_CLASS 0xEF // 기타 USB 클래스 (Miscellaneous) 
  #define DEVICE_SUBCLASS 0x02 // 기타 USB 클래스 (Miscellaneous)의 서브 클래스 
  #define DEVICE_PROTOCOL 0x01 // 다양한 인터페이스 사용 (Interface Association Descriptor)

  /* USB 장치의 이름 정보 */ 
  #define MANUFACTURER_NAME {'T','e','e','n','s','y','d','u','i','n','o'}
  #define MANUFACTURER_NAME_LEN 11
  #define PRODUCT_NAME {'M','y',' ','T','e','s','t',' ','B','o','a','r','d'}
  #define PRODUCT_NAME_LEN 13

  /* USB 장치의 엔드포인트 및 인터페이스 정보 */
  // 엔드포인트 0번은 Control Transfer 목적으로 고정 
  #define EP0_SIZE 64
  #define NUM_ENDPOINTS 6
  #define NUM_USB_BUFFERS 30
  #define NUM_INTERFACE 5

  /* CDC 관련 정보 */
  #define CDC_IAD_DESCRIPTOR 1
  #define CDC_STATUS_INTERFACE 0
  #define CDC_DATA_INTERFACE 1 // 시리얼 (Serial) 
  #define CDC_ACM_ENDPOINT 1
  #define CDC_RX_ENDPOINT 2
  #define CDC_TX_ENDPOINT 3
  #define CDC_ACM_SIZE 16
  #define CDC_RX_SIZE 64
  #define CDC_TX_SIZE 64

  /* 키보드 (Keyboard) */
  #define KEYBOARD_INTERFACE 2
  #define KEYBOARD_ENDPOINT 4
  #define KEYBOARD_SIZE 8
  #define KEYBOARD_INTERVAL 1

  /* 키보드 미디어 키 (Keyboard Media Keys) */
  #define KEYMEDIA_INTERFACE 3
  #define KEYMEDIA_ENDPOINT 5
  #define KEYMEDIA_SIZE 8
  #define KEYMEDIA_INTERVAL 4

  /* 마우스 (Mouse) */
  #define MOUSE_INTERFACE 4
  #define MOUSE_ENDPOINT 6
  #define MOUSE_SIZE 8
  #define MOUSE_INTERVAL 2

  /* 각 엔드포인트의 데이터 송수신 관련 정보 */
  #define ENDPOINT1_CONFIG ENDPOINT_TRANSMIT_ONLY // CDC ACM
  #define ENDPOINT2_CONFIG ENDPOINT_RECEIVE_ONLY // CDC RX
  #define ENDPOINT3_CONFIG ENDPOINT_TRANSMIT_ONLY // CDC TX
  #define ENDPOINT4_CONFIG ENDPOINT_TRANSMIT_ONLY // 키보드 TX (전송만) 
  #define ENDPOINT5_CONFIG ENDPOINT_TRANSMIT_ONLY // 키보드 미디어 키 TX (전송만) 
  #define ENDPOINT6_CONFIG ENDPOINT_TRANSMIT_ONLY // 마우스 TX (전송만)

 

  또한 USB Function Driver에는 HID (Human Interface Device), CDC (Communication Device Class), MSC (Mass Storage Class) 등이 포함됩니다. 이 때 Mouse, Keyboard 등은 기본적으로 HID에 속하며, 시리얼(Serial) 통신은 CDC에 속합니다.  그래서 usb_desc.h에서 CDC 부분을 빼버리면 아두이노에서 Serial 라이브러리를 사용할 수 없습니다.  마찬가지로 Mouse나 Keyboard 부분을 빼버리면 아두이노에서 Mouse, Keyboard 라이브러리를 사용할 수 없답니다.

 

  참고로 usb_desc.h 부분의 코드가 잘못되어 있으면 호스트(Host)에서 USB 장치로 인식조차 안 할 수 있습니다. 만약에 usb_desc.h 파일을 수정한 뒤에 Teensy 보드로 업로드를 했는데, 아예 인식을 안 하는 경우 (특히 포트조차 못 잡는 경우) Teensy 보드에 있는 작은 버튼을 눌러서 초기화 해주셔야 합니다. 

 

※ 예제 소스코드 ※

 

  예제 소스코드는 다음과 같습니다. Mouse, Keyboard, Serial 라이브러리를 모두 사용합니다.

 

void setup() {

}

void loop() {
    int i;
    for (i = 0; i < 40; i++) {
        Mouse.move(2, -1);
        delay(100);
    }
    delay(1000);
    Serial.println("Hello");
    Keyboard.print("Hello World");
}

 

※ 실행 결과 ※

 

  컴파일 이후에 Teensy 보드에 업로드를 하면 성공적으로 USB 장치가 동작합니다. 반복적으로 마우스를 움직이며, 키보드로 "Hello World"를 입력하고, 시리얼 통신으로 "Hello"라는 문자열을 전송합니다. 리눅스 환경에서 lsusb 명령어를 이용해 USB 장치의 상세 Description을 확인하면 다음과 같습니다.

 

  사실 Vendor ID로 사용한 0x16c0는 이미 정의되어 있네요. 이 값 또한 원래는 바꿔주어야 합니다. 

 

 

  또한 이어서 인터페이스(Interface) 정보를 확인해 보시면, 인터페이스마다 CDC, Keyboard, Mouse 등의 각기 다른 기능이 제공되는 것을 알 수 있습니다. CDC를 이용한 시리얼 통신은 Bulk Transfer를 기반으로 하고 있으며, HID를 이용한 Keyboard 기능은 Interrupt Transfer를 기반으로 하고 있네요. 각각 패킷 크기도 64 바이트와 8 바이트로 다릅니다. 사실 이 내용들도 대부분 usb_desc.h 파일에 정의되어 있습니다.

 

728x90
반응형

728x90
반응형

USB 전송 형태(Transfer Type)

  USB 프로토콜을 제대로 이해하기 위해서 공부할 것들이 몇 가지 있다.

 

  그 중에서 꼭 알아야 하는 것은 바로 USB의 전송 형태(Transfer Type)이다. 기본적으로 USB 프로토콜은 호스트(PC)와 디바이스(USB 장치)와의 통신에 관한 것이다.

 

  구체적으로 그림과 함께 설명할 예정인데, 참고해야 할 점은 '회색(Gray)' 부분은 호스트(PC)가 보내는 패킷이고, '하얀색(White)' 부분은 디바이스(Device)가 보내는 패킷이다.

 

 

1. 등시성 전송(Isochronous Transfer)

 

  첫 번째로 알아 볼 USB 전송 타입은 등시성 전송 타입이다. 이는 데이터의 전송 시간을 보증할 때 사용하는 타입이다. 데이터에 오류가 있어도 재전송을 하지 않으며, 호스트(PC)에서는 등시성 전송을 위해 시간을 스케줄링한다.

 

  일반적으로 이어폰과 같이 실시간으로 데이터를 받아서 송출해야 하는 디바이스에서 사용된다. 생각해 보면, 이어폰처럼 실시간으로 데이터를 주고 받아야 하는 경우에서 데이터에 약간의 오류가 있다고 재전송을 시킬 수는 없을 것이다.

 

  아무튼 등시성 전송은 다음과 같이 토큰 패킷과 데이터 패킷으로 처리된다. 흔히 이어폰의 경우에는 호스트가 OUT 패킷을 보냄으로써 "나 지금 음악 스트림 보낼거야."라고 말한 뒤에, 이어폰 디바이스한테 DATA 패킷을 연달아 보내는 것이다. 여기에서 토큰(Token) 패킷이란, 데이터의 방향을 알려주기 위해 호스트가 디바이스에게 보내는 패킷이다.

 

  참고: OUT 패킷은 말 그대로 호스트 PC가 디바이스에게 보낼 데이터가 있다는 의미이다. 호스트 입장에서 받아야 할 데이터가 있다면 호스트가 먼저 IN 패킷을 보낸 뒤에, 디바이스로부터 데이터를 받게 된다.

 

 

2. 인터럽트 전송(Interrupt Transfer)

 

  작은 크기의 데이터를 전달할 때 사용하는 타입이다. 일반적으로 마우스와 같이 클릭할 때만, 일시적으로 호스트(PC)가 데이터를 읽어들이는 경우에서 사용된다. 예시로는 키보드, 마우스 등이 있다.

 

  인터럽트 전송은 말 그대로 비주기적인 형태의 전송이다. 인터럽트 요청은 디바이스에 의해서 차례대로 큐에 담기게 되고, 호스트가 USB 디바이스한테, "너 보내 줄 데이터 있니?"라고 물어보면, 그 때 큐에 담겨진 인터럽트 요청들이 디바이스에서 호스트로 보내진다.

 

  예를 들어 우리가 키보드를 PC에 꽂은 뒤에, 타자를 친다고 가정해보자. 우리가 키보드에 입력한 데이터들은 일시적으로 키보드 디바이스의 내부 메모리 큐에 담기게 되고, 호스트가 폴링(Polling)을 하게 되면 그제서야 호스트에서 우리가 입력했던 문자들을 받아들이게 된다. (여기에서 알 수 있듯이, 호스트 입장에서는 주기적으로 폴링을 해야 한다.) 어느 정도의 주기로 폴링을 할 지는 디바이스의 엔드포인트 디스크립터(Endpoint Descriptor)에 명시되어 있다.

 

  구체적인 과정은 다음과 같다.

 

  ▶ IN 토큰 패킷: 호스트가 데이터를 받는 경우이다. 호스트가 IN 패킷을 보내면, 디바이스의 큐에 데이터가 저장된 경우 데이터를 보내주게 된다. 데이터를 잘 받았으면 ACK를 보낸다. 디바이스의 큐가 비어있으면 (인터럽트 메시지가 없으면) 디바이스에서는 NAK를 보낸다. 그리고 오류가 발생한 상황에서는 디바이스가 STALL을 보내는데, 이러면 호스트가 IN 패킷을 다시 보낸다. 군더더기 없이 깔끔하다.

 

  ▶ OUT 토큰 패킷: 호스트가 데이터를 보내는 경우이다. 호스트가 디바이스로 데이터를 보내는 경우에는, OUT 패킷을 발행한 뒤에 데이터를 보낸다. 디바이스 입장에서는 데이터를 차례대로 받는데, 이는 엔드포인트 버퍼에 담기게 된다. 아직 이전 패킷을 다 처리하지 못해서 엔드포인트 버퍼가 비어 있지 않다면, 디바이스는 NAK를 보낸다. 마찬가지로 오류 상황일 때는 STALL을 보낸다.

 

3. 벌크 전송(Bulk Transfer)

 

  대량의 데이터를 신뢰성 있게 전달할 때 사용하는 타입이다. 데이터 패킷 전송 과정에서 오류가 발생하면 재전송을 요구하며, 등시성 전송과는 다르게 전송 속도는 보증하지 않는다. 대표적인 예시로는 USB 저장 장치(Mass Storage)가 있다.

 

  기본적으로 Bulk Transfer와 Interrupt Transfer는 사실상 같은 트랜잭션 형태를 보인다. 그림 자체도 비슷한 걸 확인할 수 있는데, 벌크와 인터럽트의 차이점은 인터럽트에서는 전송요구에 대한 시간의 주기를 설정할 수 있다는 것이다. 즉, 호스트가 주기적으로 폴링을 수행하도록 요구할 수 있다.

 

  예를 들어 우리가 마우스를 클릭했는데 0.2초 뒤에 클릭이 된 것처럼 처리가 된다면 매우 불편할 것이다. 반면에 대용량 파일을 보내는 과정에서 0.2초 정도의 딜레이는 큰 문제가 없을 것이다. 더불어 Bulk Transfer에서는 1번의 트랜잭션으로 데이터가 전부 전송되지 않을 수 있기 때문에, 데이터를 분할해서 트랜잭션을 반복한다.

 

 

4. 제어 전송(Control Transfer)

 

  USB 기기에게 명령을 내리거나 상태 관련 연산을 수행하기 위해 사용하는 타이다. 모든 USB 기기는 제어 전송을 위해 엔드포인트(Endpoint) 0번을 사용하며, 어떤 데이터를 보낼지 또한 USB 규격으로 정해져 있다. 제어 전송의 경우 모든 USB 기기가 사용하는 전송 타입이다. 기본적으로 처음에 USB 디바이스를 PC에 꽂았을 때부터, 제어 전송을 통해 디스크립터(Descriptor) 정보를 받게 되기 때문이다. 이러한 작업들을 수행하는 것이 바로 제어 전송(Control Transfer)이다.

 

  기본적으로 제어 전송은 3가지 단계로 구성된다. 차례대로 설정 단계(Setup Stage), 데이터 단계(Data Stage), 상태 단계(Status Stage)이다.

 

  1) 설정 단계(Setup Stage): 제어 전송에서 가장 기본적인 단계이다. 이 단계는 초기 설정을 수행하며 세 개의 패킷(Token, Data, Handshake)으로 구성된다. 먼저 토큰 패킷으로는 설정을 진행할 Address 정보와 Endpoit 번호가 담기게 된다. 이후에 정확히 어떤 내용을 설정할 것인지 데이터 패킷에 담아서 보낸다. 예를 들어, 처음에 USB 디바이스에 연결된 상태라면 Descriptor 정보를 보내달라는 내용을 데이터 패킷에 담아서 보낼 것이다.

 

 

  2) 데이터 단계(Data Stage): 데이터 단계는 필요하다면 존재할 수 있다. 만약 데이터 단계가 있다면, 설정 단계에서 '데이터 단계에서 얼마나 많은 패킷이 전달될 지' 미리 설정이 된다. 데이터 단계는 다수의 IN 토큰 (호스트가 데이터를 받는 경우)과 OUT 토큰 (호스트가 데이터를 보내는 경우)으로 구성되는 경우가 많다.

 

  예를 들어 '설정 단계'에서 디바이스에게 Descriptor 정보를 달라는 패킷을 보냈다면, 이 데이터 단계에서는 IN 토큰을 발행해서 디바이스로부터 Descriptor 정보를 받게 된다.

 

 

  3) 상태 단계(Status Stage): 제어 요청의 처리 상태를 보고하기 위해 사용되는 단계이다. 즉, 정상적으로 제어 전송이 완료되었는지를 확인하는 마지막 단계라고 할 수 있다. 

 

  데이터 단계에서 호스트가 IN 토큰을 보냈다면, 호스트 입장에서는 데이터를 모두 다 정상적으로 받았는지 궁금하다. 그래서 OUT 토큰과 길이가 0인 데이터 패킷을 하나 보내서, 디바이스한테 "너 데이터 다 보낸 거야?"라고 물어본다. 그래서 디바이스에서 데이터를 다 보냈다면 호스트에게 ACK를 보냄으로써 현재의 COMMAND가 종료되었음을 알린다. 혹은 NAK를 보내서 아직 더 처리할 게 남았다는 것을 알리게 된다.

 

  반면에 데이터 단계에서 호스트가 OUT 토큰을 보냈다면, 정상적으로 데이터가 다 보내졌는지 확인하기 위해서 IN 토큰을 하나 보내서 디바이스에게 정상적으로 통신이 완료된 건지 물어보는 것이다. 

 

  * 상태 단계에서 OUT 토큰을 보내는 경우는 다음과 같다. (데이터 단계에서 호스트가 IN 토큰을 보낸 경우)

 

 

  * 상태 단계에서 IN 토큰을 보내는 경우는 다음과 같다. (데이터 단계에서 호스트가 OUT 토큰을 보낸 경우)

 

 

USB 기기 클래스(Device Class)

  USB 기기 클래스에 따라서 사용하는 전송 형태(Transfer Type)에도 차이점이 있다. 앞서 언급했듯이, 마우스나 키보드와 같은 디바이스들은 인터럽트 전송 타입을 많이 사용할 것이다. USB 기기 클래스를 간략히 소개하면 다음과 같은 것들이 있다.

 

  * MSC (Mass Storage Class): USB 외부 저장장치에 대한 클래스로, Bulk Transfer를 사용한다.
  * ADC (Audio Device Class): 오디오 장치에 대한 클래스로, Isochronous Transfer를 사용한다.

  * HID (Human Interface Device): I/O 장치(키보드, 마우스) 같은 것에 대한 클래스로, Interrupt Transfer를 사용한다.

 

  이외에도 MTP (Media Transfer Protocol)과 같은 클래스도 있다. 이 클래스는 MSC (Mass Storage Class)의 대용으로 사용될 수 있다. 기본적으로 USB 시스템에서는 Host가 주도권을 가지게 되는데, MSC의 경우 호스트가 마운트를 하고 나면, 해당 USB 저장 장치에 대해서 절대적인 제어권을 가진다. MSC는 블록 인터페이스 수준에서 동작하기 때문이다.

  반면에 MTP는 논리적인 파일 수준의 송수신이 가능하다. 예를 들어 USB를 MTP로 제공할 때는, 특정 파일에 접근할 때는 암호를 입력하도록 하거나 할 수도 있다. 즉, MTP나 MSC나 모두 내부적으로 Bulk Transfer와 비슷한 기능을 이용하지만 목적 자체가 다르다는 점이 특징이다.

728x90
반응형