USB HID – Human Interface Device Class: Exemplo com a placa FRDM-KL25Z

A classe de dispositivos USB HID foi inicialmente criada para que dispositivos de entrada de dados pudessem ser adicionados facilmente em um computador. Alguns exemplos são: teclados, mouse, joystick, entre outros. Essa classe será tomada como exemplo por ser a mais simples.

O artigo apresenta algumas definições gerais do protocolo USB. No final, um exemplo de aplicação executado no PC para controlar um LED da placa Freedom Board KL25Z é demonstrado. Em cada tópico algumas referências serão citadas para consulta.

Introdução ao protocolo USB

Quando conectamos um dispositivo USB no computador, o SO se encarrega de exportar as informações sobre o dispositivo. Essas informações são obtidas a partir dos descritores USB.

Propriedades de um dispositivo USB.
Figura 1: Propriedades de um dispositivo USB.

Essas informações são obtidas com o dispositivo conectado, utilizando requisições do tipo GetDescriptor definidas no protocolo USB. Essa requisição é estruturada por uma operação de controle chamada de Setup que é enviada para o dispositivo USB. Os elementos que compõe essa operação são ilustrados abaixo.

bmRequestTypebRequestwValuewIndexwLengthData

Esses campos especificam uma operação de controle. Por exemplo, os campos bmRequestType e bRequest indicam a operação GetDescriptor quando os respectivos valores são: 0x80 e 0x06. Outras operações são apresentadas neste link: Setup Packet.

OffsetCampoTamanho em bytesTipoDescrição
0bmRequestType1Bit-MapD7 Data Phase Transfer Direction
0 = Host to Device
1 = Device to Host
D6..5 Type
0 = Standard
1 = Class
2 = Vendor
3 = Reserved
D4..0 Recipient
0 = Device
1 = Interface
2 = Endpoint
3 = Other
4..31 = Reserved
1bRequest1Value

Request

2wValue2Value

Value

4wIndex2Index or Offset

Index

6wLength2Count

Number of bytes to transfer if there is a data phase

Alguns campos dos descritores podem especificar uma referência para outro descritor. Por exemplo, algumas configurações são apenas mensagens (strings), como o nome do dispositivo que foi conectado. Para tal, existem descritores de string.

O tipo do descritor é indicado pelo campo wValue. Já o campo wIndex é utilizado conforme o descritor solicitado. Uma requisição desse tipo com wValue e wIndex iguais a 0x03 e 0x01, respectivamente, indica uma operação de leitura do descritor de string 1.

Transferência de dados

No tópico anterior foi apresentada a operação de controle chamada Setup. A partir da operação de Setup, o host consegue enviar e receber informações do dispositivo USB. A informação apresentada é apenas uma parte do protocolo USB. Na verdade, os campos apresentados formam um pacote de dados, denominado Setup Packet.

Um pacote é um conjunto de bytes.

Em geral, a transferência de dados entre os dois elementos ocorre em pacotes – seguindo o formato especificado no protocolo de comunicação -, sendo que cada pacote tem uma função específica. De modo geral, são definidos três tipos de transferências, com o seguinte formato: TOKEN + DATA + HANDSHAKE.

O Setup é uma exemplo da informação enviada em DATA.

Idependente do pacote, o primeiro byte enviado é um número de identificação PID (Packet Identifier) que tem apenas um byte (somente os primeiros quatro bits são utilizados para definir o tipo do pacote, o restante dos bits são espelhados). Na tabela abaixo são destacados os tipos de pacote e as respectivas funções. Os dois primeiros bits determinam o tipo e o restante é utilizado para definir a função/operação.

PID TypePID NamePID<3:0>
TokenOUT0001
IN1001
SOF0101
SETUP1101
DataDATA00011
DATA11011
DATA20111
MDATA1111
HandshakeACK0010
NAK1010
STALL1110
NYET0110
SpecialPRE1100
ERR1100
SPLIT1000
PING0100
Reserved0000

Operações de Controle

Toda transferência é iniciada enviando um pacote TOKEN. Para operação de Setup, o PID do pacote deve ser 1101. A estrutura desse pacote é ilustrada abaixo (Sync e EOP indicam o início e o fim da transferência de um pacote).

