
Philips PM5139 / PM5138A / PM5136 ファンクションジェネレータのファームウェアリバースエンジニアリング:測定器として使用される 8051 エミュレータ、文書化されたハードウェア 35 セクション、および修正済みファームウェア V2.0
1994年頃の20 MHzファンクションジェネレータをソフトウェア的に分解した:2つの EPROMダンプ、測定器として使う8051エミュレータ、そして35 セクションのドキュメント。そこではすべての主張がリスティングの アドレス、エミュレータによる測定、または回路図によって裏付けられている。
その果てにあるのは、Philipsが出荷した欠陥を修正したファームウェア V2.0、 独自の6つの任意波形、そしてオリジナルのROMを命令単位で実行する ブラウザシミュレータである。

プログラムEPROM内のすべての波形テーブルを、バイナリから直接プロットしたもの。 右下にあるのが、このプロジェクトで最も興味深い部分のきっかけとなったものである。
Philips PM5139は、3機種ファミリー(PM5136 / PM5138A / PM5139)の 20 MHz最上位モデルである。内部にはPCB80C652 — ハードウェアI²Cを備えた8051コア — 、 27512プログラムEPROM、そしてシリアルバスにぶら下がる6つのアナログアセンブリがある。
PM5139のサービスマニュアルは存在しない。2010年以降、人々は フォーラムでそれを探し続けている。存在するのは、内部がほぼ同一である 10 MHzの姉妹モデル、PM5138Aのマニュアルである。
そこでこのプロジェクトは反対側から始まった:EPROMをダンプし、 計測器を改造できる程度に理解できるまでコードの動作を解明する。
入手できたファームウェアはV1.3とV1.5の2バージョンで、いずれも64 KiBの M27512ダンプである。
アナログ側はシリアルCバスである:8051のUARTはシフトレジスタモードで動作し、
TXDがクロック、RXDがデータ、そしてストローブが10個のシフトレジスタのうち
どれがバイトをラッチするかを決める。MOV DPH,#8nhに続く
MOVX @DPTR,Aがストローブnを発火させる。この1行がアナログセクション全体の鍵である。
これは自分のプロジェクトに盗んででも取り入れる価値のある部分である。
44 KBの8051バイナリを目で読んでも、せいぜい3分の1程度しか進まない。 それ以降のすべては、オリジナルのコードを実行して何が出てくるかを見る ことから得られた:```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
入力を変え、出力を読み、仮説と照合する。これが周波数、振幅、オフセット、AM深さ、FM偏差、バースト数、対称性、そして両方のスイープ特性についてうまくいった。ドキュメント内の各式には、それが検証されたサンプル点が付属している。
3つの改良によって、実際に生産的になった:
**ディスプレイではなく、バスを見よ。** セクション15は、状態ビットがディスプレイバッファに何をするかを測定しており、128ビットのうち74ビットは何もしていないように見える。しかし、それらの多くはディスプレイを駆動しているのではなく、*アナログアセンブリ*を駆動している——そしてそれらはCバス上のテレグラムとしてのみ可視化される。`MOV SBUF,…`と終端の`MOVX @DPTR`を記録することで、文書化されたビットの数は54から75に増えた。
**RAMをいじるのではなく、キーを押せ。** RAMバイトを手動で設定すると、計測器が決して取らない状態が生成される。これにより2つの誤った発見と、コマンドテーブルへの1回のクラッシュという代償を払った。エミュレートされたSAA3007を通じて実際のキーコードを注入すると、ファームウェアが実際に到達する状態が得られる——そして、どのキーがどのハンドラをトリガーするかを明らかにしたのは、全256キーコードにわたる総当たりスイープだった。
**まず自分たちのエミュレータを疑え。** 私たちのコアの3つのバグが「不可解な」ファームウェアの挙動を生み出した:`ACALL`が`AJMP`として実行され、補助キャリーフラグが欠落し(そのため`DA A`が誤動作し、ファームウェアが2進数でカウントしているように見えた)、キーボード割り込みが二重になった。その期間の発見はすべて、その後再測定された。
---
## ここに至る道
**まず静的解析。** 完全なオペコードテーブルを備えた逆アセンブラ、次にジャンプテーブルヒューリスティクスを用いた再帰的降下。これにより30 508バイトのコードが生成され、13 637バイトが未解明のまま残った。
**次に動的解析。** トレース実行——コールドスタート、全23個のフロントパネルキー、ノブの両方向、すべての動作モード、8600万サイクル——で、実際に実行されたすべてのアドレスをマークした。静的解析と照合したところ、降下が見落としていた領域をちょうど**1つ**発見し、未解明のバイトのうち10 686バイトは5つの既知のテーブルブロックであることが判明した。
**次に回路図。** サービスマニュアルのOCRは回路図には役に立たないが、400 dpiのページ画像は優れている。重なり合うタイルに分割すると、ピン番号まで読み取れる。この方法で6枚のシートを読み取った——そして5本の並行走査線が90ピクセル間隔で走っている箇所では、目視はビットマップから線分を抽出するスクリプト(`lines.py`)に置き換えられた。
**次に抜き取られた2つのチップ。** 「SINUS 1.1」とラベルされた27C64と、X28C64が読み出された。両方とも回路図に配置され、その内容がデコードされた。
**次にバージョン差分。** 両方のROMをトークン化し(絶対ターゲットではなく相対ジャンプ距離)、それらに対して`SequenceMatcher`を実行すると、コードの移動に耐えるアドレスマッピングが得られる——これがV1.3のシンボルをV1.5に引き継ぐ方法である。
---
## 優れた点
### Philipsはノイズの多い波形を出荷した
3つの内蔵任意曲線は`A047h`、`A447h`、`A847h`にある。3番目は、計算形式で既にROM内に存在するテーブルと同じ形状をしている——しかし**方向変化が563回対13回**で、標準偏差は4.1 LSBである。
これは計算されたのではなく、アナログ源からサンプリングされた。偏差の平均はゼロで、1024点のうち10を超えて外れているのはわずか2点——これは異なる波形ではなく、ノイズが乗った*同じ*波形である。
### そのテーブルは30 dBのレベルラダーである
クリーンなバージョンは、以前のドラフトで「10個のAM深さを持つ正弦波」と記述されていたが、それはコードが述べていることではなく、プロットの目視による読み取りだった。計算し通すと、1024点は10個の正弦弧に分割され、そのスパンは```
255 171 120 80 56 38 26 17 12 8
0.681 = 10^(−1/6) の比率の等比数列、つまり1ステップあたり 3.33 dB、全体で 30.1 dB。半減モデルでは最大 56、3 dB モデルでは 10 の誤差が出る。これは対数レベルのラダー — 振幅または減衰のテストパターンである。
振幅コントローラは 1 つのストローブ上に 2 つのシフトレジスタを持つが、ファームウェアは電文ごとに 1 バイトしか送信しない。回路図がそれを説明している。2 つの 4094 は QS' (ピン 10) を介してカスケード接続され、ピン 9 は未使用 — そして電文はペアで、ペア間には数百万サイクルの無音を挟んで約 42 000 サイクル離れて到着する。最初に送信されたバイトが 2 番目のレジスタへ押し込まれる。
同じカスケードパターンは、複数のシフトレジスタを持つすべてのアセンブリで見つかった — チェーンが E と呼ばれるラインを通ってアセンブリ境界を越えるケースも含めて。
STR9 電文中の 5 ビットがリレーを直接駆動する。S1 は DC ジェネレータのレンジを切り替え、S2…S5 は減衰器リレーを切り替える。20 dB (for 40dB)、20 dB、50/600 ohms — 回路図に印刷されている。計算すべきしきい値は存在しない。
0301h のジャンプテーブルは JMP @A+DPTR で読まれる。エントリ 15 は 0301h + 30 = 031Fh に着地する — そしてそこには、通常の AJMP の代わりにハンドラそのものがインラインで置かれ、ジャンプを節約している。ROM 内のどのジャンプ命令もそこを指していないため、静的解析はそれを見落とした。これは DIAL LOCK ハンドラであり、動的トレースだけがそれを見つけた。
データシートは 24 個の波形メモリを約束している。EEPROM 内のディレクトリは 6 個と言っている。算術が決着をつける:``` 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
ファームウェアの測定された読み取り範囲は `0100h–1EFFh` である — バイト単位で6つのカーブ。この計測器は小型チップで作られている。
### 存在しないデバイスと通信するデッドコード
`9AFFh` の186バイトがアドレス `5Ah` で I²C トラフィックを行う — このアドレスは他にどこにも現れない。**両方**のファームウェアバージョンにおいて、そこを指すジャンプは存在しない。これは `5Eh` のインターフェースカードと同じデバイスタイプブロックに位置し、バンクビットが異なるだけであり、受信バッファと演算レジスタを2つの10バイト電文で送信する。出荷されることのなかったデバイス向けの工場診断のように見える。
### 任意波形 EEPROM からコードを実行することはできない
明白なアイデア — 任意波形スロットにコードを置いてそこへジャンプする — は最初から不可能である。8051 はハーバードアーキテクチャである。命令はプログラム EPROM から `/PSEN` を通じて、データは任意波形 EEPROM から `/RD` を通じて供給される。ブロックされているのではなく、配線が単に存在しないのである。
### そして最後に、周波数エンコーディング
表示桁行は PCF8576 に送られるイメージの `3Eh–43h` に位置し、すべての位置が1つのセグメントエンコーディングを共有し、バイト `43h` が10年と8年の間で kHz から MHz に切り替わる。そこから:```
f = M · 10^(D−8) kHz
実機で測定した3つの周波数ステッピングシーケンスは、これによって正確に再現される。仮数2500が25 MHzを意味し、上限を超えるために途中で停止するものも含まれる。

