안경잡이개발자

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