USB nos registros: STM32L1 / STM32F1



Mesmo nível mais baixo (avr-vusb)



USB em registradores: endpoint em massa usando o exemplo de Mass Storage

USB em registradores: endpoint de interrupção usando o exemplo de HID

USB em registradores: endpoint isócrono usando o exemplo de dispositivo de áudio



Já conhecemos o software USB usando o exemplo do AVR, é hora de pegar pedras mais pesadas - stm32. Nossos assuntos experimentais serão o STM32F103C8T6 clássico, bem como um representante da série STM32L151RCT6 de baixa potência. Como antes, não usaremos placas de depuração compradas e HAL, preferindo uma bicicleta.



Como existem dois controladores no título, vale a pena falar sobre as principais diferenças. Em primeiro lugar, este é um resistor pull-up informando ao host usb que algo foi preso nele. No L151 ele é embutido e controlado pelo bit SYSCFG_PMC_USB_PU, mas no F103 não; você terá que soldá-lo na placa de fora e conectá-lo ao VCC ou à perna do controlador. No meu caso, a perna PA10 veio debaixo do meu braço. Em que UART1 trava ... E o outro pino da UART1 entra em conflito com o botão ... Joguei uma placa maravilhosa, não acham? A segunda diferença é a quantidade de memória flash: no F103 é de 64 kB e no L151 até 256 kB, que usaremos algum dia ao estudar os endpoints em massa. Eles também têm configurações de clock ligeiramente diferentes, e podem ser pendurados em pernas diferentes com lâmpadas com botões, mas isso já é uma bagatela. Exemplo para F103está disponível no repositório, então não será difícil adaptar o resto dos experimentos com o L151 para ele. Os códigos-fonte estão disponíveis aqui: github.com/COKPOWEHEU/usb



Princípio geral de trabalho com USB



A operação com USB neste controlador é presumida usando um módulo de hardware. Ou seja, dizemos a ele o que fazer, ele faz e no final puxa a interrupção “Estou pronto!”. Conseqüentemente, não precisamos chamar quase nada do main main (embora eu tenha fornecido a função usb_class_poll para o caso). O ciclo normal de trabalho é limitado a um único evento - a troca de dados. O resto - reset, hibernação e outros - são eventos excepcionais e únicos.



Desta vez, não entrarei nos detalhes de baixo nível da troca. Quem estiver interessado pode ler sobre o vusb. Mas deixe-me lembrá-lo de que a troca de dados comuns não é por um byte, mas por pacote, e a direção da transmissão é definida pelo host. E ele também dita os nomes dessas direções: transmissão IN significa que o host recebe dados (e o dispositivo transmite), e OUT significa que o host transmite dados (e nós recebemos). Além disso, cada pacote tem seu próprio endereço - o número do terminal com o qual o host deseja se comunicar. Por enquanto, teremos um único ponto final 0, responsável pelo dispositivo como um todo (por brevidade, também o chamarei de ep0). Para que servem os outros, direi em outros artigos. De acordo com o padrão, o tamanho de ep0 é estritamente de 8 bytes para dispositivos de baixa velocidade (aos quais o mesmo vusb pertence) e uma escolha de 8, 16, 32,64 bytes para velocidade total como a nossa.



E se os dados forem muito pequenos e não preencherem o buffer completamente? Tudo é simples aqui: além dos dados do pacote, seu tamanho também é transmitido (pode ser o campo wLength ou uma combinação de baixo nível de sinais SE0, indicando o fim da transmissão), portanto, mesmo se precisarmos transferir três bytes por ep0 de 64 bytes, então exatamente três bytes serão transferidos ... Como resultado, não perderemos largura de banda direcionando zeros desnecessários. Portanto, não seja pequeno demais: se podemos gastar 64 bytes, gastamos sem hesitação. Entre outras coisas, isso reduzirá um pouco a carga do barramento, porque é mais fácil transferir um pedaço de 64 bytes (mais todos os cabeçalhos e caudas) por vez do que 8 vezes 8 bytes cada (para cada um dos quais, novamente, cabeçalhos e caudas )



