Gamepad USB HID com ATtiny85

Monte seu próprio gamepad USB HID do zero usando o microcontrolador ATtiny85, o driver V-USB e muita criatividade.

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.

Latch circuit
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.

Shift register circuit
Registradores de deslocamento em série

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.

Signal line circuit
Resistores de pull-down na linha SIGNAL

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.

Pinos do microcontrolador e suas funções

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.

Circuito de conexão USB

Driver do LED RGB

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

Circuito LED RGB

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

Circuito de gamepad em uma breadboard

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

Testando se a porta USB encaixa no firo que fiz na lata

 

Lata de sardinha assustada (os olhos são marcações para furar os botões laterais e a boca é a porta USB)

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

Parte inferior da placa perfurada mostrando minha soldagem maravilhosa

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.

Placa de contato dos botões

 

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.

Vista superior do gamepad parcialmente montado

 

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

Botão direcional feito de massa epóxi branca

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

Botões feitos de botões

 

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

Botão lateral personalizado, suporte e minhas unhas de ogro

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

Vista lateral mostrando a placa de circuito, o suporte dos contatos dos botões, a tampa e os postes de montagem

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!