Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
工具/GitHubGitHub/doctormord/philips-pm-5139-5138a-5136-firmware-project
嵌入式系统安全静态分析动态分析 (沙盒)逆向工程硬件安全二进制分析论文与研究学习与教育固件分析

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享
GitHubdoctormord/philips-pm-5139-5138a-5136-firmware-project

Philips-PM-5139-5138A-5136-Firmware-Project

飞利浦 PM5139 / PM5138A / PM5136 函数发生器的固件逆向工程:将 8051 仿真器用作测量仪器、35 个章节的硬件文档,以及修正后的固件 V2.0

查看仓库
2131天前尚未审核

Philips PM5139 — 固件逆向工程

一台约 1994 年的 20 MHz 函数发生器,在软件层面被拆解:两份 EPROM 转储、一个被当作测量仪器使用的 8051 模拟器,以及 35 节文档,其中每一条论断都有清单地址、模拟器测量结果或原理图作为依据。

最终成果是一个 固件 V2.0,修复了 Philips 出厂时的一个缺陷, 六个我们自己的任意波形,以及一个逐条指令运行原始 ROM 的浏览器模拟器。

V1.3 ROM 中的所有波形表

程序 EPROM 中的每一个波形表,直接从二进制中绘制出来。右下角那个 开启了本项目最有趣的部分。


目录

  • 这是什么
  • 结果一览
  • 仪器
  • 方法:模拟器即测量仪器
  • 走到这里的路
  • 精彩之处
  • 固件 V2.0 — 新在哪里
  • 彩蛋
  • 然后发现它竟然是复音的
  • 六个我们自己的任意波形
  • 浏览器模拟器
  • 仓库结构
  • 使用工具
  • 复现一切
  • 刷写回去
  • 这有多可靠?
  • 仍未解决
  • 来源

这是什么

Philips PM5139 是三仪器系列(PM5136 / PM5138A / PM5139)中的 20 MHz 顶级型号。 内部有一块 PCB80C652 —— 带硬件 I²C 的 8051 内核 —— 一片 27512 程序 EPROM, 以及挂在串行总线上的六个模拟组件。

没有 PM5139 的维修手册。自 2010 年以来人们一直在论坛上寻找它。 存在的是其 10 MHz 姊妹型号 PM5138A 的手册,内部几乎完全相同。

所以本项目从另一端开始:转储 EPROM,弄清代码在做什么, 直到对仪器理解得足够透彻,能够修改它。

有两个固件版本可用,V1.3 和 V1.5,都是 64 KiB 的 M27512 转储。


结果一览


仪器

模拟侧是一条串行 C 总线:8051 的 UART 运行在移位寄存器模式, TXD 是时钟,RXD 是数据,一个选通决定十个移位寄存器中哪一个锁存字节。 MOV DPH,#8nh 后跟 MOVX @DPTR,A 触发选通 n。 这一行就是整个模拟部分的关键。


方法:模拟器即测量仪器

这是值得你为自己的项目借鉴的部分。

