
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ダンプである。
| 逆アセンブル | 両バージョンとも完全、約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 |
アナログ側はシリアル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 をビルドする。すべてのアドレスをハードコードするのではなくシグネチャによって検出するため、同じスクリプトが両方のソースバージョンで動作する: