Bora fazer um Gamepad HID USB usando um ATtiny85?! Este projeto é de 2020, o auge da pandemia, de quando as pessoas estavam espalhando a COVID-19 pelo mundo todo. Nesse post, eu mostrarei como programar o microcontrolador ATtiny85 como um gamepad HID USB usando a biblioteca V-USB e como construir o circuito com registradores de deslocamento e alguns componentes passivos.
O ATtiny85 é um microcontrolador pequeno e poderoso que, apesar de sua baixa contagem de pinos, é perfeito para esse tipo de projeto.
O que é HID?
HID é a sigla em inglês para Dispositivo de Interface Humana, um dispositivo que comunica-se com o computador por meio de drivers fornecidos pelo sistema operacional, eliminando a necessidade de instalação de drivers adicionais. Teclados, mouses e controladores de jogos são exemplos comuns de dispositivos HID. Esses dispositivos utilizam o protocolo USB para se comunicar com o computador, e sua configuração é definida pelo descritor de relatório HID.
O descritor de dispositivo HID é usado para informar ao host (geralmente seu desktop ou notebook, mas pode ser qualquer dispositivo que opere em “modo host” USB) como os dados enviados (relatórios) pelo dispositivo serão interpretados. É uma matriz de bytes que você define em seu programa, configurada de acordo com as necessidades do seu dispositivo. Você pode encontrar ótimas informações no site usb.org. Também na página, você encontra uma ferramenta que ajuda a criar o descritor de dispositivo para seu HID. O descritor do dispositivo configura coisas como a classe, número de dispositivos (sim, você pode usar apenas um dispositivo USB e reportar vários controles =]) e o tamanho dos seus pacotes de dados. Por exemplo, o descritor do meu controle está configurado com 8 botões e 2 eixos, mas poderia ser configurado com mais botões ou eixos alterando a matriz de bytes do descritor.
Desenhando o Circuito
A configuração de hardware para este projeto é relativamente simples. Precisaremos de um microcontrolador ATtiny85, um conector USB, dois registradores de deslocamento (shift registers) e alguns componentes passivos simples. Usei o Kicad para projetar os esquemas do circuito e a placa (apenas a posição dos componentes, pois soldei tudo com fios). Você também pode encontrar mais sobre montagem de placas no mesmo estilo nestas postagens: Construindo uma Placa de Desenvolvimento para ESP-01/ESP-01S ou Construindo um Circuito de Teste para um Oscilador.
Expandindo Pinos de Entrada e Saída
O ATtiny85 (datasheet) é um ótimo microcontrolador, mas possui poucos pinos: dois para alimentação, um para reset e cinco pinos multifuncionais (E/S). Para adequar estes pinos de E/S às minhas necessidades, usei dois registradores de deslocamento conectados em série. Um registrador de deslocamento recebe uma entrada serial (data) com clock e latch e tem a capacidade de configurar seus pinos de saída de acordo com os dados recebidos em diferentes níveis de tensão: em um estado baixo (0V), um estado alto (5V para este projeto) e, às vezes (dependendo do registrador de deslocamento) um estado de alta impedância (equivaente ao pino desconectado). Neste projeto, usei o registrador de deslocamento 74HC595 (datasheet).
Em seguida, defini os pinos do microcontrolador para as seguintes tarefas:
- Acionamento dos registradores de deslocamento (três pinos: dados, clock e latch)
- Leitura do estado de cada botão e eixos (um pino: detecção de estado)
- Comunicação USB (dois pinos: D+ e D-)
Mecanismo de Latch
Para trabalhar com o 74HC595, precisamos “segurar” os bits enviados pelo microcontrolador a cada pulso CLOCK (isto é, precisamos acionar o pino LATCH), o que significa que pelo menos três pinos dos cinco pinos de E/S disponíveis serão usados. Poderíamos programar o pino RESET para funcionar como um pino de E/S comum e obter todos os seis pinos necessários, mas depois disso, o pino RESET não poderia ser mais usado para programar o microcontrolador. Isso não é uma boa ideia nos estágios iniciais do desenvolvimento de um circuito. Em vez disso, usaremos a linha CLOCK para também acionar um transistor NPN e carregar um capacitor. Este capacitor mantém o pino LATCH do registrador de deslocamento em um estado alto por tempo suficiente para que os registradores de deslocamento possam ativar seus pinos de saída.
A imagem a seguir mostra o driver LATCH.

Selecionei o valor do capacitor por tentativa e erro. Para o meu circuito, um capacitor cerâmico de 100 nF foi suficiente.
Eixo e Botões
Meu controle tem 8 botões e 2 eixos (dois pinos para o eixo X e dois pinos para o eixo Y). Para ler todos os estados possíveis (pressionado ou não pressionado) para todas essas entradas, são necessários pelo menos 12 pinos dos 16 fornecidos pelos dois 74HC595 conectados em série. Cada saída tem um diodo para evitar leituras sobrepostas, que acontece quando vários botões são pressionados ao mesmo tempo.

