안경잡이개발자

728x90
반응형

  이번 시간에는 Teensy 보드(Board) 위에서 동작하는 HID 소프트웨어를 개발하는 방법을 소개하고자 합니다. 단, HID 코어(Core) 라이브러리를 처음부터 만드는 방법을 소개합니다. 즉, 자신만의 USB 프로토콜을 만들어 보고 싶은 사람에게 도움이 될 만한 글입니다. 본 예시는 RawHID 소스코드를 기반으로 하지만, 특히 HID가 아닌 본인만의 USB 클래스를 정의하고 싶으신 분들에게도 도움이 될 것입니다.

 

※ Teensy USB 디바이스 개발하기 ※

 

  일단 Teensy의 경우에는 아두이노 IDE 위에서 동작한다는 특징이 있는데요. 사용자 라이브러리는 다음의 경로에 만들어 주면 됩니다.

 

  ▶ 아두이노 사용자 라이브러리(스케치북) 경로: C:\Users\{사용자명}\Documents\Arduino\libraries

 

 

  기본적인 HID 기능을 이용할 수 있도록 MyHID라는 이름의 사용자 라이브러리를 생성하도록 하겠습니다. 폴더 구성은 다음과 같이 해주시면 됩니다. 일반적으로 다음과 같이 하나의 헤더 파일(.h)과 하나의 C++ 소스 코드(.cpp)로 라이브러리를 작성할 수 있습니다. 그리고 자신의 라이브러리를 어떻게 사용할 수 있을지를 간략히 보여주는 examples 폴더를 만드시면 됩니다.

 

 

  먼저 MyHID.h를 작성해 보겠습니다. 기본적으로 2개의 함수가 존재하도록 작성할 수 있는데요. 바로 hid_recv()와 hid_send()입니다. 하나는 HID 프로토콜을 이용해 데이터를 받는 함수, 하나는 보내는 함수입니다.

 

  ▶ hid_recv(): 호스트(Host)로부터 buffer만큼의 데이터를 받아 옵니다.

  ▶ hid_send(): 호스트(Host)로부터 buffer만큼의 데이터를 전송합니다.

 

  USB 디바이스(Device)는 기본적으로 여러 개의 인터페이스(Interface)를 포함할 수 있는데요. 개발 단계에서부터 어떠한 인터페이스를 HID 목적으로 사용할 지 설정할 수 있습니다. 구체적으로 Teensy 쪽의 usb_desc.h에 그러한 기본적인 인터페이스 번호를 포함한 설정 정보가 적혀 있습니다. (조금 있다가 다시 언급하겠습니다.) 아무튼 그러한 usb_desc.h를 불러와 이용하는 방식으로 소스코드가 작성됩니다.

 

#ifndef MyHID_H
#define MyHID_H

#include "usb_desc.h" // for contants (RAWHID_INTERFACE, RAWHID_RX_ENDPOINT, RAWHID_TX_ENDPOINT, ...)

#if defined(RAWHID_INTERFACE)

#include <inttypes.h>

int available(void);
int hid_recv(void *buffer, uint32_t timeout);
int hid_send(const void *buffer, uint32_t timeout);

#endif // RAWHID_INTERFACE
#endif // MyHID_H

 

  이제 MyHID.cpp를 작성할 수 있습니다. hid_recv() 함수와 hid_send() 함수를 실제로 구현하는 부분입니다. 구현을 위해서 usb_dev.h를 이용하는데요, 기본적으로 usb_dev.h 헤더 파일에는 usb_rx()나 usb_tx()와 같은 기본적인 기능들이 구현되어 있습니다. 그러한 코어 USB 기능을 이용하여 실제로 HID 관련 라이브러리를 구현하게 됩니다. 확인해 보시면 전송을 위한 timeout이 있어서, 너무 오랫동안 전송이 안 되는 데이터는 버리게 됩니다.

 

  데이터를 주고 받는 usb_rx()나 usb_tx()를 확인해 보시면, 특정한 엔드포인트(Endpoint)를 이용하는 것을 알 수 있습니다. 엔드포인트데이터를 주고 받기 위한 주소와 같은 것이라고 보시면 됩니다.

 

#include "usb_dev.h" // for USB core functions (usb_rx, usb_tx, ...)
#include "core_pins.h" // for yield(), millis()
#include <string.h> // for memcpy()
#include "MyHID.h"