E se houver muitos dados ao contrário? É mais complicado aqui. Os dados devem ser divididos pelo tamanho do terminal e transferidos em blocos. Digamos que o tamanho de ep0 seja de 8 bytes e o host esteja tentando transmitir 20 bytes. Na primeira interrupção, os bytes 0-7 chegarão até nós, na segunda 8-15, na terceira 16-20. Ou seja, para coletar o pacote inteiro, você precisa receber até três interrupções. Para isso, no mesmo HAL, um buffer complicado foi inventado, com o qual tentei descobrir, mas após o quarto nível de transferência da mesma coisa entre funções, cuspi. Como resultado, em minha implementação, o armazenamento em buffer recai sobre os ombros do programador.



Mas o host pelo menos sempre diz quantos dados está tentando transferir. Quando transferimos dados, precisamos de alguma forma enganar os estados de baixo nível das pernas para deixar claro que os dados acabaram. Mais precisamente, para deixar claro para o módulo USB que os dados acabaram e que você precisa puxar as pernas. Isso é feito de maneira óbvia - gravando apenas parte do buffer. Por exemplo, se temos 8 bytes no buffer e escrevemos 4, obviamente temos apenas 4 bytes de dados, após o que o módulo enviará a combinação mágica SE0 e todos ficarão felizes. E se escrevermos 8 bytes, isso significa que temos apenas 8 bytes, ou que esta é apenas uma parte dos dados que cabem no buffer? O módulo usb pensa que o. Portanto, se quisermos parar a transferência, depois de escrever o buffer de 8 bytes, devemos escrever o próximo de 0 bytes. Isso é chamado de ZLP, Zero Length Packet. Como fica no código,Eu te conto um pouco mais tarde.



Organização da memória



De acordo com o padrão, o tamanho do ponto de extremidade 0 pode ser de até 64 bytes. Qualquer outro tamanho - até 1024 bytes. O número de pontos também pode variar de dispositivo para dispositivo. O mesmo STM32L1 suporta até 7 pontos na entrada e 7 na saída (sem contar ep0), ou seja, até 14 kB de buffers sozinhos. O que, em tal volume, provavelmente nunca será necessário para ninguém. Consumo inaceitável de memória! Em vez disso, o módulo usb mastiga um pedaço da memória do kernel compartilhada e a usa. Essa área é chamada de PMA (área de memória de pacote) e começa com USB_PMAADDR. E para indicar onde os buffers de cada endpoint estão localizados dentro dele, uma matriz de 8 elementos, cada um com a seguinte estrutura é alocada no início, e somente então a área real para dados:



typedef struct{
    volatile uint32_t usb_tx_addr;
    volatile uint32_t usb_tx_count;
    volatile uint32_t usb_rx_addr;
    volatile union{
      uint32_t usb_rx_count;
      struct{
        uint32_t rx_count:10;
        uint32_t rx_num_blocks:5;
        uint32_t rx_blocksize:1;
      };
    };
}usb_epdata_t;
      
      





Aqui você define o início do buffer de transmissão, seu tamanho e, a seguir, o início do buffer de recepção e seu tamanho. Observe, em primeiro lugar, que usb_tx_count não define o tamanho real do buffer, mas a quantidade de dados a ser transferida. Ou seja, nosso código deve gravar dados no endereço usb_tx_addr, depois gravar seu tamanho em usb_tx_count e só então puxar o registro do módulo usb de que os dados estão gravados, transferi-los. Preste ainda mais atenção ao estranho formato do tamanho do buffer de recepção: é uma estrutura em que 10 rx_count bits são responsáveis ​​pela quantidade real de dados lidos, enquanto o resto é realmente pelo tamanho do buffer. É necessário saber o pedaço de ferro onde você pode escrever e onde começam os dados de outras pessoas. O formato desta configuração também é bastante interessante: o sinalizador rx_block_size informa em quais unidades o tamanho é definido. Se for redefinido para 0,em seguida, em palavras de 2 bytes, o tamanho do buffer é 2 * rx_num_blocks, ou seja, de 0 a 62. E se definido como 1, em blocos de 32 bytes, respectivamente, o tamanho do buffer acaba sendo 32 * rx_num_blocks e fica na faixa de 32 a 512 (sim, não até 1024, essa é a limitação do controlador).



Para colocar buffers nesta área, usaremos uma abordagem semi-dinâmica. Ou seja, alocar memória sob demanda, mas não liberá-la (malloc / free ainda não foi suficiente para inventar!). O início do espaço não alocado será apontado pela variável lastaddr, que inicialmente aponta para o início do PMA sem a tabela de estruturas discutida acima. Bem, cada vez que a função para configurar o próximo endpoint usb_ep_init () for chamada, ela será deslocada pelo tamanho do buffer especificado lá. E o valor desejado será inserido na célula correspondente da tabela, é claro. O valor desta variável é zerado em um evento de reset, seguido por uma chamada para usb_class_init (), na qual os pontos são reconfigurados de acordo com a tarefa do usuário.



