From 5710584267e1ce6469f78cb93decdb86655efbba Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Wed, 11 Feb 2026 19:46:50 -0700 Subject: [PATCH] Add custom FX2 firmware and RAM loader for open-source development Custom firmware (SDCC + fx2lib) implements all stock vendor commands (0x80-0x94) plus new commands for spectrum sweep (0xB0), raw BCM4500 register access (0xB1/0xB2), and blind scan (0xB3). Compiles to 6.3KB of code with healthy RAM margins. RAM loader (fw_load.py) uses the FX2 0xA0 vendor request to load firmware into RAM without touching EEPROM -- power cycle restores factory firmware. Supports Intel HEX and raw binary formats. --- .gitignore | 7 + firmware/Makefile | 12 + firmware/dscr.a51 | 230 +++++++++++ firmware/skywalker1.c | 864 ++++++++++++++++++++++++++++++++++++++++++ tools/fw_load.py | 616 ++++++++++++++++++++++++++++++ 5 files changed, 1729 insertions(+) create mode 100644 firmware/Makefile create mode 100644 firmware/dscr.a51 create mode 100644 firmware/skywalker1.c create mode 100755 tools/fw_load.py diff --git a/.gitignore b/.gitignore index bac1a7a..3363db1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,9 @@ # Empty extraction directories docs/diseqc/images/ + +# Third-party dependencies +firmware/fx2lib/ + +# Build artifacts +firmware/build/ +tools/__pycache__/ diff --git a/firmware/Makefile b/firmware/Makefile new file mode 100644 index 0000000..7df8558 --- /dev/null +++ b/firmware/Makefile @@ -0,0 +1,12 @@ +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 diff --git a/firmware/dscr.a51 b/firmware/dscr.a51 new file mode 100644 index 0000000..d629fe6 --- /dev/null +++ b/firmware/dscr.a51 @@ -0,0 +1,230 @@ +; USB Descriptors for Genpix SkyWalker-1 Custom Firmware +; +; VID=0x09C0, PID=0x0203 +; Single interface, EP2 IN bulk 512-byte for TS data + +.module DEV_DSCR + +; descriptor types +DSCR_DEVICE_TYPE=1 +DSCR_CONFIG_TYPE=2 +DSCR_STRING_TYPE=3 +DSCR_INTERFACE_TYPE=4 +DSCR_ENDPOINT_TYPE=5 +DSCR_DEVQUAL_TYPE=6 + +; for the repeating interfaces +DSCR_INTERFACE_LEN=9 +DSCR_ENDPOINT_LEN=7 + +; endpoint types +ENDPOINT_TYPE_CONTROL=0 +ENDPOINT_TYPE_ISO=1 +ENDPOINT_TYPE_BULK=2 +ENDPOINT_TYPE_INT=3 + + .globl _dev_dscr, _dev_qual_dscr, _highspd_dscr, _fullspd_dscr, _dev_strings, _dev_strings_end + .area DSCR_AREA (CODE) + +_dev_dscr: + .db dev_dscr_end-_dev_dscr ; len + .db DSCR_DEVICE_TYPE ; type + .dw 0x0002 ; usb 2.0 + .db 0xff ; class (vendor specific) + .db 0xff ; subclass (vendor specific) + .db 0xff ; protocol (vendor specific) + .db 64 ; packet size (ep0) + .dw 0xC009 ; vendor id 0x09C0 (byte-swapped) + .dw 0x0302 ; product id 0x0203 (byte-swapped) + .dw 0x0100 ; version id + .db 1 ; manufacturer str idx + .db 2 ; product str idx + .db 3 ; serial str idx + .db 1 ; n configurations +dev_dscr_end: + +_dev_qual_dscr: + .db dev_qualdscr_end-_dev_qual_dscr + .db DSCR_DEVQUAL_TYPE + .dw 0x0002 ; usb 2.0 + .db 0xff + .db 0xff + .db 0xff + .db 64 ; max packet + .db 1 ; n configs + .db 0 ; extra reserved byte +dev_qualdscr_end: + +; --- High speed configuration descriptor --- +_highspd_dscr: + .db highspd_dscr_end-_highspd_dscr + .db DSCR_CONFIG_TYPE + .db (highspd_dscr_realend-_highspd_dscr) % 256 ; total length lsb + .db (highspd_dscr_realend-_highspd_dscr) / 256 ; total length msb + .db 1 ; n interfaces + .db 1 ; config number + .db 0 ; config string + .db 0x80 ; attrs = bus powered, no wakeup + .db 0xFA ; max power = 500mA (0xFA * 2 = 500) +highspd_dscr_end: + +; interface 0 + .db DSCR_INTERFACE_LEN + .db DSCR_INTERFACE_TYPE + .db 0 ; index + .db 0 ; alt setting idx + .db 1 ; n endpoints (EP2 IN only) + .db 0xff ; class (vendor specific) + .db 0xff + .db 0xff + .db 4 ; string index + +; endpoint 2 IN (0x82) -- MPEG-2 transport stream bulk + .db DSCR_ENDPOINT_LEN + .db DSCR_ENDPOINT_TYPE + .db 0x82 ; ep2 dir=IN and address + .db ENDPOINT_TYPE_BULK ; type + .db 0x00 ; max packet LSB + .db 0x02 ; max packet size=512 bytes + .db 0x00 ; polling interval + +highspd_dscr_realend: + + .even +; --- Full speed configuration descriptor --- +_fullspd_dscr: + .db fullspd_dscr_end-_fullspd_dscr + .db DSCR_CONFIG_TYPE + .db (fullspd_dscr_realend-_fullspd_dscr) % 256 ; total length lsb + .db (fullspd_dscr_realend-_fullspd_dscr) / 256 ; total length msb + .db 1 ; n interfaces + .db 1 ; config number + .db 0 ; config string + .db 0x80 ; attrs = bus powered, no wakeup + .db 0xFA ; max power = 500mA +fullspd_dscr_end: + +; interface 0 + .db DSCR_INTERFACE_LEN + .db DSCR_INTERFACE_TYPE + .db 0 ; index + .db 0 ; alt setting idx + .db 1 ; n endpoints + .db 0xff ; class (vendor specific) + .db 0xff + .db 0xff + .db 4 ; string index + +; endpoint 2 IN (0x82) -- bulk 64 byte at full speed + .db DSCR_ENDPOINT_LEN + .db DSCR_ENDPOINT_TYPE + .db 0x82 ; ep2 dir=IN and address + .db ENDPOINT_TYPE_BULK ; type + .db 0x40 ; max packet LSB = 64 + .db 0x00 ; max packet size MSB + .db 0x00 ; polling interval + +fullspd_dscr_realend: + +.even +_dev_strings: + +; string 0 -- language descriptor +_string0: + .db string0end-_string0 + .db DSCR_STRING_TYPE + .db 0x09, 0x04 ; English (US) +string0end: + +; string 1 -- manufacturer +_string1: + .db string1end-_string1 + .db DSCR_STRING_TYPE + .ascii 'G' + .db 0 + .ascii 'e' + .db 0 + .ascii 'n' + .db 0 + .ascii 'p' + .db 0 + .ascii 'i' + .db 0 + .ascii 'x' + .db 0 +string1end: + +; string 2 -- product +_string2: + .db string2end-_string2 + .db DSCR_STRING_TYPE + .ascii 'S' + .db 0 + .ascii 'k' + .db 0 + .ascii 'y' + .db 0 + .ascii 'W' + .db 0 + .ascii 'a' + .db 0 + .ascii 'l' + .db 0 + .ascii 'k' + .db 0 + .ascii 'e' + .db 0 + .ascii 'r' + .db 0 + .ascii '-' + .db 0 + .ascii '1' + .db 0 + .ascii ' ' + .db 0 + .ascii 'C' + .db 0 + .ascii 'u' + .db 0 + .ascii 's' + .db 0 + .ascii 't' + .db 0 + .ascii 'o' + .db 0 + .ascii 'm' + .db 0 +string2end: + +; string 3 -- serial number +_string3: + .db string3end-_string3 + .db DSCR_STRING_TYPE + .ascii '0' + .db 0 + .ascii '0' + .db 0 + .ascii '0' + .db 0 + .ascii '1' + .db 0 +string3end: + +; string 4 -- interface +_string4: + .db string4end-_string4 + .db DSCR_STRING_TYPE + .ascii 'D' + .db 0 + .ascii 'V' + .db 0 + .ascii 'B' + .db 0 + .ascii '-' + .db 0 + .ascii 'S' + .db 0 +string4end: + +_dev_strings_end: + .dw 0x0000 diff --git a/firmware/skywalker1.c b/firmware/skywalker1.c new file mode 100644 index 0000000..9188cfa --- /dev/null +++ b/firmware/skywalker1.c @@ -0,0 +1,864 @@ +/* + * Genpix SkyWalker-1 Custom Firmware + * For Cypress CY7C68013A (FX2LP) + Broadcom BCM4500 demodulator + * + * Stock-compatible vendor commands (0x80-0x94) plus new + * spectrum sweep, raw demod access, and blind scan commands (0xB0-0xB3). + * + * SDCC + fx2lib toolchain. Loaded into FX2 RAM for testing. + */ + +#include +#include +#include +#include +#include +#include +#include + +#define SYNCDELAY SYNCDELAY4 + +/* BCM4500 I2C address (7-bit) */ +#define BCM4500_ADDR 0x10 + +/* BCM4500 indirect register protocol registers */ +#define BCM_REG_PAGE 0xA6 +#define BCM_REG_DATA 0xA7 +#define BCM_REG_CMD 0xA8 + +/* BCM4500 status registers */ +#define BCM_REG_STATUS 0xA2 +#define BCM_REG_LOCK 0xA4 + +/* BCM commands */ +#define BCM_CMD_READ 0x01 +#define BCM_CMD_WRITE 0x03 + +/* vendor command IDs */ +#define GET_8PSK_CONFIG 0x80 +#define TUNE_8PSK 0x86 +#define GET_SIGNAL_STRENGTH 0x87 +#define BOOT_8PSK 0x89 +#define START_INTERSIL 0x8A +#define SET_LNB_VOLTAGE 0x8B +#define SET_22KHZ_TONE 0x8C +#define SEND_DISEQC 0x8D +#define ARM_TRANSFER 0x85 +#define GET_SIGNAL_LOCK 0x90 +#define GET_FW_VERS 0x92 +#define USE_EXTRA_VOLT 0x94 + +/* custom vendor commands */ +#define SPECTRUM_SWEEP 0xB0 +#define RAW_DEMOD_READ 0xB1 +#define RAW_DEMOD_WRITE 0xB2 +#define BLIND_SCAN 0xB3 + +/* configuration status byte bits */ +#define BM_STARTED 0x01 +#define BM_FW_LOADED 0x02 +#define BM_INTERSIL 0x04 +#define BM_DVB_MODE 0x08 +#define BM_22KHZ 0x10 +#define BM_SEL18V 0x20 +#define BM_DC_TUNED 0x40 +#define BM_ARMED 0x80 + +/* GPIO pin definitions for v2.06 hardware */ +#define PIN_22KHZ 0x08 /* P0.3 */ +#define PIN_LNB_VOLT 0x10 /* P0.4 */ +#define PIN_DISEQC 0x80 /* P0.7 */ + +/* configuration status byte -- stored in ordinary variable */ +static volatile BYTE config_status; + +/* ISR flag */ +volatile __bit got_sud; + +/* I2C scratch buffers in xdata */ +static __xdata BYTE i2c_buf[8]; +static __xdata BYTE i2c_rd[8]; + +/* ---------- BCM4500 I2C helpers ---------- */ + +/* + * Write one byte to a BCM4500 direct I2C register (subaddr). + * This writes to the I2C register directly, not through the + * indirect protocol. + */ +static BOOL bcm_direct_write(BYTE reg, BYTE val) { + i2c_buf[0] = val; + return i2c_write(BCM4500_ADDR, 1, ®, 1, i2c_buf); +} + +/* + * Read one byte from a BCM4500 direct I2C register. + */ +static BOOL bcm_direct_read(BYTE reg, BYTE *val) { + BYTE r = reg; + if (!i2c_write(BCM4500_ADDR, 1, &r, 0, NULL)) + return FALSE; + if (!i2c_read(BCM4500_ADDR, 1, val)) + return FALSE; + return TRUE; +} + +/* + * Write a value to a BCM4500 indirect register. + * Protocol: + * 1. Write page/register to 0xA6 + * 2. Write data to 0xA7 + * 3. Write 0x03 to 0xA8 (execute write) + */ +static BOOL bcm_indirect_write(BYTE reg, BYTE val) { + if (!bcm_direct_write(BCM_REG_PAGE, reg)) + return FALSE; + if (!bcm_direct_write(BCM_REG_DATA, val)) + return FALSE; + if (!bcm_direct_write(BCM_REG_CMD, BCM_CMD_WRITE)) + return FALSE; + return TRUE; +} + +/* + * Read a value from a BCM4500 indirect register. + * Protocol: + * 1. Write page/register to 0xA6 + * 2. Write 0x01 to 0xA8 (execute read) + * 3. Read data from 0xA7 + */ +static BOOL bcm_indirect_read(BYTE reg, BYTE *val) { + if (!bcm_direct_write(BCM_REG_PAGE, reg)) + return FALSE; + if (!bcm_direct_write(BCM_REG_CMD, BCM_CMD_READ)) + return FALSE; + if (!bcm_direct_read(BCM_REG_DATA, val)) + return FALSE; + return TRUE; +} + +/* + * Write a multi-byte block to BCM4500 via indirect protocol. + * Page select, then N data bytes to 0xA7, then commit with 0x03. + */ +static BOOL bcm_indirect_write_block(BYTE page, __xdata BYTE *data, BYTE len) { + BYTE reg; + + reg = BCM_REG_PAGE; + i2c_buf[0] = page; + if (!i2c_write(BCM4500_ADDR, 1, ®, 1, i2c_buf)) + return FALSE; + + reg = BCM_REG_DATA; + if (!i2c_write(BCM4500_ADDR, 1, ®, len, data)) + return FALSE; + + if (!bcm_direct_write(BCM_REG_CMD, BCM_CMD_WRITE)) + return FALSE; + + return TRUE; +} + +/* + * Poll BCM4500 for readiness. Reads status registers and waits + * for the command register to indicate idle. + */ +static BOOL bcm_poll_ready(void) { + BYTE i, val; + for (i = 0; i < 20; i++) { + if (bcm_direct_read(BCM_REG_CMD, &val)) { + if (!(val & 0x01)) + return TRUE; + } + delay(5); + } + return FALSE; +} + +/* ---------- GPIF streaming ---------- */ + +static void gpif_start(void) { + if (config_status & BM_ARMED) + return; + + config_status |= BM_ARMED; + + /* IFCONFIG: internal 48MHz, GPIF master, async, clock output */ + IFCONFIG = 0xEE; + SYNCDELAY; + + /* EP2FIFOCFG: AUTOIN, ZEROLENIN, 8-bit */ + EP2FIFOCFG = 0x0C; + SYNCDELAY; + + /* FLOWSTATE: enable flow state + FS[3] */ + FLOWSTATE |= 0x09; + + /* Set transaction count large (effectively infinite) */ + GPIFTCB3 = 0x80; + SYNCDELAY; + GPIFTCB2 = 0x00; + SYNCDELAY; + GPIFTCB1 = 0x00; + SYNCDELAY; + GPIFTCB0 = 0x00; + SYNCDELAY; + + /* Assert P3.5 low (BCM4500 TS enable) briefly */ + IOD &= ~0x20; + + /* Wait for GPIF idle */ + while (!(GPIFTRIG & 0x80)) + ; + + IOD |= 0x20; + + /* Trigger continuous GPIF read into EP2 */ + GPIFTRIG = 0x04; + + /* P0.7 low = streaming indicator */ + IOA &= ~0x80; +} + +static void gpif_stop(void) { + if (!(config_status & BM_ARMED)) + return; + + /* P0.7 high = streaming stopped */ + IOA |= 0x80; + + /* Force-flush current FIFO buffer */ + EP2FIFOBCH = 0xFF; + SYNCDELAY; + + /* Wait for GPIF idle */ + while (!(GPIFTRIG & 0x80)) + ; + + /* Skip/discard partial EP2 packet */ + OUTPKTEND = 0x82; + SYNCDELAY; + + config_status &= ~BM_ARMED; + + /* De-assert all BCM4500 control lines on P3 */ + IOD |= 0xE0; +} + +/* ---------- DiSEqC tone burst ---------- */ + +/* + * Send a tone burst (mini DiSEqC). This is the simpler variant. + * Tone burst A: unmodulated 22kHz for 12.5ms + * Tone burst B: modulated (not implemented yet) + * + * Uses Timer2 for timing as the stock firmware does. + */ +static void diseqc_tone_burst(BYTE sat_b) { + BYTE i; + + (void)sat_b; /* both A and B send 22kHz burst for now */ + + /* Configure Timer2 auto-reload */ + /* CKCON.T2M = 0 -> Timer2 clk = 48MHz/12 = 4MHz */ + CKCON &= ~0x20; + T2CON = 0x04; /* auto-reload, running */ + RCAP2H = 0xF8; + RCAP2L = 0x2F; /* reload = 63535 -> ~500us tick */ + TL2 = 0xFF; + TH2 = 0xFF; /* force immediate overflow */ + + /* Pre-burst settling: 15 ticks (~7.5ms) with carrier off */ + IOA &= ~PIN_22KHZ; + TF2 = 0; + for (i = 0; i < 15; i++) { + while (!TF2) + ; + TF2 = 0; + } + + /* Burst: 25 ticks (~12.5ms) with carrier on */ + IOA |= PIN_22KHZ; + for (i = 0; i < 25; i++) { + while (!TF2) + ; + TF2 = 0; + } + + /* Carrier off */ + IOA &= ~PIN_22KHZ; + + /* Post-burst settling: 5 ticks (~2.5ms) */ + for (i = 0; i < 5; i++) { + while (!TF2) + ; + TF2 = 0; + } + + /* Stop Timer2 */ + TR2 = 0; +} + +/* ---------- Spectrum sweep (0xB0) ---------- */ + +/* + * Step through frequencies from start to stop, reading signal strength + * at each step. Results are packed as u16 LE power values into EP2 bulk. + * + * The host sends a 10-byte payload via EP0: + * [0..3] start_freq (u32 LE, kHz) + * [4..7] stop_freq (u32 LE, kHz) + * [8..9] step_khz (u16 LE) + * + * We tune to each frequency with a fixed symbol rate (e.g. 20000 sps, DVB-S + * QPSK auto-FEC), read the SNR register, and pack u16 results into EP2. + * + * The sweep uses a simple approach: program freq via BCM4500 indirect write + * at each step, wait briefly, and read the signal energy register. + */ +static void do_spectrum_sweep(void) { + static __xdata DWORD start_freq, stop_freq, cur_freq; + static __xdata WORD step_khz; + WORD buf_idx; + BYTE snr_lo, snr_hi; + + /* Parse the 10-byte EP0 payload */ + start_freq = (DWORD)EP0BUF[0] | + ((DWORD)EP0BUF[1] << 8) | + ((DWORD)EP0BUF[2] << 16) | + ((DWORD)EP0BUF[3] << 24); + stop_freq = (DWORD)EP0BUF[4] | + ((DWORD)EP0BUF[5] << 8) | + ((DWORD)EP0BUF[6] << 16) | + ((DWORD)EP0BUF[7] << 24); + step_khz = (WORD)EP0BUF[8] | ((WORD)EP0BUF[9] << 8); + + if (step_khz == 0) + step_khz = 1000; + + buf_idx = 0; + cur_freq = start_freq; + + while (cur_freq <= stop_freq) { + /* + * Program frequency into BCM4500 via indirect write. + * The BCM4500 expects big-endian frequency bytes at page 0. + * We write 4 freq bytes (BE) to the data register. + */ + i2c_buf[0] = (BYTE)(cur_freq >> 24); + i2c_buf[1] = (BYTE)(cur_freq >> 16); + i2c_buf[2] = (BYTE)(cur_freq >> 8); + i2c_buf[3] = (BYTE)(cur_freq); + bcm_indirect_write_block(0x00, i2c_buf, 4); + + /* Wait for demod to settle */ + delay(10); + + /* Read signal strength via indirect register */ + snr_lo = 0; + snr_hi = 0; + bcm_indirect_read(0x00, &snr_lo); + bcm_indirect_read(0x01, &snr_hi); + + /* Store u16 LE into EP2 FIFO buffer */ + if (buf_idx < 1024 - 1) { + EP2FIFOBUF[buf_idx++] = snr_lo; + EP2FIFOBUF[buf_idx++] = snr_hi; + } + + /* If buffer is nearly full, commit this chunk */ + if (buf_idx >= 512) { + EP2BCH = MSB(buf_idx); + SYNCDELAY; + EP2BCL = LSB(buf_idx); + SYNCDELAY; + buf_idx = 0; + + /* Wait for the buffer to be taken by host */ + while (EP2CS & bmEPFULL) + ; + } + + cur_freq += step_khz; + } + + /* Commit any remaining data */ + if (buf_idx > 0) { + EP2BCH = MSB(buf_idx); + SYNCDELAY; + EP2BCL = LSB(buf_idx); + SYNCDELAY; + } +} + +/* ---------- Blind scan (0xB3) ---------- */ + +/* + * Try symbol rates from sr_min to sr_max in sr_step increments + * at a given frequency, looking for signal lock. + * + * EP0 payload (16 bytes): + * [0..3] freq_khz (u32 LE) + * [4..7] sr_min (u32 LE, sps) + * [8..11] sr_max (u32 LE, sps) + * [12..15] sr_step (u32 LE, sps) + * + * Returns via EP0: 8 bytes on lock [freq_khz(4) + sr_locked(4)] + * or 1 byte 0x00 if no lock found. + */ +static BOOL do_blind_scan(void) { + static __xdata DWORD freq_khz, sr_min, sr_max, sr_step, sr_cur; + BYTE lock_val; + + freq_khz = (DWORD)EP0BUF[0] | + ((DWORD)EP0BUF[1] << 8) | + ((DWORD)EP0BUF[2] << 16) | + ((DWORD)EP0BUF[3] << 24); + sr_min = (DWORD)EP0BUF[4] | + ((DWORD)EP0BUF[5] << 8) | + ((DWORD)EP0BUF[6] << 16) | + ((DWORD)EP0BUF[7] << 24); + sr_max = (DWORD)EP0BUF[8] | + ((DWORD)EP0BUF[9] << 8) | + ((DWORD)EP0BUF[10] << 16) | + ((DWORD)EP0BUF[11] << 24); + sr_step = (DWORD)EP0BUF[12] | + ((DWORD)EP0BUF[13] << 8) | + ((DWORD)EP0BUF[14] << 16) | + ((DWORD)EP0BUF[15] << 24); + + if (sr_step == 0) + sr_step = 1000000; + + sr_cur = sr_min; + while (sr_cur <= sr_max) { + /* + * Program frequency (BE) and symbol rate (BE) into BCM4500. + * We write both in a single block: 4 bytes SR + 4 bytes freq. + */ + i2c_buf[0] = (BYTE)(sr_cur >> 24); + i2c_buf[1] = (BYTE)(sr_cur >> 16); + i2c_buf[2] = (BYTE)(sr_cur >> 8); + i2c_buf[3] = (BYTE)(sr_cur); + bcm_indirect_write_block(0x00, i2c_buf, 4); + + i2c_buf[0] = (BYTE)(freq_khz >> 24); + i2c_buf[1] = (BYTE)(freq_khz >> 16); + i2c_buf[2] = (BYTE)(freq_khz >> 8); + i2c_buf[3] = (BYTE)(freq_khz); + bcm_indirect_write_block(0x00, i2c_buf, 4); + + /* Issue tune command */ + bcm_direct_write(BCM_REG_CMD, BCM_CMD_WRITE); + + /* Wait for acquisition attempt */ + delay(100); + + /* Check lock */ + lock_val = 0; + bcm_direct_read(BCM_REG_LOCK, &lock_val); + if (lock_val & 0x20) { + /* Locked -- report back via EP0 */ + EP0BUF[0] = (BYTE)(freq_khz); + EP0BUF[1] = (BYTE)(freq_khz >> 8); + EP0BUF[2] = (BYTE)(freq_khz >> 16); + EP0BUF[3] = (BYTE)(freq_khz >> 24); + EP0BUF[4] = (BYTE)(sr_cur); + EP0BUF[5] = (BYTE)(sr_cur >> 8); + EP0BUF[6] = (BYTE)(sr_cur >> 16); + EP0BUF[7] = (BYTE)(sr_cur >> 24); + EP0BCH = 0; + EP0BCL = 8; + return TRUE; + } + + sr_cur += sr_step; + } + + /* No lock found */ + EP0BUF[0] = 0x00; + EP0BCH = 0; + EP0BCL = 1; + return FALSE; +} + +/* ---------- TUNE_8PSK (0x86) handler ---------- */ + +/* + * Parse 10-byte EP0 payload, program BCM4500 via I2C indirect registers. + * Follows the stock firmware's protocol: + * EP0BUF[0..3] = symbol_rate (LE u32, sps) + * EP0BUF[4..7] = freq_khz (LE u32, kHz) + * EP0BUF[8] = modulation index (0-9) + * EP0BUF[9] = FEC index + */ +static void do_tune(void) { + BYTE i; + __xdata BYTE tune_data[12]; + + if (!(config_status & BM_STARTED)) + return; + + /* + * Byte-reverse symbol rate (LE->BE) into tune_data[0..3] + * and frequency (LE->BE) into tune_data[4..7] + */ + for (i = 0; i < 4; i++) { + tune_data[i] = EP0BUF[3 - i]; /* SR BE */ + tune_data[4 + i] = EP0BUF[7 - i]; /* Freq BE */ + } + + /* Modulation type and FEC rate */ + tune_data[8] = EP0BUF[8]; + tune_data[9] = EP0BUF[9]; + + /* Demod mode: default standard (0x10) */ + tune_data[10] = 0x10; + + /* Turbo flag: 0x00 for DVB-S, 0x01 for turbo modes */ + tune_data[11] = 0x00; + if (EP0BUF[8] >= 1 && EP0BUF[8] <= 3) + tune_data[11] = 0x01; + + /* Set demod mode for DCII variants */ + switch (EP0BUF[8]) { + case 5: tune_data[10] = 0x12; break; /* DCII I-stream */ + case 6: tune_data[10] = 0x16; break; /* DCII Q-stream */ + case 7: tune_data[10] = 0x11; break; /* DCII Offset QPSK */ + default: break; + } + + /* Poll BCM4500 for readiness */ + bcm_poll_ready(); + + /* Write page 0 */ + bcm_direct_write(BCM_REG_PAGE, 0x00); + + /* Write all configuration data to BCM4500 data register */ + { + BYTE reg = BCM_REG_DATA; + i2c_write(BCM4500_ADDR, 1, ®, 12, tune_data); + } + + /* Execute indirect write */ + bcm_direct_write(BCM_REG_CMD, BCM_CMD_WRITE); + + /* Wait for command completion */ + bcm_poll_ready(); +} + +/* ---------- Vendor command handler ---------- */ + +BOOL handle_vendorcommand(BYTE cmd) { + WORD wval; + BYTE val; + + wval = SETUP_VALUE(); + + switch (cmd) { + + /* 0x80: GET_8PSK_CONFIG -- return config status byte */ + case GET_8PSK_CONFIG: + EP0BUF[0] = config_status; + EP0BCH = 0; + EP0BCL = 1; + return TRUE; + + /* 0x85: ARM_TRANSFER -- start/stop MPEG-2 streaming */ + case ARM_TRANSFER: + if (wval) + gpif_start(); + else + gpif_stop(); + return TRUE; + + /* 0x86: TUNE_8PSK -- 10-byte tuning payload */ + case TUNE_8PSK: + /* EP0 data phase: wait for 10 bytes from host */ + EP0BCL = 0; + SYNCDELAY; + while (EP0CS & bmEPBUSY) + ; + do_tune(); + return TRUE; + + /* 0x87: GET_SIGNAL_STRENGTH -- read 6 bytes from BCM4500 */ + case GET_SIGNAL_STRENGTH: + if (!(config_status & BM_STARTED)) { + EP0BUF[0] = 0; EP0BUF[1] = 0; + EP0BUF[2] = 0; EP0BUF[3] = 0; + EP0BUF[4] = 0; EP0BUF[5] = 0; + EP0BCH = 0; + EP0BCL = 6; + return TRUE; + } + /* 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]); + EP0BCH = 0; + EP0BCL = 6; + return TRUE; + + /* 0x89: BOOT_8PSK -- initialize BCM4500 demodulator */ + case BOOT_8PSK: + if (wval) { + /* Power on: scan for BCM4500 at address 0x10 */ + val = 0; + if (bcm_direct_read(BCM_REG_STATUS, &val)) { + config_status |= BM_STARTED; + config_status |= BM_FW_LOADED; + } + } else { + config_status &= ~BM_STARTED; + } + EP0BUF[0] = config_status; + EP0BCH = 0; + EP0BCL = 1; + return TRUE; + + /* 0x8A: START_INTERSIL -- enable LNB power supply */ + case START_INTERSIL: + if (wval) { + /* Enable LNB power */ + OEA |= (PIN_22KHZ | PIN_LNB_VOLT | PIN_DISEQC); + config_status |= BM_INTERSIL; + } else { + config_status &= ~BM_INTERSIL; + } + EP0BUF[0] = config_status; + EP0BCH = 0; + EP0BCL = 1; + return TRUE; + + /* 0x8B: SET_LNB_VOLTAGE -- 13V (wval=0) or 18V (wval=1) */ + case SET_LNB_VOLTAGE: + if (wval) { + IOA |= PIN_LNB_VOLT; + config_status |= BM_SEL18V; + } else { + IOA &= ~PIN_LNB_VOLT; + config_status &= ~BM_SEL18V; + } + return TRUE; + + /* 0x8C: SET_22KHZ_TONE -- on (wval=1) or off (wval=0) */ + case SET_22KHZ_TONE: + if (wval) { + IOA |= PIN_22KHZ; + config_status |= BM_22KHZ; + } else { + IOA &= ~PIN_22KHZ; + config_status &= ~BM_22KHZ; + } + return TRUE; + + /* 0x8D: SEND_DISEQC -- tone burst or DiSEqC message */ + case SEND_DISEQC: { + WORD wlen; + wlen = SETUP_LENGTH(); + if (wlen == 0) { + /* Tone burst: A if wval==0, B if wval!=0 */ + diseqc_tone_burst((BYTE)wval); + } + /* Full DiSEqC message: future implementation */ + return TRUE; + } + + /* 0x90: GET_SIGNAL_LOCK -- read BCM4500 lock register */ + case GET_SIGNAL_LOCK: + val = 0; + if (config_status & BM_STARTED) { + bcm_direct_read(BCM_REG_LOCK, &val); + } + EP0BUF[0] = val; + EP0BCH = 0; + EP0BCL = 1; + return TRUE; + + /* 0x92: GET_FW_VERS -- return firmware version and build date */ + case GET_FW_VERS: + EP0BUF[0] = 0x01; /* patch -> version 3.00.1 */ + EP0BUF[1] = 0x00; /* minor */ + EP0BUF[2] = 0x03; /* major */ + EP0BUF[3] = 0x0B; /* day = 11 */ + EP0BUF[4] = 0x02; /* month = 2 */ + EP0BUF[5] = 0x1A; /* year - 2000 = 26 */ + EP0BCH = 0; + EP0BCL = 6; + return TRUE; + + /* 0x94: USE_EXTRA_VOLT -- enable +1V LNB boost */ + case USE_EXTRA_VOLT: + /* This would write to the LNB regulator; no-op for now */ + return TRUE; + + /* --- Custom commands --- */ + + /* 0xB0: SPECTRUM_SWEEP */ + case SPECTRUM_SWEEP: + /* EP0 data phase: wait for 10 bytes from host */ + EP0BCL = 0; + SYNCDELAY; + while (EP0CS & bmEPBUSY) + ; + do_spectrum_sweep(); + return TRUE; + + /* 0xB1: RAW_DEMOD_READ -- read BCM4500 register */ + case RAW_DEMOD_READ: + val = 0; + bcm_indirect_read((BYTE)wval, &val); + EP0BUF[0] = val; + EP0BCH = 0; + EP0BCL = 1; + return TRUE; + + /* 0xB2: RAW_DEMOD_WRITE -- write BCM4500 register */ + case RAW_DEMOD_WRITE: { + WORD widx; + widx = SETUP_INDEX(); + bcm_indirect_write((BYTE)wval, (BYTE)widx); + return TRUE; + } + + /* 0xB3: BLIND_SCAN */ + case BLIND_SCAN: + /* EP0 data phase: wait for 16 bytes from host */ + EP0BCL = 0; + SYNCDELAY; + while (EP0CS & bmEPBUSY) + ; + do_blind_scan(); + return TRUE; + + default: + return FALSE; + } +} + +/* ---------- Required fx2lib callbacks ---------- */ + +BOOL handle_get_descriptor(void) { + return FALSE; +} + +BOOL handle_get_interface(BYTE ifc, BYTE *alt_ifc) { + if (ifc == 0) { + *alt_ifc = 0; + return TRUE; + } + return FALSE; +} + +BOOL handle_set_interface(BYTE ifc, BYTE alt_ifc) { + if (ifc == 0 && alt_ifc == 0) { + RESETTOGGLE(0x82); + RESETFIFO(0x02); + return TRUE; + } + return FALSE; +} + +BYTE handle_get_configuration(void) { + return 1; +} + +BOOL handle_set_configuration(BYTE cfg) { + return cfg == 1 ? TRUE : FALSE; +} + +/* ---------- USB interrupt handlers ---------- */ + +void sudav_isr(void) __interrupt (SUDAV_ISR) { + got_sud = TRUE; + CLEAR_SUDAV(); +} + +void usbreset_isr(void) __interrupt (USBRESET_ISR) { + handle_hispeed(FALSE); + CLEAR_USBRESET(); +} + +void hispeed_isr(void) __interrupt (HISPEED_ISR) { + handle_hispeed(TRUE); + CLEAR_HISPEED(); +} + +/* ---------- Main ---------- */ + +void main(void) { + + config_status = 0; + got_sud = FALSE; + + REVCTL = 0x03; /* NOAUTOARM + SKIPCOMMIT */ + SYNCDELAY; + + RENUMERATE_UNCOND(); + + SETCPUFREQ(CLK_48M); + SETIF48MHZ(); + + USE_USB_INTS(); + ENABLE_SUDAV(); + ENABLE_HISPEED(); + ENABLE_USBRESET(); + + /* Configure I2C: 400kHz */ + I2CTL = bm400KHZ; + + /* Configure GPIO output enables for LNB/tone/DiSEqC (v2.06 pin map) */ + OEA |= (PIN_22KHZ | PIN_LNB_VOLT | PIN_DISEQC); /* P0.3, P0.4, P0.7 output */ + + /* Initial GPIO state: LNB off, tone off, DiSEqC idle */ + IOA = 0x84; /* P0.7=1 (idle), P0.2=1 (BCM4500 control) */ + IOD = 0xE1; /* P3.7:5=1 (controls idle), P3.0=1 */ + + /* EP2 is bulk IN (0x82), 512 byte, double-buffered */ + EP2CFG = 0xE2; /* valid, IN, bulk, 512, double */ + SYNCDELAY; + + /* Disable unused endpoints */ + EP1INCFG &= ~bmVALID; + SYNCDELAY; + EP1OUTCFG &= ~bmVALID; + SYNCDELAY; + EP4CFG &= ~bmVALID; + SYNCDELAY; + EP6CFG &= ~bmVALID; + SYNCDELAY; + EP8CFG &= ~bmVALID; + SYNCDELAY; + + /* Reset all FIFOs */ + RESETFIFOS(); + + /* IFCONFIG: internal 48MHz, GPIF master, async */ + IFCONFIG = 0xEE; + SYNCDELAY; + + /* EP2FIFOCFG: AUTOIN, ZEROLENIN, 8-bit */ + EP2FIFOCFG = 0x0C; + SYNCDELAY; + + /* Disable other FIFO configs */ + EP4FIFOCFG = 0; + SYNCDELAY; + EP6FIFOCFG = 0; + SYNCDELAY; + EP8FIFOCFG = 0; + SYNCDELAY; + + EA = 1; /* global interrupt enable */ + + while (TRUE) { + if (got_sud) { + handle_setupdata(); + got_sud = FALSE; + } + } +} diff --git a/tools/fw_load.py b/tools/fw_load.py new file mode 100755 index 0000000..144a0dc --- /dev/null +++ b/tools/fw_load.py @@ -0,0 +1,616 @@ +#!/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()