#ifdef RAWHID_INTERFACE
#if F_CPU >= 20000000

int available(void)
{
	uint32_t count;

	if (!usb_configuration) return 0;
	count = usb_rx_byte_count(RAWHID_RX_ENDPOINT);
	return count;
}

int hid_recv(void *buffer, uint32_t timeout)
{
	usb_packet_t *rx_packet;
	uint32_t begin = millis();

	while (1) {
		if (!usb_configuration) return -1;
		rx_packet = usb_rx(RAWHID_RX_ENDPOINT);
		if (rx_packet) break;
		if (millis() - begin > timeout || !timeout) return 0;
		yield();
	}
	memcpy(buffer, rx_packet->buf, RAWHID_RX_SIZE);
	usb_free(rx_packet);
	return RAWHID_RX_SIZE;
}

// Maximum number of transmit packets to queue so we don't starve other endpoints for memory
#define TX_PACKET_LIMIT 4

int hid_send(const void *buffer, uint32_t timeout)
{
	usb_packet_t *tx_packet;
	uint32_t begin = millis();

	while (1) {
		if (!usb_configuration) return -1;
		if (usb_tx_packet_count(RAWHID_TX_ENDPOINT) < TX_PACKET_LIMIT) {
			tx_packet = usb_malloc();
			if (tx_packet) break;
		}
		if (millis() - begin > timeout) return 0;
		yield();
	}
	memcpy(tx_packet->buf, buffer, RAWHID_TX_SIZE);
	tx_packet->len = RAWHID_TX_SIZE;
	usb_tx(RAWHID_TX_ENDPOINT, tx_packet);
	return RAWHID_TX_SIZE;
}

#endif // F_CPU
#endif // RAWHID_INTERFACE

 

  이제 이러한 라이브러리를 이용하는 실질적인 USB 디바이스의 아두이노 코드를 작성할 수 있습니다.

 

 

  소스코드는 다음과 같이 작성할 수 있습니다. 소스코드를 간단히 설명하자면, 앞서 정의한 HID 코어 라이브러리를 이용해서 데이터를 보내고 받도록 작성되어 있습니다. 일단 코드의 앞 부분에서는 데이터를 받아서 이를 바로 출력하는 부분이 작성되어 있으며, 코드의 뒷 부분에서는 2초마다 한 번씩 64바이트의 데이터를 보내는 부분이 작성되어 있습니다.

 

#include <MyHID.h>

void setup() {
}

// RawHID packets are always 64 bytes.
byte buffer[64];
elapsedMillis msUntilNextSend;

void loop() {
  int n;
  n = hid_recv(buffer, 0); // 0 timeout = do not wait
  if (n > 0) {
    String received = String((char*)buffer);
    Serial.println(received);
  }
  // every 2 seconds, send a packet to the computer (Host PC)
  if (msUntilNextSend > 2000) {
    msUntilNextSend = msUntilNextSend - 2000;
    buffer[0] = 0x12;
    buffer[1] = 0x34;
    for (int i = 2; i < 62; i++) {
      buffer[i] = 0;
    }
    buffer[62] = 0x56;
    buffer[63] = 0x78;
    // send the packet
    n = RawHID.send(buffer, 100);
    if (n > 0) {
      Serial.println("Transmit packet ");
    } else {
      Serial.println("Unable to transmit packet");
    }
  }
}

 

  실제로 USB Device의 설정 정보는 Teensy 코어 라이브러리 위치의 usb_desc.h 헤더 파일에 정의되어 있습니다. 우리가 Teensy 장치를 RawHID로 설정하게 되면, 다음과 같이 각각의 Description 정보가 구성됩니다. 확인해 보시면, 기본적으로 Vendor ID로 0x16C0을, Product ID로 0x0486을 사용하도록 되어 있네요.

 

 

  여기에서 핵심이 되는 내용은 Vendor ID, Product ID인데요. 이 값을 통해서 Host OS가 USB Device를 정확히 찾아 접근할 수 있습니다. 또한 Product Name은 "Teensyduino RawHID"인데요, 이게 나중에 Host OS에서 출력될 USB Device의 이름이 됩니다.

 

  또한 HID 통신을 위해서 데이터를 주고 받으려면 데이터를 받는 엔드포인트(Endpoint)와 데이터를 보내는 엔드포인트(Endpoint)를 정확히 정의해야 합니다. 확인해 보시면 송신(Transmit)을 위한 엔드포인트는 3번이고, 수신(Receive)을 위한 엔드포인트는 4번으로 정의된 것을 확인할 수 있습니다. 이러한 정보는 실제로 Description 정보가 되어 Host PC로 전송됩니다. Host는 이후에 이러한 엔드포인트를 이용해서 HID 통신을 진행하게 되는 것이죠.

 

  이제 최종적으로 만들어진 HelloWorld.ino 파일을 Teensy에 업로드 해주세요.

 

 

  그럼 이제 성공적으로 우리의 Teensy는 HID 기능을 위한 목적으로 동작하게 됩니다.

 