用肉眼阅读 44 KB 的 8051 二进制文件大概只能让你走完三分之一。 其余的一切都来自运行原始代码并观察会掉出什么:```python

What formula turns the entered amplitude into the byte on the bus?

Don't read the routine. Call it.

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

root@kitploit:~
改变输入,读取输出,对照假设进行验证。这对频率、幅度、偏移、AM 深度、FM 偏差、突发计数、对称性以及两种扫描特性都有效。文档中的每个公式都附带了其验证所用的采样点。

三项改进使其真正富有成效:

**观察总线,而非显示器。** 第 15 节测量了状态位对显示缓冲区的影响,128 位中有 74 位似乎不起作用。但其中许多并不驱动显示器,它们驱动的是*模拟组件*——而这些只能作为 C 总线上的电报可见。记录 `MOV SBUF,…` 和终止的 `MOVX @DPTR` 将已记录位的数量从 54 提升到了 75。

**按键,而非直接写 RAM。** 手动设置 RAM 字节会产生仪器从未进入的状态。这让我们付出了两个错误发现和一次撞入命令表的代价。通过模拟的 SAA3007 注入真实键码,可以得到固件实际到达的状态——正是对所有 256 个键码的暴力扫描揭示了哪个键触发哪个处理程序。

**首先怀疑你自己的模拟器。** 我们核心中的三个 bug 产生了“无法解释的”固件行为:`ACALL` 被执行为 `AJMP`,缺少辅助进位标志(因此 `DA A` 行为异常,固件看起来像是在用二进制计数),以及键盘中断被加倍。那个时期的所有发现之后都重新进行了测量。

---

## 走到这里的路

**先静态。** 一个带有完整操作码表的反汇编器,然后使用跳转表启发式进行递归下降。这产生了 30 508 字节的代码,并留下 13 637 字节无法解释。

**然后动态。** 一次跟踪运行——冷启动、全部 23 个前面板按键、旋钮的两个方向、每种操作模式、8600 万周期——标记每个实际执行的地址。与静态分析对照,它恰好发现了下降分析遗漏的**一个**区域,而 10 686 个无法解释的字节中有 5 个已知表块。

**然后是原理图。** 服务手册的 OCR 对原理图毫无用处,但 400 dpi 的页面图像非常出色。切成重叠的图块后,它们可读至引脚编号。六张图纸就是这样读出的——而在五条平行走线相距 90 像素的地方,目测被一个从位图中提取线段的脚本(`lines.py`)所取代。

**然后是两个被拔出的芯片。** 一个标有“SINUS 1.1”的 27C64 和一个 X28C64 被读出。两者都被放入原理图中,其内容被解码。

**然后是版本差异。** 对两个 ROM 进行标记化(使用相对跳转距离而非绝对目标),并对它们运行 `SequenceMatcher`,得到一个能在代码移动后仍然存活的地址映射——这就是 V1.3 符号被带到 V1.5 上的方式。

---

## 精彩之处

### Philips 交付了一个有噪声的波形

三个内置任意曲线位于 `A047h`、`A447h` 和 `A847h`。第三个的形状与 ROM 中已经以计算形式存在的表相同——但有 **563 次方向变化,而后者只有 13 次**,标准差为 4.1 LSB。

它是从模拟源采样而来,而非计算得出。偏差的均值为零,1024 个点中只有两个偏差超过 10——这不是不同的波形,而是*同一个*波形上带有噪声。

### 那个表是一个 30 dB 电平阶梯

干净版本在早期草稿中被描述为“一个带有十个 AM 深度的正弦波”,这是对图表的目测解读,而不是代码所说的内容。经过计算,1024 个点分成十个正弦弧,其跨度是```
255  171  120  80  56  38  26  17  12  8

一个公比为 0.681 = 10^(−1/6) 的等比数列,即每步 3.33 dB,总共 30.1 dB。减半模型最多偏差 56,3 dB 模型偏差 10。这是一个对数电平阶梯——一种幅度或衰减测试模式。

STR9 以两个 1 字节电报发送 16 位

幅度控制器在同一个选通信号上有两个移位寄存器,但固件每次电报只发送一个字节。原理图解释了这一点:两个 4094 通过 **QS'(引脚 10)**级联,引脚 9 未使用——而且电报成对出现,相隔约 42 000 个周期,成对之间有数百万个周期的静默。先发送的字节会被推入第二个寄存器。

同样的级联模式出现在每一个拥有多个移位寄存器的组件上——包括一个案例,其中链路通过一条名为 E 的线跨越了组件边界。

衰减器不是计算出来的

STR9 电报中的五个位直接驱动继电器:S1 切换直流发生器量程,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 中的目录显示为六个。算术给出了结论:``` 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

root@kitploit:~
固件实测读取范围是 `0100h–1EFFh`——六条曲线,精确到字节。该仪器是用小芯片构建的。

### 与不存在的设备通信的死代码

`9AFFh` 处的 186 字节与地址 `5Ah` 进行 I²C 通信——该地址在其他任何地方都未出现。在**两个**固件版本中,都没有跳转指向它。它与 `5Eh` 处的接口卡位于同一设备类型块中,只是 bank 位不同,并且它以两条十字节电报发送接收缓冲区和算术寄存器。这看起来像是针对从未出货的设备的工厂诊断程序。

### 你无法从任意 EEPROM 执行代码

一个显而易见的想法——将代码放入任意波形槽并跳转到它——从一开始就行不通。8051 是哈佛架构:指令通过 `/PSEN` 从程序 EPROM 取指,数据通过 `/RD` 从任意 EEPROM 读取。这不是被阻止了;只是线路根本不存在。

### 最后是频率编码

显示数字行位于发送到 PCF8576 的图像中的 `3Eh–43h` 处,所有位置共享同一种段编码,而字节 `43h` 在第 7 和第 8 个十进制位之间从 kHz 切换到 MHz。由此:```
f = M · 10^(D−8) kHz

