
Ingeniería inversa del firmware de los generadores de funciones Philips PM5139 / PM5138A / PM5136: emuladores 8051 utilizados como instrumentos de medición, 35 secciones de hardware documentado y un firmware V2.0 corregido
Un generador de funciones de 20 MHz de alrededor de 1994, desmontado en software: dos volcados de EPROM, un emulador de 8051 usado como instrumento de medición, y 35 secciones de documentación donde cada afirmación está respaldada por una dirección de listado, una medición del emulador o el esquemático.
Al final de todo hay un firmware V2.0 que corrige un defecto que Philips envió, seis formas de onda arbitrarias propias, y un simulador de navegador que ejecuta la ROM original instrucción por instrucción.

Todas las tablas de formas de onda en la EPROM del programa, graficadas directamente desde el binario. Abajo a la derecha está la que inició la parte más interesante de este proyecto.
El Philips PM5139 es el modelo tope de 20 MHz de una familia de tres instrumentos (PM5136 / PM5138A / PM5139). En su interior hay una PCB80C652 — un núcleo 8051 con I²C por hardware — una EPROM de programa 27512, y seis ensamblajes analógicos colgando de un bus serie.
No existe manual de servicio del PM5139. La gente ha estado buscando uno en foros desde 2010. Lo que existe es el manual del PM5138A, su modelo hermano de 10 MHz, que es internamente casi idéntico.
Así que este proyecto comenzó desde el otro extremo: volcar la EPROM, y deducir qué hace el código hasta que el instrumento se entienda lo suficientemente bien como para modificarlo.
Había dos versiones de firmware disponibles, V1.3 y V1.5, ambas volcados M27512 de 64 KiB.
El lado analógico es un bus C serie: el UART del 8051 funciona en modo
registro de desplazamiento, TXD es el reloj, RXD los datos, y un strobe decide cuál
de los diez registros de desplazamiento captura los bytes. MOV DPH,#8nh seguido de
MOVX @DPTR,A dispara el strobe n. Esa única línea es la clave de toda la
sección analógica.
Esta es la parte que vale la pena robar para tu propio proyecto.
Leer un binario de 8051 de 44 KB a ojo te lleva quizás un tercio del camino. Todo lo que vino después surgió de ejecutar el código original y observar qué sale:
---```python
c = CPU(rom) for w in test_values: set_amplitude(c, w) c.call(0x0AAC) # the original routine, untouched print(w, c.ram[0x1C]) # the byte that goes out on STR9
Varía la entrada, lee la salida, compruébala contra la hipótesis. Eso
funcionó para frecuencia, amplitud, offset, profundidad de AM, desviación de FM, número de ráfagas, simetría y ambas características de barrido. Cada fórmula en la documentación viene con los puntos de muestra sobre los que se verificó.
Tres refinamientos lo hicieron realmente productivo:
**Vigila el bus, no la pantalla.** La sección 15 mide qué le hace un bit de estado al búfer de pantalla, y 74 de 128 bits parecen no hacer nada. Pero muchos de ellos no controlan la pantalla, controlan los *conjuntos analógicos* — y esos solo son visibles como telegramas en el bus C. Registrar `MOV SBUF,…` y el `MOVX @DPTR` de terminación elevó el recuento de bits documentados de 54 a 75.
**Pulsa teclas, no manipules la RAM.** Establecer un byte de RAM a mano produce estados que el instrumento nunca adopta. Eso nos costó dos hallazgos erróneos y un fallo en la tabla de comandos. Inyectar códigos de tecla reales a través del SAA3007 emulado da estados a los que el firmware realmente llega — y fue un barrido por fuerza bruta sobre los 256 códigos de tecla lo que reveló qué tecla activa qué manejador.
**Sospecha primero de tu propio emulador.** Tres errores en nuestro núcleo produjeron un comportamiento "inexplicable" del firmware: `ACALL` ejecutado como `AJMP`, un flag de acarreo auxiliar ausente (por lo que `DA A` se comportaba mal y el firmware parecía contar en binario), y una interrupción de teclado duplicada. Cada hallazgo de ese periodo se volvió a medir después.
---
## El camino hasta aquí
**Primero lo estático.** Un desensamblador con una tabla de opcodes completa, luego descenso recursivo con heurísticas de tablas de salto. Eso produjo 30 508 bytes de código y dejó 13 637 bytes sin contabilizar.
**Luego lo dinámico.** Una ejecución de traza — arranque en frío, las 23 teclas del panel frontal, ambas direcciones del mando giratorio, todos los modos de operación, 86 millones de ciclos — marcando cada dirección que realmente se ejecutó. Contrastada con el análisis estático, encontró exactamente **una** zona que el descenso había pasado por alto, y 10 686 de los bytes sin explicar resultaron ser cinco bloques de tablas conocidos.
**Luego los esquemas.** El OCR del manual de servicio es inútil para los esquemas, pero las imágenes de página a 400 dpi son excelentes. Cortadas en teselas solapadas, son legibles hasta los números de pin. Se leyeron seis hojas de esta forma — y donde cinco pistas paralelas discurren a 90 píxeles de distancia, la inspección visual se sustituyó por un script (`lines.py`) que extrae los segmentos de línea del mapa de bits.
**Luego los dos chips que se extrajeron.** Se leyeron un 27C64 etiquetado "SINUS 1.1" y un X28C64. Ambos se colocaron en el esquema y se decodificó su contenido.
**Luego el diff de versiones.** Tokenizar ambos ROMs (distancias de salto relativas en lugar de destinos absolutos) y ejecutar `SequenceMatcher` sobre ellos da un mapeo de direcciones que sobrevive al movimiento de código — así es como los símbolos de la V1.3 se trasladan a la V1.5.
---
## Las partes buenas
### Philips envió una forma de onda con ruido
Las tres curvas arbitrarias integradas residen en `A047h`, `A447h` y `A847h`. La tercera tiene la misma forma que una tabla que ya está en la ROM en forma computada — pero con **563 cambios de dirección frente a 13**, y una desviación estándar de 4,1 LSB.
Se muestreó de una fuente analógica en lugar de calcularse. La media de la desviación es cero, solo dos de 1024 puntos se desvían más de 10 — esta no es una forma de onda diferente, es la *misma* forma de onda con ruido encima.
### Esa tabla es una escalera de niveles de 30 dB
La versión limpia se describió en un borrador anterior como "una sinusoide con diez profundidades de AM", que era una lectura visual del gráfico, no algo que diga el código. Calculados hasta el final, los 1024 puntos se dividen en diez arcos de sinusoide cuyas extensiones son```
255 171 120 80 56 38 26 17 12 8
una serie geométrica con razón 0.681 = 10^(−1/6), es decir, 3.33 dB por paso y 30.1 dB en total. Un modelo de reducción a la mitad se desvía hasta 56, un modelo de 3 dB se desvía en 10. Es una escala logarítmica de niveles — un patrón de prueba de amplitud o atenuación.
El controlador de amplitud tiene dos registros de desplazamiento en un solo strobe, pero el firmware solo envía un byte por telegrama. El esquema lo explica: los dos 4094 están en cascada a través de QS' (pin 10), con el pin 9 sin usar — y los telegramas vienen en pares, con ~42 000 ciclos de separación y millones de ciclos de silencio entre pares. El byte enviado primero se empuja hacia el segundo registro.
El mismo patrón de cascada apareció en cada ensamblaje con más de un
registro de desplazamiento — incluyendo un caso donde la cadena cruza un límite de ensamblaje
a través de una línea llamada E.
Cinco bits en el telegrama STR9 controlan relés directamente: S1 conmuta el
rango del generador de CC, S2…S5 los relés del atenuador. 20 dB (for 40dB),
20 dB, 50/600 ohms — está impreso en el esquema. No hay
umbrales que calcular.
La tabla de saltos en 0301h se lee con JMP @A+DPTR. La entrada 15 aterriza en
0301h + 30 = 031Fh — y allí, en lugar del habitual AJMP, se encuentra el
manejador mismo, en línea, ahorrando un salto. Ninguna instrucción de salto en
la ROM apunta a él, por lo que el análisis estático lo perdió. Es el manejador de
DIAL LOCK, y solo la traza dinámica lo encontró.
La hoja de datos promete 24 memorias de forma de onda. El directorio en la EEPROM dice seis. La aritmética lo resuelve:``` 1024 points × 10 bit, packed 4 values per 5 bytes -> 1280 bytes per curve 6 × 1280 = 7 680 bytes, 0100h…1EFFh (X28C64, 8 KB) <- what was fitted 24 × 1280 = 30 720 bytes, 0100h…78FFh (X28C256, 32 KB) <- what the schematic says
El rango de lectura medido del firmware es `0100h–1EFFh` — seis curvas al byte. El instrumento fue construido con el chip pequeño.
### Código muerto hablando con un dispositivo que no está ahí
186 bytes en `9AFFh` hacen tráfico I²C con la dirección `5Ah` — una dirección que no aparece en ningún otro lugar. En **ambas** versiones del firmware, ningún salto apunta a ella. Se encuentra en el mismo bloque de tipo de dispositivo que la tarjeta de interfaz en `5Eh`, solo con bits de banco diferentes, y envía el búfer de recepción y los registros aritméticos en dos telegramas de diez bytes. Parece un diagnóstico de fábrica para un dispositivo que nunca se comercializó.
### No se puede ejecutar código desde la EEPROM arbitraria
Una idea obvia — poner código en una ranura de forma de onda arbitraria y saltar a él — está muerta al llegar. El 8051 es Harvard: las instrucciones llegan a través de `/PSEN` desde la EPROM de programa, los datos a través de `/RD` desde la EEPROM arbitraria. No está bloqueado; el cable simplemente no está ahí.
### Y la codificación de frecuencia, finalmente
La fila de dígitos de la pantalla vive en `3Eh–43h` de la imagen enviada al PCF8576, todas las posiciones comparten una codificación de segmento, y el byte `43h` cambia de kHz a MHz entre la década 7 y 8. De eso:```
f = M · 10^(D−8) kHz
Tres secuencias de escalonamiento de frecuencia medidas en el instrumento real son reproducidas exactamente por esto — incluida la que se detiene antes de tiempo porque la mantisa 2500 significaría 25 MHz, por encima del límite.