※ Host 프로그램 개발하기 ※

 

  Host 프로그램의 소스코드는 다음과 같이 구성됩니다.

 

  ▶ hid_WINDOWS.c: HID 기능을 위한 기본 라이브러리 (윈도우 전용)

  hid_LINUX.c: HID 기능을 위한 기본 라이브러리 (리눅스 전용)

   hid.h: HID 라이브러리를 위한 헤더 파일

   Makefile: 컴파일을 진행하기 위한 설정 파일

   my_hid.c: 메인 함수가 포함된 실제 소스코드

 

  먼저 hid_WINDOWS.c와 hid_LINUX.c의 소스코드는 다음의 경로에서 찾아보실 수 있습니다. (참고로 소스코드를 가져오신 뒤에, printf() 함수를 보이지 않도록 처리한 부분을 제거하시면 디버깅에 도움이 됩니다.)

 

  https://github.com/ndb796/Teensy-RawHID-SD/tree/master/host

 

  먼저 hid.h는 다음과 같이 작성할 수 있습니다. Host OS 쪽에서도 마찬가지로 USB 디바이스로부터 데이터를 받을 수 있어야 하기 때문에, 송신(Send) 함수와 수신(Receive) 함수로 구성됩니다.

 

int rawhid_open(int max, int vid, int pid, int usage_page, int usage);
int rawhid_recv(int num, void *buf, int len, int timeout);
int rawhid_send(int num, void *buf, int len, int timeout);
void rawhid_close(int num);

 

  그리고 Makefile은 다음과 같이 작성할 수 있습니다. 참고로 빌드(Build)는 리눅스 OS에서 진행하셔야 합니다. 리눅스 OS에서 윈도우 전용 실행 파일(.exe)도 만들 수 있습니다. 일단 아래 Makefile은 LINUX 환경에서 실행할 수 있는 실행 파일이 나오도록 작성되어 있습니다. 윈도우용 실행 파일을 만드실 때는 OS의 값으로 WINDOWS를 넣어주시면 됩니다.

 

OS = LINUX
# OS = WINDOWS
PROG = my_hid

ifeq ($(OS), LINUX)
TARGET = $(PROG)
CC = gcc
STRIP = strip
CFLAGS = -Wall -O2 -DOS_$(OS)
LIBS = -lusb
else ifeq ($(OS), WINDOWS)
TARGET = $(PROG).exe
CC = i686-w64-mingw32-gcc
STRIP = i686-w64-mingw32-strip
CFLAGS = -Wall -O2 -DOS_$(OS)
LIBS = -lhid -lsetupapi
endif

OBJS = $(PROG).o hid.o

all: $(TARGET)

$(PROG): $(OBJS)
	$(CC) -o $(PROG) $(OBJS) $(LIBS)
	$(STRIP) $(PROG)

$(PROG).exe: $(PROG)
	cp $(PROG) $(PROG).exe

hid.o: hid_$(OS).c hid.h
	$(CC) $(CFLAGS) -c -o $@ $<

clean:
	rm -f *.o $(PROG) $(PROG).exe $(PROG).dmg
	rm -rf tmp

 

  이제 my_hid.c는 다음과 같이 작성할 수 있습니다. 확인해 보시면 계속해서 데이터를 받고, 보내는 작업이 수행되는 것을 확인할 수 있는데요. 기본적으로 타임아웃(Timeout)이 걸려 있어서 무한정 대기하지는 않기 때문에, 데이터를 잘 주고 받으며 동작하게 되는 것입니다.

 

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>

#if defined(OS_LINUX)
#include <sys/ioctl.h>
#include <termios.h>
#elif defined(OS_WINDOWS)
#include <conio.h>
#endif

#include "hid.h"

static char get_keystroke(void);