在真实仪器上测得的三段频率步进序列被此完全复现——包括那段因尾数 2500 意味着 25 MHz 超出限制而提前停止的序列。


固件 V2.0 — 新特性

任意曲线 3:V1.5 与 V2.0 对比

左侧为出厂曲线,右侧为修正后的曲线。左下角是与计算表的偏差——那条 ±5 LSB 的带是采样模拟源留下的痕迹。

mkv20.py 从 V1.5(或 V1.3)构建 V2.0。它通过签名而非硬编码来查找每个地址,因此同一脚本可同时适用于两个源版本:

  1. 任意曲线 3 被替换为干净的计算表。两者均为 1024 字节的 8 位数据,因此替换在大小上是中性的,且不触及任何代码。1024 字节中有 863 字节发生变化。
  2. 任意曲线 2 被替换为对数啁啾(1 → 40 个周期)。它与曲线 1 恰好相差两个字节——多了一个针状脉冲——因此是冗余的。
  3. 版本标识位于 *IDN? 中:PHILIPS,PM5139,0,V2.0/0000。
  4. 显示屏上的版本:复位序列写入两个显示单元,这些字节现在以测量段编码拼出 2.0。
  5. 校验和重新计算并存储在固件期望的位置。

其余一切均未改动。还发现了另外三处异常,但有意保留——对不存在的 SFR 的写入(无害,两个版本中均有)、已失效的 5Ah 块,以及三个被测试但从未置位的状态位。修补它们不会改变任何行为,只会增加风险。

M27512_PM5139_V20.bin 正是此内容,别无其他。下面的旋律是一个独立的、可选的构建步骤。

已验证: 在模拟器中冷启动产生与 V1.5 相同的显示缓冲区和相同的标志,校验和验证通过,且构建是字节可复现的。它已被烧录并在真实的 PM5139 上运行——显示屏显示 2.0,且全部六个任意槽位均正常工作。


彩蛋

由于 V1.5 中校验和之后有 19 509 个未使用字节,且频率路径将音符频率作为三个 BCD 字节接收,该仪器可以通过自身输出播放音乐。

编码方式令人愉快地直接——第 3 位十进制,然后是以 0.01 Hz 为单位的 BCD 频率,因此 82.41 Hz 为 30 82 41。每个音符四个字节:三个用于音高,一个用于时长。

有趣的部分是触发机制。诊断菜单(开机时按住 LOCAL)有一个包含八个条目的跳转表,但菜单循环仅从 1 计数到 7 的 0Bh——因此第八个条目不可达。它也是冗余的:它跳转到菜单起始处,而该处本来就可以从另外两个地方到达。