Trabalhando com registradores de transmissão e recepção



Como acabamos de dizer, na recepção lemos quantos dados foram realmente recebidos (o campo usb_rx_count), depois lemos os próprios dados, depois puxamos o módulo usb para que o buffer fique livre, você pode receber o próximo pacote. Para transmissão, ao contrário: gravamos os dados no buffer, depois definimos quanto foi gravado em usb_tx_count e, finalmente, puxamos o módulo para que o buffer fique cheio, podemos transferi-lo.



O primeiro rakecomece ao trabalhar com o próprio buffer: ele não é organizado em 32 bits, como o resto do controlador, e não em 8 bits, como você poderia esperar. E 16 bits cada! Como resultado, ele é escrito e lido em 2 bytes, alinhados com 4 bytes. Obrigado ST por fazer tal perversão! Como a vida seria chata sem ele! Agora que o memcpy comum é indispensável, você deve cercar funções especiais. A propósito, se alguém adora DMA, então parece ser capaz de fazer essa transformação por conta própria, embora eu não tenha testado.



E então o segundo ancinhocom a escrita nos registros do módulo. O fato é que pela configuração de cada endpoint - pelo seu tipo (controle, bulk, etc.) e estado - um registrador USB_EPnR é o responsável, ou seja, você simplesmente não pode alterar nada nele, você precisa estar atento para não estragar o resto. E em segundo lugar, já existem quatro tipos de bits neste registro! Alguns estão disponíveis apenas para leitura (isso é ótimo), outros para leitura e escrita (também normal), outros ignoram o registro 0, mas ao escrever 1, mudam o estado para o oposto (começa a diversão), e o quarto, no contrário, ignore o registro 1, mas o registro 0 redefine-os para 0. Diga-me, o que o viciado pensou em fazer bits em um registro que ignorasse 0 e ignorasse 1? Não, estou pronto para assumir que isso foi feito para preservar a integridade do registro, quando ele é acessado do código e do hardware. Mas o que você quer,Foi com preguiça de colocar o inversor de forma que os bits fossem zerados escrevendo 1? Ou então um inversor para que outros bits sejam invertidos escrevendo 0? Como resultado, a configuração de dois bits de registro se parece com isto (obrigado novamente a ST por tal perversão):



#define ENDP_STAT_RX(num, stat) do{USB_EPx(num) = ((USB_EPx(num) & ~(USB_EP_DTOG_RX | USB_EP_DTOG_TX | USB_EPTX_STAT)) | USB_EP_CTR_RX | USB_EP_CTR_TX) ^ stat; }while(0)
      
      





Ah sim, quase esqueci: eles também não têm acesso ao cadastro pelo número. Ou seja, macros USB_EP0R, USB_EP1R, etc. eles têm, mas se o número vier em uma variável, então, infelizmente. Tive que inventar meu próprio USB_EPx () - e o que fazer.



Bem, para cumprir as formalidades, ressaltarei que o sinalizador de prontidão (isto é, que já lemos os dados anteriores) é definido pela máscara de bits USB_EP_RX_VALID, e para escrita (ou seja, nós escrevemos os dados em cheio e pode ser transferido) - pela máscara USB_EP_TX_VALID.



Processando pedidos IN e OUT



A ocorrência de uma interrupção de USB pode sinalizar coisas diferentes, mas por enquanto vamos nos concentrar nas solicitações de comunicação. O sinalizador para tal evento será o bit USB_ISTR_CTR. Se tivermos visto, podemos descobrir com qual ponto o host deseja se comunicar. O número do ponto está oculto na máscara de bits USB_ISTR_EP_ID e a direção IN ou OUT está oculta nos bits USB_EP_CTR_TX e USB_EP_CTR_RX, respectivamente.