A la izquierda la curva de fábrica, a la derecha la corregida. Abajo a la izquierda está la desviación respecto a la tabla calculada — esa banda de ±5 LSB es lo que dejó atrás una fuente analógica muestreada.
mkv20.py construye V2.0 a partir de V1.5 (o V1.3). Encuentra cada dirección por
firma en lugar de codificarlas de forma fija, por lo que el mismo script funciona en ambas
versiones de origen:
*IDN?: PHILIPS,PM5139,0,V2.0/0000.2.0 en la codificación de segmento medida.Todo lo demás queda intacto. Se encontraron tres rarezas más y se dejaron
deliberadamente sin tocar — una escritura a un SFR inexistente (inofensiva, en
ambas versiones), el bloque muerto 5Ah, y tres bits de estado que se
comprueban pero nunca se establecen. Parchearlos no cambia ningún comportamiento y solo añade
riesgo.
M27512_PM5139_V20.bin es exactamente esto y nada más. La melodía
de abajo es un paso de compilación separado y opcional.
Verificado: el arranque en frío en el emulador produce el mismo búfer de pantalla
y los mismos flags que V1.5, el checksum se valida, y la compilación es
reproducible byte a byte. Se ha flasheado y funciona en un PM5139 real — la
pantalla muestra 2.0 y las seis ranuras arbitrarias funcionan.
Dado que hay 19 509 bytes sin usar detrás del checksum en V1.5, y la ruta de frecuencia toma una frecuencia de nota como tres bytes BCD, el instrumento puede reproducir música a través de su propia salida.
La codificación es agradablemente directa — década 3, luego la frecuencia en
0.01 Hz como BCD, así que 82.41 Hz es 30 82 41. Cuatro bytes por nota: tres para
el tono, uno para la duración.
La parte interesante es el disparador. El menú de diagnóstico (mantener LOCAL
mientras se enciende) tiene una tabla de saltos con ocho entradas, pero el bucle del menú
cuenta 0Bh solo de 1 a 7 — por lo que la octava entrada es inalcanzable.
También es redundante: salta al inicio del menú, al que se llega desde
otros dos lugares de todos modos.
Así que todo el enganche son dos bytes:``` 5B94h table entry 8: LJMP 5B45h -> LJMP 5B62h count limit: 08h -> 09h
No se pierde ninguna autoprueba, no se reubica ninguna tabla y no
aparece ningún elemento de menú muerto. Mantén pulsado LOCAL, enciende,
deja que el menú cuente hasta 8, pulsa una tecla.
La temporización proviene de la hoja de datos del MCS-51. Ambos emuladores ahora cuentan
ciclos de máquina junto con las instrucciones (`mcyc`, desde `mcs51.CYCLES`), y
ejecutar paso a paso el bucle de espera mide **1009 µs** por unidad — 106,95 ms por
semicorchea a 140 BPM, 0,2 % fuera del objetivo. La cifra solía ser un cálculo
manual de 1006 µs al que se le habían escapado dos instrucciones.
`mkdoom.py` también puede convertir un archivo MIDI. Hay que elegir una voz
(nota más aguda, nota más grave, o un canal) y fusionar las secciones de menos de
~25 ms — por debajo de eso una nota grave no alcanza una oscilación completa
y solo se oye un clic.
---
## Y entonces resultó ser polifónico
La melodía anterior es una sola voz. No tiene por qué serlo, y la razón es
una frase del manual de servicio que habíamos pasado por alto:
> Durante la generación de señal, las distintas muestras de amplitud de señal se leen
> desde la RAM. Si la forma de onda básica se altera [...] las muestras de amplitud
> correspondientes se **cargan en la RAM por la CPU**.
El PM5139 es un **DDS de tabla de ondas de 1024 puntos**. El TWS no es un generador
de triángulo en ningún sentido ingenuo — es un acumulador de fase que produce
direcciones de lectura 0…1023 para una RAM rápida en la unidad 4, y esa RAM la llena
la CPU a través del bus C. Seno, cuadrada, diente de sierra y arbitraria son todas el
mismo mecanismo: una tabla.
Y la tabla contiene exactamente **un periodo de la salida**. Así que una tabla construida
a partir de una *suma de armónicos* sigue siendo periódica en sus 1024 puntos, y
suena como un acorde. No un arpegio, no un truco de modulación — varias notas
sonando a la vez a los 20 Vpp completos, con la CPU sin hacer nada en absoluto
mientras suenan. Como los parciales deben ser múltiplos enteros de la
frecuencia de la tabla, los intervalos salen en entonación justa, que para un
acorde sostenido es la mejor afinación de todos modos.
`M27512_PM5139_V20_chords.bin` está en el repositorio listo para grabar — el
riff, en acordes, con la envolvente. Para construirlo tú mismo, o para usar un
archivo MIDI propio en lugar del riff incorporado:```
python3 mkpoly.py --chord crunch M27512_PM5139_V20.bin out.bin
python3 mkpoly.py --chord crunch --midi yours.mid --channel 1 \
M27512_PM5139_V20.bin out.bin
mkchord.py construye las tablas — power (2:3:4), major (4:5:6),
minor (10:12:15), dom7 (4:5:6:7) y cinco más. mkpoly.py coloca una
en la ROM libre junto con la melodía y engancha la misma entrada de menú
muerta. Carga el acorde una vez, luego reproduce la melodía solo
reajustando, lo que transpone todo el acorde en paralelo. Cada nota del
riff de E1M1 se convierte en un power chord — que es de lo que está hecho
ese riff en el original.
Arquitectónicamente esto es un PPG Wave: un contador recorriendo una forma de onda de un solo ciclo, directo a un DAC. El truco del acorde es el que usaban los trackers de Amiga — poner el acorde en la forma de onda para que una voz toque tres notas en lugar de gastar tres canales en ello. Un C64 tiene que hacer arpegio en su lugar, porque el SID no tiene wavetable escribible.
Ni siquiera necesitas una EPROM para los acordes. Las mismas tablas caben
en la EEPROM arbitraria, así que python3 mkarb.py --chords te da seis
acordes seleccionables desde el panel frontal con el firmware intacto.
Hay dos reproductores y una imagen lleva uno u otro, ya que ambos enganchan la misma entrada de menú:
Dos mediciones dieron forma a ese diseño:
00h 44h 88h CCh) y todo
valor reconstruido es múltiplo de cuatro. La RAM de forma de onda es de
doce bits de ancho, pero el bus maneja diez — exactamente lo que almacena
el formato ARB, así que Philips no desperdició nada ahí.RAM_PAGE en 1D62h,
que suena a una, construye su palabra a partir de la frecuencia. Así que
la armonía vive en la tabla y la melodía en la palabra de frecuencia;
nada se recarga mientras la música suena.El emulador no modela la RAM de forma de onda, así que el cargador se
verifica por construcción: polytest.js registra lo que realmente llega
al bus y compara los 1024 puntos contra lo que generó mkchord.py.
Hicieron falta cinco EPROMs para llegar ahí, y el emulador solo pudo llevarnos parte del camino: modela la CPU y el bus pero no la RAM de forma de onda, así que todo lo que puede confirmar es que salen los mismos bytes que envía el firmware. Eso es necesario y no suficiente. Tres cosas tuvieron que resolverse en el propio instrumento:
El orden de bytes. Dos bytes por punto, byte alto primero. Inferirlo a partir de la propia descarga del firmware daba la respuesta opuesta y la tabla salía como ruido. Lo que lo resolvió fue una EPROM con seis patrones de prueba — una línea plana, una rampa, la misma rampa con los bytes de cada punto intercambiados, y tres más — y una mirada al osciloscopio. La rampa intercambiada era la limpia.
Un cambio de forma de onda son diecinueve telegramas, no los tres que enviaba el primer reproductor. El que importa es una escritura de dos bytes que pone la RAM en modo escritura; sin ella 2048 bytes salen por el bus y no llegan a ningún lado.
El nivel de salida. El atenuador son dos etapas de relé de 20 dB separadas en un byte, la tabla ROM para ellas se lee invertida respecto a como había sido documentada (son bits de bypass), y el DAC de nivel es de siete bits, no ocho — se desborda en 80h, así que un ajuste "más fuerte" producía silencio. Ese tomó una matriz de unas treinta combinaciones en una sola imagen, usando la frecuencia de salida como número de prueba para que la propia lectura del osciloscopio diga qué combinación está activa.``` telegrams emitted by the loader: STR6 4 byte(s) 122 machine cycles 1E 00 20 01 STR2 0 byte(s) 132 machine cycles STR1 2050 byte(s) 39490 machine cycles CC 89 88 8A 44 8B 44 8C ... -> all 1024 points identical to the table mkchord.py built
note 1 f0 = 41.20 Hz chord 2:3:4 = 82.4 / 123.6 / 164.8 Hz root E2 note 8 f0 = 36.71 Hz chord 2:3:4 = 73.4 / 110.1 / 146.8 Hz root D2
---
## Seis formas de onda arbitrarias propias

`D310_image_V20.bin` llena cada ranura de la EEPROM — grabar el chip
merece la pena una vez:
| Ranura | Forma de onda | Vpp | Para |
|---|---|---|---|
| 1 | sinc, 8 lóbulos | 12.17 | limitación de banda, sobreimpulso |
| 2 | ringing, Q≈6 | 17.81 | comportamiento de establecimiento |
| 3 | ECG | 12.80 | demo |
| 4 | escalera, 16 escalones bipolar | 20.00 | linealidad, resolución |
| 5 | seno rectificado | 10.00 | como en el original, pero calculado |
| 6 | multitono, 5 tonos | 20.00 | intermodulación |
Dos detalles que importan y son fáciles de equivocar:
**Centrar en cero supera a estirar.** El movimiento obvio es estirar cada
curva a lo largo de todo el rango de valores. No lo hagas: el offset DC
del instrumento proviene de una ruta analógica separada y suma un voltaje
*fijo*, mientras que el contenido DC de una curva asimétrica estirada escala
*con la amplitud*. Tendrías que reajustar el offset cada vez que cambias el
nivel. Poner el cero natural de la forma de onda en el cero del convertidor
cuesta de 0.2 a 1 bit — frente a los 16 LSB de ruido que la ruta analógica
original ya aporta. No es un coste real.
**Escala en punto flotante, redondea una vez.** Redondear primero y estirar
después da 1.0–1.5 pasos de cuantización de error; escalar en float
y redondear una vez da el óptimo de 0.5.
El directorio necesita un byte de identidad por curva (un checksum de los 1280
bytes de la curva, valor inicial `55h`) y el mín/máx como valores de 10 bits
alineados a la izquierda por seis bits. Si el byte de identidad es incorrecto, el instrumento
muestra **Err 8** y rechaza la fuente arbitraria — que es exactamente lo que
ocurrió en el primer flash real.
---
## El simulador de navegador
`PM5139_Simulator.html` es un único archivo autocontenido — sin paso de compilación,
sin dependencias, sin red. Ábrelo y el firmware original V1.3 arranca
frente a ti.
El núcleo 8051 ejecuta el código real. Los temporizadores, las interrupciones, el C-bus y I²C
están emulados; la pantalla se decodifica del flujo de datos real del PCF8576,
y las teclas generan la forma de onda SAA3007 codificada por ancho de pulso en P3.3. La
RAM respaldada por batería está precargada y la EEPROM arbitraria se genera al
arranque y es verificada por el propio firmware.
Un arranque en frío tarda unos 9 millones de instrucciones, así que dale un segundo.
---
## Estructura del repositorio```
Documentation
PM5139_Hardware_Reference.md the main document, 35 sections
PM5139_Firmware_Modification.md how to change the firmware and flash it back
PM5139_Tables.md command and message tables, both versions
PM5139_Changelog_V13_V15.md what changed from V1.3 to V1.5, in prose
PM5139_Bit_Crossreference.md flags 20h–2Fh: set / cleared / tested
HANDOVER.md state of play
BACKLOG.md open questions, each with an entry point
Firmware and data
M27512_PM5139_V13.bin V15.bin the two original dumps
M27512_PM5139_V20.bin our own version
D310_image.bin the arbitrary EEPROM as read out
D310_image_V20.bin six waveforms of our own, ready to burn
PCF8570_image.bin NVRAM in the factory state
PM5139_V13_annotated.asm V15 the annotated listings
Emulation
emu.py system.py system2.py keys.py Python core and peripherals
core.js the same core in JavaScript
shell.html + build.py -> PM5139_Simulator.html
Analysis
mcs51.py analyze2.py seqdiff.py mapv15.py symbols.py annotate.py
Building
romfix.py mkv20.py mkarb.py waveforms.py asm51.py mkdoom.py
midi.py mid2ton.py mkchord.py mkpoly.py
Measurement scripts (see "Using the tools")
bitmap.js flags.js cmd16.js iface.js trace.js arb.js xrange.js
polytest.js cyclecheck.py
limits.js param.js keycodes.js decade.js whoruns.js remote.js
display.js digits.js readout.js nvram.js nv2.js nv3.js …
Python 3 y Node son todo lo que necesitas. matplotlib para los gráficos, pillow
y numpy solo para lines.py.
python3 annotate.py 13 # -> PM5139_V13_annotated.asm python3 mapv15.py --write # map V1.3 symbols onto V1.5 python3 annotate.py 15 # -> PM5139_V15_annotated.asm python3 seqdiff.py # structural diff of both versions python3 romfix.py M27512_PM5139_V13.bin
### Compilación V2.0```bash
python3 mkv20.py # from V1.5 (default)
python3 mkv20.py M27512_PM5139_V13.bin out.bin # or from V1.3
python3 romfix.py M27512_PM5139_V20.bin # verify the checksum
python3 waveforms.py # what the generators produce python3 mkarb.py # -> D310_image_V20.bin python3 plot_arb.py # -> PM5139_ARB_V20.png
### Añadir una melodía```bash
# the built-in bass line, into a separate image
python3 mkdoom.py M27512_PM5139_V20.bin M27512_PM5139_V20_melody.bin
# or bring your own tune (no MIDI file is shipped here)
python3 midi.py song.mid # what is in the file
python3 mid2ton.py song.mid --voice high # inspect the conversion
python3 mkdoom.py --midi song.mid --channel 1 M27512_PM5139_V20.bin out.bin
node doomtest.js M27512_PM5139_V20_melody.bin # play it back in the emulator
mkdoom.py parchea una imagen una vez y se niega a hacerlo dos veces — construye una V2.0 nueva con mkv20.py si quieres empezar de nuevo.
python3 mkchord.py # the chords on offer python3 mkpoly.py --chord power M27512_PM5139_V20.bin out.bin python3 romfix.py out.bin node polytest.js out.bin # check it on the bus
### Gráfico```bash
python3 plot_waveforms.py # V2.0 by default
python3 plot_waveforms.py M27512_PM5139_V13.bin out.png
python3 plot_v20.py # before/after
Cada uno de estos imprime una tabla que puedes verificar contra la documentación:```bash node bitmap.js # which state bits change the display (31 / 23 / 74) node flags.js # which bits change the C-bus telegrams, over six profiles node cmd16.js # which strobes each command token triggers node keycodes.js # which key code reaches which handler node decade.js # decade limits, driven by real key presses node limits.js # parameter limits by bisection node whoruns.js # does this routine ever run in normal operation? node arb.js # does the firmware accept this EEPROM image? node xrange.js # which EEPROM addresses are read at all node iface.js # emulate the interface card, log the I²C traffic node remote.js # how the instrument enters remote mode node nvram.js # which NVRAM bytes change when you adjust something node readout.js # decode a display digit row into plain text node showversion.js # read the version indication out of all three ROMs node trace.js # dynamic execution trace
### Leer un esquema```bash
pdftoppm -f 157 -l 157 -r 400 -png pm5138A_service_manual.pdf page
python3 lines.py page-157.png 1200 800 3000 2400 150
Toda la cadena de compilación es determinista — estos comandos reconstruyen el firmware y la imagen de la EEPROM byte a byte:```bash python3 mapv15.py --write python3 annotate.py 13 && python3 annotate.py 15 python3 mkv20.py # -> M27512_PM5139_V20.bin python3 romfix.py M27512_PM5139_V20.bin python3 mkarb.py # -> D310_image_V20.bin python3 mkdoom.py M27512_PM5139_V20.bin M27512_PM5139_V20_melody.bin python3 build.py # rebuild the browser simulator
---
## Volver a grabarlo
> **Conserva tu EPROM original.** Léela dos veces, compara los volcados, guarda
> el chip en un cajón. Todo lo de aquí es reversible solo si todavía la
> tienes.
El firmware verifica una suma de bytes sobre el rango ocupado al encenderse y
la compara con el byte inmediatamente posterior. Si te equivocas obtienes
`Err 1` y un bucle infinito — el instrumento no arranca. `romfix.py`
calcula e inserta el valor correcto; todos los scripts de compilación de aquí ya lo
invocan.
| Versión | Rango | Byte de checksum | Valor |
|---|---|---|---|
| V1.3 | `0000h–AC6Fh` | `AC70h` | `F2h` |
| V1.5 | `0000h–B3C9h` | `B3CAh` | `99h` |
Dos cosas aprendidas a la mala en hardware real:
- La EEPROM arbitraria necesita que se recalculen sus **bytes de identidad**, o
obtienes `Err 8` en cada arranque y la fuente ARB no se puede seleccionar.
- Si ARB se comporta de forma extraña después de un flasheo, verifica que el pin 28 del zócalo
esté bien asentado antes de sospechar de la imagen.
---
## ¿Qué tan confiable es esto?
Todo lo marcado como verificado se confirmó llamando a las rutinas
originales en el emulador sobre varios puntos de muestra, generalmente contrastado
también con el listado o el esquemático.
Donde las cosas salieron mal, queda escrito en lugar de corregirse silenciosamente:
- **Tres errores del emulador** (`ACALL` como `AJMP`, flag AC faltante, interrupción
de teclado duplicada) estuvieron activos durante la fase media del proyecto.
Todos los hallazgos afectados se volvieron a medir después — el mapa de bits de la pantalla
volvió idéntico, la asignación de strobes coincidió con el manual de servicio,
y la sección 16 resultó tener dos strobes faltantes.
- **Una imagen NVRAM sintética** que nunca se leyó de un instrumento
real falsificó dos hallazgos, incluido "la perilla rotativa solo funciona
en una dirección". La solución fue entregarle al firmware una NVRAM inválida y
dejar que escribiera su propio estado de fábrica.
- **Estados de RAM establecidos a mano** producen configuraciones que el instrumento nunca
adopta. Dos veces esto produjo conclusiones erróneas, una vez un crash en la
tabla de comandos.
- **`core.js` cuenta una instrucción por ciclo**, no ciclos de máquina. Está bien
para el orden, mal para el timing absoluto — las afirmaciones de timing aquí provienen de
la hoja de datos del MCS-51.
Todo lo que es una suposición en lugar de una medición lo dice en el
texto.
---
## Aún abierto
- **36 de 128 bits de estado** necesitan un estímulo fuera de los seis perfiles
operativos — autotest, rutas de error, tráfico de interfaz.
- **Campos de NVRAM desde el offset 0Dh en adelante.** El diseño hasta ahí está medido
(`NVRAM offset + 4Bh = dirección RAM`), la marca de verificación se entiende
(suma de bytes, valor inicial `AAh`, 25 bytes).
- **Qué comando arbitrario llega a cuál de los 13 sub-bloques** en la
región `8871h`. Solo existen cuatro comparaciones directas de tokens; el resto
ramifica según pruebas de bits.
- **Si un comando remoto puede saltarse la verificación de rango de parámetros.**
- **Las rutinas de carga de formas de onda** son la dependencia restante más difícil para
una reimplementación completa — sin ellas no hay señal de salida.
- **Cómo el PM5139 genera 20 MHz a partir del mismo reloj** que su hermano
de 10 MHz. La cadena implica que su paso bajo está en 10 MHz en lugar de 5 MHz,
pero eso necesita un manual del PM5139 para confirmarse.
Si posees uno de estos instrumentos, dos cosas ayudarían mucho: un
**manual de servicio del PM5139**, y volcados de **otras versiones de firmware**
(puede que exista o no una V1.4).
---
## Fuentes
- **`pm5138A_service_manual.pdf`** — la fuente principal de hardware. 176
páginas, con OCR; el texto corrido se lee limpiamente con `pdftotext -layout`, los
esquemáticos tienen que renderizarse como imágenes. Las páginas 4-3 a 4-28 faltan
en el escaneo.
- **Manual de usuario del PM5139** (Fluke) — escaneo trilingüe sin capa de texto;
el capítulo 3.7.4.6 documenta los comandos arbitrarios. Vale la pena hacerle OCR tú mismo
— la parte en inglés son las páginas 13–145 del PDF.
- **Manual de usuario del PM5136** — útil como contraste: sus números de error
y su lista de comandos muestran qué parámetros le faltan al modelo más pequeño, lo que
confirmó de forma independiente el orden de parámetros en la ROM.
- **Hoja de datos de los tres modelos** — límites operativos por forma de onda.
Los manuales son documentos de terceros y **no se redistribuyen en este
repositorio**. Se pueden encontrar en línea.
---
## Licencia y uso
Dos tipos de material, bajo términos diferentes — consulta [LICENSE](https://github.com/doctormord/philips-pm-5139-5138a-5136-firmware-project/blob/main/LICENSE) para
el alcance exacto:
- **El trabajo de ingeniería inversa es MIT.** Documentación, herramientas, ambos
emuladores, tablas de símbolos, anotaciones, las formas de onda generadas y las
gráficas. Úsalo como quieras.
- **El firmware de Philips no es nuestro para licenciar.** Las imágenes ROM, los
volcados de chips de fábrica, los listados de desensamblado y el simulador de navegador
(que incorpora la imagen V1.3) reproducen o derivan del trabajo de Philips.
Están aquí como objeto de estudio, para interoperabilidad, reparación y
documentación de instrumentos que llevan décadas sin soporte.
Donde se mezcla nuestro propio trabajo — las anotaciones, la forma de onda
corregida en V2.0 — solo esa contribución es MIT.
Si posees derechos sobre el firmware original y te opones, abre un issue y
se eliminará.
Si usas algo de esto, se agradece un enlace de vuelta. Si encuentras un error,
abre un issue — cada afirmación aquí nombra la dirección o medición en la que se
basa, así que debería ser falsable.
| Desensamblado | completo para ambas versiones, ~23 000 líneas, con referencias cruzadas |
| Listado anotado | 147 rutinas nombradas, 145 comentarios de encabezado, 3 826 líneas anotadas |
| Documentación | 35 secciones, 4 600 líneas, cada afirmación con fuente |
| Ruta de señal | frecuencia, amplitud, offset, AM, FM, burst, simetría, barrido — todo calculado y verificado contra el código original |
| Hardware | los 10 strobes, el bus C, I²C con cada participante, puertos, teclado, perilla rotativa, bitmap de pantalla |
| Bits de estado | 75 de 128 con un efecto documentado |
| Diff de versiones | V1.3 vs V1.5 es 91.4 % estructuralmente idéntico; cada cambio nombrado |
| Emuladores | uno en Python, uno en JavaScript (~8 M instrucciones/s), más un simulador de navegador de archivo único |
| Nuestro propio firmware | V2.0 — un defecto de fábrica corregido, checksum gestionado, verificado en el emulador y en hardware real |
| Posición | Tipo | Función |
|---|
| D301 | PCB80C652 | núcleo 8051 con I²C por hardware, 12 MHz |
| D306 | 27512 | EPROM de programa — V1.3 ocupa 0000h–AC70h |
| D310 | X28C64 | EEPROM arbitraria en el bus MOVX |
| D305 | PCF8570 | 256 bytes de NVRAM respaldada por batería en I²C (A0h) |
| D304-A | PCF8576 | driver LCD en I²C (70h), búfer de 20 bytes |
| D302-A | SAA3007 | codificador de teclado, codificado por ancho de pulso en una sola línea |
| D307 | 74HCT4514 | decodificador de strobe — el número de strobe son los bits de dirección A8…A11 |
mkdoom.py | mkpoly.py |
|---|
| Voces | una | varias a la vez |
| Forma de onda | la que esté cargada | su propia tabla de acordes |
| Nivel | como lo dejó el panel frontal | establecido explícitamente, 11.6 Vpp medidos |
| ROM usada | 182 bytes | 2617 con el riff incorporado, 6185 desde una pista MIDI |