
Rétro-ingénierie du firmware des générateurs de fonctions Philips PM5139 / PM5138A / PM5136 : émulateurs 8051 utilisés comme instruments de mesure, 35 sections de matériel documenté, et un firmware V2.0 corrigé
Un générateur de fonctions 20 MHz d'environ 1994, démonté par le logiciel : deux dumps d'EPROM, un émulateur 8051 utilisé comme instrument de mesure, et 35 sections de documentation où chaque affirmation est étayée par une adresse de listing, une mesure d'émulateur ou le schéma.
Au bout du compte, il y a un firmware V2.0 qui corrige un défaut livré par Philips, six formes d'onde arbitraires de notre cru, et un simulateur navigateur qui exécute la ROM d'origine instruction par instruction.

Chaque table de formes d'onde du programme EPROM, tracée directement depuis le binaire. En bas à droite se trouve celle qui a lancé la partie la plus intéressante de ce projet.
Le Philips PM5139 est le modèle haut de gamme 20 MHz d'une famille de trois instruments (PM5136 / PM5138A / PM5139). À l'intérieur se trouvent un PCB80C652 — un cœur 8051 avec I²C matériel — une EPROM programme 27512, et six assemblages analogiques suspendus à un bus série.
Il n'existe pas de manuel de service du PM5139. Les gens en cherchent un sur les forums depuis 2010. Ce qui existe, c'est le manuel du PM5138A, son modèle sœur 10 MHz, qui est presque identique en interne.
Ce projet a donc commencé par l'autre bout : dumper l'EPROM, et comprendre ce que fait le code jusqu'à ce que l'instrument soit suffisamment compris pour le modifier.
Deux versions de firmware étaient disponibles, V1.3 et V1.5, toutes deux des dumps M27512 de 64 Kio.
Le côté analogique est un bus C série : l'UART du 8051 fonctionne en mode
registre à décalage, TXD est l'horloge, RXD la donnée, et un strobe décide lequel
des dix registres à décalage verrouille les octets. MOV DPH,#8nh suivi de
MOVX @DPTR,A déclenche le strobe n. Cette seule ligne est la clé de toute la
section analogique.
C'est la partie qui vaut la peine d'être reprise pour votre propre projet.
Lire un binaire 8051 de 44 Ko à l'œil vous mène peut-être à un tiers du chemin. Tout le reste est venu en exécutant le code d'origine et en observant ce qui en ressort :```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
Faites varier l'entrée, lisez la sortie, vérifiez-la par rapport à l'hypothèse. Cela a fonctionné pour la fréquence, l'amplitude, l'offset, la profondeur AM, la déviation FM, le nombre de rafales, la symétrie et les deux caractéristiques de balayage. Chaque formule de la documentation est accompagnée des points d'échantillonnage sur lesquels elle a été vérifiée.
Trois raffinements l'ont rendu réellement productif :
**Surveillez le bus, pas l'affichage.** La section 15 mesure ce qu'un bit d'état fait au tampon d'affichage, et 74 des 128 bits semblent ne rien faire. Mais beaucoup d'entre eux ne pilotent pas l'affichage, ils pilotent les *assemblages analogiques* — et ceux-ci ne sont visibles que sous forme de télégrammes sur le bus C. L'enregistrement de `MOV SBUF,…` et du `MOVX @DPTR` de terminaison a fait passer le nombre de bits documentés de 54 à 75.
**Appuyez sur les touches, ne poussez pas la RAM.** Définir un octet de RAM à la main produit des états que l'instrument ne prend jamais. Cela nous a coûté deux conclusions erronées et un plantage dans la table de commandes. Injecter de vrais codes de touches via le SAA3007 émulé donne des états que le firmware atteint réellement — et c'est un balayage par force brute sur les 256 codes de touches qui a révélé quelle touche déclenche quel gestionnaire.
**Soupçonnez d'abord votre propre émulateur.** Trois bugs dans notre cœur ont produit un comportement « inexplicable » du firmware : `ACALL` exécuté comme `AJMP`, un indicateur de retenue auxiliaire manquant (de sorte que `DA A` se comportait mal et que le firmware semblait compter en binaire), et une interruption clavier doublée. Chaque conclusion de cette période a été remesurée par la suite.
---
## Le chemin parcouru
**D'abord le statique.** Un désassembleur avec une table d'opcodes complète, puis une descente récursive avec des heuristiques de tables de saut. Cela a produit 30 508 octets de code et laissé 13 637 octets non comptabilisés.
**Puis le dynamique.** Une exécution avec trace — démarrage à froid, les 23 touches du panneau avant, les deux sens du bouton rotatif, tous les modes de fonctionnement, 86 millions de cycles — marquant chaque adresse réellement exécutée. Confrontée à l'analyse statique, elle a trouvé exactement **une** zone que la descente avait manquée, et 10 686 des octets inexpliqués se sont révélés être cinq blocs de tables connus.
**Puis les schémas.** L'OCR du manuel de service est inutilisable pour les schémas, mais les images de pages à 400 dpi sont excellentes. Découpées en tuiles qui se chevauchent, elles sont lisibles jusqu'aux numéros de broches. Six feuilles ont été lues de cette manière — et là où cinq pistes parallèles courent à 90 pixels d'écart, l'examen visuel a été remplacé par un script (`lines.py`) qui extrait les segments de ligne du bitmap.
**Puis les deux puces qui ont été extraites.** Un 27C64 étiqueté « SINUS 1.1 » et un X28C64 ont été lus. Tous deux ont été placés dans le schéma et leur contenu décodé.
**Puis le diff de versions.** Tokeniser les deux ROMs (distances de saut relatives au lieu de cibles absolues) et exécuter `SequenceMatcher` dessus donne une correspondance d'adresses qui survit aux déplacements de code — c'est ainsi que les symboles de la V1.3 sont reportés sur la V1.5.
---
## Les bons morceaux
### Philips a livré une forme d'onde bruitée
Les trois courbes arbitraires intégrées se trouvent à `A047h`, `A447h` et `A847h`. La troisième a la même forme qu'une table déjà présente dans la ROM sous forme calculée — mais avec **563 changements de direction contre 13**, et un écart-type de 4,1 LSB.
Elle a été échantillonnée à partir d'une source analogique au lieu d'être calculée. La moyenne de l'écart est nulle, seuls deux points sur 1024 s'écartent de plus de 10 — ce n'est pas une forme d'onde différente, c'est la *même* forme d'onde avec du bruit dessus.
### Cette table est une échelle de niveaux de 30 dB
La version propre était décrite dans une ébauche antérieure comme « une sinusoïde avec dix profondeurs AM », ce qui était une lecture visuelle du graphique, pas quelque chose que le code dit. Calculés, les 1024 points se répartissent en dix arcs sinusoïdaux dont les étendues sont```
255 171 120 80 56 38 26 17 12 8
une série géométrique de raison 0,681 = 10^(−1/6), soit 3,33 dB par pas et 30,1 dB au total. Un modèle par moitié est décalé jusqu'à 56, un modèle à 3 dB de 10. C'est une échelle de niveaux logarithmique — un motif de test d'amplitude ou d'atténuation.
Le contrôleur d'amplitude possède deux registres à décalage sur un seul strobe, mais le firmware n'envoie jamais qu'un seul octet par télégramme. Le schéma l'explique : les deux 4094 sont cascadés via QS' (broche 10), la broche 9 étant inutilisée — et les télégrammes arrivent par paires, à ~42 000 cycles d'intervalle avec des millions de cycles de silence entre les paires. L'octet envoyé en premier est poussé dans le second registre.
Le même schéma de cascade est apparu sur chaque assemblage comportant plus d'un
registre à décalage — y compris un cas où la chaîne traverse une frontière d'assemblage
via une ligne appelée E.
Cinq bits du télégramme STR9 pilotent directement des relais : S1 commute la
gamme du générateur DC, S2…S5 les relais de l'atténuateur. 20 dB (pour 40dB),
20 dB, 50/600 ohms — c'est imprimé sur le schéma. Il n'y a aucun
seuil à calculer.
La table de sauts à 0301h est lue avec JMP @A+DPTR. L'entrée 15 aboutit à
0301h + 30 = 031Fh — et là, au lieu du AJMP habituel, se trouve le
gestionnaire lui-même, en ligne, ce qui économise un saut. Aucune instruction de saut
dans la ROM ne pointe vers lui, l'analyse statique l'a donc manqué. C'est le gestionnaire
DIAL LOCK, et seule la trace dynamique l'a trouvé.
La fiche technique promet 24 mémoires de forme d'onde. Le répertoire dans l'EEPROM en indique six. L'arithmétique tranche :``` 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
La plage de lecture mesurée du firmware est `0100h–1EFFh` — six courbes
octet par octet. L'instrument a été construit avec la petite puce.
### Du code mort qui parle à un appareil absent
186 octets à `9AFFh` font du trafic I²C avec l'adresse `5Ah` — une adresse qui
n'apparaît nulle part ailleurs. Dans **les deux** versions du firmware, aucun saut ne pointe vers
elle. Elle se trouve dans le même bloc de type d'appareil que la carte d'interface à `5Eh`,
juste avec des bits de banque différents, et elle envoie le tampon de réception et les
registres arithmétiques en deux télégrammes de dix octets. Cela ressemble à un
diagnostic d'usine pour un appareil qui n'a jamais été commercialisé.
### On ne peut pas exécuter de code depuis l'EEPROM arbitraire
Une idée évidente — mettre du code dans un emplacement de forme d'onde arbitraire et y sauter —
est vouée à l'échec. Le 8051 est de type Harvard : les instructions passent par
`/PSEN` depuis l'EPROM programme, les données par `/RD` depuis l'EEPROM
arbitraire. Ce n'est pas bloqué ; le fil n'est simplement pas là.
### Et l'encodage de fréquence, enfin
La rangée de chiffres de l'affichage réside dans `3Eh–43h` de l'image envoyée au
PCF8576, toutes les positions partagent un même encodage de segment, et l'octet `43h`
bascule de kHz à MHz entre la décade 7 et 8. À partir de là :```
f = M · 10^(D−8) kHz
Trois séquences de saut de fréquence mesurées sur l'instrument réel sont reproduites exactement par ceci — y compris celle qui s'arrête prématurément parce que la mantisse 2500 signifierait 25 MHz, au-delà de la limite.

À gauche la courbe livrée, à droite la courbe corrigée. En bas à gauche se trouve l'écart par rapport à la table calculée — cette bande de ±5 LSB est ce qu'une source analogique échantillonnée a laissé derrière elle.
mkv20.py construit la V2.0 à partir de la V1.5 (ou de la V1.3). Il trouve chaque
adresse par signature plutôt qu'en les codant en dur, de sorte que le même script
fonctionne sur les deux versions sources :
*IDN? : PHILIPS,PM5139,0,V2.0/0000.2.0 dans l'encodage de segment
mesuré.Tout le reste est inchangé. Trois autres anomalies ont été trouvées et
délibérément laissées telles quelles — une écriture vers un SFR inexistant
(inoffensive, dans les deux versions), le bloc 5Ah mort, et trois bits d'état qui
sont testés mais jamais positionnés. Les corriger ne change aucun comportement et
n'ajoute que du risque.
M27512_PM5139_V20.bin est exactement ceci et rien d'autre. La mélodie
ci-dessous est une étape de compilation séparée et optionnelle.
Vérifié : un démarrage à froid dans l'émulateur produit le même tampon d'affichage
et les mêmes indicateurs que la V1.5, le checksum est validé, et la compilation est
reproductible octet pour octet. Il a été flashé et fonctionne sur un vrai PM5139 —
l'affichage montre 2.0 et les six emplacements arbitraires fonctionnent.
Comme il y a 19 509 octets inutilisés derrière le checksum en V1.5, et que le chemin de fréquence prend une fréquence de note sous forme de trois octets BCD, l'instrument peut jouer de la musique via sa propre sortie.
L'encodage est agréablement direct — décade 3, puis la fréquence en
0,01 Hz en BCD, donc 82,41 Hz est 30 82 41. Quatre octets par note : trois pour
la hauteur, un pour la durée.
La partie intéressante est le déclencheur. Le menu de diagnostic (maintenir LOCAL
lors de la mise sous tension) possède une table de saut avec huit entrées, mais la
boucle du menu ne compte 0Bh que de 1 à 7 — la huitième entrée est donc
inatteignable. Elle est aussi redondante : elle saute au début du menu, qui est
atteint depuis deux autres endroits de toute façon.
Donc tout le hook fait deux octets :``` 5B94h table entry 8: LJMP 5B45h -> LJMP 5B62h count limit: 08h -> 09h
Aucun autotest n'est perdu, aucun tableau n'est déplacé et aucun élément de menu mort n'apparaît. Maintenez LOCAL, allumez, laissez le menu compter jusqu'à 8, appuyez sur une touche.
Le timing provient de la fiche technique du MCS-51. Les deux émulateurs comptent désormais les cycles machine en plus des instructions (`mcyc`, depuis `mcs51.CYCLES`), et l'exécution pas à pas de la boucle d'attente mesure **1009 µs** par unité — 106,95 ms par double croche à 140 BPM, soit 0,2 % d'écart par rapport à la cible. Le chiffre utilisé auparavant était un calcul manuel de 1006 µs qui avait omis deux instructions.
`mkdoom.py` peut aussi convertir un fichier MIDI. Il faut choisir une voix (note la plus haute, note la plus basse, ou un canal) et fusionner les sections de moins de ~25 ms — en dessous, une note grave ne parvient pas à effectuer une oscillation complète et on n'entend qu'un clic.
---
## Et là, il s'est avéré que c'était polyphonique
La mélodie ci-dessus est une seule voix. Elle ne doit pas nécessairement l'être, et la raison tient à une phrase du manuel de service que nous avions lue trop vite :
> Pendant la génération du signal, les échantillons d'amplitude distincts sont lus depuis la RAM. Si la forme d'onde de base est modifiée [...] les échantillons d'amplitude correspondants sont **chargés dans la RAM par le CPU**.
Le PM5139 est un **DDS à table d'ondes de 1024 points**. Le TWS n'est pas un générateur de triangle au sens naïf — c'est un accumulateur de phase qui produit des adresses de lecture 0…1023 pour une RAM rapide sur l'unité 4, et cette RAM est remplie par le CPU via le bus C. Sinus, carré, dent de scie et arbitraire relèvent tous du même mécanisme : une table.
Et la table contient exactement **une période du signal de sortie**. Ainsi, une table construite à partir d'une *somme d'harmoniques* reste périodique sur ses 1024 points, et elle se joue comme un accord. Pas un arpège, pas une astuce de modulation — plusieurs notes résonnant simultanément à pleine amplitude de 20 Vpp, le CPU ne faisant absolument rien pendant qu'elles sonnent. Comme les partiels doivent être des multiples entiers de la fréquence de la table, les intervalles ressortent en intonation juste, ce qui, pour un accord tenu, est de toute façon l'accordage préférable.
`M27512_PM5139_V20_chords.bin` est dans le dépôt, prêt à être gravé — le riff, en accords, avec l'enveloppe. Pour le construire vous-même, ou pour utiliser votre propre fichier MIDI au lieu du riff intégré :```
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 construit les tables — power (2:3:4), major (4:5:6),
minor (10:12:15), dom7 (4:5:6:7) et cinq autres. mkpoly.py en place une
dans la ROM libre avec la mélodie et branche la même entrée de menu morte. Il charge l'accord une fois, puis joue la mélodie par réaccordage uniquement, ce qui transpose tout l'accord en parallèle. Chaque note du riff E1M1 devient un power chord — ce dont ce riff est fait dans l'original.
Architecturalement, c'est un PPG Wave : un compteur parcourant une forme d'onde à cycle unique, directement dans un DAC. L'astuce de l'accord est celle qu'utilisaient les trackers Amiga — mettre l'accord dans la forme d'onde pour qu'une seule voix joue trois notes au lieu de dépenser trois canaux pour cela. Un C64 doit à la place faire un arpège, car le SID n'a pas de wavetable inscriptible.
Vous n'avez même pas besoin d'une EPROM pour les accords. Les mêmes tables tiennent dans l'EEPROM arbitraire, donc python3 mkarb.py --chords vous donne six accords sélectionnables depuis le panneau avant sans toucher au firmware.
Il y a deux lecteurs et une image en embarque un ou l'autre, puisque les deux branchent la même entrée de menu :
Deux mesures ont façonné cette conception :
00h 44h 88h CCh) et chaque valeur reconstruite est un multiple de quatre. La RAM de forme d'onde fait douze bits de large, mais le bus en pilote dix — exactement ce que le format ARB stocke, donc Philips n'a rien gaspillé là.RAM_PAGE à 1D62h, qui en a l'air d'une, construit son mot à partir de la fréquence. Donc l'harmonie vit dans la table et la mélodie dans le mot de fréquence ; rien n'est rechargé pendant que la musique tourne.L'émulateur ne modélise aucune RAM de forme d'onde, donc le chargeur est vérifié par construction à la place : polytest.js enregistre ce qui atteint réellement le bus et compare les 1024 points avec ce que mkchord.py a généré.
Il a fallu cinq EPROM pour y arriver, et l'émulateur ne pouvait nous mener qu'une partie du chemin : il modélise le CPU et le bus mais pas la RAM de forme d'onde, donc tout ce qu'il peut confirmer est que les mêmes octets sortent que ceux que le firmware envoie. C'est nécessaire et non suffisant. Trois choses ont dû être réglées sur l'instrument lui-même :
L'ordre des octets. Deux octets par point, octet haut en premier. L'inférer depuis le propre téléchargement du firmware donnait la réponse opposée et la table ressortait en bruit. Ce qui l'a tranché fut une EPROM portant six motifs de test — une ligne plate, une rampe, la même rampe avec les octets de chaque point échangés, et trois autres — et un coup d'œil à un oscilloscope. La rampe échangée était la propre.
Un changement de forme d'onde, c'est dix-neuf télégrammes, pas les trois que le premier lecteur envoyait. Celui qui compte est une écriture de deux octets qui met la RAM en mode écriture ; sans elle, 2048 octets partent sur le bus et n'atterrissent nulle part.
Le niveau de sortie. L'atténuateur est constitué de deux étages de relais de 20 dB séparés dans un seul octet, la table ROM correspondante se lit à l'inverse de ce qui avait été documenté (ce sont des bits de contournement), et le DAC de niveau fait sept bits, pas huit — il déborde à 80h, donc un réglage « plus fort » produisait le silence. Celui-là a nécessité une matrice d'environ trente combinaisons dans une seule image, en utilisant la fréquence de sortie comme numéro de test pour que la lecture propre de l'oscilloscope indique quelle combinaison est active.``` 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
---
## Six formes d'onde arbitraires de notre cru

`D310_image_V20.bin` remplit chaque emplacement de l'EEPROM — graver la
puce vaut la peine d'être fait une fois :
| Emplacement | Forme d'onde | Vpp | Pour |
|---|---|---|---|
| 1 | sinc, 8 lobes | 12.17 | limitation de bande, dépassement |
| 2 | oscillante, Q≈6 | 17.81 | comportement d'établissement |
| 3 | ECG | 12.80 | démo |
| 4 | escalier, 16 pas bipolaire | 20.00 | linéarité, résolution |
| 5 | sinus redressé | 10.00 | comme dans l'original, mais calculé |
| 6 | multi-tonalité, 5 tons | 20.00 | intermodulation |
Deux détails qui comptent et qu'il est facile de rater :
**Le centrage sur zéro vaut mieux que l'étirement.** Le réflexe évident
est d'étirer chaque courbe sur toute la plage de valeurs. Ne le faites
pas : l'offset DC de l'instrument provient d'un chemin analogique séparé
et ajoute une tension *fixe*, tandis que le contenu DC d'une courbe
asymétrique étirée évolue *avec l'amplitude*. Il faudrait réajuster
l'offset à chaque changement de niveau. Placer le zéro naturel de la
forme d'onde sur le zéro du convertisseur coûte 0,2 à 1 bit — face aux
16 LSB de bruit que le chemin analogique d'origine apporte déjà. Pas un
vrai coût.
**Mettre à l'échelle en virgule flottante, arrondir une seule fois.**
Arrondir d'abord puis étirer ensuite donne 1,0 à 1,5 pas de
quantification d'erreur ; mettre à l'échelle en flottant et arrondir une
seule fois donne l'optimal de 0,5.
Le répertoire a besoin d'un octet d'identité par courbe (une somme de
contrôle des 1280 octets de la courbe, valeur de départ `55h`) et des
min/max sous forme de valeurs 10 bits alignées à gauche sur six bits. Si
l'octet d'identité est erroné, l'instrument affiche **Err 8** et refuse
la source arbitraire — ce qui s'est exactement produit lors du premier
flash réel.
---
## Le simulateur navigateur
`PM5139_Simulator.html` est un fichier unique et autonome — pas d'étape
de compilation, pas de dépendances, pas de réseau. Ouvrez-le et le
firmware V1.3 d'origine démarre devant vous.
Le cœur 8051 exécute le vrai code. Les temporisateurs, les interruptions,
le C-bus et l'I²C sont émulés ; l'affichage est décodé à partir du flux
de données PCF8576 réel, et les touches génèrent la forme d'onde SAA3007
codée en largeur d'impulsion sur P3.3. La RAM sauvegardée par pile est
préchargée et l'EEPROM arbitraire est générée au démarrage et vérifiée
par le firmware lui-même.
Un démarrage à froid prend environ 9 millions d'instructions, alors
laissez-lui une seconde.
---
## Organisation du dépôt```
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 et Node sont tout ce dont vous avez besoin. matplotlib pour les graphiques, pillow
et numpy uniquement pour 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
### Build 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
### Ajouter une mélodie```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 applique un correctif à une image une seule fois et refuse de le faire deux fois — construisez une nouvelle V2.0 avec mkv20.py si vous voulez recommencer.
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
### Tracé```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
Chacune de ces commandes affiche un tableau que vous pouvez vérifier par rapport à la documentation :```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
### Lire un schéma```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
Toute la chaîne de compilation est déterministe — ces commandes reconstruisent le firmware et l'image EEPROM octet par octet :```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
---
## Le reflasher
> **Conservez votre EPROM d'origine.** Lisez-la deux fois, comparez les
> dumps, rangez la puce dans un tiroir. Tout ce qui suit n'est réversible
> que si vous la possédez encore.
Le firmware vérifie une somme d'octets sur la plage occupée au démarrage et
la compare à l'octet immédiatement après. En cas d'erreur, vous obtenez
`Err 1` et une boucle infinie — l'instrument ne démarre pas. `romfix.py`
calcule et insère la valeur correcte ; tous les scripts de build ici
l'appellent déjà.
| Version | Plage | Octet de checksum | Valeur |
|---|---|---|---|
| V1.3 | `0000h–AC6Fh` | `AC70h` | `F2h` |
| V1.5 | `0000h–B3C9h` | `B3CAh` | `99h` |
Deux choses apprises à la dure sur du matériel réel :
- L'EEPROM arbitraire a besoin que ses **octets d'identité** soient
recalculés, sinon vous obtenez `Err 8` à chaque démarrage et la source
ARB ne peut pas être sélectionnée.
- Si l'ARB se comporte étrangement après un flash, vérifiez que la broche
28 du support est bien enfoncée avant de suspecter l'image.
---
## Quelle est la fiabilité de tout ceci ?
Tout ce qui est marqué comme vérifié a été confirmé en appelant les routines
originales dans l'émulateur sur plusieurs points d'échantillonnage,
généralement recoupé avec le listing ou le schéma également.
Là où les choses ont mal tourné, c'est écrit plutôt que discrètement
corrigé :
- **Trois bugs de l'émulateur** (`ACALL` en `AJMP`, flag AC manquant,
interruption clavier doublée) étaient actifs pendant la phase
intermédiaire du projet. Tous les résultats affectés ont été remesurés
par la suite — la bitmap d'affichage est revenue identique, l'affectation
des strobes correspondait au manuel de maintenance, et la section 16
s'est avérée avoir deux strobes manquants.
- **Une image NVRAM synthétique** qui n'a jamais été lue depuis un
instrument réel a faussé deux résultats, dont « le bouton rotatif ne
fonctionne que dans un sens ». Le correctif a été de fournir au firmware
une NVRAM invalide et de le laisser écrire son propre état d'usine.
- **Des états RAM définis à la main** produisent des configurations que
l'instrument n'adopte jamais. Deux fois cela a produit des conclusions
erronées, une fois un crash dans la table de commandes.
- **`core.js` compte une instruction par cycle**, pas des cycles machine.
Correct pour l'ordonnancement, faux pour le timing absolu — les
affirmations de timing ici proviennent de la fiche technique MCS-51.
Tout ce qui est une hypothèse plutôt qu'une mesure le dit dans le texte.
---
## Encore ouvert
- **36 des 128 bits d'état** nécessitent un stimulus en dehors des six
profils de fonctionnement — autotest, chemins d'erreur, trafic
d'interface.
- **Les champs NVRAM à partir de l'offset 0Dh.** La disposition jusqu'ici
est mesurée (`NVRAM offset + 4Bh = adresse RAM`), la marque de contrôle
est comprise (somme d'octets, valeur de départ `AAh`, 25 octets).
- **Quelle commande arbitraire atteint lequel des 13 sous-blocs** dans la
région `8871h`. Seules quatre comparaisons directes de tokens existent ;
le reste branche sur des tests de bits.
- **Si une commande distante peut contourner la vérification de plage des
paramètres.**
- **Les routines de chargement de forme d'onde** sont la dépendance
restante la plus difficile pour une réimplémentation complète — sans
elles, pas de signal de sortie.
- **Comment le PM5139 produit 20 MHz à partir de la même horloge** que son
frère à 10 MHz. La chaîne implique que son passe-bas se situe à 10 MHz
au lieu de 5 MHz, mais cela nécessite un manuel du PM5139 pour être
confirmé.
Si vous possédez l'un de ces instruments, deux choses aideraient
beaucoup : un **manuel de maintenance du PM5139**, et des dumps d'**autres
versions de firmware** (une V1.4 peut ou non exister).
---
## Sources
- **`pm5138A_service_manual.pdf`** — la source matérielle principale. 176
pages, OCRisées ; le texte courant se lit proprement avec
`pdftotext -layout`, les schémas doivent être rendus en images. Les pages
4-3 à 4-28 manquent dans le scan.
- **Manuel utilisateur du PM5139** (Fluke) — scan trilingue sans couche
texte ; le chapitre 3.7.4.6 documente les commandes arbitraires. Cela
vaut la peine de l'OCRiser vous-même — la partie anglaise correspond aux
pages PDF 13–145.
- **Manuel utilisateur du PM5136** — utile comme contre-vérification : ses
numéros d'erreur et sa liste de commandes montrent quels paramètres le
plus petit modèle n'a pas, ce qui a confirmé indépendamment l'ordre des
paramètres dans la ROM.
- **Fiche technique des trois modèles** — limites de fonctionnement par
forme d'onde.
Les manuels sont des documents tiers et ne sont **pas redistribués dans ce
dépôt**. Ils sont trouvables en ligne.
---
## Licence et utilisation
Deux types de matériel, sous des termes différents — voir [LICENSE](https://github.com/doctormord/philips-pm-5139-5138a-5136-firmware-project/blob/main/LICENSE)
pour la portée exacte :
- **Le travail de rétro-ingénierie est sous MIT.** Documentation, outils,
les deux émulateurs, tables de symboles, annotations, les formes d'onde
générées et les graphiques. Utilisez-les comme bon vous semble.
- **Le firmware Philips n'est pas à nous de licencier.** Les images ROM,
les dumps de puces d'usine, les listings de désassemblage et le
simulateur navigateur (qui intègre l'image V1.3) reproduisent ou
dérivent du travail de Philips. Ils sont ici comme objet d'étude, pour
l'interopérabilité, la réparation et la documentation d'instruments qui
ne sont plus supportés depuis des décennies. Là où notre propre travail
est mélangé — les annotations, la forme d'onde corrigée en V2.0 — seule
cette contribution est sous MIT.
Si vous détenez des droits sur le firmware original et que vous vous y
opposez, ouvrez une issue et il sera retiré.
Si vous utilisez tout ou partie de ceci, un lien de retour est apprécié. Si
vous trouvez une erreur, ouvrez une issue — chaque affirmation ici nomme
l'adresse ou la mesure sur laquelle elle repose, elle devrait donc être
falsifiable.
| Désassemblage | complet pour les deux versions, ~23 000 lignes, avec références croisées |
| Listing annoté | 147 routines nommées, 145 commentaires d'en-tête, 3 826 lignes annotées |
| Documentation | 35 sections, 4 600 lignes, chaque affirmation sourcée |
| Chemin du signal | fréquence, amplitude, offset, AM, FM, burst, symétrie, sweep — tout calculé et vérifié par rapport au code d'origine |
| Matériel | les 10 strobes, le bus C, l'I²C avec chaque participant, les ports, le clavier, le bouton rotatif, le bitmap de l'afficheur |
| Bits d'état | 75 sur 128 avec un effet documenté |
| Diff de version | V1.3 vs V1.5 est structurellement identique à 91,4 % ; chaque changement nommé |
| Émulateurs | un en Python, un en JavaScript (~8 M instructions/s), plus un simulateur navigateur monofichier |
| Notre propre firmware | V2.0 — un défaut d'usine corrigé, la somme de contrôle gérée, vérifié dans l'émulateur et sur matériel réel |
| Position | Type | Fonction |
|---|
| D301 | PCB80C652 | cœur 8051 avec I²C matériel, 12 MHz |
| D306 | 27512 | EPROM programme — V1.3 occupe 0000h–AC70h |
| D310 | X28C64 | EEPROM arbitraire sur le bus MOVX |
| D305 | PCF8570 | 256 octets de NVRAM sauvegardée par pile sur I²C (A0h) |
| D304-A | PCF8576 | pilote LCD sur I²C (70h), tampon de 20 octets |
| D302-A | SAA3007 | encodeur clavier, codé en largeur d'impulsion sur une seule ligne |
| D307 | 74HCT4514 | décodeur de strobe — le numéro de strobe correspond aux bits d'adresse A8…A11 |
mkdoom.py | mkpoly.py |
|---|
| Voix | une | plusieurs à la fois |
| Forme d'onde | celle qui est chargée | sa propre table d'accords |
| Niveau | tel que le panneau avant l'a laissé | défini explicitement, 11,6 Vpp mesuré |
| ROM utilisée | 182 octets | 2617 avec le riff intégré, 6185 depuis une piste MIDI |