
Philips PM5139 / PM5138A / PM5136 함수 발생기의 펌웨어 리버스 엔지니어링: 측정 기기로 사용되는 8051 에뮬레이터, 문서화된 하드웨어 35개 섹션, 그리고 수정된 펌웨어 V2.0
1994년경의 20 MHz 함수 발생기를 소프트웨어로 분해한 기록: 두 개의 EPROM 덤프, 측정 도구로 사용한 8051 에뮬레이터, 그리고 모든 주장이 리스트 주소, 에뮬레이터 측정값, 또는 회로도로 뒷받침되는 35개 섹션의 문서.
그 끝에는 Philips가 출하한 결함을 수정한 펌웨어 V2.0, 우리가 직접 만든 여섯 개의 임의 파형, 그리고 원본 ROM을 명령어 단위로 실행하는 브라우저 시뮬레이터가 있다.

프로그램 EPROM에 있는 모든 파형 테이블을 바이너리에서 바로 플롯한 것. 오른쪽 아래에 있는 것이 이 프로젝트에서 가장 흥미로운 부분을 시작하게 만든 파형이다.
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 덤프였다.
| 디스어셈블리 | 두 버전 모두 완료, 약 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 — 공장 결함 수정, 체크섬 처리, 에뮬레이터와 실제 하드웨어에서 검증됨 |
| 위치 | 유형 | 기능 |
|---|---|---|
| D301 | PCB80C652 | 하드웨어 I²C가 있는 8051 코어, 12 MHz |
| D306 | 27512 | 프로그램 EPROM — V1.3은 0000h–AC70h 차지 |
| D310 | X28C64 | MOVX 버스의 임의 EEPROM |
| D305 | PCF8570 | I²C(A0h)의 배터리 백업 NVRAM 256바이트 |
| D304-A | PCF8576 | I²C(70h)의 LCD 드라이버, 20바이트 버퍼 |
| D302-A | SAA3007 | 키보드 인코더, 단일 라인에 펄스 폭으로 코딩됨 |
| D307 | 74HCT4514 | 스트로브 디코더 — 스트로브 번호는 주소 비트 A8…A11 |
아날로그 쪽은 직렬 C-버스다: 8051의 UART가 시프트 레지스터
모드로 동작하고, TXD가 클록, RXD가 데이터이며, 스트로브가 열 개의
시프트 레지스터 중 어느 것이 바이트를 래치할지 결정한다.
MOV DPH,#8nh 다음에 MOVX @DPTR,A를 실행하면 스트로브 n이
발동된다. 이 한 줄이 전체 아날로그 섹션의 열쇠다.
이 부분은 여러분 자신의 프로젝트에 훔쳐 쓸 가치가 있다.
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 편차, 버스트 횟수, 대칭성, 그리고 두 가지 스윕 특성 모두에 대해 통했다. 문서의 각 수식에는 그것이 검증된 샘플 포인트가 함께 제공된다.
세 가지 개선을 통해 실제로 생산적으로 만들었다:
**디스플레이가 아니라 버스를 보라.** 섹션 15는 상태 비트가 디스플레이 버퍼에 어떤 영향을 미치는지 측정하는데, 128개 비트 중 74개는 아무것도 하지 않는 것으로 보인다. 하지만 그중 많은 것은 디스플레이를 구동하지 않고 *아날로그 어셈블리*를 구동한다 — 그리고 그것들은 C-버스의 텔레그램으로만 보인다. `MOV SBUF,…`와 종료 `MOVX @DPTR`를 기록함으로써 문서화된 비트 수가 54개에서 75개로 늘어났다.
**RAM을 찌르지 말고 키를 누르라.** RAM 바이트를 수동으로 설정하면 계기가 결코 취하지 않는 상태가 만들어진다. 그로 인해 두 개의 잘못된 발견과 명령 테이블로의 한 번의 충돌이 발생했다. 에뮬레이트된 SAA3007을 통해 실제 키 코드를 주입하면 펌웨어가 실제로 도달하는 상태를 얻을 수 있다 — 그리고 256개 키 코드 전체에 대한 무차별 대입 스윕을 통해 어떤 키가 어떤 핸들러를 트리거하는지 밝혀졌다.
**먼저 자신의 에뮬레이터를 의심하라.** 우리 코어의 세 가지 버그가 "설명할 수 없는" 펌웨어 동작을 만들어냈다: `ACALL`이 `AJMP`로 실행됨, 보조 캐리 플래그 누락(`DA A`가 오작동하여 펌웨어가 이진수로 세는 것처럼 보임), 그리고 키보드 인터럽트 중복. 그 시기의 모든 발견은 이후 다시 측정되었다.
---
## 여기까지 온 길
**먼저 정적 분석.** 전체 opcode 테이블을 갖춘 디스어셈블러, 그다음 점프 테이블 휴리스틱을 사용한 재귀적 하강. 그 결과 30,508바이트의 코드가 생성되었고 13,637바이트가 설명되지 않은 채 남았다.
**그다음 동적 분석.** 트레이스 실행 — 콜드 스타트, 23개 전면 패널 키 전부, 노브 양방향, 모든 동작 모드, 8,600만 사이클 — 실제로 실행된 모든 주소를 표시. 정적 분석과 대조했을 때, 하강이 놓친 영역을 정확히 **하나** 발견했고, 설명되지 않은 바이트 중 10,686개는 다섯 개의 알려진 테이블 블록으로 밝혀졌다.
**그다음 회로도.** 서비스 매뉴얼의 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이다.
이것은 계산된 것이 아니라 아날로그 소스에서 샘플링되었다. 편차의 평균은 0이고, 1024개 포인트 중 2개만 10 이상 벗어난다 — 이것은 다른 파형이 아니라, 잡음이 실린 *같은* 파형이다.
### 그 테이블은 30 dB 레벨 사다리이다
깨끗한 버전은 이전 초안에서 "10개의 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만큼 어긋납니다. 이는 로그 레벨 사다리 — 진폭 또는 감쇠 테스트 패턴입니다.
진폭 컨트롤러에는 하나의 스트로브에 두 개의 시프트 레지스터가 있지만, 펌웨어는 텔레그램당 한 바이트만 전송합니다. 회로도가 이를 설명합니다: 두 개의 4094가 **QS' (핀 10)**을 통해 캐스케이드되어 있고, 핀 9는 사용되지 않습니다 — 그리고 텔레그램은 쌍으로, 약 42,000 사이클 간격으로, 쌍 사이에는 수백만 사이클의 침묵이 있습니다. 먼저 전송된 바이트가 두 번째 레지스터로 밀려 들어갑니다.
동일한 캐스케이드 패턴이 하나 이상의 시프트 레지스터가 있는 모든 어셈블리에서 나타났습니다 — 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`의 인터페이스 카드와 동일한 장치 유형 블록에 위치하지만 뱅크 비트만 다르며, 수신 버퍼와 산술 레지스터를 두 개의 10바이트 텔레그램으로 전송한다. 이는 출시되지 않은 장치를 위한 공장 진단으로 보인다.
### 임의 EEPROM에서 코드를 실행할 수 없다
임의 파형 슬롯에 코드를 넣고 그곳으로 점프한다는 명백한 아이디어는 처음부터 불가능하다. 8051은 하버드 구조이다. 명령어는 프로그램 EPROM에서 `/PSEN`을 통해 들어오고, 데이터는 임의 EEPROM에서 `/RD`를 통해 들어온다. 차단된 것이 아니라, 배선 자체가 존재하지 않는다.
### 그리고 마지막으로 주파수 인코딩
디스플레이 숫자 행은 PCF8576으로 전송되는 이미지의 `3Eh–43h`에 위치하며, 모든 위치가 하나의 세그먼트 인코딩을 공유하고, 바이트 `43h`는 10년 7과 8 사이에서 kHz에서 MHz로 전환된다. 이로부터:```
f = M · 10^(D−8) kHz
실제 계측기에서 측정한 세 개의 주파수 스테핑 시퀀스가 이것에 의해 정확히 재현된다 — 가수 2500이 25 MHz를 의미하게 되어 한계를 초과하므로 조기에 중단되는 것까지 포함해서.

왼쪽은 출하된 곡선, 오른쪽은 수정된 곡선. 왼쪽 아래는 계산된 테이블과의 편차 — 그 ±5 LSB의 띠는 샘플링된 아날로그 소스가 남긴 흔적이다.
mkv20.py는 V1.5(또는 V1.3)로부터 V2.0을 빌드한다. 주소를 하드코딩하는 대신 시그니처로 모든 주소를 찾아내므로, 동일한 스크립트가 두 소스 버전 모두에서 작동한다: