
Firmware-Reverse-Engineering der Philips PM5139 / PM5138A / PM5136 Funktionsgeneratoren: 8051-Emulatoren als Messinstrumente, 35 Abschnitte dokumentierter Hardware und eine korrigierte Firmware V2.0
Ein 20-MHz-Funktionsgenerator von etwa 1994, in Software zerlegt: zwei EPROM-Dumps, ein 8051-Emulator als Messinstrument und 35 Dokumentationsabschnitte, in denen jede einzelne Aussage durch eine Listing- Adresse, eine Emulator-Messung oder den Schaltplan belegt ist.
Am Ende steht eine Firmware V2.0, die einen von Philips ausgelieferten Defekt behebt, sechs eigene Arbiträrwellenformen und ein Browser-Simulator, der das Original-ROM Instruktion für Instruktion ausführt.

Jede Wellenformtabelle im Programm-EPROM, direkt aus dem Binärbild geplottet. Unten rechts diejenige, die den interessantesten Teil dieses Projekts ins Rollen gebracht hat.
Der Philips PM5139 ist das 20-MHz-Topmodell einer Familie aus drei Instrumenten (PM5136 / PM5138A / PM5139). Im Inneren sitzt ein PCB80C652 — ein 8051-Kern mit Hardware-I²C — ein 27512-Programm-EPROM und sechs analoge Baugruppen, die an einem seriellen Bus hängen.
Es gibt kein Servicehandbuch für den PM5139. In Foren wird seit 2010 danach gesucht. Was existiert, ist das Handbuch für den PM5138A, sein 10-MHz-Schwestermodell, das intern nahezu identisch ist.
Dieses Projekt begann also von der anderen Seite: EPROM auslesen und herausfinden, was der Code tut, bis das Instrument gut genug verstanden ist, um es zu modifizieren.
Zwei Firmware-Versionen waren verfügbar, V1.3 und V1.5, beide 64-KiB-M27512-Dumps.
| Disassemblierung | vollständig für beide Versionen, ~23 000 Zeilen, mit Querverweisen |
| Annotiertes Listing | 147 benannte Routinen, 145 Kopfkommentare, 3 826 annotierte Zeilen |
| Dokumentation | 35 Abschnitte, 4 600 Zeilen, jede Aussage belegt |
| Signalpfad | Frequenz, Amplitude, Offset, AM, FM, Burst, Symmetrie, Sweep — alles berechnet und gegen den Originalcode verifiziert |
| Hardware | alle 10 Strobes, der C-Bus, I²C mit jedem Teilnehmer, Ports, Tastatur, Drehknopf, Display-Bitmap |
| Statusbits | 75 von 128 mit dokumentierter Wirkung |
| Versionsdiff | V1.3 vs. V1.5 ist zu 91,4 % strukturell identisch; jede Änderung benannt |
| Emulatoren | einer in Python, einer in JavaScript (~8 Mio. Instruktionen/s), plus ein Single-File-Browser-Simulator |
| Eigene Firmware | V2.0 — ein Werksdefekt behoben, Checksumme behandelt, im Emulator und auf echter Hardware verifiziert |
| Position | Typ | Funktion |
|---|---|---|
| D301 | PCB80C652 | 8051-Kern mit Hardware-I²C, 12 MHz |
| D306 | 27512 | Programm-EPROM — V1.3 belegt 0000h–AC70h |
| D310 | X28C64 | Arbiträr-EEPROM am MOVX-Bus |
| D305 | PCF8570 | 256 Bytes batteriegepuffertes NVRAM an I²C (A0h) |
| D304-A | PCF8576 | LCD-Treiber an I²C (70h), 20-Byte-Puffer |
| D302-A | SAA3007 | Tastatur-Encoder, pulslängenkodiert auf einer einzigen Leitung |
| D307 | 74HCT4514 | Strobe-Decoder — die Strobe-Nummer sind die Adressbits A8…A11 |
Die analoge Seite ist ein serieller C-Bus: Der UART des 8051 läuft im
Schieberegistermodus, TXD ist der Takt, RXD die Daten, und ein Strobe
entscheidet, welches der zehn Schieberegister die Bytes übernimmt.
MOV DPH,#8nh gefolgt von MOVX @DPTR,A löst Strobe n aus. Diese eine
Zeile ist der Schlüssel zum gesamten analogen Abschnitt.
Das ist der Teil, den man für sein eigenes Projekt übernehmen sollte.
Ein 44-KB-8051-Binärbild mit bloßem Auge zu lesen, bringt einen vielleicht ein Drittel des Weges. Alles darüber hinaus kam daraus, den Originalcode laufen zu lassen und zu beobachten, was dabei herausfällt:```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
Die Eingabe variieren, die Ausgabe lesen, sie gegen die Hypothese prüfen. Das
funktionierte für Frequenz, Amplitude, Offset, AM-Tiefe, FM-Hub, Burst-Anzahl,
Symmetrie und beide Sweep-Eigenschaften. Jede Formel in der Dokumentation
kommt mit den Abtastpunkten, über die sie verifiziert wurde.
Drei Verfeinerungen machten es tatsächlich produktiv:
**Den Bus beobachten, nicht das Display.** Abschnitt 15 misst, was ein
Statusbit mit dem Display-Puffer macht, und 74 von 128 Bits scheinen nichts zu
tun. Aber viele von ihnen steuern nicht das Display, sie steuern die *analogen
Baugruppen* — und die sind nur als Telegramme auf dem C-Bus sichtbar. Das
Aufzeichnen von `MOV SBUF,…` und dem abschließenden `MOVX @DPTR` hob die Anzahl
der dokumentierten Bits von 54 auf 75.
**Tasten drücken, nicht RAM manipulieren.** Ein RAM-Byte von Hand zu setzen
erzeugt Zustände, die das Instrument nie einnimmt. Das kostete uns zwei falsche
Erkenntnisse und einen Absturz in die Befehlstabelle. Das Einspeisen echter
Tastencodes über den emulierten SAA3007 liefert Zustände, die die Firmware
tatsächlich erreicht — und es war ein Brute-Force-Durchlauf über alle 256
Tastencodes, der aufdeckte, welche Taste welchen Handler auslöst.
**Zuerst den eigenen Emulator verdächtigen.** Drei Fehler in unserem Kern
erzeugten „unerklärliches" Firmware-Verhalten: `ACALL` als `AJMP` ausgeführt,
ein fehlendes Auxiliary-Carry-Flag (sodass `DA A` sich falsch verhielt und die
Firmware scheinbar binär zählte) und ein doppelter Tastatur-Interrupt. Jede
Erkenntnis aus diesem Zeitraum wurde danach neu gemessen.
---
## Der Weg hierher
**Zuerst statisch.** Ein Disassembler mit vollständiger Opcode-Tabelle, dann
rekursiver Abstieg mit Sprungtabellen-Heuristiken. Das ergab 30 508 Bytes Code
und ließ 13 637 Bytes ungeklärt.
**Dann dynamisch.** Ein Trace-Lauf — Kaltstart, alle 23 Frontplattentasten,
beide Drehrichtungen des Knopfes, jeder Betriebsmodus, 86 Millionen Zyklen —
der jede Adresse markierte, die tatsächlich ausgeführt wurde. Gegen die
statische Analyse gehalten, fand er genau **einen** Bereich, den der Abstieg
übersehen hatte, und 10 686 der unerklärten Bytes entpuppten sich als fünf
bekannte Tabellenblöcke.
**Dann die Schaltpläne.** Die OCR des Servicehandbuchs ist für Schaltpläne
unbrauchbar, aber die Seitenbilder mit 400 dpi sind hervorragend. In
überlappende Kacheln zerschnitten, sind sie bis hinunter zu Pin-Nummern lesbar.
Sechs Blätter wurden auf diese Weise gelesen — und wo fünf parallele Leiterbahnen
90 Pixel voneinander entfernt verlaufen, wurde das Augenmaß durch ein Skript
(`lines.py`) ersetzt, das die Liniensegmente aus der Bitmap extrahiert.
**Dann die beiden Chips, die gezogen wurden.** Ein 27C64 mit der Bezeichnung
„SINUS 1.1" und ein X28C64 wurden ausgelesen. Beide wurden in den Schaltplan
eingesetzt und ihre Inhalte dekodiert.