Sync
PID
ADDR
ENDP
CRC5
EOP
 
8 bits
7 bits
4 bits
5 bits
 

Essa operação de controle deve ser realizada em um dispositivo específico, sendo determinado pelo campo ADDR. Toda comunicação está relacionada a um local em que os dados são armazenados, denominado endpoint (ENDP). Um dispositivo USB pode apresentar até 16 endpoints para função de entrada (IN), e outros 16 para saída (OUT) de dados. A direção indica:

  • IN: do dispositivo para o host;
  • OUT: do host para o dispositivo.

Cabe ressaltar que somente o endpoint 0 é utilizado para as operações de controle.

Na sequência, as informações da operação de Setup são enviadas dentro do pacote DATA. 

Sync
PID
DATA
CRC16
EOP
 
8 bits
(0-1024)
x 8 bits
16 bits
 

Por fim, a conexão é finalizada com um pacote HANDSHAKE. Note que o campo PID define a operação: ACK, NAK, STALL ou NYET.

Sync
PID
EOP
 
8 bits
 

Fluxo de comunicação e descritores

A primeira comunicação servirá para descobrir o tamanho máximo do pacote que o dispositivo está configurado para operar. Os pacotes de configuração (SETUP) sempre serão enviados ao endpoint 0. Como destacado, o Setup possui 8 bytes para especificar a operação de controle. A estrutura que representa esse bloco de dados é mostrada abaixo.

struct tSetup{
    union {
        struct {
            uint8_t bmRequestType;
            uint8_t bRequest;
        }Fields;
        uint16_t wRequestAndType;
    }Request;
    uint16_t wValue;
    uint16_t wIndex;
    uint16_t wLength;
};

Os descritores USB também podem ser implementados como estruturas. A definição de cada campo pode ser vista neste link.

__packed struct tConfigurationDescriptor{
	uint8_t bLength;
	uint8_t bDescriptorType;
	uint16_t wTotalLength;
	uint8_t bNumInterfaces;
	uint8_t bConfigurationValues;
	uint8_t iConfiguration;
	uint8_t bmAttributes;
	uint8_t bMaxPower;
};

__packed struct tInterfaceDescriptor{
	uint8_t bLength;
	uint8_t bDescriptorType;
	uint8_t bInterfaceNumber;
	uint8_t bAlternateSetting;
	uint8_t bNumEndpoints;
	uint8_t bInterfaceClass;
	uint8_t bInterfaceSubClass;
	uint8_t bInterfaceProtocol;
	uint8_t iInterface;
};

__packed struct tHIDDescriptor{
		uint8_t bLength;
    uint8_t bDescriptorType;
    uint16_t bcdHid;
    uint8_t bCountryCode;
    uint8_t bNumDescriptors;
    uint8_t bDescriptorType1;
    uint16_t wDescriptorLength1;
};

__packed struct tEndpointDescriptor{
		uint8_t bLength;
    uint8_t bDescriptorType;
    uint8_t bEndpointAddress;
    uint8_t bmAttributes;
    uint16_t wMaxPacketSize;
    uint8_t bInterval;
};

__packed struct tDeviceDescriptor{
	uint8_t bLength;
	uint8_t bDescriptorType;
	uint16_t bcdUSB;
	uint8_t bDeviceClass;
	uint8_t bDeviceSubClass;
	uint8_t bDeviceProtocol;
	uint8_t bMaxPacketSize0;
	uint16_t idVendor;
	uint16_t idProduct;
	uint16_t bcdDevice;
	uint8_t iManufacturer;
	uint8_t iProduct;
	uint8_t iSerialNumber;
	uint8_t bNumConfigurations;
};

__packed struct tConfiguration{
	__packed struct tConfigurationDescriptor ConfigurationDescriptor;
	__packed struct tInterfaceDescriptor InterfaceDescriptor;
	__packed struct tHIDDescriptor HIDDescriptor;
	__packed struct tEndpointDescriptor EndpointDescriptor1_OUT;
};

Reports

Os dados que o dispositivos USB-HID exporta são transferidos por estruturas chamadas de reports. A estrutura do relatório é transformada em uma tabela de valores e armazenada em um descritor. Esse descritor é utilizado para informar o host sobre a estrutura do report.