Como podemos ter muitos pontos, e cada um com seu próprio algoritmo de processamento, criaremos funções de retorno de chamada para todos eles, que seriam chamadas nos eventos correspondentes. Por exemplo, o host enviou dados para o endpoint3, lemos USB-> ISTR, retiramos de lá que a solicitação é OUT e que o número do ponto é 3. Portanto, chamamos epfunc_out [3] (3). O número do ponto entre colchetes é transmitido se de repente o código do usuário quiser travar um manipulador em vários pontos. Sim, mesmo no padrão USB, é comum marcar os pontos de entrada IN com um 7º bit engatilhado. Ou seja, o endpoint3 na saída terá o número 0x03 e na entrada - 0x83. Além disso, são pontos diferentes, podem ser usados ​​simultaneamente, não interferem um no outro. Bem, quase: em stm32 eles têm uma configuração do tipo (bulk, interrupt, ...) para recepção e transmissão. Portanto, o mesmo 0x83º ponto IN corresponderá ao retorno de chamada 'em epfunc_in [3] (3 | 0x80).



O mesmo princípio se aplica ao ep0. A única diferença é que seu processamento ocorre dentro da biblioteca, e não dentro do código do usuário. Mas e se você precisar processar solicitações específicas como alguns HID - não se preocupe em escolher o código da biblioteca? Para isso, existem callbacks especiais usb_class_ep0_out e usb_class_ep0_in, que são chamados em lugares especiais e têm um formato especial, sobre o qual falarei mais perto do final.



Vale ressaltar outro ponto não muito óbvio relacionado à ocorrência de interrupções no processamento de pacotes. Com as solicitações OUT, tudo é simples: os dados vieram, aqui estão. Mas a interrupção IN é gerada não quando o host envia uma solicitação IN, mas quando o buffer de transmissão está vazio. Ou seja, em princípio, essa interrupção é semelhante à interrupção de underrun do buffer UART. Portanto, quando queremos transferir algo para o host, simplesmente escrevemos os dados no buffer de transferência, aguardamos a interrupção IN e adicionamos o que não cabe (não se esqueça do ZLP). E tudo bem, mesmo com os endpoints "usuais", eles são controlados pelo programador, você pode ignorá-los por enquanto. Mas por meio do ep0, a troca está sempre acontecendo. Portanto, o trabalho com ele deve ser integrado à biblioteca.



Como consequência, o início da transferência é realizado pela função ep0_send, que escreve o endereço do início do buffer e a quantidade de dados a serem transferidos para a variável global, após o que, observe, ela própria puxa o evento IN manipulador pela primeira vez. No futuro, esse manipulador será chamado em eventos de hardware, mas você ainda precisa dar um push.



Bem, o manipulador em si é bastante simples: ele grava a próxima parte dos dados no buffer de transferência, muda o endereço do início do buffer e reduz o número de bytes restantes para transferência. Uma muleta separada está associada ao mesmo ZLP e à necessidade de responder a algumas solicitações com um pacote vazio. Neste caso, o fim da transferência é indicado pelo fato do endereço de dados ter se tornado NULL. E um pacote vazio - que é igual à constante ZLPP. Ambos ocorrem quando o tamanho é igual a zero, portanto, nenhum registro real ocorre.



Um algoritmo semelhante terá que ser implementado ao trabalhar com outros terminais. Mas essa é a preocupação do usuário. E a lógica de seu trabalho geralmente é diferente de trabalhar com ep0, portanto, em alguns casos, essa opção será mais conveniente do que o armazenamento em buffer no nível da biblioteca.



Lógica de comunicação USB



O host determina o próprio fato da conexão pela presença de um resistor pull-up entre qualquer linha de dados e a fonte de alimentação. Ele reinicializa o dispositivo, atribui a ele um endereço no barramento e tenta determinar o que exatamente estava preso nele. Para fazer isso, ele lê descritores de dispositivo e configuração (e, se necessário, alguns específicos). Ele também pode ler os descritores de string para entender como o dispositivo chama a si mesmo (embora se o par VID: PID for familiar para ele, ele preferiria extrair as linhas de seu banco de dados). Depois disso, o host pode carregar o driver apropriado e trabalhar com o dispositivo em um idioma que ele entenda. A linguagem que ele entende inclui solicitações e chamadas específicas para interfaces e terminais específicos. Faremos isso também, mas primeiro precisamos que o dispositivo seja pelo menos exibido no sistema.



Processando solicitações SETUP: DeviceDescriptor



Uma pessoa que mexeu no USB pelo menos um pouco deveria ter se preocupado por muito tempo: COKPOWEHEU, você está falando sobre solicitações IN e OUT, mas SETUP também está especificado no padrão. Sim, é, mas é mais uma espécie de solicitação OUT, especialmente estruturada e destinada exclusivamente ao endpoint 0. Vamos falar sobre sua estrutura e características de trabalho.