int main()
{
	int i, r, num;
	char c, buf[64];

	// C-based example is 16C0:0480:FFAB:0200
	r = rawhid_open(1, 0x16C0, 0x0480, 0xFFAB, 0x0200);
	if (r <= 0) {
		// Arduino-based example is 16C0:0486:FFAB:0200
		r = rawhid_open(1, 0x16C0, 0x0486, 0xFFAB, 0x0200);
		if (r <= 0) {
			printf("no rawhid device found\n");
			return -1;
		}
	}
	printf("found rawhid device\n");

	while (1) {
		// check if any Raw HID packet has arrived
		num = rawhid_recv(0, buf, 64, 220);
		if (num < 0) {
			printf("\nerror reading, device went offline\n");
			rawhid_close(0);
			return 0;
		}
		if (num > 0) {
			printf("\nrecv %d bytes:\n", num);
			for (i=0; i<num; i++) {
				printf("%02X ", buf[i] & 255);
				if (i % 16 == 15 && i < num-1) printf("\n");
			}
			printf("\n");
		}
		// check if any input on stdin
		while ((c = get_keystroke()) >= 32) {
			printf("\ngot key '%c', sending...\n", c);
			buf[0] = c;
			for (i=1; i<64; i++) {
				buf[i] = 0;
			}
			rawhid_send(0, buf, 64, 100);
		}
	}
}

#if defined(OS_LINUX)
static int _kbhit() {
	static const int STDIN = 0;
	static int initialized = 0;
	int bytesWaiting;

	if (!initialized) {
		// Use termios to turn off line buffering
		struct termios term;
		tcgetattr(STDIN, &term);
		term.c_lflag &= ~ICANON;
		tcsetattr(STDIN, TCSANOW, &term);
		setbuf(stdin, NULL);
		initialized = 1;
	}
	ioctl(STDIN, FIONREAD, &bytesWaiting);
	return bytesWaiting;
}
static char _getch(void) {
	char c;
	if (fread(&c, 1, 1, stdin) < 1) return 0;
	return c;
}
#endif

static char get_keystroke(void)
{
	if (_kbhit()) {
		char c = _getch();
		if (c >= 32) return c;
	}
	return 0;
}

 

  결과적으로 리눅스 Host PC 입장에서 전체 소스코드는 다음과 같이 구성됩니다.

 

 

  이제 Teensy Board를 리눅스 OS에 연결합니다. 저는 가상 머신(Virtual Machine)에서 실습을 진행하고 있으므로, PC에 꽂혀 있는 USB를 가상 머신에서 인식할 수 있도록 USB 장치 설정을 해주었습니다.

 

 

  이제 소스코드를 컴파일하고, my_hid 프로그램을 실행하시면 됩니다. 참고로 리눅스에서는 알 수 없는 USB 장치와 통신하기 위해서는 루트(Root) 권한이 필요합니다. 결과적으로 다음과 같이 프로그램이 실행됩니다.

 

 

  이후에 다음과 같이 2초마다 한 번씩 USB 장치로부터 64 바이트의 데이터가 날라오는 것을 확인할 수 있습니다. 또한 Host 입장에서도 어떠한 알파벳 키를 누르면 해당 데이터가 USB 장치로 전달됩니다. 서로 데이터를 주고 받는 것을 알 수 있습니다.

 

 

  또한 Makefile 코드에서 빌드 대상(Target)을 윈도우로 설정하여 컴파일하면 다음과 같이 윈도우 호스트 실행 파일이 생성됩니다. (만약에 이미 리눅스 버전으로 컴파일을 해서 my_hid 실행 파일이 존재한다면, 이를 모두 삭제한 뒤에 다시 컴파일을 진행하셔야 합니다.)

 

 

  다음과 같이 윈도우 호스트에서 실행을 해도 정상적으로 동작하는 것을 확인할 수 있습니다.

 

 

  최종적으로 호스트(Host)와 USB 장치가 통신하는 내용을 출력한 사진은 다음과 같습니다.

 

 

  이제 이러한 소스코드 예시를 조금씩 수정해서 자신만의 USB 프로토콜 상에서 동작하는 USB 장치를 개발할 수 있게 되었습니다. 현재 예시는 RawHID를 토대로 하여 작성된 예시이지만, HID가 아닌 본인만의 USB 클래스를 정의하고 출시할 때에도 이와 비슷한 과정을 거칠 것이라는 것을 알 수 있습니다.

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
반응형