因此整个钩子只有两个字节:``` 5B94h table entry 8: LJMP 5B45h -> LJMP 5B62h count limit: 08h -> 09h

root@kitploit:~
没有丢失自检,没有表格被移动,也没有出现失效的菜单项。按住 LOCAL,打开开关,让菜单计数到 8,按一个键。

时序来自 MCS-51 数据手册。两个模拟器现在都在指令旁计数机器周期(`mcyc`,来自 `mcs51.CYCLES`),步进等待循环测得每单位 **1009 µs** —— 在 140 BPM 下每十六分音符 106.95 ms,偏离目标 0.2 %。这个数字以前是手工计算的 1006 µs,漏掉了两条指令。

`mkdoom.py` 也可以转换 MIDI 文件。必须选择一个声部(最高音、最低音或某一个通道),并且短于约 25 ms 的片段会被合并 —— 低于这个长度,低音无法完成一个完整振荡,你只会听到一声咔嗒。

---

## 然后它竟然是复音的

上面的旋律是一个声部。它不必如此,原因在于我们之前读过却忽略的服务手册中的一句话:

> 在信号生成期间,不同的信号幅度采样从 RAM 中读出。如果基本信号波形被改变 [...] 相应的幅度采样由 **CPU 加载到 RAM 中**。

PM5139 是一个 **1024 点波表 DDS**。TWS 在任何朴素意义上都不是三角波发生器 —— 它是一个相位累加器,为单元 4 上的快速 RAM 产生 0…1023 的读取地址,而该 RAM 由 CPU 通过 C 总线填充。正弦、方波、锯齿波和任意波形都是同一机制:一张表。

而这张表恰好保存**输出信号的一个周期**。因此,由*谐波之和*构建的表在其 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 riff 的每个音符 都变成一个强力和弦——这正是原曲中该 riff 的构成。

从架构上讲,这是一台 PPG Wave:一个计数器遍历单周期波形,直接送入 DAC。 和弦技巧正是 Amiga 追踪器所用的——把和弦放进波形里,这样一个声部就能演奏 三个音符,而不必用三个通道。C64 则必须改用琶音,因为 SID 没有可写的波表。

你甚至不需要 EPROM 来存放和弦。同样的表也适用于任意 EEPROM,因此 python3 mkarb.py --chords 可以让你从前面板选择六个和弦,而固件无需改动。

有两个播放器,一个镜像只携带其中一个,因为两者挂接的是同一个菜单项:

有两个测量结果塑造了这一设计:

  • 下载格式每个点携带十位,而非十二位:只会出现四种不同的低字节 (00h 44h 88h CCh),且每个重建值都是四的倍数。波形 RAM 是十二位宽, 但总线只驱动十位——正是 ARB 格式所存储的内容,所以 Philips 在那里没有浪费 任何东西。
  • 完整表重载需要 32 到 40 毫秒,且输出静音,并且没有第二个缓冲页—— 位于 1D62h 的 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

root@kitploit:~
---

## 我们自己的六种任意波形

![六个任意波形槽位](https://assets.kitploit.com/production/public/readmes/54732/9fa22b75c34e4aaa83df1b2afd284353e019cb65f181a83e5cdeceb57ea32937/4b15ae02ae1bec49f3d1b6ab79c480cd3a73b10818c41a8b04be57ab6f691ef7-display-v1.webp)

`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 | 互调 |

两个重要且容易出错的细节:

**零居中优于拉伸。** 显而易见的做法是将每条曲线拉伸到整个值范围。不要这样做:仪器的直流偏移来自独立的模拟通路,并叠加一个*固定*电压,而拉伸后的非对称曲线的直流分量会*随幅度*缩放。每次改变电平时你都得重新调整偏移。将波形的自然零点放在转换器的零点上,代价是 0.2 到 1 位——相对于原始模拟通路已经贡献的 16 LSB 噪声而言。这不是真正的代价。

**在浮点中缩放,只舍入一次。** 先舍入再拉伸会带来 1.0–1.5 个量化步长的误差;在浮点中缩放并只舍入一次则能达到最优的 0.5。

该目录需要为每条曲线提供一个标识字节(1280 个曲线字节的校验和,起始值 `55h`),以及以 10 位值左对齐六位的 min/max。标识字节弄错,仪器会显示 **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。

查看固件```bash

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

root@kitploit:~
### 构建 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

构建任意 EEPROM```bash

python3 waveforms.py # what the generators produce python3 mkarb.py # -> D310_image_V20.bin python3 plot_arb.py # -> PM5139_ARB_V20.png

root@kitploit:~
### 添加旋律```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。

播放和弦```bash

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

root@kitploit:~
### 图表```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

root@kitploit:~
### 阅读原理图```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

root@kitploit:~
---

## 刷回固件

> **保留你的原始 EPROM。** 读取两次,比对转储,把芯片放进抽屉里。
> 这里的一切只有在你还拥有它的情况下才可逆。

固件在开机时对已占用范围进行字节和校验,并与紧随其后的字节进行比较。如果出错,你会得到 `Err 1` 和无限循环——仪器无法启动。`romfix.py` 会计算并插入正确的值;这里的每个构建脚本都已经调用了它。

| 版本 | 范围 | 校验和字节 | 值 |
|---|---|---|---|
| V1.3 | `0000h–AC6Fh` | `AC70h` | `F2h` |
| V1.5 | `0000h–B3C9h` | `B3CAh` | `99h` |

在真实硬件上通过惨痛教训学到的两件事:

- 任意 EEPROM 需要重新计算其**标识字节**,否则每次启动都会得到 `Err 8`,并且无法选择 ARB 源。
- 如果刷写后 ARB 行为异常,在怀疑镜像之前,先检查插座的第 28 脚是否插好。

