analytics2

sexta-feira, 4 de março de 2011

USB Device Drivers.. em python!? (parte 1)

Bom, quase isso. Ha um tempo, eu consegui uma impressora LX-300 em bom estado.. pra quem não conhece, trata-se de uma impressora matricial -- sim! fitas, agulhas e barulho -- muito confiável e rápida.


Eu queria essa impressora só pela nerdisse, prazer de ouvir o som enquanto eu imprimisse algum código em formulário contínuo . Como sempre, existe um problema: sendo um pouco velha, a impressora só tem conexão via porta paralela, tornando impossível de ligar em qualquer um dos meus computadores. Crap.

Além disso, eu queria ter uma porta paralela disponível denovo, para poder interfacear com algum eventual hardware -- veja bem, essa porta é extremamente fácil de se manusear; tem um número razóavel de entradas e saídas, que ainda podem ser expandidas com hardware e software trivial; usa níveis lógicos TTL e é rápida o bastante (entre 50KB/s e 2MB/s, nos seus diversos modos de operação [1]) para a maioria das aplicações simples.




Controlador de leds que eu fiz em Delphi, em 2006, quando ainda usava Windows

So, a solução trivial seria comprar um adaptador usb-paralelo -- note que eu não me liguei na época, que NÃO dá pra acessar uma porta paralela usb da mesma forma que se acessa uma porta física, local. Comprei um adaptador 7705 da Mosbosta Moschip, que prometia suporte no linux. É ÓBVIO que não funcionou. A compilação (código muito nojento, btw) causava uma explosão de erros bizarros -- tentei todas as mandingas, magias e cheatcodes que eu sabia ou encontrei, mas nada de fazer funcionar. Até entrei em contato com o suporte da importadora (?) brasileira, mas tudo que eu recebi de volta foi isso:


Curiosamente, eu consegui acessar a impressora de dentro de uma máquina virtual (obrigado VirtualBox, eu te adoro :D) rodando VômitowsWindows XP com os drivers adequados. Que gracinha. Mas o que me deixa mais puto é ver que se preocuparam em fazer uma versão para o Windows 7 64 bits -- mas que se foda manter a atualização dos drivers pra linux.

Se eu estivesse precisando mesmo da funcionalidade, teria trocado o produto.. mas como esse não é o caso, existe aqui uma oportunidade para aprender alguma coisa. Então eu resolvi escrever meu próprio driver usb.

--

O primeiro passo foi estudar a documentação do padrão USB. Duas coisas que ajudaram muito foi o USB in a Nutshell, que resume e explica de forma bem clara o padrão USB e o Linux Cross Reference, um guia com todas as funções e arquivos do kernel, que foi de importância fundamental, permitindo visualizar as diferenças entre as versões.

Com alguma cachaça noção na cabeça, comecei a modificar o usb-skeleton, e logo consegui detectar o dispositivo (VendorId=0x9710 ProductId=0x7705) e colocá-lo no /dev. Com essa base pronta, minha intenção inicial era criar um char device e manipular a porta com comandos tipo "echo valor > /dev/portaparalela".

O problema dessa implementação é que não da pra especificar onde você quer escrever o byte (registrador de dados ou de controle?), sem contar que torna-se impossível de configurar/acessar as diversas características do controlador. Estudando a implementação original do driver, consegui isolar a função utilizada para se comunicar com o equipamento.

static int SendMosCmd(void *pp, __u8 request, __u16 value, __u16 index, void *data)
{
    (...)

    status = usb_control_msg (usbdev, Pipe, request, requesttype, value, index, data, size, timeout);

    (...)
}

Uma simples mensagem de controle.. a maioria desses campos estão contidos no datasheet do controlador. O resto, supostamente, seria simples de configurar. Implementei funções de leitura e escrita baseadas no envio de mensagens de controle e finalmente, dois dias depois de começar a ler as especificações, consegui acender leds.

Controlando leds e lendo botões com o adaptador paralelo-usb

É claro que nada nem tudo funcionou como esperado, com certeza que por inexperiência minha em programar a nível de kernel. Estava acontecendo todo tipo de erro bizarro -- em especial, um byte só era enviado para a porta quando o descritor era fechado. Como consequência, para mandar vários valores era necessário abrir o dispositivo, escrever e fechar. Sem um bom GDB para debugar (estava dependendo da saída do dmesg) e sem a menor idéia de como resolver, abandonei essa estratégia e resolvi partir para algo mais simples.

--

Python - XKCD.com - Randall Munroe

Não adianta nada ficar batendo a cabeça, esperando encontrar a solução na tentativa e erro. Procurando um meio de contornar a situação, descobri uma biblioteca chamada PyUSB. E tudo se resolveu da seguinte forma:

import usb.core
import usb.util

(...)

    self.dev = usb.core.find (idVendor=vendor, idProduct=product)

(...)

    self.dev.ctrl_transfer (bmRequestType=bmReq, bRequest=bReq, wValue=(wValBase + data), wIndex=index)

(...)