Nesse exemplo, o dispositivo será utilizado somente para controlar um LED. Para construir a estrutura de um report foi utilizada a ferramenta HID Descriptor. Mais detalhes dos campos do report podem ser obtidos no mesmo link.

Ferramenta HID Descriptor para listar dispositivos USB HID.
Figura 2: Ferramenta HID Descriptor.

Configurando USB- HID no microcontrolador KL25Z

O código apresentado é uma modificação deste projeto: Minimal USB HID example for Kinetis L.

A configuração do controlador USB é realizada a partir de registradores e estruturas de dados armazenados em memória. O periférico controla toda recepção de pacotes e notifica a aplicação usando interrupções, facilitando o desenvolvimento da aplicação.

Recepção de pacotes e notificação.
Figura 3: Recepção de pacotes e notificação.

Quando um dispositivo é conectado no barramento USB, ocorre a condição de RESET. Nesse momento, o dispositivo deve habilitar o endpoint 0 e configurar o endereço com o valor 0. Isso é necessário, pos o host iniciará uma comunicação com o dispositivo de endereço 0 no endpoint 0. 

Como dito, a comunicação ocorre entre o host e um endpoint. Para facilitar tal operação, o periférico USB da KL25Z conta com uma estrutura chamada Buffer Descriptor Table (BDT) que é armazenada na memória. Nessa tabela são armazenados os descritores (BD) de cada endpoint.

Buffer Descriptor

O Buffer Descriptor (BD) é utilizado para controlar as operações em um endpoint. Cada endpoint possui uma configuração de 8 bytes. Considerando que cada endpoint possui um canal de entrada e outro saída, são necessários 16 bytes.

Para possibilitar que um endpoint possa operar enquanto os dados são processados, existe um mecanismo de double buffer. Considerando isso, o BD é utilizado para:

  • Indicar quem tem acesso ao buffer do endpoint na memória (bit OWN);
  • Determinar qual buffer está sendo utilizado (bit DATA0/1);
  • Apontar para o endereço base do endpoint na memória (32 bits);
  • Definir quantos bytes serão transferidos (campo BC de 10 bits).

O formato do BD é ilustrado na figura abaixo. Para mais informações, consulte o documento do microcontrolador, na página 617.

Estrutura do Buffer Descriptor.
Figura 4: Estrutura do Buffer Descriptor. Fonte: KL25 Sub-Family Reference Manual.

Para representar o BD, a seguinte estrutura foi criada.

struct tBufferDescriptor{
    uint32_t Fields;
    void * BufferAddress;
};

Buffer Descriptor Table

Tal estrutura é organizada em 512 bytes na memória, sendo referenciada a partir de um conjunto de registradores chamados de BDT Page Registers.

A USB possui um controlador DMA dedicado que é usado para acessar o BDT. Esse mecanismo é utilizado para acessar tal região de memória quando um token é recebido. Os dados indicados no BDT serão utilizados para acessar o buffer referente ao endpoint utilizado na comunicação.

O endereço acessado na tabela é formado pelos elementos ilustrados na Figura abaixo.

Composição do endereço para acessar um BD.
Figura 5: Composição do endereço para acessar um BD. Fonte: KL25 Sub-Family Reference Manual.

Abaixo é mostrado a definição do BDT com um vetor de estruturas do tipo BD. Como dito, para cada endpoint existem dois buffer duplicados para entrada e saída. Logo, são quatro buffers por enpoint.

#define ENDPOINT_DESCRIPTORS(NUM) ((NUM)*4)
#define BD_NUM ENDPOINT_DESCRIPTORS(ENDPOINTS)

static struct tBufferDescriptor BufferDescriptorTable[BD_NUM];

Endpoint

Para representar o endpoint, foi criada a seguinte estrutura:

enum EpBufferDir{
	RX,
  TX
};

enum EpBufferDst{
	EVEN,
	ODD
};

enum EpBufferData{
	DATA0,
	DATA1
};

struct tEndpoint{
    /*Buffer*/
    uint8_t * rx_even;
    uint8_t * rx_odd;
    uint8_t * tx_even;
    uint8_t * tx_odd;
		
