
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 덤프였다.
아날로그 쪽은 직렬 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을 빌드한다. 주소를 하드코딩하는 대신 시그니처로 모든 주소를 찾아내므로, 동일한 스크립트가 두 소스 버전 모두에서 작동한다:
*IDN?에서: PHILIPS,PM5139,0,V2.0/0000.2.0을 나타낸다.그 외의 모든 것은 손대지 않았다. 세 가지 추가적인 이상한 점이 발견되었으나 의도적으로 그대로 두었다 — 존재하지 않는 SFR에 대한 쓰기(무해하며, 두 버전 모두에서), 죽은 5Ah 블록, 그리고 테스트되지만 결코 설정되지 않는 세 개의 상태 비트. 이들을 패치해도 동작은 변하지 않고 위험만 추가된다.
M27512_PM5139_V20.bin은 정확히 이것이며 그 외에는 아무것도 아니다. 아래의 멜로디는 별도의, 선택적 빌드 단계이다.
검증됨: 에뮬레이터에서 콜드 스타트 시 V1.5와 동일한 디스플레이 버퍼와 동일한 플래그를 생성하고, 체크섬이 검증되며, 빌드는 바이트 재현 가능하다. 실제 PM5139에 플래시되어 실행된다 — 디스플레이에 2.0이 표시되고 여섯 개의 임의 슬롯 모두 작동한다.
V1.5에서 체크섬 뒤에 19,509개의 미사용 바이트가 있고, 주파수 경로가 음 주파수를 세 개의 BCD 바이트로 받기 때문에, 이 계측기는 자체 출력을 통해 음악을 연주할 수 있다.
인코딩은 즐거울 정도로 직접적이다 — decade 3, 그다음 주파수를 0.01 Hz 단위의 BCD로, 따라서 82.41 Hz는 30 82 41이다. 음당 네 바이트: 세 개는 음높이, 하나는 길이.
흥미로운 부분은 트리거다. 진단 메뉴(켜면서 LOCAL을 누르고 있기)에는 여덟 개 항목의 점프 테이블이 있지만, 메뉴 루프는 0Bh를 1에서 7까지만 센다 — 따라서 여덟 번째 항목은 도달할 수 없다. 그것은 또한 중복이다: 메뉴 시작으로 점프하는데, 그곳은 어차피 다른 두 곳에서도 도달된다.
따라서 전체 훅은 두 바이트이다:``` 5B94h table entry 8: LJMP 5B45h -> LJMP 5B62h count limit: 08h -> 09h
자체 테스트가 사라지지 않고, 테이블이 재배치되지 않으며, 죽은 메뉴 항목이 나타나지 않는다. LOCAL을 누른 상태로 유지하고, 전원을 켜고, 메뉴가 8까지 세도록 한 다음 키를 누른다.
타이밍은 MCS-51 데이터 시트에서 나온다. 두 에뮬레이터 모두 이제 명령어와 함께 머신 사이클을 세며(`mcyc`, `mcs51.CYCLES`에서), 대기 루프를 스텝 실행하면 단위당 **1009 µs**가 측정된다 — 140 BPM에서 16분음표당 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-bus를 통해 채운다. 사인, 사각, 톱니, 임의 파형 모두 같은 메커니즘이다: 테이블.
그리고 그 테이블은 정확히 **출력의 한 주기**를 담는다. 따라서 *고조파의 합*으로 만들어진 테이블도 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) 재구성된 모든 값은 4의
배수다. 파형 RAM은 12비트 폭이지만 버스는 10비트를 구동한다 — 정확히 ARB
형식이 저장하는 것과 같으므로 Philips는 거기서 아무것도 낭비하지 않았다.RAM_PAGE는 그렇게 들리지만
주파수로부터 그 워드를 구성한다. 따라서 화성은 테이블에, 멜로디는 주파수
워드에 산다; 음악이 실행되는 동안에는 아무것도 재로드되지 않는다.에뮬레이터는 파형 RAM을 모델링하지 않으므로, 로더는 대신 구성에 의해
검증된다: polytest.js는 실제로 버스에 도달하는 것을 기록하고 1024개
포인트 전부를 mkchord.py가 생성한 것과 비교한다.
거기까지 도달하는 데 EPROM 다섯 개가 걸렸고, 에뮬레이터는 우리를 부분적으로만 데려다줄 수 있었다: CPU와 버스는 모델링하지만 파형 RAM은 모델링하지 않으므로, 확인할 수 있는 것은 펌웨어가 보내는 것과 같은 바이트가 나간다는 것뿐이다. 그것은 필요조건이지 충분조건이 아니다. 세 가지는 악기 자체에서 해결해야 했다:
바이트 순서. 포인트당 두 바이트, 상위 바이트가 먼저. 펌웨어 자체의 다운로드에서 추론하면 반대 답이 나왔고 테이블은 노이즈로 나왔다. 이를 해결한 것은 여섯 개의 테스트 패턴 — 평탄한 선, 램프, 각 포인트의 바이트를 교환한 같은 램프, 그리고 세 개 더 — 을 담은 EPROM 하나와 오실로스코프 관찰이었다. 교환된 램프가 깨끗한 것이었다.
파형 변경은 열아홉 개의 텔레그램이지, 첫 번째 플레이어가 보낸 세 개가 아니다. 중요한 것은 RAM을 쓰기 모드로 전환하는 2바이트 쓰기다; 그것 없이는 2048 바이트가 버스로 나가지만 아무 데도 도달하지 않는다.
출력 레벨. 감쇠기는 한 바이트 안의 두 개의 별도 20 dB 릴레이 스테이지이고, 그에 대한 ROM 테이블은 문서화된 방식과 반대로 읽히며(그것들은 바이패스 비트다), 레벨 DAC는 8비트가 아니라 7비트다 — 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
---
## 우리만의 여섯 가지 임의 파형

`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 | 상호 변조 |
중요하면서도 쉽게 틀릴 수 있는 두 가지 세부 사항:
**제로 중심 맞추기가 늘리기보다 낫다.** 당연한 방법은 각 곡선을 전체
값 범위에 걸쳐 늘리는 것입니다. 하지 마세요: 계측기의 DC 오프셋은
별도의 아날로그 경로에서 발생하여 *고정된* 전압을 더하는 반면, 늘린
비대칭 곡선의 DC 성분은 *진폭에 따라* 조정됩니다. 레벨을 변경할 때마다
오프셋을 다시 조정해야 합니다. 파형의 자연스러운 영점을 컨버터의
영점에 맞추면 0.2~1비트의 비용이 듭니다 — 원래 아날로그 경로가 이미
기여하는 16 LSB의 노이즈에 비하면 실질적인 비용이 아닙니다.
**부동 소수점으로 스케일링하고 한 번만 반올림하세요.** 먼저 반올림한
후 늘리면 1.0~1.5 양자화 단계의 오차가 발생하지만, float로 스케일링하고
한 번 반올림하면 최적인 0.5가 됩니다.
디렉터리에는 곡선별 식별 바이트(1280개 곡선 바이트의 체크섬, 시작 값
`55h`)와 최소/최대값이 6비트만큼 왼쪽 정렬된 10비트 값으로 필요합니다.
식별 바이트를 잘못 입력하면 계측기가 **Err 8**을 표시하고 임의 소스를
거부합니다 — 첫 번째 실제 플래시에서 정확히 그런 일이 일어났습니다.
---
## 브라우저 시뮬레이터
`PM5139_Simulator.html`은 단일 자체 포함 파일입니다 — 빌드 단계도,
의존성도, 네트워크도 필요 없습니다. 열면 원래 V1.3 펌웨어가 눈앞에서
부팅됩니다.
8051 코어는 실제 코드를 실행합니다. 타이머, 인터럽트, C-bus 및 I²C가
에뮬레이트되며, 디스플레이는 실제 PCF8576 데이터 스트림에서 디코딩되고,
키는 P3.3에서 펄스 폭 코딩된 SAA3007 파형을 생성합니다. 배터리 백업
RAM은 미리 로드되어 있고 임의 EEPROM은 시작 시 생성되어 펌웨어 자체에서
검사됩니다.
콜드 스타트는 약 900만 개의 명령어가 소요되므로 1초 정도 기다려 주세요.
---
## 저장소 레이아웃```
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을 보관하세요.** 두 번 읽고, 덤프를 비교한 뒤,
> 칩을 서랍에 넣어두세요. 여기 있는 모든 것은 원본이 남아 있을
> 때만 되돌릴 수 있습니다.
펌웨어는 전원 투입 시 점유된 범위에 대한 바이트 합을 검사하여 바로 다음 바이트와 비교합니다. 이 값이 틀리면 `Err 1`과 함께 무한 루프에 빠지며 기기는 부팅되지 않습니다. `romfix.py`가 올바른 값을 계산하여 삽입하며, 여기 있는 모든 빌드 스크립트는 이미 이를 호출합니다.
| 버전 | 범위 | 체크섬 바이트 | 값 |
|---|---|---|---|
| V1.3 | `0000h–AC6Fh` | `AC70h` | `F2h` |
| V1.5 | `0000h–B3C9h` | `B3CAh` | `99h` |
실제 하드웨어에서 어렵게 배운 두 가지:
- 임의 EEPROM은 **식별 바이트**를 다시 계산해야 합니다. 그렇지 않으면 시작할 때마다 `Err 8`이 발생하고 ARB 소스를 선택할 수 없습니다.
- 플래싱 후 ARB가 이상하게 동작하면 이미지를 의심하기 전에 소켓의 28번 핀이 제대로 꽂혀 있는지 확인하세요.
---
## 이것은 얼마나 신뢰할 수 있는가?
검증됨으로 표시된 모든 것은 에뮬레이터에서 원래 루틴을 여러 샘플 지점에 걸쳐 호출하여 확인했으며, 보통 리스팅이나 회로도와도 교차 검증했습니다.
잘못된 부분은 조용히 고치는 대신 기록해 두었습니다:
- **세 가지 에뮬레이터 버그**(`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를 만드는지.** 체인상으로는 저역 통과가 5 MHz 대신 10 MHz에 위치함을 암시하지만, 이를 확인하려면 PM5139 매뉴얼이 필요합니다.
이 기기 중 하나를 소유하고 계시다면 두 가지가 큰 도움이 될 것입니다: **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자 문서이며 **이 저장소에 재배포되지 않습니다**. 온라인에서 찾을 수 있습니다.
---
## 라이선스 및 사용
두 종류의 자료가 서로 다른 조건으로 제공됩니다 — 정확한 범위는 [LICENSE](https://github.com/doctormord/philips-pm-5139-5138a-5136-firmware-project/blob/main/LICENSE)를 참조하세요:
- **리버스 엔지니어링 작업은 MIT입니다.** 문서, 도구, 두 에뮬레이터, 심볼 테이블, 주석, 생성된 파형 및 플롯. 원하는 대로 사용하세요.
- **Philips 펌웨어는 우리가 라이선스를 부여할 수 있는 것이 아닙니다.** ROM 이미지, 팩토리 칩 덤프, 디스어셈블리 리스팅, 브라우저 시뮬레이터(V1.3 이미지를 내장)는 Philips의 작업을 복제하거나 파생한 것입니다. 이들은 수십 년간 지원이 중단된 기기의 상호운용성, 수리 및 문서화를 위한 연구 대상으로서 여기에 있습니다. 우리 자체 작업이 섞여 있는 부분 — 주석, V2.0의 수정된 파형 — 은 그 기여분만 MIT입니다.
원본 펌웨어에 대한 권리를 보유하고 있고 이의가 있으시면 이슈를 열어 주시면 삭제하겠습니다.
이 중 어떤 것이든 사용하신다면 출처 링크를 남겨 주시면 감사하겠습니다. 실수를 발견하시면 이슈를 열어 주세요 — 여기 있는 모든 주장은 근거가 되는 주소나 측정을 명시하고 있으므로 반증 가능해야 합니다.
| 디스어셈블리 | 두 버전 모두 완료, 약 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 |
mkdoom.py | mkpoly.py |
|---|
| 보이스 | 하나 | 동시에 여러 개 |
| 파형 | 로드된 것 그대로 | 자체 코드 테이블 |
| 레벨 | 전면 패널이 남긴 대로 | 명시적으로 설정, 11.6 Vpp 측정 |
| 사용된 ROM | 182 바이트 | 내장 리프 포함 2617, MIDI 트랙에서 6185 |