A estrutura em si é assim:



typedef struct{
  uint8_t bmRequestType;
  uint8_t bRequest;
  uint16_t wValue;
  uint16_t wIndex;
  uint16_t wLength;
}config_pack_t;
      
      





Os campos desta estrutura são considerados em muitas fontes, mas ainda assim vou lembrá-lo.

bmRequestType é uma máscara de bits, cujos bits significam o seguinte:

7: direção de transmissão. 0 - do host para o dispositivo, 1 - do dispositivo para o host. Na verdade, é o tipo da próxima transmissão, OUT ou IN.

6-5: solicitar classe

0x00 (USB_REQ_STANDARD) - padrão (por enquanto só vamos processá-los)

0x20 (USB_REQ_CLASS) - específico de classe (veremos nos próximos artigos)

0x40 (USB_REQ_VENDOR) - específico do fabricante ( Espero que não tenhamos que tocá-los)

4-0: interlocutor

0x00 (USB_REQ_DEVICE) - dispositivo como um todo

0x01 (USB_REQ_INTERFACE) - interface separada

0x02 (USB_REQ_ENDPOINT) -



ponto de extremidade

bRequest - própria solicitação wValue - pequeno campo de dados de 16 bits. No caso de pedidos simples, de modo a não conduzir transferências completas.

wIndex é o número do destinatário. Por exemplo, a interface com a qual o host deseja se comunicar.

wLength - o tamanho dos dados extras se 16 bits de wValue não forem suficientes.



Em primeiro lugar, ao conectar um dispositivo, o host tenta descobrir o que exatamente estava preso nele. Para fazer isso, ele envia uma solicitação com os seguintes dados:

bmRequestType = 0x80 (solicitação de leitura) + USB_REQ_STANDARD (padrão) + USB_REQ_DEVICE (para o dispositivo como um todo)

bRequest = 0x06 (GET_DESCRIPTOR) - solicitação do descritor

wValue = 0x0100 (DEVICE_DESCRIPTOR) - descritor do dispositivo como um todo

wIndex = 0 - não usado

wLength = 0 - sem dados adicionais

Em seguida, ele envia uma solicitação IN, onde o dispositivo deve colocar a resposta. Como lembramos, a solicitação IN do host e a interrupção do controlador estão fracamente acopladas, portanto, escreveremos a resposta imediatamente no buffer do transmissor ep0. Teoricamente, os dados deste e de todos os outros descritores estão vinculados a um dispositivo específico, portanto, não faz sentido colocá-los no núcleo da biblioteca. As solicitações correspondentes são passadas para a função usb_class_get_std_descr, que retorna ao kernel um ponteiro para o início dos dados e seu tamanho. A questão é que alguns descritores podem ser de tamanho variável. Mas DEVICE_DESCRIPTOR não é um deles. Seu tamanho e estrutura são padronizados e têm a seguinte aparência:



uint8_t bLength; // 
uint8_t bDescriptorType; // .    USB_DESCR_DEVICE (0x01)
uint16_t bcdUSB; // 0x0110  usb-1.1,  0x0200  2.0.     
uint8_t bDeviceClass; // 
uint8_t bDeviceSubClass; //
uint8_t bDeviceProtocol; //
uint8_t bMaxPacketSize0; // ep0
uint16_t idVendor; // VID
uint16_t idProduct; // PID
uint16_t bcdDevice_Ver; //  BCD-
uint8_t iManufacturer; //   
uint8_t iProduct; //   
uint8_t iSerialNumber; //  
uint8_t bNumConfigurations; //  (   1)
      
      





Em primeiro lugar, preste atenção aos dois primeiros campos - o tamanho do descritor e seu tipo. Eles são típicos para quase todos os descritores USB (exceto para HID, talvez). Além disso, se bDescriptorType for uma constante, então bLength deve ser quase contado manualmente para cada descritor. Em algum momento, me cansei disso e uma macro foi escrita



#define ARRLEN1(ign, x...) (1+sizeof((uint8_t[]){x})), x
      
      





