Brické el M-VAVE FM-1; lo revivió un Pico con cuatro resistencias
El M-VAVE FM-1 es un sintetizador FM de bolsilla — 27 teclas de silicona, una
pantallita de 240×240, motor de seis operadores que resulta ser el corazón de
Dexed corriendo sobre un SoC de JieLi. Llevo días preparando firmware propio
para él: el contenedor .fwsc ya está descifrado del lado del servidor de
actualización, tengo un pipeline que lo desempaca, parchea y reempaca
byte por byte idéntico, y el fin de semana pasado flasheó sin dramas una
imagen de prueba que era el stock con otro número de versión.
Así que el paso natural era el primer parche de verdad: transponer todo el
teclado un semitono arriba, inyectando código propio. Dos ganchos de ocho
bytes al inicio de note_on_route y note_off_route — la misma forma exacta
que usa el firmware alternativo de Baud Girl, que es la prueba viviente de
que esta plataforma se puede reescribir — y dos stubs de 24 bytes pegados al
final del app.bin: replicar el prólogo desplazado, r0 += 1 (r0 es el
número de nota; lo verifiqué en el desensamblado de la función inyectora:
construye el mensaje MIDI con 0x90, r0, r1), llamar al objetivo original
con un call r3 absoluto, y saltar de regreso al flujo del stock.
Flasheó bien. El sintetizador aceptó la imagen, la verificó, la escribió, reinició dos veces como siempre… y quedó en boot loop: la pantalla parpadeando con una línea aleatoria, USB muerto, sin pads de debug y sin botón de rescate. Brick propio, de los de manual.
La autopsia (o: por qué no se calcula un branch a mano)
El desensamblador de pi32v2 — el núcleo propietario del JieLi — fue lo siguiente que construí, entrenado contra 400 mil instrucciones de desensamblado de referencia del fabricante que la comunidad ya había publicado. Valida al 100% en longitud y mnemónico. Contra ese desensamblado, el veredicto fue demoledor en su precisión:
19e86: bf ea 7f 78 call -69378 ; call 0x02008f88 ← objetivo REAL
Mi stub llamaba a 0x02028F8A. Error de exactamente 0x20000. La
instrucción call de 4 bytes tiene una forma “cercana” con desplazamiento
de 16 bits con signo — la regla que ajusté contra miles de ejemplos — pero
también una forma extendida cuyo signo vive en bits del opcode, y este call
en particular cae en ese 4%. Lo vi en las estadísticas de ajuste y lo
descarté con un “los que no caen deben ser otra codificación”. Eran. Y para
rematar: la dirección de retorno de ese stub apuntaba dos bytes dentro de
la instrucción siguiente. Dos constantes mal; el primer note-off del arranque
(ese all-notes-off que toda UI hace al inicializarse) saltó a relleno de
datos y se acabó el USB para siempre.
Regla nueva del proyecto, no negociable: ningún parche de control de flujo sale sin que el desensamblador verifique cada objetivo. Las constantes se leen del desensamblado, no se calculan con reglas caseras.
El plot twist de hardware
Todo el plan de rescate giraba alrededor de reprogramar el chip de flash SPI externo con un clip SOIC-8 y un programador. La investigación decía que era un P25Q16SH de 2 MB, “verificado en fotos del teardown”. Le pregunté a mi propia placa y la placa dijo otra cosa: el único SOP-8 visible es el cargador de batería (ACC3610S). El flash… no existe como chip.
El datasheet del AC7911B8 lo explica en su sección 4: el sufijo del nombre
es la configuración de memoria, y 8 significa 8 Mbit de flash integrados
en el die. Un megabyte total, usado hasta 0xFC000 — lo que además
re-ajusta el presupuesto de todo el proyecto (nada de “un megabyte libre”:
hay 16 KB libres, y los ~300 KB extra hay que robarlos a la partición de
ajustes, que es exactamente lo que hace Baud Girl). El “P25Q16SH verificado
en fotos” nunca existió: una confabulación de un agente de investigación que
le creí sin verificar contra la placa real. Segunda regla del día: los
claims de hardware se verifican contra TU placa.
Sin flash externo no hay clip. Sin USB no hay OTA. Queda exactamente una puerta: la que JieLi deja abierta en la mask ROM de fábrica.
El modo download de la ROM
Cuando el AC791N enciende, antes de tocar un solo byte del flash, la ROM
monitora los cables de datos USB esperando una llave de 16 bits: 0x16EF,
enviada MSB-first a ~50 kHz con una línea de reloj y otra de datos (los dos
documentos de la comunidad discrepan sobre cuál línea es cuál — el dongle
alterna ambas polaridades cada 20 ms y se acabo la discusión). Si el chip
ve la llave, reconoce jalando ambas líneas a bajo durante 1–2 ms, se
calibra su PLL con los pulsos SOF del host, y se presenta como dispositivo
USB de almacenamiento hablando un dialecto SCSI de JieLi por el que se puede
leer y escribir el flash completo. Es el path de producción de fábrica,
sin un solo pad de debug.
ip2k documentó y diseñó el dongle para esto: un Raspberry Pi Pico que bit-bangea la llave por PIO, dos resistencias de 100 Ω en serie y dos pull-ups de 2.2 kΩ conmutables. Su propio README lo marcaba honestamente: especificado, implementado, simulado — nunca corrido contra un FM-1. El nuestro lo corrió. Con tres parches propios en el firmware del dongle, porque el mundo real tiene opiniones:
- Un objetivo apagado miente. Con el sintetizador apagado, los diodos ESD del SoC muerto clavan las líneas a bajo — y “ambas líneas en bajo” es literalmente la señal de acknowledge. El primer intento se auto-convenció de un ACK falso al segundo de conectar el cable. El parche: un ACK solo cuenta si en gaps previos las líneas leían alto — un clamp las lee bajas en todos los gaps, y el dongle sigue golpeando la puerta pacientemente (y te lo dice en consola cada 5 segundos).
- D− no importa. La fase de calibración esperaba el idle clásico de USB (D+ alto, D− bajo) y este hardware mantiene D− arriba. Solo D+ es la señal de attach; D− se ignora.
- Asumir attach y pasar el bus. Este ROM no baja su pull-up al terminar
de calibrar como las familias viejas. Tras dos segundos de pulsos con D+
firme en alto, se hace la entrega al PC y que
dmesgsea el juez.
(Y un cuarto, de infraestructura: pico-sdk sin el submódulo de TinyUSB compila “bien” un firmware sin USB. El LED parpadea, la consola no existe, y uno pierde veinte minutos culpando al cable.)
El apretón de manos
[56.2s] waiting: lines read low continuously -- power the FM-1 on
[59.4s] ACK polarity=A(D+clk,D-data) after 121761 packets, lines released
[59.7s] SOF: D+ high (chip pull-up); D- idles 1 (ignored)
[59.7s] SOF: D+ held high for 2 s of pulsing - assuming attached, handing to PC
Enciendes el FM-1 con el dongle ya golpeando, y tres segundos después la ROM acepta la llave. Tres parpadeos rápidos del LED. Luego el cable bueno a un puerto USB 2.0 trasero, y:
usb 1-8: Product: WL80UBOOT1.00
scsi 8:0:0:0: Direct-Access WL82 UBOOT1.00 1.00
La cadena USB dice WL80 (nombre genérico de familia); la capa SCSI dice
WL82 — que es exactamente la llave que jl-uboot-tool de kagaimiq
espera para cargar su loader. Faltaba una pieza de Arch (modprobe sg,
que ahí el módulo no se autocarga) y el shell del cargador abrió solo, con
dos confirmaciones que valieron el precio de la entrada:
>> Chip key: 0x980F <<
ID: 0x856014 Type: 0x03 (SPI NOR flash on SPI0)
0x980F es la misma llave del cifrado SFC que habíamos extraído del
contenedor .fwsc — el silicio confirmando el modelo criptográfico de todo
el análisis — y 0x856014 es el JEDEC ID de un flash Puya de 2²⁰ bytes:
exactamente un megabyte, la geometría del datasheet, medida.
Entonces lo obvio: dump 0 256 (byte por byte idéntico al boot region del
stock — el brick jamás lo tocó), respaldo completo de un megabyte,
write de los primeros 0x93000 con la imagen stock verificada, lectura de
verificación, cmp limpio, ciclo de poder…
…y el FM-1 botando stock V15 con los presets intactos, porque la partición VM donde viven jamás se tocó.
Lo que me llevo
- El unbricker se construye antes del primer experimento, no después del primer brick. Ahora la plataforma tiene un path de rescate probado, sin soldadura, con un Pico y cuatro resistencias — y la próxima imagen experimental que muera va a ser un incidente de cinco minutos.
- Las constantes de un binario se leen, no se derivan. El 4% de casos que no cuadra con una regla ajustada a mano no es ruido: es el bug esperando su turno.
- La comunidad hizo el 90% del camino antes que yo: el firmware VA y el instalador de Baud Girl, el reverse engineering completo de AL-255, el diseño del dongle de ip2k y el corpus de JieLi de kagaimiq. Mi parte fue conectar las piezas, parchear donde la realidad discrepaba, y verificar cada claim contra la placa que tenía enfrente.
El FM-1 ya está de vuelta en el escritorio, botando el stock y esperando. La siguiente imagen ya existe: el mismo parche de transposición, ahora con cada constante tomada del desensamblado y verificada. Esta vez con red de seguridad debajo.
Si algo de esto te sirve para resucitar el tuyo: 0x16EF, MSB-first,
50 kHz, y no aceptes un ACK falso.