diff --git a/docs/EEPROM-RECOVERY.md b/docs/EEPROM-RECOVERY.md new file mode 100644 index 0000000..92003cf --- /dev/null +++ b/docs/EEPROM-RECOVERY.md @@ -0,0 +1,144 @@ +# SkyWalker-1 EEPROM Recovery Guide + +The device is soft-bricked: the FX2 boot ROM hangs trying to load +corrupted firmware from EEPROM, preventing USB enumeration. + +## Symptoms + +- Hub shows `0101 power connect []` (D+ pull-up active, no enumeration) +- dmesg: `device descriptor read/8, error -110` (timeout) +- Does not enumerate as bare FX2 (04B4:8613) either +- NanoVNA on same hub works fine (hub hardware is OK) + +## Root Cause + +The EEPROM (24C128 at I2C 0x51) likely has corrupted boot data. The +FX2LP boot ROM reads the EEPROM at power-up and hangs if the C2 image +has invalid load record lengths or addresses. The boot ROM occupies +the 8051 core, preventing USB control transfer processing. + +## Recovery Options (pick one) + +### Option A: SOIC Clip + External Programmer (Recommended) + +Blank the first byte of the EEPROM so the boot ROM falls back to +bare FX2 enumeration. Then reload via USB. + +**Hardware needed:** +- SOIC-8 test clip (Pomona 5250 or similar, ~$5) +- CH341A USB programmer (~$3) or Bus Pirate or any I2C-capable tool +- OR: Raspberry Pi / Arduino with I2C + +**Steps:** +1. Power OFF the SkyWalker-1 (unplug USB) +2. Locate the 24C128 EEPROM on the PCB (SOIC-8 package near the FX2) +3. Clip the SOIC clip onto the EEPROM +4. Connect to your I2C programmer (SDA, SCL, VCC, GND) +5. Read and save the EEPROM contents (16KB backup!) +6. Write 0xFF to address 0x0000 (corrupts the C2 magic byte) +7. Remove clip, plug in SkyWalker-1 +8. Device should enumerate as bare FX2 (04B4:8613) +9. Load custom firmware via `fw_load.py` +10. Use the custom firmware to write good C2 image back to EEPROM + +**With CH341A:** +```bash +# Read backup +flashrom -p ch341a_spi -c "AT24C128" -r eeprom_backup.bin + +# Or use i2c-tools if CH341A is in I2C mode: +# i2cdetect -l (find the CH341A bus) +# i2cdump -y 0x51 b > dump.txt +``` + +**With Raspberry Pi (I2C):** +```bash +# Enable I2C: raspi-config -> Interfaces -> I2C +# Connect EEPROM: SDA->GPIO2, SCL->GPIO3, VCC->3.3V, GND->GND +i2cdetect -y 1 # Should show 0x51 +# Read first byte +i2cget -y 1 0x51 0x00 +# Write 0xFF to byte 0 (corrupts C2 header) +i2cset -y 1 0x51 0x00 0xFF +``` + +### Option B: Hold SDA HIGH During Boot + +Prevent the EEPROM from responding by holding SDA HIGH, forcing +the boot ROM to see "no EEPROM" and enumerate as bare FX2. + +**Steps:** +1. Locate the SDA test point or EEPROM pin 5 (SDA) +2. Connect a 1kΩ pull-up to 3.3V on SDA +3. Power on the SkyWalker-1 +4. If it enumerates as bare FX2 (04B4:8613), load firmware: + ```bash + python3 tools/fw_load.py load firmware/build/skywalker1.ihx + ``` +5. Remove the pull-up +6. Use the loaded firmware to reprogram the EEPROM + +**Note:** This only works if the SDA pull-up is strong enough to +override the EEPROM's SDA output. May need to experiment with +pull-up values (470Ω to 4.7kΩ). + +### Option C: Desolder EEPROM Pin + +Most reliable but requires soldering skill. + +1. Lift EEPROM pin 5 (SDA) from the PCB pad +2. Power on → enumerates as bare FX2 +3. Load firmware via USB +4. Resolder pin 5 +5. Use firmware to reprogram EEPROM with good C2 image + +### Option D: Wait + Watch (Long Shot) + +If the boot ROM eventually times out on the I2C read, the device +will briefly enumerate as bare FX2. This might take several minutes. + +```bash +# Watch for bare FX2 enumeration +sudo dmesg -w | grep -E "04b4|8613|New USB" + +# In another terminal, keep power cycling every 5 minutes +while true; do + sudo uhubctl -l 1-5.4.4 -p 3 -a off + sleep 5 + sudo uhubctl -l 1-5.4.4 -p 3 -a on + sleep 300 # wait 5 minutes +done +``` + +If it appears even briefly: +```bash +python3 tools/fw_load.py load firmware/build/skywalker1.ihx --force +``` + +## After Recovery + +Once the device enumerates (as bare FX2 or with loaded firmware): + +1. **Load custom firmware to RAM:** + ```bash + python3 tools/fw_load.py load firmware/build/skywalker1.ihx + ``` + +2. **Reprogram EEPROM with good C2 image:** + ```bash + # The custom firmware needs EEPROM write support first + # (vendor command to relay I2C writes to EEPROM) + python3 tools/eeprom_write.py flash firmware/build/skywalker1_eeprom.bin + ``` + +3. **Or restore stock firmware:** + If you have a backup of the original EEPROM contents, flash that + instead of the custom firmware. + +## Prevention + +- Never send `BOOT_8PSK (0x89)` with mode 0x84 ("firmware load") + unless you know what data the firmware expects +- Always backup EEPROM before experiments that touch vendor commands +- The stock firmware's I2C proxy (0x83/0x84) may have side effects + on the EEPROM that aren't documented diff --git a/docs/skywalker1-master-reference.md b/docs/skywalker1-master-reference.md index fa7951b..1cdf06a 100644 --- a/docs/skywalker1-master-reference.md +++ b/docs/skywalker1-master-reference.md @@ -1,1706 +1,1722 @@ -# Genpix SkyWalker-1 Master Hardware and Firmware Reference - -Consolidated technical reference for the Genpix SkyWalker-1 DVB-S USB 2.0 satellite receiver. Derived from Linux kernel driver analysis (`dvb_usb_gp8psk`), Ghidra firmware reverse engineering (v2.06, v2.10 Rev.2, v2.13 FW1/FW2/FW3), Windows BDA driver source review, and custom firmware development (v3.01.0, SDCC + fx2lib). - ---- - -## Table of Contents - -1. [Hardware Overview](#1-hardware-overview) -2. [USB Interface](#2-usb-interface) -3. [Vendor Command Reference](#3-vendor-command-reference) -4. [Configuration Status Byte](#4-configuration-status-byte) -5. [Boot Sequence](#5-boot-sequence) -6. [BCM4500 Demodulator Interface](#6-bcm4500-demodulator-interface) -7. [Tuning Protocol](#7-tuning-protocol) -8. [GPIF Streaming Path](#8-gpif-streaming-path) -9. [LNB and DiSEqC Control](#9-lnb-and-diseqc-control) -10. [GPIO Pin Map](#10-gpio-pin-map) -11. [Firmware Versions](#11-firmware-versions) -12. [I2C Bus Architecture](#12-i2c-bus-architecture) -13. [Custom Firmware v3.01.0](#13-custom-firmware-v3010) -14. [DVB-S2 Incompatibility](#14-dvb-s2-incompatibility) -15. [Kernel Driver Notes](#15-kernel-driver-notes) -16. [Firmware Storage Formats](#16-firmware-storage-formats) -17. [Debugging Reference](#17-debugging-reference) -18. [Sources](#18-sources) - ---- - -## 1. Hardware Overview - -The Genpix SkyWalker-1 is a standalone USB 2.0 DVB-S satellite receiver built around two ICs: - -| Component | Part | Role | -|-----------|------|------| -| MCU | Cypress CY7C68013A (FX2LP) | USB 2.0 Hi-Speed controller, 8051 core at 48 MHz | -| Demodulator | Broadcom BCM4500 | DVB-S / Turbo / DCII / DSS demodulator, 128-pin MQFP | -| EEPROM | 24Cxx-family (I2C address 0x51) | FX2 firmware storage, serial number, calibration | -| Tuner/LNB | Unknown IC (I2C address 0x10) | Tuner or LNB controller on shared I2C bus | - -The FX2 handles USB communication, LNB control, DiSEqC signaling, and orchestrates tuning via I2C commands to the BCM4500. The BCM4500 performs RF demodulation, forward error correction, and outputs an MPEG-2 transport stream on an 8-bit parallel bus. The FX2's GPIF engine transfers the transport stream directly into a USB bulk endpoint with zero firmware intervention in the data path. - -### 1.1 Supported Modulations - -| Index | Modulation | Constant | FEC Family | -|-------|-----------|----------|------------| -| 0 | DVB-S QPSK | `ADV_MOD_DVB_QPSK` | Viterbi + Reed-Solomon | -| 1 | Turbo-coded QPSK | `ADV_MOD_TURBO_QPSK` | Turbo | -| 2 | Turbo-coded 8PSK | `ADV_MOD_TURBO_8PSK` | Turbo | -| 3 | Turbo-coded 16QAM | `ADV_MOD_TURBO_16QAM` | Turbo | -| 4 | Digicipher II Combo | `ADV_MOD_DCII_C_QPSK` | DCII | -| 5 | Digicipher II I-stream (split) | `ADV_MOD_DCII_I_QPSK` | DCII | -| 6 | Digicipher II Q-stream (split) | `ADV_MOD_DCII_Q_QPSK` | DCII | -| 7 | Digicipher II Offset QPSK | `ADV_MOD_DCII_C_OQPSK` | DCII | -| 8 | DSS QPSK | `ADV_MOD_DSS_QPSK` | Viterbi + Reed-Solomon | -| 9 | DVB-S BPSK | `ADV_MOD_DVB_BPSK` | Viterbi + Reed-Solomon | - -DVB-S2 is not supported. See [Section 14](#14-dvb-s2-incompatibility). - -### 1.2 RF Specifications - -| Parameter | Value | -|-----------|-------| -| IF frequency range | 950 -- 2150 MHz | -| Symbol rate | 256 Ksps -- 30 Msps | -| Input connector | IEC F-type female | -| LNB voltage | 13V / 18V (or 14V / 19V with USE_EXTRA_VOLT) | -| LNB current | 450 mA continuous, 750 mA burst | -| Switch control | 22 kHz, Tone Burst, DiSEqC 1.0/1.2, Legacy Dish Network | - -### 1.3 Board Block Diagram - -``` - +--[ I2C EEPROM 0x51 ] - | - USB 2.0 HS | I2C Bus (400 kHz) - Host PC <----> [ CY7C68013A FX2LP ] <-----> [ BCM4500 Demod 0x08 ] - | 8051 @ 48 MHz | | - | GPIF Engine |<-----------+ 8-bit parallel TS - | EP2 Bulk IN | - | GPIO (P0/P3) |---> [ 22 kHz Osc ] ---> LNB/Coax - | |---> [ LNB Voltage Ctrl ] - +-----------------+ - | - +--[ Tuner/LNB IC 0x10 ] -``` - ---- - -## 2. USB Interface - -### 2.1 VID/PID Table - -All Genpix products share VID `0x09C0`: - -| PID | Product | cold_ids | warm_ids | Notes | -|-----|---------|----------|----------|-------| -| 0x0200 | 8PSK-to-USB2 Rev.1 Cold | Yes | No | Requires FW01 upload to RAM | -| 0x0201 | 8PSK-to-USB2 Rev.1 Warm | No | Yes | Requires FW02 (BCM4500 firmware) | -| 0x0202 | 8PSK-to-USB2 Rev.2 | No | Yes | Boots from EEPROM | -| 0x0203 | **SkyWalker-1** | No | Yes | Boots from EEPROM | -| 0x0204 | SkyWalker-1 (alternate) | No | Yes | Boots from EEPROM | -| 0x0205 | SkyWalker-2 | -- | -- | Not in kernel 6.16.5 | -| 0x0206 | SkyWalker CW3K | No | Yes | Requires CW3K_INIT (0x9D) | - -PID 0x0203 was added to the kernel device table after v6.6.1. - -### 2.2 USB Endpoints and Streaming Properties - -| Property | Value | -|----------|-------| -| Control endpoint | EP0 (default, vendor requests) | -| Bulk IN endpoint | EP2 (0x82) -- MPEG-2 transport stream | -| Generic bulk CTRL endpoint | 0x01 (BCM4500 FW02 upload, Rev.1 only) | -| URB count | 7 | -| URB buffer size | 8192 bytes each | -| Stream type | USB_BULK | -| FX2 controller type | CYPRESS_FX2 | - -### 2.3 Warm Boot Behavior - -The SkyWalker-1 (PID 0x0203) enumerates directly as a "warm" device. The DVB-USB framework skips firmware download when `cold_ids` is NULL. No host-side firmware files are required. - -| Device | PID | Needs FW01? | Needs FW02? | Boot Source | -|--------|-----|-------------|-------------|-------------| -| Rev.1 Cold | 0x0200 | Yes | -- | RAM (empty) | -| Rev.1 Warm | 0x0201 | No | Yes | RAM (FW01 loaded) | -| Rev.2 | 0x0202 | No | No | EEPROM | -| SkyWalker-1 | 0x0203 | No | No | EEPROM | -| SkyWalker CW3K | 0x0206 | No | No | EEPROM | - -The firmware files `dvb-usb-gp8psk-01.fw` and `dvb-usb-gp8psk-02.fw` were never open-sourced or included in `linux-firmware`. - ---- - -## 3. Vendor Command Reference - -All vendor commands use USB control transfers: -- **USB Type**: `USB_TYPE_VENDOR` -- **Timeout**: 2000 ms (kernel driver) -- **Retry**: Up to 3 attempts for IN operations if partial data received -- **Data buffer maximum**: 80 bytes (kernel driver state structure) - -### 3.1 Stock Command Table (0x80--0x9D) - -The vendor command dispatcher at CODE:0056 validates `bRequest` in the range 0x80--0x9D (30 entries) and dispatches via an indexed jump table at CODE:0076. Rev.2 supports only 0x80--0x9A (27 entries). - -| Cmd | Name | Dir | wValue | wIndex | wLength | Purpose | v2.06 | Rev.2 | v2.13 | -|-----|------|-----|--------|--------|---------|---------|-------|-------|-------| -| 0x80 | GET_8PSK_CONFIG | IN | 0 | 0 | 1 | Read configuration status byte | OK | OK | OK | -| 0x81 | SET_8PSK_CONFIG | OUT | varies | 0 | 0 | Set config (reserved) | STALL | STALL | STALL | -| 0x82 | (reserved) | -- | -- | -- | -- | Reserved | STALL | STALL | STALL | -| 0x83 | I2C_WRITE | OUT | dev_addr | reg_addr | N | Write to I2C device | OK | OK | OK | -| 0x84 | I2C_READ | IN | dev_addr | reg_addr | N | Read from I2C device | OK | OK | OK | -| 0x85 | ARM_TRANSFER | OUT | 0/1 | 0 | 0 | Start (1) / stop (0) MPEG-2 stream | OK | OK | OK | -| 0x86 | TUNE_8PSK | OUT | 0 | 0 | 10 | Set tuning parameters ([Section 7](#7-tuning-protocol)) | OK | OK | OK | -| 0x87 | GET_SIGNAL_STRENGTH | IN | 0 | 0 | 6 | Read SNR and diagnostics | OK | OK | Changed | -| 0x88 | LOAD_BCM4500 | OUT | 1 | 0 | 0 | Initiate BCM4500 FW download | STALL | STALL | STALL | -| 0x89 | BOOT_8PSK | IN | 0/1 | 0 | 1 | Power on (1) / off (0) demodulator | OK | OK | OK | -| 0x8A | START_INTERSIL | IN | 0/1 | 0 | 1 | Enable (1) / disable (0) LNB supply | OK | OK | OK | -| 0x8B | SET_LNB_VOLTAGE | OUT | 0/1 | 0 | 0 | 13V (0) or 18V (1) | OK | OK | OK | -| 0x8C | SET_22KHZ_TONE | OUT | 0/1 | 0 | 0 | Tone off (0) or on (1) | OK | OK | OK | -| 0x8D | SEND_DISEQC_COMMAND | OUT | msg[0] | 0 | len | DiSEqC message or tone burst | OK | OK | OK | -| 0x8E | SET_DVB_MODE | OUT | 1 | 0 | 0 | Enable DVB-S mode | STALL | STALL | STALL | -| 0x8F | SET_DN_SWITCH | OUT | cmd7bit | 0 | 0 | Legacy Dish Network switch protocol | OK | OK | OK | -| 0x90 | GET_SIGNAL_LOCK | IN | 0 | 0 | 1 | Read signal lock status | OK | OK | OK | -| 0x91 | I2C_ADDR_ADJUST | IN | 0/1 | 0 | 1 | Inc/dec internal counter (debug) | OK | OK | OK | -| 0x92 | GET_FW_VERS | IN | 0 | 0 | 6 | Read firmware version + build date | OK | OK | OK | -| 0x93 | GET_SERIAL_NUMBER | IN | 0 | 0 | 4 | Read 4-byte serial from EEPROM | OK | OK | OK | -| 0x94 | USE_EXTRA_VOLT | OUT | 0/1 | 0 | 0 | Enable +1V LNB boost (14V/19V) | OK | OK | OK | -| 0x95 | GET_FPGA_VERS | IN | 0 | 0 | 1 | Read EEPROM hardware/platform ID | OK | OK | OK | -| 0x96 | SET_LNB_GPIO_MODE | OUT | 0/1 | 0 | 0 | Configure LNB GPIO output enables | OK | OK | OK | -| 0x97 | SET_GPIO_PINS | OUT | bitmap | 0 | 0 | Direct write to LNB GPIO pins | OK | OK | OK | -| 0x98 | GET_GPIO_STATUS | IN | 0 | 0 | 1 | Read LNB feedback GPIO pin | OK | OK | OK | -| 0x99 | GET_DEMOD_STATUS | IN | 0 | 0 | 1 | Read BCM4500 register 0xF9 | STALL | Proto | OK | -| 0x9A | INIT_DEMOD | OUT | 0 | 0 | 0 | Trigger demod re-init (3 attempts) | STALL | Proto | OK | -| 0x9B | (reserved) | -- | -- | -- | -- | Reserved | STALL | N/A | STALL | -| 0x9C | DELAY_COMMAND | OUT | delay | 0 | 0 | Host-controlled tuning delay + poll | STALL | N/A | OK | -| 0x9D | CW3K_INIT / SET_MODE_FLAG | OUT | 0/1 | 0 | 0 | CW3K init or conditional demod reset | OK | N/A | Changed | - -**Status key**: OK = implemented. STALL = routes to stall handler. Proto = partial/prototype. N/A = out of range (Rev.2 supports 0x80--0x9A only). Changed = implementation differs between versions. - -**Driver usage notes**: -- The Linux driver only sends LOAD_BCM4500 (0x88) for Rev.1 Warm (PID 0x0201). On SkyWalker-1, `bm8pskFW_Loaded` is already set and 0x88 STALLs. -- The Linux driver only sends CW3K_INIT (0x9D) for SkyWalker CW3K (PID 0x0206). - -### 3.2 Vendor Command Dispatch Mechanism - -The vendor command dispatcher at CODE:0056 (identical code address across v2.06, v2.13, and Rev.2) follows this logic: - -``` -1. Check bmRequestType bit 6: if not set, not a vendor request -> handle standard -2. Read bRequest from SETUPDAT[1] -3. Subtract 0x80 (command base offset) -4. Compare against maximum: < 0x1E (v2.06/v2.13) or < 0x1B (Rev.2) -5. If in range: double the index (2 bytes per entry) and JMP @A+DPTR to jump table -6. If out of range: route to STALL handler -``` - -The jump table at CODE:0076 contains 2-byte AJMP targets. Each entry points to the handler for commands 0x80 through 0x9D (or 0x9A for Rev.2). - -**Jump table layout (first 6 entries shown, Rev.2):** - -``` -CODE:0076: 01C1 ; 0x80 GET_8PSK_CONFIG -> 0x01C1 -CODE:0078: 034B ; 0x81 SET_8PSK_CONFIG -> 0x034B (STALL) -CODE:007A: 034B ; 0x82 (reserved) -> 0x034B (STALL) -CODE:007C: 0103 ; 0x83 I2C_WRITE -> 0x0103 -CODE:007E: 00D9 ; 0x84 I2C_READ -> 0x00D9 -CODE:0080: 00C2 ; 0x85 ARM_TRANSFER -> 0x00C2 -... -``` - -### 3.3 Custom Firmware Commands (0xB0--0xB6) - -Commands added in custom firmware v3.01.0: - -| Cmd | Name | Dir | wValue | wIndex | wLength | Purpose | -|-----|------|-----|--------|--------|---------|---------| -| 0xB0 | SPECTRUM_SWEEP | OUT | 0 | 0 | 10 | Step through freq range, read SNR at each step | -| 0xB1 | RAW_DEMOD_READ | IN | reg | 0 | 1 | Read BCM4500 indirect register | -| 0xB2 | RAW_DEMOD_WRITE | OUT | reg | data | 0 | Write BCM4500 indirect register | -| 0xB3 | BLIND_SCAN | OUT | 0 | 0 | 16 | Try symbol rates at given freq, report lock | -| 0xB4 | I2C_BUS_SCAN | IN | 0 | 0 | 16 | Probe all 7-bit addresses, return 16-byte bitmap | -| 0xB5 | I2C_RAW_READ | IN | addr7 | reg | N | Combined write-read from any I2C device | -| 0xB6 | I2C_DIAG | IN | page | 0 | 8 | Step-by-step indirect register diagnostic | - -### 3.4 Detailed Parameter Formats - -**0x87 GET_SIGNAL_STRENGTH**: Returns 6 bytes. Bytes 0--1 are a 16-bit SNR value (little-endian, dBu * 256 units). Bytes 2--5 are reserved/diagnostic. SNR scaling from Windows BDA driver: `if snr_raw <= 0x0F00: strength = snr_raw * 17; else strength = 0xFFFF`. Version differences: v2.06 polls 3 registers (0xA2, 0xA8, 0xA4) up to 6 times; v2.13 consolidates to 1 register. - -**0x8D SEND_DISEQC_COMMAND**: When `wLength > 0`, the payload is a standard DiSEqC message (3--6 bytes) with `wValue` = `msg[0]` (framing byte, typically 0xE0 or 0xE1). When `wLength == 0`: `wValue == 0` sends tone burst A; `wValue != 0` sends tone burst B. See [Section 9](#9-lnb-and-diseqc-control). - -**0x8F SET_DN_SWITCH**: `wValue` carries a 7-bit Dish Network switch command, bit-banged LSB-first on GPIO P0.4. The 8th bit (0x80) of the original switch command controls LNB voltage and is sent separately via SET_LNB_VOLTAGE (0x8B). - -**0x92 GET_FW_VERS**: Returns 6 bytes of hardcoded constants: - -``` -Byte 0: version minor_minor (e.g., 0x04) -Byte 1: version minor (e.g., 0x06) -Byte 2: version major (e.g., 0x02) -Byte 3: build day (e.g., 0x0D = 13) -Byte 4: build month (e.g., 0x07 = July) -Byte 5: build year - 2000 (e.g., 0x07 = 2007) -``` - -Full version = `byte[2] << 16 | byte[1] << 8 | byte[0]`. Build date = `(2000 + byte[5]) / byte[4] / byte[3]`. - -**0x93 GET_SERIAL_NUMBER**: Returns 4 bytes read from I2C EEPROM at device address 0x51 (7-bit), extracted at 8-bit intervals using a shift/rotate routine. - -**0x94 USE_EXTRA_VOLT**: `wValue=1` writes 0x6A to XRAM 0xE0B6; `wValue=0` writes 0x62. The difference is bit 3 (0x08), which controls the voltage boost on the LNB power regulator. - -**0x95 GET_FPGA_VERS**: Reads from I2C EEPROM at 0x51. Despite the name, there is no FPGA on the SkyWalker-1 -- this returns a hardware platform ID. v2.06 reads EEPROM offset 0x31 (2 bytes); v2.13/Rev.2 read offset 0x00 (1 byte). - -**0xB0 SPECTRUM_SWEEP**: 10-byte EP0 payload: `[start_freq(u32 LE kHz), stop_freq(u32 LE kHz), step_khz(u16 LE)]`. Programs BCM4500 at each frequency step, reads SNR, packs u16 LE results into EP2 bulk FIFO. - -**0xB3 BLIND_SCAN**: 16-byte EP0 payload: `[freq_khz(u32 LE), sr_min(u32 LE sps), sr_max(u32 LE sps), sr_step(u32 LE sps)]`. Returns 8 bytes on lock `[freq_khz(4) + sr_locked(4)]` or 1 byte 0x00 if no lock found. - -**0xB4 I2C_BUS_SCAN**: Returns a 16-byte bitmap (128 bits for addresses 0x00--0x77). Each bit position corresponds to a 7-bit address; bit set = ACK received. Known devices on the SkyWalker-1 bus: - -| Address | Identity | -|---------|----------| -| 0x08 | BCM4500 demodulator (7-bit; wire addresses 0x10 write / 0x11 read) | -| 0x10 | Tuner or LNB controller | -| 0x51 | Configuration EEPROM (24Cxx-family) | - ---- - -## 4. Configuration Status Byte - -Returned by GET_8PSK_CONFIG (0x80). Stored in IRAM at a version-dependent address. - -``` -Bit 7 (0x80): bmArmed - MPEG-2 stream transfer armed / GPIF active -Bit 6 (0x40): bmDCtuned - DC offset tuning complete (set for DCII modes) -Bit 5 (0x20): bmSEL18V - 18V LNB voltage selected (else 13V) -Bit 4 (0x10): bm22kHz - 22 kHz tone active -Bit 3 (0x08): bmDVBmode - DVB mode enabled -Bit 2 (0x04): bmIntersilOn - LNB power supply enabled -Bit 1 (0x02): bm8pskFW_Loaded - BCM4500 firmware loaded (always set on SkyWalker-1) -Bit 0 (0x01): bm8pskStarted - Device booted and running -``` - -| Firmware | IRAM Address | -|----------|-------------| -| v2.06 | 0x6D | -| Rev.2 v2.10.4 | 0x4E | -| v2.13 | 0x4F | - -The kernel driver checks these bits to decide which initialization steps to perform. On the SkyWalker-1 after a successful BOOT_8PSK, `config_status = 0x03` (STARTED + FW_LOADED). - ---- - -## 5. Boot Sequence - -### 5.1 Kernel Driver Boot Flow - -``` -1. GET_8PSK_CONFIG (0x80) -- read config status byte - |-- Check bit 0: bm8pskStarted? - -2. If not started: - |-- BOOT_8PSK (0x89, wValue=1) - |-- GET_FW_VERS (0x92) -- read firmware version - -3. If bit 1 clear (bm8pskFW_Loaded): - |-- LOAD_BCM4500 (0x88) -- Rev.1 Warm only; STALLs on SkyWalker-1 - -4. If bit 2 clear (bmIntersilOn): - |-- START_INTERSIL (0x8A, wValue=1) -- enable LNB power supply - -5. SET_DVB_MODE (0x8E, wValue=1) -- STALLs on all SkyWalker-1 FW versions - -6. ARM_TRANSFER (0x85, wValue=0) -- abort any pending MPEG transfer - -7. Device ready for tuning -``` - -### 5.2 BCM4500 Boot Sequence (BOOT_8PSK, 0x89) - -As implemented in `bcm4500_boot()` in custom firmware v3.01.0, reverse-engineered from stock v2.06 `FUN_CODE_1D4F` + `FUN_CODE_0ddd`: - -``` -Step Action GPIO/I2C Duration ----- ------------------------------------ ----------------- -------- -1 Assert BCM4500 RESET P0.5 = LOW -- -2 Power on P0.1 = HIGH -- - P0.2 = LOW -3 Wait for power settle -- 30 ms -4 Release RESET P0.5 = HIGH -- -5 Wait for BCM4500 POR + ROM boot -- 50 ms -6 I2C probe (read register 0xA2) I2C read 0x08:0xA2 ~0.1 ms -7 Write init block 0 to page 0 I2C write 0xA6/A7/A8 ~2 ms -8 Write init block 1 to page 0 I2C write 0xA6/A7/A8 ~2 ms -9 Write init block 2 to page 0 I2C write 0xA6/A7/A8 ~1 ms -10 Set config_status = 0x03 -- -- -``` - -**Total boot time**: approximately 90 ms (30 ms power + 50 ms POR + ~10 ms I2C). - -### 5.3 BCM4500 Initialization Data - -Three register initialization blocks are written to BCM4500 indirect registers (page 0x00) via the 0xA6/0xA7/0xA8 protocol. Data extracted from stock v2.06 firmware `FUN_CODE_0ddd`: - -| Block | Start Register | Length | Data (hex) | -|-------|---------------|--------|------------| -| 0 | 0x06 | 7 bytes | `06 0b 17 38 9f d9 80` | -| 1 | 0x07 | 8 bytes | `07 09 39 4f 00 65 b7 10` | -| 2 | 0x0F | 3 bytes | `0f 0c 09` | - -Each block is written as: page select (0xA6 = 0x00), data bytes to 0xA7, trailing zero to 0xA7, then commit (0xA8 = 0x03). The firmware polls 0xA8 until the command completes before proceeding to the next block. - -### 5.4 FX2 CPUCS Recovery - -The FX2's CPUCS register at 0xE600 controls the 8051 run/halt state. The standard vendor request bRequest=0xA0 (RAM read/write) is handled by the FX2 boot ROM in silicon, not by user firmware. This means `fw_load.py` can reload firmware over a completely hung device: - -```bash -sudo python3 tools/fw_load.py load firmware/build/skywalker1.ihx --wait 3 -``` - -Writing 0x01 to CPUCS halts the CPU. New code is written to RAM. Writing 0x00 restarts it. The device re-enumerates with the new firmware. - ---- - -## 6. BCM4500 Demodulator Interface - -### 6.1 I2C Addressing - -| Parameter | Value | -|-----------|-------| -| 7-bit I2C address | 0x08 | -| 8-bit write address | 0x10 | -| 8-bit read address | 0x11 | -| Bus speed | 400 kHz | -| FX2 I2C controller SFRs | I2CS, I2DAT, I2CTL | -| Alternate probe addresses (v2.13) | 0x3F, 0x7F | - -The custom firmware and kernel driver use the 7-bit address 0x08. The stock firmware writes `addr << 1` = 0x10 for write and `(addr << 1) | 1` = 0x11 for read, which is the standard I2C convention for 7-bit address 0x08. - -The v2.13 firmware probes addresses 0x7F and 0x3F at startup (INT0 handler) to detect which demodulator variant is present. These may be alternative I2C address configurations or addresses for different demodulator sub-systems. - -### 6.2 Direct Registers - -Accessed via standard I2C write/read to the BCM4500's device address: - -| Register | Function | -|----------|----------| -| 0xA2 | Status register (polled for readiness during boot) | -| 0xA4 | Lock/ready register; bit 5 (0x20) = signal locked | -| 0xA6 | Indirect page/address select | -| 0xA7 | Indirect data register (read/write) | -| 0xA8 | Indirect command register | -| 0xF9 | Demod status (read by v2.13 GET_DEMOD_STATUS / INT0 polling) | - -### 6.3 Indirect Register Protocol - -The BCM4500 uses an indirect register access scheme through three directly-addressable registers: - -**Indirect Write Sequence:** - -``` -1. I2C WRITE to 0x08, register 0xA6 <- page_number (typically 0x00) -2. I2C WRITE to 0x08, register 0xA7 <- data bytes (N bytes, auto-increment) -3. I2C WRITE to 0x08, register 0xA8 <- 0x03 (execute indirect write) -4. Poll register 0xA8 until bit 0 clear (command complete) -5. Optionally read back register 0xA7 to verify -``` - -**Indirect Read Sequence:** - -``` -1. I2C WRITE to 0x08, register 0xA6 <- target_register -2. I2C WRITE to 0x08, register 0xA7 <- 0x00 (placeholder) -3. I2C WRITE to 0x08, register 0xA8 <- 0x01 (execute indirect read) -4. Short delay (~1 ms) -5. I2C READ from 0x08, register 0xA7 <- result byte -``` - -### 6.4 Indirect Protocol Auto-Increment - -The BCM4500's data register (0xA7) supports auto-increment for multi-byte writes within a single I2C transaction. When writing N data bytes to 0xA7 in one I2C WRITE operation (without issuing STOP between bytes), the BCM4500 internally advances its data buffer pointer after each byte. This allows writing an entire initialization block in a single I2C transaction: - -``` -I2C transaction: - START -> 0x10 (write) -> 0xA7 (reg) -> data[0] -> data[1] -> ... -> data[N-1] -> STOP -``` - -The firmware exploits this for initialization blocks and tuning data, reducing I2C overhead compared to byte-by-byte writes. - -**Stock firmware init block write sequence (from FUN_CODE_0ddd):** - -``` -1. I2C WRITE: [0x10] [0xA6] [0x00] -- Page select = 0 -2. I2C WRITE: [0x10] [0xA7] [data0..dataN] -- Multi-byte data (auto-increment) -3. I2C WRITE: [0x10] [0xA7] [0x00] -- Trailing zero (stock firmware quirk) -4. I2C WRITE: [0x10] [0xA8] [0x03] -- Commit indirect write -5. Poll: I2C READ [0xA8] until bit 0 clear -- Wait for completion -``` - -The trailing zero write (step 3) appears in all stock firmware versions. Its purpose is unclear -- it may zero-pad the data buffer or serve as an end-of-data marker within the BCM4500's indirect register engine. - -### 6.5 Demodulator Scan - -The tune function (stock firmware) tries up to 3 different I2C address configurations per attempt, with 3 outer retries (up to 9 total I2C programming attempts). This supports hardware variants where the BCM4500 may appear at different bus addresses. - -v2.13 adds a boot-time probe: INT0 polls addresses 0x7F and 0x3F up to 40 times (0x28), setting flag `_1_4` if neither responds. This prevents tuning attempts on boards with absent demodulators. - -### 6.6 BCM4500 FEC Architecture - -The BCM4500 contains two FEC decoder paths: - -1. **Advanced Modulation Turbo FEC Decoder**: Iterative turbo code decoder supporting QPSK (rates 1/4, 1/2, 3/4), 8PSK (rates 2/3, 3/4, 5/6, 8/9), 16QAM (rate 3/4), with Reed-Solomon (t=10) outer code. - -2. **Legacy DVB/DIRECTV/DCII-Compliant FEC Decoder**: Concatenated Viterbi inner decoder (convolutional code, rates 1/2 through 7/8) + Reed-Solomon outer decoder. - -There is no LDPC or BCH decoder hardware. See [Section 14](#14-dvb-s2-incompatibility). - ---- - -## 7. Tuning Protocol - -### 7.1 TUNE_8PSK Command Format (0x86) - -The host sends a 10-byte OUT payload via USB control transfer: - -``` -USB SETUP: bmRequestType=0x40, bRequest=0x86, wValue=0, wIndex=0, wLength=10 - -EP0BUF Layout: - Byte Content Encoding - ---- ------------------ ---------------- - [0] Symbol Rate byte 0 Little-endian LSB - [1] Symbol Rate byte 1 - [2] Symbol Rate byte 2 - [3] Symbol Rate byte 3 Little-endian MSB - [4] Frequency byte 0 Little-endian LSB - [5] Frequency byte 1 - [6] Frequency byte 2 - [7] Frequency byte 3 Little-endian MSB - [8] Modulation Type 0--9 (see Section 1.1) - [9] Inner FEC Rate Index into modulation-specific table -``` - -**Symbol Rate** is in samples per second (sps). The Windows driver multiplies ksps by 1000. - -**Frequency** is the IF frequency in kHz (950000--2150000), computed by the host as `(RF_freq - LO_freq) * multiplier`. - -### 7.2 Firmware EP0BUF Parsing - -The firmware reads the 10-byte payload from EP0BUF (XRAM 0xE740--0xE749) and stores: - -| Source | Destination | Notes | -|--------|-------------|-------| -| EP0BUF[8] (modulation) | IRAM 0x4D | Direct copy | -| EP0BUF[9] (FEC) | IRAM 0x4F | Direct copy | -| EP0BUF[4--7] (frequency) | XRAM 0xE0DB--0xE0DE | Byte-reversed (LE to BE) | -| EP0BUF[0--3] (symbol rate) | XRAM 0xE0CB--0xE0CE | Byte-reversed (LE to BE) | - -The byte reversal converts host little-endian to BCM4500 big-endian so values can be written directly to the demodulator via I2C. - -### 7.3 Modulation Dispatch - -After parsing, the firmware validates the modulation type (bounds check `< 10`) and dispatches via a 20-byte jump table (10 entries x 2 bytes) at CODE:0873. Each handler: - -1. Validates the FEC index against the maximum for that modulation -2. Looks up a preconfigured byte from an XRAM FEC rate table -3. Writes configuration to four XRAM registers (0xE0EB, 0xE0EC, 0xE0F5, 0xE0F6) - -**Modulation jump table (from Rev.2 at CODE:0873):** - -| Entry | AJMP Target | Modulation | -|-------|------------|-----------| -| 0 | 0x08B7 | DVB-S QPSK | -| 1 | 0x08DF | Turbo QPSK | -| 2 | 0x08FA | Turbo 8PSK | -| 3 | 0x0915 | Turbo 16QAM | -| 4 | 0x0947 | DCII Combo | -| 5 | 0x094F | DCII I-stream | -| 6 | 0x0957 | DCII Q-stream | -| 7 | 0x095F | DCII Offset QPSK | -| 8 | 0x0887 | DSS QPSK | -| 9 | 0x0887 | DVB BPSK (shares DSS handler) | - -DSS and DVB BPSK share the same handler. Their FEC lookup uses the same table (0xE0F9) but ORs the result with 0x80 to distinguish them from DVB-S QPSK. - -### 7.4 FEC Rate Lookup Tables - -Populated from the CODE-space init table at boot: - -| XRAM Base | Modulation | Max FEC Index | Code Rates | -|-----------|-----------|---------------|------------| -| 0xE0F9 | DVB-S QPSK, DSS, BPSK | 7 | 1/2, 2/3, 3/4, 5/6, 7/8, auto, none | -| 0xE0B7 | Turbo QPSK | 5 | Turbo-specific rates | -| 0xE0B1 | Turbo 8PSK | 5 | Turbo-specific rates | -| 0xE0BC | Turbo 16QAM | 1 | Single code rate | -| 0xE0BD | DCII (all variants) | 9 | Combined code + modulation | - -### 7.5 BCM4500 XRAM Configuration After Dispatch - -| XRAM Addr | Register | DVB-S QPSK | Turbo (Q/8/16) | DCII | DSS/BPSK | -|-----------|----------|-----------|---------------|------|----------| -| 0xE0EB | FEC Code Rate | Table lookup | Table lookup | 0xFC (fixed) | Table lookup OR 0x80 | -| 0xE0EC | Modulation Type | 0x09 | 0x09 | From DCII table | 0x09 | -| 0xE0F5 | Demod Mode | 0x10 | 0x10 | 0x10/0x11/0x12/0x16 | 0x10 | -| 0xE0F6 | Turbo Flag | 0x00 | 0x01 | 0x00 | 0x00 | - -**DCII Demod Mode values:** - -| Modulation | Index | XRAM 0xE0F5 | -|-----------|-------|-------------| -| DCII Combo | 4 | 0x10 | -| DCII I-stream | 5 | 0x12 | -| DCII Q-stream | 6 | 0x16 | -| DCII Offset QPSK | 7 | 0x11 | - -DSS (8) and DVB BPSK (9) share the DVB-S QPSK handler; they use the same FEC table but OR the lookup value with 0x80. - -### 7.6 Complete Tuning Sequence (Host to Satellite) - -``` -=== Phase 1: LNB Configuration (separate vendor commands) === -1. SET_LNB_VOLTAGE (0x8B) -- GPIO P0.4 (no I2C) - H / Circular-L -> wValue=1 (18V) - V / Circular-R -> wValue=0 (13V) -2. SET_22KHZ_TONE (0x8C) -- GPIO P0.3 (no I2C) - High band -> wValue=1 (tone on) - Low band -> wValue=0 (tone off) -3. SEND_DISEQC_COMMAND (0x8D) -- if multi-switch needed - -=== Phase 2: Tune Command === -4. TUNE_8PSK (0x86) -- 10-byte payload - -=== Phase 3: Firmware Internal Processing === -5. EP0BUF parsing: mod/FEC to IRAM, freq/SR byte-reversed to XRAM -6. Modulation dispatch: FEC lookup, XRAM config registers set -7. GPIO P3.6: DVB mode select - -=== Phase 4: BCM4500 I2C Programming (3 outer retries x 3 I2C addresses) === -8. Poll BCM4500 ready: I2C READ regs 0xA2, 0xA8, 0xA4 -9. Write page: I2C WRITE reg 0xA6 <- 0x00 -10. Write config: I2C WRITE reg 0xA7 <- [freq, SR, FEC, mod, demod params] -11. Execute: I2C WRITE reg 0xA8 <- 0x03 (indirect write command) -12. Poll completion: I2C READ regs 0xA8, 0xA2 -13. Verify: I2C READ reg 0xA7 (read-back compare) - -=== Phase 5: Signal Acquisition (host polling) === -14. GET_SIGNAL_LOCK (0x90) -- poll until non-zero -15. GET_SIGNAL_STRENGTH (0x87) -- read SNR -``` - -### 7.7 Signal Lock and Strength - -**GET_SIGNAL_LOCK (0x90)**: Returns 1 byte from BCM4500 register 0xA4. Bit 5 (0x20) indicates signal lock. The kernel driver interprets any non-zero value as locked and reports `FE_HAS_LOCK | FE_HAS_SYNC | FE_HAS_VITERBI | FE_HAS_SIGNAL | FE_HAS_CARRIER`. - -**GET_SIGNAL_STRENGTH (0x87)**: Returns 6 bytes. Bytes 0--1 = 16-bit SNR (LE, dBu * 256). SNR scaling: `snr_raw * 17` maps 0x0000--0x0F00 to 0--65535 (100% at SNR >= 0x0F00). - ---- - -## 8. GPIF Streaming Path - -### 8.1 Data Flow - -``` -BCM4500 Cypress FX2 (CY7C68013A) USB Host -Demodulator P3.5 GPIF Engine EP2 FIFO EP2 (0x82) - (I2C:0x08) <-----> (Master Read) (AUTOIN) ------------> Bulk IN - 8-bit 0xE4xx wfm 4x buffer 7 URBs - parallel 8-bit x 8KB -``` - -The path is fully hardware-managed. The GPIF engine reads data from the BCM4500's 8-bit parallel transport stream output directly into the EP2 FIFO. The AUTOIN bit causes automatic USB commit when the FIFO buffer is full. The FLOWSTATE engine re-triggers GPIF transactions when buffer space becomes available. No firmware intervention occurs in the data path after initial setup. - -### 8.2 Key Register Configuration - -All values are identical across the three stock firmware versions: - -| Register | Address | Value | Function | -|----------|---------|-------|----------| -| IFCONFIG | 0xE601 | 0xEE | Internal 48 MHz clock, GPIF master, async, debug | -| EP2FIFOCFG | 0xE618 | 0x0C | AUTOIN=1, ZEROLENIN=1, 8-bit data path | -| REVCTL | 0xE60B | 0x03 | NOAUTOARM + SKIPCOMMIT | -| CPUCS | 0xE600 | bits [4:3]=10 | 48 MHz CPU clock | -| FLOWSTATEA | 0xE668 | OR 0x09 | FSEN (flow state enable) + FS[3] | -| GPIFIE | 0xE65C | OR 0x3D | Waveform, TC, DONE, FIFO flag, WF2 interrupts | - -**IFCONFIG decode (0xEE = 1110_1110):** - -| Bit | Name | Value | Meaning | -|-----|------|-------|---------| -| 7 | IFCLKSRC | 1 | Internal clock source | -| 6 | 3048MHZ | 1 | 48 MHz IFCLK frequency | -| 5 | IFCLKOE | 1 | IFCLK pin drives output (clock to BCM4500) | -| 4 | IFCLKPOL | 0 | Non-inverted clock polarity | -| 3 | ASYNC | 1 | Asynchronous GPIF (RDY pin handshaking) | -| 2 | GSTATE | 1 | Debug state output on PORTE | -| 1:0 | IFCFG | 10 | GPIF internal master mode | - -### 8.3 ARM_TRANSFER Sequences - -**Start streaming (wValue=1):** - -1. Set config_byte bit 7 (streaming active) -2. Load GPIF transaction count: GPIFTCB3:2 = 0x8000 (effectively infinite) -3. Reset GPIF address and EP2 FIFO byte count -4. Assert P3.5 LOW (BCM4500 transport stream enable) -5. Wait for initial GPIF transaction (poll GPIFTRIG bit 7) -6. De-assert P3.5 HIGH -7. Trigger continuous GPIF read: GPIFTRIG = 0x04 (read into EP2) -8. Set P0.7 LOW (streaming indicator) - -**Stop streaming (wValue=0):** - -1. Set P0.7 HIGH (streaming stopped) -2. Write EP2FIFOBCH = 0xFF (force-flush current buffer) -3. Wait for GPIF idle (poll GPIFTRIG bit 7) -4. Write OUTPKTEND = 0x82 (skip/discard partial EP2 packet) -5. Clear config_byte bit 7 (streaming inactive) -6. Set P3 bits 7:5 = 1 (de-assert all BCM4500 control lines) - -### 8.4 Throughput Analysis - -| Metric | Value | -|--------|-------| -| USB 2.0 HS bulk theoretical | 480 Mbps | -| USB 2.0 HS bulk practical | ~280 Mbps (~35 MB/s) | -| GPIF engine theoretical | 48 MHz x 8 bits = 384 Mbps | -| Typical DVB-S TS rate | 1--5 MB/s | -| Maximum DVB-S2 rate (hypothetical) | ~7.25 MB/s (58 Mbps) | - -The USB/GPIF path has approximately 5x headroom even at maximum theoretical DVB-S2 data rates. The bottleneck for supported modes is the satellite link, not the USB data path. - -### 8.5 FIFO Reset Sequence - -All endpoint FIFOs are reset during initialization using the Cypress-prescribed procedure: - -``` -FIFORESET = 0x80 ; NAKALL: NAK all host transfers during reset -FIFORESET = 0x02 ; Reset EP2 FIFO -FIFORESET = 0x04 ; Reset EP4 FIFO -FIFORESET = 0x06 ; Reset EP6 FIFO -FIFORESET = 0x08 ; Reset EP8 FIFO -FIFORESET = 0x00 ; Release NAKALL -``` - -Three NOP instructions (mandatory SYNCDELAY) are inserted between each write per Cypress TRM requirements. - -### 8.6 EP2 Endpoint Configuration - -```c -EP2CFG = 0xE2; // valid=1, dir=IN, type=BULK, size=512, buf=DOUBLE -``` - -| Bit | Value | Meaning | -|-----|-------|---------| -| 7 (VALID) | 1 | Endpoint enabled | -| 6 (DIR) | 1 | IN (device to host) | -| 5:4 (TYPE) | 10 | Bulk transfer | -| 3 (SIZE) | 0 | 512-byte packets | -| 1:0 (BUF) | 10 | Double-buffered | - -EP4, EP6, EP8 are disabled (`&= ~bmVALID`). - -### 8.7 Interrupt Handling - -INT4 and INT6 (GPIF/FIFO events) share a common handler that sets a software flag (`_0_1`) and clears EXIF.4. The main loop polls this flag, enters CPU idle mode (PCON.0) between events, and checks EP2CS for buffer availability before re-arming the GPIF. - -**Main loop structure (from v2.06 FUN_CODE_2297):** - -```c -void main_loop_poll(void) { - if (_0_1) { // GPIF/FIFO event pending - _0_1 = 0; // Clear flag - if (EP2CS & bmEPFULL) { // EP2 buffer full? - // Wait for host to read EP2 - } - } else { - PCON |= 0x01; // CPU idle until next interrupt - } -} -``` - -### 8.8 Prior IFCONFIG Value - -During early initialization, IFCONFIG is temporarily set to 0xCA before the final 0xEE: - -| Value | Decode | Difference from 0xEE | -|-------|--------|---------------------| -| 0xCA | 1100_1010 | GSTATE=0, ASYNC=0 | -| 0xEE | 1110_1110 | GSTATE=1, ASYNC=1 (final) | - -The temporary value disables async mode and debug state output during FIFO setup. - ---- - -## 9. LNB and DiSEqC Control - -### 9.1 LNB Voltage - -LNB voltage is controlled via GPIO P0.4. No I2C is involved. - -| wValue | Voltage | GPIO P0.4 | Polarization | -|--------|---------|-----------|-------------| -| 0 | 13V | LOW | Vertical / Circular-Right | -| 1 | 18V | HIGH | Horizontal / Circular-Left | - -USE_EXTRA_VOLT (0x94) enables a +1V boost (13V->14V, 18V->19V) for long cable runs by writing to XRAM 0xE0B6 (0x62=normal, 0x6A=boosted; bit 3 is the difference). - -### 9.2 22 kHz Tone - -Controlled via GPIO P0.3. P0.3 gates an external 22 kHz oscillator on the PCB. The firmware does not generate the 22 kHz carrier directly. - -| wValue | State | GPIO P0.3 | Band | -|--------|-------|-----------|------| -| 0 | OFF | LOW | Low band (9.75 GHz LO on universal LNB) | -| 1 | ON | HIGH | High band (10.6 GHz LO on universal LNB) | - -### 9.3 DiSEqC Protocol Implementation - -All firmware versions implement DiSEqC via Timer2-based GPIO bit-bang. The algorithm is identical across versions; only the data pin differs per PCB revision. - -**Timer2 configuration (identical across all versions):** - -| Parameter | Value | -|-----------|-------| -| T2CON | 0x04 (auto-reload, running) | -| RCAP2H:RCAP2L | 0xF82F (reload = 63535) | -| CKCON.T2M | 0 (Timer2 clock = 48 MHz / 12 = 4 MHz) | -| Tick period | (65536 - 63535) / 4 MHz = 500.25 us | - -**DiSEqC timing parameters:** - -| Parameter | Value | -|-----------|-------| -| Bit period | 1.5 ms (3 Timer2 ticks) | -| Byte period | 13.5 ms (9 bits: 8 data + 1 parity) | -| Tone burst A/B | 12.5 ms (25 ticks) | -| Pre-TX settling delay | 7.5 ms (15 ticks) | -| Data '0' | 1.0 ms tone + 0.5 ms silence (2/3 duty cycle) | -| Data '1' | 0.5 ms tone + 1.0 ms silence (1/3 duty cycle) | -| Carrier frequency | 22 kHz (external oscillator, gated by P0.3) | - -**Manchester encoding (decompiled from Rev.2 FUN_CODE_213c):** - -``` -Each DiSEqC bit = 3 Timer2 ticks: - Tick 1: inter-bit gap (carrier OFF via P0.3 = 0) - Tick 2: carrier ON (P0.3 = 1) - Tick 3: if data_pin='1', carrier OFF early; if '0', carrier stays ON - End: carrier always OFF -``` - -**DiSEqC bit waveforms:** - -``` -Data '0' (2/3 tone, 1/3 silence): - Tick 1 Tick 2 Tick 3 - (500 us) (500 us) (500 us) -P0.3: _____|========|========|________| - ^tone ON ^tone OFF - (setup gap) (1.0 ms carrier) (0.5 ms silence) - -Data '1' (1/3 tone, 2/3 silence): - Tick 1 Tick 2 Tick 3 - (500 us) (500 us) (500 us) -P0.3: _____|========|________|________| - ^tone ON ^tone OFF early - (setup gap) (0.5 ms carrier) (1.0 ms silence) -``` - -**Decompiled bit symbol function (from Rev.2 FUN_CODE_213c):** - -```c -void diseqc_bit_symbol(void) { - wait_TF2(); // Tick 1: inter-bit gap (500 us) - P0 |= 0x08; // P0.3 = 1 -> 22 kHz carrier ON - wait_TF2(); // Tick 2: carrier period (500 us) - if (data_pin != 0) { // If data = '1': - P0 &= 0xF7; // P0.3 = 0 -> carrier OFF (short pulse) - } - wait_TF2(); // Tick 3: final period (500 us) - P0 &= 0xF7; // P0.3 = 0 -> carrier always OFF at end -} -``` - -**Decompiled byte transmission (from Rev.2 FUN_CODE_07d1):** - -```c -void diseqc_send_byte(char first_byte, byte data) { - byte ones_count = 0; - if (first_byte == 0) TF2 = 0; // Sync timer on first byte - - for (char i = 8; i > 0; i--) { // 8 bits, MSB first - if (data & 0x80) { - data_pin = 1; // Set data = '1' - diseqc_bit_symbol(); - ones_count++; - } else { - data_pin = 0; // Set data = '0' - diseqc_bit_symbol(); - } - data <<= 1; // Next bit - } - data_pin = ~ones_count & 1; // Odd parity - diseqc_bit_symbol(); // Transmit parity bit -} -``` - -**Timing per byte**: 9 bits x 1.5 ms = 13.5 ms - -**Tone burst (mini DiSEqC)**: 25 consecutive Timer2 ticks of carrier (12.5 ms). Tone burst A: `wValue==0` and `wLength==0`. Tone burst B: `wValue!=0` and `wLength==0`. - -**Timer tick wait (TF2 polling, identical across all versions):** - -```c -void wait_TF2(void) { - while (TF2 == 0) {} // Poll Timer2 overflow flag - TF2 = 0; // Clear flag for next tick -} -``` - -### 9.4 DiSEqC Signal Architecture - -``` -FX2 Firmware External Hardware Coax Cable -+------------------+ +--------------------+ +------------------+ -| P0.3 (carrier) |---->| 22 kHz oscillator |---->| LNB power line | -| (enable/disable) | | (gated by P0.3) | | (13V/18V + tone) | -| | | | | | -| P0.x (data bit) | | (internal firmware | | | -| (firmware only) | | logic only) | | | -+------------------+ +--------------------+ +------------------+ -``` - -The data pin (P0.7 / P0.4 / P0.0 depending on firmware version) is used only internally by the firmware's Manchester encoding logic. It controls whether the carrier gate signal is cut short or held for the full bit period. - -### 9.5 Windows BDA Driver DiSEqC Interface - -The Windows driver exposes DiSEqC through a BDA extended property: - -```c -// GUID: {0B5221EB-F4C4-4976-B959-EF74427464D9} -typedef struct __DISEQC_COMMAND { - UCHAR ucMessage[6]; // Framing, Address, Command, Data[0..2] - UCHAR ucMessageLength; // 3-6 for DiSEqC; 1 for tone burst -} DISEQC_COMMAND; -``` - -For tone burst: `ucMessageLength=1`, `ucMessage[0]=SEC_MINI_A` (0x00) or `SEC_MINI_B` (0x01). - -### 9.6 SET_DN_SWITCH (0x8F) -- Legacy Dish Network - -A 7-bit serial command bit-banged on GPIO P0.4: - -1. Assert P0.4 HIGH (start pulse), delay ~32 cycles -2. De-assert P0.4, delay ~8 cycles -3. Shift out 7 bits LSB-first via P0.4, ~8 cycle delays between bits - -The kernel calls this via `dishnetwork_send_legacy_command`. Bit 7 (0x80) of the original switch command selects LNB voltage and is sent separately via SET_LNB_VOLTAGE. - ---- - -## 10. GPIO Pin Map - -### 10.1 Port 0 / Port A (SFR 0x80, IOA) - -| Pin | v2.06 | Rev.2 v2.10 | v2.13 | Custom v3.01.0 | -|-----|-------|-------------|-------|----------------| -| P0.0 | -- | LNB control (0x97) | DiSEqC data | -- | -| P0.1 | Power enable | Power enable | Power enable | Power enable | -| P0.2 | Power disable | Power disable (init=0x84) | Power disable | Power disable | -| P0.3 | **22 kHz tone** | **22 kHz tone** | **22 kHz tone** | **22 kHz tone** | -| P0.4 | **LNB 13V/18V** | **LNB 13V/18V** + DiSEqC data | **LNB 13V/18V** | **LNB 13V/18V** | -| P0.5 | **BCM4500 RESET** | GPIO status input (0x98) | **BCM4500 RESET** | **BCM4500 RESET** | -| P0.6 | -- | GPIO control (0x97) | -- | -- | -| P0.7 | **DiSEqC data** | Streaming indicator | Streaming indicator | **DiSEqC data** + streaming | - -### 10.2 Port 3 / Port D (SFR 0xB0, IOD) - -| Pin | Function | Notes | -|-----|----------|-------| -| P3.0 | Init HIGH | | -| P3.4 | GPIO control | Used by Rev.2 FUN_CODE_1fcf | -| P3.5 | **TS_EN** | Transport stream enable: LOW=active, HIGH=idle | -| P3.6 | **DVB mode** | BCM4500 mode select; DiSEqC direction (Rev.2) | -| P3.7 | BCM4500 control | De-asserted (HIGH) when streaming stops | - -### 10.3 Port B (XRAM-mapped IOB) - -Used by internal debug commands 0x96--0x98: - -| Pin | v2.06/v2.13 | Rev.2 | -|-----|-------------|-------| -| IOB.0 | GPIO status input (0x98) | -- | -| IOB.1 | LNB control (0x97) | -- | -| IOB.2 | LNB control (0x97) | -- | -| IOB.3 | LNB GPIO mode (0x96) | -- | -| IOB.4 | -- | LNB GPIO mode (0x96) + control (0x97) | - -### 10.4 DiSEqC Data Pin Summary - -| Firmware Version | Data Pin | Carrier Pin | -|-----------------|----------|-------------| -| v2.06 | P0.7 | P0.3 | -| Rev.2 v2.10 | P0.4 | P0.3 | -| v2.13 | P0.0 | P0.3 | -| Custom v3.01.0 | P0.7 | P0.3 | - -The carrier pin (P0.3) is the same across all versions. - -### 10.5 Initial GPIO State - -| Register | Value | Decode | -|----------|-------|--------| -| IOA (P0) | 0x84 | P0.7=1 (idle), P0.2=1 (power disable active) | -| IOD (P3) | 0xE1 | P3.7:5=1 (controls idle), P3.0=1 | -| OEA | 0xBE | P0.1-5,7 as outputs | - ---- - -## 11. Firmware Versions - -### 11.1 Version Table - -| Firmware | Version ID | Build Date | PID | Functions | Binary Size | SP | -|----------|-----------|------------|-----|-----------|-------------|-----| -| v2.06.04 | 0x020604 | 2007-07-13 | 0x0203 | 61 | 9,472 bytes | 0x72 | -| Rev.2 v2.10.04 | 0x020A04 | 2010-03-12 | 0x0202 | 107 | 8,843 bytes | 0x4F | -| v2.13.01 (FW1) | 0x020D01 | 2010-03-12 | 0x0203 | 82-88 | 9,322 bytes | 0x50 | -| v2.13.02 (FW2) | 0x020D01 | 2010-03-12 | 0x0203 | 83 | 9,377 bytes | 0x50 | -| v2.13.03 (FW3) | 0x020D01 | 2010-03-12 | 0x0203 | 83 | 9,369 bytes | 0x52 | -| Custom v3.01.0 | 0x030100 | 2026-02-12 | 0x0203 | N/A | ~3 KB (RAM) | N/A | - -Rev.2 v2.10 targets PID 0x0202 (different product). The v2.13 sub-variants target different SkyWalker-1 hardware sub-revisions. Custom v3.01.0 is compiled with SDCC + fx2lib and loaded into FX2 RAM (not flashed to EEPROM). - -### 11.2 Kernel Version Constants - -From `gp8psk-fe.h`: - -``` -GP8PSK_FW_REV1 = 0x020604 (v2.06.4) -GP8PSK_FW_REV2 = 0x020704 (v2.07.4) -``` - -If `fw_vers >= GP8PSK_FW_REV2`, the kernel enables Rev.2-specific code paths. The v2.10 and v2.13 firmwares are newer than either kernel constant. - -### 11.3 Key Architectural Differences - -| Feature | v2.06 | Rev.2 v2.10 | v2.13 | -|---------|-------|-------------|-------| -| Vendor commands | 30 (0x80--0x9D) | 27 (0x80--0x9A) | 30 (0x80--0x9D) | -| INT0 handler | USB re-enumeration | USB re-enumeration | Demod availability polling | -| Demod probe at boot | No | No | Yes (40 attempts at 0x7F + 0x3F) | -| Retry loops | No | No | Yes (20-attempt with checksum verify) | -| HW revision detection | No | Yes (descriptor walker) | Yes (flag `_1_3`) | -| DiSEqC data pin | P0.7 | P0.4 | P0.0 | -| Config byte IRAM | 0x6D | 0x4E | 0x4F | -| Descriptor base | 0x1200 | 0x0E00 | 0x0E00 | -| Init table address | CODE:0B46 | CODE:0B48 | CODE:0B88 | -| BCM4500 status poll | 3 registers | 3 registers | 1 register (consolidated) | -| Anti-tampering string | No | No | Yes (at firmware offset 0x1880) | -| New commands | -- | 0x99/0x9A proto | 0x99, 0x9A, 0x9C | -| 0x9D behavior | HW revision mode | N/A (out of range) | Conditional demod reset | - -### 11.4 v2.13 Sub-Variant Differences - -The three v2.13 sub-variants target fundamentally different hardware interfaces: - -| Aspect | FW1 (v2.13.1) | FW2 (v2.13.2) | FW3 (v2.13.3) | -|--------|---------------|---------------|---------------| -| Demod interface | I2C bus | Parallel bus (P0/P1) | Parallel bus (enhanced) | -| Bus protocol | I2C START/STOP/ACK | Single-phase P1 read | Dual-phase P1 read + OR accumulate | -| Stack pointer | 0x50 | 0x50 | 0x52 | -| P0 init | 0xa4 | 0xa4 | 0xa0 | -| Status register | INTMEM 0x4F | INTMEM 0x4F | INTMEM 0x51 | -| Config source | Hardcoded | External (0xE080-0xE08E) | External (0xE080-0xE08E) | -| Binary distance from FW1 | -- | 3,993 bytes | 3,789 bytes | -| Binary distance from FW2 | 3,993 bytes | -- | 1,525 bytes | - -FW1 uses standard I2C master-mode transactions. FW2/FW3 use a parallel data bus with P0 for control signals (chip select, read strobe) and P1 for 8-bit data. FW3 adds dual-phase reading with OR-accumulation, likely for a demodulator chip with different bus timing. The updater program selects the correct sub-variant based on hardware detection. - -### 11.5 Binary Comparison Matrix - -Byte-level similarity (percentage of matching bytes within shared length): - -| | v2.06 | v2.13.1 | v2.13.2 | v2.13.3 | Rev.2 | -|---|---|---|---|---|---| -| **v2.06** | -- | 4.8% | 4.3% | 4.3% | 6.0% | -| **v2.13.1** | | -- | 57.2% | 59.4% | 8.0% | -| **v2.13.2** | | | -- | 83.5% | 5.8% | -| **v2.13.3** | | | | -- | 5.8% | -| **Rev.2** | | | | | -- | - -The very low similarity between major versions (4--8%) indicates complete recompilation with different linker configurations. Functions relocate even when logic is identical. - -### 11.6 Anti-Tampering (v2.13 Only) - -At firmware offset 0x1880, all v2.13 sub-variants contain: - -``` -"Tampering is detected. Attempt is logged. Warranty is voided ! \n" -``` - -Followed by I2C register write commands (`01 10 aa 82 02 41 41 83`). This string and mechanism are absent from v2.06 and Rev.2. - -### 11.7 Rev.2 as Transitional Firmware - -Rev.2 v2.10.4 sits architecturally between v2.06 and v2.13: -- Adopted v2.13's descriptor base (0x0E00) and similar stack pointer -- Retained v2.06's INT0 USB re-enumeration behavior -- Has the most functions (107) but smallest binary (~8.8 KB) due to granular decomposition -- Lacks v2.13's demodulator polling, retry loops, and additional vendor commands - -### 11.8 Key Function Correspondence Across Versions - -| v2.06 Function | Rev.2 Function | v2.13 Function | Role | -|---------------|---------------|---------------|------| -| `main` (0x188D) | `main` (0x155F) | `main_entry` (0x170D) | RESET vector: clear IRAM, process init table | -| `FUN_CODE_09a7` | `FUN_CODE_09a9` | `FUN_CODE_0800` | Main init + main loop | -| `FUN_CODE_13c3` | `FUN_CODE_10d9` | `FUN_CODE_11ab` | USB/peripheral descriptor setup | -| `FUN_CODE_032a` | `FUN_CODE_0319` | `FUN_CODE_034e` | Standard USB request handler | -| `FUN_CODE_0056` | `vendor_cmd_dispatch` | `FUN_CODE_0056` | Vendor request dispatcher (identical code) | -| `FUN_CODE_2297` | -- | `FUN_CODE_21ec` | Main loop poll (USB IRQ processing) | -| `FUN_CODE_1919` | `FUN_CODE_0d7c` | `FUN_CODE_1800` | GPIF/FIFO management | -| `FUN_CODE_1d4f` | -- | -- | v2.06 demod init (GPIO-based) | -| -- | -- | `FUN_CODE_1d4b` | v2.13 demod init (I2C write to 0x7F/0xF0) | -| `FUN_CODE_0ddd` | `FUN_CODE_0c64` | `FUN_CODE_0ca4` | BCM4500 firmware loader | -| `FUN_CODE_2000` | -- | `FUN_CODE_208d` | BCM4500 status polling | -| `FUN_CODE_1dfb` | `FUN_CODE_1bda` | `FUN_CODE_14b9` | Delay loop (clock-speed-aware) | -| `INT0_vec` (0x0003) | `INT0_ISR` (0x0003) | `INT0_vector` (0x0003) | INT0 handler (different purpose) | -| -- | -- | `FUN_CODE_2239` | v2.13 I2C single-byte read helper | -| -- | -- | `FUN_CODE_2031` | v2.13 USB reconnect function | -| -- | -- | `FUN_CODE_1799` | v2.13 demod signature verification | -| -- | -- | `FUN_CODE_1ac6` | v2.13 tuning acquisition sequence | - -### 11.9 INT0 Handler Evolution - -The INT0 interrupt vector (CODE:0003) was repurposed between firmware versions: - -**v2.06 and Rev.2 -- USB Re-enumeration:** - -``` -void INT0_vec(void) { - if (flag == 0) CPUCS |= 0x08; // CPUCS bit 3 - else CPUCS |= 0x0A; // CPUCS bits 3+1 - delay(5, 0xDC); // ~1500 cycles - EPIRQ = 0xFF; // Clear endpoint IRQs - USBIRQ = 0xFF; // Clear USB IRQs - EXIF &= 0xEF; // Clear external interrupt flag - CPUCS &= 0xF7; // Clear CPUCS bit 3 -} -``` - -Pulses CPUCS.3 to trigger a controlled USB re-enumeration, then clears all pending interrupts. - -**v2.13 -- Demodulator Availability Polling:** - -``` -void INT0_vector(void) { - for (counter = 0x28; counter != 0; counter--) { // 40 attempts - byte result = I2C_read(0x7F); // Demod address A - if (result != 0x01) { - result = I2C_read(0x3F); // Demod address B - if (result != 0x01) break; - } - } - no_demod_flag = (counter == 0); // Set if loop exhausted -} -``` - -Polls two I2C addresses (0x7F, 0x3F) to detect which demodulator variant is present. The `no_demod_flag` prevents tuning attempts on boards with absent or failed demodulators. - -In v2.13, the USB re-enumeration code was moved to `FUN_CODE_2031` and called as a normal function before the main loop starts, freeing INT0 for demodulator polling. - -### 11.10 v2.13 Integrity Verification - -v2.13 performs two integrity checks during initialization, absent from v2.06 and Rev.2: - -**Demodulator Signature Verification (FUN_CODE_1799):** - -1. Writes 4 bytes to I2C device 0x7F, register 0xF0 -2. Reads 5 bytes from register 0x0A (stepping by 2), each character -3. Subtracts 0x30 ('0') from each byte (ASCII to binary) -4. Sums values and compares against expected parameter (0x021C) -5. Up to 20 retry attempts - -**Descriptor Checksum Verification (FUN_CODE_1ca0):** - -1. Iterates bytes 6 through 0x29 (36 bytes) of a descriptor block -2. Computes running sum, compares against 0x0706 -3. Iterates bytes 0x2C through 0x4F (36 bytes) of same block -4. Computes second sum, compares against 0x0686 -5. Up to 20 retry attempts - -Both checks call `FUN_CODE_1ac6(100)` (tuning acquisition with 100 ms delay) as a recovery action if verification fails after all attempts. - -### 11.11 XRAM Initialization Table - -All firmware versions initialize XRAM peripheral registers from a table stored in CODE space. The table is processed at startup before entering the main loop. - -**Table format (all versions):** - -``` -Each entry: [addr_hi] [addr_lo] [data_byte] -Terminator: [0x00] [0x00] (address 0x0000) -``` - -The parser reads 3 bytes at a time: a 16-bit XRAM address (big-endian) and a data byte. It writes the byte to the address until it encounters address 0x0000. - -**Key XRAM registers initialized from the table:** - -| XRAM Address | Register | Typical Value | Purpose | -|-------------|----------|---------------|---------| -| 0xE604 | FIFORESET | 0x80 | Start FIFO reset sequence | -| 0xE601 | IFCONFIG | 0xCA | Initial interface config (overwritten later) | -| 0xE610 | EP2CFG | 0xE2 | EP2 bulk IN, 512-byte, double-buffered | -| 0xE612 | EP4CFG | 0x00 | EP4 disabled | -| 0xE618 | EP2FIFOCFG | 0x0C | AUTOIN, ZEROLENIN, 8-bit | -| 0xE620 | REVCTL | 0x03 | NOAUTOARM + SKIPCOMMIT | -| 0xE67A | I2CTL | 0x01 | I2C 400 kHz | -| 0xE68A | EP0BCH | 0x00 | EP0 byte count high = 0 | - -**Init table addresses by version:** - -| Firmware | Table Address | -|----------|--------------| -| v2.06 | CODE:0B46 | -| Rev.2 | CODE:0B48 | -| v2.13 | CODE:0B88 | - -### 11.12 Main Loop Architecture - -All firmware versions use the same main loop structure: poll the SUDAV (setup data available) interrupt flag, process vendor commands, then idle the CPU until the next interrupt. - -**v2.06 (simplified decompilation):** - -```c -void main_loop(void) { // FUN_CODE_09a7 - // 1. Process init table from CODE:0B46 - // 2. Call FUN_CODE_13c3 (USB/peripheral setup) - // 3. EA = 1 (global interrupts enable) - - while (1) { - if (sudav_flag) { - handle_setupdata(); // Process USB SETUP packet - sudav_flag = 0; - } - if (gpif_flag) { - handle_gpif_event(); - gpif_flag = 0; - } else { - PCON |= 0x01; // CPU idle until next interrupt - } - } -} -``` - -The SUDAV ISR simply sets `sudav_flag = 1` and clears the interrupt. All actual USB processing happens in the main loop context. - ---- - -## 12. I2C Bus Architecture - -### 12.1 FX2 I2C Controller - -The FX2's I2C master controller is a hardware peripheral accessed through SFRs: - -| SFR | Address | Function | -|-----|---------|----------| -| I2CS | 0xE678 (XRAM) | I2C control/status register | -| I2DAT | 0xE679 (XRAM) | I2C data register | -| I2CTL | 0xE67A (XRAM) | I2C control (speed selection) | - -Key I2CS bits: bmSTART (initiate START), bmSTOP (initiate STOP), bmLASTRD (signal last read byte), bmDONE (transaction byte complete), bmACK (ACK received), bmBERR (bus error). - -### 12.2 Bus Speed - -The I2C bus speed is 400 kHz, set via: -- C2 EEPROM header config byte = 0x40 (at boot) -- I2CTL = bm400KHZ (in custom firmware) - -### 12.3 Known Bus Devices - -| 7-bit Address | Wire Write/Read | Identity | -|---------------|-----------------|----------| -| 0x08 | 0x10 / 0x11 | BCM4500 demodulator | -| 0x10 | 0x20 / 0x21 | Tuner or LNB controller | -| 0x51 | 0xA2 / 0xA3 | Configuration EEPROM (24Cxx-family) | - -The EEPROM at 0x51 stores: device serial number (read by GET_SERIAL_NUMBER 0x93), hardware platform ID (read by GET_FPGA_VERS 0x95), and calibration data. - -### 12.4 Combined Write-Read (Repeated START) Protocol - -All BCM4500 register reads use the I2C combined write-read protocol with a repeated START condition. This is required because the BCM4500 uses a register-addressed protocol where the register number must be sent as a write phase before the read phase: - -``` -Complete I2C transaction for reading register 0xA2 from device 0x08: - - Phase 1 (Write): - [S] [0x10] [ACK] [0xA2] [ACK] - | | | | | - | | | | +-- BCM4500 ACKs register address - | | | +--------- Register address - | | +---------------- BCM4500 ACKs its address - | +----------------------- Device address (0x08 << 1) = 0x10 (write) - +---------------------------- START condition - - Phase 2 (Read with Repeated START): - [Sr] [0x11] [ACK] [DATA] [NACK] [P] - | | | | | | - | | | | | +-- STOP condition - | | | | +--------- Master NACKs (last byte) - | | | +---------------- Register data - | | +----------------------- BCM4500 ACKs its address - | +------------------------------ Device address (0x08 << 1 | 1) = 0x11 (read) - +------------------------------------ REPEATED START (no STOP between phases) -``` - -The repeated START (Sr) is essential. A STOP between phases would release the bus, and the BCM4500 would lose the register address context. - -**FX2 I2C SFR sequence for combined read (from custom firmware):** - -```c -I2CS |= bmSTART; // Generate START -I2DAT = 0x10; // Write: device addr + W -// wait bmDONE, check bmACK -I2DAT = 0xA2; // Write: register address -// wait bmDONE, check bmACK -I2CS |= bmSTART; // Generate REPEATED START (no STOP first!) -I2DAT = 0x11; // Write: device addr + R -// wait bmDONE, check bmACK -I2CS |= bmLASTRD; // Signal this is the last read byte -tmp = I2DAT; // Dummy read (triggers first clock burst) -// wait bmDONE -I2CS |= bmSTOP; // Generate STOP after reading -data = I2DAT; // Read actual data byte -// wait bmSTOP to clear -``` - -### 12.5 I2C STOP Corruption Bug - -Sending `I2CS |= bmSTOP` when no I2C transaction is active (no prior START issued, bus idle) corrupts the FX2 I2C controller's internal state machine. The bmSTOP bit may not self-clear, and subsequent START conditions fail to detect ACK from slaves. - -This was the root cause of the firmware hang in custom v3.01.0 during boot. The stock firmware's "bus reset" step: - -```c -/* BROKEN: */ -I2CS |= bmSTOP; -i2c_wait_stop(); -``` - -was removed. The correct approach is to simply proceed with a new START condition. If the bus is idle (after power-on or after the previous transaction completed normally), the START succeeds and the controller enters its normal operating state. The Cypress TRM does not document STOP as a standalone bus-reset mechanism. - -### 12.6 Timeout Protection - -The fx2lib I2C functions poll `bmDONE` with no timeout: - -```c -while (!(I2CS & bmDONE) && !cancel_i2c_trans); -``` - -Since `cancel_i2c_trans` is never set during normal operation, these loops are effectively infinite. The custom firmware replaces all fx2lib I2C functions with timeout-protected wrappers: - -```c -#define I2C_TIMEOUT 6000 - -static BOOL i2c_wait_done(void) { - WORD timeout = I2C_TIMEOUT; - while (!(I2CS & bmDONE)) { - if (--timeout == 0) return FALSE; - } - return TRUE; -} -``` - -A WORD counter of 6000 decremented in a tight SDCC-compiled loop at 48 MHz gives approximately 5--10 ms per wait. At 400 kHz I2C, a single byte transfer takes 22.5 us, so the timeout provides over 200x margin for normal operations. - ---- - -## 13. Custom Firmware v3.01.0 - -### 13.1 Overview - -Custom replacement firmware built with SDCC and fx2lib. Loaded into FX2 RAM for testing via `fw_load.py` (not flashed to EEPROM). - -| Property | Value | -|----------|-------| -| Toolchain | SDCC + fx2lib | -| Source | `firmware/skywalker1.c` (1351 lines) | -| Version ID | 0x030100 | -| Build date | 2026-02-12 | -| Load method | RAM upload via `tools/fw_load.py` | - -### 13.2 Stock-Compatible Commands - -The custom firmware implements all commands needed for the kernel driver: GET_8PSK_CONFIG (0x80), ARM_TRANSFER (0x85), TUNE_8PSK (0x86), GET_SIGNAL_STRENGTH (0x87), BOOT_8PSK (0x89), START_INTERSIL (0x8A), SET_LNB_VOLTAGE (0x8B), SET_22KHZ_TONE (0x8C), SEND_DISEQC (0x8D), GET_SIGNAL_LOCK (0x90), GET_FW_VERS (0x92), USE_EXTRA_VOLT (0x94). - -### 13.3 Custom Commands - -| Command | Function | -|---------|----------| -| SPECTRUM_SWEEP (0xB0) | Step through frequency range reading signal energy | -| RAW_DEMOD_READ (0xB1) | Read any BCM4500 indirect register | -| RAW_DEMOD_WRITE (0xB2) | Write any BCM4500 indirect register | -| BLIND_SCAN (0xB3) | Try symbol rates at a frequency looking for lock | -| I2C_BUS_SCAN (0xB4) | Probe all 7-bit I2C addresses | -| I2C_RAW_READ (0xB5) | Read from any I2C device address | -| I2C_DIAG (0xB6) | Step-by-step indirect register read diagnostic | - -### 13.4 Debug Boot Modes - -The BOOT_8PSK (0x89) command supports incremental debug modes via wValue: - -| wValue | Action | Result | -|--------|--------|--------| -| 0x80 | No-op: return `config_status` and `boot_stage` | Works | -| 0x81 | GPIO + power + delays only (no I2C) | Works | -| 0x82 | GPIO + power + I2C probe (bmSTOP removed) | Works | -| 0x83 | GPIO + power + probe + init block 0 | Works | -| 0x84 | I2C-only probe (chip already powered) | Works | -| 0x85 | Same as 0x82 without bmSTOP | Works | -| 0x01 | Full boot (production) | Works | -| 0x00 | Shutdown | Works | - -These modes were used to isolate the I2C STOP corruption bug (see [Section 12.4](#124-i2c-stop-corruption-bug)). - -### 13.5 Key Implementation Patterns - -**I2C Combined Read** (repeated START): - -```c -static BOOL i2c_combined_read(BYTE addr, BYTE reg, BYTE len, BYTE *buf) { - I2CS |= bmSTART; - I2DAT = addr << 1; // START + write address - // ... wait for DONE, check ACK ... - I2DAT = reg; // Register address - // ... wait for DONE, check ACK ... - I2CS |= bmSTART; - I2DAT = (addr << 1) | 1; // REPEATED START + read address - // ... read len bytes with LASTRD/STOP on final byte ... -} -``` - -**BCM4500 Init Block Write**: - -```c -static BOOL bcm_write_init_block(const __code BYTE *data, BYTE len) { - bcm_direct_write(BCM_REG_PAGE, 0x00); // Page select - i2c_write_multi_timeout(BCM4500_ADDR, BCM_REG_DATA, len, data); // Data - bcm_direct_write(BCM_REG_DATA, 0x00); // Trailing zero - bcm_direct_write(BCM_REG_CMD, BCM_CMD_WRITE); // Commit (0x03) - return bcm_poll_ready(); // Wait for completion -} -``` - ---- - -## 14. DVB-S2 Incompatibility - -### 14.1 Definitive Conclusion - -The SkyWalker-1's inability to receive DVB-S2 is a fundamental hardware limitation of the BCM4500 demodulator silicon. The BCM4500 was designed before the DVB-S2 standard was ratified (March 2005) and contains no LDPC or BCH decoder hardware. No firmware update can add DVB-S2 support. - -### 14.2 FEC Architecture Comparison - -| Feature | BCM4500 (SkyWalker-1) | DVB-S2 Requirement | -|---------|----------------------|-------------------| -| Inner FEC | Viterbi (DVB-S) or Turbo (proprietary) | LDPC | -| Outer FEC | Reed-Solomon (t=10) | BCH | -| Block size | Convolutional / short turbo blocks | 64,800 or 16,200 bits | -| Decoder type | Trellis (Viterbi) or iterative turbo | Iterative belief propagation | -| Hardware IP | Hardwired Viterbi + turbo silicon | Requires dedicated LDPC engine | - -### 14.3 Evidence - -**From firmware analysis:** - -1. The firmware modulation dispatch table has exactly 10 entries (0--9), with no DVB-S2-specific modes. The bounds check at CODE:0866 rejects values >= 10. -2. No LDPC/BCH code rate values exist in any FEC lookup table. The XRAM tables at 0xE0B1, 0xE0B7, 0xE0BC, 0xE0BD, and 0xE0F9 contain only Viterbi rates (1/2 through 7/8), turbo rates, and DCII combined codes. -3. No DVB-S2-specific register addresses appear in any I2C traffic. The BCM4500 is programmed exclusively through indirect registers 0xA6/0xA7/0xA8 with page 0x00. - -**From Windows BDA driver source:** - -4. `SkyWalker1TunerFilter.cpp` (line 1070): `else if(ulNewInnerFecType == BDA_FEC_VITERBI)` -- only Viterbi FEC is accepted; any other type returns `STATUS_INVALID_PARAMETER`. -5. `SkyWalker1Control.cpp` (line 292): `ucCommand[8] = ADV_MOD_DVB_QPSK;` -- the driver hardcodes modulation type 0 (DVB-S QPSK) regardless of application request. -6. `SkyWalker1Control.h` (lines 64--74): modulation constants cap at `ADV_MOD_DVB_BPSK` (9). No value 10+ exists. - -**From datasheets:** - -7. The BCM4500 datasheet describes exactly two FEC paths: "an advanced modulation turbo decoder" and "a DVB/DIRECTV/DCII-compliant FEC decoder." No third path for LDPC/BCH. -8. BCM4500 specification: 128-pin MQFP, 3.3V I/O, 1.8V digital, symbol rate 256 Ksps to 30 Msps. No mention of LDPC, BCH, or DVB-S2. - -### 14.4 Broadcom DVB-S2 Chip Timeline - -| Chip | Year | DVB-S2? | Notes | -|------|------|---------|-------| -| BCM4500 | ~2003 | No | Turbo FEC + legacy Viterbi/RS | -| BCM4501 | 2006 | Yes | First dual-tuner DVB-S2; LDPC/BCH | -| BCM4505 | 2007 | Yes | Single-channel, 65nm | -| BCM4506 | 2007 | Yes | Dual-channel, 65nm | - -Broadcom restricted BCM4501/4505/4506 sales to set-top box manufacturers, preventing Genpix from using them. - -### 14.5 What Genpix Did - -Released the SkyWalker-3, replacing the BCM4500 with a different demodulator (likely STMicroelectronics STV0903). The trade-off: gained DVB-S2 LDPC/BCH support, lost proprietary turbo-FEC support (turbo codes are Broadcom/EchoStar proprietary). - -### 14.6 USB Data Path is Not the Bottleneck - -The GPIF/USB 2.0 path has approximately 5x headroom for DVB-S2 rates (~58 Mbps max vs ~280 Mbps USB practical throughput). The 8-bit transport stream interface uses the same MPEG-TS format (188-byte packets). The bottleneck is the demodulator silicon. - ---- - -## 15. Kernel Driver Notes - -### 15.1 Module Names - -- `dvb_usb_gp8psk` -- USB transport and device management -- `gp8psk_fe` -- DVB frontend (demodulation, tuning) - -### 15.2 Kernel Driver Race Condition - -The kernel module auto-loads via udev when VID:PID `09C0:0203` appears on the USB bus (every FX2 re-enumeration after firmware load). The driver races with test tools and sends its own BOOT_8PSK command. - -Symptoms: -- "resource busy" or "entity not found" errors from test scripts -- BCM4500 enters unexpected state from partial kernel initialization -- Kernel driver detaches mid-test - -**Fix**: Blacklist the module: - -``` -# /etc/modprobe.d/blacklist-gp8psk.conf -blacklist dvb_usb_gp8psk -blacklist gp8psk_fe -``` - -Then unload: `sudo modprobe -r dvb_usb_gp8psk gp8psk_fe` - -### 15.3 FPGA Version Failure - -``` -gp8psk: usb in 149 operation failed. -gp8psk: failed to get FPGA version -``` - -Command 0x95 (GET_FPGA_VERS, decimal 149) fails on some SkyWalker-1 units. The driver logs the failure but continues normally. - -### 15.4 Commands Used by Kernel Driver - -| Command | Usage | Notes | -|---------|-------|-------| -| 0x80 GET_8PSK_CONFIG | Boot check | Always | -| 0x83 I2C_WRITE | BCM4500 reg writes | Via frontend ops | -| 0x84 I2C_READ | BCM4500 reg reads | Via frontend ops | -| 0x85 ARM_TRANSFER | Stream start/stop | Always | -| 0x86 TUNE_8PSK | Frequency tuning | Via frontend ops | -| 0x87 GET_SIGNAL_STRENGTH | SNR readback | Via frontend ops | -| 0x88 LOAD_BCM4500 | BCM4500 FW load | Rev.1 Warm only (STALLs on SW-1) | -| 0x89 BOOT_8PSK | Power on/off | Always | -| 0x8A START_INTERSIL | LNB power | Always | -| 0x8B SET_LNB_VOLTAGE | 13V/18V | Via frontend ops | -| 0x8C SET_22KHZ_TONE | Tone control | Via frontend ops | -| 0x8D SEND_DISEQC | DiSEqC messages | Via frontend ops | -| 0x8F SET_DN_SWITCH | Legacy Dish switch | Via `send_legacy_dish_cmd` callback | -| 0x90 GET_SIGNAL_LOCK | Lock status | Via frontend ops | -| 0x92 GET_FW_VERS | Version check | Boot only | -| 0x94 USE_EXTRA_VOLT | +1V boost | Via `enable_high_lnb_voltage` callback | -| 0x95 GET_FPGA_VERS | Platform ID | Boot only | -| 0x9D CW3K_INIT | CW3K init | PID 0x0206 only | - ---- - -## 16. Firmware Storage Formats - -### 16.1 Cypress C2 EEPROM Boot Format - -The SkyWalker-1 firmware is stored in Cypress C2 IIC second-stage boot format, read by the FX2's internal boot ROM on power-up. - -**Header (8 bytes):** - -| Offset | Size | Field | SkyWalker-1 Value | -|--------|------|-------|-------------------| -| 0 | 1 | Marker | 0xC2 (external memory, large code model) | -| 1 | 2 | VID (LE) | 0x09C0 | -| 3 | 2 | PID (LE) | 0x0203 | -| 5 | 2 | DID (LE) | 0x0000 | -| 7 | 1 | Config | 0x40 (400 kHz I2C) | - -**Code segments**: 2-byte length (BE) + 2-byte target address (BE) + data. Maximum segment size: 1023 bytes (FX2 I2C boot ROM buffer limit). All SkyWalker-1 variants use 10 segments. - -**Terminator**: 0x80xx (high bit set) + 2-byte entry point address (0xE600 = CPUCS). - -**Segment layout (all SkyWalker-1 variants):** - -``` -Segment Address Length -------- ------- ------ -1 0x0000 1023 Contains reset vector, interrupt handlers -2 0x03FF 1023 -3 0x07FE 1023 -4 0x0BFD 1023 -5 0x0FFC 1023 -6 0x13FB 1023 -7 0x17FA 1023 -8 0x1BF9 1023 -9 0x1FF8 1023 -10 0x23F7 varies (115--265 bytes depending on version) -``` - -### 16.2 Decoded C2 Headers - -| File | VID | PID | Segments | Code Size | Entry | -|------|-----|-----|----------|-----------|-------| -| skywalker1_eeprom.bin (v2.06) | 0x09C0 | 0x0203 | 10 | 9,472 bytes | 0xE600 | -| sw1_v213_fw_1_c2.bin (v2.13.1) | 0x09C0 | 0x0203 | 10 | 9,322 bytes | 0xE600 | -| sw1_v213_fw_2_c2.bin (v2.13.2) | 0x09C0 | 0x0203 | 10 | 9,377 bytes | 0xE600 | -| sw1_v213_fw_3_c2.bin (v2.13.3) | 0x09C0 | 0x0203 | 10 | 9,369 bytes | 0xE600 | -| rev2_v210_fw_1_c2.bin (Rev.2) | 0x09C0 | 0x0202 | 9 | 8,843 bytes | 0xE600 | - -### 16.3 DVB-USB Binary Hexline Format (Kernel FW01) - -The format the kernel expects for `dvb-usb-gp8psk-01.fw` (only needed for Rev.1 Cold, PID 0x0200): - -``` -Record structure: - Offset Size Field - 0 1 len - Number of data bytes - 1 1 addr_lo - Target address low byte - 2 1 addr_hi - Target address high byte - 3 1 type - 0x00=data, 0x01=EOF, 0x04=extended addr - 4 len data[] - Payload bytes - 4+len 1 chk - Checksum byte -``` - -### 16.4 FW02 Chunk Format (BCM4500 Firmware) - -Only needed for Rev.1 Warm (PID 0x0201): - -``` -Chunk format: - Byte 0: payload_length (N) - Bytes 1-3: header/address bytes - Bytes 4..N+3: payload data - Terminator: single byte 0xFF - Maximum chunk size: 64 bytes (USB control transfer limit) -``` - -Command 0x88 (LOAD_BCM4500) initiates the transfer. Each chunk is sent via bulk endpoint 0x01. On the SkyWalker-1, 0x88 routes to STALL (BCM4500 firmware is in ROM). - -### 16.5 Format Incompatibility - -C2 (EEPROM) and hexline (kernel FW01) are structurally different containers. They cannot be used interchangeably, but the payload data is identical. A C2 file can be converted to hexline by stripping the 8-byte header, splitting segments into 16-byte records, and appending an EOF record. - ---- - -## 17. Debugging Reference - -### 17.1 I2C STOP Corruption Root Cause - -The root cause of the initial firmware hang was traced through incremental debug modes: - -| wValue | Action | Result | Diagnosis | -|--------|--------|--------|-----------| -| 0x82 | GPIO + power + `bmSTOP` + probe | Fails | bmSTOP corrupts controller | -| 0x85 | GPIO + power + probe (no bmSTOP) | Works | Confirms bmSTOP is the cause | -| 0x84 | I2C probe only (chip already powered) | Works | BCM4500 is alive; I2C function is correct | - -Key finding: mode 0x84 succeeds immediately after 0x82 fails, proving the BCM4500 was alive the whole time. The FX2 I2C controller was in a bad state, not the bus or slave. - -### 17.2 Boot Results After Fix - -| Metric | Value | -|--------|-------| -| Boot time | ~90 ms total | -| config_status | 0x03 (STARTED + FW_LOADED) | -| boot_stage | 0xFF (COMPLETE) | -| Direct registers 0xA2-0xA8 | All return 0x02 (powered, not locked) | -| Signal lock | 0x00 (no lock -- dish not aimed) | -| USB responsiveness | No hang; fully responsive throughout | - -### 17.3 Test Tools - -Located in `tools/` directory: - -| Script | Purpose | -|--------|---------| -| `test_boot_debug.py` | Sends debug modes 0x80--0x83 sequentially | -| `test_i2c_debug.py` | Powers on via 0x81, runs bus scans, tests probe timing | -| `test_i2c_isolate.py` | Tests re-reset and insufficient delay as failure causes | -| `test_i2c_pinpoint.py` | Definitive test: compares 0x84, 0x85, and 0x82 | -| `fw_load.py` | RAM firmware loader (halt CPU, write, restart) | - -### 17.4 FX2 Register Quick Reference - -| Address | Name | Notes | -|---------|------|-------| -| 0xE600 | CPUCS | CPU control/status; write 0x01 to halt, 0x00 to run | -| 0xE601 | IFCONFIG | Interface configuration (GPIF mode, clock) | -| 0xE60B | REVCTL | Revision control (NOAUTOARM, SKIPCOMMIT) | -| 0xE618 | EP2FIFOCFG | EP2 FIFO configuration (AUTOIN, 8-bit) | -| 0xE678 | I2CS | I2C control/status | -| 0xE679 | I2DAT | I2C data | -| 0xE67A | I2CTL | I2C speed control | -| 0xE6B8 | SETUPDAT[0] | bmRequestType | -| 0xE6B9 | SETUPDAT[1] | bRequest | -| 0xE6BA | SETUPDAT[2] | wValueL | -| 0xE6BB | SETUPDAT[3] | wValueH | -| 0xE6BC | SETUPDAT[4] | wIndexL | -| 0xE6BD | SETUPDAT[5] | wIndexH | -| 0xE6BE | SETUPDAT[6] | wLengthL | -| 0xE6BF | SETUPDAT[7] | wLengthH | -| 0xE68A | EP0BCH | EP0 byte count high | -| 0xE68B | EP0BCL | EP0 byte count low (write triggers transfer) | -| 0xE740 | EP0BUF | EP0 data buffer start | -| 0xE0B6 | (custom) | LNB voltage control register (XRAM) | - ---- - -## 18. Sources - -### Firmware Analysis - -- Ghidra decompilation/disassembly of five firmware images: - - v2.06.04 (Ghidra port 8193) -- extracted from SkyWalker-1 EEPROM - - Rev.2 v2.10.04 (Ghidra port 8197) -- extracted from Rev.2 hardware - - v2.13.01 FW1 (Ghidra port 8194) -- extracted from Windows updater - - v2.13.02 FW2 (Ghidra port 8195) -- extracted from Windows updater - - v2.13.03 FW3 (Ghidra port 8196) -- extracted from Windows updater -- Firmware dumps: `firmware-dump/` - -### Driver Source - -- Linux kernel 6.16.5: `drivers/media/usb/dvb-usb/gp8psk.c`, `gp8psk.h`, `gp8psk-fe.c`, `gp8psk-fe.h` -- Linux kernel: `drivers/media/usb/dvb-usb/dvb-usb-firmware.c` -- Windows BDA driver: `SkyWalker1_Final_Release/Source/SkyWalker1Control.cpp` -- Windows BDA driver: `SkyWalker1_Final_Release/Include/SkyWalker1Control.h`, `SkyWalker1CommonDef.h` - -### Hardware Documentation - -- BCM4500 Datasheet: [DatasheetQ](https://html.datasheetq.com/pdf-html/885700/Broadcom/2page/BCM4500.html), [Elcodis](https://elcodis.com/parts/5786421/BCM4500.html) -- BCM4501 Product Page: [Broadcom](https://www.broadcom.com/products/broadband/set-top-box/bcm4501) -- Cypress CY7C68013A (FX2LP) Technical Reference Manual -- Genpix Electronics: https://www.genpix-electronics.com/index.php?act=viewDoc&docId=9 -- Genpix SkyWalker-3 specifications: https://www.genpix-electronics.com/what-is-skywalker-3.html -- Device `dmesg` output from running SkyWalker-1 hardware - -### Analysis Reports (This Project) - -1. `gp8psk-driver-analysis.md` -- Linux kernel driver analysis -2. `firmware-analysis-v206-vs-v213.md` -- v2.06 vs v2.13 firmware comparison -3. `rev2-deep-analysis.md` -- Rev.2 deep function inventory (107 functions) -4. `gpif-streaming-analysis.md` -- GPIF/MPEG-2 streaming path -5. `tuning-protocol-analysis.md` -- TUNE_8PSK protocol deep dive -6. `vendor-commands-unknown.md` -- Vendor command decode (0x8F, 0x91--0x98) -7. `kernel-fw01-analysis.md` -- Kernel firmware format and EEPROM boot -8. `firmware-dump/fw_v213_comparison_report.md` -- v2.13 sub-variant comparison -9. `dvb-s2-investigation.md` -- DVB-S2 incompatibility investigation -10. `docs/boot-debug-findings.md` -- Boot/I2C debugging findings -11. `docs/diseqc/diseqc-skywalker-1.md` -- DiSEqC Windows BDA interface -12. `firmware/skywalker1.c` -- Custom firmware v3.01.0 source - -### Community References - -- [LinuxTV mailing list: BCM4500 and DVB-S2](https://www.mail-archive.com/linux-dvb@linuxtv.org/msg24808.html) -- [SatelliteGuys: Turbo 8PSK card reverse engineering](https://www.satelliteguys.us/xen/threads/another-turbo-8psk-card.246879/) -- [SatelliteGuys: Genpix SkyWalker-1 discussion](https://www.satelliteguys.us/xen/threads/genpix-skywalker-1.214196/) +# Genpix SkyWalker-1 Master Hardware and Firmware Reference + +Consolidated technical reference for the Genpix SkyWalker-1 DVB-S USB 2.0 satellite receiver. Derived from Linux kernel driver analysis (`dvb_usb_gp8psk`), Ghidra firmware reverse engineering (v2.06, v2.10 Rev.2, v2.13 FW1/FW2/FW3), Windows BDA driver source review, and custom firmware development (v3.01.0, SDCC + fx2lib). + +--- + +## Table of Contents + +1. [Hardware Overview](#1-hardware-overview) +2. [USB Interface](#2-usb-interface) +3. [Vendor Command Reference](#3-vendor-command-reference) +4. [Configuration Status Byte](#4-configuration-status-byte) +5. [Boot Sequence](#5-boot-sequence) +6. [BCM4500 Demodulator Interface](#6-bcm4500-demodulator-interface) +7. [Tuning Protocol](#7-tuning-protocol) +8. [GPIF Streaming Path](#8-gpif-streaming-path) +9. [LNB and DiSEqC Control](#9-lnb-and-diseqc-control) +10. [GPIO Pin Map](#10-gpio-pin-map) +11. [Firmware Versions](#11-firmware-versions) +12. [I2C Bus Architecture](#12-i2c-bus-architecture) +13. [Custom Firmware v3.01.0](#13-custom-firmware-v3010) +14. [DVB-S2 Incompatibility](#14-dvb-s2-incompatibility) +15. [Kernel Driver Notes](#15-kernel-driver-notes) +16. [Firmware Storage Formats](#16-firmware-storage-formats) +17. [Debugging Reference](#17-debugging-reference) +18. [Sources](#18-sources) + +--- + +## 1. Hardware Overview + +The Genpix SkyWalker-1 is a standalone USB 2.0 DVB-S satellite receiver built around two ICs: + +| Component | Part | Role | +|-----------|------|------| +| MCU | Cypress CY7C68013A (FX2LP) | USB 2.0 Hi-Speed controller, 8051 core at 48 MHz | +| Demodulator | Broadcom BCM4500 | DVB-S / Turbo / DCII / DSS demodulator, 128-pin MQFP | +| EEPROM | 24Cxx-family (I2C address 0x51) | FX2 firmware storage, serial number, calibration | +| Tuner/LNB | Unknown IC (I2C address 0x10) | Tuner or LNB controller on shared I2C bus | + +The FX2 handles USB communication, LNB control, DiSEqC signaling, and orchestrates tuning via I2C commands to the BCM4500. The BCM4500 performs RF demodulation, forward error correction, and outputs an MPEG-2 transport stream on an 8-bit parallel bus. The FX2's GPIF engine transfers the transport stream directly into a USB bulk endpoint with zero firmware intervention in the data path. + +### 1.1 Supported Modulations + +| Index | Modulation | Constant | FEC Family | +|-------|-----------|----------|------------| +| 0 | DVB-S QPSK | `ADV_MOD_DVB_QPSK` | Viterbi + Reed-Solomon | +| 1 | Turbo-coded QPSK | `ADV_MOD_TURBO_QPSK` | Turbo | +| 2 | Turbo-coded 8PSK | `ADV_MOD_TURBO_8PSK` | Turbo | +| 3 | Turbo-coded 16QAM | `ADV_MOD_TURBO_16QAM` | Turbo | +| 4 | Digicipher II Combo | `ADV_MOD_DCII_C_QPSK` | DCII | +| 5 | Digicipher II I-stream (split) | `ADV_MOD_DCII_I_QPSK` | DCII | +| 6 | Digicipher II Q-stream (split) | `ADV_MOD_DCII_Q_QPSK` | DCII | +| 7 | Digicipher II Offset QPSK | `ADV_MOD_DCII_C_OQPSK` | DCII | +| 8 | DSS QPSK | `ADV_MOD_DSS_QPSK` | Viterbi + Reed-Solomon | +| 9 | DVB-S BPSK | `ADV_MOD_DVB_BPSK` | Viterbi + Reed-Solomon | + +DVB-S2 is not supported. See [Section 14](#14-dvb-s2-incompatibility). + +### 1.2 RF Specifications + +| Parameter | Value | +|-----------|-------| +| IF frequency range | 950 -- 2150 MHz | +| Symbol rate | 256 Ksps -- 30 Msps | +| Input connector | IEC F-type female | +| LNB voltage | 13V / 18V (or 14V / 19V with USE_EXTRA_VOLT) | +| LNB current | 450 mA continuous, 750 mA burst | +| Switch control | 22 kHz, Tone Burst, DiSEqC 1.0/1.2, Legacy Dish Network | + +### 1.3 Board Block Diagram + +``` + +--[ I2C EEPROM 0x51 ] + | + USB 2.0 HS | I2C Bus (400 kHz) + Host PC <----> [ CY7C68013A FX2LP ] <--I2C--> [ BCM3440 Tuner 0x10 ] <--gateway--> [ BCM4500 Demod ] + <--I2C--> [ BCM4500 Direct 0x08 (status only) ] + | 8051 @ 48 MHz | | + | GPIF Engine |<-----------+ 8-bit parallel TS + | EP2 Bulk IN | + | GPIO (P0/P3) |---> [ 22 kHz Osc ] ---> LNB/Coax + | |---> [ LNB Voltage Ctrl ] + +-----------------+ + | + +--[ Tuner/LNB IC 0x10 ] +``` + +--- + +## 2. USB Interface + +### 2.1 VID/PID Table + +All Genpix products share VID `0x09C0`: + +| PID | Product | cold_ids | warm_ids | Notes | +|-----|---------|----------|----------|-------| +| 0x0200 | 8PSK-to-USB2 Rev.1 Cold | Yes | No | Requires FW01 upload to RAM | +| 0x0201 | 8PSK-to-USB2 Rev.1 Warm | No | Yes | Requires FW02 (BCM4500 firmware) | +| 0x0202 | 8PSK-to-USB2 Rev.2 | No | Yes | Boots from EEPROM | +| 0x0203 | **SkyWalker-1** | No | Yes | Boots from EEPROM | +| 0x0204 | SkyWalker-1 (alternate) | No | Yes | Boots from EEPROM | +| 0x0205 | SkyWalker-2 | -- | -- | Not in kernel 6.16.5 | +| 0x0206 | SkyWalker CW3K | No | Yes | Requires CW3K_INIT (0x9D) | + +PID 0x0203 was added to the kernel device table after v6.6.1. + +### 2.2 USB Endpoints and Streaming Properties + +| Property | Value | +|----------|-------| +| Control endpoint | EP0 (default, vendor requests) | +| Bulk IN endpoint | EP2 (0x82) -- MPEG-2 transport stream | +| Generic bulk CTRL endpoint | 0x01 (BCM4500 FW02 upload, Rev.1 only) | +| URB count | 7 | +| URB buffer size | 8192 bytes each | +| Stream type | USB_BULK | +| FX2 controller type | CYPRESS_FX2 | + +### 2.3 Warm Boot Behavior + +The SkyWalker-1 (PID 0x0203) enumerates directly as a "warm" device. The DVB-USB framework skips firmware download when `cold_ids` is NULL. No host-side firmware files are required. + +| Device | PID | Needs FW01? | Needs FW02? | Boot Source | +|--------|-----|-------------|-------------|-------------| +| Rev.1 Cold | 0x0200 | Yes | -- | RAM (empty) | +| Rev.1 Warm | 0x0201 | No | Yes | RAM (FW01 loaded) | +| Rev.2 | 0x0202 | No | No | EEPROM | +| SkyWalker-1 | 0x0203 | No | No | EEPROM | +| SkyWalker CW3K | 0x0206 | No | No | EEPROM | + +The firmware files `dvb-usb-gp8psk-01.fw` and `dvb-usb-gp8psk-02.fw` were never open-sourced or included in `linux-firmware`. + +--- + +## 3. Vendor Command Reference + +All vendor commands use USB control transfers: +- **USB Type**: `USB_TYPE_VENDOR` +- **Timeout**: 2000 ms (kernel driver) +- **Retry**: Up to 3 attempts for IN operations if partial data received +- **Data buffer maximum**: 80 bytes (kernel driver state structure) + +### 3.1 Stock Command Table (0x80--0x9D) + +The vendor command dispatcher at CODE:0056 validates `bRequest` in the range 0x80--0x9D (30 entries) and dispatches via an indexed jump table at CODE:0076. Rev.2 supports only 0x80--0x9A (27 entries). + +| Cmd | Name | Dir | wValue | wIndex | wLength | Purpose | v2.06 | Rev.2 | v2.13 | +|-----|------|-----|--------|--------|---------|---------|-------|-------|-------| +| 0x80 | GET_8PSK_CONFIG | IN | 0 | 0 | 1 | Read configuration status byte | OK | OK | OK | +| 0x81 | SET_8PSK_CONFIG | OUT | varies | 0 | 0 | Set config (reserved) | STALL | STALL | STALL | +| 0x82 | (reserved) | -- | -- | -- | -- | Reserved | STALL | STALL | STALL | +| 0x83 | I2C_WRITE | OUT | dev_addr | reg_addr | N | Write to I2C device | OK | OK | OK | +| 0x84 | I2C_READ | IN | dev_addr | reg_addr | N | Read from I2C device | OK | OK | OK | +| 0x85 | ARM_TRANSFER | OUT | 0/1 | 0 | 0 | Start (1) / stop (0) MPEG-2 stream | OK | OK | OK | +| 0x86 | TUNE_8PSK | OUT | 0 | 0 | 10 | Set tuning parameters ([Section 7](#7-tuning-protocol)) | OK | OK | OK | +| 0x87 | GET_SIGNAL_STRENGTH | IN | 0 | 0 | 6 | Read SNR and diagnostics | OK | OK | Changed | +| 0x88 | LOAD_BCM4500 | OUT | 1 | 0 | 0 | Initiate BCM4500 FW download | STALL | STALL | STALL | +| 0x89 | BOOT_8PSK | IN | 0/1 | 0 | 1 | Power on (1) / off (0) demodulator | OK | OK | OK | +| 0x8A | START_INTERSIL | IN | 0/1 | 0 | 1 | Enable (1) / disable (0) LNB supply | OK | OK | OK | +| 0x8B | SET_LNB_VOLTAGE | OUT | 0/1 | 0 | 0 | 13V (0) or 18V (1) | OK | OK | OK | +| 0x8C | SET_22KHZ_TONE | OUT | 0/1 | 0 | 0 | Tone off (0) or on (1) | OK | OK | OK | +| 0x8D | SEND_DISEQC_COMMAND | OUT | msg[0] | 0 | len | DiSEqC message or tone burst | OK | OK | OK | +| 0x8E | SET_DVB_MODE | OUT | 1 | 0 | 0 | Enable DVB-S mode | STALL | STALL | STALL | +| 0x8F | SET_DN_SWITCH | OUT | cmd7bit | 0 | 0 | Legacy Dish Network switch protocol | OK | OK | OK | +| 0x90 | GET_SIGNAL_LOCK | IN | 0 | 0 | 1 | Read signal lock status | OK | OK | OK | +| 0x91 | I2C_ADDR_ADJUST | IN | 0/1 | 0 | 1 | Inc/dec internal counter (debug) | OK | OK | OK | +| 0x92 | GET_FW_VERS | IN | 0 | 0 | 6 | Read firmware version + build date | OK | OK | OK | +| 0x93 | GET_SERIAL_NUMBER | IN | 0 | 0 | 4 | Read 4-byte serial from EEPROM | OK | OK | OK | +| 0x94 | USE_EXTRA_VOLT | OUT | 0/1 | 0 | 0 | Enable +1V LNB boost (14V/19V) | OK | OK | OK | +| 0x95 | GET_FPGA_VERS | IN | 0 | 0 | 1 | Read EEPROM hardware/platform ID | OK | OK | OK | +| 0x96 | SET_LNB_GPIO_MODE | OUT | 0/1 | 0 | 0 | Configure LNB GPIO output enables | OK | OK | OK | +| 0x97 | SET_GPIO_PINS | OUT | bitmap | 0 | 0 | Direct write to LNB GPIO pins | OK | OK | OK | +| 0x98 | GET_GPIO_STATUS | IN | 0 | 0 | 1 | Read LNB feedback GPIO pin | OK | OK | OK | +| 0x99 | GET_DEMOD_STATUS | IN | 0 | 0 | 1 | Read BCM4500 register 0xF9 | STALL | Proto | OK | +| 0x9A | INIT_DEMOD | OUT | 0 | 0 | 0 | Trigger demod re-init (3 attempts) | STALL | Proto | OK | +| 0x9B | (reserved) | -- | -- | -- | -- | Reserved | STALL | N/A | STALL | +| 0x9C | DELAY_COMMAND | OUT | delay | 0 | 0 | Host-controlled tuning delay + poll | STALL | N/A | OK | +| 0x9D | CW3K_INIT / SET_MODE_FLAG | OUT | 0/1 | 0 | 0 | CW3K init or conditional demod reset | OK | N/A | Changed | + +**Status key**: OK = implemented. STALL = routes to stall handler. Proto = partial/prototype. N/A = out of range (Rev.2 supports 0x80--0x9A only). Changed = implementation differs between versions. + +**Driver usage notes**: +- The Linux driver only sends LOAD_BCM4500 (0x88) for Rev.1 Warm (PID 0x0201). On SkyWalker-1, `bm8pskFW_Loaded` is already set and 0x88 STALLs. +- The Linux driver only sends CW3K_INIT (0x9D) for SkyWalker CW3K (PID 0x0206). + +### 3.2 Vendor Command Dispatch Mechanism + +The vendor command dispatcher at CODE:0056 (identical code address across v2.06, v2.13, and Rev.2) follows this logic: + +``` +1. Check bmRequestType bit 6: if not set, not a vendor request -> handle standard +2. Read bRequest from SETUPDAT[1] +3. Subtract 0x80 (command base offset) +4. Compare against maximum: < 0x1E (v2.06/v2.13) or < 0x1B (Rev.2) +5. If in range: double the index (2 bytes per entry) and JMP @A+DPTR to jump table +6. If out of range: route to STALL handler +``` + +The jump table at CODE:0076 contains 2-byte AJMP targets. Each entry points to the handler for commands 0x80 through 0x9D (or 0x9A for Rev.2). + +**Jump table layout (first 6 entries shown, Rev.2):** + +``` +CODE:0076: 01C1 ; 0x80 GET_8PSK_CONFIG -> 0x01C1 +CODE:0078: 034B ; 0x81 SET_8PSK_CONFIG -> 0x034B (STALL) +CODE:007A: 034B ; 0x82 (reserved) -> 0x034B (STALL) +CODE:007C: 0103 ; 0x83 I2C_WRITE -> 0x0103 +CODE:007E: 00D9 ; 0x84 I2C_READ -> 0x00D9 +CODE:0080: 00C2 ; 0x85 ARM_TRANSFER -> 0x00C2 +... +``` + +### 3.3 Custom Firmware Commands (0xB0--0xB6) + +Commands added in custom firmware v3.01.0: + +| Cmd | Name | Dir | wValue | wIndex | wLength | Purpose | +|-----|------|-----|--------|--------|---------|---------| +| 0xB0 | SPECTRUM_SWEEP | OUT | 0 | 0 | 10 | Step through freq range, read SNR at each step | +| 0xB1 | RAW_DEMOD_READ | IN | reg | 0 | 1 | Read BCM4500 indirect register | +| 0xB2 | RAW_DEMOD_WRITE | OUT | reg | data | 0 | Write BCM4500 indirect register | +| 0xB3 | BLIND_SCAN | OUT | 0 | 0 | 16 | Try symbol rates at given freq, report lock | +| 0xB4 | I2C_BUS_SCAN | IN | 0 | 0 | 16 | Probe all 7-bit addresses, return 16-byte bitmap | +| 0xB5 | I2C_RAW_READ | IN | addr7 | reg | N | Combined write-read from any I2C device | +| 0xB6 | I2C_DIAG | IN | page | 0 | 8 | Step-by-step indirect register diagnostic | + +### 3.4 Detailed Parameter Formats + +**0x87 GET_SIGNAL_STRENGTH**: Returns 6 bytes. Bytes 0--1 are a 16-bit SNR value (little-endian, dBu * 256 units). Bytes 2--5 are reserved/diagnostic. SNR scaling from Windows BDA driver: `if snr_raw <= 0x0F00: strength = snr_raw * 17; else strength = 0xFFFF`. Version differences: v2.06 polls 3 registers (0xA2, 0xA8, 0xA4) up to 6 times; v2.13 consolidates to 1 register. + +**0x8D SEND_DISEQC_COMMAND**: When `wLength > 0`, the payload is a standard DiSEqC message (3--6 bytes) with `wValue` = `msg[0]` (framing byte, typically 0xE0 or 0xE1). When `wLength == 0`: `wValue == 0` sends tone burst A; `wValue != 0` sends tone burst B. See [Section 9](#9-lnb-and-diseqc-control). + +**0x8F SET_DN_SWITCH**: `wValue` carries a 7-bit Dish Network switch command, bit-banged LSB-first on GPIO P0.4. The 8th bit (0x80) of the original switch command controls LNB voltage and is sent separately via SET_LNB_VOLTAGE (0x8B). + +**0x92 GET_FW_VERS**: Returns 6 bytes of hardcoded constants: + +``` +Byte 0: version minor_minor (e.g., 0x04) +Byte 1: version minor (e.g., 0x06) +Byte 2: version major (e.g., 0x02) +Byte 3: build day (e.g., 0x0D = 13) +Byte 4: build month (e.g., 0x07 = July) +Byte 5: build year - 2000 (e.g., 0x07 = 2007) +``` + +Full version = `byte[2] << 16 | byte[1] << 8 | byte[0]`. Build date = `(2000 + byte[5]) / byte[4] / byte[3]`. + +**0x93 GET_SERIAL_NUMBER**: Returns 4 bytes read from I2C EEPROM at device address 0x51 (7-bit), extracted at 8-bit intervals using a shift/rotate routine. + +**0x94 USE_EXTRA_VOLT**: `wValue=1` writes 0x6A to XRAM 0xE0B6; `wValue=0` writes 0x62. The difference is bit 3 (0x08), which controls the voltage boost on the LNB power regulator. + +**0x95 GET_FPGA_VERS**: Reads from I2C EEPROM at 0x51. Despite the name, there is no FPGA on the SkyWalker-1 -- this returns a hardware platform ID. v2.06 reads EEPROM offset 0x31 (2 bytes); v2.13/Rev.2 read offset 0x00 (1 byte). + +**0xB0 SPECTRUM_SWEEP**: 10-byte EP0 payload: `[start_freq(u32 LE kHz), stop_freq(u32 LE kHz), step_khz(u16 LE)]`. Programs BCM4500 at each frequency step, reads SNR, packs u16 LE results into EP2 bulk FIFO. + +**0xB3 BLIND_SCAN**: 16-byte EP0 payload: `[freq_khz(u32 LE), sr_min(u32 LE sps), sr_max(u32 LE sps), sr_step(u32 LE sps)]`. Returns 8 bytes on lock `[freq_khz(4) + sr_locked(4)]` or 1 byte 0x00 if no lock found. + +**0xB4 I2C_BUS_SCAN**: Returns a 16-byte bitmap (128 bits for addresses 0x00--0x77). Each bit position corresponds to a 7-bit address; bit set = ACK received. Known devices on the SkyWalker-1 bus: + +| Address | Identity | +|---------|----------| +| 0x08 | BCM4500 demodulator (7-bit; wire addresses 0x10 write / 0x11 read) | +| 0x10 | Tuner or LNB controller | +| 0x51 | Configuration EEPROM (24Cxx-family) | + +--- + +## 4. Configuration Status Byte + +Returned by GET_8PSK_CONFIG (0x80). Stored in IRAM at a version-dependent address. + +``` +Bit 7 (0x80): bmArmed - MPEG-2 stream transfer armed / GPIF active +Bit 6 (0x40): bmDCtuned - DC offset tuning complete (set for DCII modes) +Bit 5 (0x20): bmSEL18V - 18V LNB voltage selected (else 13V) +Bit 4 (0x10): bm22kHz - 22 kHz tone active +Bit 3 (0x08): bmDVBmode - DVB mode enabled +Bit 2 (0x04): bmIntersilOn - LNB power supply enabled +Bit 1 (0x02): bm8pskFW_Loaded - BCM4500 firmware loaded (always set on SkyWalker-1) +Bit 0 (0x01): bm8pskStarted - Device booted and running +``` + +| Firmware | IRAM Address | +|----------|-------------| +| v2.06 | 0x6D | +| Rev.2 v2.10.4 | 0x4E | +| v2.13 | 0x4F | + +The kernel driver checks these bits to decide which initialization steps to perform. On the SkyWalker-1 after a successful BOOT_8PSK, `config_status = 0x03` (STARTED + FW_LOADED). + +--- + +## 5. Boot Sequence + +### 5.1 Kernel Driver Boot Flow + +``` +1. GET_8PSK_CONFIG (0x80) -- read config status byte + |-- Check bit 0: bm8pskStarted? + +2. If not started: + |-- BOOT_8PSK (0x89, wValue=1) + |-- GET_FW_VERS (0x92) -- read firmware version + +3. If bit 1 clear (bm8pskFW_Loaded): + |-- LOAD_BCM4500 (0x88) -- Rev.1 Warm only; STALLs on SkyWalker-1 + +4. If bit 2 clear (bmIntersilOn): + |-- START_INTERSIL (0x8A, wValue=1) -- enable LNB power supply + +5. SET_DVB_MODE (0x8E, wValue=1) -- STALLs on all SkyWalker-1 FW versions + +6. ARM_TRANSFER (0x85, wValue=0) -- abort any pending MPEG transfer + +7. Device ready for tuning +``` + +### 5.2 BCM4500 Boot Sequence (BOOT_8PSK, 0x89) + +As implemented in `bcm4500_boot()` in custom firmware v3.01.0, reverse-engineered from stock v2.06 `FUN_CODE_1D4F` + `FUN_CODE_0ddd`: + +``` +Step Action GPIO/I2C Duration +---- ------------------------------------ ----------------- -------- +1 Assert BCM4500 RESET P0.5 = LOW -- +2 Power on P0.1 = HIGH -- + P0.2 = LOW +3 Wait for power settle -- 30 ms +4 Release RESET P0.5 = HIGH -- +5 Wait for BCM4500 POR + ROM boot -- 50 ms +6 I2C probe (read register 0xA2) I2C read 0x08:0xA2 ~0.1 ms +7 Write init block 0 to page 0 I2C write 0xA6/A7/A8 ~2 ms +8 Write init block 1 to page 0 I2C write 0xA6/A7/A8 ~2 ms +9 Write init block 2 to page 0 I2C write 0xA6/A7/A8 ~1 ms +10 Set config_status = 0x03 -- -- +``` + +**Total boot time**: approximately 90 ms (30 ms power + 50 ms POR + ~10 ms I2C). + +### 5.3 BCM4500 Initialization Data + +Three register initialization blocks are written to BCM4500 indirect registers (page 0x00) via the 0xA6/0xA7/0xA8 protocol. Data extracted from stock v2.06 firmware `FUN_CODE_0ddd`: + +| Block | Start Register | Length | Data (hex) | +|-------|---------------|--------|------------| +| 0 | 0x06 | 7 bytes | `06 0b 17 38 9f d9 80` | +| 1 | 0x07 | 8 bytes | `07 09 39 4f 00 65 b7 10` | +| 2 | 0x0F | 3 bytes | `0f 0c 09` | + +Each block is written as: page select (0xA6 = 0x00), data bytes to 0xA7, trailing zero to 0xA7, then commit (0xA8 = 0x03). The firmware polls 0xA8 until the command completes before proceeding to the next block. + +### 5.4 FX2 CPUCS Recovery + +The FX2's CPUCS register at 0xE600 controls the 8051 run/halt state. The standard vendor request bRequest=0xA0 (RAM read/write) is handled by the FX2 boot ROM in silicon, not by user firmware. This means `fw_load.py` can reload firmware over a completely hung device: + +```bash +sudo python3 tools/fw_load.py load firmware/build/skywalker1.ihx --wait 3 +``` + +Writing 0x01 to CPUCS halts the CPU. New code is written to RAM. Writing 0x00 restarts it. The device re-enumerates with the new firmware. + +--- + +## 6. BCM4500 Demodulator Interface + +### 6.1 I2C Addressing — BCM3440 Tuner Gateway + +> **CRITICAL (2025-02-19):** The BCM4500's registers are accessed THROUGH the +> BCM3440 tuner's I2C gateway at address 0x10, NOT directly at 0x08. +> Stock firmware v2.06 disassembly of FUN_CODE_0DDD, FUN_CODE_10F2, and all +> internal register access functions confirms device address 0x10 is used +> for every register read/write. + +| Parameter | Value | +|-----------|-------| +| BCM3440 tuner gateway (7-bit) | **0x10** — all BCM4500 register access | +| BCM3440 wire write / read | 0x20 / 0x21 | +| BCM4500 direct (7-bit) | 0x08 — status byte only, no register addressing | +| BCM4500 wire write / read | 0x10 / 0x11 | +| Bus speed | 400 kHz | +| FX2 I2C controller SFRs | I2CS, I2DAT, I2CTL | +| Alternate probe addresses (v2.13) | 0x3F, 0x7F | + +The BCM3440 tuner acts as an I2C bridge/gateway: register accesses in the 0xA0+ range sent to the tuner's address (0x10) are transparently forwarded to the BCM4500 demodulator. The BCM4500's own I2C address (0x08) only exposes a single status byte via simple reads — it does NOT support register-addressed reads at that address. + +**Stock firmware evidence (functions at wire address 0x20/0x21):** +- `FUN_CODE_0DDD` (init blocks): writes A6/A7/A8 via `LCALL 0x1A81` with R7=0x10 +- `FUN_CODE_10F2` (PLL/firmware download): writes A9/AA/AB via `LCALL 0x1A81` with R7=0x10 +- `FUN_CODE_15E9` (config mode): writes A0 via device 0x10 +- `FUN_CODE_1556` (generic read): combined read `[S][0x20][reg][Sr][0x21][data][P]` + +**Stock I2C_READ (0x84) vendor command for BCM4500 (address 0x08):** Simple read only — `[S][0x11][data][P]` — no register address sent. Returns whatever the BCM4500's I2C slave has ready (global status byte). Register address from wIndex is completely ignored for device 0x08 (confirmed by disassembly of `FUN_CODE_2036`). + +The v2.13 firmware probes addresses 0x7F and 0x3F at startup (INT0 handler) to detect which demodulator variant is present. + +### 6.2 Direct Registers + +Accessed via I2C write/read through the BCM3440 gateway (address 0x10): + +| Register | Function | +|----------|----------| +| 0xA2 | Status register (polled for readiness during boot) | +| 0xA4 | Lock/ready register; bit 5 (0x20) = signal locked | +| 0xA6 | Indirect page/address select | +| 0xA7 | Indirect data register (read/write) | +| 0xA8 | Indirect command register | +| 0xF9 | Demod status (read by v2.13 GET_DEMOD_STATUS / INT0 polling) | + +### 6.3 Indirect Register Protocol + +The BCM4500 uses an indirect register access scheme through three directly-addressable registers: + +**Indirect Write Sequence:** + +``` +1. I2C WRITE to 0x08, register 0xA6 <- page_number (typically 0x00) +2. I2C WRITE to 0x08, register 0xA7 <- data bytes (N bytes, auto-increment) +3. I2C WRITE to 0x08, register 0xA8 <- 0x03 (execute indirect write) +4. Poll register 0xA8 until bit 0 clear (command complete) +5. Optionally read back register 0xA7 to verify +``` + +**Indirect Read Sequence:** + +``` +1. I2C WRITE to 0x08, register 0xA6 <- target_register +2. I2C WRITE to 0x08, register 0xA7 <- 0x00 (placeholder) +3. I2C WRITE to 0x08, register 0xA8 <- 0x01 (execute indirect read) +4. Short delay (~1 ms) +5. I2C READ from 0x08, register 0xA7 <- result byte +``` + +### 6.4 Indirect Protocol Auto-Increment + +The BCM4500's data register (0xA7) supports auto-increment for multi-byte writes within a single I2C transaction. When writing N data bytes to 0xA7 in one I2C WRITE operation (without issuing STOP between bytes), the BCM4500 internally advances its data buffer pointer after each byte. This allows writing an entire initialization block in a single I2C transaction: + +``` +I2C transaction: + START -> 0x10 (write) -> 0xA7 (reg) -> data[0] -> data[1] -> ... -> data[N-1] -> STOP +``` + +The firmware exploits this for initialization blocks and tuning data, reducing I2C overhead compared to byte-by-byte writes. + +**Stock firmware init block write sequence (from FUN_CODE_0ddd):** + +``` +1. I2C WRITE: [0x10] [0xA6] [0x00] -- Page select = 0 +2. I2C WRITE: [0x10] [0xA7] [data0..dataN] -- Multi-byte data (auto-increment) +3. I2C WRITE: [0x10] [0xA7] [0x00] -- Trailing zero (stock firmware quirk) +4. I2C WRITE: [0x10] [0xA8] [0x03] -- Commit indirect write +5. Poll: I2C READ [0xA8] until bit 0 clear -- Wait for completion +``` + +The trailing zero write (step 3) appears in all stock firmware versions. Its purpose is unclear -- it may zero-pad the data buffer or serve as an end-of-data marker within the BCM4500's indirect register engine. + +### 6.5 Demodulator Scan + +The tune function (stock firmware) tries up to 3 different I2C address configurations per attempt, with 3 outer retries (up to 9 total I2C programming attempts). This supports hardware variants where the BCM4500 may appear at different bus addresses. + +v2.13 adds a boot-time probe: INT0 polls addresses 0x7F and 0x3F up to 40 times (0x28), setting flag `_1_4` if neither responds. This prevents tuning attempts on boards with absent demodulators. + +### 6.6 BCM4500 FEC Architecture + +The BCM4500 contains two FEC decoder paths: + +1. **Advanced Modulation Turbo FEC Decoder**: Iterative turbo code decoder supporting QPSK (rates 1/4, 1/2, 3/4), 8PSK (rates 2/3, 3/4, 5/6, 8/9), 16QAM (rate 3/4), with Reed-Solomon (t=10) outer code. + +2. **Legacy DVB/DIRECTV/DCII-Compliant FEC Decoder**: Concatenated Viterbi inner decoder (convolutional code, rates 1/2 through 7/8) + Reed-Solomon outer decoder. + +There is no LDPC or BCH decoder hardware. See [Section 14](#14-dvb-s2-incompatibility). + +--- + +## 7. Tuning Protocol + +### 7.1 TUNE_8PSK Command Format (0x86) + +The host sends a 10-byte OUT payload via USB control transfer: + +``` +USB SETUP: bmRequestType=0x40, bRequest=0x86, wValue=0, wIndex=0, wLength=10 + +EP0BUF Layout: + Byte Content Encoding + ---- ------------------ ---------------- + [0] Symbol Rate byte 0 Little-endian LSB + [1] Symbol Rate byte 1 + [2] Symbol Rate byte 2 + [3] Symbol Rate byte 3 Little-endian MSB + [4] Frequency byte 0 Little-endian LSB + [5] Frequency byte 1 + [6] Frequency byte 2 + [7] Frequency byte 3 Little-endian MSB + [8] Modulation Type 0--9 (see Section 1.1) + [9] Inner FEC Rate Index into modulation-specific table +``` + +**Symbol Rate** is in samples per second (sps). The Windows driver multiplies ksps by 1000. + +**Frequency** is the IF frequency in kHz (950000--2150000), computed by the host as `(RF_freq - LO_freq) * multiplier`. + +### 7.2 Firmware EP0BUF Parsing + +The firmware reads the 10-byte payload from EP0BUF (XRAM 0xE740--0xE749) and stores: + +| Source | Destination | Notes | +|--------|-------------|-------| +| EP0BUF[8] (modulation) | IRAM 0x4D | Direct copy | +| EP0BUF[9] (FEC) | IRAM 0x4F | Direct copy | +| EP0BUF[4--7] (frequency) | XRAM 0xE0DB--0xE0DE | Byte-reversed (LE to BE) | +| EP0BUF[0--3] (symbol rate) | XRAM 0xE0CB--0xE0CE | Byte-reversed (LE to BE) | + +The byte reversal converts host little-endian to BCM4500 big-endian so values can be written directly to the demodulator via I2C. + +### 7.3 Modulation Dispatch + +After parsing, the firmware validates the modulation type (bounds check `< 10`) and dispatches via a 20-byte jump table (10 entries x 2 bytes) at CODE:0873. Each handler: + +1. Validates the FEC index against the maximum for that modulation +2. Looks up a preconfigured byte from an XRAM FEC rate table +3. Writes configuration to four XRAM registers (0xE0EB, 0xE0EC, 0xE0F5, 0xE0F6) + +**Modulation jump table (from Rev.2 at CODE:0873):** + +| Entry | AJMP Target | Modulation | +|-------|------------|-----------| +| 0 | 0x08B7 | DVB-S QPSK | +| 1 | 0x08DF | Turbo QPSK | +| 2 | 0x08FA | Turbo 8PSK | +| 3 | 0x0915 | Turbo 16QAM | +| 4 | 0x0947 | DCII Combo | +| 5 | 0x094F | DCII I-stream | +| 6 | 0x0957 | DCII Q-stream | +| 7 | 0x095F | DCII Offset QPSK | +| 8 | 0x0887 | DSS QPSK | +| 9 | 0x0887 | DVB BPSK (shares DSS handler) | + +DSS and DVB BPSK share the same handler. Their FEC lookup uses the same table (0xE0F9) but ORs the result with 0x80 to distinguish them from DVB-S QPSK. + +### 7.4 FEC Rate Lookup Tables + +Populated from the CODE-space init table at boot: + +| XRAM Base | Modulation | Max FEC Index | Code Rates | +|-----------|-----------|---------------|------------| +| 0xE0F9 | DVB-S QPSK, DSS, BPSK | 7 | 1/2, 2/3, 3/4, 5/6, 7/8, auto, none | +| 0xE0B7 | Turbo QPSK | 5 | Turbo-specific rates | +| 0xE0B1 | Turbo 8PSK | 5 | Turbo-specific rates | +| 0xE0BC | Turbo 16QAM | 1 | Single code rate | +| 0xE0BD | DCII (all variants) | 9 | Combined code + modulation | + +### 7.5 BCM4500 XRAM Configuration After Dispatch + +| XRAM Addr | Register | DVB-S QPSK | Turbo (Q/8/16) | DCII | DSS/BPSK | +|-----------|----------|-----------|---------------|------|----------| +| 0xE0EB | FEC Code Rate | Table lookup | Table lookup | 0xFC (fixed) | Table lookup OR 0x80 | +| 0xE0EC | Modulation Type | 0x09 | 0x09 | From DCII table | 0x09 | +| 0xE0F5 | Demod Mode | 0x10 | 0x10 | 0x10/0x11/0x12/0x16 | 0x10 | +| 0xE0F6 | Turbo Flag | 0x00 | 0x01 | 0x00 | 0x00 | + +**DCII Demod Mode values:** + +| Modulation | Index | XRAM 0xE0F5 | +|-----------|-------|-------------| +| DCII Combo | 4 | 0x10 | +| DCII I-stream | 5 | 0x12 | +| DCII Q-stream | 6 | 0x16 | +| DCII Offset QPSK | 7 | 0x11 | + +DSS (8) and DVB BPSK (9) share the DVB-S QPSK handler; they use the same FEC table but OR the lookup value with 0x80. + +### 7.6 Complete Tuning Sequence (Host to Satellite) + +``` +=== Phase 1: LNB Configuration (separate vendor commands) === +1. SET_LNB_VOLTAGE (0x8B) -- GPIO P0.4 (no I2C) + H / Circular-L -> wValue=1 (18V) + V / Circular-R -> wValue=0 (13V) +2. SET_22KHZ_TONE (0x8C) -- GPIO P0.3 (no I2C) + High band -> wValue=1 (tone on) + Low band -> wValue=0 (tone off) +3. SEND_DISEQC_COMMAND (0x8D) -- if multi-switch needed + +=== Phase 2: Tune Command === +4. TUNE_8PSK (0x86) -- 10-byte payload + +=== Phase 3: Firmware Internal Processing === +5. EP0BUF parsing: mod/FEC to IRAM, freq/SR byte-reversed to XRAM +6. Modulation dispatch: FEC lookup, XRAM config registers set +7. GPIO P3.6: DVB mode select + +=== Phase 4: BCM4500 I2C Programming (3 outer retries x 3 I2C addresses) === +8. Poll BCM4500 ready: I2C READ regs 0xA2, 0xA8, 0xA4 +9. Write page: I2C WRITE reg 0xA6 <- 0x00 +10. Write config: I2C WRITE reg 0xA7 <- [freq, SR, FEC, mod, demod params] +11. Execute: I2C WRITE reg 0xA8 <- 0x03 (indirect write command) +12. Poll completion: I2C READ regs 0xA8, 0xA2 +13. Verify: I2C READ reg 0xA7 (read-back compare) + +=== Phase 5: Signal Acquisition (host polling) === +14. GET_SIGNAL_LOCK (0x90) -- poll until non-zero +15. GET_SIGNAL_STRENGTH (0x87) -- read SNR +``` + +### 7.7 Signal Lock and Strength + +**GET_SIGNAL_LOCK (0x90)**: Returns 1 byte from BCM4500 register 0xA4. Bit 5 (0x20) indicates signal lock. The kernel driver interprets any non-zero value as locked and reports `FE_HAS_LOCK | FE_HAS_SYNC | FE_HAS_VITERBI | FE_HAS_SIGNAL | FE_HAS_CARRIER`. + +**GET_SIGNAL_STRENGTH (0x87)**: Returns 6 bytes. Bytes 0--1 = 16-bit SNR (LE, dBu * 256). SNR scaling: `snr_raw * 17` maps 0x0000--0x0F00 to 0--65535 (100% at SNR >= 0x0F00). + +--- + +## 8. GPIF Streaming Path + +### 8.1 Data Flow + +``` +BCM4500 Cypress FX2 (CY7C68013A) USB Host +Demodulator P3.5 GPIF Engine EP2 FIFO EP2 (0x82) + (I2C:0x08) <-----> (Master Read) (AUTOIN) ------------> Bulk IN + 8-bit 0xE4xx wfm 4x buffer 7 URBs + parallel 8-bit x 8KB +``` + +The path is fully hardware-managed. The GPIF engine reads data from the BCM4500's 8-bit parallel transport stream output directly into the EP2 FIFO. The AUTOIN bit causes automatic USB commit when the FIFO buffer is full. The FLOWSTATE engine re-triggers GPIF transactions when buffer space becomes available. No firmware intervention occurs in the data path after initial setup. + +### 8.2 Key Register Configuration + +All values are identical across the three stock firmware versions: + +| Register | Address | Value | Function | +|----------|---------|-------|----------| +| IFCONFIG | 0xE601 | 0xEE | Internal 48 MHz clock, GPIF master, async, debug | +| EP2FIFOCFG | 0xE618 | 0x0C | AUTOIN=1, ZEROLENIN=1, 8-bit data path | +| REVCTL | 0xE60B | 0x03 | NOAUTOARM + SKIPCOMMIT | +| CPUCS | 0xE600 | bits [4:3]=10 | 48 MHz CPU clock | +| FLOWSTATEA | 0xE668 | OR 0x09 | FSEN (flow state enable) + FS[3] | +| GPIFIE | 0xE65C | OR 0x3D | Waveform, TC, DONE, FIFO flag, WF2 interrupts | + +**IFCONFIG decode (0xEE = 1110_1110):** + +| Bit | Name | Value | Meaning | +|-----|------|-------|---------| +| 7 | IFCLKSRC | 1 | Internal clock source | +| 6 | 3048MHZ | 1 | 48 MHz IFCLK frequency | +| 5 | IFCLKOE | 1 | IFCLK pin drives output (clock to BCM4500) | +| 4 | IFCLKPOL | 0 | Non-inverted clock polarity | +| 3 | ASYNC | 1 | Asynchronous GPIF (RDY pin handshaking) | +| 2 | GSTATE | 1 | Debug state output on PORTE | +| 1:0 | IFCFG | 10 | GPIF internal master mode | + +### 8.3 ARM_TRANSFER Sequences + +**Start streaming (wValue=1):** + +1. Set config_byte bit 7 (streaming active) +2. Load GPIF transaction count: GPIFTCB3:2 = 0x8000 (effectively infinite) +3. Reset GPIF address and EP2 FIFO byte count +4. Assert P3.5 LOW (BCM4500 transport stream enable) +5. Wait for initial GPIF transaction (poll GPIFTRIG bit 7) +6. De-assert P3.5 HIGH +7. Trigger continuous GPIF read: GPIFTRIG = 0x04 (read into EP2) +8. Set P0.7 LOW (streaming indicator) + +**Stop streaming (wValue=0):** + +1. Set P0.7 HIGH (streaming stopped) +2. Write EP2FIFOBCH = 0xFF (force-flush current buffer) +3. Wait for GPIF idle (poll GPIFTRIG bit 7) +4. Write OUTPKTEND = 0x82 (skip/discard partial EP2 packet) +5. Clear config_byte bit 7 (streaming inactive) +6. Set P3 bits 7:5 = 1 (de-assert all BCM4500 control lines) + +### 8.4 Throughput Analysis + +| Metric | Value | +|--------|-------| +| USB 2.0 HS bulk theoretical | 480 Mbps | +| USB 2.0 HS bulk practical | ~280 Mbps (~35 MB/s) | +| GPIF engine theoretical | 48 MHz x 8 bits = 384 Mbps | +| Typical DVB-S TS rate | 1--5 MB/s | +| Maximum DVB-S2 rate (hypothetical) | ~7.25 MB/s (58 Mbps) | + +The USB/GPIF path has approximately 5x headroom even at maximum theoretical DVB-S2 data rates. The bottleneck for supported modes is the satellite link, not the USB data path. + +### 8.5 FIFO Reset Sequence + +All endpoint FIFOs are reset during initialization using the Cypress-prescribed procedure: + +``` +FIFORESET = 0x80 ; NAKALL: NAK all host transfers during reset +FIFORESET = 0x02 ; Reset EP2 FIFO +FIFORESET = 0x04 ; Reset EP4 FIFO +FIFORESET = 0x06 ; Reset EP6 FIFO +FIFORESET = 0x08 ; Reset EP8 FIFO +FIFORESET = 0x00 ; Release NAKALL +``` + +Three NOP instructions (mandatory SYNCDELAY) are inserted between each write per Cypress TRM requirements. + +### 8.6 EP2 Endpoint Configuration + +```c +EP2CFG = 0xE2; // valid=1, dir=IN, type=BULK, size=512, buf=DOUBLE +``` + +| Bit | Value | Meaning | +|-----|-------|---------| +| 7 (VALID) | 1 | Endpoint enabled | +| 6 (DIR) | 1 | IN (device to host) | +| 5:4 (TYPE) | 10 | Bulk transfer | +| 3 (SIZE) | 0 | 512-byte packets | +| 1:0 (BUF) | 10 | Double-buffered | + +EP4, EP6, EP8 are disabled (`&= ~bmVALID`). + +### 8.7 Interrupt Handling + +INT4 and INT6 (GPIF/FIFO events) share a common handler that sets a software flag (`_0_1`) and clears EXIF.4. The main loop polls this flag, enters CPU idle mode (PCON.0) between events, and checks EP2CS for buffer availability before re-arming the GPIF. + +**Main loop structure (from v2.06 FUN_CODE_2297):** + +```c +void main_loop_poll(void) { + if (_0_1) { // GPIF/FIFO event pending + _0_1 = 0; // Clear flag + if (EP2CS & bmEPFULL) { // EP2 buffer full? + // Wait for host to read EP2 + } + } else { + PCON |= 0x01; // CPU idle until next interrupt + } +} +``` + +### 8.8 Prior IFCONFIG Value + +During early initialization, IFCONFIG is temporarily set to 0xCA before the final 0xEE: + +| Value | Decode | Difference from 0xEE | +|-------|--------|---------------------| +| 0xCA | 1100_1010 | GSTATE=0, ASYNC=0 | +| 0xEE | 1110_1110 | GSTATE=1, ASYNC=1 (final) | + +The temporary value disables async mode and debug state output during FIFO setup. + +--- + +## 9. LNB and DiSEqC Control + +### 9.1 LNB Voltage + +LNB voltage is controlled via GPIO P0.4. No I2C is involved. + +| wValue | Voltage | GPIO P0.4 | Polarization | +|--------|---------|-----------|-------------| +| 0 | 13V | LOW | Vertical / Circular-Right | +| 1 | 18V | HIGH | Horizontal / Circular-Left | + +USE_EXTRA_VOLT (0x94) enables a +1V boost (13V->14V, 18V->19V) for long cable runs by writing to XRAM 0xE0B6 (0x62=normal, 0x6A=boosted; bit 3 is the difference). + +### 9.2 22 kHz Tone + +Controlled via GPIO P0.3. P0.3 gates an external 22 kHz oscillator on the PCB. The firmware does not generate the 22 kHz carrier directly. + +| wValue | State | GPIO P0.3 | Band | +|--------|-------|-----------|------| +| 0 | OFF | LOW | Low band (9.75 GHz LO on universal LNB) | +| 1 | ON | HIGH | High band (10.6 GHz LO on universal LNB) | + +### 9.3 DiSEqC Protocol Implementation + +All firmware versions implement DiSEqC via Timer2-based GPIO bit-bang. The algorithm is identical across versions; only the data pin differs per PCB revision. + +**Timer2 configuration (identical across all versions):** + +| Parameter | Value | +|-----------|-------| +| T2CON | 0x04 (auto-reload, running) | +| RCAP2H:RCAP2L | 0xF82F (reload = 63535) | +| CKCON.T2M | 0 (Timer2 clock = 48 MHz / 12 = 4 MHz) | +| Tick period | (65536 - 63535) / 4 MHz = 500.25 us | + +**DiSEqC timing parameters:** + +| Parameter | Value | +|-----------|-------| +| Bit period | 1.5 ms (3 Timer2 ticks) | +| Byte period | 13.5 ms (9 bits: 8 data + 1 parity) | +| Tone burst A/B | 12.5 ms (25 ticks) | +| Pre-TX settling delay | 7.5 ms (15 ticks) | +| Data '0' | 1.0 ms tone + 0.5 ms silence (2/3 duty cycle) | +| Data '1' | 0.5 ms tone + 1.0 ms silence (1/3 duty cycle) | +| Carrier frequency | 22 kHz (external oscillator, gated by P0.3) | + +**Manchester encoding (decompiled from Rev.2 FUN_CODE_213c):** + +``` +Each DiSEqC bit = 3 Timer2 ticks: + Tick 1: inter-bit gap (carrier OFF via P0.3 = 0) + Tick 2: carrier ON (P0.3 = 1) + Tick 3: if data_pin='1', carrier OFF early; if '0', carrier stays ON + End: carrier always OFF +``` + +**DiSEqC bit waveforms:** + +``` +Data '0' (2/3 tone, 1/3 silence): + Tick 1 Tick 2 Tick 3 + (500 us) (500 us) (500 us) +P0.3: _____|========|========|________| + ^tone ON ^tone OFF + (setup gap) (1.0 ms carrier) (0.5 ms silence) + +Data '1' (1/3 tone, 2/3 silence): + Tick 1 Tick 2 Tick 3 + (500 us) (500 us) (500 us) +P0.3: _____|========|________|________| + ^tone ON ^tone OFF early + (setup gap) (0.5 ms carrier) (1.0 ms silence) +``` + +**Decompiled bit symbol function (from Rev.2 FUN_CODE_213c):** + +```c +void diseqc_bit_symbol(void) { + wait_TF2(); // Tick 1: inter-bit gap (500 us) + P0 |= 0x08; // P0.3 = 1 -> 22 kHz carrier ON + wait_TF2(); // Tick 2: carrier period (500 us) + if (data_pin != 0) { // If data = '1': + P0 &= 0xF7; // P0.3 = 0 -> carrier OFF (short pulse) + } + wait_TF2(); // Tick 3: final period (500 us) + P0 &= 0xF7; // P0.3 = 0 -> carrier always OFF at end +} +``` + +**Decompiled byte transmission (from Rev.2 FUN_CODE_07d1):** + +```c +void diseqc_send_byte(char first_byte, byte data) { + byte ones_count = 0; + if (first_byte == 0) TF2 = 0; // Sync timer on first byte + + for (char i = 8; i > 0; i--) { // 8 bits, MSB first + if (data & 0x80) { + data_pin = 1; // Set data = '1' + diseqc_bit_symbol(); + ones_count++; + } else { + data_pin = 0; // Set data = '0' + diseqc_bit_symbol(); + } + data <<= 1; // Next bit + } + data_pin = ~ones_count & 1; // Odd parity + diseqc_bit_symbol(); // Transmit parity bit +} +``` + +**Timing per byte**: 9 bits x 1.5 ms = 13.5 ms + +**Tone burst (mini DiSEqC)**: 25 consecutive Timer2 ticks of carrier (12.5 ms). Tone burst A: `wValue==0` and `wLength==0`. Tone burst B: `wValue!=0` and `wLength==0`. + +**Timer tick wait (TF2 polling, identical across all versions):** + +```c +void wait_TF2(void) { + while (TF2 == 0) {} // Poll Timer2 overflow flag + TF2 = 0; // Clear flag for next tick +} +``` + +### 9.4 DiSEqC Signal Architecture + +``` +FX2 Firmware External Hardware Coax Cable ++------------------+ +--------------------+ +------------------+ +| P0.3 (carrier) |---->| 22 kHz oscillator |---->| LNB power line | +| (enable/disable) | | (gated by P0.3) | | (13V/18V + tone) | +| | | | | | +| P0.x (data bit) | | (internal firmware | | | +| (firmware only) | | logic only) | | | ++------------------+ +--------------------+ +------------------+ +``` + +The data pin (P0.7 / P0.4 / P0.0 depending on firmware version) is used only internally by the firmware's Manchester encoding logic. It controls whether the carrier gate signal is cut short or held for the full bit period. + +### 9.5 Windows BDA Driver DiSEqC Interface + +The Windows driver exposes DiSEqC through a BDA extended property: + +```c +// GUID: {0B5221EB-F4C4-4976-B959-EF74427464D9} +typedef struct __DISEQC_COMMAND { + UCHAR ucMessage[6]; // Framing, Address, Command, Data[0..2] + UCHAR ucMessageLength; // 3-6 for DiSEqC; 1 for tone burst +} DISEQC_COMMAND; +``` + +For tone burst: `ucMessageLength=1`, `ucMessage[0]=SEC_MINI_A` (0x00) or `SEC_MINI_B` (0x01). + +### 9.6 SET_DN_SWITCH (0x8F) -- Legacy Dish Network + +A 7-bit serial command bit-banged on GPIO P0.4: + +1. Assert P0.4 HIGH (start pulse), delay ~32 cycles +2. De-assert P0.4, delay ~8 cycles +3. Shift out 7 bits LSB-first via P0.4, ~8 cycle delays between bits + +The kernel calls this via `dishnetwork_send_legacy_command`. Bit 7 (0x80) of the original switch command selects LNB voltage and is sent separately via SET_LNB_VOLTAGE. + +--- + +## 10. GPIO Pin Map + +### 10.1 Port 0 / Port A (SFR 0x80, IOA) + +| Pin | v2.06 | Rev.2 v2.10 | v2.13 | Custom v3.01.0 | +|-----|-------|-------------|-------|----------------| +| P0.0 | -- | LNB control (0x97) | DiSEqC data | -- | +| P0.1 | Power enable | Power enable | Power enable | Power enable | +| P0.2 | Power disable | Power disable (init=0x84) | Power disable | Power disable | +| P0.3 | **22 kHz tone** | **22 kHz tone** | **22 kHz tone** | **22 kHz tone** | +| P0.4 | **LNB 13V/18V** | **LNB 13V/18V** + DiSEqC data | **LNB 13V/18V** | **LNB 13V/18V** | +| P0.5 | **BCM4500 RESET** | GPIO status input (0x98) | **BCM4500 RESET** | **BCM4500 RESET** | +| P0.6 | -- | GPIO control (0x97) | -- | -- | +| P0.7 | **DiSEqC data** | Streaming indicator | Streaming indicator | **DiSEqC data** + streaming | + +### 10.2 Port 3 / Port D (SFR 0xB0, IOD) + +| Pin | Function | Notes | +|-----|----------|-------| +| P3.0 | Init HIGH | | +| P3.4 | GPIO control | Used by Rev.2 FUN_CODE_1fcf | +| P3.5 | **TS_EN** | Transport stream enable: LOW=active, HIGH=idle | +| P3.6 | **DVB mode** | BCM4500 mode select; DiSEqC direction (Rev.2) | +| P3.7 | BCM4500 control | De-asserted (HIGH) when streaming stops | + +### 10.3 Port B (XRAM-mapped IOB) + +Used by internal debug commands 0x96--0x98: + +| Pin | v2.06/v2.13 | Rev.2 | +|-----|-------------|-------| +| IOB.0 | GPIO status input (0x98) | -- | +| IOB.1 | LNB control (0x97) | -- | +| IOB.2 | LNB control (0x97) | -- | +| IOB.3 | LNB GPIO mode (0x96) | -- | +| IOB.4 | -- | LNB GPIO mode (0x96) + control (0x97) | + +### 10.4 DiSEqC Data Pin Summary + +| Firmware Version | Data Pin | Carrier Pin | +|-----------------|----------|-------------| +| v2.06 | P0.7 | P0.3 | +| Rev.2 v2.10 | P0.4 | P0.3 | +| v2.13 | P0.0 | P0.3 | +| Custom v3.01.0 | P0.7 | P0.3 | + +The carrier pin (P0.3) is the same across all versions. + +### 10.5 Initial GPIO State + +| Register | Value | Decode | +|----------|-------|--------| +| IOA (P0) | 0x84 | P0.7=1 (idle), P0.2=1 (power disable active) | +| IOD (P3) | 0xE1 | P3.7:5=1 (controls idle), P3.0=1 | +| OEA | 0xBE | P0.1-5,7 as outputs | + +--- + +## 11. Firmware Versions + +### 11.1 Version Table + +| Firmware | Version ID | Build Date | PID | Functions | Binary Size | SP | +|----------|-----------|------------|-----|-----------|-------------|-----| +| v2.06.04 | 0x020604 | 2007-07-13 | 0x0203 | 61 | 9,472 bytes | 0x72 | +| Rev.2 v2.10.04 | 0x020A04 | 2010-03-12 | 0x0202 | 107 | 8,843 bytes | 0x4F | +| v2.13.01 (FW1) | 0x020D01 | 2010-03-12 | 0x0203 | 82-88 | 9,322 bytes | 0x50 | +| v2.13.02 (FW2) | 0x020D01 | 2010-03-12 | 0x0203 | 83 | 9,377 bytes | 0x50 | +| v2.13.03 (FW3) | 0x020D01 | 2010-03-12 | 0x0203 | 83 | 9,369 bytes | 0x52 | +| Custom v3.01.0 | 0x030100 | 2026-02-12 | 0x0203 | N/A | ~3 KB (RAM) | N/A | + +Rev.2 v2.10 targets PID 0x0202 (different product). The v2.13 sub-variants target different SkyWalker-1 hardware sub-revisions. Custom v3.01.0 is compiled with SDCC + fx2lib and loaded into FX2 RAM (not flashed to EEPROM). + +### 11.2 Kernel Version Constants + +From `gp8psk-fe.h`: + +``` +GP8PSK_FW_REV1 = 0x020604 (v2.06.4) +GP8PSK_FW_REV2 = 0x020704 (v2.07.4) +``` + +If `fw_vers >= GP8PSK_FW_REV2`, the kernel enables Rev.2-specific code paths. The v2.10 and v2.13 firmwares are newer than either kernel constant. + +### 11.3 Key Architectural Differences + +| Feature | v2.06 | Rev.2 v2.10 | v2.13 | +|---------|-------|-------------|-------| +| Vendor commands | 30 (0x80--0x9D) | 27 (0x80--0x9A) | 30 (0x80--0x9D) | +| INT0 handler | USB re-enumeration | USB re-enumeration | Demod availability polling | +| Demod probe at boot | No | No | Yes (40 attempts at 0x7F + 0x3F) | +| Retry loops | No | No | Yes (20-attempt with checksum verify) | +| HW revision detection | No | Yes (descriptor walker) | Yes (flag `_1_3`) | +| DiSEqC data pin | P0.7 | P0.4 | P0.0 | +| Config byte IRAM | 0x6D | 0x4E | 0x4F | +| Descriptor base | 0x1200 | 0x0E00 | 0x0E00 | +| Init table address | CODE:0B46 | CODE:0B48 | CODE:0B88 | +| BCM4500 status poll | 3 registers | 3 registers | 1 register (consolidated) | +| Anti-tampering string | No | No | Yes (at firmware offset 0x1880) | +| New commands | -- | 0x99/0x9A proto | 0x99, 0x9A, 0x9C | +| 0x9D behavior | HW revision mode | N/A (out of range) | Conditional demod reset | + +### 11.4 v2.13 Sub-Variant Differences + +The three v2.13 sub-variants target fundamentally different hardware interfaces: + +| Aspect | FW1 (v2.13.1) | FW2 (v2.13.2) | FW3 (v2.13.3) | +|--------|---------------|---------------|---------------| +| Demod interface | I2C bus | Parallel bus (P0/P1) | Parallel bus (enhanced) | +| Bus protocol | I2C START/STOP/ACK | Single-phase P1 read | Dual-phase P1 read + OR accumulate | +| Stack pointer | 0x50 | 0x50 | 0x52 | +| P0 init | 0xa4 | 0xa4 | 0xa0 | +| Status register | INTMEM 0x4F | INTMEM 0x4F | INTMEM 0x51 | +| Config source | Hardcoded | External (0xE080-0xE08E) | External (0xE080-0xE08E) | +| Binary distance from FW1 | -- | 3,993 bytes | 3,789 bytes | +| Binary distance from FW2 | 3,993 bytes | -- | 1,525 bytes | + +FW1 uses standard I2C master-mode transactions. FW2/FW3 use a parallel data bus with P0 for control signals (chip select, read strobe) and P1 for 8-bit data. FW3 adds dual-phase reading with OR-accumulation, likely for a demodulator chip with different bus timing. The updater program selects the correct sub-variant based on hardware detection. + +### 11.5 Binary Comparison Matrix + +Byte-level similarity (percentage of matching bytes within shared length): + +| | v2.06 | v2.13.1 | v2.13.2 | v2.13.3 | Rev.2 | +|---|---|---|---|---|---| +| **v2.06** | -- | 4.8% | 4.3% | 4.3% | 6.0% | +| **v2.13.1** | | -- | 57.2% | 59.4% | 8.0% | +| **v2.13.2** | | | -- | 83.5% | 5.8% | +| **v2.13.3** | | | | -- | 5.8% | +| **Rev.2** | | | | | -- | + +The very low similarity between major versions (4--8%) indicates complete recompilation with different linker configurations. Functions relocate even when logic is identical. + +### 11.6 Anti-Tampering (v2.13 Only) + +At firmware offset 0x1880, all v2.13 sub-variants contain: + +``` +"Tampering is detected. Attempt is logged. Warranty is voided ! \n" +``` + +Followed by I2C register write commands (`01 10 aa 82 02 41 41 83`). This string and mechanism are absent from v2.06 and Rev.2. + +### 11.7 Rev.2 as Transitional Firmware + +Rev.2 v2.10.4 sits architecturally between v2.06 and v2.13: +- Adopted v2.13's descriptor base (0x0E00) and similar stack pointer +- Retained v2.06's INT0 USB re-enumeration behavior +- Has the most functions (107) but smallest binary (~8.8 KB) due to granular decomposition +- Lacks v2.13's demodulator polling, retry loops, and additional vendor commands + +### 11.8 Key Function Correspondence Across Versions + +| v2.06 Function | Rev.2 Function | v2.13 Function | Role | +|---------------|---------------|---------------|------| +| `main` (0x188D) | `main` (0x155F) | `main_entry` (0x170D) | RESET vector: clear IRAM, process init table | +| `FUN_CODE_09a7` | `FUN_CODE_09a9` | `FUN_CODE_0800` | Main init + main loop | +| `FUN_CODE_13c3` | `FUN_CODE_10d9` | `FUN_CODE_11ab` | USB/peripheral descriptor setup | +| `FUN_CODE_032a` | `FUN_CODE_0319` | `FUN_CODE_034e` | Standard USB request handler | +| `FUN_CODE_0056` | `vendor_cmd_dispatch` | `FUN_CODE_0056` | Vendor request dispatcher (identical code) | +| `FUN_CODE_2297` | -- | `FUN_CODE_21ec` | Main loop poll (USB IRQ processing) | +| `FUN_CODE_1919` | `FUN_CODE_0d7c` | `FUN_CODE_1800` | GPIF/FIFO management | +| `FUN_CODE_1d4f` | -- | -- | v2.06 demod init (GPIO-based) | +| -- | -- | `FUN_CODE_1d4b` | v2.13 demod init (I2C write to 0x7F/0xF0) | +| `FUN_CODE_0ddd` | `FUN_CODE_0c64` | `FUN_CODE_0ca4` | BCM4500 firmware loader | +| `FUN_CODE_2000` | -- | `FUN_CODE_208d` | BCM4500 status polling | +| `FUN_CODE_1dfb` | `FUN_CODE_1bda` | `FUN_CODE_14b9` | Delay loop (clock-speed-aware) | +| `INT0_vec` (0x0003) | `INT0_ISR` (0x0003) | `INT0_vector` (0x0003) | INT0 handler (different purpose) | +| -- | -- | `FUN_CODE_2239` | v2.13 I2C single-byte read helper | +| -- | -- | `FUN_CODE_2031` | v2.13 USB reconnect function | +| -- | -- | `FUN_CODE_1799` | v2.13 demod signature verification | +| -- | -- | `FUN_CODE_1ac6` | v2.13 tuning acquisition sequence | + +### 11.9 INT0 Handler Evolution + +The INT0 interrupt vector (CODE:0003) was repurposed between firmware versions: + +**v2.06 and Rev.2 -- USB Re-enumeration:** + +``` +void INT0_vec(void) { + if (flag == 0) CPUCS |= 0x08; // CPUCS bit 3 + else CPUCS |= 0x0A; // CPUCS bits 3+1 + delay(5, 0xDC); // ~1500 cycles + EPIRQ = 0xFF; // Clear endpoint IRQs + USBIRQ = 0xFF; // Clear USB IRQs + EXIF &= 0xEF; // Clear external interrupt flag + CPUCS &= 0xF7; // Clear CPUCS bit 3 +} +``` + +Pulses CPUCS.3 to trigger a controlled USB re-enumeration, then clears all pending interrupts. + +**v2.13 -- Demodulator Availability Polling:** + +``` +void INT0_vector(void) { + for (counter = 0x28; counter != 0; counter--) { // 40 attempts + byte result = I2C_read(0x7F); // Demod address A + if (result != 0x01) { + result = I2C_read(0x3F); // Demod address B + if (result != 0x01) break; + } + } + no_demod_flag = (counter == 0); // Set if loop exhausted +} +``` + +Polls two I2C addresses (0x7F, 0x3F) to detect which demodulator variant is present. The `no_demod_flag` prevents tuning attempts on boards with absent or failed demodulators. + +In v2.13, the USB re-enumeration code was moved to `FUN_CODE_2031` and called as a normal function before the main loop starts, freeing INT0 for demodulator polling. + +### 11.10 v2.13 Integrity Verification + +v2.13 performs two integrity checks during initialization, absent from v2.06 and Rev.2: + +**Demodulator Signature Verification (FUN_CODE_1799):** + +1. Writes 4 bytes to I2C device 0x7F, register 0xF0 +2. Reads 5 bytes from register 0x0A (stepping by 2), each character +3. Subtracts 0x30 ('0') from each byte (ASCII to binary) +4. Sums values and compares against expected parameter (0x021C) +5. Up to 20 retry attempts + +**Descriptor Checksum Verification (FUN_CODE_1ca0):** + +1. Iterates bytes 6 through 0x29 (36 bytes) of a descriptor block +2. Computes running sum, compares against 0x0706 +3. Iterates bytes 0x2C through 0x4F (36 bytes) of same block +4. Computes second sum, compares against 0x0686 +5. Up to 20 retry attempts + +Both checks call `FUN_CODE_1ac6(100)` (tuning acquisition with 100 ms delay) as a recovery action if verification fails after all attempts. + +### 11.11 XRAM Initialization Table + +All firmware versions initialize XRAM peripheral registers from a table stored in CODE space. The table is processed at startup before entering the main loop. + +**Table format (all versions):** + +``` +Each entry: [addr_hi] [addr_lo] [data_byte] +Terminator: [0x00] [0x00] (address 0x0000) +``` + +The parser reads 3 bytes at a time: a 16-bit XRAM address (big-endian) and a data byte. It writes the byte to the address until it encounters address 0x0000. + +**Key XRAM registers initialized from the table:** + +| XRAM Address | Register | Typical Value | Purpose | +|-------------|----------|---------------|---------| +| 0xE604 | FIFORESET | 0x80 | Start FIFO reset sequence | +| 0xE601 | IFCONFIG | 0xCA | Initial interface config (overwritten later) | +| 0xE610 | EP2CFG | 0xE2 | EP2 bulk IN, 512-byte, double-buffered | +| 0xE612 | EP4CFG | 0x00 | EP4 disabled | +| 0xE618 | EP2FIFOCFG | 0x0C | AUTOIN, ZEROLENIN, 8-bit | +| 0xE620 | REVCTL | 0x03 | NOAUTOARM + SKIPCOMMIT | +| 0xE67A | I2CTL | 0x01 | I2C 400 kHz | +| 0xE68A | EP0BCH | 0x00 | EP0 byte count high = 0 | + +**Init table addresses by version:** + +| Firmware | Table Address | +|----------|--------------| +| v2.06 | CODE:0B46 | +| Rev.2 | CODE:0B48 | +| v2.13 | CODE:0B88 | + +### 11.12 Main Loop Architecture + +All firmware versions use the same main loop structure: poll the SUDAV (setup data available) interrupt flag, process vendor commands, then idle the CPU until the next interrupt. + +**v2.06 (simplified decompilation):** + +```c +void main_loop(void) { // FUN_CODE_09a7 + // 1. Process init table from CODE:0B46 + // 2. Call FUN_CODE_13c3 (USB/peripheral setup) + // 3. EA = 1 (global interrupts enable) + + while (1) { + if (sudav_flag) { + handle_setupdata(); // Process USB SETUP packet + sudav_flag = 0; + } + if (gpif_flag) { + handle_gpif_event(); + gpif_flag = 0; + } else { + PCON |= 0x01; // CPU idle until next interrupt + } + } +} +``` + +The SUDAV ISR simply sets `sudav_flag = 1` and clears the interrupt. All actual USB processing happens in the main loop context. + +--- + +## 12. I2C Bus Architecture + +### 12.1 FX2 I2C Controller + +The FX2's I2C master controller is a hardware peripheral accessed through SFRs: + +| SFR | Address | Function | +|-----|---------|----------| +| I2CS | 0xE678 (XRAM) | I2C control/status register | +| I2DAT | 0xE679 (XRAM) | I2C data register | +| I2CTL | 0xE67A (XRAM) | I2C control (speed selection) | + +Key I2CS bits: bmSTART (initiate START), bmSTOP (initiate STOP), bmLASTRD (signal last read byte), bmDONE (transaction byte complete), bmACK (ACK received), bmBERR (bus error). + +### 12.2 Bus Speed + +The I2C bus speed is 400 kHz, set via: +- C2 EEPROM header config byte = 0x40 (at boot) +- I2CTL = bm400KHZ (in custom firmware) + +### 12.3 Known Bus Devices + +| 7-bit Address | Wire Write/Read | Identity | +|---------------|-----------------|----------| +| 0x08 | 0x10 / 0x11 | BCM4500 demodulator | +| 0x10 | 0x20 / 0x21 | Tuner or LNB controller | +| 0x51 | 0xA2 / 0xA3 | Configuration EEPROM (24Cxx-family) | + +The EEPROM at 0x51 stores: device serial number (read by GET_SERIAL_NUMBER 0x93), hardware platform ID (read by GET_FPGA_VERS 0x95), and calibration data. + +### 12.4 Combined Write-Read (Repeated START) Protocol + +All BCM4500 register reads use the I2C combined write-read protocol with a repeated START condition. This is required because the BCM4500 uses a register-addressed protocol where the register number must be sent as a write phase before the read phase: + +``` +Complete I2C transaction for reading register 0xA2 from device 0x08: + + Phase 1 (Write): + [S] [0x10] [ACK] [0xA2] [ACK] + | | | | | + | | | | +-- BCM4500 ACKs register address + | | | +--------- Register address + | | +---------------- BCM4500 ACKs its address + | +----------------------- Device address (0x08 << 1) = 0x10 (write) + +---------------------------- START condition + + Phase 2 (Read with Repeated START): + [Sr] [0x11] [ACK] [DATA] [NACK] [P] + | | | | | | + | | | | | +-- STOP condition + | | | | +--------- Master NACKs (last byte) + | | | +---------------- Register data + | | +----------------------- BCM4500 ACKs its address + | +------------------------------ Device address (0x08 << 1 | 1) = 0x11 (read) + +------------------------------------ REPEATED START (no STOP between phases) +``` + +The repeated START (Sr) is essential. A STOP between phases would release the bus, and the BCM4500 would lose the register address context. + +**FX2 I2C SFR sequence for combined read (from custom firmware):** + +```c +I2CS |= bmSTART; // Generate START +I2DAT = 0x10; // Write: device addr + W +// wait bmDONE, check bmACK +I2DAT = 0xA2; // Write: register address +// wait bmDONE, check bmACK +I2CS |= bmSTART; // Generate REPEATED START (no STOP first!) +I2DAT = 0x11; // Write: device addr + R +// wait bmDONE, check bmACK +I2CS |= bmLASTRD; // Signal this is the last read byte +tmp = I2DAT; // Dummy read (triggers first clock burst) +// wait bmDONE +I2CS |= bmSTOP; // Generate STOP after reading +data = I2DAT; // Read actual data byte +// wait bmSTOP to clear +``` + +### 12.5 I2C STOP Corruption Bug + +Sending `I2CS |= bmSTOP` when no I2C transaction is active (no prior START issued, bus idle) corrupts the FX2 I2C controller's internal state machine. The bmSTOP bit may not self-clear, and subsequent START conditions fail to detect ACK from slaves. + +This was the root cause of the firmware hang in custom v3.01.0 during boot. The stock firmware's "bus reset" step: + +```c +/* BROKEN: */ +I2CS |= bmSTOP; +i2c_wait_stop(); +``` + +was removed. The correct approach is to simply proceed with a new START condition. If the bus is idle (after power-on or after the previous transaction completed normally), the START succeeds and the controller enters its normal operating state. The Cypress TRM does not document STOP as a standalone bus-reset mechanism. + +### 12.6 Timeout Protection + +The fx2lib I2C functions poll `bmDONE` with no timeout: + +```c +while (!(I2CS & bmDONE) && !cancel_i2c_trans); +``` + +Since `cancel_i2c_trans` is never set during normal operation, these loops are effectively infinite. The custom firmware replaces all fx2lib I2C functions with timeout-protected wrappers: + +```c +#define I2C_TIMEOUT 6000 + +static BOOL i2c_wait_done(void) { + WORD timeout = I2C_TIMEOUT; + while (!(I2CS & bmDONE)) { + if (--timeout == 0) return FALSE; + } + return TRUE; +} +``` + +A WORD counter of 6000 decremented in a tight SDCC-compiled loop at 48 MHz gives approximately 5--10 ms per wait. At 400 kHz I2C, a single byte transfer takes 22.5 us, so the timeout provides over 200x margin for normal operations. + +--- + +## 13. Custom Firmware v3.01.0 + +### 13.1 Overview + +Custom replacement firmware built with SDCC and fx2lib. Loaded into FX2 RAM for testing via `fw_load.py` (not flashed to EEPROM). + +| Property | Value | +|----------|-------| +| Toolchain | SDCC + fx2lib | +| Source | `firmware/skywalker1.c` (1351 lines) | +| Version ID | 0x030100 | +| Build date | 2026-02-12 | +| Load method | RAM upload via `tools/fw_load.py` | + +### 13.2 Stock-Compatible Commands + +The custom firmware implements all commands needed for the kernel driver: GET_8PSK_CONFIG (0x80), ARM_TRANSFER (0x85), TUNE_8PSK (0x86), GET_SIGNAL_STRENGTH (0x87), BOOT_8PSK (0x89), START_INTERSIL (0x8A), SET_LNB_VOLTAGE (0x8B), SET_22KHZ_TONE (0x8C), SEND_DISEQC (0x8D), GET_SIGNAL_LOCK (0x90), GET_FW_VERS (0x92), USE_EXTRA_VOLT (0x94). + +### 13.3 Custom Commands + +| Command | Function | +|---------|----------| +| SPECTRUM_SWEEP (0xB0) | Step through frequency range reading signal energy | +| RAW_DEMOD_READ (0xB1) | Read any BCM4500 indirect register | +| RAW_DEMOD_WRITE (0xB2) | Write any BCM4500 indirect register | +| BLIND_SCAN (0xB3) | Try symbol rates at a frequency looking for lock | +| I2C_BUS_SCAN (0xB4) | Probe all 7-bit I2C addresses | +| I2C_RAW_READ (0xB5) | Read from any I2C device address | +| I2C_DIAG (0xB6) | Step-by-step indirect register read diagnostic | + +### 13.4 Debug Boot Modes + +The BOOT_8PSK (0x89) command supports incremental debug modes via wValue: + +| wValue | Action | Result | +|--------|--------|--------| +| 0x80 | No-op: return `config_status` and `boot_stage` | Works | +| 0x81 | GPIO + power + delays only (no I2C) | Works | +| 0x82 | GPIO + power + I2C probe (bmSTOP removed) | Works | +| 0x83 | GPIO + power + probe + init block 0 | Works | +| 0x84 | I2C-only probe (chip already powered) | Works | +| 0x85 | Same as 0x82 without bmSTOP | Works | +| 0x01 | Full boot (production) | Works | +| 0x00 | Shutdown | Works | + +These modes were used to isolate the I2C STOP corruption bug (see [Section 12.4](#124-i2c-stop-corruption-bug)). + +### 13.5 Key Implementation Patterns + +**I2C Combined Read** (repeated START): + +```c +static BOOL i2c_combined_read(BYTE addr, BYTE reg, BYTE len, BYTE *buf) { + I2CS |= bmSTART; + I2DAT = addr << 1; // START + write address + // ... wait for DONE, check ACK ... + I2DAT = reg; // Register address + // ... wait for DONE, check ACK ... + I2CS |= bmSTART; + I2DAT = (addr << 1) | 1; // REPEATED START + read address + // ... read len bytes with LASTRD/STOP on final byte ... +} +``` + +**BCM4500 Init Block Write**: + +```c +static BOOL bcm_write_init_block(const __code BYTE *data, BYTE len) { + bcm_direct_write(BCM_REG_PAGE, 0x00); // Page select + i2c_write_multi_timeout(BCM4500_ADDR, BCM_REG_DATA, len, data); // Data + bcm_direct_write(BCM_REG_DATA, 0x00); // Trailing zero + bcm_direct_write(BCM_REG_CMD, BCM_CMD_WRITE); // Commit (0x03) + return bcm_poll_ready(); // Wait for completion +} +``` + +--- + +## 14. DVB-S2 Incompatibility + +### 14.1 Definitive Conclusion + +The SkyWalker-1's inability to receive DVB-S2 is a fundamental hardware limitation of the BCM4500 demodulator silicon. The BCM4500 was designed before the DVB-S2 standard was ratified (March 2005) and contains no LDPC or BCH decoder hardware. No firmware update can add DVB-S2 support. + +### 14.2 FEC Architecture Comparison + +| Feature | BCM4500 (SkyWalker-1) | DVB-S2 Requirement | +|---------|----------------------|-------------------| +| Inner FEC | Viterbi (DVB-S) or Turbo (proprietary) | LDPC | +| Outer FEC | Reed-Solomon (t=10) | BCH | +| Block size | Convolutional / short turbo blocks | 64,800 or 16,200 bits | +| Decoder type | Trellis (Viterbi) or iterative turbo | Iterative belief propagation | +| Hardware IP | Hardwired Viterbi + turbo silicon | Requires dedicated LDPC engine | + +### 14.3 Evidence + +**From firmware analysis:** + +1. The firmware modulation dispatch table has exactly 10 entries (0--9), with no DVB-S2-specific modes. The bounds check at CODE:0866 rejects values >= 10. +2. No LDPC/BCH code rate values exist in any FEC lookup table. The XRAM tables at 0xE0B1, 0xE0B7, 0xE0BC, 0xE0BD, and 0xE0F9 contain only Viterbi rates (1/2 through 7/8), turbo rates, and DCII combined codes. +3. No DVB-S2-specific register addresses appear in any I2C traffic. The BCM4500 is programmed exclusively through indirect registers 0xA6/0xA7/0xA8 with page 0x00. + +**From Windows BDA driver source:** + +4. `SkyWalker1TunerFilter.cpp` (line 1070): `else if(ulNewInnerFecType == BDA_FEC_VITERBI)` -- only Viterbi FEC is accepted; any other type returns `STATUS_INVALID_PARAMETER`. +5. `SkyWalker1Control.cpp` (line 292): `ucCommand[8] = ADV_MOD_DVB_QPSK;` -- the driver hardcodes modulation type 0 (DVB-S QPSK) regardless of application request. +6. `SkyWalker1Control.h` (lines 64--74): modulation constants cap at `ADV_MOD_DVB_BPSK` (9). No value 10+ exists. + +**From datasheets:** + +7. The BCM4500 datasheet describes exactly two FEC paths: "an advanced modulation turbo decoder" and "a DVB/DIRECTV/DCII-compliant FEC decoder." No third path for LDPC/BCH. +8. BCM4500 specification: 128-pin MQFP, 3.3V I/O, 1.8V digital, symbol rate 256 Ksps to 30 Msps. No mention of LDPC, BCH, or DVB-S2. + +### 14.4 Broadcom DVB-S2 Chip Timeline + +| Chip | Year | DVB-S2? | Notes | +|------|------|---------|-------| +| BCM4500 | ~2003 | No | Turbo FEC + legacy Viterbi/RS | +| BCM4501 | 2006 | Yes | First dual-tuner DVB-S2; LDPC/BCH | +| BCM4505 | 2007 | Yes | Single-channel, 65nm | +| BCM4506 | 2007 | Yes | Dual-channel, 65nm | + +Broadcom restricted BCM4501/4505/4506 sales to set-top box manufacturers, preventing Genpix from using them. + +### 14.5 What Genpix Did + +Released the SkyWalker-3, replacing the BCM4500 with a different demodulator (likely STMicroelectronics STV0903). The trade-off: gained DVB-S2 LDPC/BCH support, lost proprietary turbo-FEC support (turbo codes are Broadcom/EchoStar proprietary). + +### 14.6 USB Data Path is Not the Bottleneck + +The GPIF/USB 2.0 path has approximately 5x headroom for DVB-S2 rates (~58 Mbps max vs ~280 Mbps USB practical throughput). The 8-bit transport stream interface uses the same MPEG-TS format (188-byte packets). The bottleneck is the demodulator silicon. + +--- + +## 15. Kernel Driver Notes + +### 15.1 Module Names + +- `dvb_usb_gp8psk` -- USB transport and device management +- `gp8psk_fe` -- DVB frontend (demodulation, tuning) + +### 15.2 Kernel Driver Race Condition + +The kernel module auto-loads via udev when VID:PID `09C0:0203` appears on the USB bus (every FX2 re-enumeration after firmware load). The driver races with test tools and sends its own BOOT_8PSK command. + +Symptoms: +- "resource busy" or "entity not found" errors from test scripts +- BCM4500 enters unexpected state from partial kernel initialization +- Kernel driver detaches mid-test + +**Fix**: Blacklist the module: + +``` +# /etc/modprobe.d/blacklist-gp8psk.conf +blacklist dvb_usb_gp8psk +blacklist gp8psk_fe +``` + +Then unload: `sudo modprobe -r dvb_usb_gp8psk gp8psk_fe` + +### 15.3 FPGA Version Failure + +``` +gp8psk: usb in 149 operation failed. +gp8psk: failed to get FPGA version +``` + +Command 0x95 (GET_FPGA_VERS, decimal 149) fails on some SkyWalker-1 units. The driver logs the failure but continues normally. + +### 15.4 Commands Used by Kernel Driver + +| Command | Usage | Notes | +|---------|-------|-------| +| 0x80 GET_8PSK_CONFIG | Boot check | Always | +| 0x83 I2C_WRITE | BCM4500 reg writes | Via frontend ops | +| 0x84 I2C_READ | BCM4500 reg reads | Via frontend ops | +| 0x85 ARM_TRANSFER | Stream start/stop | Always | +| 0x86 TUNE_8PSK | Frequency tuning | Via frontend ops | +| 0x87 GET_SIGNAL_STRENGTH | SNR readback | Via frontend ops | +| 0x88 LOAD_BCM4500 | BCM4500 FW load | Rev.1 Warm only (STALLs on SW-1) | +| 0x89 BOOT_8PSK | Power on/off | Always | +| 0x8A START_INTERSIL | LNB power | Always | +| 0x8B SET_LNB_VOLTAGE | 13V/18V | Via frontend ops | +| 0x8C SET_22KHZ_TONE | Tone control | Via frontend ops | +| 0x8D SEND_DISEQC | DiSEqC messages | Via frontend ops | +| 0x8F SET_DN_SWITCH | Legacy Dish switch | Via `send_legacy_dish_cmd` callback | +| 0x90 GET_SIGNAL_LOCK | Lock status | Via frontend ops | +| 0x92 GET_FW_VERS | Version check | Boot only | +| 0x94 USE_EXTRA_VOLT | +1V boost | Via `enable_high_lnb_voltage` callback | +| 0x95 GET_FPGA_VERS | Platform ID | Boot only | +| 0x9D CW3K_INIT | CW3K init | PID 0x0206 only | + +--- + +## 16. Firmware Storage Formats + +### 16.1 Cypress C2 EEPROM Boot Format + +The SkyWalker-1 firmware is stored in Cypress C2 IIC second-stage boot format, read by the FX2's internal boot ROM on power-up. + +**Header (8 bytes):** + +| Offset | Size | Field | SkyWalker-1 Value | +|--------|------|-------|-------------------| +| 0 | 1 | Marker | 0xC2 (external memory, large code model) | +| 1 | 2 | VID (LE) | 0x09C0 | +| 3 | 2 | PID (LE) | 0x0203 | +| 5 | 2 | DID (LE) | 0x0000 | +| 7 | 1 | Config | 0x40 (400 kHz I2C) | + +**Code segments**: 2-byte length (BE) + 2-byte target address (BE) + data. Maximum segment size: 1023 bytes (FX2 I2C boot ROM buffer limit). All SkyWalker-1 variants use 10 segments. + +**Terminator**: 0x80xx (high bit set) + 2-byte entry point address (0xE600 = CPUCS). + +**Segment layout (all SkyWalker-1 variants):** + +``` +Segment Address Length +------- ------- ------ +1 0x0000 1023 Contains reset vector, interrupt handlers +2 0x03FF 1023 +3 0x07FE 1023 +4 0x0BFD 1023 +5 0x0FFC 1023 +6 0x13FB 1023 +7 0x17FA 1023 +8 0x1BF9 1023 +9 0x1FF8 1023 +10 0x23F7 varies (115--265 bytes depending on version) +``` + +### 16.2 Decoded C2 Headers + +| File | VID | PID | Segments | Code Size | Entry | +|------|-----|-----|----------|-----------|-------| +| skywalker1_eeprom.bin (v2.06) | 0x09C0 | 0x0203 | 10 | 9,472 bytes | 0xE600 | +| sw1_v213_fw_1_c2.bin (v2.13.1) | 0x09C0 | 0x0203 | 10 | 9,322 bytes | 0xE600 | +| sw1_v213_fw_2_c2.bin (v2.13.2) | 0x09C0 | 0x0203 | 10 | 9,377 bytes | 0xE600 | +| sw1_v213_fw_3_c2.bin (v2.13.3) | 0x09C0 | 0x0203 | 10 | 9,369 bytes | 0xE600 | +| rev2_v210_fw_1_c2.bin (Rev.2) | 0x09C0 | 0x0202 | 9 | 8,843 bytes | 0xE600 | + +### 16.3 DVB-USB Binary Hexline Format (Kernel FW01) + +The format the kernel expects for `dvb-usb-gp8psk-01.fw` (only needed for Rev.1 Cold, PID 0x0200): + +``` +Record structure: + Offset Size Field + 0 1 len - Number of data bytes + 1 1 addr_lo - Target address low byte + 2 1 addr_hi - Target address high byte + 3 1 type - 0x00=data, 0x01=EOF, 0x04=extended addr + 4 len data[] - Payload bytes + 4+len 1 chk - Checksum byte +``` + +### 16.4 FW02 Chunk Format (BCM4500 Firmware) + +Only needed for Rev.1 Warm (PID 0x0201): + +``` +Chunk format: + Byte 0: payload_length (N) + Bytes 1-3: header/address bytes + Bytes 4..N+3: payload data + Terminator: single byte 0xFF + Maximum chunk size: 64 bytes (USB control transfer limit) +``` + +Command 0x88 (LOAD_BCM4500) initiates the transfer. Each chunk is sent via bulk endpoint 0x01. On the SkyWalker-1, 0x88 routes to STALL (BCM4500 firmware is in ROM). + +### 16.5 Format Incompatibility + +C2 (EEPROM) and hexline (kernel FW01) are structurally different containers. They cannot be used interchangeably, but the payload data is identical. A C2 file can be converted to hexline by stripping the 8-byte header, splitting segments into 16-byte records, and appending an EOF record. + +--- + +## 17. Debugging Reference + +### 17.1 I2C STOP Corruption Root Cause + +The root cause of the initial firmware hang was traced through incremental debug modes: + +| wValue | Action | Result | Diagnosis | +|--------|--------|--------|-----------| +| 0x82 | GPIO + power + `bmSTOP` + probe | Fails | bmSTOP corrupts controller | +| 0x85 | GPIO + power + probe (no bmSTOP) | Works | Confirms bmSTOP is the cause | +| 0x84 | I2C probe only (chip already powered) | Works | BCM4500 is alive; I2C function is correct | + +Key finding: mode 0x84 succeeds immediately after 0x82 fails, proving the BCM4500 was alive the whole time. The FX2 I2C controller was in a bad state, not the bus or slave. + +### 17.2 Boot Results After Fix + +| Metric | Value | +|--------|-------| +| Boot time | ~90 ms total | +| config_status | 0x03 (STARTED + FW_LOADED) | +| boot_stage | 0xFF (COMPLETE) | +| Direct registers 0xA2-0xA8 | All return 0x02 (powered, not locked) | +| Signal lock | 0x00 (no lock -- dish not aimed) | +| USB responsiveness | No hang; fully responsive throughout | + +### 17.3 Test Tools + +Located in `tools/` directory: + +| Script | Purpose | +|--------|---------| +| `test_boot_debug.py` | Sends debug modes 0x80--0x83 sequentially | +| `test_i2c_debug.py` | Powers on via 0x81, runs bus scans, tests probe timing | +| `test_i2c_isolate.py` | Tests re-reset and insufficient delay as failure causes | +| `test_i2c_pinpoint.py` | Definitive test: compares 0x84, 0x85, and 0x82 | +| `fw_load.py` | RAM firmware loader (halt CPU, write, restart) | + +### 17.4 FX2 Register Quick Reference + +| Address | Name | Notes | +|---------|------|-------| +| 0xE600 | CPUCS | CPU control/status; write 0x01 to halt, 0x00 to run | +| 0xE601 | IFCONFIG | Interface configuration (GPIF mode, clock) | +| 0xE60B | REVCTL | Revision control (NOAUTOARM, SKIPCOMMIT) | +| 0xE618 | EP2FIFOCFG | EP2 FIFO configuration (AUTOIN, 8-bit) | +| 0xE678 | I2CS | I2C control/status | +| 0xE679 | I2DAT | I2C data | +| 0xE67A | I2CTL | I2C speed control | +| 0xE6B8 | SETUPDAT[0] | bmRequestType | +| 0xE6B9 | SETUPDAT[1] | bRequest | +| 0xE6BA | SETUPDAT[2] | wValueL | +| 0xE6BB | SETUPDAT[3] | wValueH | +| 0xE6BC | SETUPDAT[4] | wIndexL | +| 0xE6BD | SETUPDAT[5] | wIndexH | +| 0xE6BE | SETUPDAT[6] | wLengthL | +| 0xE6BF | SETUPDAT[7] | wLengthH | +| 0xE68A | EP0BCH | EP0 byte count high | +| 0xE68B | EP0BCL | EP0 byte count low (write triggers transfer) | +| 0xE740 | EP0BUF | EP0 data buffer start | +| 0xE0B6 | (custom) | LNB voltage control register (XRAM) | + +--- + +## 18. Sources + +### Firmware Analysis + +- Ghidra decompilation/disassembly of five firmware images: + - v2.06.04 (Ghidra port 8193) -- extracted from SkyWalker-1 EEPROM + - Rev.2 v2.10.04 (Ghidra port 8197) -- extracted from Rev.2 hardware + - v2.13.01 FW1 (Ghidra port 8194) -- extracted from Windows updater + - v2.13.02 FW2 (Ghidra port 8195) -- extracted from Windows updater + - v2.13.03 FW3 (Ghidra port 8196) -- extracted from Windows updater +- Firmware dumps: `firmware-dump/` + +### Driver Source + +- Linux kernel 6.16.5: `drivers/media/usb/dvb-usb/gp8psk.c`, `gp8psk.h`, `gp8psk-fe.c`, `gp8psk-fe.h` +- Linux kernel: `drivers/media/usb/dvb-usb/dvb-usb-firmware.c` +- Windows BDA driver: `SkyWalker1_Final_Release/Source/SkyWalker1Control.cpp` +- Windows BDA driver: `SkyWalker1_Final_Release/Include/SkyWalker1Control.h`, `SkyWalker1CommonDef.h` + +### Hardware Documentation + +- BCM4500 Datasheet: [DatasheetQ](https://html.datasheetq.com/pdf-html/885700/Broadcom/2page/BCM4500.html), [Elcodis](https://elcodis.com/parts/5786421/BCM4500.html) +- BCM4501 Product Page: [Broadcom](https://www.broadcom.com/products/broadband/set-top-box/bcm4501) +- Cypress CY7C68013A (FX2LP) Technical Reference Manual +- Genpix Electronics: https://www.genpix-electronics.com/index.php?act=viewDoc&docId=9 +- Genpix SkyWalker-3 specifications: https://www.genpix-electronics.com/what-is-skywalker-3.html +- Device `dmesg` output from running SkyWalker-1 hardware + +### Analysis Reports (This Project) + +1. `gp8psk-driver-analysis.md` -- Linux kernel driver analysis +2. `firmware-analysis-v206-vs-v213.md` -- v2.06 vs v2.13 firmware comparison +3. `rev2-deep-analysis.md` -- Rev.2 deep function inventory (107 functions) +4. `gpif-streaming-analysis.md` -- GPIF/MPEG-2 streaming path +5. `tuning-protocol-analysis.md` -- TUNE_8PSK protocol deep dive +6. `vendor-commands-unknown.md` -- Vendor command decode (0x8F, 0x91--0x98) +7. `kernel-fw01-analysis.md` -- Kernel firmware format and EEPROM boot +8. `firmware-dump/fw_v213_comparison_report.md` -- v2.13 sub-variant comparison +9. `dvb-s2-investigation.md` -- DVB-S2 incompatibility investigation +10. `docs/boot-debug-findings.md` -- Boot/I2C debugging findings +11. `docs/diseqc/diseqc-skywalker-1.md` -- DiSEqC Windows BDA interface +12. `firmware/skywalker1.c` -- Custom firmware v3.01.0 source + +### Community References + +- [LinuxTV mailing list: BCM4500 and DVB-S2](https://www.mail-archive.com/linux-dvb@linuxtv.org/msg24808.html) +- [SatelliteGuys: Turbo 8PSK card reverse engineering](https://www.satelliteguys.us/xen/threads/another-turbo-8psk-card.246879/) +- [SatelliteGuys: Genpix SkyWalker-1 discussion](https://www.satelliteguys.us/xen/threads/genpix-skywalker-1.214196/) diff --git a/firmware/Makefile b/firmware/Makefile index 7df8558..3be2c8d 100644 --- a/firmware/Makefile +++ b/firmware/Makefile @@ -1,12 +1,16 @@ -FX2LIBDIR=fx2lib/ -BASENAME=skywalker1 -SOURCES=skywalker1.c -A51_SOURCES=dscr.a51 -VID=0x09C0 -PID=0x0203 -CODE_SIZE=--code-size 0x3c00 - -include $(FX2LIBDIR)lib/fx2.mk - -load: $(BUILDDIR)/$(BASENAME).bix - ../tools/fw_load.py $(BUILDDIR)/$(BASENAME).bix +FX2LIBDIR=fx2lib/ +BASENAME=skywalker1 +SOURCES=skywalker1.c +A51_SOURCES=dscr.a51 +VID=0x09C0 +PID=0x0203 +CODE_SIZE=--code-size 0x3c00 + +include $(FX2LIBDIR)lib/fx2.mk + +load: $(BUILDDIR)/$(BASENAME).bix + ../tools/fw_load.py load $(BUILDDIR)/$(BASENAME).ihx + +eeprom: $(BUILDDIR)/$(BASENAME).ihx + python3 ../tools/eeprom_write.py convert $(BUILDDIR)/$(BASENAME).ihx \ + -o $(BUILDDIR)/$(BASENAME)_eeprom.bin diff --git a/firmware/skywalker1.c b/firmware/skywalker1.c index e19f04a..9ed9afe 100644 --- a/firmware/skywalker1.c +++ b/firmware/skywalker1.c @@ -18,7 +18,7 @@ #include #include #include -#include + #define SYNCDELAY SYNCDELAY4 @@ -149,7 +149,7 @@ static __xdata BYTE diseqc_msg[6]; * [5] = last AB count (or 0xFF if none) * [6] = config_mode_exit result (1=ok, 0=fail) * [7] = overall PLL result (1=ok, 0=fail) */ -static __xdata BYTE pll_diag[8]; +static __xdata BYTE pll_diag[24]; /* last error code for diagnostic reads via 0xBC */ static __xdata BYTE last_error; @@ -165,6 +165,14 @@ static __xdata BYTE hp_added; /* devices added in last scan */ static __xdata BYTE hp_removed; /* devices removed in last scan */ static __xdata BYTE hp_scan_ok; /* 1 after first scan completes */ +/* BCM4500 signal read: 16-byte block returned by indirect register protocol */ +static __xdata BYTE sig_block[16]; + +/* Track current modulation index for signal read sub-address selection. + * Stock firmware uses sub_addr 0x10 for standard modes (DVB-S QPSK, DSS, + * BPSK) and 0x11 for turbo/DigiCipher modes (mod_index >= 4). */ +static __xdata BYTE current_mod_index; + /* Streaming diagnostics counters */ static __xdata DWORD sd_poll_count; /* main-loop poll cycles while armed */ static __xdata WORD sd_overflow_count; /* EP2 FULL events detected */ @@ -183,57 +191,309 @@ static volatile __xdata BYTE wdt_armed; /* * BCM4500 register initialization data extracted from stock v2.06 firmware. - * FUN_CODE_0ddd writes these 3 blocks to BCM4500 indirect registers (page 0) - * via the A6/A7/A8 control interface during BOOT_8PSK. * - * Stock stores these at code:0x0B4F in 17-byte blocks: [length, data[0..15]]. - * Only the data bytes (past the length prefix) go to A7. + * Boot blocks (FUN_CODE_0ddd at 0x0DDD): written once during BOOT_8PSK. + * Tune blocks (FUN_CODE_0ee9 at 0x0EE9): written before every retune, + * zeroing filter coefficients while preserving structure/address bytes. + * + * Stock stores both sets at code:0x0B4E in 17-byte XDATA format: + * [count, data[0..count-1], padding...] where count = bytes to write to A7. + * The first data byte in each block is the indirect register block address + * (0x06, 0x07, 0x03) — the BCM4500 uses it to route the remaining bytes. */ -static const __code BYTE bcm_init_block0[] = { - 0x0b, 0x17, 0x38, 0x9f, 0xd9, 0x80 -}; -static const __code BYTE bcm_init_block1[] = { - 0x09, 0x39, 0x4f, 0x00, 0x65, 0xb7, 0x10 -}; -static const __code BYTE bcm_init_block2[] = { - 0x0f, 0x0c, 0x09 -}; -#define BCM_INIT_BLOCK0_LEN 6 -#define BCM_INIT_BLOCK1_LEN 7 -#define BCM_INIT_BLOCK2_LEN 3 -/* ---------- BCM4500 I2C helpers ---------- */ +/* Boot blocks — full coefficients (code:0x0B4E, XDATA 0xE0F7) */ +static const __code BYTE bcm_boot_block0[] = { + 0x06, 0x0b, 0x17, 0x38, 0x9f, 0xd9, 0x80 +}; +static const __code BYTE bcm_boot_block1[] = { + 0x07, 0x09, 0x39, 0x4f, 0x00, 0x65, 0xb7, 0x10 +}; +static const __code BYTE bcm_boot_block2[] = { + 0x03, 0x0f, 0x0c, 0x09, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0xeb, 0x00 +}; +#define BCM_BOOT_BLOCK0_LEN 7 +#define BCM_BOOT_BLOCK1_LEN 8 +#define BCM_BOOT_BLOCK2_LEN 16 + +/* Tune blocks — zeroed coefficients (code:0x0B85, XDATA 0xE0C4). + * Same structure as boot blocks but with filter coefficients cleared. + * Block 2 is identical in both sets. */ +static const __code BYTE bcm_tune_block0[] = { + 0x06, 0x0b, 0x17, 0x00, 0x00, 0x00, 0x00 +}; +static const __code BYTE bcm_tune_block1[] = { + 0x07, 0x09, 0x39, 0x4f, 0x00, 0x00, 0x00, 0x00 +}; +#define BCM_TUNE_BLOCK0_LEN 7 +#define BCM_TUNE_BLOCK1_LEN 8 +/* Tune block 2 = bcm_boot_block2 (identical data, reuse to save code space) */ + +/* ---------- Bit-bang I2C via GPIO ---------- */ /* - * I2C timeout: ~5ms at 48MHz CPU clock (4 clocks/cycle, ~12 MIPS). - * At 400kHz I2C, one byte = 22.5us; 5ms gives >200x margin. - * The FX2 I2C controller has no hardware timeout -- if a slave holds - * SCL low (clock stretching), the master waits forever without this. + * Bypasses the FX2LP hardware I2C controller, which deadlocks when the + * CPU is halted (via 0xA0 CPUCS write) while the stock firmware has an + * I2C transaction in progress. The deadlock is unrecoverable by any + * software means — only a full chip power cycle clears it. + * + * SDA = PA0, true open-drain via OEA toggling (PA0 latch held at 0) + * SCL = PA1, push-pull (also PIN_PWR_EN — each LOW pulse briefly + * disables the power supply, but decoupling caps ride through the + * ~2-3μs glitch with <25mV droop) + * + * PORTACFG = 0x00: GPIO mode, I2C controller connected but NMOS frozen + * OFF in BERR state. Internal 1.5kΩ pull-ups active on both lines. + * Speed: ~30kHz at 48MHz CPU (conservative — 50×SYNCDELAY per half-period) */ -#define I2C_TIMEOUT 6000 + +#define I2C_TIMEOUT 6000 /* kept for ep0_wait_data */ #define GPIF_TIMEOUT 60000 /* GPIF idle wait (~15ms at 4MHz tick) */ #define EP2_TIMEOUT 60000 /* EP2 drain wait */ -static BOOL i2c_wait_done(void) { - WORD timeout = I2C_TIMEOUT; - while (!(I2CS & bmDONE)) { - if (--timeout == 0) { - last_error = ERR_I2C_TIMEOUT; - return FALSE; - } - } - return TRUE; +/* GPIO primitives — macros for tight inner loops. + * + * CRITICAL: IOA (SFR 0x80) has the classic 8051 read-modify-write bug. + * Byte-level RMW (IOA |= / IOA &= ~) reads ACTUAL PIN LEVELS for input + * pins, not the output latch. If PA0 (SDA) is an input (HIGH via pull-up), + * any byte RMW on IOA reads pin=1 and writes it to the latch, corrupting + * our SDA LOW state. Result: SDA never goes LOW, no valid I2C waveforms. + * + * Fix: PA0/PA1 are __sbit at SFR 0x80 (from fx2regs.h). These compile to + * SETB/CLR instructions — atomic single-bit ops, no RMW, no corruption. + * + * SDA (PA0): Pure GPIO, PORTACFG bit 0 = 1 ALWAYS (v6, experiment 0xD6). + * 0xD5 proved: with PORTACFG=0x00 + OEA=0, both SDA and SCL read HIGH. + * The I2C NMOS is NOT stuck ON at startup — it only re-latches when + * PORTACFG toggles back to 0x00 AFTER a GPIO LOW drive (0xD1 confirmed). + * The charge-then-disconnect approach (v5) CAUSED the re-latch by + * reconnecting PORTACFG=0x00 during BB_SDA_HIGH after BB_SDA_LOW. + * + * Fix: never toggle PORTACFG. Keep bit 0 = 1 (SDA disconnected from I2C + * controller permanently). Use GPIO push-pull to charge bus to 3.3V, + * then OEA release for open-drain emulation. Bus capacitance holds the + * charge for >100μs — well beyond our 17μs bit period. + * + * SCL (PA1): push-pull via atomic SETB/CLR (PORTACFG bit 1 always 0). + * GPIO push-pull overpowers any SCL NMOS (confirmed by 0xCD Phase 2). + * Internal 1.5kΩ pull-up assists (connected when PORTACFG bit 1 = 0). */ + +/* True open-drain I2C primitives (v7 — 0xD7). + * + * PORTACFG = 0x00 ALWAYS: GPIO mode with I2C controller connected. + * 0xD5 proved: I2C NMOS is frozen OFF in BERR state (IOA reads H + * with PORTACFG=0x00 + OEA=0). The internal 1.5kΩ pull-up is active. + * 0xD6 proved: PORTACFG=0x01 (INT0 mode) disconnects GPIO output — + * PA0 can't drive the pin at all. Must use PORTACFG=0x00. + * + * SDA: true open-drain via OEA toggling. + * LOW: PA0=0, OEA=1 → GPIO NMOS sinks to 0V (I2C NMOS is OFF). + * HIGH: OEA=0 → release, 1.5kΩ pull-up → 3.3V in ~0.6μs RC. + * No PORTACFG toggling → no NMOS re-trigger risk. + * No voltage divider → full 3.3V swing → slaves see clean signals. + * + * SCL: GPIO push-pull via atomic PA1 SETB/CLR. + * OEA bit 1 stays enabled. Internal 1.5kΩ pull-up assists. */ +#define BB_SDA_HIGH() do { OEA &= ~0x01; } while(0) +#define BB_SDA_LOW() do { PA0 = 0; OEA |= 0x01; } while(0) +#define BB_SDA_READ() (IOA & 0x01) +#define BB_SCL_HIGH() do { PA1 = 1; } while(0) +#define BB_SCL_LOW() do { PA1 = 0; } while(0) + +/* Half-period delay: 50×SYNCDELAY4 = 200 NOPs ≈ 17μs at 48MHz. + * Deliberately slow (~30kHz I2C) to test if the bus scan "all ACK" + * problem is caused by insufficient pull-up rise time. + * Normal I2C: 4.7kΩ pull-up × 400pF bus = 1.9μs RC constant. + * At 6 SYNCDELAYs (~2μs) we're marginal; at 50 SYNCDELAYs (~17μs) + * we have ~9 RC time constants = guaranteed full charge. */ +static void bb_delay(void) { + BYTE bb_d; + for (bb_d = 0; bb_d < 50; bb_d++) + SYNCDELAY; } -static BOOL i2c_wait_stop(void) { - WORD timeout = I2C_TIMEOUT; - while (I2CS & bmSTOP) { - if (--timeout == 0) { - last_error = ERR_I2C_TIMEOUT; - return FALSE; +static void bb_i2c_start(void) { + BB_SDA_HIGH(); + BB_SCL_HIGH(); + bb_delay(); + BB_SDA_LOW(); /* SDA falls while SCL high = START */ + bb_delay(); + BB_SCL_LOW(); + bb_delay(); +} + +static void bb_i2c_stop(void) { + BB_SDA_LOW(); + bb_delay(); + BB_SCL_HIGH(); + bb_delay(); + BB_SDA_HIGH(); /* SDA rises while SCL high = STOP */ + bb_delay(); +} + +/* Write one byte MSB-first. Returns 0 on ACK, 1 on NAK. */ +static BYTE bb_i2c_write_byte(BYTE val) { + BYTE i, ack; + for (i = 0; i < 8; i++) { + if (val & 0x80) BB_SDA_HIGH(); else BB_SDA_LOW(); + val <<= 1; + BB_SCL_HIGH(); + bb_delay(); + BB_SCL_LOW(); + bb_delay(); + } + BB_SDA_HIGH(); /* release for slave ACK */ + bb_delay(); /* settle: pull-up charges bus from LOW → HIGH */ + BB_SCL_HIGH(); + bb_delay(); + ack = BB_SDA_READ() ? 1 : 0; + BB_SCL_LOW(); + bb_delay(); + return ack; +} + +/* Read one byte MSB-first. Sends ACK if ack=1, NAK if ack=0. + * + * Between each bit we do a brief SDA charge pulse (~166ns push-pull HIGH, + * then release). Without the I2C controller's 1.5kΩ pull-up, a '0' bit + * leaves bus capacitance at LOW — a subsequent '1' bit would read as '0' + * because nothing pulls SDA back HIGH. The charge pulse restores HIGH; + * the slave's NMOS can then pull LOW in ~20ns if the next bit is '0'. */ +static BYTE bb_i2c_read_byte(BYTE ack) { + BYTE i, val = 0; + BB_SDA_HIGH(); /* initial charge + release for slave */ + for (i = 0; i < 8; i++) { + BB_SCL_HIGH(); + bb_delay(); + val = (val << 1) | (BB_SDA_READ() ? 1 : 0); + BB_SCL_LOW(); + /* Recharge SDA before next bit — slave overrides if it drives LOW */ + BB_SDA_HIGH(); + bb_delay(); + } + if (ack) BB_SDA_LOW(); else BB_SDA_HIGH(); + BB_SCL_HIGH(); + bb_delay(); + BB_SCL_LOW(); + bb_delay(); + BB_SDA_HIGH(); /* release SDA */ + return val; +} + +/* Probe a 7-bit I2C address. Returns 1 if device ACKs, 0 if NAK. */ +static BYTE bb_i2c_probe(BYTE addr) { + BYTE nak; + bb_i2c_start(); + nak = bb_i2c_write_byte(addr << 1); + bb_i2c_stop(); + return nak ? 0 : 1; +} + +/* Hardware I2C probe: uses the FX2 I2C controller (NOT bit-bang). + * Sends START + (addr<<1) on the bus, checks ACK, sends STOP. + * Returns 1 if slave ACKs, 0 if NAK or timeout. + * ONLY works when the I2C controller is in a clean state (no BERR). + * If BERR is set at entry, returns 0xFF as an error indicator. */ +static BYTE hw_i2c_probe(BYTE addr) { + BYTE timeout, ack; + + /* Bail if BERR — hardware controller is unusable */ + if (I2CS & bmBERR) + return 0xFF; + + /* START condition: setting bmSTART causes the controller to + * generate START when the next byte is written to I2DAT. */ + I2CS = bmSTART; + + /* Write address byte (7-bit addr + W=0). This triggers + * the START + address clock-out on the physical bus. */ + I2DAT = addr << 1; + + /* Wait for DONE (byte clocked out, ACK/NAK received) */ + for (timeout = 0; timeout < 255; timeout++) { + SYNCDELAY; + if (I2CS & bmDONE) break; + } + + /* Check if slave acknowledged */ + ack = (I2CS & bmACK) ? 1 : 0; + + /* STOP condition */ + I2CS = bmSTOP; + + /* Wait for STOP to complete (STOP bit clears when done) */ + for (timeout = 0; timeout < 255; timeout++) { + SYNCDELAY; + if (!(I2CS & bmSTOP)) break; + } + + return ack; +} + +/* Hardware I2C register read: START → addr+W → reg → rSTART → addr+R → data → STOP. + * Uses the FX2 hardware I2C controller. Returns TRUE on success. + * Only works when the controller is clean (no BERR). */ +static BOOL hw_i2c_read(BYTE addr, BYTE reg, BYTE len, __xdata BYTE *buf) { + BYTE timeout, i; + + if (I2CS & bmBERR) + return FALSE; + + /* Write phase: START + addr_W + register */ + I2CS = bmSTART; + I2DAT = addr << 1; + for (timeout = 0; timeout < 255; timeout++) { + SYNCDELAY; + if (I2CS & bmDONE) break; + } + if (!(I2CS & bmACK)) goto hw_fail; + + I2DAT = reg; + for (timeout = 0; timeout < 255; timeout++) { + SYNCDELAY; + if (I2CS & bmDONE) break; + } + + /* Read phase: repeated START + addr_R */ + I2CS = bmSTART; + I2DAT = (addr << 1) | 1; + for (timeout = 0; timeout < 255; timeout++) { + SYNCDELAY; + if (I2CS & bmDONE) break; + } + if (!(I2CS & bmACK)) goto hw_fail; + + /* Read data bytes. + * First I2DAT read is a dummy that triggers clocking. + * Subsequent reads return previous byte and trigger next clock. + * Before last byte: set LASTRD so controller sends NAK. */ + for (i = 0; i < len; i++) { + if (i == len - 1) + I2CS = bmLASTRD; + buf[i] = I2DAT; + for (timeout = 0; timeout < 255; timeout++) { + SYNCDELAY; + if (I2CS & bmDONE) break; } } + /* Required dummy read after LASTRD */ + { volatile BYTE dummy = I2DAT; (void)dummy; } + + I2CS = bmSTOP; + for (timeout = 0; timeout < 255; timeout++) { + SYNCDELAY; + if (!(I2CS & bmSTOP)) break; + } return TRUE; + +hw_fail: + I2CS = bmSTOP; + for (timeout = 0; timeout < 255; timeout++) { + SYNCDELAY; + if (!(I2CS & bmSTOP)) break; + } + return FALSE; } /* @@ -252,219 +512,130 @@ static BOOL ep0_wait_data(void) { } /* - * Combined I2C write-read with repeated START (no STOP between - * write and read phases). The BCM3440 tuner gateway expects this - * protocol for register-addressed reads (confirmed by stock firmware - * disassembly of function 0x1556). - * - * Sequence: START -> addr+W -> reg -> RESTART -> addr+R -> data -> STOP + * Combined I2C write-read with repeated START. + * Sequence: START → addr+W → reg → RESTART → addr+R → data[0..len-1] → STOP + * Same signature as the old hardware-I2C version — all callers unchanged. */ static BOOL i2c_combined_read(BYTE addr, BYTE reg, BYTE len, BYTE *buf) { BYTE i; - BYTE tmp; - /* START + write address */ - I2CS |= bmSTART; - I2DAT = addr << 1; - if (!i2c_wait_done()) - goto fail; - if (!(I2CS & bmACK)) { - last_error = ERR_I2C_NAK; - goto fail; - } + bb_i2c_start(); + if (bb_i2c_write_byte(addr << 1)) + { last_error = ERR_I2C_NAK; goto fail; } + if (bb_i2c_write_byte(reg)) + { last_error = ERR_I2C_NAK; goto fail; } - /* Write register address */ - I2DAT = reg; - if (!i2c_wait_done()) - goto fail; - if (!(I2CS & bmACK)) { - last_error = ERR_I2C_NAK; - goto fail; - } + bb_i2c_start(); /* repeated START */ + if (bb_i2c_write_byte((addr << 1) | 1)) + { last_error = ERR_I2C_NAK; goto fail; } - /* REPEATED START + read address */ - I2CS |= bmSTART; - I2DAT = (addr << 1) | 1; - if (!i2c_wait_done()) - goto fail; - if (!(I2CS & bmACK)) { - last_error = ERR_I2C_NAK; - goto fail; - } + for (i = 0; i < len; i++) + buf[i] = bb_i2c_read_byte(i < len - 1 ? 1 : 0); - /* For single byte, set LASTRD before dummy read */ - if (len == 1) - I2CS |= bmLASTRD; - - /* Dummy read to trigger first clock burst */ - tmp = I2DAT; - - for (i = 0; i < len; i++) { - if (!i2c_wait_done()) - goto fail; - if (i == len - 2) - I2CS |= bmLASTRD; - if (i == len - 1) - I2CS |= bmSTOP; - buf[i] = I2DAT; - } - - i2c_wait_stop(); + bb_i2c_stop(); return TRUE; fail: - I2CS |= bmSTOP; - i2c_wait_stop(); + bb_i2c_stop(); return FALSE; } /* - * I2C write with timeout -- writes addr+reg+data without using fx2lib, - * so we have full control over timeout behavior. - * Sends: START -> (addr<<1) -> reg -> data -> STOP + * Single-byte I2C write (bit-bang). + * Sends: START → (addr<<1) → reg → val → STOP */ static BOOL i2c_write_timeout(BYTE addr, BYTE reg, BYTE val) { - /* START + write address */ - I2CS |= bmSTART; - I2DAT = addr << 1; - if (!i2c_wait_done()) goto fail; - if (!(I2CS & bmACK)) { last_error = ERR_I2C_NAK; goto fail; } + bb_i2c_start(); + if (bb_i2c_write_byte(addr << 1)) + { last_error = ERR_I2C_NAK; goto fail; } + if (bb_i2c_write_byte(reg)) + { last_error = ERR_I2C_NAK; goto fail; } + if (bb_i2c_write_byte(val)) + { last_error = ERR_I2C_NAK; goto fail; } - /* Register address */ - I2DAT = reg; - if (!i2c_wait_done()) goto fail; - if (!(I2CS & bmACK)) { last_error = ERR_I2C_NAK; goto fail; } - - /* Data byte */ - I2DAT = val; - if (!i2c_wait_done()) goto fail; - - /* STOP */ - I2CS |= bmSTOP; - i2c_wait_stop(); + bb_i2c_stop(); return TRUE; fail: - I2CS |= bmSTOP; - i2c_wait_stop(); + bb_i2c_stop(); return FALSE; } /* - * Multi-byte I2C write with timeout. - * Sends: START -> (addr<<1) -> reg -> data[0..len-1] -> STOP + * Multi-byte I2C write (bit-bang). + * Sends: START → (addr<<1) → reg → data[0..len-1] → STOP */ static BOOL i2c_write_multi_timeout(BYTE addr, BYTE reg, BYTE len, __xdata BYTE *data) { BYTE i; - I2CS |= bmSTART; - I2DAT = addr << 1; - if (!i2c_wait_done()) goto fail; - if (!(I2CS & bmACK)) { last_error = ERR_I2C_NAK; goto fail; } - - I2DAT = reg; - if (!i2c_wait_done()) goto fail; - if (!(I2CS & bmACK)) { last_error = ERR_I2C_NAK; goto fail; } + bb_i2c_start(); + if (bb_i2c_write_byte(addr << 1)) + { last_error = ERR_I2C_NAK; goto fail; } + if (bb_i2c_write_byte(reg)) + { last_error = ERR_I2C_NAK; goto fail; } for (i = 0; i < len; i++) { - I2DAT = data[i]; - if (!i2c_wait_done()) goto fail; + if (bb_i2c_write_byte(data[i])) + { last_error = ERR_I2C_NAK; goto fail; } } - I2CS |= bmSTOP; - i2c_wait_stop(); + bb_i2c_stop(); return TRUE; fail: - I2CS |= bmSTOP; - i2c_wait_stop(); + bb_i2c_stop(); return FALSE; } /* ---------- EEPROM (calibration data) ---------- */ /* - * Check EEPROM presence by reading 1 byte from address 0x3FFF. - * Uses 16-bit addressing: START -> 0xA2 -> 0x3F -> 0xFF -> RESTART -> 0xA3 -> data -> STOP + * Check EEPROM presence by reading 1 byte from address 0x3FFF (bit-bang). + * Sets internal pointer to 0x4000 where PLL data blocks begin. * Result stored in i2c_buf[0]. - * - * Zero parameters to minimize DSEG usage (8051 has only 14 bytes free). - * Hardcodes EEPROM_ADDR and address 0x3FFF -- matches FUN_CODE_1556. - * - * ASSUMES: AT24C128 or AT24C256 (16KB/32KB). After reading 0x3FFF, the - * internal pointer advances to 0x4000 where PLL data blocks begin. - * Smaller devices (AT24C64) NAK during the address phase. */ static BOOL eeprom_check_present(void) { - BYTE i; + bb_i2c_start(); + if (bb_i2c_write_byte(0xA2)) + { last_error = ERR_I2C_NAK; goto fail; } + if (bb_i2c_write_byte(0x3F)) + { last_error = ERR_I2C_NAK; goto fail; } + if (bb_i2c_write_byte(0xFF)) + { last_error = ERR_I2C_NAK; goto fail; } - I2CS |= bmSTART; - I2DAT = 0xA2; /* EEPROM_ADDR << 1 = write */ - if (!i2c_wait_done()) goto fail; - if (!(I2CS & bmACK)) { last_error = ERR_I2C_NAK; goto fail; } + bb_i2c_start(); /* repeated START */ + if (bb_i2c_write_byte(0xA3)) + { last_error = ERR_I2C_NAK; goto fail; } - I2DAT = 0x3F; /* address high byte */ - if (!i2c_wait_done()) goto fail; - if (!(I2CS & bmACK)) { last_error = ERR_I2C_NAK; goto fail; } - - I2DAT = 0xFF; /* address low byte */ - if (!i2c_wait_done()) goto fail; - if (!(I2CS & bmACK)) { last_error = ERR_I2C_NAK; goto fail; } - - I2CS |= bmSTART; - I2DAT = 0xA3; /* (EEPROM_ADDR << 1) | 1 = read */ - if (!i2c_wait_done()) goto fail; - if (!(I2CS & bmACK)) { last_error = ERR_I2C_NAK; goto fail; } - - I2CS |= bmLASTRD; /* single byte read */ - i = I2DAT; /* dummy read to trigger clock */ - if (!i2c_wait_done()) goto fail; - I2CS |= bmSTOP; - i2c_buf[0] = I2DAT; - - i2c_wait_stop(); + i2c_buf[0] = bb_i2c_read_byte(0); /* single byte, NAK */ + bb_i2c_stop(); return TRUE; fail: - I2CS |= bmSTOP; - i2c_wait_stop(); + bb_i2c_stop(); return FALSE; } /* - * Read 20 bytes sequentially from EEPROM into i2c_buf[]. - * START -> 0xA3 -> data[0..19] -> STOP - * - * The EEPROM auto-increments its address pointer after each byte. - * After eeprom_check_present() sets addr to 0x3FFF+1 = 0x4000, - * successive calls read consecutive 20-byte PLL config blocks. - * - * Zero parameters to minimize DSEG (hardcodes EEPROM_ADDR and len=20). + * Read 20 bytes sequentially from EEPROM into i2c_buf[] (bit-bang). + * EEPROM auto-increments its address pointer after each byte. */ static BOOL eeprom_read_block(void) { BYTE i; - I2CS |= bmSTART; - I2DAT = 0xA3; /* (EEPROM_ADDR << 1) | 1 = read */ - if (!i2c_wait_done()) goto fail; - if (!(I2CS & bmACK)) { last_error = ERR_I2C_NAK; goto fail; } + bb_i2c_start(); + if (bb_i2c_write_byte(0xA3)) + { last_error = ERR_I2C_NAK; goto fail; } - i = I2DAT; /* dummy read (value discarded; i reused as loop counter) */ + for (i = 0; i < 20; i++) + i2c_buf[i] = bb_i2c_read_byte(i < 19 ? 1 : 0); - for (i = 0; i < 20; i++) { - if (!i2c_wait_done()) goto fail; - if (i == 18) I2CS |= bmLASTRD; - if (i == 19) I2CS |= bmSTOP; - i2c_buf[i] = I2DAT; - } - - i2c_wait_stop(); + bb_i2c_stop(); return TRUE; fail: - I2CS |= bmSTOP; - i2c_wait_stop(); + bb_i2c_stop(); return FALSE; } @@ -592,6 +763,76 @@ static BOOL bcm_poll_ready(void) { return TRUE; } +/* + * Read 16-byte signal data block from BCM4500 via indirect register protocol. + * + * Reverse-engineered from stock firmware function at code:0x0C97. + * The A7 register is a bidirectional FIFO: load the indirect address, + * execute with A8=0x03, then read the result from the same A7 register. + * + * Protocol: + * 1. A6 = 0x00 (page 0) + * 2. A7 FIFO = [0x01, sub_addr] (2-byte indirect block address) + * 3. A7 FIFO += [0x00] (padding/count byte) + * 4. A8 = 0x03 (execute) + * 5. Poll A8 for completion + * 6. A6 = 0x00 (re-select page 0 for readback) + * 7. Read 16 bytes from A7 FIFO into sig_block[] + * 8. Validate: sig_block[0] must be 0x81 (BCM4500 chip signature) + * + * Result layout (16 bytes): + * [0] = 0x81 (signature) + * [1] = sub_addr echo + * [2..5] = reserved/internal + * [6..7] = AGC2 (big-endian) + * [8..9] = AGC1 (big-endian) + * [10..11] = SNR (big-endian) + * [12..15] = reserved + * + * Stock firmware copies bytes 6-11 into EP0BUF[5..0] (reversed). + * Sub_addr: 0x10 for DVB-S/DSS/BPSK, 0x11 for turbo/DigiCipher (mod >= 4). + */ +static BOOL bcm_read_signal_block(void) { + BYTE sub_addr = (current_mod_index >= 4) ? 0x11 : 0x10; + + /* A6 = page 0 */ + if (!bcm_direct_write(BCM_REG_PAGE, 0x00)) + return FALSE; + + /* A7 = [0x01, sub_addr] — indirect block address */ + i2c_rd[0] = 0x01; + i2c_rd[1] = sub_addr; + if (!i2c_write_multi_timeout(BCM4500_ADDR, BCM_REG_DATA, 2, i2c_rd)) + return FALSE; + + /* A7 = [0x00] — padding byte */ + i2c_rd[0] = 0x00; + if (!i2c_write_multi_timeout(BCM4500_ADDR, BCM_REG_DATA, 1, i2c_rd)) + return FALSE; + + /* A8 = 0x03 — execute command */ + if (!bcm_direct_write(BCM_REG_CMD, BCM_CMD_WRITE)) + return FALSE; + + /* Poll for completion */ + if (!bcm_poll_ready()) + return FALSE; + + /* Re-select page 0 for readback */ + if (!bcm_direct_write(BCM_REG_PAGE, 0x00)) + return FALSE; + + /* Read 16 bytes from A7 FIFO */ + if (!i2c_combined_read(BCM4500_ADDR, BCM_REG_DATA, 16, sig_block)) + return FALSE; + + /* Validate BCM4500 chip signature */ + if (sig_block[0] != 0x81) + return FALSE; + + return TRUE; +} + /* * Write one block of initialization data to BCM4500 indirect registers. * Replicates FUN_CODE_0ddd's per-iteration I2C sequence from stock firmware: @@ -633,6 +874,26 @@ static BOOL bcm_write_init_block(const __code BYTE *data, BYTE len) { return bcm_poll_ready(); } +/* + * Write tune-preparation blocks to BCM4500. + * + * Reverse-engineered from stock firmware FUN_CODE_0ee9 (0x0EE9), called from + * the tune handler at 0x099A with up to 3 retries. These blocks zero the + * filter coefficients while keeping the structural/address bytes intact, + * preparing the demodulator for new tuning parameters. + * + * Block 2 is identical for boot and tune, so we reuse bcm_boot_block2. + */ +static BOOL bcm_write_tune_blocks(void) { + if (!bcm_write_init_block(bcm_tune_block0, BCM_TUNE_BLOCK0_LEN)) + return FALSE; + if (!bcm_write_init_block(bcm_tune_block1, BCM_TUNE_BLOCK1_LEN)) + return FALSE; + if (!bcm_write_init_block(bcm_boot_block2, BCM_BOOT_BLOCK2_LEN)) + return FALSE; + return TRUE; +} + /* * Load PLL configuration from calibration EEPROM into BCM4500. * @@ -774,9 +1035,6 @@ fail_exit: static BOOL bcm4500_boot(void) { boot_stage = 1; /* Stage 1: GPIO setup */ - /* Ensure fx2lib I2C functions won't spin forever */ - cancel_i2c_trans = FALSE; - /* P3.7, P3.6, P3.5 HIGH (idle state for control lines) */ IOD |= 0xE0; @@ -837,15 +1095,15 @@ static BOOL bcm4500_boot(void) { * vc_diag[0] bit 2 = skip this step. */ if (!(vc_diag[0] & 0x04)) { boot_stage = 5; - if (!bcm_write_init_block(bcm_init_block0, BCM_INIT_BLOCK0_LEN)) + if (!bcm_write_init_block(bcm_boot_block0, BCM_BOOT_BLOCK0_LEN)) return FALSE; boot_stage = 6; - if (!bcm_write_init_block(bcm_init_block1, BCM_INIT_BLOCK1_LEN)) + if (!bcm_write_init_block(bcm_boot_block1, BCM_BOOT_BLOCK1_LEN)) return FALSE; boot_stage = 7; - if (!bcm_write_init_block(bcm_init_block2, BCM_INIT_BLOCK2_LEN)) + if (!bcm_write_init_block(bcm_boot_block2, BCM_BOOT_BLOCK2_LEN)) return FALSE; } @@ -877,20 +1135,13 @@ static void i2c_hotplug_scan(void) { for (hp_a = 0; hp_a < 16; hp_a++) hp_curr[hp_a] = 0; - /* Probe each 7-bit address using timeout-protected I2C ops */ + /* Probe each 7-bit address using bit-bang I2C */ for (hp_a = 1; hp_a < 0x78; hp_a++) { - I2CS |= bmSTART; - I2DAT = hp_a << 1; /* write direction */ - if (!i2c_wait_done()) - goto hp_abort; /* I2C hung — abandon scan */ - if (I2CS & bmACK) { + if (bb_i2c_probe(hp_a)) { hp_byte = hp_a >> 3; hp_bit = hp_a & 0x07; hp_curr[hp_byte] |= (1 << hp_bit); } - I2CS |= bmSTOP; - if (!i2c_wait_stop()) - goto hp_abort; } /* Compare with previous scan (only after first successful scan) */ @@ -929,13 +1180,6 @@ static void i2c_hotplug_scan(void) { hp_prev[hp_a] = hp_curr[hp_a]; hp_scan_ok = 1; - return; - -hp_abort: - /* I2C timeout during scan — issue STOP and bail out. - * hp_curr is partial so we don't update hp_prev. */ - I2CS |= bmSTOP; - last_error = ERR_I2C_TIMEOUT; } /* ---------- Streaming diagnostics poll ---------- */ @@ -1702,6 +1946,7 @@ static void do_tune(void) { /* Modulation type and FEC rate */ tune_data[8] = EP0BUF[8]; tune_data[9] = EP0BUF[9]; + current_mod_index = EP0BUF[8]; /* Demod mode: default standard (0x10) */ tune_data[10] = 0x10; @@ -1719,6 +1964,10 @@ static void do_tune(void) { default: break; } + /* Write tune-preparation blocks (stock: 0x0EE9, clears filter coefficients). + * Called before every retune — the stock firmware retries up to 3x. */ + bcm_write_tune_blocks(); + /* Poll BCM4500 for readiness */ if (!bcm_poll_ready()) return; @@ -1775,8 +2024,92 @@ BOOL handle_vendorcommand(BYTE cmd) { do_tune(); return TRUE; - /* 0x87: GET_SIGNAL_STRENGTH -- read 6 bytes from BCM4500 */ + /* 0x87: GET_SIGNAL_STRENGTH -- read signal from BCM4500 + * wValue=0: normal mode — 6 bytes (stock compatible) + * wValue=0xFF: debug mode — 16-byte raw sig_block + 1 status byte (17 total) + * Uses the correct stock protocol: load indirect address into A7 FIFO, + * execute with A8=0x03, read 16-byte result block, extract bytes 6-11. */ case GET_SIGNAL_STRENGTH: + if (wval == 0xFF) { + /* Debug: return raw 16-byte sig_block + success flag */ + BYTE sig_ok; + BYTE si; + sig_ok = bcm_read_signal_block() ? 1 : 0; + for (si = 0; si < 16; si++) + EP0BUF[si] = sig_block[si]; + EP0BUF[16] = sig_ok; + EP0BCH = 0; + EP0BCL = 17; + return TRUE; + } + if (wval == 0xFE) { + /* Diagnostic: stock-style init block write + readback. + * Uses sig_block[] (xdata) as scratch to avoid DSEG overflow. + * Returns 16 bytes in sig_block: + * [0..6] = A7 readback (7 bytes, should match block0 with bit7 toggle) + * [7] = A8 final poll value (0x02 = completed) + * [8] = A8 poll iteration count (0-10) + * [9] = A2 after write + * [10] = A4 after write + * [11] = A6 after write + * [12] = I2C readback result (1=ok, 0=fail) + * [13] = A8 before start + * [14] = A2 before start + * [15] = I2C write result (1=ok, 0=fail) */ + BYTE di; + + /* First: verify bcm_direct_write works by toggling A0 */ + bcm_direct_read(0xA0, &sig_block[13]); /* should be 0x00 */ + bcm_direct_write(0xA0, 0x01); /* enter config mode */ + bcm_direct_read(0xA0, &sig_block[14]); /* should be 0x01 if write works */ + bcm_direct_write(0xA0, 0x00); /* exit config mode */ + + /* Write page 0 */ + bcm_direct_write(BCM_REG_PAGE, 0x00); + + /* Write boot block 0 to A7 — ONE BYTE AT A TIME + * to test if multi-byte writes auto-increment register address + * (would clobber A8/A9/...) instead of using FIFO mode. */ + sig_block[15] = 1; + for (di = 0; di < BCM_BOOT_BLOCK0_LEN; di++) { + if (!bcm_direct_write(BCM_REG_DATA, bcm_boot_block0[di])) { + sig_block[15] = 0; + break; + } + } + + /* Trailing zero + commit */ + bcm_direct_write(BCM_REG_DATA, 0x00); + bcm_direct_write(BCM_REG_CMD, BCM_CMD_WRITE); + + /* Poll A8 */ + sig_block[7] = 0xFF; + sig_block[8] = 10; + for (di = 0; di < 10; di++) { + delay(2); + bcm_direct_read(BCM_REG_CMD, &sig_block[7]); + if (!(sig_block[7] & 0x01)) { + sig_block[8] = di; + break; + } + } + + /* Post-state */ + bcm_direct_read(BCM_REG_STATUS, &sig_block[9]); + bcm_direct_read(BCM_REG_LOCK, &sig_block[10]); + bcm_direct_read(BCM_REG_PAGE, &sig_block[11]); + + /* Readback from A7 */ + bcm_direct_write(BCM_REG_PAGE, 0x00); + sig_block[12] = i2c_combined_read(BCM4500_ADDR, BCM_REG_DATA, + BCM_BOOT_BLOCK0_LEN, sig_block) ? 1 : 0; + + for (di = 0; di < 16; di++) + EP0BUF[di] = sig_block[di]; + EP0BCH = 0; + EP0BCL = 16; + return TRUE; + } if (!(config_status & BM_STARTED)) { EP0BUF[0] = 0; EP0BUF[1] = 0; EP0BUF[2] = 0; EP0BUF[3] = 0; @@ -1785,16 +2118,16 @@ BOOL handle_vendorcommand(BYTE cmd) { EP0BCL = 6; return TRUE; } - /* Zero-fill before reads so I2C failures return zeros, not stale data */ EP0BUF[0] = 0; EP0BUF[1] = 0; EP0BUF[2] = 0; EP0BUF[3] = 0; EP0BUF[4] = 0; EP0BUF[5] = 0; - /* Read signal quality via indirect registers */ - bcm_indirect_read(0x00, &EP0BUF[0]); - bcm_indirect_read(0x01, &EP0BUF[1]); - bcm_indirect_read(0x02, &EP0BUF[2]); - bcm_indirect_read(0x03, &EP0BUF[3]); - bcm_indirect_read(0x04, &EP0BUF[4]); - bcm_indirect_read(0x05, &EP0BUF[5]); + if (bcm_read_signal_block()) { + EP0BUF[0] = sig_block[11]; + EP0BUF[1] = sig_block[10]; + EP0BUF[2] = sig_block[9]; + EP0BUF[3] = sig_block[8]; + EP0BUF[4] = sig_block[7]; + EP0BUF[5] = sig_block[6]; + } EP0BCH = 0; EP0BCL = 6; return TRUE; @@ -1846,7 +2179,6 @@ BOOL handle_vendorcommand(BYTE cmd) { } else if (wval == 0x83) { /* Debug: GPIO + probe + first init block */ boot_stage = 1; - cancel_i2c_trans = FALSE; IOD |= 0xE0; OEA |= PIN_BCM_RESET; IOA &= ~PIN_BCM_RESET; @@ -1862,7 +2194,7 @@ BOOL handle_vendorcommand(BYTE cmd) { boot_stage = 0xE3; } else { boot_stage = 4; - if (bcm_write_init_block(bcm_init_block0, BCM_INIT_BLOCK0_LEN)) + if (bcm_write_init_block(bcm_boot_block0, BCM_BOOT_BLOCK0_LEN)) boot_stage = 0xA3; else boot_stage = 0xE4; @@ -2070,18 +2402,11 @@ BOOL handle_vendorcommand(BYTE cmd) { EP0BUF[byte_idx] = 0; for (a = 1; a < 0x78; a++) { - /* Try START + address + write, see if ACK comes back */ - I2CS |= bmSTART; - I2DAT = a << 1; /* write direction */ - if (!i2c_wait_done()) break; - if (I2CS & bmACK) { - /* Device responded at this address */ + if (bb_i2c_probe(a)) { byte_idx = a >> 3; bit = a & 0x07; EP0BUF[byte_idx] |= (1 << bit); } - I2CS |= bmSTOP; - if (!i2c_wait_stop()) break; } EP0BCH = 0; EP0BCL = 16; @@ -2167,21 +2492,21 @@ BOOL handle_vendorcommand(BYTE cmd) { /* 0xB7: SIGNAL_MONITOR -- fast combined signal read (8 bytes) * Returns SNR(2) + AGC1(2) + AGC2(2) + lock(1) + status(1) - * in a single USB transfer instead of 3 separate reads. */ + * in a single USB transfer instead of 3 separate reads. + * Uses bcm_read_signal_block() for correct stock-compatible protocol. */ case SIGNAL_MONITOR: { BYTE sm_val; - /* Zero-fill before reads so I2C failures return zeros, not stale data */ EP0BUF[0] = 0; EP0BUF[1] = 0; EP0BUF[2] = 0; EP0BUF[3] = 0; EP0BUF[4] = 0; EP0BUF[5] = 0; - /* SNR: indirect regs 0x00-0x01 */ - bcm_indirect_read(0x00, &EP0BUF[0]); - bcm_indirect_read(0x01, &EP0BUF[1]); - /* AGC1: indirect regs 0x02-0x03 */ - bcm_indirect_read(0x02, &EP0BUF[2]); - bcm_indirect_read(0x03, &EP0BUF[3]); - /* AGC2: indirect regs 0x04-0x05 */ - bcm_indirect_read(0x04, &EP0BUF[4]); - bcm_indirect_read(0x05, &EP0BUF[5]); + if (bcm_read_signal_block()) { + /* Same byte order as GET_SIGNAL_STRENGTH (stock compatible) */ + EP0BUF[0] = sig_block[11]; /* SNR low */ + EP0BUF[1] = sig_block[10]; /* SNR high */ + EP0BUF[2] = sig_block[9]; /* AGC1 low */ + EP0BUF[3] = sig_block[8]; /* AGC1 high */ + EP0BUF[4] = sig_block[7]; /* AGC2 low */ + EP0BUF[5] = sig_block[6]; /* AGC2 high */ + } /* Lock register (direct 0xA4) */ sm_val = 0; bcm_direct_read(BCM_REG_LOCK, &sm_val); @@ -2217,13 +2542,18 @@ BOOL handle_vendorcommand(BYTE cmd) { do_tune(); if (dwell > 0) delay(dwell); - /* Read signal into result buffer */ - bcm_indirect_read(0x00, &tm_result[0]); - bcm_indirect_read(0x01, &tm_result[1]); - bcm_indirect_read(0x02, &tm_result[2]); - bcm_indirect_read(0x03, &tm_result[3]); - bcm_indirect_read(0x04, &tm_result[4]); - bcm_indirect_read(0x05, &tm_result[5]); + /* Read signal via stock-compatible block protocol */ + tm_result[0] = 0; tm_result[1] = 0; + tm_result[2] = 0; tm_result[3] = 0; + tm_result[4] = 0; tm_result[5] = 0; + if (bcm_read_signal_block()) { + tm_result[0] = sig_block[11]; /* SNR low */ + tm_result[1] = sig_block[10]; /* SNR high */ + tm_result[2] = sig_block[9]; /* AGC1 low */ + tm_result[3] = sig_block[8]; /* AGC1 high */ + tm_result[4] = sig_block[7]; /* AGC2 low */ + tm_result[5] = sig_block[6]; /* AGC2 high */ + } tm_result[6] = 0; bcm_direct_read(BCM_REG_LOCK, &tm_result[6]); tm_result[7] = 0; @@ -2353,41 +2683,26 @@ BOOL handle_vendorcommand(BYTE cmd) { if (elen > 64) elen = 64; if (elen == 0) elen = 1; - /* Set EEPROM address: START → 0xA2 → addr_hi → addr_lo */ - I2CS |= bmSTART; - I2DAT = 0xA2; - if (!i2c_wait_done()) goto ee_fail; - if (!(I2CS & bmACK)) goto ee_fail; - I2DAT = (BYTE)(wval >> 8); /* address high byte */ - if (!i2c_wait_done()) goto ee_fail; - if (!(I2CS & bmACK)) goto ee_fail; - I2DAT = (BYTE)(wval); /* address low byte */ - if (!i2c_wait_done()) goto ee_fail; - if (!(I2CS & bmACK)) goto ee_fail; + /* Set EEPROM address via bit-bang I2C */ + bb_i2c_start(); + if (bb_i2c_write_byte(0xA2)) goto ee_fail; + if (bb_i2c_write_byte((BYTE)(wval >> 8))) goto ee_fail; + if (bb_i2c_write_byte((BYTE)(wval))) goto ee_fail; - /* Repeated START → 0xA3 → read data */ - I2CS |= bmSTART; - I2DAT = 0xA3; - if (!i2c_wait_done()) goto ee_fail; - if (!(I2CS & bmACK)) goto ee_fail; + /* Repeated START → read data */ + bb_i2c_start(); + if (bb_i2c_write_byte(0xA3)) goto ee_fail; - if (elen == 1) I2CS |= bmLASTRD; - ei = I2DAT; /* dummy read */ + for (ei = 0; ei < elen; ei++) + EP0BUF[ei] = bb_i2c_read_byte(ei < elen - 1 ? 1 : 0); - for (ei = 0; ei < elen; ei++) { - if (!i2c_wait_done()) goto ee_fail; - if (ei == elen - 2) I2CS |= bmLASTRD; - if (ei == elen - 1) I2CS |= bmSTOP; - EP0BUF[ei] = I2DAT; - } - i2c_wait_stop(); + bb_i2c_stop(); EP0BCH = 0; EP0BCL = elen; return TRUE; ee_fail: - I2CS |= bmSTOP; - i2c_wait_stop(); + bb_i2c_stop(); for (ei = 0; ei < elen; ei++) EP0BUF[ei] = 0xFF; EP0BCH = 0; @@ -2396,25 +2711,15 @@ BOOL handle_vendorcommand(BYTE cmd) { } /* 0xBF: GET_PLL_DIAG -- PLL config diagnostic from last boot - * Returns 10 bytes: - * [0] eeprom_present (1=ok, 0=fail) - * [1] first_block_count (0xFF if not reached) - * [2] blocks_written - * [3] last_A9 value (0xFF if none) - * [4] last_AA value (0xFF if none) - * [5] last_AB_count (0xFF if none) - * [6] config_exit (1=ok, 0=fail, 0xFF=not reached) - * [7] overall_result (1=ok, 0=fail) - * [8] boot_stage (0xFF=complete, else stage where it stopped) - * [9] last_error snapshot */ + * Returns 26 bytes: pll_diag[0..23] + boot_stage + last_error */ case GET_PLL_DIAG: { BYTE di; - for (di = 0; di < 8; di++) + for (di = 0; di < 24; di++) EP0BUF[di] = pll_diag[di]; - EP0BUF[8] = boot_stage; - EP0BUF[9] = last_error; + EP0BUF[24] = boot_stage; + EP0BUF[25] = last_error; EP0BCH = 0; - EP0BCL = 10; + EP0BCL = 26; return TRUE; } @@ -2431,26 +2736,123 @@ BOOL handle_vendorcommand(BYTE cmd) { return TRUE; } - /* 0xC2: MULTI_WRITE_TEST -- auto-increment test for BCM3440 gateway. - * Writes 3 test bytes to register A7 as multi-byte I2C, then reads - * A6/A7/A8/A9. Returns 8 bytes. Uses vc_diag[] to avoid DSEG. */ - case 0xC2: - i2c_buf[0] = 0xAA; - i2c_buf[1] = 0xBB; - i2c_buf[2] = 0xCC; - vc_diag[0] = i2c_write_multi_timeout(BCM4500_ADDR, BCM_REG_DATA, 3, i2c_buf) ? 1 : 0; - delay(1); - vc_diag[1] = 0xEE; bcm_direct_read(BCM_REG_PAGE, &vc_diag[1]); - vc_diag[2] = 0xEE; bcm_direct_read(BCM_REG_DATA, &vc_diag[2]); - vc_diag[3] = 0xEE; bcm_direct_read(BCM_REG_CMD, &vc_diag[3]); - vc_diag[4] = 0xEE; bcm_direct_read(0xA9, &vc_diag[4]); - EP0BUF[0] = vc_diag[0]; EP0BUF[1] = vc_diag[1]; - EP0BUF[2] = vc_diag[2]; EP0BUF[3] = vc_diag[3]; - EP0BUF[4] = vc_diag[4]; EP0BUF[5] = 0; - EP0BUF[6] = 0; EP0BUF[7] = 0; + /* 0xC2: I2C_HW_DEBUG -- raw I2C controller diagnostic. + * wValue = target 7-bit addr (0=use 0x51 EEPROM). + * Returns 8 bytes: I2CS at each step of a manual transaction. */ + case 0xC2: { + /* Live register snapshot — same layout as pll_diag cold-start. + * Compare with 0xBF pll_diag to see if values changed since boot. */ + EP0BUF[0] = I2CS; /* 0xE678 */ + EP0BUF[1] = I2CTL; /* 0xE67A */ + EP0BUF[2] = CPUCS; /* 0xE600 */ + EP0BUF[3] = IFCONFIG; /* 0xE601 */ + EP0BUF[4] = REVID; /* 0xE60A */ + EP0BUF[5] = USBCS; /* 0xE680 */ + EP0BUF[6] = PORTACFG; /* 0xE670 */ + EP0BUF[7] = REVCTL; /* 0xE60B */ EP0BCH = 0; EP0BCL = 8; return TRUE; + } + + /* 0xC3: I2C_BUS_TEST -- bit-level I2C bus diagnostic. + * Performs a manual I2C probe of 0x51 (EEPROM), capturing IOA at every + * step. Returns 32 bytes: IOA snapshots through the full transaction. + * + * Layout: + * [0] OEA before test + * [1] IOA idle (before START) + * [2] IOA after SDA LOW (START setup) + * [3] IOA after SCL LOW (START complete) + * [4..11] IOA after each data bit SCL HIGH (8 bits of 0xA2) + * [12..19] IOA after each data bit SCL LOW + * [20] IOA after SDA release for ACK + * [21] IOA after ACK SCL HIGH (THIS IS THE MONEY BIT) + * [22] IOA after ACK SCL LOW + * [23] IOA after STOP (SDA rises while SCL HIGH) + * [24] OEA after test + * [25] SDA toggle test: drive LOW, read back + * [26] SDA toggle test: charge HIGH, read back + * [27] SDA toggle test: release (input), read back after 100us + * [28-31] reserved + */ + case 0xC3: { + BYTE i, val, cap_idx; + + cap_idx = 0; + EP0BUF[cap_idx++] = OEA; /* [0] OEA */ + + /* SDA toggle test first — verify GPIO actually controls the bus */ + BB_SDA_LOW(); /* drive SDA LOW */ + bb_delay(); + EP0BUF[25] = IOA & 0x03; /* should show SDA=L SCL=H */ + + BB_SDA_HIGH(); /* charge SDA HIGH then release */ + bb_delay(); + EP0BUF[26] = IOA & 0x03; /* should show SDA=H SCL=H */ + + /* Wait 100us with SDA released to see if it holds HIGH */ + { BYTE d; for (d = 0; d < 6; d++) bb_delay(); } + EP0BUF[27] = IOA & 0x03; /* SDA holding? */ + + /* Now do a real I2C transaction to EEPROM 0x51 */ + /* Idle state */ + BB_SDA_HIGH(); + BB_SCL_HIGH(); + bb_delay(); + EP0BUF[cap_idx++] = IOA & 0x03; /* [1] idle state */ + + /* START: SDA falls while SCL HIGH */ + BB_SDA_LOW(); + bb_delay(); + EP0BUF[cap_idx++] = IOA & 0x03; /* [2] after SDA LOW */ + BB_SCL_LOW(); + bb_delay(); + EP0BUF[cap_idx++] = IOA & 0x03; /* [3] START complete */ + + /* Clock out address byte 0xA2 (EEPROM 0x51 write) */ + val = 0xA2; + for (i = 0; i < 8; i++) { + if (val & 0x80) BB_SDA_HIGH(); else BB_SDA_LOW(); + val <<= 1; + BB_SCL_HIGH(); + bb_delay(); + EP0BUF[4 + i] = IOA & 0x03; /* [4..11] SDA+SCL during HIGH */ + BB_SCL_LOW(); + bb_delay(); + EP0BUF[12 + i] = IOA & 0x03; /* [12..19] after SCL LOW */ + } + + /* ACK cycle */ + BB_SDA_HIGH(); /* release SDA for slave ACK */ + bb_delay(); + EP0BUF[20] = IOA & 0x03; /* [20] SDA after release */ + BB_SCL_HIGH(); /* 9th clock */ + bb_delay(); + EP0BUF[21] = IOA & 0x03; /* [21] ACK sample: SDA=L=ACK */ + BB_SCL_LOW(); + bb_delay(); + EP0BUF[22] = IOA & 0x03; /* [22] after ACK clock */ + + /* STOP */ + BB_SDA_LOW(); + bb_delay(); + BB_SCL_HIGH(); + bb_delay(); + BB_SDA_HIGH(); + bb_delay(); + EP0BUF[23] = IOA & 0x03; /* [23] after STOP */ + + EP0BUF[24] = OEA; /* [24] OEA */ + EP0BUF[28] = 0; + EP0BUF[29] = 0; + EP0BUF[30] = 0; + EP0BUF[31] = 0; + + EP0BCH = 0; + EP0BCL = 32; + return TRUE; + } default: return FALSE; @@ -2530,6 +2932,51 @@ void timer0_isr(void) __interrupt (1) { void main(void) { + /* Experiment 0xDB: EEPROM boot with hardware I2C. + * + * Previous experiments (0xD8-0xDA) proved that the CPUCS halt/restart + * cycle (used by USB RAM loading) permanently corrupts the FX2 I2C + * controller with unclearable BERR (I2CS=0xF6). Both hardware I2C + * and bit-bang are blocked after USB load (NMOS latching on SDA). + * + * The EEPROM boot path avoids CPUCS halt/restart entirely: the boot + * ROM loads firmware from EEPROM via I2C, then jumps to it. The CPU + * never halts, so the I2C controller stays clean. + * + * This experiment detects the boot method and uses the appropriate + * I2C strategy: + * - EEPROM boot (I2CS clean): hardware I2C controller + * - USB RAM boot (I2CS=BERR): bit-bang I2C (fallback) + * + * Layout (24 bytes): + * [0] = 0xDB marker + * [1] = I2CS at boot (expect 0x00 from EEPROM, 0xF6 from USB) + * [2] = I2CTL at boot + * [3] = PORTACFG at boot (before we touch it) + * [4] = IOA & 0x03 at boot (SDA/SCL pin states) + * [5] = hw_i2c_probe(0x51) EEPROM + * [6] = hw_i2c_probe(0x10) BCM4500 via BCM3440 + * [7] = hw_i2c_probe(0x08) BCM4500 direct + * [8] = hw_i2c_probe(0x50) alt EEPROM + * [9] = I2CS after hw probes + * [10] = hw bus scan count (0x08-0x77), 0xFF if BERR + * [11] = hw_i2c_read(0x10, 0xA2, 1) result: BCM4500 status reg + * [12] = hw_i2c_read success flag (1=ok, 0=fail) + * [13] = hw_i2c_read(0x10, 0xA4, 1): BCM4500 lock register + * [14] = I2CS after all hw tests + * [15] = boot method: 0xEE=EEPROM (clean I2C), 0xBB=USB (BERR) + * [16-21] = reserved + * [22] = PORTACFG final + * [23] = IOA & 0x03 final + */ + + /* Capture boot-time register state BEFORE we touch anything */ + pll_diag[0] = 0xDB; + pll_diag[1] = I2CS; + pll_diag[2] = I2CTL; + pll_diag[3] = PORTACFG; + pll_diag[4] = IOA & 0x03; + config_status = 0; last_error = ERR_OK; got_sud = FALSE; @@ -2562,20 +3009,97 @@ void main(void) { ENABLE_HISPEED(); ENABLE_USBRESET(); - /* Configure I2C: 400kHz */ - I2CTL = bm400KHZ; + /* --- GPIO setup --- + * PORTACFG = 0x00: GPIO mode for PA0 (SDA) and PA1 (SCL). + * When I2C controller is clean (EEPROM boot): NMOS controlled by + * I2C state machine, don't touch PA0/PA1 GPIO. + * When I2C controller has BERR (USB boot): NMOS frozen, use + * bit-bang via GPIO. */ + PORTACFG = 0x00; + I2CTL = 0x01; /* 400kHz I2C */ + SYNCDELAY; - /* Configure GPIO output enables (v2.06 pin map): - * P0.1=power_en, P0.2=power_dis, P0.3=22kHz, P0.4=LNB, - * P0.5=BCM_reset, P0.7=DiSEqC/streaming */ + /* Configure non-I2C GPIO pins */ OEA |= (PIN_PWR_EN | PIN_PWR_DIS | PIN_22KHZ | PIN_LNB_VOLT | PIN_BCM_RESET | PIN_DISEQC); + IOA = PIN_DISEQC | PIN_BCM_RESET | PIN_PWR_EN; /* 0xA2 */ + IOD = 0xE1; - /* Initial GPIO state matches stock firmware: - * P0.7=1 (DiSEqC idle), P0.5=0 (BCM4500 held in reset), - * P0.2=1 (power disable), all others LOW */ - IOA = 0x84; - IOD = 0xE1; /* P3.7:5=1 (controls idle), P3.0=1 */ + /* Detect boot method from I2CS state */ + if (!(I2CS & bmBERR)) { + /* EEPROM boot: I2C controller is clean! Use hardware I2C. */ + pll_diag[15] = 0xEE; + + delay(5); /* ~15ms for power-up */ + + /* Hardware I2C probes */ + pll_diag[5] = hw_i2c_probe(0x51); /* EEPROM */ + pll_diag[6] = hw_i2c_probe(0x10); /* BCM4500 via BCM3440 */ + pll_diag[7] = hw_i2c_probe(0x08); /* BCM4500 direct */ + pll_diag[8] = hw_i2c_probe(0x50); /* alt EEPROM addr */ + pll_diag[9] = I2CS; + + /* Hardware I2C bus scan */ + { + BYTE a, cnt = 0; + for (a = 0x08; a < 0x78; a++) { + if (hw_i2c_probe(a) == 1) + cnt++; + } + pll_diag[10] = cnt; + } + + /* Read BCM4500 status register via BCM3440 gateway */ + if (hw_i2c_read(BCM4500_ADDR, BCM_REG_STATUS, 1, i2c_rd)) { + pll_diag[11] = i2c_rd[0]; + pll_diag[12] = 1; /* success */ + } else { + pll_diag[11] = 0xFF; + pll_diag[12] = 0; /* fail */ + } + + /* Read BCM4500 lock register */ + if (hw_i2c_read(BCM4500_ADDR, BCM_REG_LOCK, 1, i2c_rd)) { + pll_diag[13] = i2c_rd[0]; + } else { + pll_diag[13] = 0xFF; + } + + pll_diag[14] = I2CS; + + } else { + /* USB RAM boot: BERR present. Use bit-bang I2C (fallback). */ + pll_diag[15] = 0xBB; + + /* Set up bit-bang GPIO: PA0 latch LOW for SDA open-drain */ + PA0 = 0; + PA1 = 1; + OEA &= ~0x01; /* SDA input (floating HIGH via pull-up) */ + + delay(5); /* ~15ms for power-up */ + + /* Bit-bang I2C probes */ + pll_diag[5] = bb_i2c_probe(0x51); + pll_diag[6] = bb_i2c_probe(0x10); + pll_diag[7] = bb_i2c_probe(0x08); + pll_diag[8] = bb_i2c_probe(0x50); + pll_diag[9] = I2CS; + pll_diag[10] = 0xFF; /* bus scan skipped for bit-bang */ + + /* Try bit-bang register read */ + if (i2c_combined_read(BCM4500_ADDR, BCM_REG_STATUS, 1, i2c_rd)) { + pll_diag[11] = i2c_rd[0]; + pll_diag[12] = 1; + } else { + pll_diag[11] = 0xFF; + pll_diag[12] = 0; + } + pll_diag[13] = 0xFF; + pll_diag[14] = I2CS; + } + + pll_diag[22] = PORTACFG; + pll_diag[23] = IOA & 0x03; /* EP2 is bulk IN (0x82), 512 byte, double-buffered */ EP2CFG = 0xE2; /* valid, IN, bulk, 512, double */ @@ -2596,8 +3120,13 @@ void main(void) { /* Reset all FIFOs */ RESETFIFOS(); - /* IFCONFIG: internal 48MHz, GPIF master, async */ - IFCONFIG = 0xEE; + /* IFCONFIG: internal 48MHz, GPIF master, async. + * Stock firmware uses 0xCA (no IFCLKOE, no GSTATE output). + * IFCLKOE=1 drives 48MHz on the IFCLK pin — if that pin is routed + * to a BCM chip, it could interfere with normal operation. + * GSTATE=1 puts GPIF debug signals on Port E which may also conflict. + * Match stock: keep IFCLK tristate, Port E normal. */ + IFCONFIG = 0xCA; SYNCDELAY; /* EP2FIFOCFG: AUTOIN, ZEROLENIN, 8-bit */ diff --git a/tools/eeprom_dump.py b/tools/eeprom_dump.py index 169caef..ce767b3 100644 --- a/tools/eeprom_dump.py +++ b/tools/eeprom_dump.py @@ -1,251 +1,157 @@ -#!/usr/bin/env python3 -""" -Genpix SkyWalker-1 EEPROM firmware dump tool. - -Reads the Cypress FX2 boot EEPROM via the I2C_READ vendor command. -Protocol: I2C_READ (0x84), wValue=0x51, wIndex=offset, length=chunk_size - -The EEPROM contains firmware in Cypress C2 IIC boot format: - - Header: C2 VID_L VID_H PID_L PID_H DID_L DID_H CONFIG - - Records: LEN_H LEN_L ADDR_H ADDR_L DATA[LEN] - - End: 80 01 ENTRY_H ENTRY_L (reset vector) -""" -import usb.core, usb.util, sys, struct - -VENDOR_ID = 0x09C0 -PRODUCT_ID = 0x0203 -I2C_READ = 0x84 -EEPROM_SLAVE = 0x51 - - -def find_device(): - dev = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID) - if dev is None: - print("SkyWalker-1 not found") - sys.exit(1) - return dev - - -def detach_driver(dev): - intf_num = None - for cfg in dev: - for intf in cfg: - if dev.is_kernel_driver_active(intf.bInterfaceNumber): - try: - dev.detach_kernel_driver(intf.bInterfaceNumber) - intf_num = intf.bInterfaceNumber - except usb.core.USBError as e: - print(f"Cannot detach driver: {e}") - print("Try: sudo modprobe -r dvb_usb_gp8psk") - sys.exit(1) - try: - dev.set_configuration() - except: - pass - return intf_num - - -def eeprom_read(dev, offset, length=64): - """Read from EEPROM at given offset.""" - # wIndex holds the EEPROM byte offset (16-bit, so max 64KB) - return dev.ctrl_transfer( - usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, - I2C_READ, EEPROM_SLAVE, offset, length, 2000) - - -def parse_c2_header(data): - """Parse Cypress C2 boot EEPROM header.""" - if data[0] != 0xC2: - print(f" Not a C2 EEPROM (first byte: 0x{data[0]:02X})") - return None - - vid = data[2] << 8 | data[1] - pid = data[4] << 8 | data[3] - did = data[6] << 8 | data[5] - config = data[7] - - print(f" Format: C2 (Large EEPROM, code loads to internal RAM)") - print(f" VID: 0x{vid:04X} {'(Genpix)' if vid == 0x09C0 else ''}") - print(f" PID: 0x{pid:04X} {'(SkyWalker-1)' if pid == 0x0203 else ''}") - print(f" DID: 0x{did:04X}") - print(f" Config: 0x{config:02X}", end="") - - config_flags = [] - if config & 0x40: - config_flags.append("400kHz I2C") - if config & 0x04: - config_flags.append("disconnect") - if config_flags: - print(f" ({', '.join(config_flags)})") - else: - print() - - return {"vid": vid, "pid": pid, "did": did, "config": config} - - -def parse_records(data, offset=8): - """Parse C2 load records from EEPROM data.""" - records = [] - while offset < len(data) - 4: - rec_len = (data[offset] << 8) | data[offset + 1] - rec_addr = (data[offset + 2] << 8) | data[offset + 3] - - if rec_len == 0x8001: - # End marker - rec_addr is the entry point (reset vector) - records.append({ - "type": "end", - "entry_point": rec_addr, - "offset": offset - }) - break - elif rec_len == 0 or rec_len > 0x4000: - records.append({ - "type": "invalid", - "raw_len": rec_len, - "offset": offset - }) - break - - rec_data = data[offset + 4:offset + 4 + rec_len] - records.append({ - "type": "data", - "length": rec_len, - "load_addr": rec_addr, - "data": bytes(rec_data), - "offset": offset - }) - offset += 4 + rec_len - - return records - - -def main(): - import argparse - parser = argparse.ArgumentParser(description="Dump SkyWalker-1 EEPROM firmware") - parser.add_argument('-o', '--output', default='skywalker1_eeprom.bin', - help='Output file for raw EEPROM dump') - parser.add_argument('--extract', action='store_true', - help='Also extract firmware as flat binary') - parser.add_argument('--max-size', type=int, default=16384, - help='Maximum EEPROM size to read (default: 16384)') - args = parser.parse_args() - - print("Genpix SkyWalker-1 EEPROM Dump") - print("=" * 40) - - dev = find_device() - print(f"Found device: Bus {dev.bus} Addr {dev.address}") - intf = detach_driver(dev) - - try: - # Read EEPROM - chunk_size = 64 # Max reliable USB control transfer - eeprom = bytearray() - consecutive_ff = 0 - - print(f"\nReading EEPROM (max {args.max_size} bytes)...") - - for offset in range(0, args.max_size, chunk_size): - # wIndex only goes up to 0xFFFF, which covers 64KB EEPROMs - data = eeprom_read(dev, offset, chunk_size) - - if data is None: - print(f"\n Read failed at offset 0x{offset:04X}") - break - - chunk = bytes(data) - eeprom.extend(chunk) - - # Check for end of data - if all(b == 0xFF for b in chunk): - consecutive_ff += 1 - if consecutive_ff >= 4: - print(f"\r End of data at 0x{len(eeprom):04X} (0xFF padding) ") - break - else: - consecutive_ff = 0 - - if offset % 1024 == 0: - print(f"\r 0x{offset:04X} / 0x{args.max_size:04X} ", end="", flush=True) - - print(f"\r Read {len(eeprom)} bytes total ") - - # Save raw EEPROM - with open(args.output, 'wb') as f: - f.write(eeprom) - print(f" Saved raw EEPROM to: {args.output}") - - # Parse header - print(f"\n{'=' * 40}") - print("EEPROM Header:") - header = parse_c2_header(eeprom) - - if header: - # Parse load records - print(f"\nLoad Records:") - records = parse_records(eeprom) - total_code = 0 - entry_point = None - - for i, rec in enumerate(records): - if rec["type"] == "data": - end_addr = rec["load_addr"] + rec["length"] - 1 - preview = rec["data"][:8].hex(' ') - print(f" [{i}] {rec['length']:5d} bytes -> " - f"0x{rec['load_addr']:04X}-0x{end_addr:04X} " - f"[{preview}...]") - total_code += rec["length"] - elif rec["type"] == "end": - entry_point = rec["entry_point"] - print(f" [{i}] END MARKER -> entry point: 0x{entry_point:04X}") - else: - print(f" [{i}] INVALID (raw_len=0x{rec['raw_len']:04X}) " - f"at EEPROM offset 0x{rec['offset']:04X}") - - print(f"\n Total firmware: {total_code} bytes in " - f"{sum(1 for r in records if r['type'] == 'data')} records") - if entry_point: - print(f" Entry point: 0x{entry_point:04X} (LJMP target after boot)") - - # Extract flat binary - if args.extract and records: - # Build memory image - mem = bytearray(0x10000) # 64KB address space - for b in range(len(mem)): - mem[b] = 0xFF - - for rec in records: - if rec["type"] == "data": - addr = rec["load_addr"] - mem[addr:addr + rec["length"]] = rec["data"] - - # Find actual used range - min_addr = min(r["load_addr"] for r in records if r["type"] == "data") - max_addr = max(r["load_addr"] + r["length"] - for r in records if r["type"] == "data") - - flat_file = args.output.replace('.bin', '_flat.bin') - with open(flat_file, 'wb') as f: - f.write(mem[min_addr:max_addr]) - print(f"\n Flat binary: {flat_file}") - print(f" Address range: 0x{min_addr:04X}-0x{max_addr:04X} " - f"({max_addr - min_addr} bytes)") - - # Also save full 64KB image for Ghidra - full_file = args.output.replace('.bin', '_full64k.bin') - with open(full_file, 'wb') as f: - f.write(mem) - print(f" Full 64K image: {full_file} (for Ghidra, load at 0x0000)") - - finally: - if intf is not None: - try: - usb.util.release_interface(dev, intf) - dev.attach_kernel_driver(intf) - print("\nRe-attached kernel driver") - except: - print("\nNote: run 'sudo modprobe dvb_usb_gp8psk' to reload") - - -if __name__ == '__main__': - main() +#!/usr/bin/env python3 +""" +Genpix SkyWalker-1 EEPROM exploration tool. + +Reads the calibration EEPROM (AT24Cxxx at I2C addr 0x51) via the custom +firmware's EEPROM_READ (0xC0) vendor command. This uses 16-bit addressing +directly, bypassing the stock firmware's single-byte I2C_READ protocol. + +Primary purpose: Find where PLL configuration data is stored so the +bcm4500_load_pll_config() function reads from the correct address. +""" +import sys +sys.path.insert(0, 'tools') +from skywalker_lib import SkyWalker1 + +CMD_EEPROM_READ = 0xC0 + +sw = SkyWalker1() +sw.open() + + +def eeprom_read(addr, length): + """Read bytes from EEPROM at 16-bit address. + wValue = address, wIndex = length.""" + return sw._vendor_in(CMD_EEPROM_READ, value=addr, index=length, length=length) + + +def hex_dump(addr, data): + """Print hex dump with ASCII sidebar.""" + for i in range(0, len(data), 16): + chunk = data[i:i + 16] + hex_str = ' '.join(f'{b:02X}' for b in chunk) + ascii_str = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk) + print(f' {addr + i:04X}: {hex_str:<48s} |{ascii_str}|') + + +print('=== EEPROM Exploration ===') +print() + +# Step 1: Determine EEPROM size by aliasing detection +print('--- Size Detection ---') +data_0000 = eeprom_read(0x0000, 16) +data_4000 = eeprom_read(0x4000, 16) +data_8000 = eeprom_read(0x8000, 16) +print(f' 0x0000: {data_0000.hex(" ")}') +print(f' 0x4000: {data_4000.hex(" ")}') +print(f' 0x8000: {data_8000.hex(" ")}') +if data_0000 == data_4000: + print(' Result: 0x4000 ALIASES to 0x0000 → AT24C128 (16KB)') + eeprom_size = 16384 +elif data_0000 == data_8000: + print(' Result: 0x8000 aliases to 0x0000 → AT24C256 (32KB)') + eeprom_size = 32768 +else: + print(' Result: All different → AT24C512+ (64KB+)') + eeprom_size = 65536 +print() + +# Step 2: Dump first 512 bytes (FX2 boot firmware header + data) +print('--- EEPROM 0x0000-0x01FF (C2 boot header region) ---') +for addr in range(0x0000, 0x0200, 64): + data = eeprom_read(addr, 64) + hex_dump(addr, data) +print() + +# Step 3: Scan for PLL-like 20-byte blocks +# Format: [count(1-16), A9_val, AA_val, unused_byte, AB_data[count], padding...] +# Sentinel: count=0 +print('--- Scanning for PLL config blocks ---') +print(' Format: [count, A9, AA, unused, AB_data[count]]') +print(' Sentinel: count=0') +print() + +# Scan the entire EEPROM in 20-byte strides +pll_candidates = [] +for addr in range(0, min(eeprom_size, 0x4000), 20): + data = eeprom_read(addr, 20) + count = data[0] + # Look for potential sentinel (count=0) preceded by valid blocks + if count == 0 and addr > 0: + # Check if previous 20 bytes looked like PLL data + prev = eeprom_read(addr - 20, 20) + if 1 <= prev[0] <= 16: + pll_candidates.append({ + 'sentinel_addr': addr, + 'last_block_addr': addr - 20, + 'last_count': prev[0], + 'last_a9': prev[1], + 'last_aa': prev[2], + }) + +if pll_candidates: + print(' Found sentinel(s):') + for c in pll_candidates: + print(f' Sentinel at 0x{c["sentinel_addr"]:04X}') + print(f' Last block at 0x{c["last_block_addr"]:04X}: ' + f'count={c["last_count"]} A9=0x{c["last_a9"]:02X} AA=0x{c["last_aa"]:02X}') + # Walk backwards to find start of PLL data + start = c['last_block_addr'] + while start >= 20: + prev = eeprom_read(start - 20, 20) + if 1 <= prev[0] <= 16: + start -= 20 + else: + break + print(f' PLL data likely starts at: 0x{start:04X}') + # Dump the PLL blocks + print(f' PLL block dump:') + for baddr in range(start, c['sentinel_addr'] + 20, 20): + block = eeprom_read(baddr, 20) + cnt = block[0] + if cnt == 0: + print(f' 0x{baddr:04X}: [sentinel count=0]') + break + ab = block[4:4 + cnt] + print(f' 0x{baddr:04X}: count={cnt} A9=0x{block[1]:02X} ' + f'AA=0x{block[2]:02X} unused=0x{block[3]:02X} ' + f'AB=[{ab.hex(" ")}]') +else: + print(' No PLL sentinel found in first 16KB!') + print(' Dumping any 20-byte-aligned blocks with count 1-16:') + for addr in range(0, min(eeprom_size, 0x1000), 20): + data = eeprom_read(addr, 20) + count = data[0] + if 1 <= count <= 16: + ab = data[4:4 + count] + print(f' 0x{addr:04X}: count={count} A9=0x{data[1]:02X} ' + f'AA=0x{data[2]:02X} unused=0x{data[3]:02X} ' + f'AB=[{ab.hex(" ")}]') +print() + +# Step 4: Dump around the 16KB boundary (where our code expects PLL data) +if eeprom_size > 16384: + print('--- EEPROM 0x3FE0-0x4060 (16KB boundary) ---') + for addr in range(0x3FE0, 0x4060, 64): + data = eeprom_read(addr, 64) + hex_dump(addr, data) + print() + +# Step 5: Check for 0xFF regions (empty/erased) +print('--- Empty region scan ---') +last_was_ff = False +for addr in range(0, min(eeprom_size, 0x4000), 64): + data = eeprom_read(addr, 64) + is_ff = all(b == 0xFF for b in data) + if is_ff and not last_was_ff: + print(f' 0xFF starts at 0x{addr:04X}') + last_was_ff = True + elif not is_ff and last_was_ff: + print(f' Data resumes at 0x{addr:04X}') + last_was_ff = False +if last_was_ff: + print(f' 0xFF continues to end of scanned region') + +sw.close() +print() +print('=== Done ===') diff --git a/tools/eeprom_write.py b/tools/eeprom_write.py index e307f38..452c730 100755 --- a/tools/eeprom_write.py +++ b/tools/eeprom_write.py @@ -1,575 +1,789 @@ -#!/usr/bin/env python3 -""" -Genpix SkyWalker-1 EEPROM firmware flash tool. - -Writes C2-format firmware images to the Cypress FX2 boot EEPROM via -the I2C_WRITE vendor command. - -Protocol: - I2C_WRITE (0x83): wValue=0x51, wIndex=offset, data=bytes - I2C_READ (0x84): wValue=0x51, wIndex=offset, length=chunk_size - -The EEPROM uses Cypress C2 IIC boot format: - - Header: C2 VID_L VID_H PID_L PID_H DID_L DID_H CONFIG - - Records: LEN_H LEN_L ADDR_H ADDR_L DATA[LEN] - - End: 80 01 ENTRY_H ENTRY_L (reset vector) - -WARNING: Flashing incorrect firmware can brick the device. The FX2 -boots from this EEPROM on power-up -- a corrupted image means the -device will not enumerate on USB until the EEPROM is reprogrammed -with an external programmer or the FX2 boot ROM's A0 vendor request. -""" -import usb.core, usb.util, sys, struct, time, os - -VENDOR_ID = 0x09C0 -PRODUCT_ID = 0x0203 -I2C_WRITE = 0x83 -I2C_READ = 0x84 -EEPROM_SLAVE = 0x51 - -# EEPROM page write parameters -PAGE_SIZE = 16 # Conservative page size for 24Cxx EEPROMs -WRITE_CYCLE_MS = 10 # Max internal write cycle time per page -MAX_EEPROM_SIZE = 16384 # 128Kbit / 16KB max addressable via wIndex - - -def find_device(): - dev = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID) - if dev is None: - print("SkyWalker-1 not found") - sys.exit(1) - return dev - - -def detach_driver(dev): - intf_num = None - for cfg in dev: - for intf in cfg: - if dev.is_kernel_driver_active(intf.bInterfaceNumber): - try: - dev.detach_kernel_driver(intf.bInterfaceNumber) - intf_num = intf.bInterfaceNumber - except usb.core.USBError as e: - print(f"Cannot detach driver: {e}") - print("Try: sudo modprobe -r dvb_usb_gp8psk") - sys.exit(1) - try: - dev.set_configuration() - except: - pass - return intf_num - - -def eeprom_read(dev, offset, length=64): - """Read from EEPROM at given offset.""" - return dev.ctrl_transfer( - usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, - I2C_READ, EEPROM_SLAVE, offset, length, 2000) - - -def eeprom_write(dev, offset, data): - """Write data to EEPROM at given offset. Caller handles page alignment.""" - return dev.ctrl_transfer( - usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_OUT, - I2C_WRITE, EEPROM_SLAVE, offset, data, 2000) - - -def eeprom_read_all(dev, size, label="Reading"): - """Read entire EEPROM contents up to size bytes.""" - chunk_size = 64 - data = bytearray() - for offset in range(0, size, chunk_size): - remaining = min(chunk_size, size - offset) - chunk = eeprom_read(dev, offset, remaining) - if chunk is None: - print(f"\n Read failed at offset 0x{offset:04X}") - return None - data.extend(bytes(chunk)) - if offset % 1024 == 0: - pct = offset * 100 // size - print(f"\r {label}: 0x{offset:04X} / 0x{size:04X} [{pct:3d}%]", - end="", flush=True) - print(f"\r {label}: 0x{size:04X} / 0x{size:04X} [100%] ") - return data - - -def parse_c2_header(data): - """Parse Cypress C2 boot EEPROM header. Returns dict or None.""" - if len(data) < 8: - return None - if data[0] != 0xC2: - return None - - vid = data[2] << 8 | data[1] - pid = data[4] << 8 | data[3] - did = data[6] << 8 | data[5] - config = data[7] - - return {"vid": vid, "pid": pid, "did": did, "config": config} - - -def parse_records(data, offset=8): - """Parse C2 load records from EEPROM data.""" - records = [] - while offset < len(data) - 4: - rec_len = (data[offset] << 8) | data[offset + 1] - rec_addr = (data[offset + 2] << 8) | data[offset + 3] - - if rec_len == 0x8001: - records.append({ - "type": "end", - "entry_point": rec_addr, - "offset": offset - }) - break - elif rec_len == 0 or rec_len > 0x4000: - records.append({ - "type": "invalid", - "raw_len": rec_len, - "offset": offset - }) - break - - rec_data = data[offset + 4:offset + 4 + rec_len] - records.append({ - "type": "data", - "length": rec_len, - "load_addr": rec_addr, - "data": bytes(rec_data), - "offset": offset - }) - offset += 4 + rec_len - - return records - - -def print_c2_header(header, prefix=" "): - """Display parsed C2 header fields.""" - print(f"{prefix}Format: C2 (Large EEPROM, code loads to internal RAM)") - print(f"{prefix}VID: 0x{header['vid']:04X}" - f" {'(Genpix)' if header['vid'] == 0x09C0 else ''}") - print(f"{prefix}PID: 0x{header['pid']:04X}" - f" {'(SkyWalker-1)' if header['pid'] == 0x0203 else ''}") - print(f"{prefix}DID: 0x{header['did']:04X}") - print(f"{prefix}Config: 0x{header['config']:02X}", end="") - - config_flags = [] - if header["config"] & 0x40: - config_flags.append("400kHz I2C") - if header["config"] & 0x04: - config_flags.append("disconnect") - if config_flags: - print(f" ({', '.join(config_flags)})") - else: - print() - - -def print_c2_records(records, prefix=" "): - """Display parsed C2 load records.""" - total_code = 0 - for i, rec in enumerate(records): - if rec["type"] == "data": - end_addr = rec["load_addr"] + rec["length"] - 1 - preview = rec["data"][:8].hex(' ') - print(f"{prefix}[{i}] {rec['length']:5d} bytes -> " - f"0x{rec['load_addr']:04X}-0x{end_addr:04X} " - f"[{preview}...]") - total_code += rec["length"] - elif rec["type"] == "end": - print(f"{prefix}[{i}] END MARKER -> entry point: " - f"0x{rec['entry_point']:04X}") - else: - print(f"{prefix}[{i}] INVALID (raw_len=0x{rec['raw_len']:04X}) " - f"at EEPROM offset 0x{rec['offset']:04X}") - - data_recs = [r for r in records if r["type"] == "data"] - print(f"\n{prefix}Total firmware: {total_code} bytes in " - f"{len(data_recs)} segments") - end_recs = [r for r in records if r["type"] == "end"] - if end_recs: - print(f"{prefix}Entry point: 0x{end_recs[0]['entry_point']:04X} " - f"(LJMP target after boot)") - - -def validate_c2_image(data, label="image"): - """Validate a C2 firmware image. Returns (header, records) or exits.""" - if len(data) < 12: - print(f" {label}: too small ({len(data)} bytes, need at least 12)") - return None, None - - if data[0] != 0xC2: - print(f" {label}: not a C2 image (first byte: 0x{data[0]:02X}, " - f"expected 0xC2)") - return None, None - - header = parse_c2_header(data) - if header is None: - print(f" {label}: failed to parse C2 header") - return None, None - - records = parse_records(data) - if not records: - print(f" {label}: no load records found") - return None, None - - end_recs = [r for r in records if r["type"] == "end"] - invalid_recs = [r for r in records if r["type"] == "invalid"] - if not end_recs: - print(f" {label}: WARNING -- no end marker found") - if invalid_recs: - print(f" {label}: WARNING -- {len(invalid_recs)} invalid record(s)") - - return header, records - - -def cmd_info(args): - """Parse and display C2 header info from a .bin file.""" - if not os.path.exists(args.file): - print(f"File not found: {args.file}") - sys.exit(1) - - with open(args.file, 'rb') as f: - data = f.read() - - print(f"C2 Image: {args.file}") - print(f"File size: {len(data)} bytes") - print("=" * 40) - - header, records = validate_c2_image(data, args.file) - if header is None: - sys.exit(1) - - print("\nHeader:") - print_c2_header(header) - - print("\nLoad Records:") - print_c2_records(records) - - # Compute EEPROM usage (header + record headers + data + end marker) - if records: - last = records[-1] - if last["type"] == "end": - eeprom_end = last["offset"] + 4 - elif last["type"] == "data": - eeprom_end = last["offset"] + 4 + last["length"] - else: - eeprom_end = last["offset"] - print(f"\n EEPROM footprint: {eeprom_end} bytes " - f"(0x{eeprom_end:04X})") - - -def cmd_backup(args): - """Dump current EEPROM contents to a file.""" - print("Genpix SkyWalker-1 EEPROM Backup") - print("=" * 40) - - dev = find_device() - print(f"Found device: Bus {dev.bus} Addr {dev.address}") - intf = detach_driver(dev) - - try: - size = args.max_size - print(f"\nReading EEPROM ({size} bytes)...") - data = eeprom_read_all(dev, size) - if data is None: - print("Backup failed: read error") - sys.exit(1) - - with open(args.output, 'wb') as f: - f.write(data) - print(f" Saved to: {args.output}") - - # Show header info - header = parse_c2_header(data) - if header: - print("\nHeader:") - print_c2_header(header) - finally: - if intf is not None: - try: - usb.util.release_interface(dev, intf) - dev.attach_kernel_driver(intf) - print("\nRe-attached kernel driver") - except: - print("\nNote: run 'sudo modprobe dvb_usb_gp8psk' to reload") - - -def cmd_verify(args): - """Compare a .bin file against current EEPROM contents.""" - if not os.path.exists(args.file): - print(f"File not found: {args.file}") - sys.exit(1) - - with open(args.file, 'rb') as f: - image = f.read() - - print("Genpix SkyWalker-1 EEPROM Verify") - print("=" * 40) - - # Validate the image first - header, records = validate_c2_image(image, args.file) - if header is None: - sys.exit(1) - - print(f"\nImage: {args.file} ({len(image)} bytes)") - print_c2_header(header) - - dev = find_device() - print(f"\nFound device: Bus {dev.bus} Addr {dev.address}") - intf = detach_driver(dev) - - try: - print(f"\nReading EEPROM ({len(image)} bytes)...") - eeprom = eeprom_read_all(dev, len(image), label="Verify") - if eeprom is None: - print("Verify failed: read error") - sys.exit(1) - - # Compare byte-by-byte - mismatches = [] - for i in range(len(image)): - if i < len(eeprom) and image[i] != eeprom[i]: - mismatches.append(i) - - if not mismatches: - print(f"\n MATCH -- EEPROM contents match {args.file}") - else: - print(f"\n MISMATCH -- {len(mismatches)} byte(s) differ:") - for off in mismatches[:32]: - exp = image[off] - got = eeprom[off] if off < len(eeprom) else 0xFF - print(f" 0x{off:04X}: expected 0x{exp:02X}, " - f"got 0x{got:02X}") - if len(mismatches) > 32: - print(f" ... and {len(mismatches) - 32} more") - sys.exit(1) - finally: - if intf is not None: - try: - usb.util.release_interface(dev, intf) - dev.attach_kernel_driver(intf) - print("\nRe-attached kernel driver") - except: - print("\nNote: run 'sudo modprobe dvb_usb_gp8psk' to reload") - - -def cmd_flash(args): - """Write a C2-format .bin file to the EEPROM.""" - if not os.path.exists(args.file): - print(f"File not found: {args.file}") - sys.exit(1) - - with open(args.file, 'rb') as f: - image = f.read() - - print("Genpix SkyWalker-1 EEPROM Flash") - print("=" * 40) - print() - print(" *** FIRMWARE FLASH -- READ CAREFULLY ***") - print(" Writing bad firmware will brick the device.") - print(" The SkyWalker-1 boots from this EEPROM on power-up.") - print(" A corrupted image = no USB enumeration.") - print() - - # Validate input image - img_header, img_records = validate_c2_image(image, args.file) - if img_header is None: - sys.exit(1) - - print(f"Image: {args.file} ({len(image)} bytes)") - print_c2_header(img_header) - - # Size sanity check - if len(image) > MAX_EEPROM_SIZE: - print(f"\n Image too large: {len(image)} bytes " - f"(max {MAX_EEPROM_SIZE})") - sys.exit(1) - - if len(image) < 12: - print(f"\n Image too small: {len(image)} bytes") - sys.exit(1) - - # Connect to device - dev = find_device() - print(f"\nFound device: Bus {dev.bus} Addr {dev.address}") - intf = detach_driver(dev) - - try: - # Check VID/PID against the connected device - if not args.force: - if img_header["vid"] != VENDOR_ID: - print(f"\n VID mismatch: image has 0x{img_header['vid']:04X}," - f" device is 0x{VENDOR_ID:04X}") - print(" Use --force to override") - sys.exit(1) - if img_header["pid"] != PRODUCT_ID: - print(f"\n PID mismatch: image has 0x{img_header['pid']:04X}," - f" device is 0x{PRODUCT_ID:04X}") - print(" Use --force to override") - sys.exit(1) - elif img_header["vid"] != VENDOR_ID or img_header["pid"] != PRODUCT_ID: - print(f"\n WARNING: VID/PID mismatch (--force active)") - print(f" Image: VID=0x{img_header['vid']:04X} " - f"PID=0x{img_header['pid']:04X}") - print(f" Device: VID=0x{VENDOR_ID:04X} PID=0x{PRODUCT_ID:04X}") - - # Backup current EEPROM - if not args.no_backup: - ts = time.strftime("%Y%m%d_%H%M%S") - backup_file = f"eeprom_backup_{ts}.bin" - print(f"\nBacking up current EEPROM to {backup_file}...") - backup = eeprom_read_all(dev, MAX_EEPROM_SIZE, label="Backup") - if backup is None: - print(" Backup failed: read error. Aborting.") - sys.exit(1) - with open(backup_file, 'wb') as f: - f.write(backup) - print(f" Backup saved: {backup_file} ({len(backup)} bytes)") - - # Show what's currently on the EEPROM - cur_header = parse_c2_header(backup) - if cur_header: - print("\n Current EEPROM:") - print_c2_header(cur_header, prefix=" ") - else: - print("\n Skipping backup (--no-backup)") - - # Dry-run stops here - if args.dry_run: - print("\n DRY RUN -- would write {0} bytes in {1} pages".format( - len(image), (len(image) + PAGE_SIZE - 1) // PAGE_SIZE)) - print(" No changes made.") - return - - # Final confirmation - print(f"\nAbout to write {len(image)} bytes to EEPROM...") - print(" Press Ctrl+C within 3 seconds to abort.") - try: - for i in range(3, 0, -1): - print(f"\r Writing in {i}... ", end="", flush=True) - time.sleep(1) - print("\r Writing now... ") - except KeyboardInterrupt: - print("\n Aborted.") - return - - # Write in page-sized chunks - total_pages = (len(image) + PAGE_SIZE - 1) // PAGE_SIZE - write_errors = 0 - - for page_num in range(total_pages): - offset = page_num * PAGE_SIZE - end = min(offset + PAGE_SIZE, len(image)) - chunk = image[offset:end] - - pct = (page_num + 1) * 100 // total_pages - print(f"\r Write: 0x{offset:04X} / 0x{len(image):04X} " - f"[{pct:3d}%]", end="", flush=True) - - try: - written = eeprom_write(dev, offset, chunk) - if written != len(chunk): - print(f"\n Short write at 0x{offset:04X}: " - f"sent {len(chunk)}, wrote {written}") - write_errors += 1 - except usb.core.USBError as e: - print(f"\n Write error at 0x{offset:04X}: {e}") - write_errors += 1 - - # Wait for EEPROM internal write cycle - time.sleep(WRITE_CYCLE_MS / 1000.0) - - print(f"\r Write: 0x{len(image):04X} / 0x{len(image):04X} " - f"[100%] ") - - if write_errors: - print(f"\n WARNING: {write_errors} write error(s) occurred") - - # Verify by reading back - print(f"\nVerifying ({len(image)} bytes)...") - verify = eeprom_read_all(dev, len(image), label="Verify") - if verify is None: - print(" Verify failed: read error") - print(" *** EEPROM STATE UNKNOWN -- check before power cycling ***") - sys.exit(1) - - mismatches = [] - for i in range(len(image)): - if i < len(verify) and image[i] != verify[i]: - mismatches.append(i) - - if not mismatches: - print(f"\n VERIFIED -- all {len(image)} bytes match") - print(" Flash complete. Power cycle the device to boot new firmware.") - else: - print(f"\n VERIFY FAILED -- {len(mismatches)} byte(s) differ:") - for off in mismatches[:16]: - exp = image[off] - got = verify[off] if off < len(verify) else 0xFF - print(f" 0x{off:04X}: wrote 0x{exp:02X}, " - f"read 0x{got:02X}") - if len(mismatches) > 16: - print(f" ... and {len(mismatches) - 16} more") - print("\n *** EEPROM CONTENTS DO NOT MATCH IMAGE ***") - print(" Do NOT power cycle until this is resolved.") - sys.exit(1) - - finally: - if intf is not None: - try: - usb.util.release_interface(dev, intf) - dev.attach_kernel_driver(intf) - print("\nRe-attached kernel driver") - except: - print("\nNote: run 'sudo modprobe dvb_usb_gp8psk' to reload") - - -def main(): - import argparse - parser = argparse.ArgumentParser( - description="SkyWalker-1 EEPROM firmware flash tool") - sub = parser.add_subparsers(dest='command', required=True) - - # info - p_info = sub.add_parser('info', - help='Parse and display C2 header from a .bin file') - p_info.add_argument('file', help='C2 firmware image (.bin)') - - # backup - p_backup = sub.add_parser('backup', - help='Dump current EEPROM to a file') - p_backup.add_argument('-o', '--output', default='skywalker1_eeprom.bin', - help='Output file (default: skywalker1_eeprom.bin)') - p_backup.add_argument('--max-size', type=int, default=MAX_EEPROM_SIZE, - help=f'Bytes to read (default: {MAX_EEPROM_SIZE})') - - # verify - p_verify = sub.add_parser('verify', - help='Compare .bin file against EEPROM') - p_verify.add_argument('file', help='C2 firmware image (.bin)') - - # flash - p_flash = sub.add_parser('flash', - help='Write C2 firmware image to EEPROM') - p_flash.add_argument('file', help='C2 firmware image (.bin)') - p_flash.add_argument('--dry-run', action='store_true', - help='Show what would happen without writing') - p_flash.add_argument('--no-backup', action='store_true', - help='Skip pre-flash EEPROM backup') - p_flash.add_argument('--force', action='store_true', - help='Override VID/PID mismatch check') - - args = parser.parse_args() - - if args.command == 'info': - cmd_info(args) - elif args.command == 'backup': - cmd_backup(args) - elif args.command == 'verify': - cmd_verify(args) - elif args.command == 'flash': - cmd_flash(args) - - -if __name__ == '__main__': - main() +#!/usr/bin/env python3 +""" +Genpix SkyWalker-1 EEPROM firmware flash tool. + +Writes C2-format firmware images to the Cypress FX2 boot EEPROM via +the I2C_WRITE vendor command. + +Protocol: + I2C_WRITE (0x83): wValue=0x51, wIndex=offset, data=bytes + I2C_READ (0x84): wValue=0x51, wIndex=offset, length=chunk_size + +The EEPROM uses Cypress C2 IIC boot format: + - Header: C2 VID_L VID_H PID_L PID_H DID_L DID_H CONFIG + - Records: LEN_H LEN_L ADDR_H ADDR_L DATA[LEN] + - End: 80 01 ENTRY_H ENTRY_L (reset vector) + +WARNING: Flashing incorrect firmware can brick the device. The FX2 +boots from this EEPROM on power-up -- a corrupted image means the +device will not enumerate on USB until the EEPROM is reprogrammed +with an external programmer or the FX2 boot ROM's A0 vendor request. +""" +import usb.core, usb.util, sys, struct, time, os + +VENDOR_ID = 0x09C0 +PRODUCT_ID = 0x0203 +I2C_WRITE = 0x83 +I2C_READ = 0x84 +EEPROM_SLAVE = 0x51 + +# EEPROM page write parameters +PAGE_SIZE = 16 # Conservative page size for 24Cxx EEPROMs +WRITE_CYCLE_MS = 10 # Max internal write cycle time per page +MAX_EEPROM_SIZE = 16384 # 128Kbit / 16KB max addressable via wIndex + + +# -- Intel HEX parser (shared with fw_load.py) -- + +def parse_ihx(data): + """Parse an Intel HEX file. Returns list of (address, bytes) segments.""" + segments = [] + base_addr = 0 + + for raw_line in data.splitlines(): + line = raw_line.strip() + if not line: + continue + if isinstance(line, bytes): + line = line.decode('ascii', errors='replace') + if not line.startswith(':'): + continue + + hex_str = line[1:] + if len(hex_str) < 10: + continue + try: + raw = bytes.fromhex(hex_str) + except ValueError: + continue + + byte_count = raw[0] + addr = (raw[1] << 8) | raw[2] + rec_type = raw[3] + rec_data = raw[4:4 + byte_count] + + if rec_type == 0x00: + full_addr = base_addr + addr + segments.append((full_addr, bytes(rec_data))) + elif rec_type == 0x01: + break + elif rec_type == 0x02: + base_addr = ((rec_data[0] << 8) | rec_data[1]) << 4 + elif rec_type == 0x04: + base_addr = ((rec_data[0] << 8) | rec_data[1]) << 16 + + return segments + + +def coalesce_segments(segments): + """Merge adjacent/overlapping segments into contiguous blocks.""" + if not segments: + return [] + + sorted_segs = sorted(segments, key=lambda s: s[0]) + merged = [] + cur_addr, cur_data = sorted_segs[0] + cur_data = bytearray(cur_data) + + for addr, data in sorted_segs[1:]: + cur_end = cur_addr + len(cur_data) + if addr <= cur_end: + overlap = cur_end - addr + if overlap >= 0: + cur_data.extend(data[overlap:] if overlap < len(data) else b'') + else: + merged.append((cur_addr, bytes(cur_data))) + cur_addr = addr + cur_data = bytearray(data) + + merged.append((cur_addr, bytes(cur_data))) + return merged + + +def create_c2_image(segments, vid=0x09C0, pid=0x0203, did=0x0000, config=0x40): + """Create a Cypress C2 EEPROM boot image from code segments. + + C2 format: + Header (8 bytes): C2 VID_L VID_H PID_L PID_H DID_L DID_H CONFIG + Records: LEN_H LEN_L ADDR_H ADDR_L DATA[LEN] + End: 80 01 E6 00 00 (write CPUCS=0x00 to release CPU) + + CONFIG byte: + bit 6: 1 = 400kHz I2C (used during EEPROM load) + bit 2: 1 = disconnect (don't drive I2C after load) + """ + image = bytearray() + + # Header + image.append(0xC2) + image.append(vid & 0xFF) + image.append((vid >> 8) & 0xFF) + image.append(pid & 0xFF) + image.append((pid >> 8) & 0xFF) + image.append(did & 0xFF) + image.append((did >> 8) & 0xFF) + image.append(config & 0xFF) + + # Data records — filter out SFR region (0xE000+) + skipped = 0 + for addr, data in segments: + if addr >= 0xE000: + skipped += len(data) + continue + # Truncate if segment extends into SFR region + end = addr + len(data) + if end > 0xE000: + data = data[:0xE000 - addr] + skipped += end - 0xE000 + + length = len(data) + if length == 0: + continue + + # Split large segments (boot ROM may have record size limits) + chunk_max = 1023 # conservative limit + offset = 0 + while offset < length: + chunk_len = min(chunk_max, length - offset) + chunk_addr = addr + offset + image.append((chunk_len >> 8) & 0xFF) + image.append(chunk_len & 0xFF) + image.append((chunk_addr >> 8) & 0xFF) + image.append(chunk_addr & 0xFF) + image.extend(data[offset:offset + chunk_len]) + offset += chunk_len + + # End marker: write 0x00 to CPUCS (0xE600) + image.extend([0x80, 0x01, 0xE6, 0x00, 0x00]) + + return bytes(image), skipped + + +def find_device(): + dev = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID) + if dev is None: + print("SkyWalker-1 not found") + sys.exit(1) + return dev + + +def detach_driver(dev): + intf_num = None + for cfg in dev: + for intf in cfg: + if dev.is_kernel_driver_active(intf.bInterfaceNumber): + try: + dev.detach_kernel_driver(intf.bInterfaceNumber) + intf_num = intf.bInterfaceNumber + except usb.core.USBError as e: + print(f"Cannot detach driver: {e}") + print("Try: sudo modprobe -r dvb_usb_gp8psk") + sys.exit(1) + try: + dev.set_configuration() + except: + pass + return intf_num + + +def eeprom_read(dev, offset, length=64): + """Read from EEPROM at given offset.""" + return dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, + I2C_READ, EEPROM_SLAVE, offset, length, 2000) + + +def eeprom_write(dev, offset, data): + """Write data to EEPROM at given offset. Caller handles page alignment.""" + return dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_OUT, + I2C_WRITE, EEPROM_SLAVE, offset, data, 2000) + + +def eeprom_read_all(dev, size, label="Reading"): + """Read entire EEPROM contents up to size bytes.""" + chunk_size = 64 + data = bytearray() + for offset in range(0, size, chunk_size): + remaining = min(chunk_size, size - offset) + chunk = eeprom_read(dev, offset, remaining) + if chunk is None: + print(f"\n Read failed at offset 0x{offset:04X}") + return None + data.extend(bytes(chunk)) + if offset % 1024 == 0: + pct = offset * 100 // size + print(f"\r {label}: 0x{offset:04X} / 0x{size:04X} [{pct:3d}%]", + end="", flush=True) + print(f"\r {label}: 0x{size:04X} / 0x{size:04X} [100%] ") + return data + + +def parse_c2_header(data): + """Parse Cypress C2 boot EEPROM header. Returns dict or None.""" + if len(data) < 8: + return None + if data[0] != 0xC2: + return None + + vid = data[2] << 8 | data[1] + pid = data[4] << 8 | data[3] + did = data[6] << 8 | data[5] + config = data[7] + + return {"vid": vid, "pid": pid, "did": did, "config": config} + + +def parse_records(data, offset=8): + """Parse C2 load records from EEPROM data.""" + records = [] + while offset < len(data) - 4: + rec_len = (data[offset] << 8) | data[offset + 1] + rec_addr = (data[offset + 2] << 8) | data[offset + 3] + + if rec_len == 0x8001: + records.append({ + "type": "end", + "entry_point": rec_addr, + "offset": offset + }) + break + elif rec_len == 0 or rec_len > 0x4000: + records.append({ + "type": "invalid", + "raw_len": rec_len, + "offset": offset + }) + break + + rec_data = data[offset + 4:offset + 4 + rec_len] + records.append({ + "type": "data", + "length": rec_len, + "load_addr": rec_addr, + "data": bytes(rec_data), + "offset": offset + }) + offset += 4 + rec_len + + return records + + +def print_c2_header(header, prefix=" "): + """Display parsed C2 header fields.""" + print(f"{prefix}Format: C2 (Large EEPROM, code loads to internal RAM)") + print(f"{prefix}VID: 0x{header['vid']:04X}" + f" {'(Genpix)' if header['vid'] == 0x09C0 else ''}") + print(f"{prefix}PID: 0x{header['pid']:04X}" + f" {'(SkyWalker-1)' if header['pid'] == 0x0203 else ''}") + print(f"{prefix}DID: 0x{header['did']:04X}") + print(f"{prefix}Config: 0x{header['config']:02X}", end="") + + config_flags = [] + if header["config"] & 0x40: + config_flags.append("400kHz I2C") + if header["config"] & 0x04: + config_flags.append("disconnect") + if config_flags: + print(f" ({', '.join(config_flags)})") + else: + print() + + +def print_c2_records(records, prefix=" "): + """Display parsed C2 load records.""" + total_code = 0 + for i, rec in enumerate(records): + if rec["type"] == "data": + end_addr = rec["load_addr"] + rec["length"] - 1 + preview = rec["data"][:8].hex(' ') + print(f"{prefix}[{i}] {rec['length']:5d} bytes -> " + f"0x{rec['load_addr']:04X}-0x{end_addr:04X} " + f"[{preview}...]") + total_code += rec["length"] + elif rec["type"] == "end": + print(f"{prefix}[{i}] END MARKER -> entry point: " + f"0x{rec['entry_point']:04X}") + else: + print(f"{prefix}[{i}] INVALID (raw_len=0x{rec['raw_len']:04X}) " + f"at EEPROM offset 0x{rec['offset']:04X}") + + data_recs = [r for r in records if r["type"] == "data"] + print(f"\n{prefix}Total firmware: {total_code} bytes in " + f"{len(data_recs)} segments") + end_recs = [r for r in records if r["type"] == "end"] + if end_recs: + print(f"{prefix}Entry point: 0x{end_recs[0]['entry_point']:04X} " + f"(LJMP target after boot)") + + +def validate_c2_image(data, label="image"): + """Validate a C2 firmware image. Returns (header, records) or exits.""" + if len(data) < 12: + print(f" {label}: too small ({len(data)} bytes, need at least 12)") + return None, None + + if data[0] != 0xC2: + print(f" {label}: not a C2 image (first byte: 0x{data[0]:02X}, " + f"expected 0xC2)") + return None, None + + header = parse_c2_header(data) + if header is None: + print(f" {label}: failed to parse C2 header") + return None, None + + records = parse_records(data) + if not records: + print(f" {label}: no load records found") + return None, None + + end_recs = [r for r in records if r["type"] == "end"] + invalid_recs = [r for r in records if r["type"] == "invalid"] + if not end_recs: + print(f" {label}: WARNING -- no end marker found") + if invalid_recs: + print(f" {label}: WARNING -- {len(invalid_recs)} invalid record(s)") + + return header, records + + +def cmd_info(args): + """Parse and display C2 header info from a .bin file.""" + if not os.path.exists(args.file): + print(f"File not found: {args.file}") + sys.exit(1) + + with open(args.file, 'rb') as f: + data = f.read() + + print(f"C2 Image: {args.file}") + print(f"File size: {len(data)} bytes") + print("=" * 40) + + header, records = validate_c2_image(data, args.file) + if header is None: + sys.exit(1) + + print("\nHeader:") + print_c2_header(header) + + print("\nLoad Records:") + print_c2_records(records) + + # Compute EEPROM usage (header + record headers + data + end marker) + if records: + last = records[-1] + if last["type"] == "end": + eeprom_end = last["offset"] + 4 + elif last["type"] == "data": + eeprom_end = last["offset"] + 4 + last["length"] + else: + eeprom_end = last["offset"] + print(f"\n EEPROM footprint: {eeprom_end} bytes " + f"(0x{eeprom_end:04X})") + + +def cmd_backup(args): + """Dump current EEPROM contents to a file.""" + print("Genpix SkyWalker-1 EEPROM Backup") + print("=" * 40) + + dev = find_device() + print(f"Found device: Bus {dev.bus} Addr {dev.address}") + intf = detach_driver(dev) + + try: + size = args.max_size + print(f"\nReading EEPROM ({size} bytes)...") + data = eeprom_read_all(dev, size) + if data is None: + print("Backup failed: read error") + sys.exit(1) + + with open(args.output, 'wb') as f: + f.write(data) + print(f" Saved to: {args.output}") + + # Show header info + header = parse_c2_header(data) + if header: + print("\nHeader:") + print_c2_header(header) + finally: + if intf is not None: + try: + usb.util.release_interface(dev, intf) + dev.attach_kernel_driver(intf) + print("\nRe-attached kernel driver") + except: + print("\nNote: run 'sudo modprobe dvb_usb_gp8psk' to reload") + + +def cmd_verify(args): + """Compare a .bin file against current EEPROM contents.""" + if not os.path.exists(args.file): + print(f"File not found: {args.file}") + sys.exit(1) + + with open(args.file, 'rb') as f: + image = f.read() + + print("Genpix SkyWalker-1 EEPROM Verify") + print("=" * 40) + + # Validate the image first + header, records = validate_c2_image(image, args.file) + if header is None: + sys.exit(1) + + print(f"\nImage: {args.file} ({len(image)} bytes)") + print_c2_header(header) + + dev = find_device() + print(f"\nFound device: Bus {dev.bus} Addr {dev.address}") + intf = detach_driver(dev) + + try: + print(f"\nReading EEPROM ({len(image)} bytes)...") + eeprom = eeprom_read_all(dev, len(image), label="Verify") + if eeprom is None: + print("Verify failed: read error") + sys.exit(1) + + # Compare byte-by-byte + mismatches = [] + for i in range(len(image)): + if i < len(eeprom) and image[i] != eeprom[i]: + mismatches.append(i) + + if not mismatches: + print(f"\n MATCH -- EEPROM contents match {args.file}") + else: + print(f"\n MISMATCH -- {len(mismatches)} byte(s) differ:") + for off in mismatches[:32]: + exp = image[off] + got = eeprom[off] if off < len(eeprom) else 0xFF + print(f" 0x{off:04X}: expected 0x{exp:02X}, " + f"got 0x{got:02X}") + if len(mismatches) > 32: + print(f" ... and {len(mismatches) - 32} more") + sys.exit(1) + finally: + if intf is not None: + try: + usb.util.release_interface(dev, intf) + dev.attach_kernel_driver(intf) + print("\nRe-attached kernel driver") + except: + print("\nNote: run 'sudo modprobe dvb_usb_gp8psk' to reload") + + +def cmd_convert(args): + """Convert Intel HEX (.ihx) to Cypress C2 EEPROM format (.bin).""" + if not os.path.exists(args.file): + print(f"File not found: {args.file}") + sys.exit(1) + + with open(args.file, 'rb') as f: + raw = f.read() + + print("Genpix SkyWalker-1 IHX → C2 Converter") + print("=" * 40) + + # Parse IHX + segments = parse_ihx(raw) + if not segments: + print(" No code segments found in IHX file") + sys.exit(1) + segments = coalesce_segments(segments) + + total_code = sum(len(d) for _, d in segments) + min_addr = min(a for a, _ in segments) + max_addr = max(a + len(d) - 1 for a, d in segments) + print(f"\nInput: {args.file}") + print(f" Segments: {len(segments)}") + print(f" Code size: {total_code} bytes") + print(f" Address: 0x{min_addr:04X} - 0x{max_addr:04X}") + + # Create C2 image + vid = int(args.vid, 0) if args.vid else VENDOR_ID + pid = int(args.pid, 0) if args.pid else PRODUCT_ID + did = int(args.did, 0) if args.did else 0x0000 + config = int(args.config, 0) if args.config else 0x40 + + image, skipped = create_c2_image(segments, vid, pid, did, config) + if skipped: + print(f" Skipped: {skipped} bytes (SFR region 0xE000+)") + + # Validate the image we just created + header, records = validate_c2_image(image, "generated") + if header is None: + print(" INTERNAL ERROR: generated image failed validation") + sys.exit(1) + + print(f"\nOutput: {args.output}") + print(f" Image size: {len(image)} bytes") + print(f" EEPROM use: {len(image) * 100 / MAX_EEPROM_SIZE:.1f}% " + f"of {MAX_EEPROM_SIZE} bytes") + + print("\nC2 Header:") + print_c2_header(header) + + print("\nLoad Records:") + print_c2_records(records) + + if len(image) > MAX_EEPROM_SIZE: + print(f"\n WARNING: image ({len(image)} bytes) exceeds EEPROM " + f"capacity ({MAX_EEPROM_SIZE} bytes)") + sys.exit(1) + + with open(args.output, 'wb') as f: + f.write(image) + + print(f"\n Written: {args.output} ({len(image)} bytes)") + print(f" Flash with: python {sys.argv[0]} flash {args.output}") + + +def cmd_flash(args): + """Write a C2-format .bin file to the EEPROM.""" + if not os.path.exists(args.file): + print(f"File not found: {args.file}") + sys.exit(1) + + with open(args.file, 'rb') as f: + image = f.read() + + print("Genpix SkyWalker-1 EEPROM Flash") + print("=" * 40) + print() + print(" *** FIRMWARE FLASH -- READ CAREFULLY ***") + print(" Writing bad firmware will brick the device.") + print(" The SkyWalker-1 boots from this EEPROM on power-up.") + print(" A corrupted image = no USB enumeration.") + print() + + # Validate input image + img_header, img_records = validate_c2_image(image, args.file) + if img_header is None: + sys.exit(1) + + print(f"Image: {args.file} ({len(image)} bytes)") + print_c2_header(img_header) + + # Size sanity check + if len(image) > MAX_EEPROM_SIZE: + print(f"\n Image too large: {len(image)} bytes " + f"(max {MAX_EEPROM_SIZE})") + sys.exit(1) + + if len(image) < 12: + print(f"\n Image too small: {len(image)} bytes") + sys.exit(1) + + # Connect to device + dev = find_device() + print(f"\nFound device: Bus {dev.bus} Addr {dev.address}") + intf = detach_driver(dev) + + try: + # Check VID/PID against the connected device + if not args.force: + if img_header["vid"] != VENDOR_ID: + print(f"\n VID mismatch: image has 0x{img_header['vid']:04X}," + f" device is 0x{VENDOR_ID:04X}") + print(" Use --force to override") + sys.exit(1) + if img_header["pid"] != PRODUCT_ID: + print(f"\n PID mismatch: image has 0x{img_header['pid']:04X}," + f" device is 0x{PRODUCT_ID:04X}") + print(" Use --force to override") + sys.exit(1) + elif img_header["vid"] != VENDOR_ID or img_header["pid"] != PRODUCT_ID: + print(f"\n WARNING: VID/PID mismatch (--force active)") + print(f" Image: VID=0x{img_header['vid']:04X} " + f"PID=0x{img_header['pid']:04X}") + print(f" Device: VID=0x{VENDOR_ID:04X} PID=0x{PRODUCT_ID:04X}") + + # Backup current EEPROM + if not args.no_backup: + ts = time.strftime("%Y%m%d_%H%M%S") + backup_file = f"eeprom_backup_{ts}.bin" + print(f"\nBacking up current EEPROM to {backup_file}...") + backup = eeprom_read_all(dev, MAX_EEPROM_SIZE, label="Backup") + if backup is None: + print(" Backup failed: read error. Aborting.") + sys.exit(1) + with open(backup_file, 'wb') as f: + f.write(backup) + print(f" Backup saved: {backup_file} ({len(backup)} bytes)") + + # Show what's currently on the EEPROM + cur_header = parse_c2_header(backup) + if cur_header: + print("\n Current EEPROM:") + print_c2_header(cur_header, prefix=" ") + else: + print("\n Skipping backup (--no-backup)") + + # Dry-run stops here + if args.dry_run: + print("\n DRY RUN -- would write {0} bytes in {1} pages".format( + len(image), (len(image) + PAGE_SIZE - 1) // PAGE_SIZE)) + print(" No changes made.") + return + + # Final confirmation + print(f"\nAbout to write {len(image)} bytes to EEPROM...") + print(" Press Ctrl+C within 3 seconds to abort.") + try: + for i in range(3, 0, -1): + print(f"\r Writing in {i}... ", end="", flush=True) + time.sleep(1) + print("\r Writing now... ") + except KeyboardInterrupt: + print("\n Aborted.") + return + + # Write in page-sized chunks + total_pages = (len(image) + PAGE_SIZE - 1) // PAGE_SIZE + write_errors = 0 + + for page_num in range(total_pages): + offset = page_num * PAGE_SIZE + end = min(offset + PAGE_SIZE, len(image)) + chunk = image[offset:end] + + pct = (page_num + 1) * 100 // total_pages + print(f"\r Write: 0x{offset:04X} / 0x{len(image):04X} " + f"[{pct:3d}%]", end="", flush=True) + + try: + written = eeprom_write(dev, offset, chunk) + if written != len(chunk): + print(f"\n Short write at 0x{offset:04X}: " + f"sent {len(chunk)}, wrote {written}") + write_errors += 1 + except usb.core.USBError as e: + print(f"\n Write error at 0x{offset:04X}: {e}") + write_errors += 1 + + # Wait for EEPROM internal write cycle + time.sleep(WRITE_CYCLE_MS / 1000.0) + + print(f"\r Write: 0x{len(image):04X} / 0x{len(image):04X} " + f"[100%] ") + + if write_errors: + print(f"\n WARNING: {write_errors} write error(s) occurred") + + # Verify by reading back + print(f"\nVerifying ({len(image)} bytes)...") + verify = eeprom_read_all(dev, len(image), label="Verify") + if verify is None: + print(" Verify failed: read error") + print(" *** EEPROM STATE UNKNOWN -- check before power cycling ***") + sys.exit(1) + + mismatches = [] + for i in range(len(image)): + if i < len(verify) and image[i] != verify[i]: + mismatches.append(i) + + if not mismatches: + print(f"\n VERIFIED -- all {len(image)} bytes match") + print(" Flash complete. Power cycle the device to boot new firmware.") + else: + print(f"\n VERIFY FAILED -- {len(mismatches)} byte(s) differ:") + for off in mismatches[:16]: + exp = image[off] + got = verify[off] if off < len(verify) else 0xFF + print(f" 0x{off:04X}: wrote 0x{exp:02X}, " + f"read 0x{got:02X}") + if len(mismatches) > 16: + print(f" ... and {len(mismatches) - 16} more") + print("\n *** EEPROM CONTENTS DO NOT MATCH IMAGE ***") + print(" Do NOT power cycle until this is resolved.") + sys.exit(1) + + finally: + if intf is not None: + try: + usb.util.release_interface(dev, intf) + dev.attach_kernel_driver(intf) + print("\nRe-attached kernel driver") + except: + print("\nNote: run 'sudo modprobe dvb_usb_gp8psk' to reload") + + +def main(): + import argparse + parser = argparse.ArgumentParser( + description="SkyWalker-1 EEPROM firmware flash tool") + sub = parser.add_subparsers(dest='command', required=True) + + # info + p_info = sub.add_parser('info', + help='Parse and display C2 header from a .bin file') + p_info.add_argument('file', help='C2 firmware image (.bin)') + + # backup + p_backup = sub.add_parser('backup', + help='Dump current EEPROM to a file') + p_backup.add_argument('-o', '--output', default='skywalker1_eeprom.bin', + help='Output file (default: skywalker1_eeprom.bin)') + p_backup.add_argument('--max-size', type=int, default=MAX_EEPROM_SIZE, + help=f'Bytes to read (default: {MAX_EEPROM_SIZE})') + + # verify + p_verify = sub.add_parser('verify', + help='Compare .bin file against EEPROM') + p_verify.add_argument('file', help='C2 firmware image (.bin)') + + # convert + p_convert = sub.add_parser('convert', + help='Convert Intel HEX (.ihx) to C2 EEPROM format') + p_convert.add_argument('file', help='Input firmware file (.ihx or .hex)') + p_convert.add_argument('-o', '--output', default=None, + help='Output C2 image (.bin). Default: _eeprom.bin') + p_convert.add_argument('--vid', default=None, + help=f'USB VID (default: 0x{VENDOR_ID:04X})') + p_convert.add_argument('--pid', default=None, + help=f'USB PID (default: 0x{PRODUCT_ID:04X})') + p_convert.add_argument('--did', default=None, + help='USB DID (default: 0x0000)') + p_convert.add_argument('--config', default=None, + help='C2 CONFIG byte (default: 0x40 = 400kHz I2C)') + + # flash + p_flash = sub.add_parser('flash', + help='Write C2 firmware image to EEPROM') + p_flash.add_argument('file', help='C2 firmware image (.bin)') + p_flash.add_argument('--dry-run', action='store_true', + help='Show what would happen without writing') + p_flash.add_argument('--no-backup', action='store_true', + help='Skip pre-flash EEPROM backup') + p_flash.add_argument('--force', action='store_true', + help='Override VID/PID mismatch check') + + args = parser.parse_args() + + # Default output filename for convert + if args.command == 'convert' and args.output is None: + base = os.path.splitext(args.file)[0] + args.output = base + '_eeprom.bin' + + if args.command == 'info': + cmd_info(args) + elif args.command == 'backup': + cmd_backup(args) + elif args.command == 'verify': + cmd_verify(args) + elif args.command == 'convert': + cmd_convert(args) + elif args.command == 'flash': + cmd_flash(args) + + +if __name__ == '__main__': + main() diff --git a/tools/fw_load.py b/tools/fw_load.py index 144a0dc..32136ef 100755 --- a/tools/fw_load.py +++ b/tools/fw_load.py @@ -1,616 +1,980 @@ -#!/usr/bin/env python3 -""" -Genpix SkyWalker-1 RAM firmware loader. - -Loads firmware into the Cypress FX2 (CY7C68013A) internal/external RAM -via the standard 0xA0 vendor request. This does NOT touch the EEPROM -- -power-cycling the device restores the factory-programmed firmware. - -Use case: firmware development and testing. Load, test, power-cycle. - -Loading sequence: - 1. Halt CPU: write 0x01 to CPUCS register at 0xE600 - 2. Write code segments into RAM - 3. Start CPU: write 0x00 to CPUCS at 0xE600 - -After starting, the FX2 runs the new firmware and typically -re-enumerates on USB with new VID/PID/descriptors. - -Supports Intel HEX (.ihx/.hex) and raw binary (.bix/.bin) formats. -""" - -import sys -import argparse -import time -import os - -try: - import usb.core - import usb.util -except ImportError: - print("pyusb required: pip install pyusb") - sys.exit(1) - -# Genpix SkyWalker-1 -SKYWALKER_VID = 0x09C0 -SKYWALKER_PID = 0x0203 - -# Bare/unprogrammed Cypress FX2 (no EEPROM or blank EEPROM) -CYPRESS_VID = 0x04B4 -CYPRESS_PID = 0x8613 - -# FX2 vendor request for RAM access (built into silicon boot ROM) -FX2_RAM_REQUEST = 0xA0 - -# CPUCS register -- controls 8051 run/halt state -CPUCS_ADDR = 0xE600 - -# Max bytes per control transfer. The FX2 TRM says 64 bytes for -# the control endpoint buffer, so we stay conservative. -CHUNK_SIZE = 64 - - -def find_device(force=False): - """Find a SkyWalker-1 or bare FX2 device on USB.""" - # Try SkyWalker-1 first - dev = usb.core.find(idVendor=SKYWALKER_VID, idProduct=SKYWALKER_PID) - if dev is not None: - print(f"Found SkyWalker-1: Bus {dev.bus} Addr {dev.address} " - f"(VID 0x{SKYWALKER_VID:04X} PID 0x{SKYWALKER_PID:04X})") - return dev - - # Try bare Cypress FX2 - dev = usb.core.find(idVendor=CYPRESS_VID, idProduct=CYPRESS_PID) - if dev is not None: - print(f"Found bare Cypress FX2: Bus {dev.bus} Addr {dev.address} " - f"(VID 0x{CYPRESS_VID:04X} PID 0x{CYPRESS_PID:04X})") - return dev - - if force: - # Last resort: scan for any device the user might want - print("No SkyWalker-1 or bare FX2 found. --force is set but no " - "target device discovered.") - else: - print("No SkyWalker-1 (09C0:0203) or bare FX2 (04B4:8613) found.") - print("Is the device plugged in?") - sys.exit(1) - - -def detach_driver(dev): - """Detach kernel driver if attached. Returns interface number or None.""" - intf_num = None - for cfg in dev: - for intf in cfg: - if dev.is_kernel_driver_active(intf.bInterfaceNumber): - try: - dev.detach_kernel_driver(intf.bInterfaceNumber) - intf_num = intf.bInterfaceNumber - except usb.core.USBError as e: - print(f"Cannot detach driver: {e}") - print("Try: sudo modprobe -r dvb_usb_gp8psk") - sys.exit(1) - try: - dev.set_configuration() - except: - pass - return intf_num - - -def fx2_ram_write(dev, addr, data): - """Write bytes to FX2 RAM at the given address via vendor request 0xA0.""" - return dev.ctrl_transfer( - usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_OUT, - FX2_RAM_REQUEST, addr, 0, data, 2000) - - -def fx2_ram_read(dev, addr, length): - """Read bytes from FX2 RAM at the given address via vendor request 0xA0.""" - try: - data = dev.ctrl_transfer( - usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, - FX2_RAM_REQUEST, addr, 0, length, 2000) - return bytes(data) - except usb.core.USBError: - return None - - -def cpu_halt(dev): - """Halt the FX2 8051 CPU by writing 0x01 to CPUCS.""" - fx2_ram_write(dev, CPUCS_ADDR, bytes([0x01])) - - -def cpu_start(dev): - """Start the FX2 8051 CPU by writing 0x00 to CPUCS.""" - fx2_ram_write(dev, CPUCS_ADDR, bytes([0x00])) - - -# -- Intel HEX parser -- - -def parse_ihx(data): - """ - Parse an Intel HEX file. Returns list of (address, bytes) segments. - - Record types: - 00 = data - 01 = EOF - 02 = extended segment address (shifts base by 16) - 04 = extended linear address (shifts base by 16) - """ - segments = [] - base_addr = 0 - line_num = 0 - - for raw_line in data.splitlines(): - line_num += 1 - line = raw_line.strip() - if not line: - continue - if isinstance(line, bytes): - line = line.decode('ascii', errors='replace') - - if not line.startswith(':'): - raise ValueError(f"Line {line_num}: missing start code ':'") - - # Strip the colon and decode hex - hex_str = line[1:] - if len(hex_str) < 10: - raise ValueError(f"Line {line_num}: too short") - - try: - raw = bytes.fromhex(hex_str) - except ValueError: - raise ValueError(f"Line {line_num}: invalid hex") - - byte_count = raw[0] - addr = (raw[1] << 8) | raw[2] - rec_type = raw[3] - rec_data = raw[4:4 + byte_count] - checksum = raw[4 + byte_count] - - # Verify checksum (two's complement of sum of all bytes before it) - calc_sum = sum(raw[:4 + byte_count]) & 0xFF - calc_check = (~calc_sum + 1) & 0xFF - if checksum != calc_check: - raise ValueError( - f"Line {line_num}: checksum mismatch " - f"(expected 0x{calc_check:02X}, got 0x{checksum:02X})") - - if len(rec_data) != byte_count: - raise ValueError( - f"Line {line_num}: data length mismatch " - f"(header says {byte_count}, got {len(rec_data)})") - - if rec_type == 0x00: - # Data record - full_addr = base_addr + addr - segments.append((full_addr, bytes(rec_data))) - - elif rec_type == 0x01: - # EOF - break - - elif rec_type == 0x02: - # Extended segment address - if byte_count != 2: - raise ValueError( - f"Line {line_num}: type 02 record must have 2 data bytes") - base_addr = ((rec_data[0] << 8) | rec_data[1]) << 4 - - elif rec_type == 0x04: - # Extended linear address - if byte_count != 2: - raise ValueError( - f"Line {line_num}: type 04 record must have 2 data bytes") - base_addr = ((rec_data[0] << 8) | rec_data[1]) << 16 - - # Silently ignore unknown record types (03, 05, etc.) - - return segments - - -def coalesce_segments(segments): - """ - Merge adjacent/overlapping segments into contiguous blocks. - Returns list of (address, bytes) with no gaps. - """ - if not segments: - return [] - - # Sort by address - sorted_segs = sorted(segments, key=lambda s: s[0]) - - merged = [] - cur_addr, cur_data = sorted_segs[0] - cur_data = bytearray(cur_data) - - for addr, data in sorted_segs[1:]: - cur_end = cur_addr + len(cur_data) - if addr <= cur_end: - # Overlapping or adjacent -- extend or overwrite - overlap = cur_end - addr - if overlap >= 0: - cur_data.extend(data[overlap:] if overlap < len(data) else b'') - else: - # Gap -- pad with zeros (shouldn't happen after sort, but safe) - cur_data.extend(b'\x00' * (-overlap)) - cur_data.extend(data) - else: - merged.append((cur_addr, bytes(cur_data))) - cur_addr = addr - cur_data = bytearray(data) - - merged.append((cur_addr, bytes(cur_data))) - return merged - - -def load_firmware_file(path): - """ - Load firmware from .ihx/.hex (Intel HEX) or .bix/.bin (raw binary). - Returns list of (address, bytes) segments. - """ - ext = os.path.splitext(path)[1].lower() - - with open(path, 'rb') as f: - raw = f.read() - - if ext in ('.ihx', '.hex'): - segments = parse_ihx(raw) - segments = coalesce_segments(segments) - return segments - - elif ext in ('.bix', '.bin'): - # Raw binary loads at address 0x0000 - if not raw: - print(f"Empty file: {path}") - sys.exit(1) - return [(0x0000, raw)] - - else: - # Try to auto-detect: if it starts with ':', assume Intel HEX - if raw.startswith(b':'): - segments = parse_ihx(raw) - segments = coalesce_segments(segments) - return segments - else: - # Treat as raw binary - return [(0x0000, raw)] - - -def write_segments(dev, segments, verbose=False): - """ - Write firmware segments to FX2 RAM in CHUNK_SIZE pieces. - Returns total bytes written. - """ - total = 0 - - for seg_addr, seg_data in segments: - seg_len = len(seg_data) - seg_end = seg_addr + seg_len - 1 - print(f" 0x{seg_addr:04X}-0x{seg_end:04X} ({seg_len} bytes)") - - offset = 0 - while offset < seg_len: - chunk_len = min(CHUNK_SIZE, seg_len - offset) - chunk = seg_data[offset:offset + chunk_len] - addr = seg_addr + offset - - try: - written = fx2_ram_write(dev, addr, chunk) - if written != chunk_len: - print(f"\n Short write at 0x{addr:04X}: " - f"sent {chunk_len}, wrote {written}") - except usb.core.USBError as e: - print(f"\n Write error at 0x{addr:04X}: {e}") - return total - - if verbose and offset % 0x400 == 0: - pct = offset * 100 // seg_len - print(f"\r 0x{addr:04X} [{pct:3d}%]", end="", flush=True) - - total += chunk_len - offset += chunk_len - - if verbose and seg_len > CHUNK_SIZE: - print(f"\r 0x{seg_addr + seg_len - 1:04X} [100%] ") - - return total - - -# -- Subcommand handlers -- - -def cmd_load(args): - """Load firmware into FX2 RAM.""" - if not os.path.exists(args.file): - print(f"File not found: {args.file}") - sys.exit(1) - - # Parse firmware file - segments = load_firmware_file(args.file) - if not segments: - print("No code segments found in firmware file") - sys.exit(1) - - total_bytes = sum(len(d) for _, d in segments) - min_addr = min(a for a, _ in segments) - max_addr = max(a + len(d) - 1 for a, d in segments) - - print(f"SkyWalker-1 RAM Firmware Loader") - print(f"{'=' * 40}") - print(f"\nFirmware: {args.file}") - print(f" Segments: {len(segments)}") - print(f" Total size: {total_bytes} bytes") - print(f" Address: 0x{min_addr:04X} - 0x{max_addr:04X}") - - # Check for CPUCS region overlap (warn but don't block) - for addr, data in segments: - seg_end = addr + len(data) - 1 - if addr <= CPUCS_ADDR <= seg_end: - print(f"\n WARNING: Segment at 0x{addr:04X}-0x{seg_end:04X} " - f"overlaps CPUCS (0x{CPUCS_ADDR:04X})") - print(f" The CPU halt/start writes to 0xE600 will clobber " - f"this region") - - print() - - # Connect - dev = find_device(force=args.force) - - # Check VID/PID if it's not a known device - vid = dev.idVendor - pid = dev.idProduct - is_skywalker = (vid == SKYWALKER_VID and pid == SKYWALKER_PID) - is_bare_fx2 = (vid == CYPRESS_VID and pid == CYPRESS_PID) - - if not is_skywalker and not is_bare_fx2 and not args.force: - print(f"\n Unknown device VID 0x{vid:04X} PID 0x{pid:04X}") - print(f" Expected SkyWalker-1 (09C0:0203) or bare FX2 (04B4:8613)") - print(f" Use --force to override") - sys.exit(1) - - intf = detach_driver(dev) - - try: - # Step 1: Halt CPU - if not args.no_reset: - print("\n[1/3] Halting CPU (CPUCS = 0x01)...") - cpu_halt(dev) - time.sleep(0.05) - - # Verify halt - readback = fx2_ram_read(dev, CPUCS_ADDR, 1) - if readback and readback[0] & 0x01: - print(" CPU halted") - else: - val = f"0x{readback[0]:02X}" if readback else "read failed" - print(f" WARNING: CPUCS readback = {val} (expected 0x01)") - print(" Proceeding anyway...") - else: - print("\n[1/3] Skipping CPU reset (--no-reset)") - - # Step 2: Load segments - step = "2/3" if not args.no_reset else "2/2" - print(f"\n[{step}] Loading {len(segments)} segment(s) into RAM...") - written = write_segments(dev, segments, verbose=args.verbose) - print(f"\n {written} bytes loaded") - - if written != total_bytes: - print(f" WARNING: expected {total_bytes}, wrote {written}") - - # Step 3: Start CPU - if not args.no_reset: - print(f"\n[3/3] Starting CPU (CPUCS = 0x00)...") - cpu_start(dev) - print(" CPU released") - print(f"\n Firmware is running. The device will re-enumerate") - print(f" with new USB descriptors if the firmware does so.") - - if args.wait: - _wait_for_reenumeration(args.wait) - else: - print(f"\n Segments loaded (CPU not reset)") - - finally: - # Only re-attach if we didn't just start new firmware - # (the device may have already re-enumerated away) - if args.no_reset and intf is not None: - try: - usb.util.release_interface(dev, intf) - dev.attach_kernel_driver(intf) - print("\nRe-attached kernel driver") - except: - pass - - -def _wait_for_reenumeration(timeout): - """Wait for a USB device to re-appear after firmware load.""" - print(f"\n Waiting up to {timeout}s for re-enumeration...") - deadline = time.time() + timeout - time.sleep(1.0) # Give the device a moment to disconnect - - while time.time() < deadline: - # Check for SkyWalker-1 with potentially new VID/PID - # After loading custom firmware, VID/PID may differ - dev = usb.core.find(idVendor=SKYWALKER_VID, idProduct=SKYWALKER_PID) - if dev is not None: - print(f" Device re-appeared: Bus {dev.bus} Addr {dev.address} " - f"(0x{SKYWALKER_VID:04X}:0x{SKYWALKER_PID:04X})") - return - - dev = usb.core.find(idVendor=CYPRESS_VID, idProduct=CYPRESS_PID) - if dev is not None: - print(f" Device re-appeared: Bus {dev.bus} Addr {dev.address} " - f"(0x{CYPRESS_VID:04X}:0x{CYPRESS_PID:04X})") - return - - print(".", end="", flush=True) - time.sleep(0.5) - - print(f"\n Timeout -- device did not re-enumerate within {timeout}s") - print(f" The firmware may use different VID/PID. Check 'lsusb'.") - - -def cmd_reset(args): - """Reset the FX2 CPU (halt then start).""" - print(f"SkyWalker-1 CPU Reset") - print(f"{'=' * 40}") - - dev = find_device(force=args.force) - intf = detach_driver(dev) - - try: - print("\nHalting CPU...") - cpu_halt(dev) - time.sleep(0.05) - print(" CPUCS = 0x01 (halted)") - - time.sleep(0.1) - - print("Starting CPU...") - cpu_start(dev) - print(" CPUCS = 0x00 (running)") - print("\nCPU reset complete. Device will re-enumerate.") - - if args.wait: - _wait_for_reenumeration(args.wait) - finally: - pass # Device is likely gone after reset - - -def cmd_read(args): - """Read and hex-dump FX2 RAM contents.""" - addr = args.addr - length = args.length - - print(f"SkyWalker-1 RAM Read") - print(f"{'=' * 40}") - - dev = find_device(force=args.force) - intf = detach_driver(dev) - - try: - print(f"\nReading {length} bytes from 0x{addr:04X}...\n") - - data = bytearray() - offset = 0 - errors = 0 - - while offset < length: - chunk_len = min(CHUNK_SIZE, length - offset) - chunk = fx2_ram_read(dev, addr + offset, chunk_len) - if chunk is None: - errors += 1 - data.extend(b'\xff' * chunk_len) - else: - data.extend(chunk) - offset += chunk_len - - # Hex dump output - for i in range(0, len(data), 16): - row = data[i:i + 16] - hex_part = ' '.join(f'{b:02X}' for b in row) - ascii_part = ''.join(chr(b) if 0x20 <= b < 0x7F else '.' for b in row) - print(f" {addr + i:04X}: {hex_part:<48s} {ascii_part}") - - print(f"\n {len(data)} bytes read, {errors} chunk errors") - - if args.output: - with open(args.output, 'wb') as f: - f.write(data) - print(f" Saved to: {args.output}") - - finally: - if intf is not None: - try: - usb.util.release_interface(dev, intf) - dev.attach_kernel_driver(intf) - print("\nRe-attached kernel driver") - except: - print("\nNote: run 'sudo modprobe dvb_usb_gp8psk' to reload") - - -def main(): - parser = argparse.ArgumentParser( - description="SkyWalker-1 RAM firmware loader (FX2 vendor request 0xA0)", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog="""\ -examples: - %(prog)s load firmware.ihx - %(prog)s load firmware.bix --wait 5 - %(prog)s load firmware.ihx --no-reset - %(prog)s reset - %(prog)s read --addr 0x0000 --len 256 - %(prog)s read --addr 0xe600 --len 1 - -This tool loads firmware into RAM only -- the EEPROM is never touched. -Power-cycle the device to restore the factory-programmed firmware. -""") - parser.add_argument('-v', '--verbose', action='store_true', - help="Show detailed transfer progress") - parser.add_argument('--force', action='store_true', - help="Allow loading to unknown VID/PID devices") - - sub = parser.add_subparsers(dest='command') - - # load (default) - p_load = sub.add_parser('load', - help='Load firmware into FX2 RAM') - p_load.add_argument('file', help='Firmware file (.ihx, .hex, .bix, .bin)') - p_load.add_argument('--no-reset', action='store_true', - help="Load without halting/starting the CPU") - p_load.add_argument('--wait', type=float, default=0, metavar='SECONDS', - help="Wait for USB re-enumeration after load") - p_load.add_argument('-v', '--verbose', action='store_true', - help="Show detailed transfer progress") - p_load.add_argument('--force', action='store_true', - help="Allow loading to unknown VID/PID devices") - - # reset - p_reset = sub.add_parser('reset', - help='Reset the FX2 CPU (halt then start)') - p_reset.add_argument('--wait', type=float, default=0, metavar='SECONDS', - help="Wait for USB re-enumeration after reset") - p_reset.add_argument('--force', action='store_true', - help="Allow reset on unknown VID/PID devices") - - # read - p_read = sub.add_parser('read', - help='Read and hex-dump FX2 RAM') - p_read.add_argument('--addr', type=lambda x: int(x, 0), default=0x0000, - help="Start address (default: 0x0000)") - p_read.add_argument('--len', dest='length', type=lambda x: int(x, 0), - default=256, - help="Number of bytes to read (default: 256)") - p_read.add_argument('-o', '--output', metavar='FILE', - help="Save raw bytes to file") - p_read.add_argument('--force', action='store_true', - help="Allow read on unknown VID/PID devices") - - args = parser.parse_args() - - # Default to 'load' if a positional arg is given but no subcommand - if not args.command: - parser.print_help() - sys.exit(0) - - # Propagate top-level flags to subcommands - if hasattr(args, 'verbose') and not args.verbose: - args.verbose = parser.parse_args().verbose - if hasattr(args, 'force') and not args.force: - args.force = parser.parse_args().force - - dispatch = { - 'load': cmd_load, - 'reset': cmd_reset, - 'read': cmd_read, - } - - handler = dispatch.get(args.command) - if handler is None: - parser.print_help() - sys.exit(1) - - handler(args) - - -if __name__ == '__main__': - main() +#!/usr/bin/env python3 +""" +Genpix SkyWalker-1 RAM firmware loader. + +Loads firmware into the Cypress FX2 (CY7C68013A) internal/external RAM +via the standard 0xA0 vendor request. This does NOT touch the EEPROM -- +power-cycling the device restores the factory-programmed firmware. + +Use case: firmware development and testing. Load, test, power-cycle. + +Loading sequence: + 1. Halt CPU: write 0x01 to CPUCS register at 0xE600 + 2. Write code segments into RAM + 3. Start CPU: write 0x00 to CPUCS at 0xE600 + +After starting, the FX2 runs the new firmware and typically +re-enumerates on USB with new VID/PID/descriptors. + +Supports Intel HEX (.ihx/.hex) and raw binary (.bix/.bin) formats. +""" + +import sys +import argparse +import time +import os + +try: + import usb.core + import usb.util +except ImportError: + print("pyusb required: pip install pyusb") + sys.exit(1) + +# Genpix SkyWalker-1 +SKYWALKER_VID = 0x09C0 +SKYWALKER_PID = 0x0203 + +# Bare/unprogrammed Cypress FX2 (no EEPROM or blank EEPROM) +CYPRESS_VID = 0x04B4 +CYPRESS_PID = 0x8613 + +# FX2 vendor request for RAM access (built into silicon boot ROM) +FX2_RAM_REQUEST = 0xA0 + +# CPUCS register -- controls 8051 run/halt state +CPUCS_ADDR = 0xE600 + +# Max bytes per control transfer. The FX2 TRM says 64 bytes for +# the control endpoint buffer, so we stay conservative. +CHUNK_SIZE = 64 + + +def find_device(force=False): + """Find a SkyWalker-1 or bare FX2 device on USB.""" + # Try SkyWalker-1 first + dev = usb.core.find(idVendor=SKYWALKER_VID, idProduct=SKYWALKER_PID) + if dev is not None: + print(f"Found SkyWalker-1: Bus {dev.bus} Addr {dev.address} " + f"(VID 0x{SKYWALKER_VID:04X} PID 0x{SKYWALKER_PID:04X})") + return dev + + # Try bare Cypress FX2 + dev = usb.core.find(idVendor=CYPRESS_VID, idProduct=CYPRESS_PID) + if dev is not None: + print(f"Found bare Cypress FX2: Bus {dev.bus} Addr {dev.address} " + f"(VID 0x{CYPRESS_VID:04X} PID 0x{CYPRESS_PID:04X})") + return dev + + if force: + # Last resort: scan for any device the user might want + print("No SkyWalker-1 or bare FX2 found. --force is set but no " + "target device discovered.") + else: + print("No SkyWalker-1 (09C0:0203) or bare FX2 (04B4:8613) found.") + print("Is the device plugged in?") + sys.exit(1) + + +def detach_driver(dev): + """Detach kernel driver if attached. Returns interface number or None.""" + intf_num = None + for cfg in dev: + for intf in cfg: + if dev.is_kernel_driver_active(intf.bInterfaceNumber): + try: + dev.detach_kernel_driver(intf.bInterfaceNumber) + intf_num = intf.bInterfaceNumber + except usb.core.USBError as e: + print(f"Cannot detach driver: {e}") + print("Try: sudo modprobe -r dvb_usb_gp8psk") + sys.exit(1) + try: + dev.set_configuration() + except: + pass + return intf_num + + +def fx2_ram_write(dev, addr, data): + """Write bytes to FX2 RAM at the given address via vendor request 0xA0.""" + return dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_OUT, + FX2_RAM_REQUEST, addr, 0, data, 2000) + + +def fx2_ram_read(dev, addr, length): + """Read bytes from FX2 RAM at the given address via vendor request 0xA0.""" + try: + data = dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, + FX2_RAM_REQUEST, addr, 0, length, 2000) + return bytes(data) + except usb.core.USBError: + return None + + +def cpu_halt(dev): + """Halt the FX2 8051 CPU by writing 0x01 to CPUCS.""" + fx2_ram_write(dev, CPUCS_ADDR, bytes([0x01])) + + +def cpu_start(dev): + """Start the FX2 8051 CPU by writing 0x00 to CPUCS.""" + fx2_ram_write(dev, CPUCS_ADDR, bytes([0x00])) + + +# -- Intel HEX parser -- + +def parse_ihx(data): + """ + Parse an Intel HEX file. Returns list of (address, bytes) segments. + + Record types: + 00 = data + 01 = EOF + 02 = extended segment address (shifts base by 16) + 04 = extended linear address (shifts base by 16) + """ + segments = [] + base_addr = 0 + line_num = 0 + + for raw_line in data.splitlines(): + line_num += 1 + line = raw_line.strip() + if not line: + continue + if isinstance(line, bytes): + line = line.decode('ascii', errors='replace') + + if not line.startswith(':'): + raise ValueError(f"Line {line_num}: missing start code ':'") + + # Strip the colon and decode hex + hex_str = line[1:] + if len(hex_str) < 10: + raise ValueError(f"Line {line_num}: too short") + + try: + raw = bytes.fromhex(hex_str) + except ValueError: + raise ValueError(f"Line {line_num}: invalid hex") + + byte_count = raw[0] + addr = (raw[1] << 8) | raw[2] + rec_type = raw[3] + rec_data = raw[4:4 + byte_count] + checksum = raw[4 + byte_count] + + # Verify checksum (two's complement of sum of all bytes before it) + calc_sum = sum(raw[:4 + byte_count]) & 0xFF + calc_check = (~calc_sum + 1) & 0xFF + if checksum != calc_check: + raise ValueError( + f"Line {line_num}: checksum mismatch " + f"(expected 0x{calc_check:02X}, got 0x{checksum:02X})") + + if len(rec_data) != byte_count: + raise ValueError( + f"Line {line_num}: data length mismatch " + f"(header says {byte_count}, got {len(rec_data)})") + + if rec_type == 0x00: + # Data record + full_addr = base_addr + addr + segments.append((full_addr, bytes(rec_data))) + + elif rec_type == 0x01: + # EOF + break + + elif rec_type == 0x02: + # Extended segment address + if byte_count != 2: + raise ValueError( + f"Line {line_num}: type 02 record must have 2 data bytes") + base_addr = ((rec_data[0] << 8) | rec_data[1]) << 4 + + elif rec_type == 0x04: + # Extended linear address + if byte_count != 2: + raise ValueError( + f"Line {line_num}: type 04 record must have 2 data bytes") + base_addr = ((rec_data[0] << 8) | rec_data[1]) << 16 + + # Silently ignore unknown record types (03, 05, etc.) + + return segments + + +def coalesce_segments(segments): + """ + Merge adjacent/overlapping segments into contiguous blocks. + Returns list of (address, bytes) with no gaps. + """ + if not segments: + return [] + + # Sort by address + sorted_segs = sorted(segments, key=lambda s: s[0]) + + merged = [] + cur_addr, cur_data = sorted_segs[0] + cur_data = bytearray(cur_data) + + for addr, data in sorted_segs[1:]: + cur_end = cur_addr + len(cur_data) + if addr <= cur_end: + # Overlapping or adjacent -- extend or overwrite + overlap = cur_end - addr + if overlap >= 0: + cur_data.extend(data[overlap:] if overlap < len(data) else b'') + else: + # Gap -- pad with zeros (shouldn't happen after sort, but safe) + cur_data.extend(b'\x00' * (-overlap)) + cur_data.extend(data) + else: + merged.append((cur_addr, bytes(cur_data))) + cur_addr = addr + cur_data = bytearray(data) + + merged.append((cur_addr, bytes(cur_data))) + return merged + + +def load_firmware_file(path): + """ + Load firmware from .ihx/.hex (Intel HEX) or .bix/.bin (raw binary). + Returns list of (address, bytes) segments. + """ + ext = os.path.splitext(path)[1].lower() + + with open(path, 'rb') as f: + raw = f.read() + + if ext in ('.ihx', '.hex'): + segments = parse_ihx(raw) + segments = coalesce_segments(segments) + return segments + + elif ext in ('.bix', '.bin'): + # Raw binary loads at address 0x0000 + if not raw: + print(f"Empty file: {path}") + sys.exit(1) + return [(0x0000, raw)] + + else: + # Try to auto-detect: if it starts with ':', assume Intel HEX + if raw.startswith(b':'): + segments = parse_ihx(raw) + segments = coalesce_segments(segments) + return segments + else: + # Treat as raw binary + return [(0x0000, raw)] + + +def write_segments(dev, segments, verbose=False): + """ + Write firmware segments to FX2 RAM in CHUNK_SIZE pieces. + Returns total bytes written. + """ + total = 0 + + for seg_addr, seg_data in segments: + seg_len = len(seg_data) + seg_end = seg_addr + seg_len - 1 + print(f" 0x{seg_addr:04X}-0x{seg_end:04X} ({seg_len} bytes)") + + offset = 0 + while offset < seg_len: + chunk_len = min(CHUNK_SIZE, seg_len - offset) + chunk = seg_data[offset:offset + chunk_len] + addr = seg_addr + offset + + try: + written = fx2_ram_write(dev, addr, chunk) + if written != chunk_len: + print(f"\n Short write at 0x{addr:04X}: " + f"sent {chunk_len}, wrote {written}") + except usb.core.USBError as e: + print(f"\n Write error at 0x{addr:04X}: {e}") + return total + + if verbose and offset % 0x400 == 0: + pct = offset * 100 // seg_len + print(f"\r 0x{addr:04X} [{pct:3d}%]", end="", flush=True) + + total += chunk_len + offset += chunk_len + + if verbose and seg_len > CHUNK_SIZE: + print(f"\r 0x{seg_addr + seg_len - 1:04X} [100%] ") + + return total + + +# -- I2C controller cleanup -- + +# FX2LP I2C controller XDATA registers (accessible via vendor request 0xA0) +I2CS_ADDR = 0xE678 # I2C Control/Status +I2DAT_ADDR = 0xE679 # I2C Data +I2CTL_ADDR = 0xE67A # I2C Control + +# I2CS bit definitions +I2CS_START = 0x80 +I2CS_STOP = 0x40 +I2CS_LASTRD = 0x20 +I2CS_ID1 = 0x10 +I2CS_ID0 = 0x08 +I2CS_BERR = 0x04 +I2CS_ACK = 0x02 +I2CS_DONE = 0x01 + +def i2cs_decode(val): + """Decode I2CS register value into human-readable string.""" + flags = [] + for bit, name in [(7, 'START'), (6, 'STOP'), (5, 'LASTRD'), + (4, 'ID1'), (3, 'ID0'), (2, 'BERR'), + (1, 'ACK'), (0, 'DONE')]: + if val & (1 << bit): + flags.append(name) + id_val = (val >> 3) & 0x03 + state = {0: 'idle', 1: 'data', 2: 'addr-wait', 3: 'busy'}[id_val] + return f"0x{val:02X} ({' | '.join(flags) if flags else 'idle'}) [state={state}]" + + +def build_i2c_cleanup_stub(): + """Build a tiny 8051 stub that terminates any stuck I2C transaction. + + USB vendor request 0xA0 can READ I2C registers but WRITES are ignored + by the I2C controller during CPU halt (the peripheral only recognizes + 8051-initiated XDATA writes). So we need the 8051 itself to do the + cleanup. + + The stub: + 1. Reads I2CS + 2. If BERR: clears it + 3. If mid-transaction (ID bits): reads I2DAT, sends STOP + 4. If residual flags: sends STOP + 5. Loops up to 10 times + 6. Stores diagnostics at XDATA 0x3C00-0x3C07 + 7. Enters infinite loop (host halts CPU after reading diagnostics) + + Diagnostics layout: + 0x3C00: 0xAA = stub started + 0x3C01: I2CS at entry (before any cleanup) + 0x3C02: I2CS after cleanup (should be 0x00) + 0x3C03: iteration count (how many loops needed) + 0x3C04: I2DAT value read during cleanup + 0x3C05: 0xDD = stub completed successfully + """ + # 8051 machine code — hand-assembled for clarity + code = [] + + # 0x0000: LJMP 0x0010 (skip interrupt vector area) + code += [0x02, 0x00, 0x10] + + # Pad to 0x0010 + while len(code) < 0x10: + code += [0x00] + + # ========== 0x0010: Main code ========== + + # --- Marker: stub started (0xAA → 0x3C00) --- + code += [0x74, 0xAA] # MOV A, #0xAA + code += [0x90, 0x3C, 0x00] # MOV DPTR, #0x3C00 + code += [0xF0] # MOVX @DPTR, A + + # --- Read initial I2CS → 0x3C01 --- + code += [0x90, 0xE6, 0x78] # MOV DPTR, #0xE678 (I2CS) + code += [0xE0] # MOVX A, @DPTR + code += [0x90, 0x3C, 0x01] # MOV DPTR, #0x3C01 + code += [0xF0] # MOVX @DPTR, A + + # --- Init loop counter R0=0 --- + code += [0x78, 0x00] # MOV R0, #0 + + # ========== CLEANUP LOOP ========== + loop_top = len(code) + + # Increment loop counter + code += [0x08] # INC R0 + + # Read I2CS + code += [0x90, 0xE6, 0x78] # MOV DPTR, #0xE678 (I2CS) + code += [0xE0] # MOVX A, @DPTR (A = I2CS) + + # If I2CS == 0x00 (idle), jump to done + code += [0x60] # JZ done (offset filled later) + jz_done_pc = len(code) + code += [0x00] # placeholder + + # Check BERR (bit 2): if set, clear it + code += [0x30, 0xE2] # JNB ACC.2, skip_berr + jnb_berr_pc = len(code) + code += [0x00] # placeholder + # Clear BERR: write 0x04 to I2CS + code += [0x74, 0x04] # MOV A, #0x04 + code += [0x90, 0xE6, 0x78] # MOV DPTR, #0xE678 + code += [0xF0] # MOVX @DPTR, A + # Small delay + code += [0x79, 100] # MOV R1, #100 + code += [0xD9, 0xFE] # DJNZ R1, $-2 + # Jump to loop check + code += [0x80] # SJMP loop_check + sjmp_check1_pc = len(code) + code += [0x00] # placeholder + + skip_berr = len(code) + + # Check ID bits (bits 3,4): if either set, flush I2DAT + STOP + code += [0x54, 0x18] # ANL A, #0x18 (mask ID bits) + code += [0x60] # JZ skip_id + jz_skip_id_pc = len(code) + code += [0x00] # placeholder + + # Read I2DAT (flush pending data) + code += [0x90, 0xE6, 0x79] # MOV DPTR, #0xE679 (I2DAT) + code += [0xE0] # MOVX A, @DPTR + code += [0x90, 0x3C, 0x04] # MOV DPTR, #0x3C04 + code += [0xF0] # MOVX @DPTR, A (save I2DAT) + # Small delay + code += [0x79, 100] # MOV R1, #100 + code += [0xD9, 0xFE] # DJNZ R1, $-2 + # Send STOP: write 0x40 to I2CS + code += [0x74, 0x40] # MOV A, #0x40 + code += [0x90, 0xE6, 0x78] # MOV DPTR, #0xE678 + code += [0xF0] # MOVX @DPTR, A + # Longer delay for STOP to complete + code += [0x7A, 10] # MOV R2, #10 + code += [0x79, 250] # MOV R1, #250 + code += [0xD9, 0xFE] # DJNZ R1, $-2 + code += [0xDA, 0xFC] # DJNZ R2, $-4 + # Jump to loop check + code += [0x80] # SJMP loop_check + sjmp_check2_pc = len(code) + code += [0x00] # placeholder + + skip_id = len(code) + + # Residual flags — send STOP anyway + code += [0x74, 0x40] # MOV A, #0x40 + code += [0x90, 0xE6, 0x78] # MOV DPTR, #0xE678 + code += [0xF0] # MOVX @DPTR, A + code += [0x7A, 10] # MOV R2, #10 + code += [0x79, 250] # MOV R1, #250 + code += [0xD9, 0xFE] # DJNZ R1, $-2 + code += [0xDA, 0xFC] # DJNZ R2, $-4 + + loop_check = len(code) + + # Loop up to 10 times + code += [0xB8, 10] # CJNE R0, #10, loop_top + cjne_pc = len(code) + code += [(loop_top - (cjne_pc + 1)) & 0xFF] + + # ========== DONE ========== + done = len(code) + + # Store final I2CS → 0x3C02 + code += [0x90, 0xE6, 0x78] # MOV DPTR, #0xE678 + code += [0xE0] # MOVX A, @DPTR + code += [0x90, 0x3C, 0x02] # MOV DPTR, #0x3C02 + code += [0xF0] # MOVX @DPTR, A + + # Store iteration count → 0x3C03 + code += [0xE8] # MOV A, R0 + code += [0x90, 0x3C, 0x03] # MOV DPTR, #0x3C03 + code += [0xF0] # MOVX @DPTR, A + + # Marker: stub done (0xDD → 0x3C05) + code += [0x74, 0xDD] # MOV A, #0xDD + code += [0x90, 0x3C, 0x05] # MOV DPTR, #0x3C05 + code += [0xF0] # MOVX @DPTR, A + + # Infinite loop + code += [0x80, 0xFE] # SJMP $ (loop forever) + + # ========== Patch jump offsets ========== + # All relative jump offsets: target - (offset_byte_position + 1) + # because 8051 PC points to the NEXT instruction when the branch executes. + code[jz_done_pc] = (done - (jz_done_pc + 1)) & 0xFF + code[jnb_berr_pc] = (skip_berr - (jnb_berr_pc + 1)) & 0xFF + code[sjmp_check1_pc] = (loop_check - (sjmp_check1_pc + 1)) & 0xFF + code[jz_skip_id_pc] = (skip_id - (jz_skip_id_pc + 1)) & 0xFF + code[sjmp_check2_pc] = (loop_check - (sjmp_check2_pc + 1)) & 0xFF + + return bytes(code) + + +def i2c_cleanup(dev): + """Attempt host-side I2C controller recovery after CPU halt. + + After halting the stock firmware mid-I2C-transaction, I2CS reads 0x1A + (mid-transaction, no BERR). BERR (0xF6) only appears on CPU restart. + + Strategy: write STOP to I2CS from the host via 0xA0 vendor request + BEFORE restarting the CPU. If the I2C controller accepts host writes, + the pending transaction ends cleanly and our firmware gets a working + I2C controller. + + Even if the controller doesn't process STOP while halted, latching the + bit in the register may cause the hardware to execute it on CPU restart, + preventing BERR from being set. + """ + print(f"\n I2C controller recovery (host-side):") + + # 1. Read initial state + i2cs = fx2_ram_read(dev, I2CS_ADDR, 1) + if not i2cs: + print(f" I2CS read failed — skipping recovery") + return + i2cs_val = i2cs[0] + print(f" I2CS initial: {i2cs_decode(i2cs_val)}") + + if i2cs_val == 0x00: + print(f" I2C controller idle — no recovery needed") + return + + # Also read I2CTL for diagnostics + i2ctl = fx2_ram_read(dev, I2CTL_ADDR, 1) + if i2ctl: + speed = "400kHz" if i2ctl[0] & 0x01 else "100kHz" + print(f" I2CTL: 0x{i2ctl[0]:02X} ({speed})") + + berr_set = bool(i2cs_val & I2CS_BERR) + + if berr_set: + print(f" BERR already set at halt time — unusual") + # Try clearing BERR (write bit 2) + print(f" Writing 0x04 to I2CS (BERR clear)...") + fx2_ram_write(dev, I2CS_ADDR, bytes([I2CS_BERR])) + time.sleep(0.010) + i2cs2 = fx2_ram_read(dev, I2CS_ADDR, 1) + if i2cs2: + print(f" I2CS after: {i2cs_decode(i2cs2[0])}") + return + + # 2. Mid-transaction — attempt recovery + id_bits = (i2cs_val >> 3) & 0x03 + print(f" Transaction state: ID={id_bits} ({'idle' if id_bits == 0 else 'active'})") + + # Strategy A: Read I2DAT to flush pending byte, then STOP + print(f" [A] Flushing I2DAT + STOP...") + i2dat = fx2_ram_read(dev, I2DAT_ADDR, 1) + if i2dat: + print(f" I2DAT read: 0x{i2dat[0]:02X}") + time.sleep(0.005) + + fx2_ram_write(dev, I2CS_ADDR, bytes([I2CS_STOP])) + time.sleep(0.010) + + i2cs_a = fx2_ram_read(dev, I2CS_ADDR, 1) + if i2cs_a: + print(f" I2CS after: {i2cs_decode(i2cs_a[0])}") + if i2cs_a[0] == 0x00 or (i2cs_a[0] & I2CS_DONE): + print(f" ✓ Recovery may have worked!") + return + + # Strategy B: LASTRD + STOP (end read transaction cleanly) + print(f" [B] LASTRD + STOP...") + fx2_ram_write(dev, I2CS_ADDR, bytes([I2CS_LASTRD | I2CS_STOP])) + time.sleep(0.010) + + i2cs_b = fx2_ram_read(dev, I2CS_ADDR, 1) + if i2cs_b: + print(f" I2CS after: {i2cs_decode(i2cs_b[0])}") + if i2cs_b[0] == 0x00 or (i2cs_b[0] & I2CS_DONE): + print(f" ✓ Recovery may have worked!") + return + + # Strategy C: Just STOP again (in case controller needed time) + print(f" [C] Retry STOP...") + fx2_ram_write(dev, I2CS_ADDR, bytes([I2CS_STOP])) + time.sleep(0.020) + + i2cs_c = fx2_ram_read(dev, I2CS_ADDR, 1) + if i2cs_c: + print(f" I2CS after: {i2cs_decode(i2cs_c[0])}") + + # If BERR appeared during recovery attempts, try to clear it + if i2cs_c and (i2cs_c[0] & I2CS_BERR): + print(f" [D] BERR appeared — attempting clear...") + fx2_ram_write(dev, I2CS_ADDR, bytes([I2CS_BERR])) + time.sleep(0.010) + i2cs_d = fx2_ram_read(dev, I2CS_ADDR, 1) + if i2cs_d: + print(f" I2CS after: {i2cs_decode(i2cs_d[0])}") + + # Final state + time.sleep(0.010) + i2cs_final = fx2_ram_read(dev, I2CS_ADDR, 1) + if i2cs_final: + print(f" I2CS final: {i2cs_decode(i2cs_final[0])}") + if i2cs_final[0] == 0x00: + print(f" ✓ I2C controller recovered!") + elif not (i2cs_final[0] & I2CS_BERR): + print(f" ~ I2C controller not idle but no BERR — STOP may be latched") + else: + print(f" ✗ BERR persists — host-side recovery did not work") + + +# -- Subcommand handlers -- + +def cmd_load(args): + """Load firmware into FX2 RAM.""" + if not os.path.exists(args.file): + print(f"File not found: {args.file}") + sys.exit(1) + + # Parse firmware file + segments = load_firmware_file(args.file) + if not segments: + print("No code segments found in firmware file") + sys.exit(1) + + total_bytes = sum(len(d) for _, d in segments) + min_addr = min(a for a, _ in segments) + max_addr = max(a + len(d) - 1 for a, d in segments) + + print(f"SkyWalker-1 RAM Firmware Loader") + print(f"{'=' * 40}") + print(f"\nFirmware: {args.file}") + print(f" Segments: {len(segments)}") + print(f" Total size: {total_bytes} bytes") + print(f" Address: 0x{min_addr:04X} - 0x{max_addr:04X}") + + # Check for CPUCS region overlap (warn but don't block) + for addr, data in segments: + seg_end = addr + len(data) - 1 + if addr <= CPUCS_ADDR <= seg_end: + print(f"\n WARNING: Segment at 0x{addr:04X}-0x{seg_end:04X} " + f"overlaps CPUCS (0x{CPUCS_ADDR:04X})") + print(f" The CPU halt/start writes to 0xE600 will clobber " + f"this region") + + print() + + # Connect + dev = find_device(force=args.force) + + # Check VID/PID if it's not a known device + vid = dev.idVendor + pid = dev.idProduct + is_skywalker = (vid == SKYWALKER_VID and pid == SKYWALKER_PID) + is_bare_fx2 = (vid == CYPRESS_VID and pid == CYPRESS_PID) + + if not is_skywalker and not is_bare_fx2 and not args.force: + print(f"\n Unknown device VID 0x{vid:04X} PID 0x{pid:04X}") + print(f" Expected SkyWalker-1 (09C0:0203) or bare FX2 (04B4:8613)") + print(f" Use --force to override") + sys.exit(1) + + intf = detach_driver(dev) + + try: + # Step 1: Halt CPU + if not args.no_reset: + if args.settle_delay > 0: + print(f"\n Settle delay: waiting {args.settle_delay}s for stock " + f"firmware I2C to finish...") + time.sleep(args.settle_delay) + print(f" Settle complete.") + + # Pre-halt I2C flush: send vendor requests to the stock firmware + # that trigger I2C operations. After the firmware completes the + # I2C transaction (including STOP), the controller should be idle. + # We then immediately halt before any new I2C operation starts. + # + # Try multiple approaches — the stock firmware may support + # different subsets of the gp8psk vendor request protocol. + print("\n Pre-halt I2C flush...") + i2c_flushed = False + # Approach 1: GET_SIGNAL_LOCK (0x90) — reads BCM4500 via I2C + try: + lock_data = dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, + 0x90, 0, 0, 1, 2000) + print(f" 0x90 GET_SIGNAL_LOCK: 0x{lock_data[0]:02X} (I2C to BCM4500)") + i2c_flushed = True + except usb.core.USBError: + print(f" 0x90 not supported") + # Approach 2: I2C_READ (0x84) with shifted address + if not i2c_flushed: + try: + eeprom = dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, + 0x84, 0xA2, 0, 1, 2000) # addr<<1 per gp8psk + print(f" 0x84 I2C_READ: 0x{eeprom[0]:02X} (EEPROM)") + i2c_flushed = True + except usb.core.USBError: + print(f" 0x84 not supported") + # Approach 3: GET_8PSK_CONFIG (0x80) — may trigger I2C indirectly + if not i2c_flushed: + try: + cfg = dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, + 0x80, 0, 0, 1, 2000) + print(f" 0x80 GET_8PSK_CONFIG: 0x{cfg[0]:02X}") + i2c_flushed = True + except usb.core.USBError: + print(f" 0x80 not supported") + if not i2c_flushed: + print(f" No stock vendor requests succeeded — halt may catch mid-I2C") + + print("\n[1/3] Halting CPU (CPUCS = 0x01)...") + cpu_halt(dev) + time.sleep(0.01) # minimal delay — I2C should be idle from flush + + # Verify halt + readback = fx2_ram_read(dev, CPUCS_ADDR, 1) + if readback and readback[0] & 0x01: + print(" CPU halted") + else: + val = f"0x{readback[0]:02X}" if readback else "read failed" + print(f" WARNING: CPUCS readback = {val} (expected 0x01)") + print(" Proceeding anyway...") + else: + print("\n[1/3] Skipping CPU reset (--no-reset)") + + # Step 1.5: I2C controller cleanup (two-stage boot) + # The stock firmware's I2C polling is almost certainly interrupted + # by our CPUCS halt. The I2C controller runs independently — it + # enters a stuck state that causes BERR (I2CS=0xF6) on CPU restart. + # Must run BEFORE firmware load since the cleanup stub uses 0x0000. + if not args.no_reset: + i2c_cleanup(dev) + + # Step 2: Load segments (CPU is halted from cleanup or step 1) + step = "2/3" if not args.no_reset else "2/2" + print(f"\n[{step}] Loading {len(segments)} segment(s) into RAM...") + written = write_segments(dev, segments, verbose=args.verbose) + print(f"\n {written} bytes loaded") + + if written != total_bytes: + print(f" WARNING: expected {total_bytes}, wrote {written}") + + # Step 3: Start CPU + if not args.no_reset: + print(f"\n[3/3] Starting CPU (CPUCS = 0x00)...") + cpu_start(dev) + print(" CPU released") + print(f"\n Firmware is running. The device will re-enumerate") + print(f" with new USB descriptors if the firmware does so.") + + if args.wait: + _wait_for_reenumeration(args.wait) + else: + print(f"\n Segments loaded (CPU not reset)") + + finally: + # Only re-attach if we didn't just start new firmware + # (the device may have already re-enumerated away) + if args.no_reset and intf is not None: + try: + usb.util.release_interface(dev, intf) + dev.attach_kernel_driver(intf) + print("\nRe-attached kernel driver") + except: + pass + + +def _wait_for_reenumeration(timeout): + """Wait for a USB device to re-appear after firmware load.""" + print(f"\n Waiting up to {timeout}s for re-enumeration...") + deadline = time.time() + timeout + time.sleep(1.0) # Give the device a moment to disconnect + + while time.time() < deadline: + # Check for SkyWalker-1 with potentially new VID/PID + # After loading custom firmware, VID/PID may differ + dev = usb.core.find(idVendor=SKYWALKER_VID, idProduct=SKYWALKER_PID) + if dev is not None: + print(f" Device re-appeared: Bus {dev.bus} Addr {dev.address} " + f"(0x{SKYWALKER_VID:04X}:0x{SKYWALKER_PID:04X})") + return + + dev = usb.core.find(idVendor=CYPRESS_VID, idProduct=CYPRESS_PID) + if dev is not None: + print(f" Device re-appeared: Bus {dev.bus} Addr {dev.address} " + f"(0x{CYPRESS_VID:04X}:0x{CYPRESS_PID:04X})") + return + + print(".", end="", flush=True) + time.sleep(0.5) + + print(f"\n Timeout -- device did not re-enumerate within {timeout}s") + print(f" The firmware may use different VID/PID. Check 'lsusb'.") + + +def cmd_reset(args): + """Reset the FX2 CPU (halt then start).""" + print(f"SkyWalker-1 CPU Reset") + print(f"{'=' * 40}") + + dev = find_device(force=args.force) + intf = detach_driver(dev) + + try: + print("\nHalting CPU...") + cpu_halt(dev) + time.sleep(0.05) + print(" CPUCS = 0x01 (halted)") + + time.sleep(0.1) + + print("Starting CPU...") + cpu_start(dev) + print(" CPUCS = 0x00 (running)") + print("\nCPU reset complete. Device will re-enumerate.") + + if args.wait: + _wait_for_reenumeration(args.wait) + finally: + pass # Device is likely gone after reset + + +def cmd_read(args): + """Read and hex-dump FX2 RAM contents.""" + addr = args.addr + length = args.length + + print(f"SkyWalker-1 RAM Read") + print(f"{'=' * 40}") + + dev = find_device(force=args.force) + intf = detach_driver(dev) + + try: + print(f"\nReading {length} bytes from 0x{addr:04X}...\n") + + data = bytearray() + offset = 0 + errors = 0 + + while offset < length: + chunk_len = min(CHUNK_SIZE, length - offset) + chunk = fx2_ram_read(dev, addr + offset, chunk_len) + if chunk is None: + errors += 1 + data.extend(b'\xff' * chunk_len) + else: + data.extend(chunk) + offset += chunk_len + + # Hex dump output + for i in range(0, len(data), 16): + row = data[i:i + 16] + hex_part = ' '.join(f'{b:02X}' for b in row) + ascii_part = ''.join(chr(b) if 0x20 <= b < 0x7F else '.' for b in row) + print(f" {addr + i:04X}: {hex_part:<48s} {ascii_part}") + + print(f"\n {len(data)} bytes read, {errors} chunk errors") + + if args.output: + with open(args.output, 'wb') as f: + f.write(data) + print(f" Saved to: {args.output}") + + finally: + if intf is not None: + try: + usb.util.release_interface(dev, intf) + dev.attach_kernel_driver(intf) + print("\nRe-attached kernel driver") + except: + print("\nNote: run 'sudo modprobe dvb_usb_gp8psk' to reload") + + +def main(): + parser = argparse.ArgumentParser( + description="SkyWalker-1 RAM firmware loader (FX2 vendor request 0xA0)", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="""\ +examples: + %(prog)s load firmware.ihx + %(prog)s load firmware.bix --wait 5 + %(prog)s load firmware.ihx --no-reset + %(prog)s reset + %(prog)s read --addr 0x0000 --len 256 + %(prog)s read --addr 0xe600 --len 1 + +This tool loads firmware into RAM only -- the EEPROM is never touched. +Power-cycle the device to restore the factory-programmed firmware. +""") + parser.add_argument('-v', '--verbose', action='store_true', + help="Show detailed transfer progress") + parser.add_argument('--force', action='store_true', + help="Allow loading to unknown VID/PID devices") + + sub = parser.add_subparsers(dest='command') + + # load (default) + p_load = sub.add_parser('load', + help='Load firmware into FX2 RAM') + p_load.add_argument('file', help='Firmware file (.ihx, .hex, .bix, .bin)') + p_load.add_argument('--no-reset', action='store_true', + help="Load without halting/starting the CPU") + p_load.add_argument('--wait', type=float, default=0, metavar='SECONDS', + help="Wait for USB re-enumeration after load") + p_load.add_argument('-v', '--verbose', action='store_true', + help="Show detailed transfer progress") + p_load.add_argument('--force', action='store_true', + help="Allow loading to unknown VID/PID devices") + p_load.add_argument('--settle-delay', type=float, default=0, metavar='SECONDS', + help="Wait N seconds before halting CPU (lets stock firmware " + "finish I2C init — may avoid I2C BERR on restart)") + + # reset + p_reset = sub.add_parser('reset', + help='Reset the FX2 CPU (halt then start)') + p_reset.add_argument('--wait', type=float, default=0, metavar='SECONDS', + help="Wait for USB re-enumeration after reset") + p_reset.add_argument('--force', action='store_true', + help="Allow reset on unknown VID/PID devices") + + # read + p_read = sub.add_parser('read', + help='Read and hex-dump FX2 RAM') + p_read.add_argument('--addr', type=lambda x: int(x, 0), default=0x0000, + help="Start address (default: 0x0000)") + p_read.add_argument('--len', dest='length', type=lambda x: int(x, 0), + default=256, + help="Number of bytes to read (default: 256)") + p_read.add_argument('-o', '--output', metavar='FILE', + help="Save raw bytes to file") + p_read.add_argument('--force', action='store_true', + help="Allow read on unknown VID/PID devices") + + args = parser.parse_args() + + # Default to 'load' if a positional arg is given but no subcommand + if not args.command: + parser.print_help() + sys.exit(0) + + # Propagate top-level flags to subcommands + if hasattr(args, 'verbose') and not args.verbose: + args.verbose = parser.parse_args().verbose + if hasattr(args, 'force') and not args.force: + args.force = parser.parse_args().force + + dispatch = { + 'load': cmd_load, + 'reset': cmd_reset, + 'read': cmd_read, + } + + handler = dispatch.get(args.command) + if handler is None: + parser.print_help() + sys.exit(1) + + handler(args) + + +if __name__ == '__main__': + main() diff --git a/tools/skywalker_lib.py b/tools/skywalker_lib.py index 8eba328..89b55a2 100644 --- a/tools/skywalker_lib.py +++ b/tools/skywalker_lib.py @@ -1,961 +1,965 @@ -#!/usr/bin/env python3 -""" -Genpix SkyWalker-1 shared library. - -Provides the SkyWalker1 USB interface class, constants, and signal -processing utilities used by skywalker.py and tune.py. -""" - -import sys -import struct -import time -import math - -try: - import usb.core - import usb.util -except ImportError: - print("pyusb required: pip install pyusb") - sys.exit(1) - - -# --- USB identifiers --- - -VENDOR_ID = 0x09C0 -PRODUCT_ID = 0x0203 -EP2_ADDR = 0x82 -EP2_URB_SIZE = 8192 - -# --- Vendor commands --- - -CMD_GET_8PSK_CONFIG = 0x80 -CMD_I2C_WRITE = 0x83 -CMD_I2C_READ = 0x84 -CMD_ARM_TRANSFER = 0x85 -CMD_TUNE_8PSK = 0x86 -CMD_GET_SIGNAL_STRENGTH = 0x87 -CMD_LOAD_BCM4500 = 0x88 -CMD_BOOT_8PSK = 0x89 -CMD_START_INTERSIL = 0x8A -CMD_SET_LNB_VOLTAGE = 0x8B -CMD_SET_22KHZ_TONE = 0x8C -CMD_SEND_DISEQC = 0x8D -CMD_GET_SIGNAL_LOCK = 0x90 -CMD_GET_FW_VERS = 0x92 -CMD_GET_SERIAL_NUMBER = 0x93 -CMD_USE_EXTRA_VOLT = 0x94 - -# Custom commands (v3.01+) -CMD_SPECTRUM_SWEEP = 0xB0 -CMD_RAW_DEMOD_READ = 0xB1 -CMD_RAW_DEMOD_WRITE = 0xB2 -CMD_BLIND_SCAN = 0xB3 -CMD_I2C_BUS_SCAN = 0xB4 -CMD_I2C_RAW_READ = 0xB5 -CMD_I2C_DIAG = 0xB6 - -# Custom commands (v3.02+) -CMD_SIGNAL_MONITOR = 0xB7 -CMD_TUNE_MONITOR = 0xB8 -CMD_MULTI_REG_READ = 0xB9 - -# Custom commands (v3.03+) -CMD_PARAM_SWEEP = 0xBA -CMD_ADAPTIVE_BLIND_SCAN = 0xBB -CMD_GET_LAST_ERROR = 0xBC -CMD_GET_STREAM_DIAG = 0xBD -CMD_GET_HOTPLUG_STATUS = 0xBE - -# Error codes (returned by CMD_GET_LAST_ERROR) -ERR_OK = 0x00 -ERR_I2C_TIMEOUT = 0x01 -ERR_I2C_NAK = 0x02 -ERR_I2C_ARB_LOST = 0x03 -ERR_BCM_NOT_READY = 0x04 -ERR_BCM_TIMEOUT = 0x05 - -ERROR_NAMES = { - ERR_OK: "OK", - ERR_I2C_TIMEOUT: "I2C timeout", - ERR_I2C_NAK: "I2C NAK (no ACK from slave)", - ERR_I2C_ARB_LOST: "I2C arbitration lost", - ERR_BCM_NOT_READY: "BCM4500 not ready", - ERR_BCM_TIMEOUT: "BCM4500 command timeout", -} - -# --- Config status bits --- - -CONFIG_BITS = { - 0x01: ("8PSK Started", "bm8pskStarted"), - 0x02: ("BCM4500 FW Loaded", "bm8pskFW_Loaded"), - 0x04: ("LNB Power On", "bmIntersilOn"), - 0x08: ("DVB Mode", "bmDVBmode"), - 0x10: ("22 kHz Tone", "bm22kHz"), - 0x20: ("18V Selected", "bmSEL18V"), - 0x40: ("DC Tuned", "bmDCtuned"), - 0x80: ("Armed (streaming)", "bmArmed"), -} - -# --- Modulation and FEC tables --- - -MODULATIONS = { - "qpsk": (0, "DVB-S QPSK"), - "turbo-qpsk": (1, "Turbo QPSK"), - "turbo-8psk": (2, "Turbo 8PSK"), - "turbo-16qam": (3, "Turbo 16QAM"), - "dcii-combo": (4, "DCII Combo"), - "dcii-i": (5, "DCII I-stream"), - "dcii-q": (6, "DCII Q-stream"), - "dcii-oqpsk": (7, "DCII Offset QPSK"), - "dss": (8, "DSS QPSK"), - "bpsk": (9, "DVB BPSK"), -} - -FEC_RATES = { - "dvbs": { - "1/2": 0, "2/3": 1, "3/4": 2, "5/6": 3, - "7/8": 4, "auto": 5, "none": 6, - }, - "turbo": { - "1/2": 0, "2/3": 1, "3/4": 2, "5/6": 3, "auto": 4, - }, - "turbo-16qam": { - "3/4": 0, "auto": 0, - }, - "dcii": { - "1/2": 0, "2/3": 1, "6/7": 2, "3/4": 3, "5/11": 4, - "1/2+": 5, "2/3+": 6, "6/7+": 7, "3/4+": 8, "auto": 0, - }, -} - -MOD_FEC_GROUP = { - "qpsk": "dvbs", - "turbo-qpsk": "turbo", - "turbo-8psk": "turbo", - "turbo-16qam": "turbo-16qam", - "dcii-combo": "dcii", - "dcii-i": "dcii", - "dcii-q": "dcii", - "dcii-oqpsk": "dcii", - "dss": "dvbs", - "bpsk": "dvbs", -} - -# --- LNB defaults --- - -LNB_LO_LOW = 9750 # Universal LNB low-band (MHz) -LNB_LO_HIGH = 10600 # Universal LNB high-band (MHz) - -# --- L-band allocations (for annotation) --- - -LBAND_ALLOCATIONS = [ - (1240, 1300, "Amateur 23cm"), - (1525, 1559, "Inmarsat downlink"), - (1559, 1610, "GNSS (GPS L1, Galileo E1)"), - (1610, 1626, "Iridium downlink"), - (1670, 1710, "MetSat (GOES LRIT, NOAA HRPT)"), - (1710, 1785, "LTE/AWS uplink"), - (1920, 2025, "UMTS uplink"), -] - - -# --- Signal processing helpers --- - -def snr_raw_to_db(snr_raw: int) -> float: - """Convert BCM4500 SNR register to dB. Register is dBu * 256.""" - return snr_raw / 256.0 - - -def snr_raw_to_pct(snr_raw: int) -> float: - """Convert raw SNR to percentage (0-100 scale, clamped).""" - scaled = min(snr_raw * 17, 65535) - return (scaled / 65535) * 100 - - -def agc_to_power_db(agc1: int, agc2: int) -> float: - """ - Estimate received power from AGC register values. - - The AGC loop adjusts gain to keep the signal level constant at the - ADC input. Higher AGC = weaker signal (more gain needed). This is - an approximation; the exact mapping depends on the BCM3440 tuner's - gain curve. - - Returns a relative dB value (higher = stronger signal). - """ - # AGC1 is the primary gain control, AGC2 is fine adjustment. - # Invert: low AGC value = high signal = high power. - # Scale to approximate dB with ~40 dB dynamic range. - combined = agc1 + (agc2 >> 4) - if combined == 0: - return 0.0 - # Rough linear-to-dB: 65535 AGC ≈ -40 dB, 0 AGC ≈ 0 dB - return -40.0 * (combined / 65535.0) - - -def detect_peaks(freqs: list, powers: list, threshold_db: float = 3.0) -> list: - """ - Find peaks in a spectrum sweep. - - Returns list of (freq_mhz, power_db, index) tuples for each local - maximum that exceeds the noise floor by threshold_db. - """ - if len(powers) < 3: - return [] - - # Estimate noise floor as the 25th percentile - sorted_p = sorted(powers) - noise_floor = sorted_p[len(sorted_p) // 4] - - peaks = [] - for i in range(1, len(powers) - 1): - if powers[i] > powers[i - 1] and powers[i] > powers[i + 1]: - if powers[i] - noise_floor >= threshold_db: - peaks.append((freqs[i], powers[i], i)) - - return peaks - - -def if_to_rf(if_mhz: float, lnb_lo: float) -> float: - """Convert IF frequency to actual RF frequency given LNB LO.""" - return if_mhz + lnb_lo - - -def rf_to_if(rf_mhz: float, lnb_lo: float) -> float: - """Convert actual RF frequency to IF frequency given LNB LO.""" - return rf_mhz - lnb_lo - - -def signal_bar(pct: float, width: int = 40) -> str: - """Render an ASCII signal strength bar.""" - filled = int(pct / 100 * width) - filled = max(0, min(filled, width)) - bar = '#' * filled + '-' * (width - filled) - return f"[{bar}] {pct:.1f}%" - - -def format_config_bits(status: int) -> list: - """Return list of (bit_name, is_set) tuples for config byte.""" - result = [] - for bit, (name, _field) in CONFIG_BITS.items(): - result.append((name, bool(status & bit))) - return result - - -# --- SkyWalker1 USB interface --- - -class SkyWalker1: - """USB interface to the Genpix SkyWalker-1 DVB-S receiver.""" - - def __init__(self, verbose: bool = False): - self.dev = None - self.detached_intf = None - self.verbose = verbose - - def open(self) -> None: - """Find and claim the SkyWalker-1 USB device.""" - self.dev = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID) - if self.dev is None: - print("SkyWalker-1 not found (VID 0x09C0, PID 0x0203). Is it plugged in?") - sys.exit(1) - - for cfg in self.dev: - for intf in cfg: - if self.dev.is_kernel_driver_active(intf.bInterfaceNumber): - try: - self.dev.detach_kernel_driver(intf.bInterfaceNumber) - self.detached_intf = intf.bInterfaceNumber - if self.verbose: - print(f" Detached kernel driver from interface {intf.bInterfaceNumber}") - except usb.core.USBError as e: - print(f"Cannot detach kernel driver: {e}") - print("The gp8psk module must be unbound first. Try one of:") - print(" sudo modprobe -r dvb_usb_gp8psk") - print(" echo '' | sudo tee /sys/bus/usb/drivers/gp8psk/unbind") - sys.exit(1) - - try: - self.dev.set_configuration() - except usb.core.USBError: - pass - - def close(self) -> None: - """Release device and re-attach kernel driver.""" - if self.dev is None: - return - if self.detached_intf is not None: - try: - usb.util.release_interface(self.dev, self.detached_intf) - self.dev.attach_kernel_driver(self.detached_intf) - if self.verbose: - print("Re-attached kernel driver") - except usb.core.USBError: - print("Note: run 'sudo modprobe dvb_usb_gp8psk' to reload driver") - - def __enter__(self): - self.open() - return self - - def __exit__(self, *exc): - self.close() - - # -- Low-level USB transfers -- - - def _vendor_in(self, request: int, value: int = 0, index: int = 0, - length: int = 64, retries: int = 3) -> bytes: - """Vendor IN control transfer (device-to-host), with retry.""" - for attempt in range(retries): - try: - data = self.dev.ctrl_transfer( - usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, - request, value, index, length, 2000 - ) - if self.verbose: - raw = bytes(data).hex(' ') - print(f" USB IN req=0x{request:02X} val=0x{value:04X} " - f"idx=0x{index:04X} -> [{len(data)}] {raw}") - if len(data) == length: - return bytes(data) - if self.verbose: - print(f" Partial read ({len(data)}/{length}), retry {attempt + 1}") - continue - except usb.core.USBError as e: - if self.verbose: - print(f" USB IN req=0x{request:02X} FAILED: {e}") - if attempt == retries - 1: - raise - return bytes(data) - - def _vendor_out(self, request: int, value: int = 0, index: int = 0, - data: bytes = b'') -> int: - """Vendor OUT control transfer (host-to-device).""" - if self.verbose: - raw = data.hex(' ') if data else "(no data)" - print(f" USB OUT req=0x{request:02X} val=0x{value:04X} " - f"idx=0x{index:04X} data=[{len(data)}] {raw}") - return self.dev.ctrl_transfer( - usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_OUT, - request, value, index, data, 2000 - ) - - # -- Device info -- - - def get_config(self) -> int: - """Read 8PSK config status byte.""" - data = self._vendor_in(CMD_GET_8PSK_CONFIG, length=1) - return data[0] - - def get_fw_version(self) -> dict: - """Read firmware version. Returns dict with version string and date.""" - data = self._vendor_in(CMD_GET_FW_VERS, length=6) - return { - "major": data[2], - "minor": data[1], - "patch": data[0], - "version": f"{data[2]}.{data[1]:02d}.{data[0]}", - "date": f"20{data[5]:02d}-{data[4]:02d}-{data[3]:02d}", - } - - def get_signal_lock(self) -> bool: - """Read signal lock status.""" - data = self._vendor_in(CMD_GET_SIGNAL_LOCK, length=1) - return data[0] != 0 - - def get_signal_strength(self) -> dict: - """Read signal strength. Returns SNR info dict.""" - data = self._vendor_in(CMD_GET_SIGNAL_STRENGTH, length=6) - snr_raw = struct.unpack_from(' int: - """Power on/off the 8PSK demodulator.""" - data = self._vendor_in(CMD_BOOT_8PSK, value=int(on), length=1) - return data[0] - - def start_intersil(self, on: bool = True) -> int: - """Enable/disable LNB power supply.""" - data = self._vendor_in(CMD_START_INTERSIL, value=int(on), length=1) - return data[0] - - def set_lnb_voltage(self, high: bool) -> None: - """Set LNB voltage: high=True for 18V, False for 13V.""" - self._vendor_out(CMD_SET_LNB_VOLTAGE, value=int(high)) - - def set_22khz_tone(self, on: bool) -> None: - """Enable/disable 22 kHz tone.""" - self._vendor_out(CMD_SET_22KHZ_TONE, value=int(on)) - - def set_extra_voltage(self, on: bool) -> None: - """Enable +1V LNB boost: 13->14V, 18->19V.""" - self._vendor_out(CMD_USE_EXTRA_VOLT, value=int(on)) - - # -- Tuning -- - - def tune(self, symbol_rate_sps: int, freq_khz: int, - mod_index: int, fec_index: int) -> None: - """Send TUNE_8PSK with 10-byte payload.""" - payload = struct.pack(' None: - """Start/stop MPEG-2 transport stream.""" - self._vendor_out(CMD_ARM_TRANSFER, value=int(on)) - - def read_stream(self, size: int = EP2_URB_SIZE, - timeout: int = 1000) -> bytes: - """Read a chunk from the TS bulk endpoint.""" - try: - data = self.dev.read(EP2_ADDR, size, timeout) - return bytes(data) - except usb.core.USBTimeoutError: - return b'' - except usb.core.USBError as e: - if self.verbose: - print(f" EP2 read error: {e}") - return b'' - - # -- DiSEqC -- - - def send_diseqc_tone_burst(self, mini_cmd: int) -> None: - """Send tone burst (mini-DiSEqC). 0=SEC_MINI_A, 1=SEC_MINI_B.""" - self._vendor_out(CMD_SEND_DISEQC, value=mini_cmd) - - def send_diseqc_message(self, msg: bytes) -> None: - """Send full DiSEqC message (3-6 bytes).""" - if len(msg) < 3 or len(msg) > 6: - raise ValueError(f"DiSEqC message must be 3-6 bytes, got {len(msg)}") - self._vendor_out(CMD_SEND_DISEQC, value=msg[0], data=msg) - - # -- New commands (v3.02+) -- - - def signal_monitor(self) -> dict: - """ - Fast combined signal read (0xB7). Returns 8 bytes in one transfer: - SNR(2) + AGC1(2) + AGC2(2) + lock(1) + status(1). - """ - data = self._vendor_in(CMD_SIGNAL_MONITOR, length=8) - snr_raw = struct.unpack_from(' dict: - """ - Tune + dwell + signal read in one round-trip (0xB8). - - Sends tune parameters via OUT phase, firmware tunes + waits - dwell_ms + reads signal. Then IN phase returns the result. - """ - dwell_ms = max(1, min(255, dwell_ms)) - payload = struct.pack(' bytes: - """ - Batch read of contiguous BCM4500 indirect registers (0xB9). - - Returns count bytes, one per register. Up to 64 registers - in a single USB transfer (vs. individual 0xB1 reads). - """ - count = max(1, min(64, count)) - data = self._vendor_in(CMD_MULTI_REG_READ, value=start_reg, - index=count, length=count) - return bytes(data) - - # -- Device info (extended) -- - - def get_serial_number(self) -> bytes: - """Read 8-byte serial number from device.""" - return self._vendor_in(CMD_GET_SERIAL_NUMBER, length=8) - - def get_usb_speed(self) -> int: - """Read USB connection speed. 0=unknown, 1=Full, 2=High.""" - data = self._vendor_in(0x07, length=1) - return data[0] - - def get_vendor_string(self) -> str: - """Read vendor string descriptor from FX2.""" - data = self._vendor_in(0x0C, length=64) - return bytes(data).rstrip(b'\x00').decode('ascii', errors='replace') - - def get_product_string(self) -> str: - """Read product string descriptor from FX2.""" - data = self._vendor_in(0x0D, length=64) - return bytes(data).rstrip(b'\x00').decode('ascii', errors='replace') - - # -- FX2 RAM access (standard Cypress A0 vendor request) -- - - def fx2_ram_read(self, addr: int, length: int) -> bytes: - """Read FX2 internal RAM via A0 vendor request. Non-destructive.""" - length = max(1, min(64, length)) - data = self.dev.ctrl_transfer( - usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, - 0xA0, addr, 0, length, 2000 - ) - if self.verbose: - raw = bytes(data).hex(' ') - print(f" RAM IN addr=0x{addr:04X} len={length} -> {raw}") - return bytes(data) - - def fx2_ram_write(self, addr: int, data: bytes) -> int: - """Write FX2 internal RAM via A0 vendor request. Reverts on power cycle.""" - if self.verbose: - raw = data.hex(' ') - print(f" RAM OUT addr=0x{addr:04X} len={len(data)} data={raw}") - return self.dev.ctrl_transfer( - usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_OUT, - 0xA0, addr, 0, data, 2000 - ) - - def fx2_cpu_halt(self) -> None: - """Halt FX2 CPU by writing 1 to CPUCS register (0xE600).""" - self.fx2_ram_write(0xE600, b'\x01') - - def fx2_cpu_start(self) -> None: - """Release FX2 CPU by writing 0 to CPUCS register (0xE600).""" - self.fx2_ram_write(0xE600, b'\x00') - - # -- EEPROM access (via I2C proxy commands) -- - - EEPROM_SLAVE = 0x51 - EEPROM_PAGE_SIZE = 16 - EEPROM_WRITE_CYCLE_MS = 10 - - def eeprom_read(self, offset: int, length: int = 64) -> bytes: - """Read from boot EEPROM at given offset via I2C.""" - return self._vendor_in(CMD_I2C_READ, value=self.EEPROM_SLAVE, - index=offset, length=length) - - def eeprom_write_page(self, offset: int, data: bytes) -> int: - """Write a page (up to 16 bytes) to EEPROM. Caller handles alignment.""" - return self._vendor_out(CMD_I2C_WRITE, value=self.EEPROM_SLAVE, - index=offset, data=data) - - def eeprom_read_all(self, size: int = 16384) -> bytes: - """Read entire EEPROM contents up to size bytes.""" - chunk_size = 64 - result = bytearray() - for offset in range(0, size, chunk_size): - remaining = min(chunk_size, size - offset) - chunk = self.eeprom_read(offset, remaining) - result.extend(chunk) - return bytes(result) - - # -- Diagnostics -- - - def boot_debug(self, mode: int) -> dict: - """ - Run boot diagnostic with specified mode byte. - - Modes: 0x80=no-op, 0x81=GPIO init, 0x82=I2C probe, - 0x83=BCM4500 reset, 0x84=FW load, 0x85=full boot. - Returns 3-byte status: {stage, result, detail}. - """ - data = self._vendor_in(CMD_BOOT_8PSK, value=mode, length=3) - return { - "stage": data[0], - "result": data[1], - "detail": data[2], - } - - def i2c_bus_scan(self) -> list[int]: - """ - Scan I2C bus for responding devices. - - Returns list of 7-bit slave addresses that ACK'd. - The firmware returns a 16-byte bitmap (128 bits for addresses 0-127). - """ - data = self._vendor_in(CMD_I2C_BUS_SCAN, length=16) - addresses = [] - for byte_idx in range(16): - for bit_idx in range(8): - if data[byte_idx] & (1 << bit_idx): - addresses.append(byte_idx * 8 + bit_idx) - return addresses - - def i2c_raw_read(self, slave: int, reg: int) -> int: - """Read a single register from an I2C device.""" - data = self._vendor_in(CMD_I2C_RAW_READ, value=slave, - index=reg, length=1) - return data[0] - - # -- High-level sweep helpers -- - - def sweep_spectrum(self, start_mhz: float, stop_mhz: float, - step_mhz: float = 5.0, dwell_ms: int = 10, - sr_ksps: int = 20000, mod_index: int = 0, - fec_index: int = 5, - callback=None) -> tuple: - """ - Sweep a frequency range and return power measurements. - - Uses TUNE_MONITOR (0xB8) at each step for efficient measurement. - Default tune params: QPSK, auto-FEC, 20 Msps. - - callback(freq_mhz, step_num, total_steps, result) is called - per step if provided. - - Returns (freqs_mhz[], powers_db[], raw_results[]). - """ - sr_sps = sr_ksps * 1000 - freqs = [] - powers = [] - results = [] - - freq = start_mhz - steps = int((stop_mhz - start_mhz) / step_mhz) + 1 - step_num = 0 - - while freq <= stop_mhz: - freq_khz = int(freq * 1000) - result = self.tune_monitor(sr_sps, freq_khz, mod_index, - fec_index, dwell_ms) - freqs.append(freq) - powers.append(result["power_db"]) - results.append(result) - - if callback: - callback(freq, step_num, steps, result) - - step_num += 1 - freq += step_mhz - - return freqs, powers, results - - def ensure_booted(self) -> None: - """Boot demodulator and enable LNB power if not already running.""" - status = self.get_config() - if not (status & 0x01): - self.boot(on=True) - time.sleep(0.5) - status = self.get_config() - if not (status & 0x01): - raise RuntimeError("Device failed to start") - if not (status & 0x04): - self.start_intersil(on=True) - time.sleep(0.3) - - def configure_lnb(self, pol: str = None, band: str = None, - lnb_lo: float = None, disable_lnb: bool = False) -> float: - """ - Configure LNB voltage, tone, and return the effective LO frequency. - - pol: 'H'/'V'/'L'/'R' or None (don't change) - band: 'low'/'high' or None (don't change) - lnb_lo: explicit LO freq in MHz, or None for auto - disable_lnb: True to disable LNB power (for direct input) - """ - if disable_lnb: - self.start_intersil(on=False) - return 0.0 - - if pol: - high_voltage = pol.upper() in ("H", "L") - self.set_lnb_voltage(high_voltage) - - if band: - self.set_22khz_tone(band == "high") - - if lnb_lo is not None: - return lnb_lo - elif band == "high": - return LNB_LO_HIGH - else: - return LNB_LO_LOW - - # -- New commands (v3.03+) -- - - def get_last_error(self) -> int: - """Read last firmware error code (0xBC).""" - data = self._vendor_in(CMD_GET_LAST_ERROR, length=1) - return data[0] - - def get_last_error_str(self) -> str: - """Read last firmware error code as human-readable string.""" - code = self.get_last_error() - return ERROR_NAMES.get(code, f"Unknown (0x{code:02X})") - - def param_sweep(self, start_khz: int, stop_khz: int, step_khz: int, - sr_sps: int, mod_index: int = 0, - fec_index: int = 5) -> bytes: - """ - Parameterized spectrum sweep (0xBA). Returns raw EP2 bulk data - containing u16 LE power values, one per frequency step. - """ - payload = struct.pack(' dict | None: - """ - Adaptive blind scan (0xBB) with AGC pre-check. - Returns lock result dict or None if no lock found. - """ - payload = struct.pack(' None: - """Stop motor movement immediately.""" - self.send_diseqc_message(diseqc_halt()) - - def motor_drive_east(self, steps: int = 0) -> None: - """Drive motor east. steps=0 for continuous, 1-127 for step count.""" - self.send_diseqc_message(diseqc_drive_east(steps)) - - def motor_drive_west(self, steps: int = 0) -> None: - """Drive motor west. steps=0 for continuous, 1-127 for step count.""" - self.send_diseqc_message(diseqc_drive_west(steps)) - - def motor_store_position(self, slot: int) -> None: - """Store current position in slot (0-255).""" - self.send_diseqc_message(diseqc_store_position(slot)) - - def motor_goto_position(self, slot: int) -> None: - """Go to stored position slot (0-255). Slot 0 = reference/zero.""" - self.send_diseqc_message(diseqc_goto_position(slot)) - - def motor_goto_x(self, observer_lon: float, sat_lon: float) -> None: - """USALS GotoX: calculate and drive to satellite position.""" - self.send_diseqc_message(diseqc_goto_x(observer_lon, sat_lon)) - - def motor_set_limit(self, direction: str) -> None: - """Set soft limit at current position. direction: 'east' or 'west'.""" - self.send_diseqc_message(diseqc_set_limit(direction)) - - def motor_disable_limits(self) -> None: - """Disable east/west soft limits.""" - self.send_diseqc_message(diseqc_disable_limits()) - - # -- Streaming diagnostics (v3.04+) -- - - def get_stream_diag(self, reset: bool = False) -> dict: - """Read streaming diagnostics counters (0xBD). - - Returns dict with poll_count, overflow_count, sync_loss, - last_status, last_lock, armed, had_sync. - Set reset=True to clear counters after read. - """ - wval = 1 if reset else 0 - data = self._vendor_in(CMD_GET_STREAM_DIAG, value=wval, length=12) - poll_count = struct.unpack_from(' dict: - """Read I2C hot-plug detection status (0xBE). - - Returns dict with current/previous bus bitmaps, change count, - devices added/removed in last scan, and decoded address lists. - Set reset=True to clear change counter. - Set force_scan=True to trigger immediate I2C rescan. - """ - wval = 2 if force_scan else (1 if reset else 0) - data = self._vendor_in(CMD_GET_HOTPLUG_STATUS, value=wval, length=36) - current_bitmap = bytes(data[0:16]) - changes = struct.unpack_from(' list[int]: - """Convert 16-byte I2C address bitmap to list of 7-bit addresses.""" - addrs = [] - for byte_idx in range(16): - for bit in range(8): - if bitmap[byte_idx] & (1 << bit): - addrs.append((byte_idx << 3) | bit) - return addrs - - -# --- DiSEqC 1.2 command builders --- - -def diseqc_halt() -> bytes: - """Stop positioner movement (DiSEqC 1.2 Halt).""" - return bytes([0xE0, 0x31, 0x60]) - - -def diseqc_drive_east(steps: int = 0) -> bytes: - """Drive east. steps=0 for continuous, 1-127 for step count.""" - return bytes([0xE0, 0x31, 0x68, min(steps, 0x7F)]) - - -def diseqc_drive_west(steps: int = 0) -> bytes: - """Drive west. steps=0 for continuous, 1-127 for step count.""" - return bytes([0xE0, 0x31, 0x69, min(steps, 0x7F)]) - - -def diseqc_store_position(slot: int) -> bytes: - """Store current position in slot (0-255).""" - return bytes([0xE0, 0x31, 0x6A, slot & 0xFF]) - - -def diseqc_goto_position(slot: int) -> bytes: - """Go to stored position (0-255). Slot 0 = reference/zero.""" - return bytes([0xE0, 0x31, 0x6B, slot & 0xFF]) - - -def diseqc_set_limit(direction: str) -> bytes: - """Set east or west software limit at current position.""" - if direction.lower() == "east": - return bytes([0xE0, 0x31, 0x66, 0x00]) - else: - return bytes([0xE0, 0x31, 0x66, 0x01]) - - -def diseqc_disable_limits() -> bytes: - """Disable software limits.""" - return bytes([0xE0, 0x31, 0x63]) - - -def diseqc_goto_x(observer_lon: float, sat_lon: float) -> bytes: - """ - USALS GotoX command (DiSEqC 1.3 extension). - Calculates motor rotation angle from observer and satellite longitude, - then encodes as DiSEqC 1.2 GotoX (E0 31 6E HH LL). - """ - angle = usals_angle(observer_lon, sat_lon) - hh, ll = usals_encode_angle(angle) - return bytes([0xE0, 0x31, 0x6E, hh, ll]) - - -def usals_angle(observer_lon: float, sat_lon: float, - observer_lat: float = 0.0) -> float: - """ - Calculate USALS motor rotation angle in degrees. - - Positive = east, negative = west. - Uses the standard USALS formula from DiSEqC 1.3 spec. - observer_lat defaults to 0 (equator) for simplicity; the motor - corrects for elevation internally. - """ - # Convert to radians - obs_lon_r = math.radians(observer_lon) - sat_lon_r = math.radians(sat_lon) - obs_lat_r = math.radians(observer_lat) - - # Longitude difference - delta_lon = sat_lon_r - obs_lon_r - - # USALS formula: angle = atan2(sin(delta_lon), cos(delta_lon) - R) - # where R = Re / (Re + h) ≈ 0.1513 for GEO orbit - # Simplified for equatorial mount: - angle = math.degrees(math.atan2( - math.sin(delta_lon), - math.cos(delta_lon) - 6378.0 / (6378.0 + 35786.0) - )) - - return angle - - -def usals_encode_angle(angle_deg: float) -> tuple: - """ - Encode USALS angle to DiSEqC 1.3 byte pair (HH, LL). - - Format: HH.HL where HH = integer degrees, H nibble of LL = tenths, - L nibble of LL = sixteenths. Bit 7 of HH = direction (1=west). - """ - west = angle_deg < 0 - angle = abs(angle_deg) - - degrees = int(angle) - fraction = angle - degrees - - # Fraction encoded as: upper nibble = tenths (0-9), - # lower nibble = sixteenths (0-15) - tenths = int(fraction * 10) & 0x0F - sixteenths = int((fraction * 10 - tenths) * 16) & 0x0F - - hh = degrees & 0x7F - if west: - hh |= 0x80 # bit 7 = west - - ll = (tenths << 4) | sixteenths - - return hh, ll +#!/usr/bin/env python3 +""" +Genpix SkyWalker-1 shared library. + +Provides the SkyWalker1 USB interface class, constants, and signal +processing utilities used by skywalker.py and tune.py. +""" + +import sys +import struct +import time +import math + +try: + import usb.core + import usb.util +except ImportError: + print("pyusb required: pip install pyusb") + sys.exit(1) + + +# --- USB identifiers --- + +VENDOR_ID = 0x09C0 +PRODUCT_ID = 0x0203 +EP2_ADDR = 0x82 +EP2_URB_SIZE = 8192 + +# --- Vendor commands --- + +CMD_GET_8PSK_CONFIG = 0x80 +CMD_I2C_WRITE = 0x83 +CMD_I2C_READ = 0x84 +CMD_ARM_TRANSFER = 0x85 +CMD_TUNE_8PSK = 0x86 +CMD_GET_SIGNAL_STRENGTH = 0x87 +CMD_LOAD_BCM4500 = 0x88 +CMD_BOOT_8PSK = 0x89 +CMD_START_INTERSIL = 0x8A +CMD_SET_LNB_VOLTAGE = 0x8B +CMD_SET_22KHZ_TONE = 0x8C +CMD_SEND_DISEQC = 0x8D +CMD_GET_SIGNAL_LOCK = 0x90 +CMD_GET_FW_VERS = 0x92 +CMD_GET_SERIAL_NUMBER = 0x93 +CMD_USE_EXTRA_VOLT = 0x94 + +# Custom commands (v3.01+) +CMD_SPECTRUM_SWEEP = 0xB0 +CMD_RAW_DEMOD_READ = 0xB1 +CMD_RAW_DEMOD_WRITE = 0xB2 +CMD_BLIND_SCAN = 0xB3 +CMD_I2C_BUS_SCAN = 0xB4 +CMD_I2C_RAW_READ = 0xB5 +CMD_I2C_DIAG = 0xB6 + +# Custom commands (v3.02+) +CMD_SIGNAL_MONITOR = 0xB7 +CMD_TUNE_MONITOR = 0xB8 +CMD_MULTI_REG_READ = 0xB9 + +# Custom commands (v3.03+) +CMD_PARAM_SWEEP = 0xBA +CMD_ADAPTIVE_BLIND_SCAN = 0xBB +CMD_GET_LAST_ERROR = 0xBC +CMD_GET_STREAM_DIAG = 0xBD +CMD_GET_HOTPLUG_STATUS = 0xBE + +# Error codes (returned by CMD_GET_LAST_ERROR) +ERR_OK = 0x00 +ERR_I2C_TIMEOUT = 0x01 +ERR_I2C_NAK = 0x02 +ERR_I2C_ARB_LOST = 0x03 +ERR_BCM_NOT_READY = 0x04 +ERR_BCM_TIMEOUT = 0x05 + +ERROR_NAMES = { + ERR_OK: "OK", + ERR_I2C_TIMEOUT: "I2C timeout", + ERR_I2C_NAK: "I2C NAK (no ACK from slave)", + ERR_I2C_ARB_LOST: "I2C arbitration lost", + ERR_BCM_NOT_READY: "BCM4500 not ready", + ERR_BCM_TIMEOUT: "BCM4500 command timeout", +} + +# --- Config status bits --- + +CONFIG_BITS = { + 0x01: ("8PSK Started", "bm8pskStarted"), + 0x02: ("BCM4500 FW Loaded", "bm8pskFW_Loaded"), + 0x04: ("LNB Power On", "bmIntersilOn"), + 0x08: ("DVB Mode", "bmDVBmode"), + 0x10: ("22 kHz Tone", "bm22kHz"), + 0x20: ("18V Selected", "bmSEL18V"), + 0x40: ("DC Tuned", "bmDCtuned"), + 0x80: ("Armed (streaming)", "bmArmed"), +} + +# --- Modulation and FEC tables --- + +MODULATIONS = { + "qpsk": (0, "DVB-S QPSK"), + "turbo-qpsk": (1, "Turbo QPSK"), + "turbo-8psk": (2, "Turbo 8PSK"), + "turbo-16qam": (3, "Turbo 16QAM"), + "dcii-combo": (4, "DCII Combo"), + "dcii-i": (5, "DCII I-stream"), + "dcii-q": (6, "DCII Q-stream"), + "dcii-oqpsk": (7, "DCII Offset QPSK"), + "dss": (8, "DSS QPSK"), + "bpsk": (9, "DVB BPSK"), +} + +FEC_RATES = { + "dvbs": { + "1/2": 0, "2/3": 1, "3/4": 2, "5/6": 3, + "7/8": 4, "auto": 5, "none": 6, + }, + "turbo": { + "1/2": 0, "2/3": 1, "3/4": 2, "5/6": 3, "auto": 4, + }, + "turbo-16qam": { + "3/4": 0, "auto": 0, + }, + "dcii": { + "1/2": 0, "2/3": 1, "6/7": 2, "3/4": 3, "5/11": 4, + "1/2+": 5, "2/3+": 6, "6/7+": 7, "3/4+": 8, "auto": 0, + }, +} + +MOD_FEC_GROUP = { + "qpsk": "dvbs", + "turbo-qpsk": "turbo", + "turbo-8psk": "turbo", + "turbo-16qam": "turbo-16qam", + "dcii-combo": "dcii", + "dcii-i": "dcii", + "dcii-q": "dcii", + "dcii-oqpsk": "dcii", + "dss": "dvbs", + "bpsk": "dvbs", +} + +# --- LNB defaults --- + +LNB_LO_LOW = 9750 # Universal LNB low-band (MHz) +LNB_LO_HIGH = 10600 # Universal LNB high-band (MHz) + +# --- L-band allocations (for annotation) --- + +LBAND_ALLOCATIONS = [ + (1240, 1300, "Amateur 23cm"), + (1525, 1559, "Inmarsat downlink"), + (1559, 1610, "GNSS (GPS L1, Galileo E1)"), + (1610, 1626, "Iridium downlink"), + (1670, 1710, "MetSat (GOES LRIT, NOAA HRPT)"), + (1710, 1785, "LTE/AWS uplink"), + (1920, 2025, "UMTS uplink"), +] + + +# --- Signal processing helpers --- + +def snr_raw_to_db(snr_raw: int) -> float: + """Convert BCM4500 SNR register to dB. Register is dBu * 256.""" + return snr_raw / 256.0 + + +def snr_raw_to_pct(snr_raw: int) -> float: + """Convert raw SNR to percentage (0-100 scale, clamped).""" + scaled = min(snr_raw * 17, 65535) + return (scaled / 65535) * 100 + + +def agc_to_power_db(agc1: int, agc2: int) -> float: + """ + Estimate received power from AGC register values. + + The AGC loop adjusts gain to keep the signal level constant at the + ADC input. Higher AGC = weaker signal (more gain needed). This is + an approximation; the exact mapping depends on the BCM3440 tuner's + gain curve. + + Returns a relative dB value (higher = stronger signal). + """ + # AGC1 is the primary gain control, AGC2 is fine adjustment. + # Invert: low AGC value = high signal = high power. + # Scale to approximate dB with ~40 dB dynamic range. + combined = agc1 + (agc2 >> 4) + if combined == 0: + return 0.0 + # Rough linear-to-dB: 65535 AGC ≈ -40 dB, 0 AGC ≈ 0 dB + return -40.0 * (combined / 65535.0) + + +def detect_peaks(freqs: list, powers: list, threshold_db: float = 3.0) -> list: + """ + Find peaks in a spectrum sweep. + + Returns list of (freq_mhz, power_db, index) tuples for each local + maximum that exceeds the noise floor by threshold_db. + """ + if len(powers) < 3: + return [] + + # Estimate noise floor as the 25th percentile + sorted_p = sorted(powers) + noise_floor = sorted_p[len(sorted_p) // 4] + + peaks = [] + for i in range(1, len(powers) - 1): + if powers[i] > powers[i - 1] and powers[i] > powers[i + 1]: + if powers[i] - noise_floor >= threshold_db: + peaks.append((freqs[i], powers[i], i)) + + return peaks + + +def if_to_rf(if_mhz: float, lnb_lo: float) -> float: + """Convert IF frequency to actual RF frequency given LNB LO.""" + return if_mhz + lnb_lo + + +def rf_to_if(rf_mhz: float, lnb_lo: float) -> float: + """Convert actual RF frequency to IF frequency given LNB LO.""" + return rf_mhz - lnb_lo + + +def signal_bar(pct: float, width: int = 40) -> str: + """Render an ASCII signal strength bar.""" + filled = int(pct / 100 * width) + filled = max(0, min(filled, width)) + bar = '#' * filled + '-' * (width - filled) + return f"[{bar}] {pct:.1f}%" + + +def format_config_bits(status: int) -> list: + """Return list of (bit_name, is_set) tuples for config byte.""" + result = [] + for bit, (name, _field) in CONFIG_BITS.items(): + result.append((name, bool(status & bit))) + return result + + +# --- SkyWalker1 USB interface --- + +class SkyWalker1: + """USB interface to the Genpix SkyWalker-1 DVB-S receiver.""" + + def __init__(self, verbose: bool = False): + self.dev = None + self.detached_intf = None + self.verbose = verbose + + def open(self) -> None: + """Find and claim the SkyWalker-1 USB device.""" + self.dev = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID) + if self.dev is None: + print("SkyWalker-1 not found (VID 0x09C0, PID 0x0203). Is it plugged in?") + sys.exit(1) + + for cfg in self.dev: + for intf in cfg: + if self.dev.is_kernel_driver_active(intf.bInterfaceNumber): + try: + self.dev.detach_kernel_driver(intf.bInterfaceNumber) + self.detached_intf = intf.bInterfaceNumber + if self.verbose: + print(f" Detached kernel driver from interface {intf.bInterfaceNumber}") + except usb.core.USBError as e: + print(f"Cannot detach kernel driver: {e}") + print("The gp8psk module must be unbound first. Try one of:") + print(" sudo modprobe -r dvb_usb_gp8psk") + print(" echo '' | sudo tee /sys/bus/usb/drivers/gp8psk/unbind") + sys.exit(1) + + try: + self.dev.set_configuration() + except usb.core.USBError: + pass + + def close(self) -> None: + """Release device and re-attach kernel driver.""" + if self.dev is None: + return + if self.detached_intf is not None: + try: + usb.util.release_interface(self.dev, self.detached_intf) + self.dev.attach_kernel_driver(self.detached_intf) + if self.verbose: + print("Re-attached kernel driver") + except usb.core.USBError: + print("Note: run 'sudo modprobe dvb_usb_gp8psk' to reload driver") + + def __enter__(self): + self.open() + return self + + def __exit__(self, *exc): + self.close() + + # -- Low-level USB transfers -- + + def _vendor_in(self, request: int, value: int = 0, index: int = 0, + length: int = 64, retries: int = 3) -> bytes: + """Vendor IN control transfer (device-to-host), with retry.""" + for attempt in range(retries): + try: + data = self.dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, + request, value, index, length, 2000 + ) + if self.verbose: + raw = bytes(data).hex(' ') + print(f" USB IN req=0x{request:02X} val=0x{value:04X} " + f"idx=0x{index:04X} -> [{len(data)}] {raw}") + if len(data) == length: + return bytes(data) + if self.verbose: + print(f" Partial read ({len(data)}/{length}), retry {attempt + 1}") + continue + except usb.core.USBError as e: + if self.verbose: + print(f" USB IN req=0x{request:02X} FAILED: {e}") + if attempt == retries - 1: + raise + return bytes(data) + + def _vendor_out(self, request: int, value: int = 0, index: int = 0, + data: bytes = b'') -> int: + """Vendor OUT control transfer (host-to-device).""" + if self.verbose: + raw = data.hex(' ') if data else "(no data)" + print(f" USB OUT req=0x{request:02X} val=0x{value:04X} " + f"idx=0x{index:04X} data=[{len(data)}] {raw}") + return self.dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_OUT, + request, value, index, data, 2000 + ) + + # -- Device info -- + + def get_config(self) -> int: + """Read 8PSK config status byte.""" + data = self._vendor_in(CMD_GET_8PSK_CONFIG, length=1) + return data[0] + + def get_fw_version(self) -> dict: + """Read firmware version. Returns dict with version string and date.""" + data = self._vendor_in(CMD_GET_FW_VERS, length=6) + return { + "major": data[2], + "minor": data[1], + "patch": data[0], + "version": f"{data[2]}.{data[1]:02d}.{data[0]}", + "date": f"20{data[5]:02d}-{data[4]:02d}-{data[3]:02d}", + } + + def get_signal_lock(self) -> bool: + """Read signal lock status.""" + data = self._vendor_in(CMD_GET_SIGNAL_LOCK, length=1) + return data[0] != 0 + + def get_signal_strength(self) -> dict: + """Read signal strength. Returns SNR info dict.""" + data = self._vendor_in(CMD_GET_SIGNAL_STRENGTH, length=6) + snr_raw = struct.unpack_from(' int: + """Power on/off the 8PSK demodulator. + + Custom firmware returns 3 bytes: [config_status, boot_stage, debug]. + Stock firmware returns 1 byte. Request 3 to handle both. + """ + data = self._vendor_in(CMD_BOOT_8PSK, value=int(on), length=3) + return data[0] + + def start_intersil(self, on: bool = True) -> int: + """Enable/disable LNB power supply.""" + data = self._vendor_in(CMD_START_INTERSIL, value=int(on), length=1) + return data[0] + + def set_lnb_voltage(self, high: bool) -> None: + """Set LNB voltage: high=True for 18V, False for 13V.""" + self._vendor_out(CMD_SET_LNB_VOLTAGE, value=int(high)) + + def set_22khz_tone(self, on: bool) -> None: + """Enable/disable 22 kHz tone.""" + self._vendor_out(CMD_SET_22KHZ_TONE, value=int(on)) + + def set_extra_voltage(self, on: bool) -> None: + """Enable +1V LNB boost: 13->14V, 18->19V.""" + self._vendor_out(CMD_USE_EXTRA_VOLT, value=int(on)) + + # -- Tuning -- + + def tune(self, symbol_rate_sps: int, freq_khz: int, + mod_index: int, fec_index: int) -> None: + """Send TUNE_8PSK with 10-byte payload.""" + payload = struct.pack(' None: + """Start/stop MPEG-2 transport stream.""" + self._vendor_out(CMD_ARM_TRANSFER, value=int(on)) + + def read_stream(self, size: int = EP2_URB_SIZE, + timeout: int = 1000) -> bytes: + """Read a chunk from the TS bulk endpoint.""" + try: + data = self.dev.read(EP2_ADDR, size, timeout) + return bytes(data) + except usb.core.USBTimeoutError: + return b'' + except usb.core.USBError as e: + if self.verbose: + print(f" EP2 read error: {e}") + return b'' + + # -- DiSEqC -- + + def send_diseqc_tone_burst(self, mini_cmd: int) -> None: + """Send tone burst (mini-DiSEqC). 0=SEC_MINI_A, 1=SEC_MINI_B.""" + self._vendor_out(CMD_SEND_DISEQC, value=mini_cmd) + + def send_diseqc_message(self, msg: bytes) -> None: + """Send full DiSEqC message (3-6 bytes).""" + if len(msg) < 3 or len(msg) > 6: + raise ValueError(f"DiSEqC message must be 3-6 bytes, got {len(msg)}") + self._vendor_out(CMD_SEND_DISEQC, value=msg[0], data=msg) + + # -- New commands (v3.02+) -- + + def signal_monitor(self) -> dict: + """ + Fast combined signal read (0xB7). Returns 8 bytes in one transfer: + SNR(2) + AGC1(2) + AGC2(2) + lock(1) + status(1). + """ + data = self._vendor_in(CMD_SIGNAL_MONITOR, length=8) + snr_raw = struct.unpack_from(' dict: + """ + Tune + dwell + signal read in one round-trip (0xB8). + + Sends tune parameters via OUT phase, firmware tunes + waits + dwell_ms + reads signal. Then IN phase returns the result. + """ + dwell_ms = max(1, min(255, dwell_ms)) + payload = struct.pack(' bytes: + """ + Batch read of contiguous BCM4500 indirect registers (0xB9). + + Returns count bytes, one per register. Up to 64 registers + in a single USB transfer (vs. individual 0xB1 reads). + """ + count = max(1, min(64, count)) + data = self._vendor_in(CMD_MULTI_REG_READ, value=start_reg, + index=count, length=count) + return bytes(data) + + # -- Device info (extended) -- + + def get_serial_number(self) -> bytes: + """Read 8-byte serial number from device.""" + return self._vendor_in(CMD_GET_SERIAL_NUMBER, length=8) + + def get_usb_speed(self) -> int: + """Read USB connection speed. 0=unknown, 1=Full, 2=High.""" + data = self._vendor_in(0x07, length=1) + return data[0] + + def get_vendor_string(self) -> str: + """Read vendor string descriptor from FX2.""" + data = self._vendor_in(0x0C, length=64) + return bytes(data).rstrip(b'\x00').decode('ascii', errors='replace') + + def get_product_string(self) -> str: + """Read product string descriptor from FX2.""" + data = self._vendor_in(0x0D, length=64) + return bytes(data).rstrip(b'\x00').decode('ascii', errors='replace') + + # -- FX2 RAM access (standard Cypress A0 vendor request) -- + + def fx2_ram_read(self, addr: int, length: int) -> bytes: + """Read FX2 internal RAM via A0 vendor request. Non-destructive.""" + length = max(1, min(64, length)) + data = self.dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, + 0xA0, addr, 0, length, 2000 + ) + if self.verbose: + raw = bytes(data).hex(' ') + print(f" RAM IN addr=0x{addr:04X} len={length} -> {raw}") + return bytes(data) + + def fx2_ram_write(self, addr: int, data: bytes) -> int: + """Write FX2 internal RAM via A0 vendor request. Reverts on power cycle.""" + if self.verbose: + raw = data.hex(' ') + print(f" RAM OUT addr=0x{addr:04X} len={len(data)} data={raw}") + return self.dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_OUT, + 0xA0, addr, 0, data, 2000 + ) + + def fx2_cpu_halt(self) -> None: + """Halt FX2 CPU by writing 1 to CPUCS register (0xE600).""" + self.fx2_ram_write(0xE600, b'\x01') + + def fx2_cpu_start(self) -> None: + """Release FX2 CPU by writing 0 to CPUCS register (0xE600).""" + self.fx2_ram_write(0xE600, b'\x00') + + # -- EEPROM access (via I2C proxy commands) -- + + EEPROM_SLAVE = 0x51 + EEPROM_PAGE_SIZE = 16 + EEPROM_WRITE_CYCLE_MS = 10 + + def eeprom_read(self, offset: int, length: int = 64) -> bytes: + """Read from boot EEPROM at given offset via I2C.""" + return self._vendor_in(CMD_I2C_READ, value=self.EEPROM_SLAVE, + index=offset, length=length) + + def eeprom_write_page(self, offset: int, data: bytes) -> int: + """Write a page (up to 16 bytes) to EEPROM. Caller handles alignment.""" + return self._vendor_out(CMD_I2C_WRITE, value=self.EEPROM_SLAVE, + index=offset, data=data) + + def eeprom_read_all(self, size: int = 16384) -> bytes: + """Read entire EEPROM contents up to size bytes.""" + chunk_size = 64 + result = bytearray() + for offset in range(0, size, chunk_size): + remaining = min(chunk_size, size - offset) + chunk = self.eeprom_read(offset, remaining) + result.extend(chunk) + return bytes(result) + + # -- Diagnostics -- + + def boot_debug(self, mode: int) -> dict: + """ + Run boot diagnostic with specified mode byte. + + Modes: 0x80=no-op, 0x81=GPIO init, 0x82=I2C probe, + 0x83=BCM4500 reset, 0x84=FW load, 0x85=full boot. + Returns 3-byte status: {stage, result, detail}. + """ + data = self._vendor_in(CMD_BOOT_8PSK, value=mode, length=3) + return { + "stage": data[0], + "result": data[1], + "detail": data[2], + } + + def i2c_bus_scan(self) -> list[int]: + """ + Scan I2C bus for responding devices. + + Returns list of 7-bit slave addresses that ACK'd. + The firmware returns a 16-byte bitmap (128 bits for addresses 0-127). + """ + data = self._vendor_in(CMD_I2C_BUS_SCAN, length=16) + addresses = [] + for byte_idx in range(16): + for bit_idx in range(8): + if data[byte_idx] & (1 << bit_idx): + addresses.append(byte_idx * 8 + bit_idx) + return addresses + + def i2c_raw_read(self, slave: int, reg: int) -> int: + """Read a single register from an I2C device.""" + data = self._vendor_in(CMD_I2C_RAW_READ, value=slave, + index=reg, length=1) + return data[0] + + # -- High-level sweep helpers -- + + def sweep_spectrum(self, start_mhz: float, stop_mhz: float, + step_mhz: float = 5.0, dwell_ms: int = 10, + sr_ksps: int = 20000, mod_index: int = 0, + fec_index: int = 5, + callback=None) -> tuple: + """ + Sweep a frequency range and return power measurements. + + Uses TUNE_MONITOR (0xB8) at each step for efficient measurement. + Default tune params: QPSK, auto-FEC, 20 Msps. + + callback(freq_mhz, step_num, total_steps, result) is called + per step if provided. + + Returns (freqs_mhz[], powers_db[], raw_results[]). + """ + sr_sps = sr_ksps * 1000 + freqs = [] + powers = [] + results = [] + + freq = start_mhz + steps = int((stop_mhz - start_mhz) / step_mhz) + 1 + step_num = 0 + + while freq <= stop_mhz: + freq_khz = int(freq * 1000) + result = self.tune_monitor(sr_sps, freq_khz, mod_index, + fec_index, dwell_ms) + freqs.append(freq) + powers.append(result["power_db"]) + results.append(result) + + if callback: + callback(freq, step_num, steps, result) + + step_num += 1 + freq += step_mhz + + return freqs, powers, results + + def ensure_booted(self) -> None: + """Boot demodulator and enable LNB power if not already running.""" + status = self.get_config() + if not (status & 0x01): + self.boot(on=True) + time.sleep(0.5) + status = self.get_config() + if not (status & 0x01): + raise RuntimeError("Device failed to start") + if not (status & 0x04): + self.start_intersil(on=True) + time.sleep(0.3) + + def configure_lnb(self, pol: str = None, band: str = None, + lnb_lo: float = None, disable_lnb: bool = False) -> float: + """ + Configure LNB voltage, tone, and return the effective LO frequency. + + pol: 'H'/'V'/'L'/'R' or None (don't change) + band: 'low'/'high' or None (don't change) + lnb_lo: explicit LO freq in MHz, or None for auto + disable_lnb: True to disable LNB power (for direct input) + """ + if disable_lnb: + self.start_intersil(on=False) + return 0.0 + + if pol: + high_voltage = pol.upper() in ("H", "L") + self.set_lnb_voltage(high_voltage) + + if band: + self.set_22khz_tone(band == "high") + + if lnb_lo is not None: + return lnb_lo + elif band == "high": + return LNB_LO_HIGH + else: + return LNB_LO_LOW + + # -- New commands (v3.03+) -- + + def get_last_error(self) -> int: + """Read last firmware error code (0xBC).""" + data = self._vendor_in(CMD_GET_LAST_ERROR, length=1) + return data[0] + + def get_last_error_str(self) -> str: + """Read last firmware error code as human-readable string.""" + code = self.get_last_error() + return ERROR_NAMES.get(code, f"Unknown (0x{code:02X})") + + def param_sweep(self, start_khz: int, stop_khz: int, step_khz: int, + sr_sps: int, mod_index: int = 0, + fec_index: int = 5) -> bytes: + """ + Parameterized spectrum sweep (0xBA). Returns raw EP2 bulk data + containing u16 LE power values, one per frequency step. + """ + payload = struct.pack(' dict | None: + """ + Adaptive blind scan (0xBB) with AGC pre-check. + Returns lock result dict or None if no lock found. + """ + payload = struct.pack(' None: + """Stop motor movement immediately.""" + self.send_diseqc_message(diseqc_halt()) + + def motor_drive_east(self, steps: int = 0) -> None: + """Drive motor east. steps=0 for continuous, 1-127 for step count.""" + self.send_diseqc_message(diseqc_drive_east(steps)) + + def motor_drive_west(self, steps: int = 0) -> None: + """Drive motor west. steps=0 for continuous, 1-127 for step count.""" + self.send_diseqc_message(diseqc_drive_west(steps)) + + def motor_store_position(self, slot: int) -> None: + """Store current position in slot (0-255).""" + self.send_diseqc_message(diseqc_store_position(slot)) + + def motor_goto_position(self, slot: int) -> None: + """Go to stored position slot (0-255). Slot 0 = reference/zero.""" + self.send_diseqc_message(diseqc_goto_position(slot)) + + def motor_goto_x(self, observer_lon: float, sat_lon: float) -> None: + """USALS GotoX: calculate and drive to satellite position.""" + self.send_diseqc_message(diseqc_goto_x(observer_lon, sat_lon)) + + def motor_set_limit(self, direction: str) -> None: + """Set soft limit at current position. direction: 'east' or 'west'.""" + self.send_diseqc_message(diseqc_set_limit(direction)) + + def motor_disable_limits(self) -> None: + """Disable east/west soft limits.""" + self.send_diseqc_message(diseqc_disable_limits()) + + # -- Streaming diagnostics (v3.04+) -- + + def get_stream_diag(self, reset: bool = False) -> dict: + """Read streaming diagnostics counters (0xBD). + + Returns dict with poll_count, overflow_count, sync_loss, + last_status, last_lock, armed, had_sync. + Set reset=True to clear counters after read. + """ + wval = 1 if reset else 0 + data = self._vendor_in(CMD_GET_STREAM_DIAG, value=wval, length=12) + poll_count = struct.unpack_from(' dict: + """Read I2C hot-plug detection status (0xBE). + + Returns dict with current/previous bus bitmaps, change count, + devices added/removed in last scan, and decoded address lists. + Set reset=True to clear change counter. + Set force_scan=True to trigger immediate I2C rescan. + """ + wval = 2 if force_scan else (1 if reset else 0) + data = self._vendor_in(CMD_GET_HOTPLUG_STATUS, value=wval, length=36) + current_bitmap = bytes(data[0:16]) + changes = struct.unpack_from(' list[int]: + """Convert 16-byte I2C address bitmap to list of 7-bit addresses.""" + addrs = [] + for byte_idx in range(16): + for bit in range(8): + if bitmap[byte_idx] & (1 << bit): + addrs.append((byte_idx << 3) | bit) + return addrs + + +# --- DiSEqC 1.2 command builders --- + +def diseqc_halt() -> bytes: + """Stop positioner movement (DiSEqC 1.2 Halt).""" + return bytes([0xE0, 0x31, 0x60]) + + +def diseqc_drive_east(steps: int = 0) -> bytes: + """Drive east. steps=0 for continuous, 1-127 for step count.""" + return bytes([0xE0, 0x31, 0x68, min(steps, 0x7F)]) + + +def diseqc_drive_west(steps: int = 0) -> bytes: + """Drive west. steps=0 for continuous, 1-127 for step count.""" + return bytes([0xE0, 0x31, 0x69, min(steps, 0x7F)]) + + +def diseqc_store_position(slot: int) -> bytes: + """Store current position in slot (0-255).""" + return bytes([0xE0, 0x31, 0x6A, slot & 0xFF]) + + +def diseqc_goto_position(slot: int) -> bytes: + """Go to stored position (0-255). Slot 0 = reference/zero.""" + return bytes([0xE0, 0x31, 0x6B, slot & 0xFF]) + + +def diseqc_set_limit(direction: str) -> bytes: + """Set east or west software limit at current position.""" + if direction.lower() == "east": + return bytes([0xE0, 0x31, 0x66, 0x00]) + else: + return bytes([0xE0, 0x31, 0x66, 0x01]) + + +def diseqc_disable_limits() -> bytes: + """Disable software limits.""" + return bytes([0xE0, 0x31, 0x63]) + + +def diseqc_goto_x(observer_lon: float, sat_lon: float) -> bytes: + """ + USALS GotoX command (DiSEqC 1.3 extension). + Calculates motor rotation angle from observer and satellite longitude, + then encodes as DiSEqC 1.2 GotoX (E0 31 6E HH LL). + """ + angle = usals_angle(observer_lon, sat_lon) + hh, ll = usals_encode_angle(angle) + return bytes([0xE0, 0x31, 0x6E, hh, ll]) + + +def usals_angle(observer_lon: float, sat_lon: float, + observer_lat: float = 0.0) -> float: + """ + Calculate USALS motor rotation angle in degrees. + + Positive = east, negative = west. + Uses the standard USALS formula from DiSEqC 1.3 spec. + observer_lat defaults to 0 (equator) for simplicity; the motor + corrects for elevation internally. + """ + # Convert to radians + obs_lon_r = math.radians(observer_lon) + sat_lon_r = math.radians(sat_lon) + obs_lat_r = math.radians(observer_lat) + + # Longitude difference + delta_lon = sat_lon_r - obs_lon_r + + # USALS formula: angle = atan2(sin(delta_lon), cos(delta_lon) - R) + # where R = Re / (Re + h) ≈ 0.1513 for GEO orbit + # Simplified for equatorial mount: + angle = math.degrees(math.atan2( + math.sin(delta_lon), + math.cos(delta_lon) - 6378.0 / (6378.0 + 35786.0) + )) + + return angle + + +def usals_encode_angle(angle_deg: float) -> tuple: + """ + Encode USALS angle to DiSEqC 1.3 byte pair (HH, LL). + + Format: HH.HL where HH = integer degrees, H nibble of LL = tenths, + L nibble of LL = sixteenths. Bit 7 of HH = direction (1=west). + """ + west = angle_deg < 0 + angle = abs(angle_deg) + + degrees = int(angle) + fraction = angle - degrees + + # Fraction encoded as: upper nibble = tenths (0-9), + # lower nibble = sixteenths (0-15) + tenths = int(fraction * 10) & 0x0F + sixteenths = int((fraction * 10 - tenths) * 16) & 0x0F + + hh = degrees & 0x7F + if west: + hh |= 0x80 # bit 7 = west + + ll = (tenths << 4) | sixteenths + + return hh, ll