Ele calcula o tamanho dos argumentos passados ​​a ele e o substitui em vez do primeiro. O fato é que às vezes os descritores são aninhados, de modo que um, digamos, requer um tamanho no primeiro byte, outro em 3 e 4 (número de 16 bits) e o terceiro em 6 e 7 (novamente um número de 16 bits) . As macros não se preocupam com os valores exatos dos argumentos, mas pelo menos o número deve ser o mesmo. Na verdade, macros para substituição em 1, em 3 e 4, bem como em 6 e 7 bytes também estão lá, mas mostrarei sua aplicação com um exemplo mais típico.



Por enquanto, vamos examinar os campos de 16 bits como VID e PID. É claro que misturar constantes de 8 e 16 bits em um array não funcionará, além de endiannes ... em geral, as macros vêm ao resgate novamente: USB_U16 (x).



Em termos de seleção de VID: PID é uma questão complicada. Se você planeja produzir produtos produzidos em massa, ainda vale a pena comprar um par pessoal. Para uso pessoal, você pode pegar o de outra pessoa em um dispositivo semelhante. Digamos que eu tenha pares de AVR LUFA e STM em meus exemplos. De qualquer forma, o host determina bugs de implementação específicos ao invés de atribuição deste par. Porque a finalidade do dispositivo é descrita em detalhes em um descritor especial.



Atenção, ancinho!Acontece que o Windows associa os drivers a este par, ou seja, você montou o dispositivo HID, mostrou o sistema e instalou os drivers. E então atualizamos o dispositivo no MSD (unidade flash) sem alterar o VID: PID, então os drivers permanecerão antigos e, naturalmente, o dispositivo não funcionará. Teremos que entrar em "gerenciamento de hardware", remover drivers e forçar o sistema a encontrar novos. Acho que não será surpresa para ninguém que o Linux não tenha esse problema: os dispositivos simplesmente se conectam e funcionam.



StringDescriptor



Outro recurso interessante dos descritores USB é o amor por strings. No modelo do descritor, eles são denotados pelo prefixo i, como iSerialNumber ou iPhone... Essas linhas estão incluídas em muitos descritores e, francamente, não sei por que existem tantos deles. Além disso, quando o dispositivo está conectado, apenas iManufacturer, iProduct e iSerialNumber estarão visíveis. Seja como for, as strings são os mesmos descritores (ou seja, os campos bLength e bDescriptorType estão presentes), mas em vez da estrutura adicional, há um fluxo de caracteres do tipo Unicode de 16 bits. O significado dessa perversão é novamente incompreensível para mim, porque esses nomes geralmente são dados em inglês de qualquer maneira, onde ASCII de 8 bits seria suficiente. Ok, você quer um conjunto de caracteres estendido, então UTF-8 seria usado. Pessoas estranhas ... Para a formação conveniente de strings, é conveniente usar - adivinhe - certo, macros. Mas desta vez não foi meu projeto, mas espiões do EddyEm. Uma vez que as strings são descritores, o host irá solicitá-los como descritores normais,apenas no campo wValue irá substituir 0x0300 (STRING_DESCRIPTOR). E em vez do byte menos significativo, haverá o índice da string real. Por exemplo, a consulta 0x0300 é uma string com índice 0 (ela é reservada para o idioma do dispositivo e quase sempre é igual a u "\ x0409") e a consulta 0x0302 é uma string com índice 2.



Atenção, ancinho! Não importa o quão grande seja a tentação de colar apenas uma string em iSerialNumber, mesmo uma string com uma versão honesta como u``1.2.3 '' - não faça isso! Alguns sistemas operacionais acreditam que deveria haver apenas dígitos hexadecimais, ou seja, '0' - '9', 'A' - 'Z' e é isso. Você não consegue nem pontos. Provavelmente, de alguma forma contam o hash desse "número" para identificá-lo ao reconectar, não sei. Mas notei tal problema ao testar em uma máquina virtual com Windows 7, ela considerou o dispositivo com defeito. Curiosamente, o Windows XP e 10 não notou o problema.



ConfigurationDescriptor



Do ponto de vista do host, o dispositivo representa um conjunto de interfaces separadas, cada uma delas projetada para resolver algum problema. Um descritor de interface descreve seu dispositivo e terminais associados. Sim, os terminais não são descritos por si próprios, mas apenas como parte da interface. Normalmente, as interfaces com uma arquitetura complexa são controladas por solicitações SETUP (ou seja, por meio de ep0), nas quais o campo wIndex corresponde ao número da interface. O máximo é permitido para embolsar o ponto final para interrupções. E a partir das interfaces de dados, o host só precisa de descrições dos terminais e a troca irá passar por eles.



