Archivo por años: 2022

Problemas tecladiles (4)

Como ya comenté, decidí reemplazar el cargador original del Atmega32U4, llamado DFU y que se puede descargar desde la página de Atmega, por Ubaboot, un bootloader reducido que sólo ocupa 512 bytes y que es muy sencillo de utilizar. A mayores envié dos parches para simplificarlo aún más:

Para instalar Ubaboot, lo primero que hay que hacer es editar el fichero config.h y configurar los parámetros de nuestra placa. En el caso de mi teclado, hay que descomentar OSC_MHZ_16 (pues el reloj es de 16MHz), y USB_REGULATOR:

Una vez hecho esto, hay que programar los fuses del microcontrolador para un bootloader de 512bytes, además de asegurarnos de configurar bien el resto. En el caso de mi teclado, la configuración es la siguiente:

Fuse LOW: 0x5E

  • CKDIV8 0
  • CKOUT 1
  • SUT1/0 = 0x01
  • CKSEL3/0 = 0x0E

En este bloque básicamente no tocamos nada.

Fuse HIGH: 0xDC

  • OCDEN 1 (desactivado)
  • JTAGEN 1 (JTAG desactivado)
  • SPIEN 0 (programación por SPI activada)
  • WDTON 1 (watchdog desactivado)
  • EESAVE 1 (preservar EEPROM al borrar la memoria)
  • BOOTSZ1/0 0x10 (bootloader de 512 bytes)
  • BOOTRST 0 (vector de reset apunta al bootloader)

En este bloque básicamente desactivamos el JTAG, pues no lo necesitamos para nada, ponemos la dirección de inicio del cargador a 512 bytes antes del final de la memoria, y por último configuramos el vector de reset para que salte al cargador directamente. Esto último es muy importante, pues el propio cargador detecta si estamos arrancando «en frío» (en cuyo caso saltará a nuestro código directamente) o si hemos pulsado el botón de reset (en cuyo caso entrará en modo programación).

Fuse EXTRA: 0xFE

  • RESERVADO 7/4 tienen que estar todos a 1 (0xF)
  • HWBE 1
  • BODLEVEL2/0 0x6

Es cierto que algunos valores, como el BODLEVEL, me los cambió el entorno arduino cuando metí su cargador, por lo que no creo que sea algo crítico.

El siguiente paso es conectar un programador SPI al teclado a través de los pines correspondientes. Yo uso un BusPirate que me prestaron, para lo cual tuve que editar el fichero Makefile y sustituir usbtiny -B10 por buspirate -P /dev/ttyUSB0 -b 115200, además de instalar AVRdude y el resto de utilidades de AVR. Con esto ya sólo me queda conectarlo al teclado y ya puedo leer el estado con

avrdude -c buspirate -P /dev/ttyUSB0 -b 115200 -p m32u4

borrar todo el estado para desprotegerlo con

avrdude -c buspirate -P /dev/ttyUSB0 -b 115200 -p m32u4 -e

grabar los fusibles con

avrdude -c buspirate -P /dev/ttyUSB0 -b 115200 -p m32u4 -U efuse:w:0xFE:m
avrdude -c buspirate -P /dev/ttyUSB0 -b 115200 -p m32u4 -U hfuse:w:0xDC:m
avrdude -c buspirate -P /dev/ttyUSB0 -b 115200 -p m32u4 -U lfuse:w:0x5E:m

y hacer make y make program para grabar el Ubaboot. Con él grabado, sólo tengo que pulsar el boton de RESET del teclado y se pone automáticamente en modo programación por USB, con lo que ya no necesito más el BusPirate.

Y ya, por último, instalé Arduino y Teensyduino para disponer de las bibliotecas de USB. Sin embargo, hice un pequeño cambio para integrar la programación mejor. Los cambios concretos están descritos en el fichero README del segundo parche que envié a Ubaboot.

Y con esto escribí la primera versión del software de mi teclado, pero eso lo comentaré con calma en otra entrada.

Problemas tecladiles (2)

Acabo de subir la revisión 1.7 de la placa del teclado. Uno de los cambios más importantes ha sido conectar una de las columnas del teclado desde el puerto PE2 al PB7. Para entender el motivo, primero necesitamos entender cómo es el proceso de arranque del Atmega32Ux.

El procesador Atmega32Ux permite añadir un bootloader y arrancarlo en determinadas circunstancias. Hay varias posibilidades:

  • Llamarlo manualmente desde el programa principal (útil para cambiar a modo programación de manera «manual»)
  • Llamarlo siempre que se resetee el procesador
  • Llamarlo tras un reset si se da cierta condición externa

Y esta última es la clave: esa condición externa es que el pin PE2 esté a nivel bajo en el momento de ocurrir el reset. Esto permite que, por defecto, se arranque el código principal, pero si se desea arrancar el bootloader, sólo haya que conectar dicho pin a masa y pulsar reset.