O deslocar um bit através dos registradores de deslocamento ativa a leitura de cada botão individualmente e o microcontrolador lê o nível de tensão na linha SIGNAL.
Lendo Sinais de Entrada
A linha SIGNAL deve ter um resistor pull-down, caso contrário, capacitâncias parasitas podem causar leituras incorretas ou ativar permanentemente todos os botões e eixos ao mesmo tempo. Usei um resistor de 1k para o pull-down, mas acho que valores entre 1k e 4k7 podem ser usados.

Conexão USB
De acordo com a documentação da biblioteca V-USB, devemos usar um pino de interrupção para a linha D-. Neste ponto, podemos definir todos os pinos físicos do microcontrolador para cada função, como mostrado abaixo.

A documentação da biblioteca V-USB fornece alguns exemplos de circuitos para as linhas de dados USB, como mostrado na próxima figura. Ela deve seguir os padrões USB, caso contrário, o dispositivo pode não funcionar como esperado.

Driver do LED RGB
Usei três pinos livres dos registradores de deslocamento para acionar um LED RGB.

Eu construí o circuito em uma breadboard e comecei a escrever o firmware.

Desenvolvimento do Firmware
A maior parte do trabalho pesado deste firmware é feita pela biblioteca V-USB (que sorte hein?!), que também é a que mais consome tempo do próprio microcontrolador. O firmware pode ser dividido em cinco partes:
- Configuração do driver V-USB
- Definição do descritor de relatório HID
- Driver do registrador de deslocamento
- Driver do LED RGB
- Loop principal
Configuração do driver V-USB
A configuração do driver V-USB é bem tranquila porque o cabeçalho usbconfig.h está bem documentado. Basta seguir as instruções nos comentários em cada linha definida, modificando de acordo com as nossas necessidades. Aqui estão algumas configurações importantes que fiz para este projeto:
#define USB_CFG_IOPORTNAME B
#define USB_CFG_DMINUS_BIT 3
#define USB_CFG_DPLUS_BIT 4
#define USB_INTR_CFG PCMSK
#define USB_INTR_CFG_SET (1 << USB_CFG_DPLUS_BIT)
#define USB_INTR_CFG_CLR 0
#define USB_INTR_ENABLE GIMSK
#define USB_INTR_ENABLE_BIT PCIE
#define USB_INTR_PENDING GIFR
#define USB_INTR_PENDING_BIT PCIF
#define USB_INTR_VECTOR PCINT0_vect
#define USB_CFG_VENDOR_ID 0xc0, 0x16
#define USB_CFG_DEVICE_ID 0xdc, 0x27
#define USB_CFG_VENDOR_NAME 'A', 'R', 'E', 'N', 'A', '6', '4'
#define USB_CFG_VENDOR_NAME_LEN 7
#define USB_CFG_DEVICE_NAME 'F', 'i', 'n', 'g', 'e', 'r', 'G', 'r', 'i', 'n','d', 'e', 'r'
#define USB_CFG_DEVICE_NAME_LEN 13
Você deve colocar o nome do seu controle em USB_CFG_DEVICE_NAME. É o nome que aparecerá para o sistema operacional quando você conectar o controle. O meu é FingerGrinder.
Definição do descritor de relatório HID
Usei o documento “HID usage table”, que pode ser encontrado aqui, para criar meu descritor. Este arquivo é atualizado periodicamente. Você pode encontrar a atualização aqui. Você também pode usar a ferramenta “HID descriptor tool” para criar o descritor de dispositivo.
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x05, // USAGE (Game Pad)
0xa1, 0x01, // COLLECTION (Application)
0x09, 0x01, // USAGE (Pointer)
0xa1, 0x00, // COLLECTION (Physical)
0x09, 0x30, // USAGE (X)
0x09, 0x31, // USAGE (Y)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x26, 0xff, 0x00, // LOGICAL_MAXIMUM (255)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x02, // REPORT_COUNT (2)
0x81, 0x02, // INPUT (Data, Var, Abs)
0xc0, // END_COLLECTION
0x05, 0x09, // USAGE_PAGE (Button)
0x19, 0x01, // USAGE_MINIMUM (Button 1)
0x29, 0x08, // USAGE_MAXIMUM (Button 8)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x75, 0x01, // REPORT_SIZE (1)
0x95, 0x08, // REPORT_COUNT (8)
0x81, 0x02, // INPUT (Data, Var, Abs)
0xc0 // END_COLLECTION
Driver do Registrador de Deslocamento
O ATtiny85 possui uma interface serial genéria chamada USI (Universal Serial Interface), que pode ser configurada para operar em modo SPI. Usei essa interface para gerar o sinal de CLOCK e enviar os DADOS seriais para os registradores de deslocamento.
A parte do LATCH deste driver precisa ser colocada no loop principal porque precisamos “segurar” o nível do sinal antes de tentar ler cada botão.
SPI_PORT |= (1 << USCK_PIN); //enable clock (and charge latch cap)
_delay_us(1); //waiting for charge
SPI_PORT &= ~(1 << USCK_PIN); //disable clock
Se você não entende como eu expandi a E/S do microcontrolador com registradores de deslocamento ou não entendeu como ler o estado dos botões usando um único pino de entrada, sugiro que você assista este vídeo.
Driver do LED RGB
O driver do LED RGB foi construído com base neste código elegante feito por Łukasz Podkalicki
Loop Principal
No loop principal, é onde consultamos o USB e “esperamos” para enviar os dados relativos ao estado atual do controle caso algum botão seja pressionado. É importante classificar/ordenar os dados de acordo com o descritor definido anteriormente.
usbPoll();
if(usbInterruptIsReady()) { //send data when the usb is ready to receive
if(has_changed) { //and when any change occurred
buildReport(); //build the report according descriptor
has_changed = 0; //clean change flag
}
usbSetInterrupt(report_buffer, sizeof(report_buffer)); //send data
}
Depois disso, temos um loop para ler todos os botões e marcar se ele foi pressionado ou não.
while(!(mask & 0x1000)) {
// placing bit on right place to shift into shifters
b2 = (uchar)((mask & 0xff00) >> 8);
b1 = (uchar)(mask & 0x00ff);
b2 |= (rgb_led_state() << 5);
SPI_PORT |= (1 << USCK_PIN); //enable clock (and charge latch cap)
_delay_us(1); //waiting for charge
SPI_PORT &= ~(1 << USCK_PIN); //disable clock
// loading serial data (shifting bits to the shift register)
simple_spi_send(b2);
simple_spi_send(b1);
_delay_us(666); //waiting for button settle down (debouncing)
if(PINB & 0x01) { //reading if the button is pressed
signal |= mask; //add the state of each button to the buffer
}
mask = (mask << 1); //set the next button to be read
}
Compilação e Instalação
Para escrever o código, usei o VSCode com extensões C/CPP. O processo de compilação é feito com Makefile. Aqui está um exemplo.
# -------------- start of configurtion --------------
PROJ_NAME = firmware
DEVICE = attiny85
CLOCK = 16500000L
PROGRAMMER = usbasp
# https://www.engbedded.com/fusecalc/
FUSE_LOW = 0x62
FUSE_HIGH = 0xdd
FUSE_EXTENDED = 0xff
INCLUDES = -I/usr/lib/avr/include -I./src -I./src/usbdrv
LFLAGS =
LIBS =
CFLAGS = -std=c11 -Wl,-Map,$(PROJ_NAME).map -mmcu=$(DEVICE) -DF_CPU=$(CLOCK) $(INCLUDES)
CPPFLAGS =
# -------------- end of configuration --------------
AVRDUDE = avrdude
OBJCOPY = avr-objcopy
OBJDUMP = avr-objdump
AVRSIZE = avr-size
CC = avr-gcc
H_SOURCE = $(wildcard ./src/*.h)
H_SOURCE += $(wildcard ./src/usbdrv/*.h)
C_SOURCE = $(wildcard ./src/*.c)
C_SOURCE += $(wildcard ./src/usbdrv/*.c)
S_SOURCE = $(wildcard ./src/usbdrv/*.S)
CPP_SOURCE = $(wildcard ./src/*.cpp)
OBJ = $(C_SOURCE:.c=.o) $(S_SOURCE:.cpp=.o) $(CPP_SOURCE:.cpp=.o)
RM = rm -rf
all: objFolder hex eep size
$(PROJ_NAME).elf: $(OBJ)
@ echo 'Linking: $@'
$(CC) $(CFLAGS) $(CPPFLAGS) $(LFLAGS) $(LIBS) $^ -o $@
@ echo 'Finished linking: $@'
@ echo ' '
./obj/%.o: ./src/%.c ./src/%.cpp ./src/%.h
@ echo 'Building objects: $<'
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
@ echo ' '
./obj/main.o: ./src/main.c $(H_SOURCE)
@ echo 'Building main: $<'
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
@ echo ' '
hex: $(PROJ_NAME).elf
$(OBJCOPY) -R .eeprom -R .fuse -R .lock -R .signature -O ihex $(PROJ_NAME).elf $(PROJ_NAME).hex
eep: $(PROJ_NAME).elf
$(OBJCOPY) -j .eeprom --no-change-warnings --change-section-lma .eeprom=0 -O ihex $(PROJ_NAME).elf $(PROJ_NAME).eep
size: $(PROJ_NAME).elf
$(AVRSIZE) --format=avr --mcu=$(DEVICE) $(PROJ_NAME).elf
disasm: $(PROJ_NAME).elf
$(OBJDUMP) -d $(PROJ_NAME).elf
objFolder:
@ mkdir -p obj
clean:
@ $(RM) ./obj/*.o ./src/*.o $(PROJ_NAME).elf $(PROJ_NAME).hex $(PROJ_NAME).eep $(PROJ_NAME).map
test:
$(AVRDUDE) -c $(PROGRAMMER) -p $(DEVICE) -v
flash: all
$(AVRDUDE) -c $(PROGRAMMER) -p $(DEVICE) -U flash:w:$(PROJ_NAME).hex:i
dump:
$(AVRDUDE) -c $(PROGRAMMER) -p $(DEVICE) -U flash:r:dump_$(shell date +'%y%m%d%H%M%S').hex:i
fuse:
$(AVRDUDE) -c $(PROGRAMMER) -p $(DEVICE) -U lfuse:w:$(FUSE_LOW):m -U hfuse:w:$(FUSE_HIGH):m -U efuse:w:$(FUSE_EXTENDED):m
.PHONY: all clean
# DO NOT DELETE THIS LINE -- make depend needs it
Para compilar basta digitar:
make clean && make
Para atualizar o firmware, eu uso o AVRDUDE (também configurado no makefile). Basta executar este comando:
make flash
Os fusíveis foram configurados para este projeto específico. Antes de executar make fuse, leia datasheet do ATtiny85 para entender seus significados.
Construindo o Corpo do Gamepad
Esta tarefa foi a que mais consumiu tempo. Não tenho uma cortadora a laser nem uma impressora 3D, então todo o trabalho foi feito à mão, usando ferramentas pré-históricas, como serras, limas e lixas. O trabalho só não foi pior porque tenho uma micro retífica e uma furadeira.
Pensei que todo o trabalho seria concluído num fim de semana, mas eu estava completamente errado! Levei quase quinze dias para terminar o projeto inteiro. A parte eletrônica e a programação foram finalizadas no fim de semana, mas todo o trabalho manual estrutural levou mais de dez dias. A maior parte do tempo foi gasta procurando a peça certa, cortando, lixando, colando e encaixando tudo.
Lata de Sardinha e Acrílico
Usei uma lata de sardinha como carcaça, acrílico reaproveitado como painel para os botões e direcionais, suportes de placa-mãe Desktop para separar a montagem, “botões” como botões (isso mesmo, aqueles botões usados para costura), partes de uma calculadora de bolso para fazer os contatos dos botões e algumas coisas aleatórias para manter tudo no lugar.
Comi toda a sardinha dentro desta lata, lavei-a com bastante detergente, apliquei um filme de vinil azul por fora, cortei um pedaço de papel cartão e coloquei no fundo para garantir que não houvesse curto-circuito, marquei o local para furar a porta USB e finalizei com uma lima pequena. No final, cortei pedaços fininhos de EVA preto e colei na borda para ficar bonito!
Imagens


A ontagem do citcuito e a soldagem manual também levaram tempo, mas foram concluídas no fim de semana.

A placa de contatos dos botões foi construída usando acrílico reaproveitado, pedaços de uma placa de circuito impresso de uma calculadora antiga, fios de cobre e cabo de uma unidade de disquete antiga.

Os buracos dos botões foram feitos com uma furadeira e o rolo de lixa da micro-retifica. Cortei o lugar do direcional com o disco de corte e finalizei com uma lima.

O controle direcional foi feito com massa epóxi e os botões do gamepad com botões coloridos.

Botões coloridos foram empilhados e colados com cola epóxi sobre dois botões brancos.

Os botões laterais e seus suportes foram feitos sob medida usando restos aleatórios de acrílico e pedaços de algumas canetas.

A estrutura foi empilhada com cola epóxi, postes de montagem usados para segurar placas-mãe e parafusos.

Conclusão

O controle tem um desempenho surpreendentemente bom, da pra jogar jogos que pedem velocidade e precisão. No entanto, há falhas no D-PAD e nos botões que poderiam ser corrigidas com o uso de métodos ou ferramentas mais precisas durante a construção. Talvez eu use resina epóxi e moldes na próxima vez.
Construir um controle HID com o microcontrolador Atmel ATtiny85 e a biblioteca V-USB foi bem tranquilo. Apesar dos desafios, a tarefa de reaproveitar materiais descartados em algo útil foi muito boa. Preciso de uma impressora 3D e uma cortadora a laser, hehehe!
Pegue o código fonte: https://github.com/raffsalvetti/FingerGrinder
Até mais!