Pode haver muitas interfaces em um dispositivo, e outras muito diferentes. Portanto, para não se confundir onde uma interface termina e outra começa, o descritor indica não apenas o tamanho do "cabeçalho", mas também separadamente (geralmente 3-4 bytes) o tamanho total da interface. Assim, a interface se dobra como uma boneca aninhada: dentro de um contêiner comum (que armazena o tamanho do "título", bDescriptorType e o tamanho total do conteúdo, incluindo o título) pode haver alguns contêineres menores, mas dispostos em o mesmo caminho. E cada vez mais dentro. Aqui está um exemplo de um descritor para um dispositivo HID primitivo:




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
    0, // bNumEndpoints
    HIDCLASS_HID, // bInterfaceClass: 
    HIDSUBCLASS_NONE, // bInterfaceSubClass: 
    HIDPROTOCOL_NONE, // bInterfaceProtocol: 
    0x00, // iInterface
  )
  ARRLEN1(
    bLENGTH, //bLength
    USB_DESCR_HID, //bDescriptorType
    USB_U16(0x0101), //bcdHID
    0, //bCountryCode
    1, //bNumDescriptors
    USB_DESCR_HID_REPORT, //bDescriptorType
    USB_U16( sizeof(USB_HIDDescriptor) ), //wDescriptorLength
  )
  )
};
      
      





Aqui, o nível de aninhamento é pequeno, e nenhum ponto final é descrito - bem, tentei escolher um dispositivo mais simples. Alguma confusão aqui pode ser causada pelas constantes bLENGTH e wTOTALLENGTH iguais a zeros de oito e dezesseis bits. Visto que neste caso as macros são usadas para calcular o tamanho, seria estranho duplicar seu trabalho e contar bytes manualmente. Como é estranho escrever zeros. E as constantes são algo perceptível, contribuindo para a clareza do código.



Como você pode ver, este descritor consiste no "cabeçalho" USB_DESCR_CONFIG (armazenando o tamanho total do conteúdo incluindo ele mesmo!), A interface USB_DESCR_INTERFACE (descrevendo os detalhes do dispositivo) e USB_DESCR_HID, que em termos gerais diz que tipo de HID nós estamos renderizando. E exatamente o que em termos gerais: uma estrutura HID específica é descrita em um descritor especial HID_REPORT_DESCRIPTOR, que não vou considerar aqui, simplesmente porque a conheço muito mal. Portanto, vamos nos restringir a copiar e colar de algum exemplo .



Voltemos às interfaces. Considerando que eles têm números, é lógico supor que podem haver várias interfaces em um dispositivo. Além disso, eles podem ser responsáveis ​​por uma tarefa comum (digamos, a interface de controle USB-CDC e a interface de dados) e por tarefas fundamentalmente não relacionadas. Digamos, nada nos impede (exceto pela falta de conhecimento até agora) em um controlador implementar dois adaptadores USB-CDC mais uma unidade flash USB e, digamos, um teclado. Obviamente, a interface da unidade flash não conhece a porta COM. No entanto, existem armadilhas aqui, que, espero, um dia iremos considerar. Também é importante notar que uma interface pode ter várias configurações alternativas (bAlternateSetting) que diferem, digamos, no número de terminais ou na frequência de sua pesquisa. Na verdade, é por isso que foi feito: se o host achar que é melhor economizar largura de banda,ele pode mudar a interface para o modo alternativo de sua preferência.



Comunicação com HID



De um modo geral, os dispositivos HID simulam objetos do mundo real, que não possuem tantos dados, mas um conjunto de determinados parâmetros que podem ser medidos ou definidos (solicitações SET_REPORT / GET_REPORT) e que podem notificar o host sobre um evento externo repentino (INTERRUPT). Assim, na verdade, esses dispositivos não se destinam à troca de dados ... mas quem parou quando!



Não tocaremos nas interrupções por enquanto, uma vez que elas precisam de um endpoint especial. Mas vamos considerar a leitura e configuração de parâmetros. Neste caso, existe apenas um parâmetro, que é uma estrutura de dois bytes, que, por predefinição, são responsáveis ​​por dois LEDs, ou seja, por um botão e um contador.