Pues bien, hasta ahora yo utilizaba dicho pin para una de las columnas del teclado simplemente por comodidad de diseño: al hacer el enrutado de las pistas, era más sencillo utilizar ese pin que otro.

El problema es que la placa Teensy 2.0, que es la que lleva el mismo procesador, tiene ese pin permanentemente conectado a masa a través de un pull-down y, además, no está conectado al exterior. Eso significa que en las bibliotecas de teensyduino no está disponible la opción de utilizar dicho pin.

Por simplicidad, yo quería mantener la compatibilidad con la Teensy 2.0 porque me da un entorno de programación y bibliotecas listas para utilizar, lo que simplifica mucho el trabajo, pero este problema me obligaba a rediseñar la placa, algo que no me apetecía mucho porque ya mandé fabricarlas y, aunque la versión actual es mucho más avanzada que la que tengo físicamente, los cambios hechos hasta ahora eran cosméticos y no afectaban a la lógica en sí, mientras que éste sí implicaba un cambio en la lógica de funcionamiento.

Decidí ver qué alternativas tenía, y descubrí que las placas Arduino Leonardo y Arduino Micro también utilizaban el mismo micro, así que decidí echar un vistazo a ver si podía utilizarlas. Al principio tenía buena pinta porque el bootloader estaba disponible, por lo que podía convertir mi teclado en un Arduino «con todas las de la ley». Por desgracia, la alegría no duró mucho: en el caso del Micro, el pin PE2 tampoco estaba conectado al exterior, con lo que estaba en las mismas. Aún peor: otros dos de los pines que utilizo en el teclado están asignados a LEDs, con lo que probablemente no tendría mucha libertad a la hora de utilizarlos. En el caso del Leonardo, tres cuartos de lo mismo.

Al final no me quedó otra que cortar la pista que conectaba PE2, y añadir dos puentes: uno entre PB7 y la pista original, y otra entre masa y una resistencia conectada a PE2.

Probando otros bootloaders

A pesar de todo, decidí probar el bootloader de Arduino, por si era más cómodo que el ubaboot que estaba utilizando hasta ese momento. Arranqué el editor Arduino IDE, configuré una placa Leonardo, le indiqué que tenía un programador Bus Pirate, y le di a Quemar bootloader. Varios minutos (sí, sí… tarda un muy buen rato y no muestra nada en pantalla que indique progreso) finalizó la grabación. Lo primero que descubrí es que me había cambiado los fuses del Atmega, algo que no me gustó mucho porque algunos flags eran… raros. Por ejemplo, me activaba el divisor entre ocho del cuarzo, lo que supone que la CPU va más lenta. Pero lo más raro es que pone a cero los bits 4 y 5 del byte extendido, los cuales se supone que no se deben escribir, y deben estar siempre a 1. Lo peor es que no soy capaz de devolverlos a su estado normal.

Tras varias pruebas, no me acabó de convencer el bootloader, así que volví al ubaboot de nuevo, pero esta vez quería asegurarme de corregir un par de problemas que tenía, y es que al pulsar el botón de reset para entrar en modo programación, no siempre funcionaba, sino que normalmente tenía que darle dos o tres veces antes de que el identificador USB fuese el de OpenMoko en lugar del de Teensy. Por otro lado, al darle la orden de reiniciar desde el programador ubaboot, el USB dejaba de funcionar.

Al final, tras echar un vistazo al código fuente, me di cuenta de que este bootloader espera ser llamado siempre durante el arranque, lo que significa que hay que poner a cero el bit 0 del Fuse High Byte, el flag BOOTRST. De esta manera, siempre arrancará desde el bootloader, y si éste detecta que el procesador está arrancando por haber sido conectado a la corriente, saltará directamente al código principal, y sólo entrará en modo bootloader si se pulsó el botón de reset.