Entendido como a biblioteca funciona, foi possível escrever um driver em "user-space" -- funciona da seguinte forma: o programa procura o dispositivo e cria um objeto para manipulá-lo; é criada uma área de memória compartilhada através de um módulo escrito em C; três threads (dados; controle; status) são lançadas para monitorar essa área -- talvez isso não seja uma boa idéia, preciso de opniões; quando o conteúdo de dados/controle muda, o novo valor é enviado para a porta; a cada 1ms o valor de status é lido da porta e salvo na área; isso continua infinitamente...

O resultado final pode ser visto no vídeo abaixo.


 
É claro que essa solução está longe de ser ideal, mas serve perfeitamente como prova de conceito. Do jeito que está agora, a porta funciona com as configurações padrão (seja lá quais forem elas. Está no datasheet) e não serve para controlar uma impressora -- a lx vai ter que esperar mais um pouco. O consumo de cpu também é escrotamente alto, ficando em torno de 20% a 30%. Não chega a afetar o desempenho do sistema, mas é totalmente inaceitável.

O próximo passo desse projeto é implementar as funções de configuração do controlador (fácil, mas é trabalho braçal) e ver se é possível criar um device do tipo "porta paralela" em python, visível para o gerenciador de impressoras -- finalmente, com o que foi aprendido, escrever um driver adequado em nível de kernel. E foi assim que aconteceu.

Os códigos estão disponíveis aqui. São feios. Muito feios. Cuidado.

7 comentários:

Jorcy disse...

iaehauheuaeuhauehuahea .... pois eh neh ..... dps de seculos do ultimo post.

1 - "Moshit" seria mais adequado .....

2 - ECP, EPP ou ECP+EPP ?!

3 - "um byte só era enviado para a porta quando o descritor era fechado." -> mesma merda ainda ?! .... pow ..... faltou umas nocoes de debugging do Kernel aqui ... e o dmesg naum ajuda mto nisso ...

4 - "três threads (dados; controle; status) são lançadas para monitorar essa área -- talvez isso não seja uma boa idéia, preciso de opniões" -> foi pratica a solucao ..... =P =P =P ...

5 - "O consumo de cpu também é escrotamente alto, ficando em torno de 20% a 30%" -> a paralela usa PIO .... eh ineficiente por natureza !!!!!

Marcos Campos disse...

1. ahusdahusduh certamente xD

2. nenhum deles.. o controlador (MCS7705) só suporta os modos SPP, nibble e CB-FIFO (não sei o que é isso).. Entao a ideia é usar SPP pra falar com a impressora

3. pois eh.. nao sei porque isso acontece. Eu criei funçoes de leitura e escrita de bytes que enviam diretamente uma mensagem de controle, e elas podem ser usadas dentro de loops sem o menor problema =p

4. pois eh.. mas talvez nem precisasse. Talvez isso evite que eu tenha problema com timing depois.

5. De fato.. mas essa porta é usb! Se não me engano, a usb tambem não usa dma, entao tambem nao adianta nada. Com certeza vai ser melhor quando eu reescrever em C, mas mesmo assim.. se eu conseguir imprimir com ele, estarei satisfeito XD

Victor Henriquez disse...

Amigo boa noite, desculpa o meu Português é muito ruim, já que eu escrevi em espanhol e Traduci no google, eu estou fazendo um projeto semelhante em visual basic, o que eu preciso é saber como você conectou tudo, se você tem um diagrama de conexão dos LEDs e outros cabos que você vê na imagem seria o ideal, que é um projeto para a faculdade, eu apreciaria se você poderia me enviar o.

Marcos Campos disse...

Oi Victor,
A conexão é bem simples.. esse adaptador ai tem uma porta centronics de 36 pinos onde ligam os leds.

Voce pode ligar diretamente através de um resistor (usei 1k ali) mas melhor ainda é usar um buffer tipo 74ls541 pra proteger a porta..

Da uma olhada aqui (http://www.rogercom.com/pparalela/introducao.htm) tem detalhes sobre o uso da porta.. circuitos, codigos, etc o/

Victor Henriquez disse...

Muito obrigado para sua informação e sua velocidade em responder, e desculpe você tem outras perguntas, é que realmente me interessou muito desde o seu projeto como eu entendo, você está usando um adaptador USB para Centronics (porta paralela) e é exatamente o que eu preciso, mas eu estou trabalhando em meu laptop com o Windows 7 e Visual Basic, e como eu vejo, e que o verdadeiro problema que eu tenho é que eu não enviar os dados, então eu pensei que a solução seria fazer o circuito, incluindo o strobe (pino 1) e outros para ver se eu gosto, que envia os dados. equibocado me dizer se eu sou. obrigado antecipadamente pela ajuda ....

Marcos Campos disse...

we can speak in english if you prefer o/

Já que você está no windows isso pode ser mais fácil ou mais dificil. Existem drivers oficiais pra todas as versões do windows desde o XP. Por outro lado ele é meio complicado quanto às permissões de acesso ao hardware.

Vale lembrar que não da pra acessar a porta do adaptador usb como se fosse uma porta normal (escrevendo no endereço 378h por exemplo) então vc precisa fazer isso de outro jeito..

Marcos Campos disse...

Coloquei os códigos ali, talvez ajude em alguma coisa. É um módulo em C que cria blocos de memória compartilhada e alguns arquivos python (driver e programas de exemplo).

Postar um comentário