Tachibana

Un DIR que apaga la pantalla: la caza de un cerrojo compartido

#neo6502 #firmware

En la Neo6502, en modo Hercules, el primer comando DIR tras un arranque apagaba la pantalla. Doce correcciones, once refutadas por la medición, cuatro instrumentos mal calibrados — y al final, un cerrojo del SDK compartido entre los dos núcleos, que retrasaba la interrupción de vídeo una sola línea. Una línea bastaba.

Por bmarty · Tachibana · 25 de septiembre de 2026

La Neo6502 combina un 65C02 y un RP2040: el segundo hace todo lo demás, incluida la salida de vídeo DVI, mediante la biblioteca PicoDVI de Luke Wren. Nuestro firmware Trinity añade un modo «Hercules»: 720 × 350 monocromo dentro de una señal 720 × 480 a 60 Hz — un modo nativo, una línea de pantalla por línea de imagen, mientras que el modo 320 × 240 duplica cada línea y dispone por tanto del doble de tiempo.

El fallo era reproducible y absurdo: MODE 1 funcionaba, y luego el primer DIR apagaba la pantalla. El segundo pasaba sin inmutarse. El teclado seguía respondiendo durante la avería. Solo un cambio de modo devolvía la imagen.

1. Lo que la medición refutó, una hipótesis tras otra

La diferencia entre ambos comandos era la caché de FatFs: en frío, el primero lee veintidós sectores del lápiz USB; en caliente, casi nada. La carga de disco era el disparador. Quedaba por saber cómo rompía la visualización.

Primero sospechamos de una falta de memoria, luego de búferes insuficientes, luego del arbitraje del bus, luego de un monitor que pierde el enganche. Cada explicación fue refutada por la medición siguiente, y dos correcciones entregadas sobre esa base agravaron el fallo antes de ser retiradas:

También probamos un timing VGA 720 × 400 a 70 Hz — el del modo texto de las tarjetas MDA, exactamente 720 píxeles de ancho. El monitor por fin anunció un modo, cosa que nunca había hecho a 720 × 480 (un timing pensado para televisores). Pero la imagen salía mal, y la pista quedó en reserva.

2. El instrumento falsea la medición, cuatro veces

El puerto serie de depuración del firmware nunca funcionó: el hilo nunca se encontró en el conector de expansión. Toda la instrumentación pasó por la sonda SWD, que lee la memoria del RP2040 mientras funciona. Escribimos una herramienta que teclea en la máquina escribiendo directamente en la cola de teclado del firmware, y lee los contadores — utilizable con la pantalla en negro.

Ese instrumento nos mintió cuatro veces, y cada mentira costó una hipótesis:

Regla adoptada: para cualquier medición, partir de un corte de alimentación, nunca de un reset por software. Y nunca comparar dos ensayos que no parten del mismo estado.

3. Lo que mostró la caja negra

Todas las lecturas se tomaban después, una vez instalada la avería. Así que colocamos una caja negra: en el instante exacto en que se detecta la anomalía, y antes de cualquier reparación, el firmware copia el estado completo de los seis canales DMA y de la máquina de estados de vídeo.

El resultado fue inequívoco. Para cada una de las tres vías TMDS, PicoDVI usa un canal de datos y un canal de control que lo reprograma. Durante la avería, dos canales de datos llevaban la configuración de otra vía — señal de petición equivocada, FIFO equivocada. Dos canales escribían en la misma FIFO, una tercera ya no recibía nada. Las listas de bloques en memoria, en cambio, estaban intactas: el fallo no estaba en los datos, sino en su carga.

Una foto de la pantalla completó el cuadro mejor que diez lecturas: tras un DIR, el texto aparecía duplicado en dos copias, una azul y otra amarilla, separadas unos cuarenta píxeles. Amarillo = rojo + verde: los otros dos canales seguían alineados entre sí, solo el azul se desplazaba. El azul es la vía de sincronización, la única que lleva cuatro bloques de control donde las demás llevan dos. Un desfase de un rango no la mueve la misma cantidad: las tres vías se desincronizan.

4. Hacer inofensivo el fallo, antes de entenderlo

De ahí salió una corrección que no corrige la causa: dar el mismo número de bloques a todas las vías, troceando el blanking de las dos vías de datos como el de la vía de sincronización. Un desbordamiento las desplaza entonces todas con la misma fase, y la imagen permanece de una pieza.