Con estos cambios ya puedo programar fácilmente la placa del teclado: simplemente tengo que instalar Teensyduino para disponer de todas las bibliotecas, indicar al IDE que tengo un Teensy 2.0, y cada vez que compile, fijarme en donde ha almacenado el fichero hex con el código objeto (normalmente en /tmp/arduino_build_XXXXXX, y utilizar el comando

sudo ubaboot.py write path_al_fichero.hex

para programarlo.

Problemas tecladiles

Hace tiempo que quiero construirme un portátil/cyberdeck alrededor de una Raspberry Pi 4, y basado en el diseño del Z88 de Sinclair/Cambridge Computers. Sin embargo, algo que tengo muy claro es que quiero un teclado completo, no uno de portátil, pues, como programador, suelo utilizar mucho las teclas SUPR, INICIO, FIN, RePAG y AvPAG, y en los portátiles algunas necesitan combinarse con Fn.

La primera opción sería utilizar un diseño TenKeyless, también conocido como «teclado 87%», como éste (pero sin lucecitas):

Sin embargo, hay mucho espacio desaprovechado. Así que sopesé utilizar un diseño «75%», como éste de Keychron:

Pero no tenía a mano la tecla de Imprimir pantalla, que uso mucho, ni tampoco la de SysReq, que utilizo de vez en cuando, así que tampoco me convencía.

Al final, después de mucho pensarlo, me decidí por hacer mi propio diseño, y ésta es la distribución final que decidí:

Como se puede ver, tiene todas las teclas de un Tenkeyless, pero es muchísimo más compacto. E incluso tiene una tecla de Fn para funciones no tan comunes, como subir y bajar el volumen. Todo ventajas.

Decidí construir uno en plan «barato», y para ello utilicé pulsadores de circuito impreso y los capuchones de un teclado que compré en el chino el bazar de al lado de casa, y lo conecté directamente a los pines de E/S de la raspberry. Este fue el resultado:

La distribución era buena, pero el tacto… en fin… hace unos treinta años había construido un teclado para mi Spectrum utilizando el mismo tipo de pulsadores, y no recordaba que fuese tan malo 🤣

Pero ya tenía el gusanillo, así que decidí coger el toro por los cuernos y diseñar un teclado en condiciones. Para ello partí de un diseño básico creado mediante el programa Klepcbgen, de Jeroen Bouwens, al que hice varios cambios para permitir, entre otras cosas, utilizar diodos normales en lugar de la versión de montaje superficial. A partir de ahí seguí puliendo el diseño hasta llegar a la versión 1.5 de mi teclado, que está disponible en mi repositorio de gitlab.

En el diseño utilicé un Atmega32U4, el mismo que se utiliza en las placas Teensy 2.0. El motivo es que son las placas que más se utilizan para diseños de teclados propios, por lo que podría reutilizar mucho software. Además, incluí, como se puede ver en la imagen de arriba, dos filas de 10 pads cada una en donde están disponibles las filas y columnas del teclado. Esto permite conectar la misma placa a un microcontrolador externo (o, en mi caso, directamente a los pines de E/S de la Raspberry PI) en lugar de utilizar el incorporado en la placa.

La primera dificultad fue donde colocar el microcontrolador. Al principio junté todas las teclas de la primera fila hacia la izquierda y coloqué el microcontrolador y el conector USB arriba a la derecha, por el lado inferior de la placa. Esto tenía la ventaja de que las pistas del par diferencial del USB eran lo más cortas posible, y además me dejaba sitio para un NeoPixel en la esquina superior derecha, pero tenía el inconveniente de que la estética de la colocación de las teclas no era muy allá, además de que, por el espacio disponible, la placa sobresalía bastante por arriba.

Ante esto, decidí intentar mover el microcontrolador a otra zona, en este caso debajo de la barra espaciadora, donde había mucho espacio libre, y así poder mover el conector USB un poco más hacia las teclas. Sin embargo, me preocupaba que las pistas del par diferencial serían muy largas… ¿daría problemas con el USB?

Decidí informarme, así que me descargué la especificación del estándar USB 1.1 y descubrí que el par diferencial tiene que tener una impedancia de 90 ohmios +/- 15%. Ante esto, utilicé la herramienta de cálculo de Kicad para líneas de transmisión, y me salió que tenía que utilizar una anchura de pista de 0,9 milímetros (sí, bastante ancha):

De todas formas, mi intención era utilizar el modo low-speed, de 1,5Mbps en lugar del full-speed de 12Mbps, por lo que lo más probable es que incluso unas pistas sin ajustar funcionasen correctamente; pero decidí no arriesgarme e intentar hacer las cosas bien. Aquí, además, conté con la inestimable ayuda de mis compañeros de A Industriosa, que me resolvieron todas mis dudas sobre diseño con Kicad. A fin de cuentas, soy programador, no electrónico.

Para hacer bien un par diferencial, lo primero es definir una malla específica para él en las clases de red. De esta manera podremos estar seguros de que los parámetros de las pistas serán correctos:

Para trazar el par, tenemos que utilizar la herramienta específica. Ella nos permite asegurarnos de que ambas pistas están siempre a la distancia correcta:

Una vez trazado el par, veremos que los ángulos son rectos (de 45 o 90 grados), cosa que no es buena. Para solucionar esto sólo tenemos que escoger dos segmentos consecutivos, darle al botón derecho, y escoger pistas de filete. Nos preguntará un valor para el radio, y una vez aceptado, añadirá una esquina curva entre las dos pistas. Aquí tenemos un ejemplo de una esquina ya con el filete, y la esquina paralela aún sin él:

Yo utilicé un radio de 3 milímetros para las curvas internas. Sin embargo, es importante recordar que las curvas deben ser concéntricas, lo que significa que las curvas externas tienen que tener un radio mayor. Dicho radio debe ser radio_interior + ancho_de_pista + separación_de_pista. En mi caso, con un radio interior de 3 milímetros, un ancho de pista de 0,9 milímetros, y una separación de pista de 0,2 milímetros, el resultado es que para los radios exteriores tengo que utilizar un radio de 4,1 milímetros. Y así queda:

Sin embargo, si seleccionamos cada una de las dos pistas del par y nos fijamos en el parámetro Longitud enrutada de cada una, veremos que la longitud no es la misma. Aunque la diferencia es pequeña (casi dos milímetros), quise hacer las cosas bien, así que utilicé la herramienta de Afinar desvío de un par diferencial para asegurarme de que la diferencia de distancias estuviese por debajo de una décima de milímetro:

Esta herramienta añade unas ondulaciones en la pista, de manera que la distancia sea correcta a la vez que se mantiene la impedancia:

Distribuciones de teclado

Durante todo el diseño inicial mantuve una distribución ISO de las teclas, pues es la que yo voy a utilizar. Sin embargo, dado que iba a publicarlo todo bajo una licencia libre, decidí intentar hacer una placa dual, que permitiese montar también una distribución ANSI de teclas. Sin embargo, me encontré con el problema de que había un par de agujeros que quedaban bastante cerca y no veía claro que los estabilizadores que iban en ellos quedasen bien anclados, por lo que ese diseño no parecía una buena idea. Pese a todo, quería poder tener ambas distribuciones, así que al final se me ocurrió una idea relativamente sencilla: en main tendría la placa dual, y luego tendría dos ramas, una para cada distribución, que mantendría actualizadas a partir de main. Al principio era bastante peñazo porque, al quitar los pulsadores que sobraban, quedaban muchas pistas mal situadas, lo que me obligaba a ajustar bastante ambos diseños antes de poder hacer el merge; pero después de afinar la placa dual conseguí solucionar esos problemas, y ahora me lleva segundos actualizar cada rama.

Distribución ISO renderizada desde Kicad
Distribución ANSI renderizada desde Kicad

Otros detalles que añadí fueron un conjunto de pads para un programador de Atmega (ese grupo de dos por tres que se ven junto al microcontrolador), dos pads conectados a PE2 y masa para que, al cortocircuitarlos, se pueda entrar en el modo de carga del bootloader de Atmega (que son esos dos que se ven entre el pulsador del espacio y el microcontrolador, y que está etiquetado por debajo como bootloader), y dos pads más para dos pines de E/S que me quedaban libres (PE6 y PB0), pues puede ser interesante aprovecharlos. Teniendo en cuenta que PB1, PB2 y PB3 ya están disponibles en el conector del programador, el resultado es que casi todos los pines de E/S del microcontrolador están accesibles a través de pads. La única excepción es PB7, que se quedó tan encerrado que no encontré una manera razonable de extraer una pista. Es cierto que podría hacer sitio eliminando C7, el condensador de reset, pero la verdad es que no me parece tan necesario poder acceder a ese pin. Ese condensador lo añadí únicamente por si acaso, pero realmente no es buena idea ponerlo pues, al generar un reset automático al enchufar la placa, impide aprovechar la característica de entrar en modo depuración tras un reset del bootloader.

También incluí un rectángulo en blanco en cada lado de la placa, lo que permite hacer anotaciones con un rotulador.

Protección contra sobretensiones

Al principio no tuve en cuenta la necesidad de proteger contra sobretensiones en el bus USB. A fin de cuentas, aunque en la documentación del Atmega32U4 dicen que es recomendable añadirla, no aparecía en ninguno de los diseños de referencia que incluían. Es más: la propia placa Teensy 2.0 no incluye ningún tipo de protección, sino que los dos pines del microcontrolador van a pelo al conector.

Sin embargo, un compañero de AIndustriosa me recomendó encarecidamente que la añadiese, pues parece ser que es habitual que en los conectores USB de los equipos de sobremesa (cuya alimentación va directa a la fuente) haya picos de tensión en determinadas circunstancias. Le eché un vistazo a las opciones que me daba Kicad, y me encontré con que casi todas eran diminutas: algunos chips medían aproximadamente un milímetro de largo… algo imposible de soldar a mano. Afortunadamente había otras opciones, y al final me quedé con el USB6B1, pues además de estar pensado específicamente para USB, tiene un patillaje que simplifica el diseño.

Reloj, o no reloj… he ahí la cuestión

Cuando estaba haciendo el diseño original descubrí que el Atmega32U4 incluye un oscilador RC que viene calibrado de fábrica de manera que su precisión es suficiente para que el USB funcione en modo low-speed. Dado que meter un cuarzo de 16MHz me asustaba bastante (es una frecuencia relativamente alta, y yo no soy para nada un experto en diseño electrónico), decidí que sería mejor hacer un sistema sin cuarzo externo, y utilizar sólo el oscilador RC interno. Sin embargo, a la vez, temía que hubiese algún problema, así que al final decidí que no usaría cuarzo, pero dejaría hechas las pistas para poder poner uno de ser necesario.

¡Menos mal que lo hice! Cuando monté la primera placa y quise programar el micro, descubrí que no funcionaba. Al principio pensé que podía ser un problema con el programador que estaba utilizando, un Bus Pirate, pero después de probar y probar una y otra vez, y de leerme la documentación repetidamente, descubrí que la programación mediante el puerto SPI (que es lo que estaba usando) necesita que haya un reloj activo para funcionar. Pero para que el chip utilizase el RC interno necesitaba programar los fuses de configuración, para lo cual… necesitaba un reloj activo… El huevo y la gallina. Fue aquí cuando descubrí que se vendían dos modelos: el Atmega32U4 a secas, que viene configurado de fábrica para trabajar con un cuarzo externo, y el Atmega32U4RC, idéntico en absolutamente todo excepto en que, de fábrica, viene configurado para trabajar con el oscilador RC interno. Adivinad cual había encargado yo…

Decidí no desesperarme y buscar una solución, así que intenté utilizar la interfaz JTAG para programarlo, confiando en que esa no necesitase un reloj externo. A fin de cuentas, el Bus Pirate es un dispositivo programable, y soporta todo tipo de interfaces…

O no, porque como como estaban justos de espacio de código en el microcontrolador, en las últimas revisiones eliminaron el soporte de JTAG. A fin de cuentas, «era demasiado lento para ser útil» 😡

Afortunadamente había un fork del firmware del Bus Pirate mantenido por la comunidad que, supuestamente, incluía soporte de JTAG, así que lo descargué, lo compilé con el MPlabX, lo flasheé… y no había JTAG.

Revisé el código y ahí estaba… y además se estaba compilando en el binario que estaba grabando, pero no aparecía en la lista de protocolos soportados. No entendía qué estaba pasando.

Encima, leyendo bien la documentación descubrí que el oscilador RC está calibrado para una tensión de trabajo de 3 voltios, cuando yo estaba alimentando el Atmega con 5. Eso me obligaba a recalibrarlo antes de poder usarlo.

Al final me cansé, me fui a la tienda de electrónica de mi ciudad y me compré un cuarzo de 16MHz por 60 míseros céntimos de euro, lo soldé, y se acabaron mis problemas: el teclado aparece como un dispositivo OpenMoko (porque, además, antes del cuarzo de 16MHz le puse, por error, uno de 12MHz pensando que era esa la frecuencia correcta, en base a los 12Mbps del bus USB; pero no…), y como no aparecía el dispositivo, probé a grabar un bootloader diferente del que trae de fábrica: UdaBoot.

Moraleja: no compensa ahorrarse unos céntimos.

¿Y ahora?

Ahora me queda la parte de programar el micro para que se comporte como un teclado, además de ver cómo puedo publicar a través de HID el NeoPixel, de manera que sea fácilmente controlable mediante software. Una vez hecho esto, montaré dos teclados más, y diseñaré la carcasa para el portátil, y una carcasa independiente para poder utilizar este teclado en mis ordenadores de casa.

Cargando mis cascos (y 3)

Hace unos años escribí una entrada explicando cómo modifiqué mis cascos inalámbricos para que se cargasen sólo con colgarlos de un soporte. Hace algo menos de tiempo publiqué un nuevo diseño, con un mejor acabado pero aún algo chapucero. Hoy ha llegado el momento de mostrar el diseño de-fi-ni-ti-vo.

Este es el diseño en FreeCAD, el cual está disponible en mi gitlab:

Si lo comparamos con el diseño anterior, éste está mucho más pulido porque incluye las lengüetas de carga y agujeros para pasar los cables. También tiene una ranura por debajo, de manera que los cables quedan completamente ocultos.

El diseño, además, está completamente acotado, por lo que es muy sencillo ajustarlo para otros tamaños de cascos. Así, la diadema de mis cascos mide 35 milímetros de ancho, por lo que la separación entre las lengüetas está ajustada a 33 milímetros, de manera que se garantiza que siempre hará contacto al colgarlos. Pero si queremos utilizarlo con otros cascos, sólo tenemos que editar esa medida para ajustarlo al valor necesario.

Así quedó tras imprimirlo en un tono anaranjado que hace juego con la madera del mueble donde va a ir atornillado, y tras pegarle cinta adhesiva de cobre en las lengüetas para hacerlas conductoras:

El siguiente paso fue meter los cables del transformador por cada uno de los agujeros, y soldarlos al cobre. La mejor manera de hacerlo es estañar primero el punto en el que vamos a soldar el cable, cortar el cable a la longitud correcta, estañar la punta, meterlo por el agujero, y simplemente con un pequeño toque del soldador, unir ambas partes. Así evitaremos recalentar demasiado el plástico y que se derrita o deforme.

Por último, añadimos un poco de cola térmica en la guía de la base para que el cable quede firme y evitar que un tirón lo arranque de las lengüetas:

¡Y ya está! Éste es el resultado final:

Haciendo que mi tableta de dibujo funcione en Linux

Hace tiempo me compré una tableta de dibujo de estas para «pintar con lápiz», pero por desgracia nunca la pude usar porque no funcionaba correctamente en Linux: al mover el lápiz, el cursor del ratón se movía correctamente, pero no hacía nada cuando tocaba sobre la superficie ni cuando pulsaba los botones del lápiz. Sin embargo, como estos días tenía algo de tiempo libre, decidí ponerme con ella a ver qué podía descubrir, y si conseguía resolver el problema.

La tableta de dibujo

Lo primero que hice fue echar un vistazo a la información de lsusb. Ahí puede ver el código del fabricante y el del dispositivo:

Bus 002 Device 006: ID 2179:0004 UGTABLET TABLET WP5540

Una búsqueda rápida en una de las muchas bases de datos de USB me mostró que el chip controlador pertenece a la empresa UGtizer Corp, pero en su web no encontré ningún driver adecuado. Una búsqueda en la página de USB Linux tampoco me dio información útil.

Decidí entonces que podría probarla en Windows, a ver si era un problema de la tableta en sí. Para ello instalé uno en una máquina virtual y configuré un filtro USB para que redirigiese la tableta a éste tan pronto se enchufase, y evitar así interferencias desde Linux.

Y funcionó perfectamente: si movía el lápiz a unos milímetros de la superficie, el cursor se movía igual que en Linux, pero si lo apoyaba, Windows identificaba una pulsación de botón de ratón y podía seleccionar iconos, cosa que en Linux no ocurría. Los dos botones del lápiz también funcionaban como el botón derecho y central del ratón.

Por supuesto, dado que la tablet medio funcionaba en Linux, yo supuse que debía seguir el estándar USB HID, pero que habría algo que se salía de él y Linux no era capaz de identificar correctamente. O bien podría ser que hubiese que configurar primero de alguna manera la tableta, y que Windows lo estuviese haciendo bien y Linux no.

Esnifando paquetes USB

Decidí comprobar primero la segunda posibilidad. Para ello desenchufé primero la tableta y cargué el módulo usbmon con:

sudo modprobe usbmon

Luego lancé Wireshark, empecé a capturar desde el puerto 2 de USB (pues, si vemos la salida de lsusb, es en él en donde está el puerto que usé), conecté la tableta, e hice algunos movimientos con el lápiz sin apoyar y apoyado en la superficie.

Hecho esto, detuve la captura, desconecté la tableta, apagué la máquina virtual, e hice una nueva captura, para ver qué hacía Linux. Luego abrí ambas capturas, cada una en una ventana diferente, y filtré los paquetes de la tableta con:

usb.bus_id == X&& usb.device_address == YY

Siendo X el bus en el que estaba conectada la tableta, e YY el identificador que se le asignó en cada momento.

El resultado no fue nada conclusivo, pues en ambas capturas salía más o menos lo mismo: primero se piden los datos básicos del dispositivo, luego la configuración completa (donde pone que es un dispositivo HID), algunas cosas más sin importancia, y finalmente lee el HID Report, donde vienen los datos HID para configurar el dispositivo. Tanto en Windows como en Linux eran exactamente iguales, por lo que no parecían ir por ahí los tiros. Una vez hecho todo esto, el resto de datos se enviaban sólo desde la tableta hacia el ordenador en forma de interrupciones USB, y parecían bastante similares entre ambos sistemas operativos.

Llegados a este punto no podía hacer mucho más, porque no sabía nada sobre el estándar HID ni sobre el significado de cada byte que se estaba enviando sobre el cable, así que decidí hacer un alto y leerme la documentación oficial de HID (una lectura amena y tremendamente divertida, como cualquier documentación oficial 😝 ).

El estándar HID

La idea detrás de HID es crear un protocolo que permita definir absolutamente cualquier dispositivo de interfaz humana, tanto actual como futuro, y hacerlo además de una manera que permita que cada fabricante lo implemente casi de la manera que quiera. Para ello, un dispositivo HID simplemente tiene que definir un HID Report, que no es más que una tabla donde se definen qué tipos de entrada tiene, y poco más. Dicha tabla puede estar definida directamente en ROM, lo que simplifica mucho el diseño.

¿Y qué contiene dicha tabla? Pues básicamente una serie de entradas en las que se define, para cada posible sensor o actuador del dispositivo:

  • Tipo (por ejemplo: Eje X relativo, Tecla Y, presión…)
  • Número de bits que ocupa el dato (el bit inicial se deduce de la propia lista)
  • Rango físico y lógico

Y algunas cosas más. Además, las entradas se pueden agrupar por tipo de dispositivo, de manera que no es lo mismo un botón de ratón que un botón de teclado, y asignarle un identificador a cada grupo.

En el caso de mi tableta, la tabla HID es así:

  • Uso digitizer (id: 0x07)
    • Punta de lápiz: 1 bit
    • Botón de lápiz: 1 bit
    • Goma de borrar de lápiz: 1 bit
    • No usados: 2 bits
    • Lápiz dentro de rango: 1 bit
    • No usados: 2 bits
    • Eje X: 16 bits
      • rango físico: 0, 5500
      • rango lógico: 0, 22000
    • Eje Y: 16 bits
      • rango físico: 0, 4000
      • rango lógico: 0, 16000
  • Uso mouse (id: 0x08)
    • Botón 1: 1 bit
    • Botón 2: 1 bit
    • Botón 3: 1 bit
    • No usados: 5 bits
    • Eje X relativo: 8 bits
      • rango lógico: -127, 127
    • Eje Y relativo: 8 bits
      • rango lógico: -127, 127
    • Rueda de ratón: 8 bits
      • rango lógico: -127, 127
    • No usados: 8 bits
  • Uso mouse (id: 0x09)
    • Botón 1: 1 bit
    • Botón 2: 1 bit
    • Botón 3: 1 bit
    • No usados: 5 bits
    • Eje X absoluto: 16 bits
      • rango lógico: 0, 32767
    • Eje Y absoluto: 16 bits
      • rango lógico: 0, 32767
    • Presión del lápiz: 16 bits
      • rango lógico: 0, 1023

Con esta tabla a mano, si nos llega un bloque de interrupción con los siguientes valores:

09 00 5d 49 05 56 00 00

Podemos deducir fácilmente su significado: el primer byte es el uso, por lo que, al ser 9, tenemos que fijarnos en el último de los tres bloque de la tabla. El primer byte contiene el estado de tres botones de tipo ratón, pero como los bits correspondientes son 0, significa que no están pulsados. Los dos siguientes bytes son la posición del lápiz en el eje X, los dos siguientes la posición en el eje Y, y los dos últimos la presión. Fácil.

La cuestión es que tanto en Windows como en Linux los datos que se enviaban eran exactamente iguales: si tocaba con el lápiz en la superficie se activaba el bit 0 del primer byte y aparecía un valor en los dos últimos bytes, que aumentaba con la presión que ejercía. Si pulsaba alguno de los dos botones en el lápiz se activaban los bits 1 o 2 del primer byte… todo era correcto, salvo por el detalle de que Linux no hacía caso. Incluso escribí un pequeño programa en python para leer e interpretar los datos en tiempo real desde la interfaz usbmon, y todo era correcto: los datos se enviaban correctamente, y siempre era con bloques de tipo 9.

De hecho, no podía evitar preguntarme por qué había dos entradas de tipo ratón, y sobre todo por qué una de ellas tenía ejes relativos. La respuesta se me ocurrió de casualidad: este chip sirve para tabletas híbridas, donde se puede usar un lápiz o un ratón:

Ejemplo de tableta con lápiz y ratón

Eso explicaba los dos tipos: si se usaba el ratón, se emitirían interrupciones con el tipo 0x08, y si se usa el lápiz entonces serían de tipo 0x09. Y el tipo 0x07 supongo que sería para algún tipo de lápiz diferente.

Vamos al kernel

Llegados a este punto comprendí que el problema tenía que estar, necesariamente, en el driver HID de Linux, así que descargué las fuentes del kernel, las compilé y las instalé en mi Debian con:

make localmodconfig # copia la configuración del kernel actual
make -j5
make deb-pkg -j5 # crea paquetes .deb

Y con ello, me fui al fichero hid-input.c. En él encontré el punto en el que procesa la tabla HID y descubrí un detalle interesante hacia el final de la función:

¡Si un evento estaba duplicado, parecía ignorarlo a menos que se activase HID_QUIRK_INCREMENT_USAGE_ON_DUPLICATE! Esto tenía buena pinta, porque, efectivamente, los eventos de pulsación de ratón están duplicados en el bloque 8 y en el 9. Probé a añadir el Quirk para mi tableta en el fichero hid-quirks.c, compilé, desmonté los módulos de HID y volví a montarlos, y…

¡CASI! Me había asignado al lápiz las siguientes tres acciones del ratón: siguiente página, anterior página y no-se-cual-más. Claramente iba por buen camino.

Se me ocurrió entonces modificar el código para invertir el orden en el que se procesaban las entradas de la tabla HID, de manera que primero se añadiesen las del bloque 9 y luego las del 8, y el resultado fue que… ¡¡¡FUNCIONÓ!!! Parecía que lo había resuelto, así que creé un parche para poder configurarlo como un Quirk más, añadí mi tableta a la lista de dispositivos con Quirks, y envié un parche a la gente del kernel. En respuesta, amablemente me indicaron que mi solución se veía demasiado alambicada, y que quizás debería probar con HID_QUIRK_MULTI_INPUT, que crearía un dispositivo diferente para cada tipo de bloque. Lo probé, y efectivamente, esa era la solución, así que creé un nuevo parche, mucho más sencillo, lo envié, y fue aceptado. ¡¡Hurra!!

Añadiendo un QUIRK a un dispositivo en el driver HID

Vamos a ver este detalle con más calma porque es importante, y puede serle útil a más gente. Lo primero que tenemos que hacer es bajarnos las fuentes del kernel actual desde el repositorio GIT oficial:

git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git

Una vez que lo tenemos, nos hacemos un branch para nuestro parche y abrimos el fichero drivers/hid/hid-ids.h. En él buscamos si el fabricante de nuestro dispositivo ya está añadido o no (lo más probable es que sí), mediante el identificador que obtuvimos con lsusb. En mi caso, el identificador 0x2179 ya estaba añadido a nombre de UGTizer con:

#define USB_VENDOR_ID_UGTIZER 0x2179

pero dentro de él no estaba mi tableta, por lo que añadí una línea con su definición:

#define USB_DEVICE_ID_UGTIZER_TABLET_WP5540 0x0004

Una vez hecho esto sólo tuve que abrir el fichero drivers/hid/hid-quirks.c y añadir una entrada con mi vendor, mi device y los quirks que quería añadir para él:

{ HID_USB_DEVICE(USB_VENDOR_ID_UGTIZER, USB_DEVICE_ID_UGTIZER_TABLET_WP5540), HID_QUIRK_MULTI_INPUT },

Y con esto estuvo listo.

¿Y mientras espero a que mi distro añada el parche?

Obviamente no quería estar con el kernel de desarrollo en mi sistema, sino que quería tener el oficial. ¿Significa eso que no podré usar mi tableta hasta que Debian haga un backport del parche? Pues afortunadamente no, porque el módulo HID permite pasarle manualmente un grupo de quirks a añadir a un grupo de dispositivos. Para ello basta con editar el fichero /etc/default/grub y, en la línea donde se definen los parámetros de arranque del kernel (que, en general, será la de GRUB_CMDLINE_LINUX_DEFAULT) añadir lo siguiente:

usbhid.quirks=0x2179:0x0004:0x0040

El primer número hexadecimal es el identificador USB del fabricante, el segundo es el identificador de dispositivo, y el tercero es un mapa de bits con los quirks a activar. La lista está definida en el fichero drivers/hid/hid.h, y en el momento de escribir este artículo es:

#define HID_QUIRK_INVERT BIT(0)
#define HID_QUIRK_NOTOUCH BIT(1)
#define HID_QUIRK_IGNORE BIT(2)
#define HID_QUIRK_NOGET BIT(3)
#define HID_QUIRK_HIDDEV_FORCE BIT(4)
#define HID_QUIRK_BADPAD BIT(5)
#define HID_QUIRK_MULTI_INPUT BIT(6)
#define HID_QUIRK_HIDINPUT_FORCE BIT(7)
/* BIT(8) reserved for backward compatibility, was HID_QUIRK_NO_EMPTY_INPUT */
/* BIT(9) reserved for backward compatibility, was NO_INIT_INPUT_REPORTS */
#define HID_QUIRK_ALWAYS_POLL BIT(10)
#define HID_QUIRK_INPUT_PER_APP BIT(11)
#define HID_QUIRK_X_INVERT BIT(12)
#define HID_QUIRK_Y_INVERT BIT(13)
#define HID_QUIRK_SKIP_OUTPUT_REPORTS BIT(16)
#define HID_QUIRK_SKIP_OUTPUT_REPORT_ID BIT(17)
#define HID_QUIRK_NO_OUTPUT_REPORTS_ON_INTR_EP BIT(18)
#define HID_QUIRK_HAVE_SPECIAL_DRIVER BIT(19)
#define HID_QUIRK_INCREMENT_USAGE_ON_DUPLICATE BIT(20)
#define HID_QUIRK_FULLSPEED_INTERVAL BIT(28)
#define HID_QUIRK_NO_INIT_REPORTS BIT(29)
#define HID_QUIRK_NO_IGNORE BIT(30)
#define HID_QUIRK_NO_INPUT_SYNC BIT(31)

Algunos son obvios, pero otros, la verdad, es que no se para qué pueden servir, así que si algún día me encuentro con un dispositivo HID que no va, iré probando de uno en uno a ver.