Um nível ainda mais baixo (avr-vusb)
USB em registradores: STM32L1 / STM32F1
USB em registradores: endpoint em massa no exemplo de Mass Storage
USB em registradores: endpoint isócrono no exemplo de dispositivo de áudio
Continuamos a lidar com USB em controladores STM32L151. Como na parte anterior, não haverá nada dependente de plataforma aqui, mas será dependente de USB. Mais precisamente, consideraremos o terceiro tipo de endpoint - interrupção. E faremos isso usando o exemplo de um dispositivo composto "teclado + tablet" ( link para a fonte ).
Por via das dúvidas, aviso: este artigo (como todo mundo) é antes uma sinopse do que entendi enquanto entendia este assunto. Muitas coisas permaneceram "mágicas" e ficarei grato se houver um especialista que as possa explicar.
Em primeiro lugar, gostaria de lembrar que o protocolo HID (Human Interface Device) não se destina à troca de grandes quantidades de dados. Todo o intercâmbio é baseado em dois conceitos: um evento e um estado . Um evento é uma mensagem única que ocorre em resposta a um impacto externo ou interno. Por exemplo, o usuário pressionou um botão ou moveu o mouse. Ou em um teclado desativei o NumLock, após o qual o host é forçado e o segundo a enviar o comando apropriado para
Assim, o propósito de um ponto de interrupção é o mesmo que uma interrupção em um controlador - relatar rapidamente um evento raro. Mas o USB é centrado no host, então o dispositivo não tem o direito de iniciar a transferência sozinho. Para contornar isso, os desenvolvedores USB criaram uma muleta: o host envia periodicamente solicitações para ler todos os pontos de interrupção. A frequência da solicitação é configurada pelo último parâmetro no EndpointDescriptor (faz parte do ConfigurationDescriptor). Já vimos o campo bInterval nos capítulos anteriores, mas seu valor foi ignorado. Agora ele finalmente encontrou um uso. O valor tem um tamanho de 1 byte e é definido em milissegundos, portanto, seremos pesquisados em intervalos de 1 ms a 2,55 segundos. Para dispositivos de baixa velocidade, o intervalo mínimo é de 10ms. A presença de uma muleta com pontos de interrupção na votação significa para nósque mesmo na ausência de troca, eles estarão desperdiçando largura de banda do barramento.
A conclusão lógica: os pontos de interrupção são apenas para transações IN. Em particular, eles são usados para transmitir eventos do teclado ou mouse, para notificar sobre mudanças nas linhas de serviço da porta COM, para sincronizar o fluxo de áudio e similares. Mas para tudo isso, você terá que adicionar outros tipos de pontos. Portanto, para não complicar o exemplo, vamos nos restringir à implementação do dispositivo HID. Na verdade, já fizemos tal dispositivo na primeira parte, mas não foram usados pontos adicionais e a estrutura do protocolo HID não foi considerada.
ConfigurationDescriptor
static const uint8_t USB_ConfigDescriptor[] = {
ARRLEN34(
ARRLEN1(
bLENGTH, // bLength: Configuration Descriptor size
USB_DESCR_CONFIG, //bDescriptorType: Configuration
wTOTALLENGTH, //wTotalLength
1, // bNumInterfaces
1, // bConfigurationValue: Configuration value
0, // iConfiguration: Index of string descriptor describing the configuration
0x80, // bmAttributes: bus powered
0x32, // MaxPower 100 mA
)
ARRLEN1(
bLENGTH, //bLength
USB_DESCR_INTERFACE, //bDescriptorType
0, //bInterfaceNumber
0, // bAlternateSetting
2, // bNumEndpoints
HIDCLASS_HID, // bInterfaceClass:
HIDSUBCLASS_BOOT, // bInterfaceSubClass:
HIDPROTOCOL_KEYBOARD, // bInterfaceProtocol:
0x00, // iInterface
)
ARRLEN1(
bLENGTH, //bLength
USB_DESCR_HID, //bDescriptorType
USB_U16(0x0110), //bcdHID
0, //bCountryCode
1, //bNumDescriptors
USB_DESCR_HID_REPORT, //bDescriptorType
USB_U16( sizeof(USB_HIDDescriptor) ), //wDescriptorLength
)
ARRLEN1(
bLENGTH, //bLength
USB_DESCR_ENDPOINT, //bDescriptorType
INTR_NUM, //bEdnpointAddress
USB_ENDP_INTR, //bmAttributes
USB_U16( INTR_SIZE ), //MaxPacketSize
10, //bInterval
)
ARRLEN1(
bLENGTH, //bLength
USB_DESCR_ENDPOINT, //bDescriptorType
INTR_NUM | 0x80, //bEdnpointAddress
USB_ENDP_INTR, //bmAttributes
USB_U16( INTR_SIZE ), //MaxPacketSize
10, //bInterval
)
)
};
O leitor atento pode notar imediatamente as descrições dos terminais. Com o segundo, tudo está em ordem - ponto IN (já que a adição com 0x80) é do tipo interrupção, o tamanho e o intervalo são especificados. Mas o primeiro parece ser declarado OUT, mas ao mesmo tempo interromper, o que contradiz o que foi dito anteriormente. E o bom senso também: o hospedeiro não precisa de muletas para transferir nada para o dispositivo a qualquer momento. Mas, desta forma, outros rakes são contornados: o tipo de endpoint em STM32 é definido não para um ponto, mas apenas para o par IN / OUT, então não funcionará para definir o ponto 0x81st para o tipo de interrupção, mas para o 0x01st ao controle. No entanto, isso não é um problema para o host, ele provavelmente enviaria os mesmos dados no ponto em massa também ... o que, no entanto, não vou verificar.
Descritor HID
A estrutura do descritor HID é mais semelhante ao arquivo de configuração "nome = valor", mas ao contrário, "nome" é uma constante numérica da lista específica de USB e "valor" também é uma constante ou uma variável de tamanho 0 até 3 bytes.
Importante:para alguns "nomes", o comprimento do "valor" é especificado nos 2 bits menos significativos do campo "nome". Por exemplo, vamos pegar LOGICAL_MINIMUM (o valor mínimo que esta variável pode assumir no modo normal). O código para esta constante é 0x14. Assim, se não houver "valor" (parece que isso não acontece, mas não vou discutir - por algum motivo esse caso foi inserido), o descritor conterá um único número 0x14. Se o "valor" for 1 (um byte), então 0x15, 0x01 serão gravados. Para um valor de dois bytes 0x1234, 0x16, 0x34, 0x12 serão gravados - o valor é gravado de baixo para cima. Bem, antes da pilha, o número 0x123456 será 0x17, 0x56, 0x34, 0x12.
Naturalmente, sou muito preguiçoso para memorizar todas essas constantes numéricas, então usaremos macros. Infelizmente, nunca encontrei uma maneira de fazer com que eles descobrissem o tamanho do valor passado e se expandissem para 1, 2, 3 ou 4 bytes. Portanto, tive que fazer uma muleta: uma macro sem sufixo é responsável pelos valores de 8 bits mais comuns, com um sufixo 16 para valores de 16 bits e 24 para valores de 24 bits. As macros também foram escritas para valores "compostos", como o intervalo LOGICAL_MINMAX24 (min, max), que se expandem em 4, 6 ou 8 bytes.
Tal como acontece com os arquivos de configuração, existem “seções” chamadas páginas (usage_page) que agrupam os dispositivos por propósito. Por exemplo, há uma página com periféricos básicos, como teclados, mouses e apenas botões, há joysticks e gamepads (eu sinceramente recomendo olhar quais deles! Há também para tanques e para naves espaciais e para submarinos e para qualquer outra coisa ), há até monitores ... É verdade, onde procurar um software que funcione com tudo isso, não tenho ideia.
Em cada página, um dispositivo específico é selecionado. Por exemplo, para um mouse, é um ponteiro e botões, e para um tablet - uma caneta ou o dedo de um usuário (o quê?!). Eles também designam as partes componentes do dispositivo. Então, parte do ponteiro são suas coordenadas X e Y. Algumas características podem ser agrupadas em uma "coleção", mas eu realmente não entendo porque isso é feito. Na documentação, os campos às vezes são marcados com algumas letras sobre a finalidade do campo e como trabalhar com ele:
| CA | Coleção (aplicativo) | Informação de serviço, não correspondendo a nenhuma variável |
| CL | Coleção (lógico) | - / - |
| PC | Coleção (física) | - / - |
| Dv | Valor Dinâmico | valor de entrada ou saída (variável) |
| MC | Controle momentâneo | sinalizador de status (1 sinalizador armado, 0 desmarcado) |
| OSC | Controle de um tiro | evento único. Apenas a transição 0-> 1 é processada |
Existem outros, é claro, mas eles não são usados em meu exemplo. Se, por exemplo, o campo X estiver marcado como DV, ele será considerado uma variável de comprimento diferente de zero e será incluído na estrutura do relatório. Os campos MC ou OSC também estão incluídos no relatório, mas têm o tamanho de 1 bit.
Um relatório (pacote de dados enviado ou recebido pelo dispositivo) contém os valores de todas as variáveis nele descritas. A descrição do botão fala de apenas um bit ocupado, mas para coordenadas relativas (quanto o mouse se moveu, por exemplo), pelo menos um byte é necessário, e para coordenadas absolutas (como para uma tela sensível ao toque), pelo menos 2 bytes são precisos. Além disso, muitos controles têm suas próprias limitações físicas. Por exemplo, um ADC da mesma tela sensível ao toque pode ter resolução de apenas 10 bits, ou seja, dar valores de 0 a 1023, que o host deverá escalar para resolução de tela inteira. Portanto, além da finalidade de cada campo, o descritor também especifica a faixa de seus valores permitidos (LOGICAL_MINMAX), mais algumas vezes a faixa de valores físicos (em milimatres lá, ou em graus) e a apresentação no o relatório é obrigatório.A representação é definida por dois números: o tamanho de uma variável (em bits) e seu número. Por exemplo, as coordenadas de toque na tela sensível ao toque no dispositivo que estamos criando são definidas da seguinte forma:
USAGE( USAGE_X ), // 0x09, 0x30,
USAGE( USAGE_Y ), // 0x09, 0x31,
LOGICAL_MINMAX16( 0, 10000 ), //0x16, 0x00, 0x00, 0x26, 0x10, 0x27,
REPORT_FMT( 16, 2 ), // 0x75, 0x10, 0x95, 0x02,
INPUT_HID( HID_VAR | HID_ABS | HID_DATA), // 0x91, 0x02,
Aqui você pode ver que duas variáveis são declaradas, variando na faixa de 0 a 10000 e ocupando duas seções de 16 bits no relatório.
O último campo diz que as variáveis acima serão lidas pelo host (IN) e explica exatamente como. Não vou descrever suas bandeiras em detalhes, vou me alongar apenas em algumas. O sinalizador HID_ABS indica que o valor é absoluto, ou seja, nenhum histórico o afeta. O valor alternativo HID_REL indica que o valor é um deslocamento do anterior. O sinalizador HID_VAR diz que cada campo é responsável por sua própria variável. O valor alternativo HID_ARR diz que não serão transmitidos os estados de todos os botões da lista, mas apenas os números dos botões ativos. Este sinalizador se aplica apenas a campos de bit único. Em vez de transmitir 101/102 estados de todos os botões do teclado, você pode se limitar a alguns bytes com uma lista de teclas pressionadas. Então o primeiro parâmetro REPORT_FMT será responsável pelo tamanho do número, e o segundo pelo número.
Como o tamanho de todas as variáveis é definido em bits, é lógico perguntar: e os botões, porque seu número pode não ser múltiplo de 8, e isso levará a dificuldades de alinhamento na leitura e escrita. Seria possível alocar um byte para cada botão, mas então o volume do relatório aumentaria muito, o que é desagradável para programas de alta velocidade como interrupção. Em vez disso, os botões são colocados mais próximos uns dos outros e o espaço restante é preenchido com bits com o sinalizador HID_CONST.
Agora podemos, se não escrever um descritor do zero, pelo menos tentar lê-lo, ou seja, determinar a quais bits este ou aquele campo corresponde. Basta contar os INPUT_HIDs e os REPORT_FMTs correspondentes. Apenas tenha em mente que eu criei essas macros, ninguém mais as usa. Nos descritores de outras pessoas, você terá que procurar input, report_size, report_count ou mesmo constantes numéricas.
Agora você pode trazer o descritor completo:
static const uint8_t USB_HIDDescriptor[] = {
//keyboard
USAGE_PAGE( USAGEPAGE_GENERIC ),//0x05, 0x01,
USAGE( USAGE_KEYBOARD ), // 0x09, 0x06,
COLLECTION( COLL_APPLICATION, // 0xA1, 0x01,
REPORT_ID( 1 ), // 0x85, 0x01,
USAGE_PAGE( USAGEPAGE_KEYBOARD ), // 0x05, 0x07,
USAGE_MINMAX(224, 231), //0x19, 0xE0, 0x29, 0xE7,
LOGICAL_MINMAX(0, 1), //0x15, 0x00, 0x25, 0x01,
REPORT_FMT(1, 8), //0x75, 0x01, 0x95, 0x08
INPUT_HID( HID_DATA | HID_VAR | HID_ABS ), // 0x81, 0x02,
//reserved
REPORT_FMT(8, 1), // 0x75, 0x08, 0x95, 0x01,
INPUT_HID(HID_CONST), // 0x81, 0x01,
REPORT_FMT(1, 5), // 0x75, 0x01, 0x95, 0x05,
USAGE_PAGE( USAGEPAGE_LEDS ), // 0x05, 0x08,
USAGE_MINMAX(1, 5), //0x19, 0x01, 0x29, 0x05,
OUTPUT_HID( HID_DATA | HID_VAR | HID_ABS ), // 0x91, 0x02,
// 1
REPORT_FMT(3, 1), // 0x75, 0x03, 0x95, 0x01,
OUTPUT_HID( HID_CONST ), // 0x91, 0x01,
REPORT_FMT(8, 6), // 0x75, 0x08, 0x95, 0x06,
LOGICAL_MINMAX(0, 101), // 0x15, 0x00, 0x25, 0x65,
USAGE_PAGE( USAGEPAGE_KEYBOARD ), // 0x05, 0x07,
USAGE_MINMAX(0, 101), // 0x19, 0x00, 0x29, 0x65,
INPUT_HID( HID_DATA | HID_ARR ), // 0x81, 0x00,
)
//touchscreen
USAGE_PAGE( USAGEPAGE_DIGITIZER ), // 0x05, 0x0D,
USAGE( USAGE_PEN ), // 0x09, 0x02,
COLLECTION( COLL_APPLICATION, // 0xA1, 0x0x01,
REPORT_ID( 2 ), //0x85, 0x02,
USAGE( USAGE_FINGER ), // 0x09, 0x22,
COLLECTION( COLL_PHISICAL, // 0xA1, 0x00,
USAGE( USAGE_TOUCH ), // 0x09, 0x42,
USAGE( USAGE_IN_RANGE ), // 0x09, 0x32,
LOGICAL_MINMAX( 0, 1), // 0x15, 0x00, 0x25, 0x01,
REPORT_FMT( 1, 2 ), // 0x75, 0x01, 0x95, 0x02,
INPUT_HID( HID_VAR | HID_DATA | HID_ABS ), // 0x91, 0x02,
REPORT_FMT( 1, 6 ), // 0x75, 0x01, 0x95, 0x06,
INPUT_HID( HID_CONST ), // 0x81, 0x01,
USAGE_PAGE( USAGEPAGE_GENERIC ), //0x05, 0x01,
USAGE( USAGE_POINTER ), // 0x09, 0x01,
COLLECTION( COLL_PHISICAL, // 0xA1, 0x00,
USAGE( USAGE_X ), // 0x09, 0x30,
USAGE( USAGE_Y ), // 0x09, 0x31,
LOGICAL_MINMAX16( 0, 10000 ), //0x16, 0x00, 0x00, 0x26, 0x10, 0x27,
REPORT_FMT( 16, 2 ), // 0x75, 0x10, 0x95, 0x02,
INPUT_HID( HID_VAR | HID_ABS | HID_DATA), // 0x91, 0x02,
)
)
)
};
Além dos campos discutidos anteriormente, também existe um campo interessante como REPORT_ID. Visto que, como fica claro pelos comentários, nosso dispositivo é composto, o host precisa de alguma forma determinar quais dados ele recebe. Para isso, este campo é necessário.
E mais um campo para o qual gostaria de chamar a atenção é OUTPUT_HID. Como o nome indica, não é responsável por receber um relatório (IN), mas sim por transmitir (OUT). Ele está localizado na seção do teclado e descreve os indicadores CapsLock, NumLock, ScrollLock, bem como dois exóticos - Compose (uma bandeira para inserir alguns caracteres que não têm seus próprios botões como á, µ ou) e Kana (inserir hieróglifos) . Na verdade, por causa deste campo, começamos o ponto OUT. Em seu manipulador, verificaremos se os indicadores CapsLock e NumLock precisam estar acesos: há apenas dois diodos na placa e estão ligados.
Existe um terceiro campo relacionado à troca de dados - FEATURE_HID, que usamos no primeiro exemplo. Se INPUT e OUTPUT destinam-se a transmitir eventos, FEATURE é um estado que pode ser lido ou escrito. É verdade que isso não é feito por meio de terminais dedicados, mas por meio do ep0 usual por meio de solicitações apropriadas.
Se você olhar atentamente para o descritor, poderá restaurar a estrutura do relatório. Mais precisamente, dois relatórios:
struct{
uint8_t report_id; //1
union{
uint8_t modifiers;
struct{
uint8_t lctrl:1; //left control
uint8_t lshift:1;//left shift
uint8_t lalt:1; //left alt
uint8_t lgui:1; //left gui. hyper, winkey
uint8_t rctrl:1; //right control
uint8_t rshift:1;//right shift
uint8_t ralt:1; //right alt
uint8_t rgui:1; //right gui
};
};
uint8_t reserved; //
uint8_t keys[6]; //
}__attribute__((packed)) report_kbd;
struct{
uint8_t report_id; //2
union{
uint8_t buttons;
struct{
uint8_t touch:1; //
uint8_t inrange:1; //
uint8_t reserved:6;// 1
};
};
uint16_t x;
uint16_t y;
}__attribute__((packed)) report_tablet;
Vamos enviá-los pressionando os botões do quadro, além disso. já que estamos escrevendo apenas um exemplo de implementação, e não um dispositivo completo, faremos isso de forma bárbara - enviando dois relatórios, no primeiro deles "pressionando" as teclas, e no segundo - "liberando". Além disso, com um enorme atraso "estúpido" entre as mensagens. Se você não enviar um relatório com as teclas "liberadas", o sistema irá considerar que a tecla ainda está pressionada e irá repeti-la. Naturalmente, não há dúvida de qualquer eficiência, segurança também, mas servirá para o teste. Oh sim, onde sem outro ancinho! O tamanho da estrutura deve corresponder ao que está descrito no descritor, caso contrário, o Windows fingirá que não entende o que eles querem dele. Como de costume, o Linux ignora esses erros e funciona como se nada tivesse acontecido.
Durante o teste, me deparei com um efeito colateral engraçado: no Windows7, quando você clica em "touchscreen", a janela de escrita à mão aparece. Eu não sabia sobre esse recurso.
Se você tiver um dispositivo concluído
... e eu quero olhar por dentro. Em primeiro lugar, é claro, olhamos, você pode até mesmo de um usuário comum, ConfigurationDescriptor:
lsusb -v -d <VID:PID>
Para o descritor HID, não encontrei (e não procurei) uma maneira melhor do que a partir da raiz:
cat /sys/kernel/debug/hid/<address>/rdes
Para fins de integridade, valeria a pena adicionar aqui como ver coisas semelhantes em outros sistemas operacionais. Mas não tenho nenhum conhecimento relevante, talvez eles digam nos comentários. É desejável, claro, sem instalar software de terceiros.
Conclusão
Isso é, na verdade, tudo o que descobri no HID. O plano mínimo - aprender a ler descritores prontos, emular vários dispositivos ao mesmo tempo e implementar a entrada do tablet - está completo. Bem, a filosofia dos pontos de interrupção foi considerada ao mesmo tempo.
Como na época ruim, deixei um pouco de documentação no repositório para o caso de os designers do USB-IF decidirem estragar o site novamente.