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.
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.
| bmRequestType | bRequest | wValue | wIndex | wLength | Data |
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.
| Offset | Campo | Tamanho em bytes | Tipo | Descrição |
| 0 | bmRequestType | 1 | Bit-Map | D7 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 |
| 1 | bRequest | 1 | Value |
Request |
| 2 | wValue | 2 | Value |
Value |
| 4 | wIndex | 2 | Index or Offset |
Index |
| 6 | wLength | 2 | Count |
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 Type | PID Name | PID<3:0> |
| Token | OUT | 0001 |
| IN | 1001 | |
| SOF | 0101 | |
| SETUP | 1101 | |
| Data | DATA0 | 0011 |
| DATA1 | 1011 | |
| DATA2 | 0111 | |
| MDATA | 1111 | |
| Handshake | ACK | 0010 |
| NAK | 1010 | |
| STALL | 1110 | |
| NYET | 0110 | |
| Special | PRE | 1100 |
| ERR | 1100 | |
| SPLIT | 1000 | |
| PING | 0100 | |
| Reserved | 0000 |
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.
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.
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.
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.
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.
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
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.












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?