    /*Control*/
    enum EpBufferDst tx_buffer; 
    enum EpBufferData tx_data1; 
    uint8_t * remaining_tx_data_ptr;
    uint16_t remaining_tx_data_length;
   
    /*ISR only*/
    void * data;
    void (*Handler)(uint8_t ep, uint8_t token, struct tEndpoint * context);
};

O handler é utilizado para definir a rotina de tratamento da transação conforme o PID recebido. Já o membro data foi utilizado para apontar para o buffer utilizado (BD que está sendo usado no momento).

Configuração inicial do periférico

Para operar no modo FullSpeed é necessário que a USB opere em 48 MHz. Tal configuração é realizada na função de inicialização do sistema. Abaixo são mostrados os procedimentos de configuração da USB.

void USB_Init(void) {
    //1: Select clock source
    SIM->SOPT2 |= SIM_SOPT2_USBSRC_MASK | SIM_SOPT2_PLLFLLSEL_MASK;

    //2: Gate USB clock
    SIM->SCGC4 |= SIM_SCGC4_USBOTG_MASK;

    //3: Software USB module reset
    USB0->USBTRC0 |= USB_USBTRC0_USBRESET_MASK;
    while (USB0->USBTRC0 & USB_USBTRC0_USBRESET_MASK);

    /*4: BDTPAGEx provides the base address where the current Buffer Descriptor Table (BDT) resides in system memory*/
    USB0->BDTPAGE1 = ((uint32_t) BufferDescriptorTable) >> 8;  //bits 15-9
    USB0->BDTPAGE2 = ((uint32_t) BufferDescriptorTable) >> 16; //bits 23-16
    USB0->BDTPAGE3 = ((uint32_t) BufferDescriptorTable) >> 24; //bits 31-24

    //5: Clear all ISR flags and enable weak pull downs
    USB0->ISTAT = 0xFF;
    USB0->ERRSTAT = 0xFF;
    USB0->OTGISTAT = 0xFF;
    USB0->USBTRC0 |= 0x40; //a hint was given that this is an undocumented interrupt bit

    
    /*Setting this bit causes the SIE to reset all of its ODD bits to the BDTs. Therefore, setting this bit
     *resets much of the logic in the SIE.
    */
    USB0->CTL = USB_CTL_USBENSOFEN_MASK;
    USB0->USBCTRL = 0;

    //6: Enable USB reset interrupt
    USB0->INTEN |= USB_INTEN_USBRSTEN_MASK;
    NVIC_EnableIRQ(USB0_IRQn);

    //7: Enable pull-up resistor on D+ (Full speed, 12Mbit/s)
    USB0->CONTROL = USB_CONTROL_DPPULLUPNONOTG_MASK;
}

No início somente a interrupção por Reset é ativada. A rotina de tratamento é mostrada a seguir:

  • Reset: Habilita o endpoint 0, determina o endereço do dispositivo igual a zero e habilita outras interrupções;
  • Token: Ocorre sempre que uma transação é finalizada. Obtém o endpoint, a operação e os dados envolvidos na transação.
void USB0_IRQHandler(void) {
    uint8_t status;
    uint8_t stat, endpoint;
    uint8_t tx, odd;

    /*After an interrupt bit has been set it may only be cleared by writing a one to the respective interrupt bit.*/
    status = USB0->ISTAT;

    /*
     *  This bit is set when the USB Module has decoded a valid USB reset. This informs the processor that it
     *	should write 0x00 into the address register and enable endpoint 0. USBRST is set after a USB reset has
     *	been detected for 2.5 microseconds. It is not asserted again until the USB reset condition has been
     *	removed and then reasserted.
     */
    if (status & USB_ISTAT_USBRST_MASK) {
        /*Reset all the BDT ODD ping/pong fields to 0, which then specifies the EVEN BDT bank.*/			
	/*enable endpoint 0*/
	/*write 0x00 into the address register*/
        /*clear all interrupts*/
        /*all necessary interrupts are now active*/
        return;
    }

    /*This bit is set when the current token being processed has completed.*/
    if (status & USB_ISTAT_TOKDNE_MASK) {
        /*The processor must immediately read the STATUS (STAT) register to determine the EndPoint and BD used for this token*/
        /*determine which token has been processed*/
        /*Call endpoint handler*/
    }
}