En la placa, con cuatro DIR encadenados: el desbordamiento seguía produciéndose (quince veces), pero las tres vías se movían juntas — cero desfase — y el texto era nítido. El fallo se había vuelto invisible.

Es útil, no es satisfactorio. Quedaba la verdadera pregunta: ¿por qué los canales de control tomaban una vuelta de ventaja?

5. La causa: un cerrojo que los dos núcleos compartían sin saberlo

La medición de latencia, por fin bien filtrada, dio una cifra modesta y decisiva. Entre dos líneas activas el intervalo nominal es de 32 µs. Durante el DIR, la peor lectura fue de 63 µs — es decir una sola línea de retraso, nunca más. Ningún bloqueo largo, ninguna sección crítica de centenares de microsegundos: una línea.

Y una línea basta. Durante ese retraso, los canales de datos terminan su bloque y encadenan hacia sus canales de control, que entonces avanzan solos por la lista — sin que nadie se lo pida. Toman una vuelta de ventaja, las vías se desplazan, y todo lo demás sigue.

Faltaba saber qué retenía la interrupción. La respuesta estaba en los números de cerrojo. PicoDVI protege sus cuatro colas con spinlocks obtenidos por next_striped_spin_lock_num(). Esa función del SDK reparte los cerrojos 16 a 23 por turno entre todos los que llaman — colas, mutex, pools de alarmas, y lo que usen las bibliotecas. Leídos en la placa: PicoDVI tenía los números 18 y 19, que FatFs y la pila USB podían tomar desde el otro núcleo.

El detalle decisivo está en la documentación del SDK: spin_lock_blocking empieza por desactivar las interrupciones del núcleo que espera. Cuando el núcleo de aplicación tenía el cerrojo durante una ráfaga de disco, el núcleo de vídeo lo esperaba, ciego. Su interrupción de línea llegaba una línea tarde.

El SDK documenta el compromiso sin rodeos: esos cerrojos son compartidos, y sortear los números dentro de un rango «reduce la probabilidad» de que dos usos caigan en el mismo. Probabilidad, no garantía.

6. La corrección cabe en una línea

// antes
dvi_init(&dvi0, next_striped_spin_lock_num(), next_striped_spin_lock_num());

// después
static int slTmds = -1, slColour = -1;
if (slTmds < 0) { slTmds = spin_lock_claim_unused(true); slColour = spin_lock_claim_unused(true); }
dvi_init(&dvi0, slTmds, slColour);

Cerrojos reservados, que nadie más puede tomar (números 24 y 25, fuera del rango compartido). Medido en la placa, DIR en frío de veintidós sectores, dos ensayos incluido uno desde un corte de alimentación:

antesdespués
desbordamientos de canal9 a 150
intervalo entre dos líneas63 µs33 µs (el nominal)
líneas retrasadas durante el DIR+10 a +16+2
pantallanegra, o texto duplicadoperfecta

El fallo ya no se produce. Esto ya no es un rodeo.

7. Por qué nadie más lo ha visto

Miramos cómo los demás proyectos PicoDVI inicializan la biblioteca: el original, el fork ikjordan, el fork xep80, pico-pacPlus. Todos retoman el patrón del ejemplo de origen, next_striped_spin_lock_num() — y les conviene. El compromiso del SDK solo se revela en una situación particular, que reúne tres condiciones:

Los emuladores que cargan programas desde una tarjeta SD reúnen las dos primeras. No sabemos si otros se han topado con este caso: hace falta una sonda y un contador de adelanto de canal DMA para verlo, y no lo hemos encontrado descrito en ninguna parte. Esa es justamente la razón de este artículo — si la firma le suena a alguien, le ahorrará los dos días que nos costó.

8. Lo que deja esta caza

Doce correcciones, once refutadas. Cuatro mediciones falsas que hubo que recalibrar. Y dos cosas que realmente desbloquearon el caso, ninguna de ellas salida de una lectura: una foto de la pantalla, que mostraba que solo una vía se desplazaba, y la lectura de los números de cerrojo, que dio la causa.

Una corrección juzgada por sus propios contadores es una corrección no juzgada. Dos veces la misma noche, todos los indicadores estaban en verde mientras la pantalla no mostraba nada.

La firma de este fallo merece recordarse, porque es genérica del RP2040: un retraso de exactamente una unidad de tiempo del sistema de tiempo real — aquí una línea — insensible a la frecuencia de reloj. No es un problema de caudal: es alguien que tiene un cerrojo.

Fuentes y enlaces

← Neo6502 · Nuestros proyectos →