左が出荷時の波形、右が修正後の波形。左下は計算されたテーブルからの偏差 — この±5 LSBの帯域は、サンプリングされたアナログ信号源が残したものである。
mkv20.py は V1.5(または V1.3)から V2.0 をビルドする。すべてのアドレスをハードコードするのではなくシグネチャによって検出するため、同じスクリプトが両方のソースバージョンで動作する:
*IDN? に追加: PHILIPS,PM5139,0,V2.0/0000。2.0 を表すようになった。それ以外はすべて変更されていない。さらに3つの奇妙な点が見つかったが、意図的にそのままにしてある — 存在しないSFRへの書き込み(両バージョンで無害)、死んだ 5Ah ブロック、そしてテストされるが決して設定されない3つの状態ビットである。これらにパッチを当てても動作は変わらず、リスクが増えるだけである。
M27512_PM5139_V20.bin はまさにこれであり、それ以外ではない。以下のメロディは別の、オプトインのビルドステップである。
検証済み: エミュレータでのコールドスタートは V1.5 と同じディスプレイバッファと同じフラグを生成し、チェックサムが検証され、ビルドはバイト単位で再現可能である。実機の PM5139 に書き込んで動作確認済み — ディスプレイには 2.0 が表示され、6つの任意スロットすべてが機能する。
V1.5 のチェックサムの後ろに 19 509 バイトの未使用領域があり、周波数パスがノート周波数を3つのBCDバイトとして受け取るため、この計測器は自身の出力を通じて音楽を演奏できる。
エンコーディングは嬉しいほど直接的である — まず decade 3、次に周波数を0.01 Hz単位のBCDで表すので、82.41 Hz は 30 82 41 となる。1ノートあたり4バイト: 3バイトがピッチ、1バイトがデュレーションである。
興味深いのはトリガーである。診断メニュー(電源投入時に LOCAL を押し続ける)には 8つのエントリを持つジャンプテーブルがあるが、メニューループは 0Bh を1から7までしかカウントしない — つまり8番目のエントリは到達不能である。また冗長でもある: メニュー開始位置にジャンプするが、そこへは他の2箇所からも到達する。
したがってフック全体は 2バイトである:``` 5B94h table entry 8: LJMP 5B45h -> LJMP 5B62h count limit: 08h -> 09h
セルフテストが失われることも、テーブルが移動されることも、死んだメニュー項目が現れることもない。LOCALを押し、電源を入れ、メニューが8まで数えるのを待ち、キーを押す。
タイミングはMCS-51のデータシートから来ている。両方のエミュレータは今や命令と並んでマシンサイクルをカウントし(`mcyc`、`mcs51.CYCLES`から)、ウェイトループをステップ実行すると1単位あたり**1009 µs**を計測する — 140 BPMで16分音符あたり106.95 ms、目標から0.2 %のずれである。この数値は以前、2命令を落とした1006 µsの手計算だった。
`mkdoom.py`はMIDIファイルも変換できる。ボイスを選ぶ必要があり(最高音、最低音、または1チャンネル)、約25 msより短いセクションはマージされる — それ以下では低い音が完全な振動を達成できず、クリック音だけが聞こえる。
---
## そしてそれはポリフォニックであることが判明した
上のメロディは1つのボイスである。そうである必要はなく、その理由は私たちが読み飛ばしていたサービス・マニュアルの一文にある:
> 信号生成中、個別の信号振幅サンプルはRAMから読み出される。基本信号波形が変更された場合[...]対応する振幅サンプルは**CPUによってRAMにロードされる**。
PM5139は**1024ポイントのウェーブテーブルDDS**である。TWSは素朴な意味での三角波発生器ではない — それはユニット4上の高速RAMに対して読み出しアドレス0…1023を生成する位相アキュムレータであり、そのRAMはCPUによってCバスを介して満たされる。サイン、スクエア、ノコギリ波、任意波形はすべて同じメカニズムである:テーブルである。
そしてテーブルは正確に**出力の1周期**を保持する。したがって*倍音の和*から構築されたテーブルも、その1024ポイント内で周期的であり、コードとして再生される。アルペジオでも、変調のトリックでもない — 複数の音がフル20 Vppで同時に鳴り、それらが鳴っている間CPUは何もしていない。倍音はテーブル周波数の整数倍でなければならないため、音程は純正律になり、持続するコードにとってはどのみちそちらの方が良い調律である。
`M27512_PM5139_V20_chords.bin`はリポジトリにあり、書き込み可能な状態である — リフを、コードで、エンベロープ付きで。自分でビルドする場合、または内蔵のリフの代わりに自分のMIDIファイルを使う場合:```
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 がテーブルを構築する — power (2:3:4)、major (4:5:6)、minor (10:12:15)、dom7 (4:5:6:7)、さらに五つ。mkpoly.py はそれをメロディと一緒に空き ROM に配置し、同じ死んだメニューエントリをフックする。コードを一度ロードし、その後は再チューニングのみでメロディを演奏する。これによりコード全体が並行して移調される。E1M1 リフのすべての音がパワーコードになる — それはオリジナルでこのリフが作られているまさにそのものだ。
アーキテクチャ的にはこれは PPG Wave である:単一周期波形を走るカウンタが、そのまま DAC に入る。コードのトリックは Amiga トラッカーが使っていたものだ — コードを波形に入れて、一つのボイスが三つの音を演奏し、三つのチャンネルを使わずに済む。C64 は代わりにアルペジオを使わなければならない。SID には書き込み可能なウェーブテーブルがないからだ。
コードのために EPROM すら必要ない。同じテーブルは任意の EEPROM に収まるので、python3 mkarb.py --chords でファームウェアに手を触れずにフロントパネルから選択できる六つのコードが得られる。
二つのプレイヤーがあり、イメージはどちらか一方を搭載する。両方とも同じメニューエントリをフックするからだ:
二つの測定がその設計を形作った:
00h 44h 88h CCh)で、再構築された値はすべて四の倍数である。波形 RAM は十二ビット幅だが、バスは十ビットを駆動する — まさに ARB フォーマットが保存するもので、Philips はそこで何も無駄にしていない。RAM_PAGE はそれらしく聞こえるが、そのワードを周波数から構築する。だからハーモニーはテーブルに、メロディは周波数ワードに住む;音楽が走っている間は何もリロードされない。エミュレータは波形 RAM をモデル化していないので、ローダーは代わりに構成によって検証される:polytest.js は実際にバスに到達するものを記録し、1024 ポイントすべてを mkchord.py が生成したものと比較する。
そこに至るまでに五つの EPROM を要し、エミュレータは道のりの一部しか進めなかった:CPU とバスはモデル化するが波形 RAM はモデル化しないので、確認できるのはファームウェアが送るのと同じバイトが出ていくことだけだ。それは必要だが十分ではない。三つのことは楽器自体で決着をつけなければならなかった:
バイト順。 ポイントあたり二バイト、上位バイトが先。ファームウェア自身のダウンロードから推測すると逆の答えが出て、テーブルはノイズになった。決着をつけたのは、六つのテストパターンを載せた一つの EPROM — フラットライン、ランプ、各ポイントのバイトを交換した同じランプ、さらに三つ — と、オシロスコープでの観察だった。交換したランプがきれいな方だった。
波形の変更は十九のテレグラムであり、最初のプレイヤーが送った三つではない。重要なのは RAM を書き込みモードにする二バイト書き込みだ;それがなければ 2048 バイトがバスに出てどこにも着地しない。
出力レベル。 アッテネータは一バイト内の二つの独立した 20 dB リレーステージで、それらの ROM テーブルは文書化されていたのとは逆に読める(それらはバイパスビットである)、そしてレベル DAC は八ビットではなく七ビットだ — 80h でラップするので、「より大きい」設定の一つが無音を生んだ。それには単一イメージ内の約三十の組み合わせのマトリクスが必要で、出力周波数をテスト番号として使うことで、オシロスコープ自身の読み取りがどの組み合わせが生きているかを示す。``` 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
---
## 独自の任意波形6種

`D310_image_V20.bin` は EEPROM の全スロットを埋める — チップへの書き込みは一度やる価値がある:
| スロット | 波形 | Vpp | 用途 |
|---|---|---|---|
| 1 | sinc、8ローブ | 12.17 | 帯域制限、オーバーシュート |
| 2 | リンギング、Q≈6 | 17.81 | 整定挙動 |
| 3 | ECG | 12.80 | デモ |
| 4 | 階段波、16ステップ双極 | 20.00 | 直線性、分解能 |
| 5 | 全波整流正弦波 | 10.00 | オリジナルと同じだが、計算によるもの |
| 6 | マルチトーン、5トーン | 20.00 | 相互変調 |
重要で間違えやすい2つのポイント:
**ゼロ中心合わせは引き伸ばしに勝る。** 明らかな方法は各曲線を値域全体に引き伸ばすことだ。やってはいけない: 計測器の DC オフセットは別のアナログ経路から供給され *固定* 電圧を加算するのに対し、引き伸ばされた非対称曲線の DC 成分は *振幅に比例して* スケールする。レベルを変えるたびにオフセットを再調整しなければならなくなる。波形本来のゼロをコンバータのゼロに合わせることで失うのは 0.2~1 ビット — 元のアナログ経路がすでに寄与している 16 LSB のノイズに比べれば。実際のコストではない。
**浮動小数点でスケーリングし、丸めは一度だけ。** 先に丸めてから引き伸ばすと 1.0~1.5 量子化ステップの誤差が出る; 浮動小数点でスケーリングして一度だけ丸めれば最適な 0.5 になる。
ディレクトリには曲線ごとの識別バイト (1280 曲線バイトのチェックサム、開始値 `55h`) と、最小/最大を 6 ビット左詰めした 10 ビット値として格納する必要がある。識別バイトを間違えると計測器は **Err 8** を表示し任意波源を拒否する — 最初の実機フラッシュでまさにこれが起きた。
---
## ブラウザシミュレータ
`PM5139_Simulator.html` は単一の自己完結ファイル — ビルド手順なし、依存関係なし、ネットワーク不要。開けばオリジナルの V1.3 ファームウェアが目の前で起動する。
8051 コアは実コードを実行する。タイマ、割り込み、C バスと I²C はエミュレートされ; ディスプレイは実際の PCF8576 データストリームからデコードされ、キーは P3.3 上にパルス幅符号化された SAA3007 波形を生成する。バッテリバックアップ RAM はプリロードされ、任意波形 EEPROM は起動時に生成されファームウェア自身によってチェックされる。
コールドスタートには約 900 万命令かかるので、少し待ってほしい。
---
## リポジトリ構成```
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 と Node だけです。プロットには matplotlib、lines.py にのみ pillow と numpy が必要です。
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
### ビルド 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
### メロディを追加する```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 はイメージに一度だけパッチを適用し、二度目は拒否します — 最初からやり直したい場合は、mkv20.py で新しい V2.0 をビルドしてください。
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
### プロット```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
これらのコマンドはすべて、ドキュメントと照合できる表を出力します:```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
### 回路図を読む```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
ビルドチェーン全体は決定論的です — 以下のコマンドで ファームウェアと EEPROM イメージをバイト単位で再ビルドできます:```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
---
## 書き戻し
> **元のEPROMは保管しておいてください。** 2回読み出してダンプを比較し、
> チップは引き出しにしまっておきましょう。ここにあるものはすべて、
> それを持っている場合にのみ元に戻せます。
ファームウェアは電源投入時に占有範囲のバイトサムを検査し、
その直後のバイトと比較します。間違えると `Err 1` と無限ループになり、
計測器は起動しません。`romfix.py` が正しい値を計算して挿入します。
ここにあるすべてのビルドスクリプトはすでにそれを呼び出しています。
| バージョン | 範囲 | チェックサムバイト | 値 |
|---|---|---|---|
| V1.3 | `0000h–AC6Fh` | `AC70h` | `F2h` |
| V1.5 | `0000h–B3C9h` | `B3CAh` | `99h` |
実機で苦労して学んだ2つのこと:
- 任意波形用EEPROMは**識別バイト**を再計算する必要があります。そうしないと
起動のたびに `Err 8` が出て、ARBソースを選択できません。
- フラッシュ後にARBがおかしい場合は、イメージを疑う前にソケットの
28番ピンが正しく装着されているか確認してください。
---
## これはどの程度信頼できるのか?
検証済みと記されたものはすべて、エミュレータ上で元のルーチンを
複数のサンプル点で呼び出して確認したもので、通常はリスティングや
回路図とも照合しています。
うまくいかなかった箇所は、黙って修正するのではなく書き残しています:
- **3つのエミュレータのバグ**(`ACALL` を `AJMP` として扱う、ACフラグの欠落、
キーボード割り込みの二重化)がプロジェクト中間段階で存在していました。
影響を受けた知見はすべてその後再測定しました — ディスプレイのビットマップは
同一の結果が得られ、ストローブの割り当てはサービスマニュアルと一致し、
セクション16には2つのストローブが欠けていることが判明しました。
- **実機から一度も読み出されなかった合成NVRAMイメージ**が2つの知見を
誤らせました。その1つは「ロータリーノブは一方向にしか効かない」というものです。
修正方法は、ファームウェアに無効なNVRAMを渡し、自身の工場出荷状態を
書き込ませることでした。
- **手動で設定したRAM状態**は、計測器が決して取らない構成を生み出します。
これにより2回は誤った結論を導き、1回はコマンドテーブルへのクラッシュを
引き起こしました。
- **`core.js` は1命令を1サイクルとして数えており**、マシンサイクルではありません。
順序付けには問題ありませんが、絶対的なタイミングには誤りです — ここでの
タイミングに関する主張はMCS-51のデータシートに基づいています。
測定ではなく仮定に過ぎないものは、本文中でその旨を明記しています。
---
## 未解決の事項
- **128個の状態ビットのうち36個**は、6つの動作プロファイル外の刺激 —
セルフテスト、エラーパス、インターフェーストラフィック — を必要とします。
- **オフセット0Dh以降のNVRAMフィールド。** そこまでのレイアウトは測定済みで
(`NVRAM offset + 4Bh = RAM address`)、チェックマークも理解されています
(バイトサム、開始値 `AAh`、25バイト)。
- **`8871h` 領域の13個のサブブロックのうち、どの任意コマンドがどれに到達するか。**
直接的なトークン比較は4つしかなく、残りはビットテストで分岐しています。
- **リモートコマンドがパラメータ範囲チェックを回避できるかどうか。**
- **波形ロードルーチン**は、完全な再実装に向けた残る最も困難な依存部分です —
それがなければ出力信号はありません。
- **PM5139が10 MHzの兄弟機と同じクロックからどうやって20 MHzを作り出すのか。**
その系統はローパスが5 MHzではなく10 MHzにあることを示唆していますが、
確認にはPM5139のマニュアルが必要です。
これらの計測器を1台お持ちでしたら、2つのことが大いに助けになります:
**PM5139サービスマニュアル**と、**他のファームウェアバージョン**のダンプです
(V1.4が存在するかどうかは不明です)。
---
## 出典
- **`pm5138A_service_manual.pdf`** — 主要なハードウェア資料。176ページ、
OCR済み。本文は `pdftotext -layout` できれいに読めますが、回路図は
画像としてレンダリングする必要があります。スキャンからページ4-3から
4-28が欠落しています。
- **PM5139ユーザーマニュアル**(Fluke) — テキストレイヤーのない3言語スキャン。
3.7.4.6章に任意コマンドが記載されています。自分でOCRする価値があります —
英語部分はPDFの13〜145ページです。
- **PM5136ユーザーマニュアル** — 照合に有用です。そのエラー番号とコマンド
リストは、最小モデルにどのパラメータが欠けているかを示しており、ROM内の
パラメータ順序を独立に裏付けました。
- **3モデルすべてのデータシート** — 波形ごとの動作限界。
これらのマニュアルは第三者の文書であり、**このリポジトリには再配布されて
いません**。オンラインで見つけることができます。
---
## ライセンスと使用
2種類の素材があり、それぞれ異なる条件です — 正確な範囲は
[LICENSE](https://github.com/doctormord/philips-pm-5139-5138a-5136-firmware-project/blob/main/LICENSE) を参照してください:
- **リバースエンジニアリングの成果はMITです。** ドキュメント、ツール、
両方のエミュレータ、シンボルテーブル、注釈、生成された波形、プロット。
どのようにでも使ってください。
- **Philipsのファームウェアは我々がライセンスできるものではありません。**
ROMイメージ、工場出荷チップのダンプ、逆アセンブリのリスティング、
ブラウザシミュレータ(V1.3イメージを埋め込んでいるもの)は、Philipsの
成果物を複製または派生したものです。これらは、何十年もサポートが
終了している計測器の研究対象、相互運用性、修理、および文書化のために
ここにあります。我々自身の成果が混在している部分 — 注釈、V2.0での
修正された波形 — については、その貢献部分のみがMITです。
オリジナルのファームウェアに関する権利をお持ちで異議がある場合は、
issueを開いてください。削除されます。
これらを使用する場合は、リンクを貼っていただけると嬉しいです。誤りを
見つけた場合は、issueを開いてください — ここにあるすべての主張は、
それが依拠するアドレスまたは測定を明記しているため、反証可能である
はずです。
| 逆アセンブル | 両バージョンとも完全、約23 000行、クロスリファレンス付き |
| 注釈付きリスティング | 147の命名済みルーチン、145のヘッダコメント、3 826行の注釈 |
| ドキュメント | 35セクション、4 600行、すべての主張に出典あり |
| 信号経路 | 周波数、振幅、オフセット、AM、FM、バースト、対称性、スイープ — すべて計算し、オリジナルのコードと照合済み |
| ハードウェア | 10個のストローブすべて、Cバス、全参加者とのI²C、ポート、キーボード、ロータリーノブ、ディスプレイビットマップ |
| 状態ビット | 128個中75個について効果を文書化 |
| バージョン差分 | V1.3とV1.5は構造的に91.4 %同一、すべての変更点を命名 |
| エミュレータ | Python版1つ、JavaScript版1つ(約8 M命令/秒)、加えて単一ファイルのブラウザシミュレータ |
| 独自ファームウェア | V2.0 — 工場出荷時の欠陥を修正、チェックサムを処理、エミュレータと実機で検証済み |
| 位置 | 型番 | 機能 |
|---|
| D301 | PCB80C652 | ハードウェアI²C付き8051コア、12 MHz |
| D306 | 27512 | プログラムEPROM — V1.3は0000h–AC70hを占める |
| D310 | X28C64 | MOVXバス上の任意波形用EEPROM |
| D305 | PCF8570 | I²C上のバッテリバックアップ付き256バイトNVRAM(A0h) |
| D304-A | PCF8576 | I²C上のLCDドライバ(70h)、20バイトバッファ |
| D302-A | SAA3007 | キーボードエンコーダ、単一線上でパルス幅符号化 |
| D307 | 74HCT4514 | ストローブデコーダ — ストローブ番号はアドレスビットA8…A11 |
mkdoom.py | mkpoly.py |
|---|
| ボイス | 一つ | 同時に複数 |
| 波形 | ロードされたもの | 独自のコードテーブル |
| レベル | フロントパネルが残したまま | 明示的に設定、11.6 Vpp 実測 |
| 使用 ROM | 182 バイト | 内蔵リフ付きで 2617、MIDI トラックからで 6185 |