Vamos começar com um mais simples - lendo a pedido HIDREQ_GET_REPORT. Na verdade, esta é a mesma solicitação de qualquer DEVICE_DESCRIPTOR, apenas específico para o HID. Além disso, essa solicitação não é endereçada ao dispositivo como um todo, mas à interface. Ou seja, se implementamos vários dispositivos HID independentes em um dispositivo, eles podem ser diferenciados pelo campo wIndex da solicitação. É verdade que essa não é a melhor abordagem especificamente para HID: é mais fácil fazer o próprio descritor composto. Em todo caso, estamos longe de tais perversões, portanto nem mesmo analisaremos o que e para onde o host tentou enviar: para qualquer solicitação à interface e com o campo bRequest igual a HIDREQ_GET_REPORT, retornaremos os dados reais. Em teoria, essa abordagem pretende retornar descritores (com todos bLength e bDescriptorType), mas no caso do HID, os desenvolvedores decidiram simplificar tudo e trocar apenas dados.Assim, retornamos um ponteiro para nossa estrutura e seu tamanho. Bem, um pouco de lógica adicional, como botões de processamento e um contador de solicitações.



Um caso mais complexo é uma solicitação de gravação. Esta é a primeira vez que encontramos dados adicionais em uma solicitação SETUP. Ou seja, o núcleo de nossa biblioteca deve primeiro ler a própria solicitação e só depois os dados. E transferi-los para a função de usuário. E eu lembro a você que não temos buffer. Como resultado de alguma magia de baixo nível, o seguinte algoritmo foi desenvolvido. O retorno de chamada sempre será chamado, mas diremos a partir de qual byte os dados estão agora no buffer de recebimento do terminal (deslocamento) e o tamanho desses dados (tamanho). Ou seja, quando a própria solicitação é recebida, os valores de deslocamento e tamanho são zero (não há dados). Quando o primeiro pacote é recebido, o deslocamento ainda é zero e o tamanho é o tamanho dos dados recebidos. Para o segundo, o deslocamento será igual ao tamanho de ep0 (porque se os dados tiverem que ser divididos, eles o farão de acordo com o tamanho do ponto final), e o tamanho será igual ao tamanho dos dados recebidos.Etc. Importante! Se os dados forem aceitos, eles devem ser lidos. Isso pode ser feito pelo manipulador chamando usb_ep_read () e retornando 1 (eles dizem "Eu pensei nisso, não se preocupe") ou simplesmente retornando 0 ("Eu não preciso desses dados") sem ler - então, o núcleo da biblioteca tratará da limpeza. A função baseia-se neste princípio: verifica se os dados estão disponíveis e, em caso afirmativo, lê-os e acende os LEDs.



Software de troca de dados



Aqui eu não reinventar a roda, mas tomou um ready-made programa do anterior artigo .



Conclusão



Isso, na verdade, é tudo. Falei o básico de como trabalhar com USB usando um módulo de hardware no STM32, também toquei em alguns rake. Considerando a quantidade de código muito menor do que o horror que STMCube gera, será mais fácil descobrir. Na verdade, ainda não descobri isso no macarrão Cube, há muitas chamadas da mesma coisa em combinações diferentes. Muito melhor para entender a opção do EddyEm , da qual comecei. Claro, não existe sem ombreiras, mas pelo menos é adequado para a compreensão. Também me gabo de que o tamanho da minha versão é quase 5 vezes menor do que o do ST (~ 2,7 kB versus 14) - apesar do fato de que não estive envolvido na otimização e, com certeza, você ainda pode reduzi-la.



Também gostaria de observar a diferença no comportamento de vários sistemas operacionais ao conectar equipamentos questionáveis. O Linux funciona mesmo se houver erros nos descritores. Windows XP, 7, 10, ao menor erro, juram que "o aparelho está quebrado, me recuso a trabalhar com ele". E o XP às vezes até no BSOD caiu de indignação. Sim, eles também exibem constantemente "o dispositivo pode funcionar mais rápido", não sei o que fazer a respeito. Em geral, não importa o quão bom o Linux seja para o desenvolvimento, ele perdoa muito, é necessário testar em sistemas menos amigáveis.



Planos adicionais: considerar outros tipos de terminais (até agora havia um exemplo apenas com Controle); considere outros controladores (digamos, eu ainda tenho at90usb162 (AVR) e gd32vf103 (RISC_V) por aí), mas esses são planos muito distantes. Também seria bom dar uma olhada em dispositivos USB individuais como os mesmos HIDs, mas também não é uma tarefa prioritária.



All Articles