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.
This commit is contained in:
Ryan Malloy 2026-02-11 19:46:50 -07:00
parent c7b5932cc0
commit 5710584267
5 changed files with 1729 additions and 0 deletions

7
.gitignore vendored
View File

@ -1,2 +1,9 @@
# Empty extraction directories # Empty extraction directories
docs/diseqc/images/ docs/diseqc/images/
# Third-party dependencies
firmware/fx2lib/
# Build artifacts
firmware/build/
tools/__pycache__/

12
firmware/Makefile Normal file
View File

@ -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

230
firmware/dscr.a51 Normal file
View File

@ -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

864
firmware/skywalker1.c Normal file
View File

@ -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 <fx2regs.h>
#include <fx2macros.h>
#include <delay.h>
#include <autovector.h>
#include <setupdat.h>
#include <eputils.h>
#include <i2c.h>
#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, &reg, 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, &reg, 1, i2c_buf))
return FALSE;
reg = BCM_REG_DATA;
if (!i2c_write(BCM4500_ADDR, 1, &reg, 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, &reg, 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;
}
}
}

616
tools/fw_load.py Executable file
View File

@ -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()