Operações de controle

As operações realizadas no endpoint 0 são para obter informações do dispositivo e também configurá-lo. Nessa aplicação, somente os comandos SetAddress, SetConfiguration, GetDescriptor e GetInterface foram implementados. A seguir é mostrado um exemplo de operação do Setup. A partir dos dados é determinado qual requisição será processada.

switch (setup.Request.wRequestAndType) {

	case 0x0500: //set address (wait for IN packet)
			break;

	case 0x0900: //set configuration
			//we only have one configuration at this time
			break;

	case 0x0680: //get descriptor
	case 0x0681:
									
			if((setup.wValue & 0x0300) == 0x0300){ //StringDescriptor
				uint8_t idx = setup.wValue & 0x00FF;
				if(idx < STRING_DESCRIPTORS){
							endpoint->remaining_tx_data_ptr = (uint8_t*)StringDescriptorTable[idx].descriptor;
							endpoint->remaining_tx_data_length = StringDescriptorTable[idx].size;
							must_stall = false;
				}
			}
			else if (setup.wValue == 0x0100){ //DeviceDescriptor
				endpoint->remaining_tx_data_ptr = (uint8_t*)&DeviceDescriptor;
				endpoint->remaining_tx_data_length = sizeof(DeviceDescriptor);
				must_stall = false;
			}
			else if (setup.wValue == 0x0200){ //Configuration
				endpoint->remaining_tx_data_ptr = (uint8_t*)&Configuration;
				endpoint->remaining_tx_data_length = sizeof(Configuration);
				must_stall = false;
			}
			else if (setup.wValue == 0x2200){ //Report
				endpoint->remaining_tx_data_ptr = (uint8_t*)ReportDescriptor;
				endpoint->remaining_tx_data_length = sizeof(ReportDescriptor);
				must_stall = false;
			}
			else{
				must_stall = true;
			}

			break;

	default:
			must_stall = true;
}

Descritores

Dispositivos HID utilizam apenas transferências de controle e de interrupção. Nessas transferências, um pacote de dados pode conter até 64 bytes. Interrupção significa que o endpoint é utilizado para transferir poucos dados rapidamente. Nesse caso, o dispositivo USB deve ser reativo. Para tal, o endpoint é usado somente em uma direção.

Essas configurações são realizadas com o preenchimento das estruturas de dados mostradas a seguir.

const struct tDeviceDescriptor DeviceDescriptor =
{
	.bLength = 18,
	.bDescriptorType = 1,
	.bcdUSB = 0x0101,
	.bDeviceClass = 0,
	.bDeviceSubClass = 0,
	.bDeviceProtocol = 0,
	.bMaxPacketSize0 = 64,
	.idVendor = 0xabcd,
	.idProduct = 0x1234,
	.bcdDevice = 0,
	.iManufacturer = 1,
	.iProduct = 2,
	.iSerialNumber = 3,
	.bNumConfigurations = 1
};

const struct tConfiguration Configuration = {
	.ConfigurationDescriptor =
	{
	  .bLength = 9,
	  .bDescriptorType = 2, //Configuration
	  .wTotalLength = sizeof(struct tConfiguration), //
	  .bNumInterfaces = 1,
	  .bConfigurationValues = 1,
	  .iConfiguration = 5, //StringDescriptor: Default Configuration
	  .bmAttributes = 0x80,
	  .bMaxPower = 0xfa,
	},
	.InterfaceDescriptor = 
	{
	  .bLength = 9,
	  .bDescriptorType = 4, //Interface
	  .bInterfaceNumber = 0,
	  .bAlternateSetting = 0,
	  .bNumEndpoints = 1,
	  .bInterfaceClass = 3, //HID
	  .bInterfaceSubClass = 0,
	  .bInterfaceProtocol = 0,
	  .iInterface = 4	//String Descriptor: Interface
	},
	.HIDDescriptor = {
	  .bLength = 9,
          .bDescriptorType = 33, //HID
          .bcdHid = 0x0110,			 //HID class spec version
	  .bCountryCode = 0,
          .bNumDescriptors = 1,
          .bDescriptorType1 = 34,//REPORT
          .wDescriptorLength1 = sizeof(ReportDescriptor)
	},
	.EndpointDescriptor_IN = {
	  .bLength = 7,
          .bDescriptorType = 5,
	  .bEndpointAddress = 0x01, //Endpoint 1 - OUT
	  .bmAttributes = 3, //Interrupt. Data endpoint.
          .wMaxPacketSize = 64, //64
	  .bInterval = 0x01
	}
};