---

## 这有多可靠?

所有标记为已验证的内容,都是通过在模拟器中调用原始例程在多个采样点上确认的,通常还会与清单或原理图进行交叉核对。

出错的地方,会被记录下来,而不是悄悄修复:

- **三个模拟器 bug**(`ACALL` 被当作 `AJMP`、缺少 AC 标志、键盘中断被加倍)在项目中期阶段一直存在。所有受影响的发现之后都重新测量过——显示位图结果一致,选通分配与服务手册相符,并且第 16 节被发现缺少两个选通。
- **一个合成 NVRAM 镜像**从未从真实仪器中读出,它伪造了两项发现,包括“旋转旋钮只能在一个方向上工作”。修复方法是给固件一个无效的 NVRAM,让它写入自己的出厂状态。
- **手动设置的 RAM 状态**会产生仪器永远不会采用的配置。这曾两次导致错误结论,一次导致崩溃进入命令表。
- **`core.js` 每个周期计数一条指令**,而不是机器周期。对于排序来说没问题,但对于绝对时序来说是错误的——这里的时序声明来自 MCS-51 数据手册。

任何属于假设而非测量的内容,都会在文中说明。

---

## 仍未解决

- **128 个状态位中的 36 个**需要六种操作配置文件之外的激励——自检、错误路径、接口通信。
- **从偏移 0Dh 开始的 NVRAM 字段。** 到那里的布局已经测量(`NVRAM offset + 4Bh = RAM address`),校验标记已经理解(字节和,起始值 `AAh`,25 字节)。
- **在 `8871h` 区域中,哪条任意命令到达 13 个子块中的哪一个。** 只存在四个直接令牌比较;其余都根据位测试分支。
- **远程命令是否可以绕过参数范围检查。**
- **波形加载例程**是完整重新实现中剩余的最难依赖——没有它们就没有输出信号。
- **PM5139 如何从与其 10 MHz 兄弟型号相同的时钟产生 20 MHz。** 该链路暗示其低通滤波器位于 10 MHz 而不是 5 MHz,但这需要 PM5139 手册来确认。

如果你拥有其中一台仪器,有两件事会很有帮助:一份 **PM5139 服务手册**,以及来自**其他固件版本**的转储(V1.4 可能存在,也可能不存在)。

---

## 来源

- **`pm5138A_service_manual.pdf`** —— 主要硬件来源。176 页,已 OCR;正文用 `pdftotext -layout` 读取很干净,原理图必须渲染为图像。扫描件中缺少第 4-3 到 4-28 页。
- **PM5139 用户手册**(Fluke)—— 三语扫描件,没有文本层;第 3.7.4.6 节记录了任意命令。值得自己 OCR——英文部分位于 PDF 第 13–145 页。
- **PM5136 用户手册** —— 作为交叉检查很有用:其错误编号和命令列表显示了最小型号缺少哪些参数,这独立确认了 ROM 中的参数顺序。
- **三种型号的数据手册** —— 每种波形的工作限制。

这些手册是第三方文档,**不在本仓库中重新分发**。它们可以在网上找到。

---

## 许可与使用

两类材料,适用不同条款——确切范围见 [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 版,一个 JavaScript 版(约 8 M 指令/秒),外加一个单文件浏览器模拟器
我们自己的固件V2.0 —— 修复了一个出厂缺陷,处理了校验和,在模拟器和真实硬件上验证通过
位置类型功能
D301PCB80C652带硬件 I²C 的 8051 内核,12 MHz
D30627512程序 EPROM —— V1.3 占用 0000h–AC70h
D310X28C64MOVX 总线上的任意 EEPROM
D305PCF8570I²C 上 256 字节电池后备 NVRAM(A0h)
D304-APCF8576I²C 上的 LCD 驱动器(70h),20 字节缓冲
D302-ASAA3007键盘编码器,单线上脉冲宽度编码
D30774HCT4514选通解码器 —— 选通编号即地址位 A8…A11
mkdoom.pymkpoly.py
声部一个同时多个
波形已加载的内容自己的和弦表
电平保持前面板设置显式设置,实测 11.6 Vpp
使用的 ROM182 字节内置 riff 为 2617,来自 MIDI 轨道为 6185