From 97c1000d8b4121d52751938bb3d8e98b4509b1c2 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Fri, 20 Feb 2026 10:56:59 -0700 Subject: [PATCH] Add stock firmware dump, 8051 disassembler, and analysis notes - stock_firmware.bin: 15KB dump from working device (v2.13) - disasm8051.py / v2: Custom 8051 disassemblers for FX2LP firmware analysis, used to trace init block loading and I2C sequences - STARTUP_DISASSEMBLY.md: Annotated startup sequence disassembly - TODO: Notes on stock vs custom firmware BCM4500 init differences --- TODO | 8 + firmware-dump/STARTUP_DISASSEMBLY.md | 604 ++++++++++++++++++++++++ firmware-dump/disasm8051.py | 660 +++++++++++++++++++++++++++ firmware-dump/disasm8051_v2.py | 660 +++++++++++++++++++++++++++ firmware-dump/stock_firmware.bin | Bin 0 -> 15360 bytes 5 files changed, 1932 insertions(+) create mode 100644 TODO create mode 100644 firmware-dump/STARTUP_DISASSEMBLY.md create mode 100644 firmware-dump/disasm8051.py create mode 100644 firmware-dump/disasm8051_v2.py create mode 100644 firmware-dump/stock_firmware.bin diff --git a/TODO b/TODO new file mode 100644 index 0000000..7bc27f4 --- /dev/null +++ b/TODO @@ -0,0 +1,8 @@ + Both stock and our custom firmware report the same status + progression (0x00 → 0x03 → 0x07), but the stock firmware's + BCM4500 initialization actually works (SNR raw ~64000) while + the custom firmware's returns all zeros. The custom + firmware's bcm4500_boot() silently fails somewhere in the + I2C init block writes — it sets BM_STARTED | BM_FW_LOADED + but the hardware isn't actually running. This is a firmware + bug that needs investigation. diff --git a/firmware-dump/STARTUP_DISASSEMBLY.md b/firmware-dump/STARTUP_DISASSEMBLY.md new file mode 100644 index 0000000..776fba6 --- /dev/null +++ b/firmware-dump/STARTUP_DISASSEMBLY.md @@ -0,0 +1,604 @@ +# Genpix SkyWalker-1 Firmware Startup Disassembly + +**Binary**: `skywalker1_eeprom_full64k.bin` (65536 bytes, RAM image after EEPROM boot) +**CPU**: FX2LP (CY7C68013A) — 8051 core @ 48MHz +**Firmware region**: 0x0000-0x24FF (9472 bytes) +**Compiler**: Keil C51 +**USB**: VID=0x09C0 (Cypress), PID=0x0203 + +--- + +## Phase 1: Reset Vector (0x0000) + +``` +0000: 02 18 8D LJMP 0x188D ; -> Keil C51 startup +``` + +## Phase 2: Keil C51 Startup (0x188D) + +### IDATA Clear +``` +188D: 78 7F MOV R0,#7Fh ; Start at IDATA address 0x7F +188F: E4 CLR A ; A = 0 +1890: F6 MOV @R0,A ; Clear IDATA[R0] +1891: D8 FD DJNZ R0,1890h ; Decrement R0, loop until 0 + ; Clears IDATA 0x7F..0x01 (127 bytes) +``` + +### Stack Pointer +``` +1893: 75 81 72 MOV SP,#72h ; Stack pointer = 0x72 + ; Stack grows up: 0x73..0x7F = 13 bytes +``` + +### XDATA Initialization +``` +1896: 02 18 D4 LJMP 18D4h ; Jump to init table interpreter + +; Init table interpreter at 0x18D4 (Keil standard STARTUP.A51): +; Reads structured init records from CODE space +; Each record has: +; - Length byte (bits 5:0 = count, bits 7:6 = mode) +; - Mode 00: IDATA/XDATA fill (address + data pairs) +; - Mode 01: Bit initialization +; - Mode 10: XDATA block copy (source addr + destination + data) +; - Zero byte terminates the table +; +; When table exhausted: +1899: 02 09 A7 LJMP 09A7h ; -> main() +``` + +## Phase 3: main() (0x09A7) + +### Clear Global Variables +``` +09A7: E4 CLR A ; A = 0 +09A8: F5 2D MOV 2Dh,A ; Clear counter/state variables +09AA: F5 2C MOV 2Ch,A ; 4-byte counter at 0x2A-0x2D +09AC: F5 2B MOV 2Bh,A +09AE: F5 2A MOV 2Ah,A +09B0: F5 35 MOV 35h,A ; 4-byte counter at 0x32-0x35 +09B2: F5 34 MOV 34h,A +09B4: F5 33 MOV 33h,A +09B6: F5 32 MOV 32h,A +09B8: C2 03 CLR bit_03h ; Clear SUSPEND-pending flag +09BA: C2 01 CLR bit_01h ; Clear SETUP-pending flag +``` + +### Call hw_init() +``` +09BC: 12 13 C3 LCALL 13C3h ; -> hw_init() +``` + +## Phase 4: hw_init() (0x13C3) + +### CPU Configuration +``` +13C3: MOV DPTR,#E605h ; REVCTL (FX2LP revision control) +13C6: MOVX A,@DPTR ; Read current value +13C7: ANL A,#FDh ; Clear bit 1 (NOAUTOARM) +13C9: MOVX @DPTR,A ; REVCTL &= ~0x02 + +13CE: MOV DPTR,#E600h ; CPUCS +13D1: MOVX A,@DPTR +13D2: ANL A,#E5h ; Clear bits 4,3,1 (clock bits) +13D4: ORL A,#10h ; Set bit 4 +13D6: MOVX @DPTR,A ; CPUCS = 48MHz clock mode + ; NOP x3 (sync delay) +``` + +### Interface Configuration +``` +13DA: MOV DPTR,#E601h ; IFCONFIG +13DD: MOV A,#CAh ; 0xCA = 1100_1010: + ; IFCLKSRC=1 (internal) + ; 3048MHZ=1 (48MHz) + ; IFCLKOE=0 (don't output IFCLK) + ; IFCLKPOL=0 + ; ASYNC=1 (async GPIF) + ; GSTATE=0 + ; IFCFG=10 (GPIF mode) +13DF: MOVX @DPTR,A +``` + +### GPIF Waveform Init +``` +13E3: MOV DPTR,#E6F5h ; Undocumented register +13E6: MOV A,#FFh +13E8: MOVX @DPTR,A + +13E9: MOV 0xAF,#07h ; Unknown SFR (FX2-specific) + +13F1: LCALL 12EAh ; GPIF waveform configuration: + ; Reads waveform data from XDATA 0xE000-0xE08E + ; Copies 128 bytes to GPIF waveform RAM via autopointers + ; Configures: GPIFCTLCFG, GPIFIDLECS, GPIFIDLECTL, + ; GPIFWFSELECT, GPIFADR, CTL states + ; PORTCCFG=0xFF (all alt function for GPIF) + ; PORTECFG.7=1 (GPIF ready signal) +``` + +### State Initialization +``` +13F4: CLR A +13F5: MOV 6Dh,A ; Status flags byte = 0 + ; bit 0: init in progress + ; bit 1: 8PSK firmware loaded + ; bit 3: signature check mode + ; bit 6: GPIF configured + ; bit 7: streaming active +13F7: CLR bit_05h +13F9: CLR bit_04h +13FB: MOV 68h,A ; Clear more state +13FD: MOV 69h,A +13FF: MOV 66h,A +1401: CLR bit_06h ; GPIF-configured flag = 0 +``` + +### GPIO Configuration +``` +1403: MOV DPTR,#E670h ; PORTACFG +1406: MOVX @DPTR,A ; = 0x00 (all GPIO, no alt functions) + +1407: MOV IOA,#A4h ; Port A initial: 1010_0100 + ; PA7=1 (GPIF/transport select?) + ; PA5=1 (8PSK_RESET deasserted) + ; PA2=1 (LNB voltage select?) + ; PA1=0, PA0=input +140A: MOV OEA,#FEh ; Port A direction: 1111_1110 + ; PA7-PA1 = output + ; PA0 = input (lock detect?) + +140D: MOV IOD,#F0h ; Port D initial: 1111_0000 + ; PD7=1 (8PSK bus D7?) + ; PD6=1 (8PSK bus D6?) + ; PD5=1 (8PSK bus D5?) + ; PD4=1 (8PSK bus D4?) +1410: MOV OED,#F0h ; Port D direction: upper 4 output +``` + +### FIFO Reset Sequence +``` +141C: MOV DPTR,#E604h ; FIFORESET +141F: MOV A,#80h ; NAK-ALL = 1 (hold off USB during reset) +1421: MOVX @DPTR,A +1425: MOV A,#02h ; Reset EP2 +1427: MOVX @DPTR,A +142B: MOV A,#04h ; Reset EP4 +142D: MOVX @DPTR,A +1431: MOV A,#06h ; Reset EP6 +1433: MOVX @DPTR,A +1437: MOV A,#08h ; Reset EP8 +1439: MOVX @DPTR,A +143D: CLR A ; NAK-ALL = 0 (release) +143E: MOVX @DPTR,A +``` + +### GPIF Timing +``` +1442: MOV DPTR,#E618h ; GPIFHOLDAMOUNT +1445: MOV A,#0Ch ; 12 IFCLK cycles hold +1447: MOVX @DPTR,A + +; E619-E61B (FLOWSTATE regs) = 0x00 +``` + +### I2C Configuration +``` +1461: MOV DPTR,#E67Ah ; I2CTL +1464: MOVX A,@DPTR +1465: ORL A,#01h ; Set bit 0: 400kHz I2C clock +1467: MOVX @DPTR,A +``` + +### Timer 2 Setup +``` +1468: ANL CKCON,#DFh ; Clear Timer2 clock source bit +146B: MOV T2CON,#04h ; Timer 2: auto-reload mode +146E: MOV RCAP2H,#F8h ; Reload value = 0xF8xx (fast tick) +``` + +## Phase 5: Descriptor Setup (back in main, 0x09BF) + +``` +09BF: MOV 0Ch,#12h ; Descriptor table high byte +09C2: MOV 0Dh,#00h ; Descriptor table low byte = 0x1200 + ; -> USB Device Descriptor + +; Multiple descriptor pointer pairs stored in direct RAM: +; [0C:0D] = 0x1200 Device Descriptor +; [14:15] = 0x1212 Device Qualifier +; [0A:0B] = 0x121C Configuration Descriptor +; [12:13] = 0x1254 Other Speed Config +; [16:17] = 0x128C String Descriptors +; [08:09] = 0x12E8 (high-speed descriptor variant?) + +; 09E3-0AE0: Copy all descriptors from CODE to XDATA +; Calculates actual XDATA base address +; Adjusts all pointers for runtime location +; Prepares EP configuration from config descriptor +``` + +## Phase 6: USB Enable (0x0AE1) + +``` +0AE1: MOV R7,09h / R6,08h ; Pass descriptor base pointer +0AE5: LCALL 1A0Eh ; Configure USB descriptors in FX2 + ; Reads XDATA desc, calculates checksums + ; Sets up descriptor table for USB core + ; Checks descriptor type -> sets bit_06h + +; Enable USB interrupts +0AE8: SETB EIE.0 ; Enable INT2 (USB interrupt) +0AEA: ORL EICON,#20h ; INT2 edge trigger + +0AED: MOV DPTR,#E668h ; INTSETUP +0AF0: MOVX A,@DPTR +0AF1: ORL A,#09h ; Enable autovectoring for INT2 and INT4 +0AF3: MOVX @DPTR,A + +0AF4: MOV DPTR,#E65Ch ; USBIE (USB Interrupt Enable) +0AF7: MOVX A,@DPTR +0AF8: ORL A,#3Dh ; Enable: SUDAV|SOF|SUTOK|SUSPEND|USBRESET + ; 0x3D = 0011_1101 +0AFA: MOVX @DPTR,A + +0AFB: SETB EA ; Global interrupt enable (IE.7) +``` + +### USB Re-enumeration +``` +0AFD: SETB bit_07h ; Mark "disconnected" state +0AFF: LCALL 0003h ; Call re-enumeration handler: + ; Sets USBCS.DISCON=1, USBCS.RENUM=1 + ; Delay ~375,000 cycles (~7.8ms @ 48MHz) + ; Clears USBIRQ (write 0xFF) + ; Clears EPIRQ (write 0xFF) + ; Clears EXIF.4 + ; Clears USBCS.DISCON (reconnect) + +; After re-enum completes: +0B02: MOV DPTR,#E680h ; USBCS +0B05: MOVX A,@DPTR +0B06: ANL A,#F7h ; Clear DISCON bit (bit 3) +0B08: MOVX @DPTR,A ; *** USB device now visible to host *** + +0B09: ANL CKCON,#F8h ; Final timer prescaler setup +``` + +## Phase 7: Main Loop (0x0B0C) + +``` +; ---- MAIN POLLING LOOP ---- +0B0C: LCALL 2297h ; ep1_poll(): + ; Check EP1IN ready, arm if needed + ; Check FLOW state registers + ; Return C=1 if EP1 armed + +0B0F: JNB bit_01h,0B17h ; SETUP pending (from SUDAV ISR)? +0B12: LCALL 032Ah ; Yes -> handle_setup() +0B15: CLR bit_01h ; Clear pending flag + +0B17: JNB bit_03h,0B0Ch ; SUSPEND pending (from SUSPEND ISR)? +0B1A: LCALL 24DAh ; Check suspend condition +0B1D: JNC 0B0Ch ; False alarm -> continue loop +0B1F: CLR bit_03h +0B21: LCALL 21EDh ; Enter suspend (WAKEUPCS handling) +; ... wakeup recovery code ... +0B3D: LCALL 211Dh ; I2C device polling (demod status?) +; ... loop back to 0B0C ... +``` + +## Phase 8: SETUP Packet Handler (0x032A) + +### Standard Request Dispatch +``` +032A: MOV DPTR,#E6B9h ; SETUPDAT[1] = bRequest (alt mapping) +032D: MOVX A,@DPTR ; Read bRequest +032E: CJNE A,#0Ch,0331h ; Compare with 12 +0331: JC 0335h ; If bRequest < 12 -> standard USB request +0333: AJMP 05ABh ; Else -> vendor request handler + +; Standard USB request jump table (bRequest 0-11): +0335: MOV DPTR,#033Bh +0338: ADD A,ACC ; index * 2 (each AJMP is 2 bytes) +033A: JMP @A+DPTR ; Computed jump + +; Table at 033B (12 entries): +; [0] GET_STATUS -> 0403h +; [1] CLEAR_FEATURE -> 04A6h +; [2] (reserved) -> 05ABh (stall) +; [3] SET_FEATURE -> 0536h +; [4] (reserved) -> 05ABh +; [5] SET_ADDRESS -> 05ABh (handled by hardware) +; [6] GET_DESCRIPTOR -> 0353h +; [7] (reserved) -> 05ABh +; [8] GET_CONFIGURATION -> 03FEh +; [9] SET_CONFIGURATION -> 03F4h +; [10] GET_INTERFACE -> 03DBh +; [11] SET_INTERFACE -> 03E5h +``` + +### Vendor Request Dispatch (0x05AB -> 0x0056) +``` +05AB: LCALL 0056h ; Call vendor request dispatcher +05AE: JC 05B7h ; If handled (C=1) -> skip stall +05B0: MOV DPTR,#E6A0h ; EP0CS +05B3: MOVX A,@DPTR +05B4: ORL A,#01h ; Set STALL bit (unhandled request) +05B6: MOVX @DPTR,A + +05B7: MOV DPTR,#E6A0h ; EP0CS +05BA: MOVX A,@DPTR +05BB: ORL A,#80h ; Set HSNAK bit (handshake) +05BD: MOVX @DPTR,A +05BE: RET +``` + +### Vendor Dispatch at 0x0056 +``` +; NOTE: E6B8-E6BF maps to SETUPDAT[0]-SETUPDAT[7] (alternate XDATA addresses) +; This is an undocumented but valid FX2LP mapping. + +0056: MOV DPTR,#E6B8h ; SETUPDAT[0] = bmRequestType +0059: MOVX A,@DPTR +005A: JB ACC.6,005Fh ; Test bit 6: vendor request flag +005D: CLR C ; Not vendor -> return unhandled +005E: RET + +005F: MOV DPTR,#E6B9h ; SETUPDAT[1] = bRequest +0062: MOVX A,@DPTR +0063: ADD A,#80h ; Remap: bRequest 0x80 -> index 0x00 + ; bRequest 0x89 -> index 0x09 + ; bRequest 0x9D -> index 0x1D +0065: CJNE A,#1Eh,0068h ; Check if index < 30 +0068: JC 006Ch ; In range -> use jump table +006A: AJMP 0326h ; Out of range -> return unhandled (C=0) + +006C: MOV DPTR,#0076h ; Jump table base +006F: ADD A,ACC ; index * 2 (AJMP = 2 bytes each) +0071: JNC 0075h ; Handle page crossing +0073: INC DPH +0075: JMP @A+DPTR ; Computed jump into vendor table +``` + +### Vendor Command Jump Table (0x0076) + +| bRequest | Index | Target | Function | +|----------|-------|--------|----------| +| 0x80 | 0 | 00B2h | GET_STATUS — returns status byte `6Dh` via EP0 | +| 0x81 | 1 | 0326h | (unhandled) | +| 0x82 | 2 | 0326h | (unhandled) | +| 0x83 | 3 | 00F1h | EP0_SETUP_READ — sets up descriptor read via SUDPTR | +| 0x84 | 4 | 0102h | EP0_DATA_WRITE — host writes data to EP0 | +| 0x85 | 5 | 0110h | EP0_DATA_READ — host reads data from EP0 | +| 0x86 | 6 | 012Eh | I2C_WRITE — write I2C data block | +| 0x87 | 7 | 0140h | EP0_MULTI_READ — multi-byte EP0 read | +| 0x88 | 8 | 0326h | (unhandled) | +| **0x89** | **9** | **00C4h** | **BOOT_8PSK** — boot the 8PSK demodulator | +| 0x8A | 10 | 019Ch | I2C_READ (with status check) | +| 0x8B | 11 | 01CBh | I2C_WRITE (variant) | +| 0x8C | 12 | 01DDh | I2C_READ (variant) | +| 0x8D | 13 | 01EFh | DiSEqC / LNB control? | +| 0x8E | 14 | 0326h | (unhandled) | +| 0x8F | 15 | 01FCh | GPIF flow control / read register | +| 0x90 | 16 | 020Bh | TUNE — set frequency / symbol rate | +| 0x91 | 17 | 022Ch | TUNE_STATUS — check lock | +| 0x92 | 18 | 024Ah | SET_TRANSPORT — configure TS stream | +| 0x93 | 19 | 026Fh | GET_SIGNAL_QUALITY — read SNR/BER | +| 0x94 | 20 | 01B9h | I2C bus control | +| 0x95 | 21 | 02DFh | GET_DEMOD_STATUS | +| 0x96 | 22 | 02B4h | I2C_ADDR_SELECT | +| 0x97 | 23 | 02C1h | RAW_REGISTER_READ | +| 0x98 | 24 | 02CBh | GET_EP0_BUFFER | +| 0x99-9C | 25-28 | 0326h | (unhandled) | +| 0x9D | 29 | 02FAh | FIRMWARE_VERSION? | + +## BOOT_8PSK (0x89) Handler: Complete Trace + +### Entry Point (0x00C4) +``` +00C4: JNB bit_06h,00DAh ; Is GPIF configured? +; [If GPIF configured]: +00C7: MOV DPTR,#E6BAh ; SETUPDAT[2] = wValueL +00CA: MOVX A,@DPTR +00CB: ADD A,#FFh ; Test if wValue == 0 (ADD #FF sets C if A>0) +00CD: MOV bit_07h,C ; bit_07h = (wValue != 0) = "load-from-host" flag +00CF: LCALL 1D4Fh ; -> boot_8psk_dispatch() + +; [If NOT configured]: +00DA: CLR bit_07h ; Force "no load" mode +00DC: LCALL 1D4Fh ; -> boot_8psk_dispatch() + +; [Common exit]: +00E5: CLR A +00E6: MOV DPTR,#E68Ah ; EP0BCH = 0 +00E9: MOVX @DPTR,A +00EA: MOV DPTR,#E68Bh ; EP0BCL = 1 +00ED: INC A +00EE: MOVX @DPTR,A ; Send 1-byte response (success/fail in EP0BUF[0]) +00EF: AJMP 0328h ; SETB C; RET (command handled) +``` + +### boot_8psk_dispatch (0x1D4F) +``` +1D4F: MOV A,6Dh ; Read status flags +1D51: RRC A ; bit 0 (init flag) -> C -> bit_08h +1D52: MOV bit_08h,C ; bit_08h = "currently booted" state +1D54: ORL IOD,#E0h ; PD7:PD5 = 1 (8PSK bus enable) + +1D57: JNB bit_07h,1D93h ; If NOT "load from host" -> error path +1D5A: JB bit_08h,1DA5h ; If already booted -> return success + +; --- Signature Check --- +1D5D: LCALL 16B8h ; Check 8PSK demod chip signature: + ; Read descriptor type byte from XDATA + ; Switch IFCONFIG to port mode (0xC0) + ; Set OEB=0 (Port B = input) + ; Set PA7=1, PA6=1 (select chip) + ; Read Port B: + ; Type 3: expect IOB=0xA5 + ; Type 4: expect IOB=0x5A + ; Type 5: expect IOB=0x5B + ; Type 6: expect IOB=0x5C + ; Restore IFCONFIG + ; Return C=1 if signature match +1D60: JNC 1DA5h ; Signature fail -> return with current state + +; --- Begin 8PSK Boot --- +1D62: ANL IOA,#DFh ; PA5=0 (assert 8PSK reset) +1D65: ANL 6Dh,#BFh ; Clear "streaming" flag in 6Dh +1D68: CLR bit_09h ; Clear streaming-active flag +1D6A: LCALL 1919h ; GPIF abort + FIFO cleanup: + ; EP2FIFOPFH, GPIFABORT, GPIFCTRL + ; Wait for GPIF idle + ; Reset EP2 FIFO + +; --- Configure for programming --- +1D6D: MOV A,IOA +1D6F: ANL A,#FBh ; PA2=0 +1D71: ORL A,#02h ; PA1=1 (select programming mode) +1D73: MOV IOA,A +1D75: MOV R7,#1Eh / R6,#00h +1D79: LCALL 1DFBh ; Delay ~30 * (CPUCS-based divisor) cycles + +; --- Deassert reset --- +1D7C: ORL IOA,#20h ; PA5=1 (deassert 8PSK reset) +1D7F: ORL 6Dh,#01h ; Set "init in progress" flag + +; --- Load 8PSK firmware --- +1D82: LCALL 10F2h ; load_8psk_firmware(): + ; I2C read from demod (addr 0x51) to get segment table + ; Loop: read segment header (addr, length) + ; I2C write firmware data blocks to demod + ; Retry up to 5x on write failures + ; Return C=0 on success, C=1 on failure +1D85: JC 1D93h ; If load failed -> error + +; --- Boot init blocks --- +1D87: LCALL 0DDDh ; boot_init_blocks(): + ; 3 iterations (init blocks 0,1,2) + ; Each: wait_for_ready() then I2C write + ; Sends register configuration to demod +1D8A: JC 1D93h ; If init failed -> error +1D8C: SETB bit_08h ; Mark "8PSK booted" +1D8E: ORL IOD,#E0h ; PD7:PD5 = 1 (8PSK bus active) +1D91: SJMP 1DA5h ; -> success exit + +; --- Error path --- +1D93: ANL 6Dh,#BCh ; Clear bits 0,1,6 in status +1D96: CLR bit_09h ; Clear streaming flag +1D98: LCALL 1919h ; GPIF abort + cleanup +1D9B: MOV A,IOA +1D9D: ANL A,#FDh ; PA1=0 +1D9F: ORL A,#04h ; PA2=1 (restore normal GPIO) +1DA1: MOV IOA,A +1DA3: CLR bit_08h ; Mark "not booted" + +; --- Return --- +1DA5: MOV C,bit_08h ; Return C = boot success +1DA7: RET +``` + +## USB Interrupt Vector Table (0x1600) + +The FX2LP USB autovector table at 0x1600. Each entry is a 3-byte LJMP + 1 NOP (4 bytes per slot). The hardware indexes into this table via INT2IVEC. + +| Offset | IRQ Source | Target | Status | +|--------|-----------------|--------|--------| +| +0x00 | SUDAV | 22E4h | **Active** — sets bit_01h, clears USBIRQ | +| +0x04 | SOF | 239Fh | **Active** | +| +0x08 | SUTOK | 2389h | **Active** | +| +0x0C | SUSPEND | 22FCh | **Active** — sets bit_03h | +| +0x10 | USBRESET | 1FB6h | **Active** | +| +0x14 | HISPEED | 1EC7h | **Active** | +| +0x18 | EP0ACK | 0032h | Stub (RETI) | +| +0x24 | EP0OUT | 0FFFh | **Active** | +| +0x28+ | EP1-8, IBN, etc | 24E0h+ | All stubs | + +## USB Descriptors (0x1200) + +``` +Device Descriptor (18 bytes): + bLength: 18 + bDescriptorType: 1 (DEVICE) + bcdUSB: 02.00 + bDeviceClass: 0xFF (vendor-specific) + bDeviceSubClass: 0xFF + bDeviceProtocol: 0x00 + bMaxPacketSize0: 64 + idVendor: 0x09C0 (Cypress Semiconductor) + idProduct: 0x0203 + bcdDevice: 0x0001 + iManufacturer: 1 + iProduct: 2 + iSerialNumber: 3 + bNumConfigurations: 1 +``` + +## Bit-Addressable Flag Map + +| Bit | Name/Purpose | +|-------|-------------| +| bit_00h | Wakeup/resume active | +| bit_01h | SETUP packet pending (from SUDAV ISR) | +| bit_02h | System initialized | +| bit_03h | SUSPEND pending (from SUSPEND ISR) | +| bit_04h | (unused/reserved) | +| bit_05h | (unused/reserved) | +| bit_06h | GPIF descriptor present | +| bit_07h | USB disconnect / "load-from-host" flag (dual use) | +| bit_08h | 8PSK demod booted successfully | +| bit_09h | Streaming active (GPIF/TS transfer in progress) | +| bit_0Ah | Autopointer direction flag | + +## Direct RAM Variable Map + +| Address | Name/Purpose | +|---------|-------------| +| 08h:09h | Descriptor pointer 7 (high-speed variant?) | +| 0Ah:0Bh | Configuration descriptor pointer | +| 0Ch:0Dh | Device descriptor pointer (0x1200) | +| 0Eh:0Fh | (adjusted descriptor offset) | +| 10h:11h | (adjusted descriptor offset) | +| 12h:13h | Other-speed config descriptor pointer | +| 14h:15h | Device qualifier descriptor pointer | +| 16h:17h | String descriptor pointer | +| 22h-25h | 32-bit counter/offset (XDATA copy) | +| 26h-29h | 32-bit counter (descriptor calculation) | +| 2Ah-2Dh | 32-bit counter (main state) | +| 32h-35h | 32-bit counter (main state) | +| 36h:37h | USB descriptor base (passed to 1A0E) | +| 38h | Descriptor iteration counter | +| 3Bh | Boot init block index / temp | +| 3Ch | I2C temp / ready flag | +| 3Dh:3Eh | wait_for_ready countdown | +| 3Fh:40h | Delay counter / retry counter | +| 45h:46h | I2C buffer pointer high:low | +| 49h-4Bh | Autopointer save area | +| 4Ch:4Dh | Saved IRQ state | +| 4Eh-50h | EP buffer config | +| 66h | (state variable) | +| 68h:69h | (state variables) | +| 6Dh | Master status flags byte | +| 6Fh:70h | Secondary wait counter | + +## Key Observations for RAM-Load Boot Failure + +1. **IDATA cleared on reset**: The startup clears IDATA 0x01-0x7F. All bit-addressable flags and direct RAM variables start at 0. This is critical — if you're loading into RAM without resetting the CPU, these won't be cleared. + +2. **XDATA init from CODE space**: The Keil startup copies initialization data from a table in CODE space to XDATA. If your RAM load doesn't include this init data at the correct CODE-relative offset, XDATA won't be initialized properly. + +3. **CPUCS clock switch**: The firmware switches to 48MHz immediately in hw_init(). If CPUCS is already set (from a previous boot), the second write may cause timing issues. + +4. **USB re-enumeration**: The firmware explicitly disconnects (USBCS.DISCON=1), waits ~8ms, then reconnects. If you're loading via USB, this disconnect cycle will kill your USB connection. + +5. **E6B8-E6BF SETUPDAT mapping**: The firmware uses an alternate XDATA mapping for SETUPDAT at 0xE6B8 instead of the standard 0xE6A5. Both mappings are valid, but your host-side code must account for this if doing SETUP data inspection. + +6. **Interrupt vector overwrite risk**: The reset vector at 0x0000 and all interrupt vectors (0x0003-0x006B) contain actual code, not just jump stubs. The INT0 vector at 0x0003 contains the USB re-enumeration handler. Loading code that overwrites these vectors will break USB. + +7. **GPIF waveform data**: The GPIF config at 0x12EA reads waveform data from XDATA 0xE000-0xE08E. This data must be present in XDATA before hw_init() runs. If it's missing (because the EEPROM-to-XDATA copy didn't happen), GPIF will be misconfigured. + +--- +*Generated from firmware analysis of skywalker1_eeprom_full64k.bin* +*Disassembly tool: disasm8051.py (custom 8051 disassembler)* diff --git a/firmware-dump/disasm8051.py b/firmware-dump/disasm8051.py new file mode 100644 index 0000000..89d2ef5 --- /dev/null +++ b/firmware-dump/disasm8051.py @@ -0,0 +1,660 @@ +#!/usr/bin/env python3 +"""8051 disassembler for Genpix SkyWalker-1 firmware analysis. + +Produces annotated disassembly of the FX2LP (8051-based) firmware, +focusing on the Keil C51 startup sequence and USB initialization. +""" +import sys +import re + +OPCODES = {} + +def op(code, mnem, length, desc=""): + OPCODES[code] = (mnem, length, desc) + +# NOP +op(0x00, "NOP", 1) +# AJMP/ACALL (page 0-7) +for page in range(8): + op(0x01 + page*0x20, f"AJMP {{a11:{page}}}", 2) + op(0x11 + page*0x20, f"ACALL {{a11:{page}}}", 2) + +op(0x02, "LJMP {a16}", 3) +op(0x12, "LCALL {a16}", 3) +op(0x03, "RR A", 1) +op(0x13, "RRC A", 1) +op(0x23, "RL A", 1) +op(0x33, "RLC A", 1) + +# INC +op(0x04, "INC A", 1) +op(0x05, "INC {d}", 2) +op(0x06, "INC @R0", 1) +op(0x07, "INC @R1", 1) +for i in range(8): op(0x08+i, f"INC R{i}", 1) + +# JBC, JB, JNB, JC, JNC, JZ, JNZ +op(0x10, "JBC {bit},{r8}", 3) +op(0x20, "JB {bit},{r8}", 3) +op(0x30, "JNB {bit},{r8}", 3) +op(0x40, "JC {r8}", 2) +op(0x50, "JNC {r8}", 2) +op(0x60, "JZ {r8}", 2) +op(0x70, "JNZ {r8}", 2) + +# DEC +op(0x14, "DEC A", 1) +op(0x15, "DEC {d}", 2) +op(0x16, "DEC @R0", 1) +op(0x17, "DEC @R1", 1) +for i in range(8): op(0x18+i, f"DEC R{i}", 1) + +# ADD +op(0x24, "ADD A,#{imm}", 2) +op(0x25, "ADD A,{d}", 2) +op(0x26, "ADD A,@R0", 1) +op(0x27, "ADD A,@R1", 1) +for i in range(8): op(0x28+i, f"ADD A,R{i}", 1) + +# ADDC +op(0x34, "ADDC A,#{imm}", 2) +op(0x35, "ADDC A,{d}", 2) +op(0x36, "ADDC A,@R0", 1) +op(0x37, "ADDC A,@R1", 1) +for i in range(8): op(0x38+i, f"ADDC A,R{i}", 1) + +# ORL +op(0x42, "ORL {d},A", 2) +op(0x43, "ORL {d},#{imm}", 3) +op(0x44, "ORL A,#{imm}", 2) +op(0x45, "ORL A,{d}", 2) +op(0x46, "ORL A,@R0", 1) +op(0x47, "ORL A,@R1", 1) +for i in range(8): op(0x48+i, f"ORL A,R{i}", 1) + +# ANL +op(0x52, "ANL {d},A", 2) +op(0x53, "ANL {d},#{imm}", 3) +op(0x54, "ANL A,#{imm}", 2) +op(0x55, "ANL A,{d}", 2) +op(0x56, "ANL A,@R0", 1) +op(0x57, "ANL A,@R1", 1) +for i in range(8): op(0x58+i, f"ANL A,R{i}", 1) + +# XRL +op(0x62, "XRL {d},A", 2) +op(0x63, "XRL {d},#{imm}", 3) +op(0x64, "XRL A,#{imm}", 2) +op(0x65, "XRL A,{d}", 2) +op(0x66, "XRL A,@R0", 1) +op(0x67, "XRL A,@R1", 1) +for i in range(8): op(0x68+i, f"XRL A,R{i}", 1) + +# ORL C, ANL C +op(0x72, "ORL C,{bit}", 2) +op(0xA0, "ORL C,/{bit}", 2) +op(0x82, "ANL C,{bit}", 2) +op(0xB0, "ANL C,/{bit}", 2) + +# MOV bit +op(0x92, "MOV {bit},C", 2) +op(0xA2, "MOV C,{bit}", 2) + +# MOV direct +op(0x75, "MOV {d},#{imm}", 3) +op(0x85, "MOV {d2},{d1}", 3) +op(0xE5, "MOV A,{d}", 2) +op(0xF5, "MOV {d},A", 2) +op(0xA5, "DB A5h", 1) + +# MOV A,#imm +op(0x74, "MOV A,#{imm}", 2) + +# MOV A,@Ri / MOV @Ri,A +op(0xE6, "MOV A,@R0", 1) +op(0xE7, "MOV A,@R1", 1) +op(0xF6, "MOV @R0,A", 1) +op(0xF7, "MOV @R1,A", 1) + +# MOV A,Rn / MOV Rn,A +for i in range(8): + op(0xE8+i, f"MOV A,R{i}", 1) + op(0xF8+i, f"MOV R{i},A", 1) + +# MOV Rn,#imm +for i in range(8): op(0x78+i, f"MOV R{i},#{{imm}}", 2) +# MOV Rn,direct +for i in range(8): op(0xA8+i, f"MOV R{i},{{d}}", 2) +# MOV direct,Rn +for i in range(8): op(0x88+i, f"MOV {{d}},R{i}", 2) + +op(0x86, "MOV {d},@R0", 2) +op(0x87, "MOV {d},@R1", 2) +op(0xA6, "MOV @R0,{d}", 2) +op(0xA7, "MOV @R1,{d}", 2) +op(0x76, "MOV @R0,#{imm}", 2) +op(0x77, "MOV @R1,#{imm}", 2) + +# MOV DPTR,#imm16 +op(0x90, "MOV DPTR,#{imm16}", 3) + +# MOVC +op(0x83, "MOVC A,@A+PC", 1) +op(0x93, "MOVC A,@A+DPTR", 1) + +# MOVX +op(0xE0, "MOVX A,@DPTR", 1) +op(0xE2, "MOVX A,@R0", 1) +op(0xE3, "MOVX A,@R1", 1) +op(0xF0, "MOVX @DPTR,A", 1) +op(0xF2, "MOVX @R0,A", 1) +op(0xF3, "MOVX @R1,A", 1) + +# PUSH/POP +op(0xC0, "PUSH {d}", 2) +op(0xD0, "POP {d}", 2) + +# XCH +op(0xC5, "XCH A,{d}", 2) +op(0xC6, "XCH A,@R0", 1) +op(0xC7, "XCH A,@R1", 1) +for i in range(8): op(0xC8+i, f"XCH A,R{i}", 1) + +# XCHD +op(0xD6, "XCHD A,@R0", 1) +op(0xD7, "XCHD A,@R1", 1) + +# DJNZ +op(0xD5, "DJNZ {d},{r8}", 3) +for i in range(8): op(0xD8+i, f"DJNZ R{i},{{r8}}", 2) + +# CJNE +op(0xB4, "CJNE A,#{imm},{r8}", 3) +op(0xB5, "CJNE A,{d},{r8}", 3) +op(0xB6, "CJNE @R0,#{imm},{r8}", 3) +op(0xB7, "CJNE @R1,#{imm},{r8}", 3) +for i in range(8): op(0xB8+i, f"CJNE R{i},#{{imm}},{{r8}}", 3) + +# SJMP +op(0x80, "SJMP {r8}", 2) +# JMP @A+DPTR +op(0x73, "JMP @A+DPTR", 1) +# RET, RETI +op(0x22, "RET", 1) +op(0x32, "RETI", 1) + +# CLR, SETB, CPL +op(0xC2, "CLR {bit}", 2) +op(0xC3, "CLR C", 1) +op(0xD2, "SETB {bit}", 2) +op(0xD3, "SETB C", 1) +op(0xB2, "CPL {bit}", 2) +op(0xB3, "CPL C", 1) +op(0xE4, "CLR A", 1) +op(0xF4, "CPL A", 1) + +# MUL, DIV, DA, SWAP +op(0xA4, "MUL AB", 1) +op(0x84, "DIV AB", 1) +op(0xD4, "DA A", 1) +op(0xC4, "SWAP A", 1) + +# SUBB +op(0x94, "SUBB A,#{imm}", 2) +op(0x95, "SUBB A,{d}", 2) +op(0x96, "SUBB A,@R0", 1) +op(0x97, "SUBB A,@R1", 1) +for i in range(8): op(0x98+i, f"SUBB A,R{i}", 1) + +# INC DPTR +op(0xA3, "INC DPTR", 1) + + +# ============ SFR / XDATA / BIT names ============ + +SFR_NAMES = { + 0x80: "IOA", 0x81: "SP", 0x82: "DPL", 0x83: "DPH", + 0x84: "DPL1", 0x85: "DPH1", 0x86: "DPS", 0x87: "PCON", + 0x88: "TCON", 0x89: "TMOD", 0x8A: "TL0", 0x8B: "TL1", + 0x8C: "TH0", 0x8D: "TH1", 0x8E: "CKCON", + 0x90: "IOB", 0x91: "EXIF", + 0x98: "SCON0", 0x99: "SBUF0", + 0x9A: "AUTOPTRH1", 0x9B: "AUTOPTRL1", + 0x9C: "AUTOPTRH2", 0x9D: "AUTOPTRL2", 0x9E: "AUTOPTRSETUP", + 0xA0: "IOC", 0xA8: "IE", + 0xAA: "EP2468STAT", 0xAB: "EP24FIFOFLGS", 0xAC: "EP68FIFOFLGS", + 0xB0: "IOD", 0xB1: "IOE", + 0xB2: "OEA", 0xB3: "OEB", 0xB4: "OEC", 0xB5: "OED", 0xB6: "OEE", + 0xC0: "SCON1", 0xC1: "SBUF1", + 0xC8: "T2CON", 0xCA: "RCAP2L", 0xCB: "RCAP2H", 0xCC: "TL2", 0xCD: "TH2", + 0xD0: "PSW", 0xD8: "EICON", + 0xE0: "ACC", 0xE8: "EIE", 0xF0: "B", 0xF8: "EIP", +} + +XREG_NAMES = { + 0xE600: "CPUCS", 0xE601: "IFCONFIG", 0xE602: "PINFLAGSAB", + 0xE603: "PINFLAGSCD", 0xE604: "FIFORESET", 0xE609: "REVCTL", + 0xE60A: "GPIFTRIG", 0xE60B: "GPIFSGLDATH", 0xE60C: "GPIFSGLDATLX", + 0xE610: "FLOWSTATE", 0xE611: "FLOWLOGIC", + 0xE618: "GPIFHOLDAMOUNT", + 0xE620: "EP2CFG", 0xE621: "EP4CFG", 0xE622: "EP6CFG", 0xE623: "EP8CFG", + 0xE624: "EP2FIFOCFG", 0xE625: "EP4FIFOCFG", + 0xE626: "EP6FIFOCFG", 0xE627: "EP8FIFOCFG", + 0xE628: "EP2AUTOINLENH", 0xE629: "EP2AUTOINLENL", + 0xE630: "EP2FIFOPFH", 0xE631: "EP2FIFOPFL", + 0xE640: "EP2ISOINPKTS", + 0xE648: "INPKTEND", 0xE649: "OUTPKTEND", + 0xE650: "EP2FIFOIE", 0xE651: "EP2FIFOIRQ", + 0xE65C: "USBIE", 0xE65D: "USBIRQ", + 0xE65E: "EPIE", 0xE65F: "EPIRQ", + 0xE660: "GPIFIE", 0xE661: "GPIFIRQ", + 0xE662: "USBERRIE", 0xE663: "USBERRIRQ", + 0xE666: "INT2IVEC", 0xE667: "INT4IVEC", 0xE668: "INTSETUP", + 0xE670: "PORTACFG", 0xE671: "PORTCCFG", 0xE672: "PORTECFG", + 0xE678: "I2CS", 0xE679: "I2DAT", 0xE67A: "I2CTL", + 0xE67B: "XAUTODAT1", 0xE67C: "XAUTODAT2", + 0xE680: "USBCS", 0xE681: "SUSPEND", 0xE682: "WAKEUPCS", + 0xE683: "TOGCTL", + 0xE684: "USBFRAMEH", 0xE685: "USBFRAMEL", 0xE686: "MICROFRAME", + 0xE687: "FNADDR", + 0xE68A: "EP0BCH", 0xE68B: "EP0BCL", + 0xE68C: "EP1OUTBC", 0xE68D: "EP1INBC", + 0xE68F: "EP1INCS", + 0xE690: "EP2CS", 0xE691: "EP4CS", 0xE692: "EP6CS", 0xE693: "EP8CS", + 0xE694: "EP2FIFOFLGS", 0xE695: "EP4FIFOFLGS", + 0xE696: "EP6FIFOFLGS", 0xE697: "EP8FIFOFLGS", + 0xE698: "EP2BCH", 0xE699: "EP2BCL", + 0xE6A0: "EP0CS", 0xE6A1: "EP0STAT", 0xE6A2: "EP0STALLBITS", + 0xE6A3: "CLRTOGS", + 0xE6A5: "SETUPDAT[0]", 0xE6A6: "SETUPDAT[1]", + 0xE6A7: "SETUPDAT[2]", 0xE6A8: "SETUPDAT[3]", + 0xE6A9: "SETUPDAT[4]", 0xE6AA: "SETUPDAT[5]", + 0xE6AB: "SETUPDAT[6]", 0xE6AC: "SETUPDAT[7]", + 0xE6C0: "GPIFWFSELECT", 0xE6C1: "GPIFIDLECS", + 0xE6C2: "GPIFIDLECTL", 0xE6C3: "GPIFCTLCFG", + 0xE6C4: "GPIFADRH", 0xE6C5: "GPIFADRL", + 0xE6CE: "GPIFREADYCFG", 0xE6CF: "GPIFREADYSTAT", + 0xE6D0: "GPIFABORT", + 0xE6F8: "SUDPTRH", 0xE6F9: "SUDPTRL", 0xE6FA: "SUDPTRCTL", + 0xE740: "EP0BUF[0]", + 0xE780: "EP1OUTBUF[0]", + 0xE7C0: "EP1INBUF[0]", +} + +BIT_NAMES = { + 0xD0: "PSW.P", 0xD1: "PSW.1", 0xD2: "PSW.OV", 0xD3: "PSW.RS0", + 0xD4: "PSW.RS1", 0xD5: "PSW.F0", 0xD6: "PSW.AC", 0xD7: "PSW.CY", + 0xA8: "EX0", 0xA9: "ET0", 0xAA: "EX1", 0xAB: "ET1", + 0xAC: "ES0", 0xAD: "ET2", 0xAE: "ES1", 0xAF: "EA", + 0x80: "IOA.0", 0x81: "IOA.1", 0x82: "IOA.2", 0x83: "IOA.3", + 0x84: "IOA.4", 0x85: "IOA.5", 0x86: "IOA.6", 0x87: "IOA.7", + 0x90: "IOB.0", 0x91: "IOB.1", 0x92: "IOB.2", 0x93: "IOB.3", + 0x94: "IOB.4", 0x95: "IOB.5", 0x96: "IOB.6", 0x97: "IOB.7", + 0x88: "IT0", 0x89: "IE0", 0x8A: "IT1", 0x8B: "IE1", + 0x8C: "TR0", 0x8D: "TF0", 0x8E: "TR1", 0x8F: "TF1", + 0x98: "RI_0", 0x99: "TI_0", + 0xD8: "EICON.0", 0xDB: "INT6", 0xDC: "RESI", 0xDD: "ERESI", +} + +KNOWN_LABELS = { + 0x0000: "reset_vector", + 0x0003: "int0_vector", + 0x000B: "timer0_vector", + 0x0013: "int1_vector", + 0x001B: "timer1_vector", + 0x0023: "serial0_vector", + 0x002B: "timer2_vector", + 0x0033: "resume_vector", + 0x003B: "serial1_vector", + 0x0043: "usb_int2_vector", + 0x004B: "i2c_int3_vector", + 0x0053: "gpif_int4_vector", + 0x005B: "int5_vector", + 0x0063: "int6_vector", + 0x099A: "tune_function", + 0x0DDD: "boot_init_blocks", + 0x0EE9: "tune_init_blocks", + 0x1200: "usb_device_desc", + 0x1556: "i2c_combined_read", + 0x188D: "keil_startup", + 0x1A81: "i2c_write", + 0x1D87: "boot_8psk", + 0x2000: "wait_for_ready", +} + + +def sfr_name(addr): + if addr in SFR_NAMES: + return SFR_NAMES[addr] + if addr >= 0x80: + return f"SFR_{addr:02X}h" + return f"{addr:02X}h" + +def bit_name(addr): + if addr in BIT_NAMES: + return BIT_NAMES[addr] + if addr >= 0x80: + base = addr & 0xF8 + bit = addr & 0x07 + if base in SFR_NAMES: + return f"{SFR_NAMES[base]}.{bit}" + return f"bit_{addr:02X}h" + +def xreg_name(addr): + if addr in XREG_NAMES: + return XREG_NAMES[addr] + if 0xE740 <= addr <= 0xE77F: + return f"EP0BUF[{addr-0xE740}]" + if 0xE780 <= addr <= 0xE7BF: + return f"EP1OUTBUF[{addr-0xE780}]" + if 0xE7C0 <= addr <= 0xE7FF: + return f"EP1INBUF[{addr-0xE7C0}]" + if 0xE600 <= addr <= 0xE6FF: + return f"XSFR_{addr:04X}h" + return None + + +def disasm_one(data, pc): + """Returns (mnem, hex_str, length, comment, branch_target).""" + if pc >= len(data): + return ("???", "", 1, "", None) + + opc = data[pc] + entry = OPCODES.get(opc) + + if entry is None: + return (f"DB {opc:02X}h", f"{opc:02X}", 1, "; UNKNOWN", None) + + mnem_fmt, length, desc = entry + + if pc + length > len(data): + return (f"DB {opc:02X}h", f"{opc:02X}", 1, "; TRUNCATED", None) + + raw_bytes = data[pc:pc+length] + hex_str = " ".join(f"{b:02X}" for b in raw_bytes) + comment = "" + branch_target = None + mnem = mnem_fmt + + # Resolve address fields + if "{a16}" in mnem: + addr16 = (data[pc+1] << 8) | data[pc+2] + label = KNOWN_LABELS.get(addr16) + mnem = mnem.replace("{a16}", label if label else f"{addr16:04X}h") + branch_target = addr16 + + m = re.search(r'\{a11:(\d+)\}', mnem) + if m: + page = int(m.group(1)) + addr11 = (page << 8) | data[pc+1] | ((pc + 2) & 0xF800) + label = KNOWN_LABELS.get(addr11) + mnem = re.sub(r'\{a11:\d+\}', label if label else f"{addr11:04X}h", mnem) + branch_target = addr11 + + if "{r8}" in mnem: + rel_byte = data[pc + length - 1] + rel = rel_byte if rel_byte < 0x80 else rel_byte - 256 + target = (pc + length + rel) & 0xFFFF + label = KNOWN_LABELS.get(target) + mnem = mnem.replace("{r8}", label if label else f"{target:04X}h") + branch_target = target + + # Direct addressing - handle the special MOV d2,d1 case + if "{d2}" in mnem and "{d1}" in mnem: + src = data[pc+1] + dst = data[pc+2] + mnem = mnem.replace("{d1}", sfr_name(src) if src >= 0x20 else f"{src:02X}h") + mnem = mnem.replace("{d2}", sfr_name(dst) if dst >= 0x20 else f"{dst:02X}h") + elif "{d}" in mnem: + d = data[pc+1] + mnem = mnem.replace("{d}", sfr_name(d) if d >= 0x20 else f"{d:02X}h") + + if "{bit}" in mnem: + b = data[pc+1] + mnem = mnem.replace("{bit}", bit_name(b)) + + if "{imm16}" in mnem: + imm16 = (data[pc+1] << 8) | data[pc+2] + xname = xreg_name(imm16) + if xname: + mnem = mnem.replace("{imm16}", f"{imm16:04X}h") + comment = f"; = {xname}" + elif imm16 in KNOWN_LABELS: + mnem = mnem.replace("{imm16}", f"{imm16:04X}h") + comment = f"; -> {KNOWN_LABELS[imm16]}" + else: + mnem = mnem.replace("{imm16}", f"{imm16:04X}h") + + if "{imm}" in mnem: + # Determine which byte is the immediate + if length == 3 and opc in (0x43, 0x53, 0x63): + imm = data[pc+2] # ORL/ANL/XRL d,#imm + elif length == 3 and opc == 0x75: + imm = data[pc+2] # MOV d,#imm + elif length == 3 and opc == 0xB4: + imm = data[pc+1] # CJNE A,#imm,r8 + elif length == 3 and 0xB6 <= opc <= 0xBF: + imm = data[pc+1] # CJNE @Ri/#imm or CJNE Rn,#imm + elif length == 3 and opc == 0xD5: + # DJNZ d,r8 - no imm, but {d} already handled + imm = 0 # shouldn't happen + else: + imm = data[pc + length - 1] + mnem = mnem.replace("{imm}", f"{imm:02X}h") + + return (mnem, hex_str, length, comment, branch_target) + + +def disasm_range(data, start, end, title=""): + lines = [] + if title: + lines.append(f"\n{'='*80}") + lines.append(f" {title}") + lines.append(f"{'='*80}") + + pc = start + while pc < end and pc < len(data): + if pc in KNOWN_LABELS: + lines.append(f"\n {KNOWN_LABELS[pc]}:") + + mnem, hex_str, length, comment, target = disasm_one(data, pc) + addr_str = f"{pc:04X}" + hex_padded = f"{hex_str:<12s}" + line = f" {addr_str}: {hex_padded} {mnem}" + if comment: + line = f"{line:<58s} {comment}" + lines.append(line) + pc += length + + return "\n".join(lines) + + +def track_dptr_accesses(data, start, end): + """Track all MOVX accesses with DPTR context.""" + results = [] + pc = start + dptr = None + while pc < end and pc < len(data): + opc = data[pc] + entry = OPCODES.get(opc) + if entry is None: + pc += 1 + continue + _, length, _ = entry + if pc + length > len(data): + break + if opc == 0x90: # MOV DPTR,#imm16 + dptr = (data[pc+1] << 8) | data[pc+2] + elif opc in (0xE0, 0xF0) and dptr is not None: + xname = xreg_name(dptr) + direction = "READ" if opc == 0xE0 else "WRITE" + results.append((pc, dptr, xname, direction)) + elif opc == 0xA3: # INC DPTR + if dptr is not None: + dptr += 1 + pc += length + return results + + +def main(): + fw_path = "/home/rpm/claude/ham/satellite/genpix/skywalker-1/firmware-dump/skywalker1_eeprom_full64k.bin" + with open(fw_path, "rb") as f: + data = f.read() + + out = [] + out.append("=" * 80) + out.append(" GENPIX SKYWALKER-1 FIRMWARE DISASSEMBLY") + out.append(" FX2LP (CY7C68013A) 8051 Core - Keil C51 Compiled") + out.append(" Firmware size: 9472 bytes (0x0000-0x24FF)") + out.append(" Binary: 65536 bytes total (RAM image)") + out.append("=" * 80) + + # ==== SECTION 1: Reset + Interrupt Vectors ==== + out.append(disasm_range(data, 0x0000, 0x006B, + "SECTION 1: Reset Vector & Interrupt Vector Table (0x0000-0x006A)")) + + # ==== SECTION 2: Keil C51 Startup ==== + out.append(disasm_range(data, 0x188D, 0x1950, + "SECTION 2: Keil C51 Startup (STARTUP.A51) @ 0x188D")) + + # Find main() by tracing startup + pc = 0x188D + main_addr = None + for _ in range(500): + if pc >= len(data) - 2: + break + opc = data[pc] + entry = OPCODES.get(opc) + if entry is None: + break + _, length, _ = entry + if opc == 0x02: # LJMP + addr = (data[pc+1] << 8) | data[pc+2] + # The startup has two LJMPs: one internal loop, one to main + # main() is the last LJMP before we hit data/different code + if addr < 0x188D: # Jump backward = likely to main() + main_addr = addr + pc += length + + if main_addr: + KNOWN_LABELS[main_addr] = "main" + out.append(f"\n >>> Keil startup jumps to main() at {main_addr:04X}h") + + # ==== SECTION 3: main() ==== + if main_addr: + # Disassemble main() - generous range to cover init + out.append(disasm_range(data, main_addr, min(main_addr + 400, 0x2500), + f"SECTION 3: main() @ {main_addr:04X}h (first ~400 bytes)")) + + # ==== SECTION 4: Vendor command dispatch ==== + out.append("\n" + "=" * 80) + out.append(" SECTION 4: USB Vendor Command Dispatch") + out.append("=" * 80) + + # Find all reads of SETUPDAT[1] (bRequest) + for pc in range(0x2500): + if pc + 2 < len(data) and data[pc] == 0x90 and data[pc+1] == 0xE6 and data[pc+2] == 0xA6: + out.append(f"\n SETUPDAT[1] read at {pc:04X}h:") + ctx_start = max(0, pc - 16) + ctx_end = min(pc + 120, 0x2500) + out.append(disasm_range(data, ctx_start, ctx_end, + f" Dispatch context @ {pc:04X}h")) + + # ==== SECTION 5: Find all CJNE A,#xx comparisons (command dispatch) ==== + out.append("\n" + "=" * 80) + out.append(" SECTION 5: All CJNE A,#xx Instructions (Command Dispatch Table)") + out.append("=" * 80) + + for pc in range(0x2500): + if pc + 2 < len(data) and data[pc] == 0xB4: + imm = data[pc+1] + rel = data[pc+2] + if rel >= 0x80: + rel -= 256 + target = (pc + 3 + rel) & 0xFFFF + out.append(f" {pc:04X}: CJNE A,#{imm:02X}h,{target:04X}h ; bRequest?=0x{imm:02X} skip->{target:04X}h") + + # ==== SECTION 6: boot_8psk ==== + out.append(disasm_range(data, 0x1D87, min(0x1D87 + 250, 0x2500), + "SECTION 6: boot_8psk() @ 0x1D87")) + + # ==== SECTION 7: SFR access map ==== + out.append("\n" + "=" * 80) + out.append(" SECTION 7: All XDATA SFR Accesses (DPTR-tracked)") + out.append("=" * 80) + + accesses = track_dptr_accesses(data, 0x0000, 0x2500) + for pc, addr, name, direction in accesses: + name_str = name if name else f"XDATA_{addr:04X}h" + out.append(f" {pc:04X}: {direction:5s} [{addr:04X}h] {name_str}") + + # ==== SECTION 8: Key functions ==== + out.append(disasm_range(data, 0x0DDD, 0x0DDD + 120, + "SECTION 8: boot_init_blocks() @ 0x0DDD")) + + out.append(disasm_range(data, 0x1A81, 0x1A81 + 150, + "SECTION 9: i2c_write() @ 0x1A81")) + + out.append(disasm_range(data, 0x1556, 0x1556 + 150, + "SECTION 10: i2c_combined_read() @ 0x1556")) + + out.append(disasm_range(data, 0x2000, 0x2000 + 120, + "SECTION 11: wait_for_ready() @ 0x2000")) + + out.append(disasm_range(data, 0x099A, 0x099A + 200, + "SECTION 12: tune_function() @ 0x099A")) + + # ==== SECTION 13: USB Descriptors ==== + out.append("\n" + "=" * 80) + out.append(" SECTION 13: USB Descriptors @ 0x1200") + out.append("=" * 80) + + desc = data[0x1200:0x1212] + out.append(f" Device Descriptor (18 bytes):") + out.append(f" bLength: {desc[0]} (0x{desc[0]:02X})") + out.append(f" bDescriptorType: {desc[1]} (DEVICE)") + out.append(f" bcdUSB: {desc[3]:02X}.{desc[2]:02X}") + out.append(f" bDeviceClass: 0x{desc[4]:02X} (vendor-specific)") + out.append(f" bDeviceSubClass: 0x{desc[5]:02X}") + out.append(f" bDeviceProtocol: 0x{desc[6]:02X}") + out.append(f" bMaxPacketSize0: {desc[7]} bytes") + vid = desc[8] | (desc[9] << 8) + pid = desc[10] | (desc[11] << 8) + out.append(f" idVendor: 0x{vid:04X} (Cypress)") + out.append(f" idProduct: 0x{pid:04X}") + bcd = desc[12] | (desc[13] << 8) + out.append(f" bcdDevice: {bcd:04X}") + out.append(f" iManufacturer: {desc[14]}") + out.append(f" iProduct: {desc[15]}") + out.append(f" iSerialNumber: {desc[16]}") + out.append(f" bNumConfigurations: {desc[17]}") + + # Raw descriptor dump + out.append(f"\n Raw descriptor area 0x1200-0x1300:") + for off in range(0, 0x100, 16): + addr = 0x1200 + off + chunk = data[addr:addr+16] + hexl = " ".join(f"{b:02X}" for b in chunk) + asciil = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk) + out.append(f" {addr:04X}: {hexl} {asciil}") + + # ==== SECTION 14: Hex dump of interesting code regions ==== + out.append("\n" + "=" * 80) + out.append(" SECTION 14: Raw Hex - Code Space Near USB ISR Targets") + out.append("=" * 80) + + # Show hex around the USB interrupt vector dispatch + # The USB ISR at 0x0043 likely jumps somewhere + for start_addr in [0x0043, 0x004B]: + out.append(f"\n Hex dump at {start_addr:04X}h:") + for off in range(0, 48, 16): + addr = start_addr + off + chunk = data[addr:addr+16] + hexl = " ".join(f"{b:02X}" for b in chunk) + out.append(f" {addr:04X}: {hexl}") + + print("\n".join(out)) + + +if __name__ == "__main__": + main() diff --git a/firmware-dump/disasm8051_v2.py b/firmware-dump/disasm8051_v2.py new file mode 100644 index 0000000..89d2ef5 --- /dev/null +++ b/firmware-dump/disasm8051_v2.py @@ -0,0 +1,660 @@ +#!/usr/bin/env python3 +"""8051 disassembler for Genpix SkyWalker-1 firmware analysis. + +Produces annotated disassembly of the FX2LP (8051-based) firmware, +focusing on the Keil C51 startup sequence and USB initialization. +""" +import sys +import re + +OPCODES = {} + +def op(code, mnem, length, desc=""): + OPCODES[code] = (mnem, length, desc) + +# NOP +op(0x00, "NOP", 1) +# AJMP/ACALL (page 0-7) +for page in range(8): + op(0x01 + page*0x20, f"AJMP {{a11:{page}}}", 2) + op(0x11 + page*0x20, f"ACALL {{a11:{page}}}", 2) + +op(0x02, "LJMP {a16}", 3) +op(0x12, "LCALL {a16}", 3) +op(0x03, "RR A", 1) +op(0x13, "RRC A", 1) +op(0x23, "RL A", 1) +op(0x33, "RLC A", 1) + +# INC +op(0x04, "INC A", 1) +op(0x05, "INC {d}", 2) +op(0x06, "INC @R0", 1) +op(0x07, "INC @R1", 1) +for i in range(8): op(0x08+i, f"INC R{i}", 1) + +# JBC, JB, JNB, JC, JNC, JZ, JNZ +op(0x10, "JBC {bit},{r8}", 3) +op(0x20, "JB {bit},{r8}", 3) +op(0x30, "JNB {bit},{r8}", 3) +op(0x40, "JC {r8}", 2) +op(0x50, "JNC {r8}", 2) +op(0x60, "JZ {r8}", 2) +op(0x70, "JNZ {r8}", 2) + +# DEC +op(0x14, "DEC A", 1) +op(0x15, "DEC {d}", 2) +op(0x16, "DEC @R0", 1) +op(0x17, "DEC @R1", 1) +for i in range(8): op(0x18+i, f"DEC R{i}", 1) + +# ADD +op(0x24, "ADD A,#{imm}", 2) +op(0x25, "ADD A,{d}", 2) +op(0x26, "ADD A,@R0", 1) +op(0x27, "ADD A,@R1", 1) +for i in range(8): op(0x28+i, f"ADD A,R{i}", 1) + +# ADDC +op(0x34, "ADDC A,#{imm}", 2) +op(0x35, "ADDC A,{d}", 2) +op(0x36, "ADDC A,@R0", 1) +op(0x37, "ADDC A,@R1", 1) +for i in range(8): op(0x38+i, f"ADDC A,R{i}", 1) + +# ORL +op(0x42, "ORL {d},A", 2) +op(0x43, "ORL {d},#{imm}", 3) +op(0x44, "ORL A,#{imm}", 2) +op(0x45, "ORL A,{d}", 2) +op(0x46, "ORL A,@R0", 1) +op(0x47, "ORL A,@R1", 1) +for i in range(8): op(0x48+i, f"ORL A,R{i}", 1) + +# ANL +op(0x52, "ANL {d},A", 2) +op(0x53, "ANL {d},#{imm}", 3) +op(0x54, "ANL A,#{imm}", 2) +op(0x55, "ANL A,{d}", 2) +op(0x56, "ANL A,@R0", 1) +op(0x57, "ANL A,@R1", 1) +for i in range(8): op(0x58+i, f"ANL A,R{i}", 1) + +# XRL +op(0x62, "XRL {d},A", 2) +op(0x63, "XRL {d},#{imm}", 3) +op(0x64, "XRL A,#{imm}", 2) +op(0x65, "XRL A,{d}", 2) +op(0x66, "XRL A,@R0", 1) +op(0x67, "XRL A,@R1", 1) +for i in range(8): op(0x68+i, f"XRL A,R{i}", 1) + +# ORL C, ANL C +op(0x72, "ORL C,{bit}", 2) +op(0xA0, "ORL C,/{bit}", 2) +op(0x82, "ANL C,{bit}", 2) +op(0xB0, "ANL C,/{bit}", 2) + +# MOV bit +op(0x92, "MOV {bit},C", 2) +op(0xA2, "MOV C,{bit}", 2) + +# MOV direct +op(0x75, "MOV {d},#{imm}", 3) +op(0x85, "MOV {d2},{d1}", 3) +op(0xE5, "MOV A,{d}", 2) +op(0xF5, "MOV {d},A", 2) +op(0xA5, "DB A5h", 1) + +# MOV A,#imm +op(0x74, "MOV A,#{imm}", 2) + +# MOV A,@Ri / MOV @Ri,A +op(0xE6, "MOV A,@R0", 1) +op(0xE7, "MOV A,@R1", 1) +op(0xF6, "MOV @R0,A", 1) +op(0xF7, "MOV @R1,A", 1) + +# MOV A,Rn / MOV Rn,A +for i in range(8): + op(0xE8+i, f"MOV A,R{i}", 1) + op(0xF8+i, f"MOV R{i},A", 1) + +# MOV Rn,#imm +for i in range(8): op(0x78+i, f"MOV R{i},#{{imm}}", 2) +# MOV Rn,direct +for i in range(8): op(0xA8+i, f"MOV R{i},{{d}}", 2) +# MOV direct,Rn +for i in range(8): op(0x88+i, f"MOV {{d}},R{i}", 2) + +op(0x86, "MOV {d},@R0", 2) +op(0x87, "MOV {d},@R1", 2) +op(0xA6, "MOV @R0,{d}", 2) +op(0xA7, "MOV @R1,{d}", 2) +op(0x76, "MOV @R0,#{imm}", 2) +op(0x77, "MOV @R1,#{imm}", 2) + +# MOV DPTR,#imm16 +op(0x90, "MOV DPTR,#{imm16}", 3) + +# MOVC +op(0x83, "MOVC A,@A+PC", 1) +op(0x93, "MOVC A,@A+DPTR", 1) + +# MOVX +op(0xE0, "MOVX A,@DPTR", 1) +op(0xE2, "MOVX A,@R0", 1) +op(0xE3, "MOVX A,@R1", 1) +op(0xF0, "MOVX @DPTR,A", 1) +op(0xF2, "MOVX @R0,A", 1) +op(0xF3, "MOVX @R1,A", 1) + +# PUSH/POP +op(0xC0, "PUSH {d}", 2) +op(0xD0, "POP {d}", 2) + +# XCH +op(0xC5, "XCH A,{d}", 2) +op(0xC6, "XCH A,@R0", 1) +op(0xC7, "XCH A,@R1", 1) +for i in range(8): op(0xC8+i, f"XCH A,R{i}", 1) + +# XCHD +op(0xD6, "XCHD A,@R0", 1) +op(0xD7, "XCHD A,@R1", 1) + +# DJNZ +op(0xD5, "DJNZ {d},{r8}", 3) +for i in range(8): op(0xD8+i, f"DJNZ R{i},{{r8}}", 2) + +# CJNE +op(0xB4, "CJNE A,#{imm},{r8}", 3) +op(0xB5, "CJNE A,{d},{r8}", 3) +op(0xB6, "CJNE @R0,#{imm},{r8}", 3) +op(0xB7, "CJNE @R1,#{imm},{r8}", 3) +for i in range(8): op(0xB8+i, f"CJNE R{i},#{{imm}},{{r8}}", 3) + +# SJMP +op(0x80, "SJMP {r8}", 2) +# JMP @A+DPTR +op(0x73, "JMP @A+DPTR", 1) +# RET, RETI +op(0x22, "RET", 1) +op(0x32, "RETI", 1) + +# CLR, SETB, CPL +op(0xC2, "CLR {bit}", 2) +op(0xC3, "CLR C", 1) +op(0xD2, "SETB {bit}", 2) +op(0xD3, "SETB C", 1) +op(0xB2, "CPL {bit}", 2) +op(0xB3, "CPL C", 1) +op(0xE4, "CLR A", 1) +op(0xF4, "CPL A", 1) + +# MUL, DIV, DA, SWAP +op(0xA4, "MUL AB", 1) +op(0x84, "DIV AB", 1) +op(0xD4, "DA A", 1) +op(0xC4, "SWAP A", 1) + +# SUBB +op(0x94, "SUBB A,#{imm}", 2) +op(0x95, "SUBB A,{d}", 2) +op(0x96, "SUBB A,@R0", 1) +op(0x97, "SUBB A,@R1", 1) +for i in range(8): op(0x98+i, f"SUBB A,R{i}", 1) + +# INC DPTR +op(0xA3, "INC DPTR", 1) + + +# ============ SFR / XDATA / BIT names ============ + +SFR_NAMES = { + 0x80: "IOA", 0x81: "SP", 0x82: "DPL", 0x83: "DPH", + 0x84: "DPL1", 0x85: "DPH1", 0x86: "DPS", 0x87: "PCON", + 0x88: "TCON", 0x89: "TMOD", 0x8A: "TL0", 0x8B: "TL1", + 0x8C: "TH0", 0x8D: "TH1", 0x8E: "CKCON", + 0x90: "IOB", 0x91: "EXIF", + 0x98: "SCON0", 0x99: "SBUF0", + 0x9A: "AUTOPTRH1", 0x9B: "AUTOPTRL1", + 0x9C: "AUTOPTRH2", 0x9D: "AUTOPTRL2", 0x9E: "AUTOPTRSETUP", + 0xA0: "IOC", 0xA8: "IE", + 0xAA: "EP2468STAT", 0xAB: "EP24FIFOFLGS", 0xAC: "EP68FIFOFLGS", + 0xB0: "IOD", 0xB1: "IOE", + 0xB2: "OEA", 0xB3: "OEB", 0xB4: "OEC", 0xB5: "OED", 0xB6: "OEE", + 0xC0: "SCON1", 0xC1: "SBUF1", + 0xC8: "T2CON", 0xCA: "RCAP2L", 0xCB: "RCAP2H", 0xCC: "TL2", 0xCD: "TH2", + 0xD0: "PSW", 0xD8: "EICON", + 0xE0: "ACC", 0xE8: "EIE", 0xF0: "B", 0xF8: "EIP", +} + +XREG_NAMES = { + 0xE600: "CPUCS", 0xE601: "IFCONFIG", 0xE602: "PINFLAGSAB", + 0xE603: "PINFLAGSCD", 0xE604: "FIFORESET", 0xE609: "REVCTL", + 0xE60A: "GPIFTRIG", 0xE60B: "GPIFSGLDATH", 0xE60C: "GPIFSGLDATLX", + 0xE610: "FLOWSTATE", 0xE611: "FLOWLOGIC", + 0xE618: "GPIFHOLDAMOUNT", + 0xE620: "EP2CFG", 0xE621: "EP4CFG", 0xE622: "EP6CFG", 0xE623: "EP8CFG", + 0xE624: "EP2FIFOCFG", 0xE625: "EP4FIFOCFG", + 0xE626: "EP6FIFOCFG", 0xE627: "EP8FIFOCFG", + 0xE628: "EP2AUTOINLENH", 0xE629: "EP2AUTOINLENL", + 0xE630: "EP2FIFOPFH", 0xE631: "EP2FIFOPFL", + 0xE640: "EP2ISOINPKTS", + 0xE648: "INPKTEND", 0xE649: "OUTPKTEND", + 0xE650: "EP2FIFOIE", 0xE651: "EP2FIFOIRQ", + 0xE65C: "USBIE", 0xE65D: "USBIRQ", + 0xE65E: "EPIE", 0xE65F: "EPIRQ", + 0xE660: "GPIFIE", 0xE661: "GPIFIRQ", + 0xE662: "USBERRIE", 0xE663: "USBERRIRQ", + 0xE666: "INT2IVEC", 0xE667: "INT4IVEC", 0xE668: "INTSETUP", + 0xE670: "PORTACFG", 0xE671: "PORTCCFG", 0xE672: "PORTECFG", + 0xE678: "I2CS", 0xE679: "I2DAT", 0xE67A: "I2CTL", + 0xE67B: "XAUTODAT1", 0xE67C: "XAUTODAT2", + 0xE680: "USBCS", 0xE681: "SUSPEND", 0xE682: "WAKEUPCS", + 0xE683: "TOGCTL", + 0xE684: "USBFRAMEH", 0xE685: "USBFRAMEL", 0xE686: "MICROFRAME", + 0xE687: "FNADDR", + 0xE68A: "EP0BCH", 0xE68B: "EP0BCL", + 0xE68C: "EP1OUTBC", 0xE68D: "EP1INBC", + 0xE68F: "EP1INCS", + 0xE690: "EP2CS", 0xE691: "EP4CS", 0xE692: "EP6CS", 0xE693: "EP8CS", + 0xE694: "EP2FIFOFLGS", 0xE695: "EP4FIFOFLGS", + 0xE696: "EP6FIFOFLGS", 0xE697: "EP8FIFOFLGS", + 0xE698: "EP2BCH", 0xE699: "EP2BCL", + 0xE6A0: "EP0CS", 0xE6A1: "EP0STAT", 0xE6A2: "EP0STALLBITS", + 0xE6A3: "CLRTOGS", + 0xE6A5: "SETUPDAT[0]", 0xE6A6: "SETUPDAT[1]", + 0xE6A7: "SETUPDAT[2]", 0xE6A8: "SETUPDAT[3]", + 0xE6A9: "SETUPDAT[4]", 0xE6AA: "SETUPDAT[5]", + 0xE6AB: "SETUPDAT[6]", 0xE6AC: "SETUPDAT[7]", + 0xE6C0: "GPIFWFSELECT", 0xE6C1: "GPIFIDLECS", + 0xE6C2: "GPIFIDLECTL", 0xE6C3: "GPIFCTLCFG", + 0xE6C4: "GPIFADRH", 0xE6C5: "GPIFADRL", + 0xE6CE: "GPIFREADYCFG", 0xE6CF: "GPIFREADYSTAT", + 0xE6D0: "GPIFABORT", + 0xE6F8: "SUDPTRH", 0xE6F9: "SUDPTRL", 0xE6FA: "SUDPTRCTL", + 0xE740: "EP0BUF[0]", + 0xE780: "EP1OUTBUF[0]", + 0xE7C0: "EP1INBUF[0]", +} + +BIT_NAMES = { + 0xD0: "PSW.P", 0xD1: "PSW.1", 0xD2: "PSW.OV", 0xD3: "PSW.RS0", + 0xD4: "PSW.RS1", 0xD5: "PSW.F0", 0xD6: "PSW.AC", 0xD7: "PSW.CY", + 0xA8: "EX0", 0xA9: "ET0", 0xAA: "EX1", 0xAB: "ET1", + 0xAC: "ES0", 0xAD: "ET2", 0xAE: "ES1", 0xAF: "EA", + 0x80: "IOA.0", 0x81: "IOA.1", 0x82: "IOA.2", 0x83: "IOA.3", + 0x84: "IOA.4", 0x85: "IOA.5", 0x86: "IOA.6", 0x87: "IOA.7", + 0x90: "IOB.0", 0x91: "IOB.1", 0x92: "IOB.2", 0x93: "IOB.3", + 0x94: "IOB.4", 0x95: "IOB.5", 0x96: "IOB.6", 0x97: "IOB.7", + 0x88: "IT0", 0x89: "IE0", 0x8A: "IT1", 0x8B: "IE1", + 0x8C: "TR0", 0x8D: "TF0", 0x8E: "TR1", 0x8F: "TF1", + 0x98: "RI_0", 0x99: "TI_0", + 0xD8: "EICON.0", 0xDB: "INT6", 0xDC: "RESI", 0xDD: "ERESI", +} + +KNOWN_LABELS = { + 0x0000: "reset_vector", + 0x0003: "int0_vector", + 0x000B: "timer0_vector", + 0x0013: "int1_vector", + 0x001B: "timer1_vector", + 0x0023: "serial0_vector", + 0x002B: "timer2_vector", + 0x0033: "resume_vector", + 0x003B: "serial1_vector", + 0x0043: "usb_int2_vector", + 0x004B: "i2c_int3_vector", + 0x0053: "gpif_int4_vector", + 0x005B: "int5_vector", + 0x0063: "int6_vector", + 0x099A: "tune_function", + 0x0DDD: "boot_init_blocks", + 0x0EE9: "tune_init_blocks", + 0x1200: "usb_device_desc", + 0x1556: "i2c_combined_read", + 0x188D: "keil_startup", + 0x1A81: "i2c_write", + 0x1D87: "boot_8psk", + 0x2000: "wait_for_ready", +} + + +def sfr_name(addr): + if addr in SFR_NAMES: + return SFR_NAMES[addr] + if addr >= 0x80: + return f"SFR_{addr:02X}h" + return f"{addr:02X}h" + +def bit_name(addr): + if addr in BIT_NAMES: + return BIT_NAMES[addr] + if addr >= 0x80: + base = addr & 0xF8 + bit = addr & 0x07 + if base in SFR_NAMES: + return f"{SFR_NAMES[base]}.{bit}" + return f"bit_{addr:02X}h" + +def xreg_name(addr): + if addr in XREG_NAMES: + return XREG_NAMES[addr] + if 0xE740 <= addr <= 0xE77F: + return f"EP0BUF[{addr-0xE740}]" + if 0xE780 <= addr <= 0xE7BF: + return f"EP1OUTBUF[{addr-0xE780}]" + if 0xE7C0 <= addr <= 0xE7FF: + return f"EP1INBUF[{addr-0xE7C0}]" + if 0xE600 <= addr <= 0xE6FF: + return f"XSFR_{addr:04X}h" + return None + + +def disasm_one(data, pc): + """Returns (mnem, hex_str, length, comment, branch_target).""" + if pc >= len(data): + return ("???", "", 1, "", None) + + opc = data[pc] + entry = OPCODES.get(opc) + + if entry is None: + return (f"DB {opc:02X}h", f"{opc:02X}", 1, "; UNKNOWN", None) + + mnem_fmt, length, desc = entry + + if pc + length > len(data): + return (f"DB {opc:02X}h", f"{opc:02X}", 1, "; TRUNCATED", None) + + raw_bytes = data[pc:pc+length] + hex_str = " ".join(f"{b:02X}" for b in raw_bytes) + comment = "" + branch_target = None + mnem = mnem_fmt + + # Resolve address fields + if "{a16}" in mnem: + addr16 = (data[pc+1] << 8) | data[pc+2] + label = KNOWN_LABELS.get(addr16) + mnem = mnem.replace("{a16}", label if label else f"{addr16:04X}h") + branch_target = addr16 + + m = re.search(r'\{a11:(\d+)\}', mnem) + if m: + page = int(m.group(1)) + addr11 = (page << 8) | data[pc+1] | ((pc + 2) & 0xF800) + label = KNOWN_LABELS.get(addr11) + mnem = re.sub(r'\{a11:\d+\}', label if label else f"{addr11:04X}h", mnem) + branch_target = addr11 + + if "{r8}" in mnem: + rel_byte = data[pc + length - 1] + rel = rel_byte if rel_byte < 0x80 else rel_byte - 256 + target = (pc + length + rel) & 0xFFFF + label = KNOWN_LABELS.get(target) + mnem = mnem.replace("{r8}", label if label else f"{target:04X}h") + branch_target = target + + # Direct addressing - handle the special MOV d2,d1 case + if "{d2}" in mnem and "{d1}" in mnem: + src = data[pc+1] + dst = data[pc+2] + mnem = mnem.replace("{d1}", sfr_name(src) if src >= 0x20 else f"{src:02X}h") + mnem = mnem.replace("{d2}", sfr_name(dst) if dst >= 0x20 else f"{dst:02X}h") + elif "{d}" in mnem: + d = data[pc+1] + mnem = mnem.replace("{d}", sfr_name(d) if d >= 0x20 else f"{d:02X}h") + + if "{bit}" in mnem: + b = data[pc+1] + mnem = mnem.replace("{bit}", bit_name(b)) + + if "{imm16}" in mnem: + imm16 = (data[pc+1] << 8) | data[pc+2] + xname = xreg_name(imm16) + if xname: + mnem = mnem.replace("{imm16}", f"{imm16:04X}h") + comment = f"; = {xname}" + elif imm16 in KNOWN_LABELS: + mnem = mnem.replace("{imm16}", f"{imm16:04X}h") + comment = f"; -> {KNOWN_LABELS[imm16]}" + else: + mnem = mnem.replace("{imm16}", f"{imm16:04X}h") + + if "{imm}" in mnem: + # Determine which byte is the immediate + if length == 3 and opc in (0x43, 0x53, 0x63): + imm = data[pc+2] # ORL/ANL/XRL d,#imm + elif length == 3 and opc == 0x75: + imm = data[pc+2] # MOV d,#imm + elif length == 3 and opc == 0xB4: + imm = data[pc+1] # CJNE A,#imm,r8 + elif length == 3 and 0xB6 <= opc <= 0xBF: + imm = data[pc+1] # CJNE @Ri/#imm or CJNE Rn,#imm + elif length == 3 and opc == 0xD5: + # DJNZ d,r8 - no imm, but {d} already handled + imm = 0 # shouldn't happen + else: + imm = data[pc + length - 1] + mnem = mnem.replace("{imm}", f"{imm:02X}h") + + return (mnem, hex_str, length, comment, branch_target) + + +def disasm_range(data, start, end, title=""): + lines = [] + if title: + lines.append(f"\n{'='*80}") + lines.append(f" {title}") + lines.append(f"{'='*80}") + + pc = start + while pc < end and pc < len(data): + if pc in KNOWN_LABELS: + lines.append(f"\n {KNOWN_LABELS[pc]}:") + + mnem, hex_str, length, comment, target = disasm_one(data, pc) + addr_str = f"{pc:04X}" + hex_padded = f"{hex_str:<12s}" + line = f" {addr_str}: {hex_padded} {mnem}" + if comment: + line = f"{line:<58s} {comment}" + lines.append(line) + pc += length + + return "\n".join(lines) + + +def track_dptr_accesses(data, start, end): + """Track all MOVX accesses with DPTR context.""" + results = [] + pc = start + dptr = None + while pc < end and pc < len(data): + opc = data[pc] + entry = OPCODES.get(opc) + if entry is None: + pc += 1 + continue + _, length, _ = entry + if pc + length > len(data): + break + if opc == 0x90: # MOV DPTR,#imm16 + dptr = (data[pc+1] << 8) | data[pc+2] + elif opc in (0xE0, 0xF0) and dptr is not None: + xname = xreg_name(dptr) + direction = "READ" if opc == 0xE0 else "WRITE" + results.append((pc, dptr, xname, direction)) + elif opc == 0xA3: # INC DPTR + if dptr is not None: + dptr += 1 + pc += length + return results + + +def main(): + fw_path = "/home/rpm/claude/ham/satellite/genpix/skywalker-1/firmware-dump/skywalker1_eeprom_full64k.bin" + with open(fw_path, "rb") as f: + data = f.read() + + out = [] + out.append("=" * 80) + out.append(" GENPIX SKYWALKER-1 FIRMWARE DISASSEMBLY") + out.append(" FX2LP (CY7C68013A) 8051 Core - Keil C51 Compiled") + out.append(" Firmware size: 9472 bytes (0x0000-0x24FF)") + out.append(" Binary: 65536 bytes total (RAM image)") + out.append("=" * 80) + + # ==== SECTION 1: Reset + Interrupt Vectors ==== + out.append(disasm_range(data, 0x0000, 0x006B, + "SECTION 1: Reset Vector & Interrupt Vector Table (0x0000-0x006A)")) + + # ==== SECTION 2: Keil C51 Startup ==== + out.append(disasm_range(data, 0x188D, 0x1950, + "SECTION 2: Keil C51 Startup (STARTUP.A51) @ 0x188D")) + + # Find main() by tracing startup + pc = 0x188D + main_addr = None + for _ in range(500): + if pc >= len(data) - 2: + break + opc = data[pc] + entry = OPCODES.get(opc) + if entry is None: + break + _, length, _ = entry + if opc == 0x02: # LJMP + addr = (data[pc+1] << 8) | data[pc+2] + # The startup has two LJMPs: one internal loop, one to main + # main() is the last LJMP before we hit data/different code + if addr < 0x188D: # Jump backward = likely to main() + main_addr = addr + pc += length + + if main_addr: + KNOWN_LABELS[main_addr] = "main" + out.append(f"\n >>> Keil startup jumps to main() at {main_addr:04X}h") + + # ==== SECTION 3: main() ==== + if main_addr: + # Disassemble main() - generous range to cover init + out.append(disasm_range(data, main_addr, min(main_addr + 400, 0x2500), + f"SECTION 3: main() @ {main_addr:04X}h (first ~400 bytes)")) + + # ==== SECTION 4: Vendor command dispatch ==== + out.append("\n" + "=" * 80) + out.append(" SECTION 4: USB Vendor Command Dispatch") + out.append("=" * 80) + + # Find all reads of SETUPDAT[1] (bRequest) + for pc in range(0x2500): + if pc + 2 < len(data) and data[pc] == 0x90 and data[pc+1] == 0xE6 and data[pc+2] == 0xA6: + out.append(f"\n SETUPDAT[1] read at {pc:04X}h:") + ctx_start = max(0, pc - 16) + ctx_end = min(pc + 120, 0x2500) + out.append(disasm_range(data, ctx_start, ctx_end, + f" Dispatch context @ {pc:04X}h")) + + # ==== SECTION 5: Find all CJNE A,#xx comparisons (command dispatch) ==== + out.append("\n" + "=" * 80) + out.append(" SECTION 5: All CJNE A,#xx Instructions (Command Dispatch Table)") + out.append("=" * 80) + + for pc in range(0x2500): + if pc + 2 < len(data) and data[pc] == 0xB4: + imm = data[pc+1] + rel = data[pc+2] + if rel >= 0x80: + rel -= 256 + target = (pc + 3 + rel) & 0xFFFF + out.append(f" {pc:04X}: CJNE A,#{imm:02X}h,{target:04X}h ; bRequest?=0x{imm:02X} skip->{target:04X}h") + + # ==== SECTION 6: boot_8psk ==== + out.append(disasm_range(data, 0x1D87, min(0x1D87 + 250, 0x2500), + "SECTION 6: boot_8psk() @ 0x1D87")) + + # ==== SECTION 7: SFR access map ==== + out.append("\n" + "=" * 80) + out.append(" SECTION 7: All XDATA SFR Accesses (DPTR-tracked)") + out.append("=" * 80) + + accesses = track_dptr_accesses(data, 0x0000, 0x2500) + for pc, addr, name, direction in accesses: + name_str = name if name else f"XDATA_{addr:04X}h" + out.append(f" {pc:04X}: {direction:5s} [{addr:04X}h] {name_str}") + + # ==== SECTION 8: Key functions ==== + out.append(disasm_range(data, 0x0DDD, 0x0DDD + 120, + "SECTION 8: boot_init_blocks() @ 0x0DDD")) + + out.append(disasm_range(data, 0x1A81, 0x1A81 + 150, + "SECTION 9: i2c_write() @ 0x1A81")) + + out.append(disasm_range(data, 0x1556, 0x1556 + 150, + "SECTION 10: i2c_combined_read() @ 0x1556")) + + out.append(disasm_range(data, 0x2000, 0x2000 + 120, + "SECTION 11: wait_for_ready() @ 0x2000")) + + out.append(disasm_range(data, 0x099A, 0x099A + 200, + "SECTION 12: tune_function() @ 0x099A")) + + # ==== SECTION 13: USB Descriptors ==== + out.append("\n" + "=" * 80) + out.append(" SECTION 13: USB Descriptors @ 0x1200") + out.append("=" * 80) + + desc = data[0x1200:0x1212] + out.append(f" Device Descriptor (18 bytes):") + out.append(f" bLength: {desc[0]} (0x{desc[0]:02X})") + out.append(f" bDescriptorType: {desc[1]} (DEVICE)") + out.append(f" bcdUSB: {desc[3]:02X}.{desc[2]:02X}") + out.append(f" bDeviceClass: 0x{desc[4]:02X} (vendor-specific)") + out.append(f" bDeviceSubClass: 0x{desc[5]:02X}") + out.append(f" bDeviceProtocol: 0x{desc[6]:02X}") + out.append(f" bMaxPacketSize0: {desc[7]} bytes") + vid = desc[8] | (desc[9] << 8) + pid = desc[10] | (desc[11] << 8) + out.append(f" idVendor: 0x{vid:04X} (Cypress)") + out.append(f" idProduct: 0x{pid:04X}") + bcd = desc[12] | (desc[13] << 8) + out.append(f" bcdDevice: {bcd:04X}") + out.append(f" iManufacturer: {desc[14]}") + out.append(f" iProduct: {desc[15]}") + out.append(f" iSerialNumber: {desc[16]}") + out.append(f" bNumConfigurations: {desc[17]}") + + # Raw descriptor dump + out.append(f"\n Raw descriptor area 0x1200-0x1300:") + for off in range(0, 0x100, 16): + addr = 0x1200 + off + chunk = data[addr:addr+16] + hexl = " ".join(f"{b:02X}" for b in chunk) + asciil = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk) + out.append(f" {addr:04X}: {hexl} {asciil}") + + # ==== SECTION 14: Hex dump of interesting code regions ==== + out.append("\n" + "=" * 80) + out.append(" SECTION 14: Raw Hex - Code Space Near USB ISR Targets") + out.append("=" * 80) + + # Show hex around the USB interrupt vector dispatch + # The USB ISR at 0x0043 likely jumps somewhere + for start_addr in [0x0043, 0x004B]: + out.append(f"\n Hex dump at {start_addr:04X}h:") + for off in range(0, 48, 16): + addr = start_addr + off + chunk = data[addr:addr+16] + hexl = " ".join(f"{b:02X}" for b in chunk) + out.append(f" {addr:04X}: {hexl}") + + print("\n".join(out)) + + +if __name__ == "__main__": + main() diff --git a/firmware-dump/stock_firmware.bin b/firmware-dump/stock_firmware.bin new file mode 100644 index 0000000000000000000000000000000000000000..f94dcd09b070e6476f94d9af8629e1fc479fb71e GIT binary patch literal 15360 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ξm>L{LaH|7-8lFf zk84Py$*D-_z6D_fP=@Uv#PC6i!=`)tO;=WWw2)hrV;??fn7PThU_=h`VxDS*lIrCl z)$4tANAVpV_;vOY^926m~Aj8RLH6KXhWBg0u6gt*HjzL}Uy8y^S zb!B~ppJ^@d^e_hvZx273QsdrQRmYcV94SZ0kzEdw!34ZK1F$@g>d;l3+-x#t84@}E zJ@r(kZ8W09f-)_NbJ_7J4da^PvR;S}-=TZ_I^29A+-iN5V!=ylE|`Hk?M5!}iwa$x z;4Z3i++oi9&wG2}1zQ|8O$>^L(*#^sY*aZJdbu4InPFcb*i{-~RnZIfBpkYQ5V)Ed zVO8=faN!Hc=TYNTA>1_7#41jZ7V=rVk{l_7m>n@V%QwJX-Kx7hj0>{9w0V{leuMzu zvs`%BG4HgI;5^Qkpez|b?3{AigmcnZO}}^{pUpIa zKR+!%M%PHcT@T<`bQ_{)Xi0}m(S}V?O$XGI zKVbTT$Cu%c;R*mB#G#o1B4a^W8UwMhxa_J*Fcy@VfN;CG%+F*Q4eC{kfeRSiz6N!U z4|%u=%x~v2&?8hoh6*mR2#j5W@3o7W>e40EPd?J5{In=SY~YKp_#twG(KnV!$C#{3 zfyPS=V5H2{{G=Hol=@Garhm(yJoGo@fgukJd0@x`Lmn9Nz>o)qJTT;eArB0BV8{bQ d9vJe#kOzi5Fyw(D4-9!=$OA(j_