Note que no descritor HID existe uma referência para estrutura do report.

const uint8_t ReportDescriptor[REPORT_SIZE] = {
    0x05, 0x08,                    
    0x09, 0x00,
    0xA1, 0x01,                    
    0x75, 0x08,                    
    0x95, 0x40,                    
    0x25, 0x01,                    
    0x15, 0x00,                    
    0x09, 0x00,                    
    0x91, 0x02,                    
    0xc0                           
};

Exemplo

A seguir é mostrado um exemplo de operação no endpoint 1, controlando o estado do LED RGB conectado no pino PB19. A aplicação verifica dois bytes recebidos para definir qual operação será realizada. O primeiro byte é utilizado para definir o LED e o segundo para determinar o estado.

static void endpoint_1_handler(uint8_t ep, uint8_t token, struct tEndpoint * endpoint) {
	
    struct tBufferDescriptor * buf_desc = (struct tBufferDescriptor *)endpoint->data;
    uint8_t size;

    switch (token) {
	case TOK_IN:			
	break;

	case TOK_OUT:
		size = buf_desc->Fields >> 16;
					
		if (size == 64) {
		   if(((uint8_t*)buf_desc->BufferAddress)[0] == 0x00){
		      if(((uint8_t*)buf_desc->BufferAddress)[1] == 0x01){
		         FPTB->PSOR = (1UL << 19);
		      }
		      else{
		         FPTB->PCOR = (1UL << 19);
		      }
		   }
		}
	break;
    }
}

Enviando informações para um dispositivo

Para enviar dados ao dispositivo foi criada uma aplicação em C#, utilizando a biblioteca HIDInterface.

Aplicação para enviar comandos de controle do LED.
Figura 6: Aplicação para enviar comandos de controle do LED.

O dispositivo é localizado utilizando as informações do PID e VID.

HIDDevice.interfaceDetails[] devices = HIDDevice.getConnectedDevices();
int selectedDeviceIndex = -1;

foreach (HIDDevice.interfaceDetails device in devices)
{
     selectedDeviceIndex++;
     if (device.VID == 0xabcd && device.PID == 0x1234)
     {
         break;
     }
}

Por fim, os dados são enviados para o dispositivo conforme a operação solicitada.

private void SendStatus(bool enable)
{
   if (usb_device != null)
   {
       byte[] writeData;

       if (enable)
       {
           writeData = new byte[]{ 0x00, 0x01 };
       }
       else
       {
          writeData = new byte[] { 0x00, 0x00 };
       }

       usb_device.write(writeData);
   }
}

Para saber mais

Algumas partes do projeto não foram apresentadas no artigo. O projeto completo pode ser acessado no github.

Para conhecer mais sobre o protocolo USB acesse os materiais:

Para descrição dos reports:

Para descrição do periférico, acesse o datasheet no microcontrolador:

Outros artigos do Embarcados:

Raspberry Pi Zero W – Utilizando placa de som USB

Usando FreeRTOS para aplicações com a Freescale FRDM KL25z

Conheça a FRDM KL25Z da NXP

Vale conferir o livro Tecnologia ARM escrito pelo Fábio Pereira. Esse livro possui uma seção com muita informação sobre o protocolo USB.

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Notificações
Notificar
1 Comentário
recentes
antigos mais votados
Inline Feedbacks
View all comments
Miguel
Miguel
20/04/2019 20:38

Olá Fernando, seu artigo realmente foi um show de conhecimento sobre o protocolo USB.
Tenho uma duvida, eu consigo enviar comando “bytes” para um dispositivo USB HID Keybord, para fazer determinadas alterações?

Home » Software » USB HID – Human Interface Device Class: Exemplo com a placa FRDM-KL25Z

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: