Compare commits
9 Commits
1df2be8a43
...
0d6facb321
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d6facb321 | |||
| 97c1000d8b | |||
| a12a394099 | |||
| 3d2cd477b2 | |||
| bbdcb243dc | |||
| 696d2dd387 | |||
| 29df688f28 | |||
| 7f1e0cf0d7 | |||
| d117782dcf |
36
.gitattributes
vendored
Normal file
36
.gitattributes
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Normalize line endings to LF on commit
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# Force LF for all source files (prevents CRLF contamination)
|
||||||
|
*.py text eol=lf
|
||||||
|
*.c text eol=lf
|
||||||
|
*.h text eol=lf
|
||||||
|
*.js text eol=lf
|
||||||
|
*.mjs text eol=lf
|
||||||
|
*.ts text eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
|
*.md text eol=lf
|
||||||
|
*.mdx text eol=lf
|
||||||
|
*.yml text eol=lf
|
||||||
|
*.yaml text eol=lf
|
||||||
|
*.toml text eol=lf
|
||||||
|
*.cfg text eol=lf
|
||||||
|
*.txt text eol=lf
|
||||||
|
*.css text eol=lf
|
||||||
|
*.astro text eol=lf
|
||||||
|
*.html text eol=lf
|
||||||
|
*.svg text eol=lf
|
||||||
|
Makefile text eol=lf
|
||||||
|
Dockerfile text eol=lf
|
||||||
|
*.sh text eol=lf
|
||||||
|
|
||||||
|
# Binary files — leave as-is
|
||||||
|
*.bin binary
|
||||||
|
*.ihx binary
|
||||||
|
*.hex binary
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
8
TODO
Normal file
8
TODO
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
Both stock and our custom firmware report the same status
|
||||||
|
progression (0x00 → 0x03 → 0x07), but the stock firmware's
|
||||||
|
BCM4500 initialization actually works (SNR raw ~64000) while
|
||||||
|
the custom firmware's returns all zeros. The custom
|
||||||
|
firmware's bcm4500_boot() silently fails somewhere in the
|
||||||
|
I2C init block writes — it sets BM_STARTED | BM_FW_LOADED
|
||||||
|
but the hardware isn't actually running. This is a firmware
|
||||||
|
bug that needs investigation.
|
||||||
144
docs/EEPROM-RECOVERY.md
Normal file
144
docs/EEPROM-RECOVERY.md
Normal file
@ -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 <bus> 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
|
||||||
@ -74,7 +74,8 @@ DVB-S2 is not supported. See [Section 14](#14-dvb-s2-incompatibility).
|
|||||||
+--[ I2C EEPROM 0x51 ]
|
+--[ I2C EEPROM 0x51 ]
|
||||||
|
|
|
|
||||||
USB 2.0 HS | I2C Bus (400 kHz)
|
USB 2.0 HS | I2C Bus (400 kHz)
|
||||||
Host PC <----> [ CY7C68013A FX2LP ] <-----> [ BCM4500 Demod 0x08 ]
|
Host PC <----> [ CY7C68013A FX2LP ] <--I2C--> [ BCM3440 Tuner 0x10 ] <--gateway--> [ BCM4500 Demod ]
|
||||||
|
<--I2C--> [ BCM4500 Direct 0x08 (status only) ]
|
||||||
| 8051 @ 48 MHz | |
|
| 8051 @ 48 MHz | |
|
||||||
| GPIF Engine |<-----------+ 8-bit parallel TS
|
| GPIF Engine |<-----------+ 8-bit parallel TS
|
||||||
| EP2 Bulk IN |
|
| EP2 Bulk IN |
|
||||||
@ -364,24 +365,39 @@ Writing 0x01 to CPUCS halts the CPU. New code is written to RAM. Writing 0x00 re
|
|||||||
|
|
||||||
## 6. BCM4500 Demodulator Interface
|
## 6. BCM4500 Demodulator Interface
|
||||||
|
|
||||||
### 6.1 I2C Addressing
|
### 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 |
|
| Parameter | Value |
|
||||||
|-----------|-------|
|
|-----------|-------|
|
||||||
| 7-bit I2C address | 0x08 |
|
| BCM3440 tuner gateway (7-bit) | **0x10** — all BCM4500 register access |
|
||||||
| 8-bit write address | 0x10 |
|
| BCM3440 wire write / read | 0x20 / 0x21 |
|
||||||
| 8-bit read address | 0x11 |
|
| BCM4500 direct (7-bit) | 0x08 — status byte only, no register addressing |
|
||||||
|
| BCM4500 wire write / read | 0x10 / 0x11 |
|
||||||
| Bus speed | 400 kHz |
|
| Bus speed | 400 kHz |
|
||||||
| FX2 I2C controller SFRs | I2CS, I2DAT, I2CTL |
|
| FX2 I2C controller SFRs | I2CS, I2DAT, I2CTL |
|
||||||
| Alternate probe addresses (v2.13) | 0x3F, 0x7F |
|
| 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 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.
|
||||||
|
|
||||||
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.
|
**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
|
### 6.2 Direct Registers
|
||||||
|
|
||||||
Accessed via standard I2C write/read to the BCM4500's device address:
|
Accessed via I2C write/read through the BCM3440 gateway (address 0x10):
|
||||||
|
|
||||||
| Register | Function |
|
| Register | Function |
|
||||||
|----------|----------|
|
|----------|----------|
|
||||||
|
|||||||
604
firmware-dump/STARTUP_DISASSEMBLY.md
Normal file
604
firmware-dump/STARTUP_DISASSEMBLY.md
Normal file
@ -0,0 +1,604 @@
|
|||||||
|
# Genpix SkyWalker-1 Firmware Startup Disassembly
|
||||||
|
|
||||||
|
**Binary**: `skywalker1_eeprom_full64k.bin` (65536 bytes, RAM image after EEPROM boot)
|
||||||
|
**CPU**: FX2LP (CY7C68013A) — 8051 core @ 48MHz
|
||||||
|
**Firmware region**: 0x0000-0x24FF (9472 bytes)
|
||||||
|
**Compiler**: Keil C51
|
||||||
|
**USB**: VID=0x09C0 (Cypress), PID=0x0203
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Reset Vector (0x0000)
|
||||||
|
|
||||||
|
```
|
||||||
|
0000: 02 18 8D LJMP 0x188D ; -> Keil C51 startup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 2: Keil C51 Startup (0x188D)
|
||||||
|
|
||||||
|
### IDATA Clear
|
||||||
|
```
|
||||||
|
188D: 78 7F MOV R0,#7Fh ; Start at IDATA address 0x7F
|
||||||
|
188F: E4 CLR A ; A = 0
|
||||||
|
1890: F6 MOV @R0,A ; Clear IDATA[R0]
|
||||||
|
1891: D8 FD DJNZ R0,1890h ; Decrement R0, loop until 0
|
||||||
|
; Clears IDATA 0x7F..0x01 (127 bytes)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stack Pointer
|
||||||
|
```
|
||||||
|
1893: 75 81 72 MOV SP,#72h ; Stack pointer = 0x72
|
||||||
|
; Stack grows up: 0x73..0x7F = 13 bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
### XDATA Initialization
|
||||||
|
```
|
||||||
|
1896: 02 18 D4 LJMP 18D4h ; Jump to init table interpreter
|
||||||
|
|
||||||
|
; Init table interpreter at 0x18D4 (Keil standard STARTUP.A51):
|
||||||
|
; Reads structured init records from CODE space
|
||||||
|
; Each record has:
|
||||||
|
; - Length byte (bits 5:0 = count, bits 7:6 = mode)
|
||||||
|
; - Mode 00: IDATA/XDATA fill (address + data pairs)
|
||||||
|
; - Mode 01: Bit initialization
|
||||||
|
; - Mode 10: XDATA block copy (source addr + destination + data)
|
||||||
|
; - Zero byte terminates the table
|
||||||
|
;
|
||||||
|
; When table exhausted:
|
||||||
|
1899: 02 09 A7 LJMP 09A7h ; -> main()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 3: main() (0x09A7)
|
||||||
|
|
||||||
|
### Clear Global Variables
|
||||||
|
```
|
||||||
|
09A7: E4 CLR A ; A = 0
|
||||||
|
09A8: F5 2D MOV 2Dh,A ; Clear counter/state variables
|
||||||
|
09AA: F5 2C MOV 2Ch,A ; 4-byte counter at 0x2A-0x2D
|
||||||
|
09AC: F5 2B MOV 2Bh,A
|
||||||
|
09AE: F5 2A MOV 2Ah,A
|
||||||
|
09B0: F5 35 MOV 35h,A ; 4-byte counter at 0x32-0x35
|
||||||
|
09B2: F5 34 MOV 34h,A
|
||||||
|
09B4: F5 33 MOV 33h,A
|
||||||
|
09B6: F5 32 MOV 32h,A
|
||||||
|
09B8: C2 03 CLR bit_03h ; Clear SUSPEND-pending flag
|
||||||
|
09BA: C2 01 CLR bit_01h ; Clear SETUP-pending flag
|
||||||
|
```
|
||||||
|
|
||||||
|
### Call hw_init()
|
||||||
|
```
|
||||||
|
09BC: 12 13 C3 LCALL 13C3h ; -> hw_init()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 4: hw_init() (0x13C3)
|
||||||
|
|
||||||
|
### CPU Configuration
|
||||||
|
```
|
||||||
|
13C3: MOV DPTR,#E605h ; REVCTL (FX2LP revision control)
|
||||||
|
13C6: MOVX A,@DPTR ; Read current value
|
||||||
|
13C7: ANL A,#FDh ; Clear bit 1 (NOAUTOARM)
|
||||||
|
13C9: MOVX @DPTR,A ; REVCTL &= ~0x02
|
||||||
|
|
||||||
|
13CE: MOV DPTR,#E600h ; CPUCS
|
||||||
|
13D1: MOVX A,@DPTR
|
||||||
|
13D2: ANL A,#E5h ; Clear bits 4,3,1 (clock bits)
|
||||||
|
13D4: ORL A,#10h ; Set bit 4
|
||||||
|
13D6: MOVX @DPTR,A ; CPUCS = 48MHz clock mode
|
||||||
|
; NOP x3 (sync delay)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Interface Configuration
|
||||||
|
```
|
||||||
|
13DA: MOV DPTR,#E601h ; IFCONFIG
|
||||||
|
13DD: MOV A,#CAh ; 0xCA = 1100_1010:
|
||||||
|
; IFCLKSRC=1 (internal)
|
||||||
|
; 3048MHZ=1 (48MHz)
|
||||||
|
; IFCLKOE=0 (don't output IFCLK)
|
||||||
|
; IFCLKPOL=0
|
||||||
|
; ASYNC=1 (async GPIF)
|
||||||
|
; GSTATE=0
|
||||||
|
; IFCFG=10 (GPIF mode)
|
||||||
|
13DF: MOVX @DPTR,A
|
||||||
|
```
|
||||||
|
|
||||||
|
### GPIF Waveform Init
|
||||||
|
```
|
||||||
|
13E3: MOV DPTR,#E6F5h ; Undocumented register
|
||||||
|
13E6: MOV A,#FFh
|
||||||
|
13E8: MOVX @DPTR,A
|
||||||
|
|
||||||
|
13E9: MOV 0xAF,#07h ; Unknown SFR (FX2-specific)
|
||||||
|
|
||||||
|
13F1: LCALL 12EAh ; GPIF waveform configuration:
|
||||||
|
; Reads waveform data from XDATA 0xE000-0xE08E
|
||||||
|
; Copies 128 bytes to GPIF waveform RAM via autopointers
|
||||||
|
; Configures: GPIFCTLCFG, GPIFIDLECS, GPIFIDLECTL,
|
||||||
|
; GPIFWFSELECT, GPIFADR, CTL states
|
||||||
|
; PORTCCFG=0xFF (all alt function for GPIF)
|
||||||
|
; PORTECFG.7=1 (GPIF ready signal)
|
||||||
|
```
|
||||||
|
|
||||||
|
### State Initialization
|
||||||
|
```
|
||||||
|
13F4: CLR A
|
||||||
|
13F5: MOV 6Dh,A ; Status flags byte = 0
|
||||||
|
; bit 0: init in progress
|
||||||
|
; bit 1: 8PSK firmware loaded
|
||||||
|
; bit 3: signature check mode
|
||||||
|
; bit 6: GPIF configured
|
||||||
|
; bit 7: streaming active
|
||||||
|
13F7: CLR bit_05h
|
||||||
|
13F9: CLR bit_04h
|
||||||
|
13FB: MOV 68h,A ; Clear more state
|
||||||
|
13FD: MOV 69h,A
|
||||||
|
13FF: MOV 66h,A
|
||||||
|
1401: CLR bit_06h ; GPIF-configured flag = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### GPIO Configuration
|
||||||
|
```
|
||||||
|
1403: MOV DPTR,#E670h ; PORTACFG
|
||||||
|
1406: MOVX @DPTR,A ; = 0x00 (all GPIO, no alt functions)
|
||||||
|
|
||||||
|
1407: MOV IOA,#A4h ; Port A initial: 1010_0100
|
||||||
|
; PA7=1 (GPIF/transport select?)
|
||||||
|
; PA5=1 (8PSK_RESET deasserted)
|
||||||
|
; PA2=1 (LNB voltage select?)
|
||||||
|
; PA1=0, PA0=input
|
||||||
|
140A: MOV OEA,#FEh ; Port A direction: 1111_1110
|
||||||
|
; PA7-PA1 = output
|
||||||
|
; PA0 = input (lock detect?)
|
||||||
|
|
||||||
|
140D: MOV IOD,#F0h ; Port D initial: 1111_0000
|
||||||
|
; PD7=1 (8PSK bus D7?)
|
||||||
|
; PD6=1 (8PSK bus D6?)
|
||||||
|
; PD5=1 (8PSK bus D5?)
|
||||||
|
; PD4=1 (8PSK bus D4?)
|
||||||
|
1410: MOV OED,#F0h ; Port D direction: upper 4 output
|
||||||
|
```
|
||||||
|
|
||||||
|
### FIFO Reset Sequence
|
||||||
|
```
|
||||||
|
141C: MOV DPTR,#E604h ; FIFORESET
|
||||||
|
141F: MOV A,#80h ; NAK-ALL = 1 (hold off USB during reset)
|
||||||
|
1421: MOVX @DPTR,A
|
||||||
|
1425: MOV A,#02h ; Reset EP2
|
||||||
|
1427: MOVX @DPTR,A
|
||||||
|
142B: MOV A,#04h ; Reset EP4
|
||||||
|
142D: MOVX @DPTR,A
|
||||||
|
1431: MOV A,#06h ; Reset EP6
|
||||||
|
1433: MOVX @DPTR,A
|
||||||
|
1437: MOV A,#08h ; Reset EP8
|
||||||
|
1439: MOVX @DPTR,A
|
||||||
|
143D: CLR A ; NAK-ALL = 0 (release)
|
||||||
|
143E: MOVX @DPTR,A
|
||||||
|
```
|
||||||
|
|
||||||
|
### GPIF Timing
|
||||||
|
```
|
||||||
|
1442: MOV DPTR,#E618h ; GPIFHOLDAMOUNT
|
||||||
|
1445: MOV A,#0Ch ; 12 IFCLK cycles hold
|
||||||
|
1447: MOVX @DPTR,A
|
||||||
|
|
||||||
|
; E619-E61B (FLOWSTATE regs) = 0x00
|
||||||
|
```
|
||||||
|
|
||||||
|
### I2C Configuration
|
||||||
|
```
|
||||||
|
1461: MOV DPTR,#E67Ah ; I2CTL
|
||||||
|
1464: MOVX A,@DPTR
|
||||||
|
1465: ORL A,#01h ; Set bit 0: 400kHz I2C clock
|
||||||
|
1467: MOVX @DPTR,A
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timer 2 Setup
|
||||||
|
```
|
||||||
|
1468: ANL CKCON,#DFh ; Clear Timer2 clock source bit
|
||||||
|
146B: MOV T2CON,#04h ; Timer 2: auto-reload mode
|
||||||
|
146E: MOV RCAP2H,#F8h ; Reload value = 0xF8xx (fast tick)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 5: Descriptor Setup (back in main, 0x09BF)
|
||||||
|
|
||||||
|
```
|
||||||
|
09BF: MOV 0Ch,#12h ; Descriptor table high byte
|
||||||
|
09C2: MOV 0Dh,#00h ; Descriptor table low byte = 0x1200
|
||||||
|
; -> USB Device Descriptor
|
||||||
|
|
||||||
|
; Multiple descriptor pointer pairs stored in direct RAM:
|
||||||
|
; [0C:0D] = 0x1200 Device Descriptor
|
||||||
|
; [14:15] = 0x1212 Device Qualifier
|
||||||
|
; [0A:0B] = 0x121C Configuration Descriptor
|
||||||
|
; [12:13] = 0x1254 Other Speed Config
|
||||||
|
; [16:17] = 0x128C String Descriptors
|
||||||
|
; [08:09] = 0x12E8 (high-speed descriptor variant?)
|
||||||
|
|
||||||
|
; 09E3-0AE0: Copy all descriptors from CODE to XDATA
|
||||||
|
; Calculates actual XDATA base address
|
||||||
|
; Adjusts all pointers for runtime location
|
||||||
|
; Prepares EP configuration from config descriptor
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 6: USB Enable (0x0AE1)
|
||||||
|
|
||||||
|
```
|
||||||
|
0AE1: MOV R7,09h / R6,08h ; Pass descriptor base pointer
|
||||||
|
0AE5: LCALL 1A0Eh ; Configure USB descriptors in FX2
|
||||||
|
; Reads XDATA desc, calculates checksums
|
||||||
|
; Sets up descriptor table for USB core
|
||||||
|
; Checks descriptor type -> sets bit_06h
|
||||||
|
|
||||||
|
; Enable USB interrupts
|
||||||
|
0AE8: SETB EIE.0 ; Enable INT2 (USB interrupt)
|
||||||
|
0AEA: ORL EICON,#20h ; INT2 edge trigger
|
||||||
|
|
||||||
|
0AED: MOV DPTR,#E668h ; INTSETUP
|
||||||
|
0AF0: MOVX A,@DPTR
|
||||||
|
0AF1: ORL A,#09h ; Enable autovectoring for INT2 and INT4
|
||||||
|
0AF3: MOVX @DPTR,A
|
||||||
|
|
||||||
|
0AF4: MOV DPTR,#E65Ch ; USBIE (USB Interrupt Enable)
|
||||||
|
0AF7: MOVX A,@DPTR
|
||||||
|
0AF8: ORL A,#3Dh ; Enable: SUDAV|SOF|SUTOK|SUSPEND|USBRESET
|
||||||
|
; 0x3D = 0011_1101
|
||||||
|
0AFA: MOVX @DPTR,A
|
||||||
|
|
||||||
|
0AFB: SETB EA ; Global interrupt enable (IE.7)
|
||||||
|
```
|
||||||
|
|
||||||
|
### USB Re-enumeration
|
||||||
|
```
|
||||||
|
0AFD: SETB bit_07h ; Mark "disconnected" state
|
||||||
|
0AFF: LCALL 0003h ; Call re-enumeration handler:
|
||||||
|
; Sets USBCS.DISCON=1, USBCS.RENUM=1
|
||||||
|
; Delay ~375,000 cycles (~7.8ms @ 48MHz)
|
||||||
|
; Clears USBIRQ (write 0xFF)
|
||||||
|
; Clears EPIRQ (write 0xFF)
|
||||||
|
; Clears EXIF.4
|
||||||
|
; Clears USBCS.DISCON (reconnect)
|
||||||
|
|
||||||
|
; After re-enum completes:
|
||||||
|
0B02: MOV DPTR,#E680h ; USBCS
|
||||||
|
0B05: MOVX A,@DPTR
|
||||||
|
0B06: ANL A,#F7h ; Clear DISCON bit (bit 3)
|
||||||
|
0B08: MOVX @DPTR,A ; *** USB device now visible to host ***
|
||||||
|
|
||||||
|
0B09: ANL CKCON,#F8h ; Final timer prescaler setup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 7: Main Loop (0x0B0C)
|
||||||
|
|
||||||
|
```
|
||||||
|
; ---- MAIN POLLING LOOP ----
|
||||||
|
0B0C: LCALL 2297h ; ep1_poll():
|
||||||
|
; Check EP1IN ready, arm if needed
|
||||||
|
; Check FLOW state registers
|
||||||
|
; Return C=1 if EP1 armed
|
||||||
|
|
||||||
|
0B0F: JNB bit_01h,0B17h ; SETUP pending (from SUDAV ISR)?
|
||||||
|
0B12: LCALL 032Ah ; Yes -> handle_setup()
|
||||||
|
0B15: CLR bit_01h ; Clear pending flag
|
||||||
|
|
||||||
|
0B17: JNB bit_03h,0B0Ch ; SUSPEND pending (from SUSPEND ISR)?
|
||||||
|
0B1A: LCALL 24DAh ; Check suspend condition
|
||||||
|
0B1D: JNC 0B0Ch ; False alarm -> continue loop
|
||||||
|
0B1F: CLR bit_03h
|
||||||
|
0B21: LCALL 21EDh ; Enter suspend (WAKEUPCS handling)
|
||||||
|
; ... wakeup recovery code ...
|
||||||
|
0B3D: LCALL 211Dh ; I2C device polling (demod status?)
|
||||||
|
; ... loop back to 0B0C ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 8: SETUP Packet Handler (0x032A)
|
||||||
|
|
||||||
|
### Standard Request Dispatch
|
||||||
|
```
|
||||||
|
032A: MOV DPTR,#E6B9h ; SETUPDAT[1] = bRequest (alt mapping)
|
||||||
|
032D: MOVX A,@DPTR ; Read bRequest
|
||||||
|
032E: CJNE A,#0Ch,0331h ; Compare with 12
|
||||||
|
0331: JC 0335h ; If bRequest < 12 -> standard USB request
|
||||||
|
0333: AJMP 05ABh ; Else -> vendor request handler
|
||||||
|
|
||||||
|
; Standard USB request jump table (bRequest 0-11):
|
||||||
|
0335: MOV DPTR,#033Bh
|
||||||
|
0338: ADD A,ACC ; index * 2 (each AJMP is 2 bytes)
|
||||||
|
033A: JMP @A+DPTR ; Computed jump
|
||||||
|
|
||||||
|
; Table at 033B (12 entries):
|
||||||
|
; [0] GET_STATUS -> 0403h
|
||||||
|
; [1] CLEAR_FEATURE -> 04A6h
|
||||||
|
; [2] (reserved) -> 05ABh (stall)
|
||||||
|
; [3] SET_FEATURE -> 0536h
|
||||||
|
; [4] (reserved) -> 05ABh
|
||||||
|
; [5] SET_ADDRESS -> 05ABh (handled by hardware)
|
||||||
|
; [6] GET_DESCRIPTOR -> 0353h
|
||||||
|
; [7] (reserved) -> 05ABh
|
||||||
|
; [8] GET_CONFIGURATION -> 03FEh
|
||||||
|
; [9] SET_CONFIGURATION -> 03F4h
|
||||||
|
; [10] GET_INTERFACE -> 03DBh
|
||||||
|
; [11] SET_INTERFACE -> 03E5h
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vendor Request Dispatch (0x05AB -> 0x0056)
|
||||||
|
```
|
||||||
|
05AB: LCALL 0056h ; Call vendor request dispatcher
|
||||||
|
05AE: JC 05B7h ; If handled (C=1) -> skip stall
|
||||||
|
05B0: MOV DPTR,#E6A0h ; EP0CS
|
||||||
|
05B3: MOVX A,@DPTR
|
||||||
|
05B4: ORL A,#01h ; Set STALL bit (unhandled request)
|
||||||
|
05B6: MOVX @DPTR,A
|
||||||
|
|
||||||
|
05B7: MOV DPTR,#E6A0h ; EP0CS
|
||||||
|
05BA: MOVX A,@DPTR
|
||||||
|
05BB: ORL A,#80h ; Set HSNAK bit (handshake)
|
||||||
|
05BD: MOVX @DPTR,A
|
||||||
|
05BE: RET
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vendor Dispatch at 0x0056
|
||||||
|
```
|
||||||
|
; NOTE: E6B8-E6BF maps to SETUPDAT[0]-SETUPDAT[7] (alternate XDATA addresses)
|
||||||
|
; This is an undocumented but valid FX2LP mapping.
|
||||||
|
|
||||||
|
0056: MOV DPTR,#E6B8h ; SETUPDAT[0] = bmRequestType
|
||||||
|
0059: MOVX A,@DPTR
|
||||||
|
005A: JB ACC.6,005Fh ; Test bit 6: vendor request flag
|
||||||
|
005D: CLR C ; Not vendor -> return unhandled
|
||||||
|
005E: RET
|
||||||
|
|
||||||
|
005F: MOV DPTR,#E6B9h ; SETUPDAT[1] = bRequest
|
||||||
|
0062: MOVX A,@DPTR
|
||||||
|
0063: ADD A,#80h ; Remap: bRequest 0x80 -> index 0x00
|
||||||
|
; bRequest 0x89 -> index 0x09
|
||||||
|
; bRequest 0x9D -> index 0x1D
|
||||||
|
0065: CJNE A,#1Eh,0068h ; Check if index < 30
|
||||||
|
0068: JC 006Ch ; In range -> use jump table
|
||||||
|
006A: AJMP 0326h ; Out of range -> return unhandled (C=0)
|
||||||
|
|
||||||
|
006C: MOV DPTR,#0076h ; Jump table base
|
||||||
|
006F: ADD A,ACC ; index * 2 (AJMP = 2 bytes each)
|
||||||
|
0071: JNC 0075h ; Handle page crossing
|
||||||
|
0073: INC DPH
|
||||||
|
0075: JMP @A+DPTR ; Computed jump into vendor table
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vendor Command Jump Table (0x0076)
|
||||||
|
|
||||||
|
| bRequest | Index | Target | Function |
|
||||||
|
|----------|-------|--------|----------|
|
||||||
|
| 0x80 | 0 | 00B2h | GET_STATUS — returns status byte `6Dh` via EP0 |
|
||||||
|
| 0x81 | 1 | 0326h | (unhandled) |
|
||||||
|
| 0x82 | 2 | 0326h | (unhandled) |
|
||||||
|
| 0x83 | 3 | 00F1h | EP0_SETUP_READ — sets up descriptor read via SUDPTR |
|
||||||
|
| 0x84 | 4 | 0102h | EP0_DATA_WRITE — host writes data to EP0 |
|
||||||
|
| 0x85 | 5 | 0110h | EP0_DATA_READ — host reads data from EP0 |
|
||||||
|
| 0x86 | 6 | 012Eh | I2C_WRITE — write I2C data block |
|
||||||
|
| 0x87 | 7 | 0140h | EP0_MULTI_READ — multi-byte EP0 read |
|
||||||
|
| 0x88 | 8 | 0326h | (unhandled) |
|
||||||
|
| **0x89** | **9** | **00C4h** | **BOOT_8PSK** — boot the 8PSK demodulator |
|
||||||
|
| 0x8A | 10 | 019Ch | I2C_READ (with status check) |
|
||||||
|
| 0x8B | 11 | 01CBh | I2C_WRITE (variant) |
|
||||||
|
| 0x8C | 12 | 01DDh | I2C_READ (variant) |
|
||||||
|
| 0x8D | 13 | 01EFh | DiSEqC / LNB control? |
|
||||||
|
| 0x8E | 14 | 0326h | (unhandled) |
|
||||||
|
| 0x8F | 15 | 01FCh | GPIF flow control / read register |
|
||||||
|
| 0x90 | 16 | 020Bh | TUNE — set frequency / symbol rate |
|
||||||
|
| 0x91 | 17 | 022Ch | TUNE_STATUS — check lock |
|
||||||
|
| 0x92 | 18 | 024Ah | SET_TRANSPORT — configure TS stream |
|
||||||
|
| 0x93 | 19 | 026Fh | GET_SIGNAL_QUALITY — read SNR/BER |
|
||||||
|
| 0x94 | 20 | 01B9h | I2C bus control |
|
||||||
|
| 0x95 | 21 | 02DFh | GET_DEMOD_STATUS |
|
||||||
|
| 0x96 | 22 | 02B4h | I2C_ADDR_SELECT |
|
||||||
|
| 0x97 | 23 | 02C1h | RAW_REGISTER_READ |
|
||||||
|
| 0x98 | 24 | 02CBh | GET_EP0_BUFFER |
|
||||||
|
| 0x99-9C | 25-28 | 0326h | (unhandled) |
|
||||||
|
| 0x9D | 29 | 02FAh | FIRMWARE_VERSION? |
|
||||||
|
|
||||||
|
## BOOT_8PSK (0x89) Handler: Complete Trace
|
||||||
|
|
||||||
|
### Entry Point (0x00C4)
|
||||||
|
```
|
||||||
|
00C4: JNB bit_06h,00DAh ; Is GPIF configured?
|
||||||
|
; [If GPIF configured]:
|
||||||
|
00C7: MOV DPTR,#E6BAh ; SETUPDAT[2] = wValueL
|
||||||
|
00CA: MOVX A,@DPTR
|
||||||
|
00CB: ADD A,#FFh ; Test if wValue == 0 (ADD #FF sets C if A>0)
|
||||||
|
00CD: MOV bit_07h,C ; bit_07h = (wValue != 0) = "load-from-host" flag
|
||||||
|
00CF: LCALL 1D4Fh ; -> boot_8psk_dispatch()
|
||||||
|
|
||||||
|
; [If NOT configured]:
|
||||||
|
00DA: CLR bit_07h ; Force "no load" mode
|
||||||
|
00DC: LCALL 1D4Fh ; -> boot_8psk_dispatch()
|
||||||
|
|
||||||
|
; [Common exit]:
|
||||||
|
00E5: CLR A
|
||||||
|
00E6: MOV DPTR,#E68Ah ; EP0BCH = 0
|
||||||
|
00E9: MOVX @DPTR,A
|
||||||
|
00EA: MOV DPTR,#E68Bh ; EP0BCL = 1
|
||||||
|
00ED: INC A
|
||||||
|
00EE: MOVX @DPTR,A ; Send 1-byte response (success/fail in EP0BUF[0])
|
||||||
|
00EF: AJMP 0328h ; SETB C; RET (command handled)
|
||||||
|
```
|
||||||
|
|
||||||
|
### boot_8psk_dispatch (0x1D4F)
|
||||||
|
```
|
||||||
|
1D4F: MOV A,6Dh ; Read status flags
|
||||||
|
1D51: RRC A ; bit 0 (init flag) -> C -> bit_08h
|
||||||
|
1D52: MOV bit_08h,C ; bit_08h = "currently booted" state
|
||||||
|
1D54: ORL IOD,#E0h ; PD7:PD5 = 1 (8PSK bus enable)
|
||||||
|
|
||||||
|
1D57: JNB bit_07h,1D93h ; If NOT "load from host" -> error path
|
||||||
|
1D5A: JB bit_08h,1DA5h ; If already booted -> return success
|
||||||
|
|
||||||
|
; --- Signature Check ---
|
||||||
|
1D5D: LCALL 16B8h ; Check 8PSK demod chip signature:
|
||||||
|
; Read descriptor type byte from XDATA
|
||||||
|
; Switch IFCONFIG to port mode (0xC0)
|
||||||
|
; Set OEB=0 (Port B = input)
|
||||||
|
; Set PA7=1, PA6=1 (select chip)
|
||||||
|
; Read Port B:
|
||||||
|
; Type 3: expect IOB=0xA5
|
||||||
|
; Type 4: expect IOB=0x5A
|
||||||
|
; Type 5: expect IOB=0x5B
|
||||||
|
; Type 6: expect IOB=0x5C
|
||||||
|
; Restore IFCONFIG
|
||||||
|
; Return C=1 if signature match
|
||||||
|
1D60: JNC 1DA5h ; Signature fail -> return with current state
|
||||||
|
|
||||||
|
; --- Begin 8PSK Boot ---
|
||||||
|
1D62: ANL IOA,#DFh ; PA5=0 (assert 8PSK reset)
|
||||||
|
1D65: ANL 6Dh,#BFh ; Clear "streaming" flag in 6Dh
|
||||||
|
1D68: CLR bit_09h ; Clear streaming-active flag
|
||||||
|
1D6A: LCALL 1919h ; GPIF abort + FIFO cleanup:
|
||||||
|
; EP2FIFOPFH, GPIFABORT, GPIFCTRL
|
||||||
|
; Wait for GPIF idle
|
||||||
|
; Reset EP2 FIFO
|
||||||
|
|
||||||
|
; --- Configure for programming ---
|
||||||
|
1D6D: MOV A,IOA
|
||||||
|
1D6F: ANL A,#FBh ; PA2=0
|
||||||
|
1D71: ORL A,#02h ; PA1=1 (select programming mode)
|
||||||
|
1D73: MOV IOA,A
|
||||||
|
1D75: MOV R7,#1Eh / R6,#00h
|
||||||
|
1D79: LCALL 1DFBh ; Delay ~30 * (CPUCS-based divisor) cycles
|
||||||
|
|
||||||
|
; --- Deassert reset ---
|
||||||
|
1D7C: ORL IOA,#20h ; PA5=1 (deassert 8PSK reset)
|
||||||
|
1D7F: ORL 6Dh,#01h ; Set "init in progress" flag
|
||||||
|
|
||||||
|
; --- Load 8PSK firmware ---
|
||||||
|
1D82: LCALL 10F2h ; load_8psk_firmware():
|
||||||
|
; I2C read from demod (addr 0x51) to get segment table
|
||||||
|
; Loop: read segment header (addr, length)
|
||||||
|
; I2C write firmware data blocks to demod
|
||||||
|
; Retry up to 5x on write failures
|
||||||
|
; Return C=0 on success, C=1 on failure
|
||||||
|
1D85: JC 1D93h ; If load failed -> error
|
||||||
|
|
||||||
|
; --- Boot init blocks ---
|
||||||
|
1D87: LCALL 0DDDh ; boot_init_blocks():
|
||||||
|
; 3 iterations (init blocks 0,1,2)
|
||||||
|
; Each: wait_for_ready() then I2C write
|
||||||
|
; Sends register configuration to demod
|
||||||
|
1D8A: JC 1D93h ; If init failed -> error
|
||||||
|
1D8C: SETB bit_08h ; Mark "8PSK booted"
|
||||||
|
1D8E: ORL IOD,#E0h ; PD7:PD5 = 1 (8PSK bus active)
|
||||||
|
1D91: SJMP 1DA5h ; -> success exit
|
||||||
|
|
||||||
|
; --- Error path ---
|
||||||
|
1D93: ANL 6Dh,#BCh ; Clear bits 0,1,6 in status
|
||||||
|
1D96: CLR bit_09h ; Clear streaming flag
|
||||||
|
1D98: LCALL 1919h ; GPIF abort + cleanup
|
||||||
|
1D9B: MOV A,IOA
|
||||||
|
1D9D: ANL A,#FDh ; PA1=0
|
||||||
|
1D9F: ORL A,#04h ; PA2=1 (restore normal GPIO)
|
||||||
|
1DA1: MOV IOA,A
|
||||||
|
1DA3: CLR bit_08h ; Mark "not booted"
|
||||||
|
|
||||||
|
; --- Return ---
|
||||||
|
1DA5: MOV C,bit_08h ; Return C = boot success
|
||||||
|
1DA7: RET
|
||||||
|
```
|
||||||
|
|
||||||
|
## USB Interrupt Vector Table (0x1600)
|
||||||
|
|
||||||
|
The FX2LP USB autovector table at 0x1600. Each entry is a 3-byte LJMP + 1 NOP (4 bytes per slot). The hardware indexes into this table via INT2IVEC.
|
||||||
|
|
||||||
|
| Offset | IRQ Source | Target | Status |
|
||||||
|
|--------|-----------------|--------|--------|
|
||||||
|
| +0x00 | SUDAV | 22E4h | **Active** — sets bit_01h, clears USBIRQ |
|
||||||
|
| +0x04 | SOF | 239Fh | **Active** |
|
||||||
|
| +0x08 | SUTOK | 2389h | **Active** |
|
||||||
|
| +0x0C | SUSPEND | 22FCh | **Active** — sets bit_03h |
|
||||||
|
| +0x10 | USBRESET | 1FB6h | **Active** |
|
||||||
|
| +0x14 | HISPEED | 1EC7h | **Active** |
|
||||||
|
| +0x18 | EP0ACK | 0032h | Stub (RETI) |
|
||||||
|
| +0x24 | EP0OUT | 0FFFh | **Active** |
|
||||||
|
| +0x28+ | EP1-8, IBN, etc | 24E0h+ | All stubs |
|
||||||
|
|
||||||
|
## USB Descriptors (0x1200)
|
||||||
|
|
||||||
|
```
|
||||||
|
Device Descriptor (18 bytes):
|
||||||
|
bLength: 18
|
||||||
|
bDescriptorType: 1 (DEVICE)
|
||||||
|
bcdUSB: 02.00
|
||||||
|
bDeviceClass: 0xFF (vendor-specific)
|
||||||
|
bDeviceSubClass: 0xFF
|
||||||
|
bDeviceProtocol: 0x00
|
||||||
|
bMaxPacketSize0: 64
|
||||||
|
idVendor: 0x09C0 (Cypress Semiconductor)
|
||||||
|
idProduct: 0x0203
|
||||||
|
bcdDevice: 0x0001
|
||||||
|
iManufacturer: 1
|
||||||
|
iProduct: 2
|
||||||
|
iSerialNumber: 3
|
||||||
|
bNumConfigurations: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bit-Addressable Flag Map
|
||||||
|
|
||||||
|
| Bit | Name/Purpose |
|
||||||
|
|-------|-------------|
|
||||||
|
| bit_00h | Wakeup/resume active |
|
||||||
|
| bit_01h | SETUP packet pending (from SUDAV ISR) |
|
||||||
|
| bit_02h | System initialized |
|
||||||
|
| bit_03h | SUSPEND pending (from SUSPEND ISR) |
|
||||||
|
| bit_04h | (unused/reserved) |
|
||||||
|
| bit_05h | (unused/reserved) |
|
||||||
|
| bit_06h | GPIF descriptor present |
|
||||||
|
| bit_07h | USB disconnect / "load-from-host" flag (dual use) |
|
||||||
|
| bit_08h | 8PSK demod booted successfully |
|
||||||
|
| bit_09h | Streaming active (GPIF/TS transfer in progress) |
|
||||||
|
| bit_0Ah | Autopointer direction flag |
|
||||||
|
|
||||||
|
## Direct RAM Variable Map
|
||||||
|
|
||||||
|
| Address | Name/Purpose |
|
||||||
|
|---------|-------------|
|
||||||
|
| 08h:09h | Descriptor pointer 7 (high-speed variant?) |
|
||||||
|
| 0Ah:0Bh | Configuration descriptor pointer |
|
||||||
|
| 0Ch:0Dh | Device descriptor pointer (0x1200) |
|
||||||
|
| 0Eh:0Fh | (adjusted descriptor offset) |
|
||||||
|
| 10h:11h | (adjusted descriptor offset) |
|
||||||
|
| 12h:13h | Other-speed config descriptor pointer |
|
||||||
|
| 14h:15h | Device qualifier descriptor pointer |
|
||||||
|
| 16h:17h | String descriptor pointer |
|
||||||
|
| 22h-25h | 32-bit counter/offset (XDATA copy) |
|
||||||
|
| 26h-29h | 32-bit counter (descriptor calculation) |
|
||||||
|
| 2Ah-2Dh | 32-bit counter (main state) |
|
||||||
|
| 32h-35h | 32-bit counter (main state) |
|
||||||
|
| 36h:37h | USB descriptor base (passed to 1A0E) |
|
||||||
|
| 38h | Descriptor iteration counter |
|
||||||
|
| 3Bh | Boot init block index / temp |
|
||||||
|
| 3Ch | I2C temp / ready flag |
|
||||||
|
| 3Dh:3Eh | wait_for_ready countdown |
|
||||||
|
| 3Fh:40h | Delay counter / retry counter |
|
||||||
|
| 45h:46h | I2C buffer pointer high:low |
|
||||||
|
| 49h-4Bh | Autopointer save area |
|
||||||
|
| 4Ch:4Dh | Saved IRQ state |
|
||||||
|
| 4Eh-50h | EP buffer config |
|
||||||
|
| 66h | (state variable) |
|
||||||
|
| 68h:69h | (state variables) |
|
||||||
|
| 6Dh | Master status flags byte |
|
||||||
|
| 6Fh:70h | Secondary wait counter |
|
||||||
|
|
||||||
|
## Key Observations for RAM-Load Boot Failure
|
||||||
|
|
||||||
|
1. **IDATA cleared on reset**: The startup clears IDATA 0x01-0x7F. All bit-addressable flags and direct RAM variables start at 0. This is critical — if you're loading into RAM without resetting the CPU, these won't be cleared.
|
||||||
|
|
||||||
|
2. **XDATA init from CODE space**: The Keil startup copies initialization data from a table in CODE space to XDATA. If your RAM load doesn't include this init data at the correct CODE-relative offset, XDATA won't be initialized properly.
|
||||||
|
|
||||||
|
3. **CPUCS clock switch**: The firmware switches to 48MHz immediately in hw_init(). If CPUCS is already set (from a previous boot), the second write may cause timing issues.
|
||||||
|
|
||||||
|
4. **USB re-enumeration**: The firmware explicitly disconnects (USBCS.DISCON=1), waits ~8ms, then reconnects. If you're loading via USB, this disconnect cycle will kill your USB connection.
|
||||||
|
|
||||||
|
5. **E6B8-E6BF SETUPDAT mapping**: The firmware uses an alternate XDATA mapping for SETUPDAT at 0xE6B8 instead of the standard 0xE6A5. Both mappings are valid, but your host-side code must account for this if doing SETUP data inspection.
|
||||||
|
|
||||||
|
6. **Interrupt vector overwrite risk**: The reset vector at 0x0000 and all interrupt vectors (0x0003-0x006B) contain actual code, not just jump stubs. The INT0 vector at 0x0003 contains the USB re-enumeration handler. Loading code that overwrites these vectors will break USB.
|
||||||
|
|
||||||
|
7. **GPIF waveform data**: The GPIF config at 0x12EA reads waveform data from XDATA 0xE000-0xE08E. This data must be present in XDATA before hw_init() runs. If it's missing (because the EEPROM-to-XDATA copy didn't happen), GPIF will be misconfigured.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Generated from firmware analysis of skywalker1_eeprom_full64k.bin*
|
||||||
|
*Disassembly tool: disasm8051.py (custom 8051 disassembler)*
|
||||||
660
firmware-dump/disasm8051.py
Normal file
660
firmware-dump/disasm8051.py
Normal file
@ -0,0 +1,660 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""8051 disassembler for Genpix SkyWalker-1 firmware analysis.
|
||||||
|
|
||||||
|
Produces annotated disassembly of the FX2LP (8051-based) firmware,
|
||||||
|
focusing on the Keil C51 startup sequence and USB initialization.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
OPCODES = {}
|
||||||
|
|
||||||
|
def op(code, mnem, length, desc=""):
|
||||||
|
OPCODES[code] = (mnem, length, desc)
|
||||||
|
|
||||||
|
# NOP
|
||||||
|
op(0x00, "NOP", 1)
|
||||||
|
# AJMP/ACALL (page 0-7)
|
||||||
|
for page in range(8):
|
||||||
|
op(0x01 + page*0x20, f"AJMP {{a11:{page}}}", 2)
|
||||||
|
op(0x11 + page*0x20, f"ACALL {{a11:{page}}}", 2)
|
||||||
|
|
||||||
|
op(0x02, "LJMP {a16}", 3)
|
||||||
|
op(0x12, "LCALL {a16}", 3)
|
||||||
|
op(0x03, "RR A", 1)
|
||||||
|
op(0x13, "RRC A", 1)
|
||||||
|
op(0x23, "RL A", 1)
|
||||||
|
op(0x33, "RLC A", 1)
|
||||||
|
|
||||||
|
# INC
|
||||||
|
op(0x04, "INC A", 1)
|
||||||
|
op(0x05, "INC {d}", 2)
|
||||||
|
op(0x06, "INC @R0", 1)
|
||||||
|
op(0x07, "INC @R1", 1)
|
||||||
|
for i in range(8): op(0x08+i, f"INC R{i}", 1)
|
||||||
|
|
||||||
|
# JBC, JB, JNB, JC, JNC, JZ, JNZ
|
||||||
|
op(0x10, "JBC {bit},{r8}", 3)
|
||||||
|
op(0x20, "JB {bit},{r8}", 3)
|
||||||
|
op(0x30, "JNB {bit},{r8}", 3)
|
||||||
|
op(0x40, "JC {r8}", 2)
|
||||||
|
op(0x50, "JNC {r8}", 2)
|
||||||
|
op(0x60, "JZ {r8}", 2)
|
||||||
|
op(0x70, "JNZ {r8}", 2)
|
||||||
|
|
||||||
|
# DEC
|
||||||
|
op(0x14, "DEC A", 1)
|
||||||
|
op(0x15, "DEC {d}", 2)
|
||||||
|
op(0x16, "DEC @R0", 1)
|
||||||
|
op(0x17, "DEC @R1", 1)
|
||||||
|
for i in range(8): op(0x18+i, f"DEC R{i}", 1)
|
||||||
|
|
||||||
|
# ADD
|
||||||
|
op(0x24, "ADD A,#{imm}", 2)
|
||||||
|
op(0x25, "ADD A,{d}", 2)
|
||||||
|
op(0x26, "ADD A,@R0", 1)
|
||||||
|
op(0x27, "ADD A,@R1", 1)
|
||||||
|
for i in range(8): op(0x28+i, f"ADD A,R{i}", 1)
|
||||||
|
|
||||||
|
# ADDC
|
||||||
|
op(0x34, "ADDC A,#{imm}", 2)
|
||||||
|
op(0x35, "ADDC A,{d}", 2)
|
||||||
|
op(0x36, "ADDC A,@R0", 1)
|
||||||
|
op(0x37, "ADDC A,@R1", 1)
|
||||||
|
for i in range(8): op(0x38+i, f"ADDC A,R{i}", 1)
|
||||||
|
|
||||||
|
# ORL
|
||||||
|
op(0x42, "ORL {d},A", 2)
|
||||||
|
op(0x43, "ORL {d},#{imm}", 3)
|
||||||
|
op(0x44, "ORL A,#{imm}", 2)
|
||||||
|
op(0x45, "ORL A,{d}", 2)
|
||||||
|
op(0x46, "ORL A,@R0", 1)
|
||||||
|
op(0x47, "ORL A,@R1", 1)
|
||||||
|
for i in range(8): op(0x48+i, f"ORL A,R{i}", 1)
|
||||||
|
|
||||||
|
# ANL
|
||||||
|
op(0x52, "ANL {d},A", 2)
|
||||||
|
op(0x53, "ANL {d},#{imm}", 3)
|
||||||
|
op(0x54, "ANL A,#{imm}", 2)
|
||||||
|
op(0x55, "ANL A,{d}", 2)
|
||||||
|
op(0x56, "ANL A,@R0", 1)
|
||||||
|
op(0x57, "ANL A,@R1", 1)
|
||||||
|
for i in range(8): op(0x58+i, f"ANL A,R{i}", 1)
|
||||||
|
|
||||||
|
# XRL
|
||||||
|
op(0x62, "XRL {d},A", 2)
|
||||||
|
op(0x63, "XRL {d},#{imm}", 3)
|
||||||
|
op(0x64, "XRL A,#{imm}", 2)
|
||||||
|
op(0x65, "XRL A,{d}", 2)
|
||||||
|
op(0x66, "XRL A,@R0", 1)
|
||||||
|
op(0x67, "XRL A,@R1", 1)
|
||||||
|
for i in range(8): op(0x68+i, f"XRL A,R{i}", 1)
|
||||||
|
|
||||||
|
# ORL C, ANL C
|
||||||
|
op(0x72, "ORL C,{bit}", 2)
|
||||||
|
op(0xA0, "ORL C,/{bit}", 2)
|
||||||
|
op(0x82, "ANL C,{bit}", 2)
|
||||||
|
op(0xB0, "ANL C,/{bit}", 2)
|
||||||
|
|
||||||
|
# MOV bit
|
||||||
|
op(0x92, "MOV {bit},C", 2)
|
||||||
|
op(0xA2, "MOV C,{bit}", 2)
|
||||||
|
|
||||||
|
# MOV direct
|
||||||
|
op(0x75, "MOV {d},#{imm}", 3)
|
||||||
|
op(0x85, "MOV {d2},{d1}", 3)
|
||||||
|
op(0xE5, "MOV A,{d}", 2)
|
||||||
|
op(0xF5, "MOV {d},A", 2)
|
||||||
|
op(0xA5, "DB A5h", 1)
|
||||||
|
|
||||||
|
# MOV A,#imm
|
||||||
|
op(0x74, "MOV A,#{imm}", 2)
|
||||||
|
|
||||||
|
# MOV A,@Ri / MOV @Ri,A
|
||||||
|
op(0xE6, "MOV A,@R0", 1)
|
||||||
|
op(0xE7, "MOV A,@R1", 1)
|
||||||
|
op(0xF6, "MOV @R0,A", 1)
|
||||||
|
op(0xF7, "MOV @R1,A", 1)
|
||||||
|
|
||||||
|
# MOV A,Rn / MOV Rn,A
|
||||||
|
for i in range(8):
|
||||||
|
op(0xE8+i, f"MOV A,R{i}", 1)
|
||||||
|
op(0xF8+i, f"MOV R{i},A", 1)
|
||||||
|
|
||||||
|
# MOV Rn,#imm
|
||||||
|
for i in range(8): op(0x78+i, f"MOV R{i},#{{imm}}", 2)
|
||||||
|
# MOV Rn,direct
|
||||||
|
for i in range(8): op(0xA8+i, f"MOV R{i},{{d}}", 2)
|
||||||
|
# MOV direct,Rn
|
||||||
|
for i in range(8): op(0x88+i, f"MOV {{d}},R{i}", 2)
|
||||||
|
|
||||||
|
op(0x86, "MOV {d},@R0", 2)
|
||||||
|
op(0x87, "MOV {d},@R1", 2)
|
||||||
|
op(0xA6, "MOV @R0,{d}", 2)
|
||||||
|
op(0xA7, "MOV @R1,{d}", 2)
|
||||||
|
op(0x76, "MOV @R0,#{imm}", 2)
|
||||||
|
op(0x77, "MOV @R1,#{imm}", 2)
|
||||||
|
|
||||||
|
# MOV DPTR,#imm16
|
||||||
|
op(0x90, "MOV DPTR,#{imm16}", 3)
|
||||||
|
|
||||||
|
# MOVC
|
||||||
|
op(0x83, "MOVC A,@A+PC", 1)
|
||||||
|
op(0x93, "MOVC A,@A+DPTR", 1)
|
||||||
|
|
||||||
|
# MOVX
|
||||||
|
op(0xE0, "MOVX A,@DPTR", 1)
|
||||||
|
op(0xE2, "MOVX A,@R0", 1)
|
||||||
|
op(0xE3, "MOVX A,@R1", 1)
|
||||||
|
op(0xF0, "MOVX @DPTR,A", 1)
|
||||||
|
op(0xF2, "MOVX @R0,A", 1)
|
||||||
|
op(0xF3, "MOVX @R1,A", 1)
|
||||||
|
|
||||||
|
# PUSH/POP
|
||||||
|
op(0xC0, "PUSH {d}", 2)
|
||||||
|
op(0xD0, "POP {d}", 2)
|
||||||
|
|
||||||
|
# XCH
|
||||||
|
op(0xC5, "XCH A,{d}", 2)
|
||||||
|
op(0xC6, "XCH A,@R0", 1)
|
||||||
|
op(0xC7, "XCH A,@R1", 1)
|
||||||
|
for i in range(8): op(0xC8+i, f"XCH A,R{i}", 1)
|
||||||
|
|
||||||
|
# XCHD
|
||||||
|
op(0xD6, "XCHD A,@R0", 1)
|
||||||
|
op(0xD7, "XCHD A,@R1", 1)
|
||||||
|
|
||||||
|
# DJNZ
|
||||||
|
op(0xD5, "DJNZ {d},{r8}", 3)
|
||||||
|
for i in range(8): op(0xD8+i, f"DJNZ R{i},{{r8}}", 2)
|
||||||
|
|
||||||
|
# CJNE
|
||||||
|
op(0xB4, "CJNE A,#{imm},{r8}", 3)
|
||||||
|
op(0xB5, "CJNE A,{d},{r8}", 3)
|
||||||
|
op(0xB6, "CJNE @R0,#{imm},{r8}", 3)
|
||||||
|
op(0xB7, "CJNE @R1,#{imm},{r8}", 3)
|
||||||
|
for i in range(8): op(0xB8+i, f"CJNE R{i},#{{imm}},{{r8}}", 3)
|
||||||
|
|
||||||
|
# SJMP
|
||||||
|
op(0x80, "SJMP {r8}", 2)
|
||||||
|
# JMP @A+DPTR
|
||||||
|
op(0x73, "JMP @A+DPTR", 1)
|
||||||
|
# RET, RETI
|
||||||
|
op(0x22, "RET", 1)
|
||||||
|
op(0x32, "RETI", 1)
|
||||||
|
|
||||||
|
# CLR, SETB, CPL
|
||||||
|
op(0xC2, "CLR {bit}", 2)
|
||||||
|
op(0xC3, "CLR C", 1)
|
||||||
|
op(0xD2, "SETB {bit}", 2)
|
||||||
|
op(0xD3, "SETB C", 1)
|
||||||
|
op(0xB2, "CPL {bit}", 2)
|
||||||
|
op(0xB3, "CPL C", 1)
|
||||||
|
op(0xE4, "CLR A", 1)
|
||||||
|
op(0xF4, "CPL A", 1)
|
||||||
|
|
||||||
|
# MUL, DIV, DA, SWAP
|
||||||
|
op(0xA4, "MUL AB", 1)
|
||||||
|
op(0x84, "DIV AB", 1)
|
||||||
|
op(0xD4, "DA A", 1)
|
||||||
|
op(0xC4, "SWAP A", 1)
|
||||||
|
|
||||||
|
# SUBB
|
||||||
|
op(0x94, "SUBB A,#{imm}", 2)
|
||||||
|
op(0x95, "SUBB A,{d}", 2)
|
||||||
|
op(0x96, "SUBB A,@R0", 1)
|
||||||
|
op(0x97, "SUBB A,@R1", 1)
|
||||||
|
for i in range(8): op(0x98+i, f"SUBB A,R{i}", 1)
|
||||||
|
|
||||||
|
# INC DPTR
|
||||||
|
op(0xA3, "INC DPTR", 1)
|
||||||
|
|
||||||
|
|
||||||
|
# ============ SFR / XDATA / BIT names ============
|
||||||
|
|
||||||
|
SFR_NAMES = {
|
||||||
|
0x80: "IOA", 0x81: "SP", 0x82: "DPL", 0x83: "DPH",
|
||||||
|
0x84: "DPL1", 0x85: "DPH1", 0x86: "DPS", 0x87: "PCON",
|
||||||
|
0x88: "TCON", 0x89: "TMOD", 0x8A: "TL0", 0x8B: "TL1",
|
||||||
|
0x8C: "TH0", 0x8D: "TH1", 0x8E: "CKCON",
|
||||||
|
0x90: "IOB", 0x91: "EXIF",
|
||||||
|
0x98: "SCON0", 0x99: "SBUF0",
|
||||||
|
0x9A: "AUTOPTRH1", 0x9B: "AUTOPTRL1",
|
||||||
|
0x9C: "AUTOPTRH2", 0x9D: "AUTOPTRL2", 0x9E: "AUTOPTRSETUP",
|
||||||
|
0xA0: "IOC", 0xA8: "IE",
|
||||||
|
0xAA: "EP2468STAT", 0xAB: "EP24FIFOFLGS", 0xAC: "EP68FIFOFLGS",
|
||||||
|
0xB0: "IOD", 0xB1: "IOE",
|
||||||
|
0xB2: "OEA", 0xB3: "OEB", 0xB4: "OEC", 0xB5: "OED", 0xB6: "OEE",
|
||||||
|
0xC0: "SCON1", 0xC1: "SBUF1",
|
||||||
|
0xC8: "T2CON", 0xCA: "RCAP2L", 0xCB: "RCAP2H", 0xCC: "TL2", 0xCD: "TH2",
|
||||||
|
0xD0: "PSW", 0xD8: "EICON",
|
||||||
|
0xE0: "ACC", 0xE8: "EIE", 0xF0: "B", 0xF8: "EIP",
|
||||||
|
}
|
||||||
|
|
||||||
|
XREG_NAMES = {
|
||||||
|
0xE600: "CPUCS", 0xE601: "IFCONFIG", 0xE602: "PINFLAGSAB",
|
||||||
|
0xE603: "PINFLAGSCD", 0xE604: "FIFORESET", 0xE609: "REVCTL",
|
||||||
|
0xE60A: "GPIFTRIG", 0xE60B: "GPIFSGLDATH", 0xE60C: "GPIFSGLDATLX",
|
||||||
|
0xE610: "FLOWSTATE", 0xE611: "FLOWLOGIC",
|
||||||
|
0xE618: "GPIFHOLDAMOUNT",
|
||||||
|
0xE620: "EP2CFG", 0xE621: "EP4CFG", 0xE622: "EP6CFG", 0xE623: "EP8CFG",
|
||||||
|
0xE624: "EP2FIFOCFG", 0xE625: "EP4FIFOCFG",
|
||||||
|
0xE626: "EP6FIFOCFG", 0xE627: "EP8FIFOCFG",
|
||||||
|
0xE628: "EP2AUTOINLENH", 0xE629: "EP2AUTOINLENL",
|
||||||
|
0xE630: "EP2FIFOPFH", 0xE631: "EP2FIFOPFL",
|
||||||
|
0xE640: "EP2ISOINPKTS",
|
||||||
|
0xE648: "INPKTEND", 0xE649: "OUTPKTEND",
|
||||||
|
0xE650: "EP2FIFOIE", 0xE651: "EP2FIFOIRQ",
|
||||||
|
0xE65C: "USBIE", 0xE65D: "USBIRQ",
|
||||||
|
0xE65E: "EPIE", 0xE65F: "EPIRQ",
|
||||||
|
0xE660: "GPIFIE", 0xE661: "GPIFIRQ",
|
||||||
|
0xE662: "USBERRIE", 0xE663: "USBERRIRQ",
|
||||||
|
0xE666: "INT2IVEC", 0xE667: "INT4IVEC", 0xE668: "INTSETUP",
|
||||||
|
0xE670: "PORTACFG", 0xE671: "PORTCCFG", 0xE672: "PORTECFG",
|
||||||
|
0xE678: "I2CS", 0xE679: "I2DAT", 0xE67A: "I2CTL",
|
||||||
|
0xE67B: "XAUTODAT1", 0xE67C: "XAUTODAT2",
|
||||||
|
0xE680: "USBCS", 0xE681: "SUSPEND", 0xE682: "WAKEUPCS",
|
||||||
|
0xE683: "TOGCTL",
|
||||||
|
0xE684: "USBFRAMEH", 0xE685: "USBFRAMEL", 0xE686: "MICROFRAME",
|
||||||
|
0xE687: "FNADDR",
|
||||||
|
0xE68A: "EP0BCH", 0xE68B: "EP0BCL",
|
||||||
|
0xE68C: "EP1OUTBC", 0xE68D: "EP1INBC",
|
||||||
|
0xE68F: "EP1INCS",
|
||||||
|
0xE690: "EP2CS", 0xE691: "EP4CS", 0xE692: "EP6CS", 0xE693: "EP8CS",
|
||||||
|
0xE694: "EP2FIFOFLGS", 0xE695: "EP4FIFOFLGS",
|
||||||
|
0xE696: "EP6FIFOFLGS", 0xE697: "EP8FIFOFLGS",
|
||||||
|
0xE698: "EP2BCH", 0xE699: "EP2BCL",
|
||||||
|
0xE6A0: "EP0CS", 0xE6A1: "EP0STAT", 0xE6A2: "EP0STALLBITS",
|
||||||
|
0xE6A3: "CLRTOGS",
|
||||||
|
0xE6A5: "SETUPDAT[0]", 0xE6A6: "SETUPDAT[1]",
|
||||||
|
0xE6A7: "SETUPDAT[2]", 0xE6A8: "SETUPDAT[3]",
|
||||||
|
0xE6A9: "SETUPDAT[4]", 0xE6AA: "SETUPDAT[5]",
|
||||||
|
0xE6AB: "SETUPDAT[6]", 0xE6AC: "SETUPDAT[7]",
|
||||||
|
0xE6C0: "GPIFWFSELECT", 0xE6C1: "GPIFIDLECS",
|
||||||
|
0xE6C2: "GPIFIDLECTL", 0xE6C3: "GPIFCTLCFG",
|
||||||
|
0xE6C4: "GPIFADRH", 0xE6C5: "GPIFADRL",
|
||||||
|
0xE6CE: "GPIFREADYCFG", 0xE6CF: "GPIFREADYSTAT",
|
||||||
|
0xE6D0: "GPIFABORT",
|
||||||
|
0xE6F8: "SUDPTRH", 0xE6F9: "SUDPTRL", 0xE6FA: "SUDPTRCTL",
|
||||||
|
0xE740: "EP0BUF[0]",
|
||||||
|
0xE780: "EP1OUTBUF[0]",
|
||||||
|
0xE7C0: "EP1INBUF[0]",
|
||||||
|
}
|
||||||
|
|
||||||
|
BIT_NAMES = {
|
||||||
|
0xD0: "PSW.P", 0xD1: "PSW.1", 0xD2: "PSW.OV", 0xD3: "PSW.RS0",
|
||||||
|
0xD4: "PSW.RS1", 0xD5: "PSW.F0", 0xD6: "PSW.AC", 0xD7: "PSW.CY",
|
||||||
|
0xA8: "EX0", 0xA9: "ET0", 0xAA: "EX1", 0xAB: "ET1",
|
||||||
|
0xAC: "ES0", 0xAD: "ET2", 0xAE: "ES1", 0xAF: "EA",
|
||||||
|
0x80: "IOA.0", 0x81: "IOA.1", 0x82: "IOA.2", 0x83: "IOA.3",
|
||||||
|
0x84: "IOA.4", 0x85: "IOA.5", 0x86: "IOA.6", 0x87: "IOA.7",
|
||||||
|
0x90: "IOB.0", 0x91: "IOB.1", 0x92: "IOB.2", 0x93: "IOB.3",
|
||||||
|
0x94: "IOB.4", 0x95: "IOB.5", 0x96: "IOB.6", 0x97: "IOB.7",
|
||||||
|
0x88: "IT0", 0x89: "IE0", 0x8A: "IT1", 0x8B: "IE1",
|
||||||
|
0x8C: "TR0", 0x8D: "TF0", 0x8E: "TR1", 0x8F: "TF1",
|
||||||
|
0x98: "RI_0", 0x99: "TI_0",
|
||||||
|
0xD8: "EICON.0", 0xDB: "INT6", 0xDC: "RESI", 0xDD: "ERESI",
|
||||||
|
}
|
||||||
|
|
||||||
|
KNOWN_LABELS = {
|
||||||
|
0x0000: "reset_vector",
|
||||||
|
0x0003: "int0_vector",
|
||||||
|
0x000B: "timer0_vector",
|
||||||
|
0x0013: "int1_vector",
|
||||||
|
0x001B: "timer1_vector",
|
||||||
|
0x0023: "serial0_vector",
|
||||||
|
0x002B: "timer2_vector",
|
||||||
|
0x0033: "resume_vector",
|
||||||
|
0x003B: "serial1_vector",
|
||||||
|
0x0043: "usb_int2_vector",
|
||||||
|
0x004B: "i2c_int3_vector",
|
||||||
|
0x0053: "gpif_int4_vector",
|
||||||
|
0x005B: "int5_vector",
|
||||||
|
0x0063: "int6_vector",
|
||||||
|
0x099A: "tune_function",
|
||||||
|
0x0DDD: "boot_init_blocks",
|
||||||
|
0x0EE9: "tune_init_blocks",
|
||||||
|
0x1200: "usb_device_desc",
|
||||||
|
0x1556: "i2c_combined_read",
|
||||||
|
0x188D: "keil_startup",
|
||||||
|
0x1A81: "i2c_write",
|
||||||
|
0x1D87: "boot_8psk",
|
||||||
|
0x2000: "wait_for_ready",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def sfr_name(addr):
|
||||||
|
if addr in SFR_NAMES:
|
||||||
|
return SFR_NAMES[addr]
|
||||||
|
if addr >= 0x80:
|
||||||
|
return f"SFR_{addr:02X}h"
|
||||||
|
return f"{addr:02X}h"
|
||||||
|
|
||||||
|
def bit_name(addr):
|
||||||
|
if addr in BIT_NAMES:
|
||||||
|
return BIT_NAMES[addr]
|
||||||
|
if addr >= 0x80:
|
||||||
|
base = addr & 0xF8
|
||||||
|
bit = addr & 0x07
|
||||||
|
if base in SFR_NAMES:
|
||||||
|
return f"{SFR_NAMES[base]}.{bit}"
|
||||||
|
return f"bit_{addr:02X}h"
|
||||||
|
|
||||||
|
def xreg_name(addr):
|
||||||
|
if addr in XREG_NAMES:
|
||||||
|
return XREG_NAMES[addr]
|
||||||
|
if 0xE740 <= addr <= 0xE77F:
|
||||||
|
return f"EP0BUF[{addr-0xE740}]"
|
||||||
|
if 0xE780 <= addr <= 0xE7BF:
|
||||||
|
return f"EP1OUTBUF[{addr-0xE780}]"
|
||||||
|
if 0xE7C0 <= addr <= 0xE7FF:
|
||||||
|
return f"EP1INBUF[{addr-0xE7C0}]"
|
||||||
|
if 0xE600 <= addr <= 0xE6FF:
|
||||||
|
return f"XSFR_{addr:04X}h"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def disasm_one(data, pc):
|
||||||
|
"""Returns (mnem, hex_str, length, comment, branch_target)."""
|
||||||
|
if pc >= len(data):
|
||||||
|
return ("???", "", 1, "", None)
|
||||||
|
|
||||||
|
opc = data[pc]
|
||||||
|
entry = OPCODES.get(opc)
|
||||||
|
|
||||||
|
if entry is None:
|
||||||
|
return (f"DB {opc:02X}h", f"{opc:02X}", 1, "; UNKNOWN", None)
|
||||||
|
|
||||||
|
mnem_fmt, length, desc = entry
|
||||||
|
|
||||||
|
if pc + length > len(data):
|
||||||
|
return (f"DB {opc:02X}h", f"{opc:02X}", 1, "; TRUNCATED", None)
|
||||||
|
|
||||||
|
raw_bytes = data[pc:pc+length]
|
||||||
|
hex_str = " ".join(f"{b:02X}" for b in raw_bytes)
|
||||||
|
comment = ""
|
||||||
|
branch_target = None
|
||||||
|
mnem = mnem_fmt
|
||||||
|
|
||||||
|
# Resolve address fields
|
||||||
|
if "{a16}" in mnem:
|
||||||
|
addr16 = (data[pc+1] << 8) | data[pc+2]
|
||||||
|
label = KNOWN_LABELS.get(addr16)
|
||||||
|
mnem = mnem.replace("{a16}", label if label else f"{addr16:04X}h")
|
||||||
|
branch_target = addr16
|
||||||
|
|
||||||
|
m = re.search(r'\{a11:(\d+)\}', mnem)
|
||||||
|
if m:
|
||||||
|
page = int(m.group(1))
|
||||||
|
addr11 = (page << 8) | data[pc+1] | ((pc + 2) & 0xF800)
|
||||||
|
label = KNOWN_LABELS.get(addr11)
|
||||||
|
mnem = re.sub(r'\{a11:\d+\}', label if label else f"{addr11:04X}h", mnem)
|
||||||
|
branch_target = addr11
|
||||||
|
|
||||||
|
if "{r8}" in mnem:
|
||||||
|
rel_byte = data[pc + length - 1]
|
||||||
|
rel = rel_byte if rel_byte < 0x80 else rel_byte - 256
|
||||||
|
target = (pc + length + rel) & 0xFFFF
|
||||||
|
label = KNOWN_LABELS.get(target)
|
||||||
|
mnem = mnem.replace("{r8}", label if label else f"{target:04X}h")
|
||||||
|
branch_target = target
|
||||||
|
|
||||||
|
# Direct addressing - handle the special MOV d2,d1 case
|
||||||
|
if "{d2}" in mnem and "{d1}" in mnem:
|
||||||
|
src = data[pc+1]
|
||||||
|
dst = data[pc+2]
|
||||||
|
mnem = mnem.replace("{d1}", sfr_name(src) if src >= 0x20 else f"{src:02X}h")
|
||||||
|
mnem = mnem.replace("{d2}", sfr_name(dst) if dst >= 0x20 else f"{dst:02X}h")
|
||||||
|
elif "{d}" in mnem:
|
||||||
|
d = data[pc+1]
|
||||||
|
mnem = mnem.replace("{d}", sfr_name(d) if d >= 0x20 else f"{d:02X}h")
|
||||||
|
|
||||||
|
if "{bit}" in mnem:
|
||||||
|
b = data[pc+1]
|
||||||
|
mnem = mnem.replace("{bit}", bit_name(b))
|
||||||
|
|
||||||
|
if "{imm16}" in mnem:
|
||||||
|
imm16 = (data[pc+1] << 8) | data[pc+2]
|
||||||
|
xname = xreg_name(imm16)
|
||||||
|
if xname:
|
||||||
|
mnem = mnem.replace("{imm16}", f"{imm16:04X}h")
|
||||||
|
comment = f"; = {xname}"
|
||||||
|
elif imm16 in KNOWN_LABELS:
|
||||||
|
mnem = mnem.replace("{imm16}", f"{imm16:04X}h")
|
||||||
|
comment = f"; -> {KNOWN_LABELS[imm16]}"
|
||||||
|
else:
|
||||||
|
mnem = mnem.replace("{imm16}", f"{imm16:04X}h")
|
||||||
|
|
||||||
|
if "{imm}" in mnem:
|
||||||
|
# Determine which byte is the immediate
|
||||||
|
if length == 3 and opc in (0x43, 0x53, 0x63):
|
||||||
|
imm = data[pc+2] # ORL/ANL/XRL d,#imm
|
||||||
|
elif length == 3 and opc == 0x75:
|
||||||
|
imm = data[pc+2] # MOV d,#imm
|
||||||
|
elif length == 3 and opc == 0xB4:
|
||||||
|
imm = data[pc+1] # CJNE A,#imm,r8
|
||||||
|
elif length == 3 and 0xB6 <= opc <= 0xBF:
|
||||||
|
imm = data[pc+1] # CJNE @Ri/#imm or CJNE Rn,#imm
|
||||||
|
elif length == 3 and opc == 0xD5:
|
||||||
|
# DJNZ d,r8 - no imm, but {d} already handled
|
||||||
|
imm = 0 # shouldn't happen
|
||||||
|
else:
|
||||||
|
imm = data[pc + length - 1]
|
||||||
|
mnem = mnem.replace("{imm}", f"{imm:02X}h")
|
||||||
|
|
||||||
|
return (mnem, hex_str, length, comment, branch_target)
|
||||||
|
|
||||||
|
|
||||||
|
def disasm_range(data, start, end, title=""):
|
||||||
|
lines = []
|
||||||
|
if title:
|
||||||
|
lines.append(f"\n{'='*80}")
|
||||||
|
lines.append(f" {title}")
|
||||||
|
lines.append(f"{'='*80}")
|
||||||
|
|
||||||
|
pc = start
|
||||||
|
while pc < end and pc < len(data):
|
||||||
|
if pc in KNOWN_LABELS:
|
||||||
|
lines.append(f"\n {KNOWN_LABELS[pc]}:")
|
||||||
|
|
||||||
|
mnem, hex_str, length, comment, target = disasm_one(data, pc)
|
||||||
|
addr_str = f"{pc:04X}"
|
||||||
|
hex_padded = f"{hex_str:<12s}"
|
||||||
|
line = f" {addr_str}: {hex_padded} {mnem}"
|
||||||
|
if comment:
|
||||||
|
line = f"{line:<58s} {comment}"
|
||||||
|
lines.append(line)
|
||||||
|
pc += length
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def track_dptr_accesses(data, start, end):
|
||||||
|
"""Track all MOVX accesses with DPTR context."""
|
||||||
|
results = []
|
||||||
|
pc = start
|
||||||
|
dptr = None
|
||||||
|
while pc < end and pc < len(data):
|
||||||
|
opc = data[pc]
|
||||||
|
entry = OPCODES.get(opc)
|
||||||
|
if entry is None:
|
||||||
|
pc += 1
|
||||||
|
continue
|
||||||
|
_, length, _ = entry
|
||||||
|
if pc + length > len(data):
|
||||||
|
break
|
||||||
|
if opc == 0x90: # MOV DPTR,#imm16
|
||||||
|
dptr = (data[pc+1] << 8) | data[pc+2]
|
||||||
|
elif opc in (0xE0, 0xF0) and dptr is not None:
|
||||||
|
xname = xreg_name(dptr)
|
||||||
|
direction = "READ" if opc == 0xE0 else "WRITE"
|
||||||
|
results.append((pc, dptr, xname, direction))
|
||||||
|
elif opc == 0xA3: # INC DPTR
|
||||||
|
if dptr is not None:
|
||||||
|
dptr += 1
|
||||||
|
pc += length
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
fw_path = "/home/rpm/claude/ham/satellite/genpix/skywalker-1/firmware-dump/skywalker1_eeprom_full64k.bin"
|
||||||
|
with open(fw_path, "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
out = []
|
||||||
|
out.append("=" * 80)
|
||||||
|
out.append(" GENPIX SKYWALKER-1 FIRMWARE DISASSEMBLY")
|
||||||
|
out.append(" FX2LP (CY7C68013A) 8051 Core - Keil C51 Compiled")
|
||||||
|
out.append(" Firmware size: 9472 bytes (0x0000-0x24FF)")
|
||||||
|
out.append(" Binary: 65536 bytes total (RAM image)")
|
||||||
|
out.append("=" * 80)
|
||||||
|
|
||||||
|
# ==== SECTION 1: Reset + Interrupt Vectors ====
|
||||||
|
out.append(disasm_range(data, 0x0000, 0x006B,
|
||||||
|
"SECTION 1: Reset Vector & Interrupt Vector Table (0x0000-0x006A)"))
|
||||||
|
|
||||||
|
# ==== SECTION 2: Keil C51 Startup ====
|
||||||
|
out.append(disasm_range(data, 0x188D, 0x1950,
|
||||||
|
"SECTION 2: Keil C51 Startup (STARTUP.A51) @ 0x188D"))
|
||||||
|
|
||||||
|
# Find main() by tracing startup
|
||||||
|
pc = 0x188D
|
||||||
|
main_addr = None
|
||||||
|
for _ in range(500):
|
||||||
|
if pc >= len(data) - 2:
|
||||||
|
break
|
||||||
|
opc = data[pc]
|
||||||
|
entry = OPCODES.get(opc)
|
||||||
|
if entry is None:
|
||||||
|
break
|
||||||
|
_, length, _ = entry
|
||||||
|
if opc == 0x02: # LJMP
|
||||||
|
addr = (data[pc+1] << 8) | data[pc+2]
|
||||||
|
# The startup has two LJMPs: one internal loop, one to main
|
||||||
|
# main() is the last LJMP before we hit data/different code
|
||||||
|
if addr < 0x188D: # Jump backward = likely to main()
|
||||||
|
main_addr = addr
|
||||||
|
pc += length
|
||||||
|
|
||||||
|
if main_addr:
|
||||||
|
KNOWN_LABELS[main_addr] = "main"
|
||||||
|
out.append(f"\n >>> Keil startup jumps to main() at {main_addr:04X}h")
|
||||||
|
|
||||||
|
# ==== SECTION 3: main() ====
|
||||||
|
if main_addr:
|
||||||
|
# Disassemble main() - generous range to cover init
|
||||||
|
out.append(disasm_range(data, main_addr, min(main_addr + 400, 0x2500),
|
||||||
|
f"SECTION 3: main() @ {main_addr:04X}h (first ~400 bytes)"))
|
||||||
|
|
||||||
|
# ==== SECTION 4: Vendor command dispatch ====
|
||||||
|
out.append("\n" + "=" * 80)
|
||||||
|
out.append(" SECTION 4: USB Vendor Command Dispatch")
|
||||||
|
out.append("=" * 80)
|
||||||
|
|
||||||
|
# Find all reads of SETUPDAT[1] (bRequest)
|
||||||
|
for pc in range(0x2500):
|
||||||
|
if pc + 2 < len(data) and data[pc] == 0x90 and data[pc+1] == 0xE6 and data[pc+2] == 0xA6:
|
||||||
|
out.append(f"\n SETUPDAT[1] read at {pc:04X}h:")
|
||||||
|
ctx_start = max(0, pc - 16)
|
||||||
|
ctx_end = min(pc + 120, 0x2500)
|
||||||
|
out.append(disasm_range(data, ctx_start, ctx_end,
|
||||||
|
f" Dispatch context @ {pc:04X}h"))
|
||||||
|
|
||||||
|
# ==== SECTION 5: Find all CJNE A,#xx comparisons (command dispatch) ====
|
||||||
|
out.append("\n" + "=" * 80)
|
||||||
|
out.append(" SECTION 5: All CJNE A,#xx Instructions (Command Dispatch Table)")
|
||||||
|
out.append("=" * 80)
|
||||||
|
|
||||||
|
for pc in range(0x2500):
|
||||||
|
if pc + 2 < len(data) and data[pc] == 0xB4:
|
||||||
|
imm = data[pc+1]
|
||||||
|
rel = data[pc+2]
|
||||||
|
if rel >= 0x80:
|
||||||
|
rel -= 256
|
||||||
|
target = (pc + 3 + rel) & 0xFFFF
|
||||||
|
out.append(f" {pc:04X}: CJNE A,#{imm:02X}h,{target:04X}h ; bRequest?=0x{imm:02X} skip->{target:04X}h")
|
||||||
|
|
||||||
|
# ==== SECTION 6: boot_8psk ====
|
||||||
|
out.append(disasm_range(data, 0x1D87, min(0x1D87 + 250, 0x2500),
|
||||||
|
"SECTION 6: boot_8psk() @ 0x1D87"))
|
||||||
|
|
||||||
|
# ==== SECTION 7: SFR access map ====
|
||||||
|
out.append("\n" + "=" * 80)
|
||||||
|
out.append(" SECTION 7: All XDATA SFR Accesses (DPTR-tracked)")
|
||||||
|
out.append("=" * 80)
|
||||||
|
|
||||||
|
accesses = track_dptr_accesses(data, 0x0000, 0x2500)
|
||||||
|
for pc, addr, name, direction in accesses:
|
||||||
|
name_str = name if name else f"XDATA_{addr:04X}h"
|
||||||
|
out.append(f" {pc:04X}: {direction:5s} [{addr:04X}h] {name_str}")
|
||||||
|
|
||||||
|
# ==== SECTION 8: Key functions ====
|
||||||
|
out.append(disasm_range(data, 0x0DDD, 0x0DDD + 120,
|
||||||
|
"SECTION 8: boot_init_blocks() @ 0x0DDD"))
|
||||||
|
|
||||||
|
out.append(disasm_range(data, 0x1A81, 0x1A81 + 150,
|
||||||
|
"SECTION 9: i2c_write() @ 0x1A81"))
|
||||||
|
|
||||||
|
out.append(disasm_range(data, 0x1556, 0x1556 + 150,
|
||||||
|
"SECTION 10: i2c_combined_read() @ 0x1556"))
|
||||||
|
|
||||||
|
out.append(disasm_range(data, 0x2000, 0x2000 + 120,
|
||||||
|
"SECTION 11: wait_for_ready() @ 0x2000"))
|
||||||
|
|
||||||
|
out.append(disasm_range(data, 0x099A, 0x099A + 200,
|
||||||
|
"SECTION 12: tune_function() @ 0x099A"))
|
||||||
|
|
||||||
|
# ==== SECTION 13: USB Descriptors ====
|
||||||
|
out.append("\n" + "=" * 80)
|
||||||
|
out.append(" SECTION 13: USB Descriptors @ 0x1200")
|
||||||
|
out.append("=" * 80)
|
||||||
|
|
||||||
|
desc = data[0x1200:0x1212]
|
||||||
|
out.append(f" Device Descriptor (18 bytes):")
|
||||||
|
out.append(f" bLength: {desc[0]} (0x{desc[0]:02X})")
|
||||||
|
out.append(f" bDescriptorType: {desc[1]} (DEVICE)")
|
||||||
|
out.append(f" bcdUSB: {desc[3]:02X}.{desc[2]:02X}")
|
||||||
|
out.append(f" bDeviceClass: 0x{desc[4]:02X} (vendor-specific)")
|
||||||
|
out.append(f" bDeviceSubClass: 0x{desc[5]:02X}")
|
||||||
|
out.append(f" bDeviceProtocol: 0x{desc[6]:02X}")
|
||||||
|
out.append(f" bMaxPacketSize0: {desc[7]} bytes")
|
||||||
|
vid = desc[8] | (desc[9] << 8)
|
||||||
|
pid = desc[10] | (desc[11] << 8)
|
||||||
|
out.append(f" idVendor: 0x{vid:04X} (Cypress)")
|
||||||
|
out.append(f" idProduct: 0x{pid:04X}")
|
||||||
|
bcd = desc[12] | (desc[13] << 8)
|
||||||
|
out.append(f" bcdDevice: {bcd:04X}")
|
||||||
|
out.append(f" iManufacturer: {desc[14]}")
|
||||||
|
out.append(f" iProduct: {desc[15]}")
|
||||||
|
out.append(f" iSerialNumber: {desc[16]}")
|
||||||
|
out.append(f" bNumConfigurations: {desc[17]}")
|
||||||
|
|
||||||
|
# Raw descriptor dump
|
||||||
|
out.append(f"\n Raw descriptor area 0x1200-0x1300:")
|
||||||
|
for off in range(0, 0x100, 16):
|
||||||
|
addr = 0x1200 + off
|
||||||
|
chunk = data[addr:addr+16]
|
||||||
|
hexl = " ".join(f"{b:02X}" for b in chunk)
|
||||||
|
asciil = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
|
||||||
|
out.append(f" {addr:04X}: {hexl} {asciil}")
|
||||||
|
|
||||||
|
# ==== SECTION 14: Hex dump of interesting code regions ====
|
||||||
|
out.append("\n" + "=" * 80)
|
||||||
|
out.append(" SECTION 14: Raw Hex - Code Space Near USB ISR Targets")
|
||||||
|
out.append("=" * 80)
|
||||||
|
|
||||||
|
# Show hex around the USB interrupt vector dispatch
|
||||||
|
# The USB ISR at 0x0043 likely jumps somewhere
|
||||||
|
for start_addr in [0x0043, 0x004B]:
|
||||||
|
out.append(f"\n Hex dump at {start_addr:04X}h:")
|
||||||
|
for off in range(0, 48, 16):
|
||||||
|
addr = start_addr + off
|
||||||
|
chunk = data[addr:addr+16]
|
||||||
|
hexl = " ".join(f"{b:02X}" for b in chunk)
|
||||||
|
out.append(f" {addr:04X}: {hexl}")
|
||||||
|
|
||||||
|
print("\n".join(out))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
660
firmware-dump/disasm8051_v2.py
Normal file
660
firmware-dump/disasm8051_v2.py
Normal file
@ -0,0 +1,660 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""8051 disassembler for Genpix SkyWalker-1 firmware analysis.
|
||||||
|
|
||||||
|
Produces annotated disassembly of the FX2LP (8051-based) firmware,
|
||||||
|
focusing on the Keil C51 startup sequence and USB initialization.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
OPCODES = {}
|
||||||
|
|
||||||
|
def op(code, mnem, length, desc=""):
|
||||||
|
OPCODES[code] = (mnem, length, desc)
|
||||||
|
|
||||||
|
# NOP
|
||||||
|
op(0x00, "NOP", 1)
|
||||||
|
# AJMP/ACALL (page 0-7)
|
||||||
|
for page in range(8):
|
||||||
|
op(0x01 + page*0x20, f"AJMP {{a11:{page}}}", 2)
|
||||||
|
op(0x11 + page*0x20, f"ACALL {{a11:{page}}}", 2)
|
||||||
|
|
||||||
|
op(0x02, "LJMP {a16}", 3)
|
||||||
|
op(0x12, "LCALL {a16}", 3)
|
||||||
|
op(0x03, "RR A", 1)
|
||||||
|
op(0x13, "RRC A", 1)
|
||||||
|
op(0x23, "RL A", 1)
|
||||||
|
op(0x33, "RLC A", 1)
|
||||||
|
|
||||||
|
# INC
|
||||||
|
op(0x04, "INC A", 1)
|
||||||
|
op(0x05, "INC {d}", 2)
|
||||||
|
op(0x06, "INC @R0", 1)
|
||||||
|
op(0x07, "INC @R1", 1)
|
||||||
|
for i in range(8): op(0x08+i, f"INC R{i}", 1)
|
||||||
|
|
||||||
|
# JBC, JB, JNB, JC, JNC, JZ, JNZ
|
||||||
|
op(0x10, "JBC {bit},{r8}", 3)
|
||||||
|
op(0x20, "JB {bit},{r8}", 3)
|
||||||
|
op(0x30, "JNB {bit},{r8}", 3)
|
||||||
|
op(0x40, "JC {r8}", 2)
|
||||||
|
op(0x50, "JNC {r8}", 2)
|
||||||
|
op(0x60, "JZ {r8}", 2)
|
||||||
|
op(0x70, "JNZ {r8}", 2)
|
||||||
|
|
||||||
|
# DEC
|
||||||
|
op(0x14, "DEC A", 1)
|
||||||
|
op(0x15, "DEC {d}", 2)
|
||||||
|
op(0x16, "DEC @R0", 1)
|
||||||
|
op(0x17, "DEC @R1", 1)
|
||||||
|
for i in range(8): op(0x18+i, f"DEC R{i}", 1)
|
||||||
|
|
||||||
|
# ADD
|
||||||
|
op(0x24, "ADD A,#{imm}", 2)
|
||||||
|
op(0x25, "ADD A,{d}", 2)
|
||||||
|
op(0x26, "ADD A,@R0", 1)
|
||||||
|
op(0x27, "ADD A,@R1", 1)
|
||||||
|
for i in range(8): op(0x28+i, f"ADD A,R{i}", 1)
|
||||||
|
|
||||||
|
# ADDC
|
||||||
|
op(0x34, "ADDC A,#{imm}", 2)
|
||||||
|
op(0x35, "ADDC A,{d}", 2)
|
||||||
|
op(0x36, "ADDC A,@R0", 1)
|
||||||
|
op(0x37, "ADDC A,@R1", 1)
|
||||||
|
for i in range(8): op(0x38+i, f"ADDC A,R{i}", 1)
|
||||||
|
|
||||||
|
# ORL
|
||||||
|
op(0x42, "ORL {d},A", 2)
|
||||||
|
op(0x43, "ORL {d},#{imm}", 3)
|
||||||
|
op(0x44, "ORL A,#{imm}", 2)
|
||||||
|
op(0x45, "ORL A,{d}", 2)
|
||||||
|
op(0x46, "ORL A,@R0", 1)
|
||||||
|
op(0x47, "ORL A,@R1", 1)
|
||||||
|
for i in range(8): op(0x48+i, f"ORL A,R{i}", 1)
|
||||||
|
|
||||||
|
# ANL
|
||||||
|
op(0x52, "ANL {d},A", 2)
|
||||||
|
op(0x53, "ANL {d},#{imm}", 3)
|
||||||
|
op(0x54, "ANL A,#{imm}", 2)
|
||||||
|
op(0x55, "ANL A,{d}", 2)
|
||||||
|
op(0x56, "ANL A,@R0", 1)
|
||||||
|
op(0x57, "ANL A,@R1", 1)
|
||||||
|
for i in range(8): op(0x58+i, f"ANL A,R{i}", 1)
|
||||||
|
|
||||||
|
# XRL
|
||||||
|
op(0x62, "XRL {d},A", 2)
|
||||||
|
op(0x63, "XRL {d},#{imm}", 3)
|
||||||
|
op(0x64, "XRL A,#{imm}", 2)
|
||||||
|
op(0x65, "XRL A,{d}", 2)
|
||||||
|
op(0x66, "XRL A,@R0", 1)
|
||||||
|
op(0x67, "XRL A,@R1", 1)
|
||||||
|
for i in range(8): op(0x68+i, f"XRL A,R{i}", 1)
|
||||||
|
|
||||||
|
# ORL C, ANL C
|
||||||
|
op(0x72, "ORL C,{bit}", 2)
|
||||||
|
op(0xA0, "ORL C,/{bit}", 2)
|
||||||
|
op(0x82, "ANL C,{bit}", 2)
|
||||||
|
op(0xB0, "ANL C,/{bit}", 2)
|
||||||
|
|
||||||
|
# MOV bit
|
||||||
|
op(0x92, "MOV {bit},C", 2)
|
||||||
|
op(0xA2, "MOV C,{bit}", 2)
|
||||||
|
|
||||||
|
# MOV direct
|
||||||
|
op(0x75, "MOV {d},#{imm}", 3)
|
||||||
|
op(0x85, "MOV {d2},{d1}", 3)
|
||||||
|
op(0xE5, "MOV A,{d}", 2)
|
||||||
|
op(0xF5, "MOV {d},A", 2)
|
||||||
|
op(0xA5, "DB A5h", 1)
|
||||||
|
|
||||||
|
# MOV A,#imm
|
||||||
|
op(0x74, "MOV A,#{imm}", 2)
|
||||||
|
|
||||||
|
# MOV A,@Ri / MOV @Ri,A
|
||||||
|
op(0xE6, "MOV A,@R0", 1)
|
||||||
|
op(0xE7, "MOV A,@R1", 1)
|
||||||
|
op(0xF6, "MOV @R0,A", 1)
|
||||||
|
op(0xF7, "MOV @R1,A", 1)
|
||||||
|
|
||||||
|
# MOV A,Rn / MOV Rn,A
|
||||||
|
for i in range(8):
|
||||||
|
op(0xE8+i, f"MOV A,R{i}", 1)
|
||||||
|
op(0xF8+i, f"MOV R{i},A", 1)
|
||||||
|
|
||||||
|
# MOV Rn,#imm
|
||||||
|
for i in range(8): op(0x78+i, f"MOV R{i},#{{imm}}", 2)
|
||||||
|
# MOV Rn,direct
|
||||||
|
for i in range(8): op(0xA8+i, f"MOV R{i},{{d}}", 2)
|
||||||
|
# MOV direct,Rn
|
||||||
|
for i in range(8): op(0x88+i, f"MOV {{d}},R{i}", 2)
|
||||||
|
|
||||||
|
op(0x86, "MOV {d},@R0", 2)
|
||||||
|
op(0x87, "MOV {d},@R1", 2)
|
||||||
|
op(0xA6, "MOV @R0,{d}", 2)
|
||||||
|
op(0xA7, "MOV @R1,{d}", 2)
|
||||||
|
op(0x76, "MOV @R0,#{imm}", 2)
|
||||||
|
op(0x77, "MOV @R1,#{imm}", 2)
|
||||||
|
|
||||||
|
# MOV DPTR,#imm16
|
||||||
|
op(0x90, "MOV DPTR,#{imm16}", 3)
|
||||||
|
|
||||||
|
# MOVC
|
||||||
|
op(0x83, "MOVC A,@A+PC", 1)
|
||||||
|
op(0x93, "MOVC A,@A+DPTR", 1)
|
||||||
|
|
||||||
|
# MOVX
|
||||||
|
op(0xE0, "MOVX A,@DPTR", 1)
|
||||||
|
op(0xE2, "MOVX A,@R0", 1)
|
||||||
|
op(0xE3, "MOVX A,@R1", 1)
|
||||||
|
op(0xF0, "MOVX @DPTR,A", 1)
|
||||||
|
op(0xF2, "MOVX @R0,A", 1)
|
||||||
|
op(0xF3, "MOVX @R1,A", 1)
|
||||||
|
|
||||||
|
# PUSH/POP
|
||||||
|
op(0xC0, "PUSH {d}", 2)
|
||||||
|
op(0xD0, "POP {d}", 2)
|
||||||
|
|
||||||
|
# XCH
|
||||||
|
op(0xC5, "XCH A,{d}", 2)
|
||||||
|
op(0xC6, "XCH A,@R0", 1)
|
||||||
|
op(0xC7, "XCH A,@R1", 1)
|
||||||
|
for i in range(8): op(0xC8+i, f"XCH A,R{i}", 1)
|
||||||
|
|
||||||
|
# XCHD
|
||||||
|
op(0xD6, "XCHD A,@R0", 1)
|
||||||
|
op(0xD7, "XCHD A,@R1", 1)
|
||||||
|
|
||||||
|
# DJNZ
|
||||||
|
op(0xD5, "DJNZ {d},{r8}", 3)
|
||||||
|
for i in range(8): op(0xD8+i, f"DJNZ R{i},{{r8}}", 2)
|
||||||
|
|
||||||
|
# CJNE
|
||||||
|
op(0xB4, "CJNE A,#{imm},{r8}", 3)
|
||||||
|
op(0xB5, "CJNE A,{d},{r8}", 3)
|
||||||
|
op(0xB6, "CJNE @R0,#{imm},{r8}", 3)
|
||||||
|
op(0xB7, "CJNE @R1,#{imm},{r8}", 3)
|
||||||
|
for i in range(8): op(0xB8+i, f"CJNE R{i},#{{imm}},{{r8}}", 3)
|
||||||
|
|
||||||
|
# SJMP
|
||||||
|
op(0x80, "SJMP {r8}", 2)
|
||||||
|
# JMP @A+DPTR
|
||||||
|
op(0x73, "JMP @A+DPTR", 1)
|
||||||
|
# RET, RETI
|
||||||
|
op(0x22, "RET", 1)
|
||||||
|
op(0x32, "RETI", 1)
|
||||||
|
|
||||||
|
# CLR, SETB, CPL
|
||||||
|
op(0xC2, "CLR {bit}", 2)
|
||||||
|
op(0xC3, "CLR C", 1)
|
||||||
|
op(0xD2, "SETB {bit}", 2)
|
||||||
|
op(0xD3, "SETB C", 1)
|
||||||
|
op(0xB2, "CPL {bit}", 2)
|
||||||
|
op(0xB3, "CPL C", 1)
|
||||||
|
op(0xE4, "CLR A", 1)
|
||||||
|
op(0xF4, "CPL A", 1)
|
||||||
|
|
||||||
|
# MUL, DIV, DA, SWAP
|
||||||
|
op(0xA4, "MUL AB", 1)
|
||||||
|
op(0x84, "DIV AB", 1)
|
||||||
|
op(0xD4, "DA A", 1)
|
||||||
|
op(0xC4, "SWAP A", 1)
|
||||||
|
|
||||||
|
# SUBB
|
||||||
|
op(0x94, "SUBB A,#{imm}", 2)
|
||||||
|
op(0x95, "SUBB A,{d}", 2)
|
||||||
|
op(0x96, "SUBB A,@R0", 1)
|
||||||
|
op(0x97, "SUBB A,@R1", 1)
|
||||||
|
for i in range(8): op(0x98+i, f"SUBB A,R{i}", 1)
|
||||||
|
|
||||||
|
# INC DPTR
|
||||||
|
op(0xA3, "INC DPTR", 1)
|
||||||
|
|
||||||
|
|
||||||
|
# ============ SFR / XDATA / BIT names ============
|
||||||
|
|
||||||
|
SFR_NAMES = {
|
||||||
|
0x80: "IOA", 0x81: "SP", 0x82: "DPL", 0x83: "DPH",
|
||||||
|
0x84: "DPL1", 0x85: "DPH1", 0x86: "DPS", 0x87: "PCON",
|
||||||
|
0x88: "TCON", 0x89: "TMOD", 0x8A: "TL0", 0x8B: "TL1",
|
||||||
|
0x8C: "TH0", 0x8D: "TH1", 0x8E: "CKCON",
|
||||||
|
0x90: "IOB", 0x91: "EXIF",
|
||||||
|
0x98: "SCON0", 0x99: "SBUF0",
|
||||||
|
0x9A: "AUTOPTRH1", 0x9B: "AUTOPTRL1",
|
||||||
|
0x9C: "AUTOPTRH2", 0x9D: "AUTOPTRL2", 0x9E: "AUTOPTRSETUP",
|
||||||
|
0xA0: "IOC", 0xA8: "IE",
|
||||||
|
0xAA: "EP2468STAT", 0xAB: "EP24FIFOFLGS", 0xAC: "EP68FIFOFLGS",
|
||||||
|
0xB0: "IOD", 0xB1: "IOE",
|
||||||
|
0xB2: "OEA", 0xB3: "OEB", 0xB4: "OEC", 0xB5: "OED", 0xB6: "OEE",
|
||||||
|
0xC0: "SCON1", 0xC1: "SBUF1",
|
||||||
|
0xC8: "T2CON", 0xCA: "RCAP2L", 0xCB: "RCAP2H", 0xCC: "TL2", 0xCD: "TH2",
|
||||||
|
0xD0: "PSW", 0xD8: "EICON",
|
||||||
|
0xE0: "ACC", 0xE8: "EIE", 0xF0: "B", 0xF8: "EIP",
|
||||||
|
}
|
||||||
|
|
||||||
|
XREG_NAMES = {
|
||||||
|
0xE600: "CPUCS", 0xE601: "IFCONFIG", 0xE602: "PINFLAGSAB",
|
||||||
|
0xE603: "PINFLAGSCD", 0xE604: "FIFORESET", 0xE609: "REVCTL",
|
||||||
|
0xE60A: "GPIFTRIG", 0xE60B: "GPIFSGLDATH", 0xE60C: "GPIFSGLDATLX",
|
||||||
|
0xE610: "FLOWSTATE", 0xE611: "FLOWLOGIC",
|
||||||
|
0xE618: "GPIFHOLDAMOUNT",
|
||||||
|
0xE620: "EP2CFG", 0xE621: "EP4CFG", 0xE622: "EP6CFG", 0xE623: "EP8CFG",
|
||||||
|
0xE624: "EP2FIFOCFG", 0xE625: "EP4FIFOCFG",
|
||||||
|
0xE626: "EP6FIFOCFG", 0xE627: "EP8FIFOCFG",
|
||||||
|
0xE628: "EP2AUTOINLENH", 0xE629: "EP2AUTOINLENL",
|
||||||
|
0xE630: "EP2FIFOPFH", 0xE631: "EP2FIFOPFL",
|
||||||
|
0xE640: "EP2ISOINPKTS",
|
||||||
|
0xE648: "INPKTEND", 0xE649: "OUTPKTEND",
|
||||||
|
0xE650: "EP2FIFOIE", 0xE651: "EP2FIFOIRQ",
|
||||||
|
0xE65C: "USBIE", 0xE65D: "USBIRQ",
|
||||||
|
0xE65E: "EPIE", 0xE65F: "EPIRQ",
|
||||||
|
0xE660: "GPIFIE", 0xE661: "GPIFIRQ",
|
||||||
|
0xE662: "USBERRIE", 0xE663: "USBERRIRQ",
|
||||||
|
0xE666: "INT2IVEC", 0xE667: "INT4IVEC", 0xE668: "INTSETUP",
|
||||||
|
0xE670: "PORTACFG", 0xE671: "PORTCCFG", 0xE672: "PORTECFG",
|
||||||
|
0xE678: "I2CS", 0xE679: "I2DAT", 0xE67A: "I2CTL",
|
||||||
|
0xE67B: "XAUTODAT1", 0xE67C: "XAUTODAT2",
|
||||||
|
0xE680: "USBCS", 0xE681: "SUSPEND", 0xE682: "WAKEUPCS",
|
||||||
|
0xE683: "TOGCTL",
|
||||||
|
0xE684: "USBFRAMEH", 0xE685: "USBFRAMEL", 0xE686: "MICROFRAME",
|
||||||
|
0xE687: "FNADDR",
|
||||||
|
0xE68A: "EP0BCH", 0xE68B: "EP0BCL",
|
||||||
|
0xE68C: "EP1OUTBC", 0xE68D: "EP1INBC",
|
||||||
|
0xE68F: "EP1INCS",
|
||||||
|
0xE690: "EP2CS", 0xE691: "EP4CS", 0xE692: "EP6CS", 0xE693: "EP8CS",
|
||||||
|
0xE694: "EP2FIFOFLGS", 0xE695: "EP4FIFOFLGS",
|
||||||
|
0xE696: "EP6FIFOFLGS", 0xE697: "EP8FIFOFLGS",
|
||||||
|
0xE698: "EP2BCH", 0xE699: "EP2BCL",
|
||||||
|
0xE6A0: "EP0CS", 0xE6A1: "EP0STAT", 0xE6A2: "EP0STALLBITS",
|
||||||
|
0xE6A3: "CLRTOGS",
|
||||||
|
0xE6A5: "SETUPDAT[0]", 0xE6A6: "SETUPDAT[1]",
|
||||||
|
0xE6A7: "SETUPDAT[2]", 0xE6A8: "SETUPDAT[3]",
|
||||||
|
0xE6A9: "SETUPDAT[4]", 0xE6AA: "SETUPDAT[5]",
|
||||||
|
0xE6AB: "SETUPDAT[6]", 0xE6AC: "SETUPDAT[7]",
|
||||||
|
0xE6C0: "GPIFWFSELECT", 0xE6C1: "GPIFIDLECS",
|
||||||
|
0xE6C2: "GPIFIDLECTL", 0xE6C3: "GPIFCTLCFG",
|
||||||
|
0xE6C4: "GPIFADRH", 0xE6C5: "GPIFADRL",
|
||||||
|
0xE6CE: "GPIFREADYCFG", 0xE6CF: "GPIFREADYSTAT",
|
||||||
|
0xE6D0: "GPIFABORT",
|
||||||
|
0xE6F8: "SUDPTRH", 0xE6F9: "SUDPTRL", 0xE6FA: "SUDPTRCTL",
|
||||||
|
0xE740: "EP0BUF[0]",
|
||||||
|
0xE780: "EP1OUTBUF[0]",
|
||||||
|
0xE7C0: "EP1INBUF[0]",
|
||||||
|
}
|
||||||
|
|
||||||
|
BIT_NAMES = {
|
||||||
|
0xD0: "PSW.P", 0xD1: "PSW.1", 0xD2: "PSW.OV", 0xD3: "PSW.RS0",
|
||||||
|
0xD4: "PSW.RS1", 0xD5: "PSW.F0", 0xD6: "PSW.AC", 0xD7: "PSW.CY",
|
||||||
|
0xA8: "EX0", 0xA9: "ET0", 0xAA: "EX1", 0xAB: "ET1",
|
||||||
|
0xAC: "ES0", 0xAD: "ET2", 0xAE: "ES1", 0xAF: "EA",
|
||||||
|
0x80: "IOA.0", 0x81: "IOA.1", 0x82: "IOA.2", 0x83: "IOA.3",
|
||||||
|
0x84: "IOA.4", 0x85: "IOA.5", 0x86: "IOA.6", 0x87: "IOA.7",
|
||||||
|
0x90: "IOB.0", 0x91: "IOB.1", 0x92: "IOB.2", 0x93: "IOB.3",
|
||||||
|
0x94: "IOB.4", 0x95: "IOB.5", 0x96: "IOB.6", 0x97: "IOB.7",
|
||||||
|
0x88: "IT0", 0x89: "IE0", 0x8A: "IT1", 0x8B: "IE1",
|
||||||
|
0x8C: "TR0", 0x8D: "TF0", 0x8E: "TR1", 0x8F: "TF1",
|
||||||
|
0x98: "RI_0", 0x99: "TI_0",
|
||||||
|
0xD8: "EICON.0", 0xDB: "INT6", 0xDC: "RESI", 0xDD: "ERESI",
|
||||||
|
}
|
||||||
|
|
||||||
|
KNOWN_LABELS = {
|
||||||
|
0x0000: "reset_vector",
|
||||||
|
0x0003: "int0_vector",
|
||||||
|
0x000B: "timer0_vector",
|
||||||
|
0x0013: "int1_vector",
|
||||||
|
0x001B: "timer1_vector",
|
||||||
|
0x0023: "serial0_vector",
|
||||||
|
0x002B: "timer2_vector",
|
||||||
|
0x0033: "resume_vector",
|
||||||
|
0x003B: "serial1_vector",
|
||||||
|
0x0043: "usb_int2_vector",
|
||||||
|
0x004B: "i2c_int3_vector",
|
||||||
|
0x0053: "gpif_int4_vector",
|
||||||
|
0x005B: "int5_vector",
|
||||||
|
0x0063: "int6_vector",
|
||||||
|
0x099A: "tune_function",
|
||||||
|
0x0DDD: "boot_init_blocks",
|
||||||
|
0x0EE9: "tune_init_blocks",
|
||||||
|
0x1200: "usb_device_desc",
|
||||||
|
0x1556: "i2c_combined_read",
|
||||||
|
0x188D: "keil_startup",
|
||||||
|
0x1A81: "i2c_write",
|
||||||
|
0x1D87: "boot_8psk",
|
||||||
|
0x2000: "wait_for_ready",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def sfr_name(addr):
|
||||||
|
if addr in SFR_NAMES:
|
||||||
|
return SFR_NAMES[addr]
|
||||||
|
if addr >= 0x80:
|
||||||
|
return f"SFR_{addr:02X}h"
|
||||||
|
return f"{addr:02X}h"
|
||||||
|
|
||||||
|
def bit_name(addr):
|
||||||
|
if addr in BIT_NAMES:
|
||||||
|
return BIT_NAMES[addr]
|
||||||
|
if addr >= 0x80:
|
||||||
|
base = addr & 0xF8
|
||||||
|
bit = addr & 0x07
|
||||||
|
if base in SFR_NAMES:
|
||||||
|
return f"{SFR_NAMES[base]}.{bit}"
|
||||||
|
return f"bit_{addr:02X}h"
|
||||||
|
|
||||||
|
def xreg_name(addr):
|
||||||
|
if addr in XREG_NAMES:
|
||||||
|
return XREG_NAMES[addr]
|
||||||
|
if 0xE740 <= addr <= 0xE77F:
|
||||||
|
return f"EP0BUF[{addr-0xE740}]"
|
||||||
|
if 0xE780 <= addr <= 0xE7BF:
|
||||||
|
return f"EP1OUTBUF[{addr-0xE780}]"
|
||||||
|
if 0xE7C0 <= addr <= 0xE7FF:
|
||||||
|
return f"EP1INBUF[{addr-0xE7C0}]"
|
||||||
|
if 0xE600 <= addr <= 0xE6FF:
|
||||||
|
return f"XSFR_{addr:04X}h"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def disasm_one(data, pc):
|
||||||
|
"""Returns (mnem, hex_str, length, comment, branch_target)."""
|
||||||
|
if pc >= len(data):
|
||||||
|
return ("???", "", 1, "", None)
|
||||||
|
|
||||||
|
opc = data[pc]
|
||||||
|
entry = OPCODES.get(opc)
|
||||||
|
|
||||||
|
if entry is None:
|
||||||
|
return (f"DB {opc:02X}h", f"{opc:02X}", 1, "; UNKNOWN", None)
|
||||||
|
|
||||||
|
mnem_fmt, length, desc = entry
|
||||||
|
|
||||||
|
if pc + length > len(data):
|
||||||
|
return (f"DB {opc:02X}h", f"{opc:02X}", 1, "; TRUNCATED", None)
|
||||||
|
|
||||||
|
raw_bytes = data[pc:pc+length]
|
||||||
|
hex_str = " ".join(f"{b:02X}" for b in raw_bytes)
|
||||||
|
comment = ""
|
||||||
|
branch_target = None
|
||||||
|
mnem = mnem_fmt
|
||||||
|
|
||||||
|
# Resolve address fields
|
||||||
|
if "{a16}" in mnem:
|
||||||
|
addr16 = (data[pc+1] << 8) | data[pc+2]
|
||||||
|
label = KNOWN_LABELS.get(addr16)
|
||||||
|
mnem = mnem.replace("{a16}", label if label else f"{addr16:04X}h")
|
||||||
|
branch_target = addr16
|
||||||
|
|
||||||
|
m = re.search(r'\{a11:(\d+)\}', mnem)
|
||||||
|
if m:
|
||||||
|
page = int(m.group(1))
|
||||||
|
addr11 = (page << 8) | data[pc+1] | ((pc + 2) & 0xF800)
|
||||||
|
label = KNOWN_LABELS.get(addr11)
|
||||||
|
mnem = re.sub(r'\{a11:\d+\}', label if label else f"{addr11:04X}h", mnem)
|
||||||
|
branch_target = addr11
|
||||||
|
|
||||||
|
if "{r8}" in mnem:
|
||||||
|
rel_byte = data[pc + length - 1]
|
||||||
|
rel = rel_byte if rel_byte < 0x80 else rel_byte - 256
|
||||||
|
target = (pc + length + rel) & 0xFFFF
|
||||||
|
label = KNOWN_LABELS.get(target)
|
||||||
|
mnem = mnem.replace("{r8}", label if label else f"{target:04X}h")
|
||||||
|
branch_target = target
|
||||||
|
|
||||||
|
# Direct addressing - handle the special MOV d2,d1 case
|
||||||
|
if "{d2}" in mnem and "{d1}" in mnem:
|
||||||
|
src = data[pc+1]
|
||||||
|
dst = data[pc+2]
|
||||||
|
mnem = mnem.replace("{d1}", sfr_name(src) if src >= 0x20 else f"{src:02X}h")
|
||||||
|
mnem = mnem.replace("{d2}", sfr_name(dst) if dst >= 0x20 else f"{dst:02X}h")
|
||||||
|
elif "{d}" in mnem:
|
||||||
|
d = data[pc+1]
|
||||||
|
mnem = mnem.replace("{d}", sfr_name(d) if d >= 0x20 else f"{d:02X}h")
|
||||||
|
|
||||||
|
if "{bit}" in mnem:
|
||||||
|
b = data[pc+1]
|
||||||
|
mnem = mnem.replace("{bit}", bit_name(b))
|
||||||
|
|
||||||
|
if "{imm16}" in mnem:
|
||||||
|
imm16 = (data[pc+1] << 8) | data[pc+2]
|
||||||
|
xname = xreg_name(imm16)
|
||||||
|
if xname:
|
||||||
|
mnem = mnem.replace("{imm16}", f"{imm16:04X}h")
|
||||||
|
comment = f"; = {xname}"
|
||||||
|
elif imm16 in KNOWN_LABELS:
|
||||||
|
mnem = mnem.replace("{imm16}", f"{imm16:04X}h")
|
||||||
|
comment = f"; -> {KNOWN_LABELS[imm16]}"
|
||||||
|
else:
|
||||||
|
mnem = mnem.replace("{imm16}", f"{imm16:04X}h")
|
||||||
|
|
||||||
|
if "{imm}" in mnem:
|
||||||
|
# Determine which byte is the immediate
|
||||||
|
if length == 3 and opc in (0x43, 0x53, 0x63):
|
||||||
|
imm = data[pc+2] # ORL/ANL/XRL d,#imm
|
||||||
|
elif length == 3 and opc == 0x75:
|
||||||
|
imm = data[pc+2] # MOV d,#imm
|
||||||
|
elif length == 3 and opc == 0xB4:
|
||||||
|
imm = data[pc+1] # CJNE A,#imm,r8
|
||||||
|
elif length == 3 and 0xB6 <= opc <= 0xBF:
|
||||||
|
imm = data[pc+1] # CJNE @Ri/#imm or CJNE Rn,#imm
|
||||||
|
elif length == 3 and opc == 0xD5:
|
||||||
|
# DJNZ d,r8 - no imm, but {d} already handled
|
||||||
|
imm = 0 # shouldn't happen
|
||||||
|
else:
|
||||||
|
imm = data[pc + length - 1]
|
||||||
|
mnem = mnem.replace("{imm}", f"{imm:02X}h")
|
||||||
|
|
||||||
|
return (mnem, hex_str, length, comment, branch_target)
|
||||||
|
|
||||||
|
|
||||||
|
def disasm_range(data, start, end, title=""):
|
||||||
|
lines = []
|
||||||
|
if title:
|
||||||
|
lines.append(f"\n{'='*80}")
|
||||||
|
lines.append(f" {title}")
|
||||||
|
lines.append(f"{'='*80}")
|
||||||
|
|
||||||
|
pc = start
|
||||||
|
while pc < end and pc < len(data):
|
||||||
|
if pc in KNOWN_LABELS:
|
||||||
|
lines.append(f"\n {KNOWN_LABELS[pc]}:")
|
||||||
|
|
||||||
|
mnem, hex_str, length, comment, target = disasm_one(data, pc)
|
||||||
|
addr_str = f"{pc:04X}"
|
||||||
|
hex_padded = f"{hex_str:<12s}"
|
||||||
|
line = f" {addr_str}: {hex_padded} {mnem}"
|
||||||
|
if comment:
|
||||||
|
line = f"{line:<58s} {comment}"
|
||||||
|
lines.append(line)
|
||||||
|
pc += length
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def track_dptr_accesses(data, start, end):
|
||||||
|
"""Track all MOVX accesses with DPTR context."""
|
||||||
|
results = []
|
||||||
|
pc = start
|
||||||
|
dptr = None
|
||||||
|
while pc < end and pc < len(data):
|
||||||
|
opc = data[pc]
|
||||||
|
entry = OPCODES.get(opc)
|
||||||
|
if entry is None:
|
||||||
|
pc += 1
|
||||||
|
continue
|
||||||
|
_, length, _ = entry
|
||||||
|
if pc + length > len(data):
|
||||||
|
break
|
||||||
|
if opc == 0x90: # MOV DPTR,#imm16
|
||||||
|
dptr = (data[pc+1] << 8) | data[pc+2]
|
||||||
|
elif opc in (0xE0, 0xF0) and dptr is not None:
|
||||||
|
xname = xreg_name(dptr)
|
||||||
|
direction = "READ" if opc == 0xE0 else "WRITE"
|
||||||
|
results.append((pc, dptr, xname, direction))
|
||||||
|
elif opc == 0xA3: # INC DPTR
|
||||||
|
if dptr is not None:
|
||||||
|
dptr += 1
|
||||||
|
pc += length
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
fw_path = "/home/rpm/claude/ham/satellite/genpix/skywalker-1/firmware-dump/skywalker1_eeprom_full64k.bin"
|
||||||
|
with open(fw_path, "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
out = []
|
||||||
|
out.append("=" * 80)
|
||||||
|
out.append(" GENPIX SKYWALKER-1 FIRMWARE DISASSEMBLY")
|
||||||
|
out.append(" FX2LP (CY7C68013A) 8051 Core - Keil C51 Compiled")
|
||||||
|
out.append(" Firmware size: 9472 bytes (0x0000-0x24FF)")
|
||||||
|
out.append(" Binary: 65536 bytes total (RAM image)")
|
||||||
|
out.append("=" * 80)
|
||||||
|
|
||||||
|
# ==== SECTION 1: Reset + Interrupt Vectors ====
|
||||||
|
out.append(disasm_range(data, 0x0000, 0x006B,
|
||||||
|
"SECTION 1: Reset Vector & Interrupt Vector Table (0x0000-0x006A)"))
|
||||||
|
|
||||||
|
# ==== SECTION 2: Keil C51 Startup ====
|
||||||
|
out.append(disasm_range(data, 0x188D, 0x1950,
|
||||||
|
"SECTION 2: Keil C51 Startup (STARTUP.A51) @ 0x188D"))
|
||||||
|
|
||||||
|
# Find main() by tracing startup
|
||||||
|
pc = 0x188D
|
||||||
|
main_addr = None
|
||||||
|
for _ in range(500):
|
||||||
|
if pc >= len(data) - 2:
|
||||||
|
break
|
||||||
|
opc = data[pc]
|
||||||
|
entry = OPCODES.get(opc)
|
||||||
|
if entry is None:
|
||||||
|
break
|
||||||
|
_, length, _ = entry
|
||||||
|
if opc == 0x02: # LJMP
|
||||||
|
addr = (data[pc+1] << 8) | data[pc+2]
|
||||||
|
# The startup has two LJMPs: one internal loop, one to main
|
||||||
|
# main() is the last LJMP before we hit data/different code
|
||||||
|
if addr < 0x188D: # Jump backward = likely to main()
|
||||||
|
main_addr = addr
|
||||||
|
pc += length
|
||||||
|
|
||||||
|
if main_addr:
|
||||||
|
KNOWN_LABELS[main_addr] = "main"
|
||||||
|
out.append(f"\n >>> Keil startup jumps to main() at {main_addr:04X}h")
|
||||||
|
|
||||||
|
# ==== SECTION 3: main() ====
|
||||||
|
if main_addr:
|
||||||
|
# Disassemble main() - generous range to cover init
|
||||||
|
out.append(disasm_range(data, main_addr, min(main_addr + 400, 0x2500),
|
||||||
|
f"SECTION 3: main() @ {main_addr:04X}h (first ~400 bytes)"))
|
||||||
|
|
||||||
|
# ==== SECTION 4: Vendor command dispatch ====
|
||||||
|
out.append("\n" + "=" * 80)
|
||||||
|
out.append(" SECTION 4: USB Vendor Command Dispatch")
|
||||||
|
out.append("=" * 80)
|
||||||
|
|
||||||
|
# Find all reads of SETUPDAT[1] (bRequest)
|
||||||
|
for pc in range(0x2500):
|
||||||
|
if pc + 2 < len(data) and data[pc] == 0x90 and data[pc+1] == 0xE6 and data[pc+2] == 0xA6:
|
||||||
|
out.append(f"\n SETUPDAT[1] read at {pc:04X}h:")
|
||||||
|
ctx_start = max(0, pc - 16)
|
||||||
|
ctx_end = min(pc + 120, 0x2500)
|
||||||
|
out.append(disasm_range(data, ctx_start, ctx_end,
|
||||||
|
f" Dispatch context @ {pc:04X}h"))
|
||||||
|
|
||||||
|
# ==== SECTION 5: Find all CJNE A,#xx comparisons (command dispatch) ====
|
||||||
|
out.append("\n" + "=" * 80)
|
||||||
|
out.append(" SECTION 5: All CJNE A,#xx Instructions (Command Dispatch Table)")
|
||||||
|
out.append("=" * 80)
|
||||||
|
|
||||||
|
for pc in range(0x2500):
|
||||||
|
if pc + 2 < len(data) and data[pc] == 0xB4:
|
||||||
|
imm = data[pc+1]
|
||||||
|
rel = data[pc+2]
|
||||||
|
if rel >= 0x80:
|
||||||
|
rel -= 256
|
||||||
|
target = (pc + 3 + rel) & 0xFFFF
|
||||||
|
out.append(f" {pc:04X}: CJNE A,#{imm:02X}h,{target:04X}h ; bRequest?=0x{imm:02X} skip->{target:04X}h")
|
||||||
|
|
||||||
|
# ==== SECTION 6: boot_8psk ====
|
||||||
|
out.append(disasm_range(data, 0x1D87, min(0x1D87 + 250, 0x2500),
|
||||||
|
"SECTION 6: boot_8psk() @ 0x1D87"))
|
||||||
|
|
||||||
|
# ==== SECTION 7: SFR access map ====
|
||||||
|
out.append("\n" + "=" * 80)
|
||||||
|
out.append(" SECTION 7: All XDATA SFR Accesses (DPTR-tracked)")
|
||||||
|
out.append("=" * 80)
|
||||||
|
|
||||||
|
accesses = track_dptr_accesses(data, 0x0000, 0x2500)
|
||||||
|
for pc, addr, name, direction in accesses:
|
||||||
|
name_str = name if name else f"XDATA_{addr:04X}h"
|
||||||
|
out.append(f" {pc:04X}: {direction:5s} [{addr:04X}h] {name_str}")
|
||||||
|
|
||||||
|
# ==== SECTION 8: Key functions ====
|
||||||
|
out.append(disasm_range(data, 0x0DDD, 0x0DDD + 120,
|
||||||
|
"SECTION 8: boot_init_blocks() @ 0x0DDD"))
|
||||||
|
|
||||||
|
out.append(disasm_range(data, 0x1A81, 0x1A81 + 150,
|
||||||
|
"SECTION 9: i2c_write() @ 0x1A81"))
|
||||||
|
|
||||||
|
out.append(disasm_range(data, 0x1556, 0x1556 + 150,
|
||||||
|
"SECTION 10: i2c_combined_read() @ 0x1556"))
|
||||||
|
|
||||||
|
out.append(disasm_range(data, 0x2000, 0x2000 + 120,
|
||||||
|
"SECTION 11: wait_for_ready() @ 0x2000"))
|
||||||
|
|
||||||
|
out.append(disasm_range(data, 0x099A, 0x099A + 200,
|
||||||
|
"SECTION 12: tune_function() @ 0x099A"))
|
||||||
|
|
||||||
|
# ==== SECTION 13: USB Descriptors ====
|
||||||
|
out.append("\n" + "=" * 80)
|
||||||
|
out.append(" SECTION 13: USB Descriptors @ 0x1200")
|
||||||
|
out.append("=" * 80)
|
||||||
|
|
||||||
|
desc = data[0x1200:0x1212]
|
||||||
|
out.append(f" Device Descriptor (18 bytes):")
|
||||||
|
out.append(f" bLength: {desc[0]} (0x{desc[0]:02X})")
|
||||||
|
out.append(f" bDescriptorType: {desc[1]} (DEVICE)")
|
||||||
|
out.append(f" bcdUSB: {desc[3]:02X}.{desc[2]:02X}")
|
||||||
|
out.append(f" bDeviceClass: 0x{desc[4]:02X} (vendor-specific)")
|
||||||
|
out.append(f" bDeviceSubClass: 0x{desc[5]:02X}")
|
||||||
|
out.append(f" bDeviceProtocol: 0x{desc[6]:02X}")
|
||||||
|
out.append(f" bMaxPacketSize0: {desc[7]} bytes")
|
||||||
|
vid = desc[8] | (desc[9] << 8)
|
||||||
|
pid = desc[10] | (desc[11] << 8)
|
||||||
|
out.append(f" idVendor: 0x{vid:04X} (Cypress)")
|
||||||
|
out.append(f" idProduct: 0x{pid:04X}")
|
||||||
|
bcd = desc[12] | (desc[13] << 8)
|
||||||
|
out.append(f" bcdDevice: {bcd:04X}")
|
||||||
|
out.append(f" iManufacturer: {desc[14]}")
|
||||||
|
out.append(f" iProduct: {desc[15]}")
|
||||||
|
out.append(f" iSerialNumber: {desc[16]}")
|
||||||
|
out.append(f" bNumConfigurations: {desc[17]}")
|
||||||
|
|
||||||
|
# Raw descriptor dump
|
||||||
|
out.append(f"\n Raw descriptor area 0x1200-0x1300:")
|
||||||
|
for off in range(0, 0x100, 16):
|
||||||
|
addr = 0x1200 + off
|
||||||
|
chunk = data[addr:addr+16]
|
||||||
|
hexl = " ".join(f"{b:02X}" for b in chunk)
|
||||||
|
asciil = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
|
||||||
|
out.append(f" {addr:04X}: {hexl} {asciil}")
|
||||||
|
|
||||||
|
# ==== SECTION 14: Hex dump of interesting code regions ====
|
||||||
|
out.append("\n" + "=" * 80)
|
||||||
|
out.append(" SECTION 14: Raw Hex - Code Space Near USB ISR Targets")
|
||||||
|
out.append("=" * 80)
|
||||||
|
|
||||||
|
# Show hex around the USB interrupt vector dispatch
|
||||||
|
# The USB ISR at 0x0043 likely jumps somewhere
|
||||||
|
for start_addr in [0x0043, 0x004B]:
|
||||||
|
out.append(f"\n Hex dump at {start_addr:04X}h:")
|
||||||
|
for off in range(0, 48, 16):
|
||||||
|
addr = start_addr + off
|
||||||
|
chunk = data[addr:addr+16]
|
||||||
|
hexl = " ".join(f"{b:02X}" for b in chunk)
|
||||||
|
out.append(f" {addr:04X}: {hexl}")
|
||||||
|
|
||||||
|
print("\n".join(out))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
firmware-dump/stock_firmware.bin
Normal file
BIN
firmware-dump/stock_firmware.bin
Normal file
Binary file not shown.
@ -9,4 +9,8 @@ CODE_SIZE=--code-size 0x3c00
|
|||||||
include $(FX2LIBDIR)lib/fx2.mk
|
include $(FX2LIBDIR)lib/fx2.mk
|
||||||
|
|
||||||
load: $(BUILDDIR)/$(BASENAME).bix
|
load: $(BUILDDIR)/$(BASENAME).bix
|
||||||
../tools/fw_load.py $(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
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -122,6 +122,7 @@ export default defineConfig({
|
|||||||
{ label: 'TS Analyzer', slug: 'tools/ts-analyzer' },
|
{ label: 'TS Analyzer', slug: 'tools/ts-analyzer' },
|
||||||
{ label: 'Spectrum Analysis', slug: 'tools/spectrum-analysis' },
|
{ label: 'Spectrum Analysis', slug: 'tools/spectrum-analysis' },
|
||||||
{ label: 'Hydrogen 21 cm', slug: 'tools/h21cm' },
|
{ label: 'Hydrogen 21 cm', slug: 'tools/h21cm' },
|
||||||
|
{ label: 'RF Test Bench', slug: 'tools/rf-testbench' },
|
||||||
{ label: 'Beacon Logger', slug: 'tools/beacon-logger' },
|
{ label: 'Beacon Logger', slug: 'tools/beacon-logger' },
|
||||||
{ label: 'Arc Survey', slug: 'tools/arc-survey' },
|
{ label: 'Arc Survey', slug: 'tools/arc-survey' },
|
||||||
{ label: 'MCP Server', slug: 'tools/mcp-server' },
|
{ label: 'MCP Server', slug: 'tools/mcp-server' },
|
||||||
@ -131,6 +132,7 @@ export default defineConfig({
|
|||||||
{
|
{
|
||||||
label: 'Guides',
|
label: 'Guides',
|
||||||
items: [
|
items: [
|
||||||
|
{ label: 'Applications & Use Cases', slug: 'guides/applications' },
|
||||||
{ label: 'QO-100 DATV Reception', slug: 'guides/qo100-datv' },
|
{ label: 'QO-100 DATV Reception', slug: 'guides/qo100-datv' },
|
||||||
{ label: "Experimenter's Roadmap", slug: 'guides/experimenter-roadmap' },
|
{ label: "Experimenter's Roadmap", slug: 'guides/experimenter-roadmap' },
|
||||||
],
|
],
|
||||||
|
|||||||
322
site/src/content/docs/guides/applications.mdx
Normal file
322
site/src/content/docs/guides/applications.mdx
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
---
|
||||||
|
title: Applications & Use Cases
|
||||||
|
description: What can the SkyWalker-1 actually do? Satellite TV, multi-standard signal analysis, radio astronomy, RF measurement, and more.
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside, Badge, Card, CardGrid, Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The SkyWalker-1 shipped as a DVB-S satellite TV receiver. With [custom firmware](/firmware/custom-v305/) and
|
||||||
|
the reverse-engineered USB/I2C interface, it becomes something more interesting: a programmable RF instrument
|
||||||
|
covering 950-2150 MHz with ten demodulation modes, 256 Ksps to 30 Msps symbol rates, and full Python control.
|
||||||
|
|
||||||
|
Here's what you can actually do with it.
|
||||||
|
|
||||||
|
## Satellite TV Reception
|
||||||
|
|
||||||
|
The obvious one. The SkyWalker-1 receives free-to-air (FTA) DVB-S content — unencrypted satellite television
|
||||||
|
and radio that anyone with a dish can watch.
|
||||||
|
|
||||||
|
### What's Up There
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="Ku-Band">
|
||||||
|
Most FTA content in North America lives on Ku-band satellites. A standard 30-36 inch dish and
|
||||||
|
universal LNB is all you need.
|
||||||
|
|
||||||
|
| Satellite | Position | What's On It |
|
||||||
|
|-----------|----------|-------------|
|
||||||
|
| Galaxy 19 | 97.0°W | The FTA motherlode. ~135+ channels: Chinese, Korean, South Asian, religious, shopping, some English |
|
||||||
|
| Galaxy 16 | 99.0°W | Religious programming, international |
|
||||||
|
| SES-2 | 87.0°W | International, government |
|
||||||
|
| AMC-18 | 105.0°W | Mixed FTA and encrypted |
|
||||||
|
|
||||||
|
Typical tuning parameters: 11836 MHz V-pol, 20770 ksps, DVB-S QPSK FEC 3/4.
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="C-Band">
|
||||||
|
C-band requires a larger dish (6-12 feet) and a C-band LNB, but carries content that never made it
|
||||||
|
to Ku-band — including DigiCipher II muxes that the SkyWalker-1 uniquely supports.
|
||||||
|
|
||||||
|
| Satellite | Position | What's On It |
|
||||||
|
|-----------|----------|-------------|
|
||||||
|
| AMC-18 | 105.0°W | DCII cable distribution, some FTA |
|
||||||
|
| SES-2 | 87.0°W | International, government feeds |
|
||||||
|
| Galaxy 16 | 99.0°W | Mixed distribution |
|
||||||
|
|
||||||
|
The FCC C-band transition compressed services into the upper 3.98-4.2 GHz range. Additional
|
||||||
|
spectrum auctions are proposed for 2027 — C-band FTA is on borrowed time.
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
### FTA Resources
|
||||||
|
|
||||||
|
Current channel listings change frequently. These sites track what's active:
|
||||||
|
|
||||||
|
- [LyngSat](https://www.lyngsat.com/) — comprehensive transponder and channel database
|
||||||
|
- [SatExpat](https://www.satexpat.com/) — FTA channel listings with satellite footprints
|
||||||
|
- [FTAList](https://ftalist.com/) — North American FTA community and channel guide
|
||||||
|
|
||||||
|
<Aside type="caution" title="DVB-S2 is not supported">
|
||||||
|
An increasing percentage of satellite content uses **DVB-S2**, which relies on LDPC forward error correction
|
||||||
|
instead of the Reed-Solomon/Viterbi scheme the BCM4500 implements. The SkyWalker-1 can detect DVB-S2
|
||||||
|
carriers as RF energy (they show up in spectrum sweeps), but it cannot demodulate or decode them.
|
||||||
|
|
||||||
|
If a transponder listing says "DVB-S2" or "8PSK" (in the DVB-S2 sense, not Turbo 8PSK), it won't work.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Multi-Standard Signal Analysis
|
||||||
|
|
||||||
|
This is where the SkyWalker-1 becomes genuinely rare hardware. The BCM4500 demodulates standards that are
|
||||||
|
nearly extinct in available consumer equipment — standards that are still actively broadcasting.
|
||||||
|
|
||||||
|
<CardGrid>
|
||||||
|
<Card title="DigiCipher II" icon="rocket">
|
||||||
|
Cable headend distribution format (Comcast HITS, Motorola). One of very few modern devices with DCII
|
||||||
|
support. "Zero Key" unencrypted services are directly receivable.
|
||||||
|
</Card>
|
||||||
|
<Card title="DSS" icon="star">
|
||||||
|
Digital Satellite Service — legacy DirecTV format with 127-byte transport packets (vs 188-byte DVB).
|
||||||
|
Extraordinarily rare outside DirecTV hardware.
|
||||||
|
</Card>
|
||||||
|
<Card title="Turbo 8PSK" icon="setting">
|
||||||
|
DISH Network transponder format. Encrypted content, but demodulator lock and transport stream capture
|
||||||
|
work — useful for signal analysis and protocol research.
|
||||||
|
</Card>
|
||||||
|
<Card title="Turbo QPSK" icon="open-book">
|
||||||
|
Earlier turbo-coded variant. Better spectral efficiency than standard DVB-S QPSK, still used on
|
||||||
|
some distribution paths.
|
||||||
|
</Card>
|
||||||
|
</CardGrid>
|
||||||
|
|
||||||
|
### Why This Matters
|
||||||
|
|
||||||
|
These standards are still active on-air, but the hardware to receive them is disappearing. Off-the-shelf
|
||||||
|
satellite receivers dropped DCII and DSS support years ago. The SkyWalker-1, through its BCM4500 demodulator,
|
||||||
|
retains these capabilities — making it a **preservation and research tool** for signal formats that will
|
||||||
|
eventually go silent.
|
||||||
|
|
||||||
|
The [TS Analyzer](/tools/ts-analyzer/) can parse transport streams from all supported modulation types,
|
||||||
|
making it possible to compare DVB-S, DCII, and DSS packet structures side by side.
|
||||||
|
|
||||||
|
See [BCM4500 Demodulator](/bcm4500/demodulator/) for register-level details on how each modulation type
|
||||||
|
is configured.
|
||||||
|
|
||||||
|
## Wild Feed & Backhaul Hunting
|
||||||
|
|
||||||
|
Satellite transponders carry more than scheduled programming. Temporary unencrypted uplinks — "wild feeds" —
|
||||||
|
appear and disappear throughout the day:
|
||||||
|
|
||||||
|
- **Live news remotes**: Raw camera feeds from field reporters, unedited and uncensored
|
||||||
|
- **Sports backhauls**: Stadium camera feeds before production mixing
|
||||||
|
- **Network distribution**: Programs fed to affiliates before air time
|
||||||
|
- **Event coverage**: Press conferences, hearings, launches
|
||||||
|
|
||||||
|
The SkyWalker-1's blind scan capability and wide symbol rate range (256 Ksps - 30 Msps) make it well-suited
|
||||||
|
for finding these transient signals. The [Carrier Survey](/tools/survey/) tool automates the sweep-and-lock
|
||||||
|
cycle across a full satellite.
|
||||||
|
|
||||||
|
<Aside type="tip" title="Community resource">
|
||||||
|
[Rick's Satellite Wildfeed Forum](https://www.satelliteguys.us/xen/forums/wild-feeds.42/) on SatelliteGuys
|
||||||
|
is the primary community hub for reporting and tracking wild feeds on North American satellites.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Radio Astronomy
|
||||||
|
|
||||||
|
The 950-2150 MHz IF range — or, without an LNB, the direct input range — overlaps with several
|
||||||
|
astrophysically interesting frequencies. The BCM4500's AGC registers respond to any RF energy at the
|
||||||
|
tuned frequency, regardless of whether it carries a demodulatable signal.
|
||||||
|
|
||||||
|
### Hydrogen 21 cm
|
||||||
|
|
||||||
|
<Badge text="tools/h21cm.py" variant="note" />
|
||||||
|
|
||||||
|
Neutral hydrogen emits at **1420.405 MHz** — directly in the IF range with no LNB. Connect an L-band
|
||||||
|
antenna (patch, helical, or horn) to the F-connector and the SkyWalker-1 becomes a hydrogen line
|
||||||
|
radiometer. The velocity-dispersed emission from the Milky Way's spiral arms is detectable even
|
||||||
|
with the BCM4500's ~346 kHz resolution bandwidth.
|
||||||
|
|
||||||
|
See [Hydrogen 21 cm Radiometer](/tools/h21cm/) for the full tool reference.
|
||||||
|
|
||||||
|
### Ku-Band Solar Observation
|
||||||
|
|
||||||
|
Point a standard satellite dish + LNB at the Sun. At 10-12 GHz, solar thermal emission produces a
|
||||||
|
detectable **6+ dB rise** above the cold-sky background. Solar flares produce wideband bursts that
|
||||||
|
are even more dramatic.
|
||||||
|
|
||||||
|
The SkyWalker-1's advantage over an RTL-SDR here is bandwidth: the 30 Msps sweep capability covers
|
||||||
|
a much wider swath of spectrum (~30 MHz effective) compared to the RTL-SDR's ~2.4 MHz, making it
|
||||||
|
easier to detect and characterize broadband solar events.
|
||||||
|
|
||||||
|
Use the [Spectrum Analysis](/tools/spectrum-analysis/) sweep mode to build solar emission profiles.
|
||||||
|
|
||||||
|
### Moon Thermal Emission
|
||||||
|
|
||||||
|
The Moon is a calibrated thermal source at microwave frequencies. Measuring its emission relative to
|
||||||
|
cold sky provides a reference point for system noise temperature estimation — a standard radio
|
||||||
|
astronomy calibration technique.
|
||||||
|
|
||||||
|
## RF Test & Measurement
|
||||||
|
|
||||||
|
The custom firmware turns the SkyWalker-1 into a basic but useful L-band test instrument.
|
||||||
|
|
||||||
|
### L-Band Spectrum Analyzer
|
||||||
|
|
||||||
|
<Badge text="tools/skywalker.py spectrum" variant="note" />
|
||||||
|
|
||||||
|
Sweep 950-2150 MHz in configurable steps, recording AGC power at each frequency. Not calibrated
|
||||||
|
to absolute dBm, but relative measurements are consistent enough for transponder identification,
|
||||||
|
interference detection, and comparative analysis.
|
||||||
|
|
||||||
|
See [Spectrum Analysis](/tools/spectrum-analysis/) for sweep techniques and interpretation.
|
||||||
|
|
||||||
|
### CW Injection Test Bench
|
||||||
|
|
||||||
|
<Badge text="tools/rf_testbench.py" variant="note" />
|
||||||
|
|
||||||
|
Connect a NanoVNA as a CW source through an [HMC472A digital attenuator](https://hmc472.l.zmesh.systems/)
|
||||||
|
to the SkyWalker-1's F-connector. The `rf_testbench.py` tool automates five test sequences:
|
||||||
|
AGC linearity mapping, IF band flatness, frequency accuracy, minimum detectable signal, and
|
||||||
|
BPSK mode 9 probing. The HMC472A provides 0-31.5 dB of programmable attenuation in 0.5 dB
|
||||||
|
steps via its REST API, giving precision level control without swapping fixed pads.
|
||||||
|
|
||||||
|
See [RF Test Bench](/tools/rf-testbench/) for hardware setup, calibration, and test descriptions.
|
||||||
|
|
||||||
|
### LNB Characterization
|
||||||
|
|
||||||
|
Measure gain flatness across the IF band by sweeping a known satellite's transponder plan and
|
||||||
|
comparing received power levels. Track LO drift over temperature by monitoring a stable carrier's
|
||||||
|
frequency offset over 24 hours with the [Beacon Logger](/tools/beacon-logger/).
|
||||||
|
|
||||||
|
The I2C-exposed tuner and demodulator registers make internal signal chain parameters directly
|
||||||
|
readable — something most consumer receivers hide completely.
|
||||||
|
|
||||||
|
### Transponder Fingerprinting
|
||||||
|
|
||||||
|
Each satellite transponder has unique RF characteristics: center frequency, symbol rate, rolloff,
|
||||||
|
power level, modulation type. The [Carrier Survey](/tools/survey/) tool builds a catalog of these
|
||||||
|
parameters. Over time, this creates a fingerprint database useful for satellite identification
|
||||||
|
and change detection.
|
||||||
|
|
||||||
|
### 5G Interference Monitoring
|
||||||
|
|
||||||
|
The FCC's C-band auction reallocated 3.7-3.98 GHz to 5G operators. Spillover from 5G base stations
|
||||||
|
into the satellite C-band (3.98-4.2 GHz) is an increasing concern for satellite operators and
|
||||||
|
earth station licensees. With a C-band LNB, the SkyWalker-1 can sweep the IF band and detect
|
||||||
|
interference signatures.
|
||||||
|
|
||||||
|
## Propagation Science & Weather
|
||||||
|
|
||||||
|
Long-duration signal monitoring produces datasets that map directly to atmospheric physics.
|
||||||
|
|
||||||
|
### Rain Fade Analysis
|
||||||
|
|
||||||
|
<Badge text="tools/beacon_logger.py" variant="note" />
|
||||||
|
|
||||||
|
Lock onto a stable Ku-band transponder and log SNR at 1 Hz for days or weeks. Ku-band signals
|
||||||
|
attenuate predictably in rain — the ITU-R P.618 model describes the relationship between rainfall
|
||||||
|
rate and attenuation at specific frequencies. Real measurement data validates (or challenges)
|
||||||
|
these models for your specific location and dish geometry.
|
||||||
|
|
||||||
|
### Diurnal Thermal Effects
|
||||||
|
|
||||||
|
LNB gain varies with temperature. A 24-hour beacon log correlated with ambient temperature data
|
||||||
|
reveals the thermal gain coefficient of your specific LNB — useful for separating real propagation
|
||||||
|
events from equipment drift.
|
||||||
|
|
||||||
|
### Link Budget Validation
|
||||||
|
|
||||||
|
Compare long-term average received signal levels against calculated link budgets (EIRP, free space
|
||||||
|
loss, atmospheric absorption, antenna gain, system noise temperature). The gap between prediction
|
||||||
|
and measurement is where engineering meets reality.
|
||||||
|
|
||||||
|
See [Beacon Logger](/tools/beacon-logger/) for unattended multi-day logging with auto-relock.
|
||||||
|
|
||||||
|
## Education & Research
|
||||||
|
|
||||||
|
The SkyWalker-1 exposes the complete satellite signal chain from RF input to MPEG-2 transport stream
|
||||||
|
output, with every intermediate stage accessible over I2C.
|
||||||
|
|
||||||
|
### University Lab Platform
|
||||||
|
|
||||||
|
A single SkyWalker-1 + dish + LNB covers a semester of satellite communications topics with
|
||||||
|
live signals:
|
||||||
|
|
||||||
|
| Topic | What's Observable |
|
||||||
|
|-------|------------------|
|
||||||
|
| QPSK/8PSK demodulation | Lock status, constellation quality via SNR |
|
||||||
|
| Forward error correction | Viterbi, Reed-Solomon, Turbo code — switchable by modulation type |
|
||||||
|
| Link budgets | Real measurements vs. theoretical calculations |
|
||||||
|
| MPEG-2 transport streams | Live PSI/SI table parsing, PID analysis |
|
||||||
|
| Spectrum analysis | Transponder identification from raw power sweeps |
|
||||||
|
| Antenna pointing | Signal strength vs. azimuth/elevation in real time |
|
||||||
|
|
||||||
|
### Transport Stream Protocol Research
|
||||||
|
|
||||||
|
The SkyWalker-1's multi-standard support makes it uniquely suited for comparative protocol analysis:
|
||||||
|
|
||||||
|
- **DVB-S**: 188-byte MPEG-2 TS packets, standard PID structure
|
||||||
|
- **DigiCipher II**: Motorola proprietary transport, conditional access
|
||||||
|
- **DSS**: 127-byte packets — shorter than DVB, different header format
|
||||||
|
|
||||||
|
Tools like [TSDuck](https://tsduck.io/) and dvbsnoop can parse captured streams. The [TS Analyzer](/tools/ts-analyzer/)
|
||||||
|
handles the initial capture and PSI extraction.
|
||||||
|
|
||||||
|
### Accessible Signal Chain
|
||||||
|
|
||||||
|
The I2C bus provides direct read access to tuner, demodulator, and FEC status registers. Students can
|
||||||
|
observe the AGC settling, watch the demodulator acquire lock, and read error correction statistics —
|
||||||
|
the internal workings of the signal chain, visible in real time. See [I2C Bus Architecture](/i2c/bus-architecture/)
|
||||||
|
and [Signal Monitoring](/bcm4500/signal-monitoring/) for register details.
|
||||||
|
|
||||||
|
## What's NOT Compatible
|
||||||
|
|
||||||
|
Setting honest expectations is more valuable than overselling.
|
||||||
|
|
||||||
|
<Aside type="danger" title="Hardware limitations">
|
||||||
|
The following are common requests that the SkyWalker-1 **cannot** fulfill. Understanding these
|
||||||
|
boundaries prevents wasted time and money on incompatible setups.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
| Signal / Application | Why Not |
|
||||||
|
|---------------------|---------|
|
||||||
|
| **DVB-S2** | Incompatible FEC — uses LDPC instead of Reed-Solomon/Viterbi. This is a growing percentage of satellite content. |
|
||||||
|
| **GOES weather satellite imagery** | LRIT uses BPSK/CCSDS (not DVB-S), GRB uses DVB-S2. Cannot decode imagery. However, the BCM4500's BPSK mode 9 uses the same inner FEC (Viterbi rate 1/2 K=7) as LRIT — the signal chain gets four stages deep before breaking at RS decoder block size and CCSDS framing. The LRIT carrier at 1694.1 MHz is within the [direct input range](/tools/h21cm/#antenna-setup) and can be used for antenna alignment and propagation monitoring. See [RF Test Bench](/tools/rf-testbench/) for BPSK mode 9 probing. |
|
||||||
|
| **QO-100 from North America** | Es'hail-2 is at 25.9°E — visible from Europe, Africa, and the Middle East, but not North America. See [QO-100 DATV Reception](/guides/qo100-datv/) for coverage details. |
|
||||||
|
| **Military/government feeds** | Encrypted and increasingly DVB-S2 or proprietary modulation. |
|
||||||
|
| **ATSC / DVB-T terrestrial** | Completely different modulation family (OFDM), different frequency band. |
|
||||||
|
| **Analog satellite TV** | The BCM4500 is a digital demodulator. Analog satellite is also effectively extinct. |
|
||||||
|
|
||||||
|
## Modulation Support Reference
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="By Standard">
|
||||||
|
| Modulation | Standard | Typical Use | FTA Content? |
|
||||||
|
|-----------|----------|-------------|-------------|
|
||||||
|
| DVB-S QPSK | DVB-S EN 300 421 | Free-to-air satellite TV worldwide | Yes — most FTA content |
|
||||||
|
| Turbo QPSK | Proprietary (Comstream) | Distribution, some DISH | Rare |
|
||||||
|
| Turbo 8PSK | Proprietary | DISH Network | No — encrypted |
|
||||||
|
| DCII Combo | Motorola DigiCipher II | Cable headend distribution | Some ("Zero Key") |
|
||||||
|
| DCII Split I | Motorola DigiCipher II | Cable headend distribution | Some |
|
||||||
|
| DCII Split Q | Motorola DigiCipher II | Cable headend distribution | Some |
|
||||||
|
| DCII Offset QPSK | Motorola DigiCipher II | Cable headend distribution | Some |
|
||||||
|
| DSS QPSK | Hughes DSS | Legacy DirecTV | No — service winding down |
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="By Receiver Use">
|
||||||
|
| What You Want to Do | Modulation to Select | Symbol Rate Range |
|
||||||
|
|--------------------|--------------------|------------------|
|
||||||
|
| Watch FTA satellite TV | DVB-S QPSK | 2-30 Msps |
|
||||||
|
| Analyze DISH Network signals | Turbo 8PSK | 20-30 Msps |
|
||||||
|
| Receive DCII cable distribution | DCII Combo/Split/Offset | 2-30 Msps |
|
||||||
|
| Study DSS transport format | DSS QPSK | 20 Msps typical |
|
||||||
|
| Hydrogen 21 cm (no LNB) | N/A — AGC power only | Any (for carrier lock attempt) |
|
||||||
|
| Spectrum sweep / signal detection | N/A — AGC power only | Set during tune, not critical |
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [RF Specifications](/hardware/rf-specifications/) — frequency range, symbol rate limits, LNB power
|
||||||
|
- [BCM4500 Demodulator](/bcm4500/demodulator/) — register-level modulation configuration
|
||||||
|
- [Spectrum Analysis](/tools/spectrum-analysis/) — sweep techniques and transponder scanning
|
||||||
|
- [RF Test Bench](/tools/rf-testbench/) — CW injection testing with NanoVNA + HMC472A
|
||||||
|
- [Experimenter's Roadmap](/guides/experimenter-roadmap/) — future experiment tiers and creative applications
|
||||||
|
- [MCP Server](/tools/mcp-server/) — programmatic access to all hardware functions
|
||||||
@ -3,13 +3,63 @@ title: Hydrogen 21 cm Radiometer
|
|||||||
description: Detect neutral hydrogen emission at 1420.405 MHz using the SkyWalker-1 as an L-band radiometer.
|
description: Detect neutral hydrogen emission at 1420.405 MHz using the SkyWalker-1 as an L-band radiometer.
|
||||||
---
|
---
|
||||||
|
|
||||||
import { Aside, Steps, Tabs, TabItem } from '@astrojs/starlight/components';
|
import { Aside, Card, CardGrid, Steps, Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
The `h21cm.py` tool turns the SkyWalker-1 into a hydrogen line radiometer. Neutral hydrogen
|
The `h21cm.py` tool turns the SkyWalker-1 into a hydrogen line radiometer. Neutral hydrogen
|
||||||
atoms emit radiation at **1420.405 MHz** when the electron's spin flips relative to the proton —
|
atoms emit radiation at **1420.405 MHz** when the electron's spin flips relative to the proton —
|
||||||
the most fundamental spectral line in radio astronomy, and it falls directly in the IF range.
|
the most fundamental spectral line in radio astronomy, and it falls directly in the IF range.
|
||||||
|
|
||||||
No LNB is needed. Connect an L-band antenna directly to the F-connector.
|
## Antenna Setup
|
||||||
|
|
||||||
|
The SkyWalker-1 normally receives satellite TV through an LNB (Low Noise Block downconverter) mounted
|
||||||
|
at the focal point of a dish. For hydrogen line work, the LNB must be **removed or bypassed entirely** —
|
||||||
|
it would block the signal. An LNB's waveguide feed is dimensioned for Ku-band wavelengths (~2.5 cm)
|
||||||
|
and its internal filters reject everything outside the 10.7-12.75 GHz range. At 1420 MHz (wavelength
|
||||||
|
~21 cm), nothing gets through.
|
||||||
|
|
||||||
|
Instead, connect an L-band antenna directly to the SkyWalker-1's F-connector with coaxial cable.
|
||||||
|
The tool disables LNB power automatically, so there's no voltage on the cable.
|
||||||
|
|
||||||
|
### Antenna Options
|
||||||
|
|
||||||
|
<CardGrid>
|
||||||
|
<Card title="Horn Antenna" icon="rocket">
|
||||||
|
The classic radio astronomy choice. A tin-can "cantenna" or sheet-metal pyramidal horn provides
|
||||||
|
10-15 dBi gain with predictable, calculable performance. Easy to build from hardware store materials.
|
||||||
|
A circular waveguide horn from a ~15 cm diameter can works well at 1420 MHz.
|
||||||
|
</Card>
|
||||||
|
<Card title="Dish + L-Band Feed" icon="star">
|
||||||
|
Reuse your satellite dish — replace the LNB with a 1420 MHz feed (dipole + reflector, or a small
|
||||||
|
horn at the focal point). The dish surface accuracy matters less at 21 cm wavelength than at Ku-band,
|
||||||
|
so even mesh dishes work fine. This gives the highest gain of any option here.
|
||||||
|
</Card>
|
||||||
|
<Card title="Helical Antenna" icon="setting">
|
||||||
|
A helical antenna is circularly polarized and offers good gain in a compact package. Hydrogen emission
|
||||||
|
is unpolarized, so a circularly-polarized antenna captures half the power (~3 dB penalty vs linear),
|
||||||
|
but helix construction is forgiving and well-documented for L-band.
|
||||||
|
</Card>
|
||||||
|
<Card title="Patch Antenna" icon="open-book">
|
||||||
|
Commercial L-band patch antennas (GPS antennas at 1575 MHz are close) are compact and cheap. Lower
|
||||||
|
gain than the other options (~5-7 dBi), and narrow bandwidth may not cover the full hydrogen emission
|
||||||
|
profile. Fine for a first detection attempt.
|
||||||
|
</Card>
|
||||||
|
</CardGrid>
|
||||||
|
|
||||||
|
<Aside type="note" title="Impedance mismatch">
|
||||||
|
The SkyWalker-1's F-connector is 75Ω. Most L-band antennas and amateur radio feedlines are 50Ω.
|
||||||
|
The resulting 1.5:1 VSWR costs about **0.2 dB** of mismatch loss — negligible for AGC power detection.
|
||||||
|
No matching network is needed. Use an SMA-to-F adapter if your antenna has an SMA connector.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### Cable and Connectors
|
||||||
|
|
||||||
|
Run 75Ω coax (RG-6 is standard satellite TV cable) from the antenna to the SkyWalker-1. At 1420 MHz,
|
||||||
|
cable loss matters more than impedance mismatch — keep runs under 10 meters if possible. RG-6 loses
|
||||||
|
roughly **0.2 dB per meter** at 1.4 GHz (about 6 dB per 100 feet), so a 10m run costs ~2 dB.
|
||||||
|
Shorter is better.
|
||||||
|
|
||||||
|
If your antenna uses 50Ω connectors (SMA, N-type), a simple adapter to F-type is fine. The 0.2 dB
|
||||||
|
impedance mismatch is far less than a meter of extra cable.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
|||||||
260
site/src/content/docs/tools/rf-testbench.mdx
Normal file
260
site/src/content/docs/tools/rf-testbench.mdx
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
---
|
||||||
|
title: RF Test Bench
|
||||||
|
description: Automated CW injection testing with NanoVNA, HMC472A digital attenuator, and SkyWalker-1 receiver.
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside, Badge, Card, CardGrid, Steps, Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
<Badge text="tools/rf_testbench.py" variant="note" />
|
||||||
|
|
||||||
|
The `rf_testbench.py` tool turns a NanoVNA, an HMC472A digital attenuator, and the SkyWalker-1 into
|
||||||
|
an automated RF test bench. It injects CW signals at known frequencies and power levels, then
|
||||||
|
measures the receiver's response — characterizing AGC linearity, IF band flatness, frequency
|
||||||
|
accuracy, sensitivity, and BPSK mode 9 behavior.
|
||||||
|
|
||||||
|
## Hardware Setup
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
1. **NanoVNA CH0 output** (SMA) connects to a **DC blocker** (SMA inline, required)
|
||||||
|
2. **DC blocker output** connects to the **HMC472A RF IN** (SMA)
|
||||||
|
3. **HMC472A RF OUT** (SMA) connects via **SMA-to-F adapter** to the **SkyWalker-1 F-connector**
|
||||||
|
4. **HMC472A ESP32-S2 controller** connected to your network (WiFi) — reachable at `http://attenuator.local`
|
||||||
|
5. **NanoVNA** connected via USB (for mcnanovna automation) or operated via touchscreen (manual mode)
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
```
|
||||||
|
NanoVNA CH0 ──→ DC Blocker ──→ HMC472A (0-31.5 dB) ──→ SMA-to-F ──→ SkyWalker-1
|
||||||
|
(SMA) (SMA) REST API control adapter (F-type)
|
||||||
|
http://attenuator.local
|
||||||
|
```
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
| Component | Purpose | Notes |
|
||||||
|
|-----------|---------|-------|
|
||||||
|
| NanoVNA-H (9 kHz-1.5 GHz) | CW signal source | Output ~-15 dBm at max power. Overlaps SkyWalker-1 IF band at 950-1500 MHz |
|
||||||
|
| DC Blocker (SMA inline) | Protect NanoVNA from LNB voltage | Required — even though the tool disables LNB power, this prevents accidental damage |
|
||||||
|
| HMC472A attenuator module | Precision level control | 0-31.5 dB in 0.5 dB steps, controlled via ESP32-S2 REST API |
|
||||||
|
| SMA-to-F adapter | Connector transition | 50-to-75 ohm mismatch is ~0.2 dB — negligible |
|
||||||
|
|
||||||
|
<Aside type="danger" title="DC blocker is required">
|
||||||
|
The SkyWalker-1 can supply 13-18V DC through the F-connector for LNB power. Although `rf_testbench.py`
|
||||||
|
disables LNB power on startup, a bug, power glitch, or running a different tool without disconnecting
|
||||||
|
could send DC voltage backward through the signal path. The DC blocker prevents this from reaching
|
||||||
|
the HMC472A and NanoVNA.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### HMC472A Attenuator
|
||||||
|
|
||||||
|
The [HMC472A digital attenuator](https://hmc472.l.zmesh.systems/) provides programmable signal level
|
||||||
|
control via its ESP32-S2 REST API:
|
||||||
|
|
||||||
|
- **Range**: 0 to 31.5 dB in 0.5 dB steps (64 discrete settings)
|
||||||
|
- **Bandwidth**: DC to 3.8 GHz (covers the full SkyWalker-1 IF range)
|
||||||
|
- **Insertion loss**: 1.4-1.9 dB typical
|
||||||
|
- **Control**: HTTP REST — `POST /set {"attenuation_db": 10.5}`
|
||||||
|
- **Switching speed**: 60 ns (faster than any measurement cycle)
|
||||||
|
|
||||||
|
The tool communicates with the attenuator at `http://attenuator.local` by default. Override with
|
||||||
|
`--attenuator http://10.0.0.50` if your device has a different address.
|
||||||
|
|
||||||
|
### NanoVNA Frequency Overlap
|
||||||
|
|
||||||
|
The NanoVNA-H (HW3.7) covers 9 kHz to 1.5 GHz. The SkyWalker-1's IF range is 950-2150 MHz.
|
||||||
|
The **overlapping usable range is 950-1500 MHz** — the lower portion of the IF band. This is
|
||||||
|
sufficient for characterizing the tuner and AGC, and includes the 1420 MHz hydrogen line region.
|
||||||
|
|
||||||
|
For testing above 1500 MHz, a different signal source (bladeRF, signal generator) would be needed.
|
||||||
|
|
||||||
|
## Calibration
|
||||||
|
|
||||||
|
Before running quantitative tests, characterize the signal path loss:
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
1. Disconnect the SkyWalker-1 end of the cable
|
||||||
|
2. Connect: **NanoVNA CH0** → DC blocker → HMC472A (set to 0 dB) → cable → **NanoVNA CH1**
|
||||||
|
3. Run an S21 sweep from 950 to 1500 MHz using mcnanovna or the NanoVNA touchscreen
|
||||||
|
4. Export as CSV with columns `freq_mhz` and `s21_db` (or `frequency_hz` and `loss_db`)
|
||||||
|
5. Pass to `rf_testbench.py` with `--cal path_loss.csv`
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
The tool interpolates the measured path loss at each test frequency and subtracts it from AGC
|
||||||
|
readings. Without a calibration file, raw AGC values are still reported — useful for relative
|
||||||
|
measurements but not calibrated to absolute power.
|
||||||
|
|
||||||
|
<Aside type="tip" title="HMC472A insertion loss">
|
||||||
|
The HMC472A adds 1.4-1.9 dB of insertion loss even at 0 dB attenuation setting. The calibration
|
||||||
|
sweep captures this automatically since the signal passes through the attenuator during the S21
|
||||||
|
measurement.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **SkyWalker-1** with [custom firmware v3.02+](/firmware/custom-v302/) (for `tune_monitor` command)
|
||||||
|
- **HMC472A** attenuator with ESP32-S2 controller on the network
|
||||||
|
- **NanoVNA-H** (manual mode works with any VNA; auto mode requires [mcnanovna](https://git.supported.systems/rf/mcnanovna))
|
||||||
|
- **Python 3.10+** with `pyusb` installed
|
||||||
|
- **DC blocker** (SMA inline)
|
||||||
|
- **SMA-to-F adapter**
|
||||||
|
|
||||||
|
## Test Descriptions
|
||||||
|
|
||||||
|
### AGC Power Linearity
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/rf_testbench.py agc-linearity --freq 1200
|
||||||
|
```
|
||||||
|
|
||||||
|
Injects CW at a fixed frequency while sweeping the HMC472A from 0 to 31.5 dB in 0.5 dB steps.
|
||||||
|
At each attenuation level, the SkyWalker-1 reports AGC1, AGC2, and derived power. This maps the
|
||||||
|
**AGC transfer function** — how the receiver's automatic gain control responds to known changes
|
||||||
|
in input power.
|
||||||
|
|
||||||
|
The output shows whether the AGC is linear, where it saturates, and its effective dynamic range.
|
||||||
|
With 64 measurement points across 31.5 dB, the resolution is high enough to reveal nonlinearities
|
||||||
|
in the BCM3440 tuner's gain control loop.
|
||||||
|
|
||||||
|
### IF Band Flatness
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/rf_testbench.py band-flatness --start 950 --stop 1500 --step 10
|
||||||
|
```
|
||||||
|
|
||||||
|
Sweeps the NanoVNA CW frequency across the IF band while keeping the HMC472A at a fixed
|
||||||
|
attenuation (10 dB default). At each frequency, the SkyWalker-1 tunes and reads AGC power.
|
||||||
|
|
||||||
|
The result reveals:
|
||||||
|
- **Tuner gain slope**: the BCM3440 may have more gain at some frequencies than others
|
||||||
|
- **Passband ripple**: resonances or nulls in the IF filter chain
|
||||||
|
- **Cable/path frequency response**: if a calibration file is loaded, this is subtracted out
|
||||||
|
|
||||||
|
### Frequency Accuracy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/rf_testbench.py freq-accuracy --freqs 1000,1100,1200,1300,1400
|
||||||
|
```
|
||||||
|
|
||||||
|
At each test frequency, the NanoVNA injects CW while the SkyWalker-1 runs a narrow spectrum
|
||||||
|
sweep (+/- 5 MHz) around the expected frequency. The detected power peak is compared against the
|
||||||
|
injected frequency.
|
||||||
|
|
||||||
|
This characterizes the **BCM3440 tuner's frequency accuracy** — how much the actual tuned
|
||||||
|
frequency differs from the commanded frequency. The error may be systematic (constant offset)
|
||||||
|
or frequency-dependent.
|
||||||
|
|
||||||
|
### Minimum Detectable Signal
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/rf_testbench.py mds --freq 1200
|
||||||
|
```
|
||||||
|
|
||||||
|
First measures the noise floor with maximum attenuation (31.5 dB). Then injects CW and steps
|
||||||
|
the HMC472A from 0 dB upward in 1 dB increments until the signal drops below 3-sigma above
|
||||||
|
the noise floor.
|
||||||
|
|
||||||
|
The attenuation level where the signal disappears, combined with the NanoVNA output power
|
||||||
|
(~-15 dBm), gives an approximate **minimum detectable signal level** in dBm.
|
||||||
|
|
||||||
|
### BPSK Mode 9 CW Probe
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/rf_testbench.py bpsk-probe --freq 1200
|
||||||
|
```
|
||||||
|
|
||||||
|
An exploratory test that tunes the SkyWalker-1 in **BPSK mode (index 9)** — the same Viterbi
|
||||||
|
rate 1/2 K=7 inner FEC used by GOES LRIT. A CW carrier has no modulation, so the demodulator
|
||||||
|
shouldn't acquire lock, but the AGC and carrier recovery behavior is informative.
|
||||||
|
|
||||||
|
Tests several symbol rates (293,883 sps matching LRIT, plus 500K, 1M, and 5M) and compares
|
||||||
|
against QPSK mode 0 at the same frequency. This establishes a baseline for what mode 9 reports
|
||||||
|
with an unmodulated carrier — useful context for future modulated-signal experiments with a
|
||||||
|
bladeRF.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
| Flag | Default | Description |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| `--attenuator` | `http://attenuator.local` | HMC472A REST API base URL |
|
||||||
|
| `--nanovna` | `auto` | NanoVNA control: `auto` (mcnanovna) or `manual` (prompted) |
|
||||||
|
| `--cal` | — | Path loss calibration CSV file |
|
||||||
|
| `--settle` | 200 | Settle time in ms after changing attenuation |
|
||||||
|
| `--output` / `-o` | — | CSV output file |
|
||||||
|
| `--verbose` / `-v` | — | Show raw USB traffic |
|
||||||
|
|
||||||
|
### Per-Test Options
|
||||||
|
|
||||||
|
| Test | Flag | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `agc-linearity` | `--freq` | 1200 | Test frequency in MHz |
|
||||||
|
| `band-flatness` | `--start` | 950 | Start frequency in MHz |
|
||||||
|
| `band-flatness` | `--stop` | 1500 | Stop frequency in MHz |
|
||||||
|
| `band-flatness` | `--step` | 10 | Frequency step in MHz |
|
||||||
|
| `freq-accuracy` | `--freqs` | 1000,1100,1200,1300,1400 | Comma-separated test frequencies |
|
||||||
|
| `mds` | `--freq` | 1200 | Test frequency in MHz |
|
||||||
|
| `bpsk-probe` | `--freq` | 1200 | Test frequency in MHz |
|
||||||
|
|
||||||
|
## CSV Output Format
|
||||||
|
|
||||||
|
All tests write the same CSV format when `--output` is specified:
|
||||||
|
|
||||||
|
| Column | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `timestamp` | ISO 8601 UTC timestamp |
|
||||||
|
| `test_name` | Test identifier (agc_linearity, band_flatness, freq_accuracy, mds, bpsk_probe) |
|
||||||
|
| `freq_mhz` | Frequency in MHz |
|
||||||
|
| `atten_db` | HMC472A attenuation setting in dB |
|
||||||
|
| `agc1` | BCM3440 AGC1 register value |
|
||||||
|
| `agc2` | BCM3440 AGC2 register value |
|
||||||
|
| `power_db` | Derived power estimate in dB (relative) |
|
||||||
|
| `snr_raw` | Raw SNR register value |
|
||||||
|
| `snr_db` | SNR in dB |
|
||||||
|
| `locked` | Demodulator lock status |
|
||||||
|
| `lock_raw` | Raw lock status byte |
|
||||||
|
| `status` | Status byte |
|
||||||
|
| `notes` | Test-specific metadata |
|
||||||
|
|
||||||
|
## Interpreting Results
|
||||||
|
|
||||||
|
### AGC Linearity Curves
|
||||||
|
|
||||||
|
A well-behaved AGC should show a roughly linear relationship between attenuation (dB) and AGC
|
||||||
|
register value. Look for:
|
||||||
|
|
||||||
|
- **Linear region**: Where AGC tracks input power changes 1:1 in dB — this is the useful
|
||||||
|
measurement range
|
||||||
|
- **Saturation**: Where adding more signal doesn't change AGC — the tuner's front end is
|
||||||
|
compressing
|
||||||
|
- **Noise floor**: Where reducing signal doesn't change AGC — the receiver's internal noise
|
||||||
|
dominates
|
||||||
|
|
||||||
|
### Band Flatness
|
||||||
|
|
||||||
|
Ideal response is flat across the band. In practice:
|
||||||
|
- **1-3 dB variation** across 950-1500 MHz is typical for a consumer-grade tuner
|
||||||
|
- **Sharp dips** may indicate cable resonances or connector issues
|
||||||
|
- **Systematic slope** (gain increasing or decreasing with frequency) is common and can be
|
||||||
|
corrected in post-processing
|
||||||
|
|
||||||
|
### Frequency Error
|
||||||
|
|
||||||
|
Consumer satellite tuners typically have **50-200 kHz frequency accuracy**. A consistent offset
|
||||||
|
suggests LO error in the BCM3440. Frequency-dependent error suggests tuning nonlinearity.
|
||||||
|
|
||||||
|
## Mock Mode
|
||||||
|
|
||||||
|
Run with `SKYWALKER_MOCK=1` for testing without hardware:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SKYWALKER_MOCK=1 python tools/rf_testbench.py agc-linearity --freq 1200 --nanovna manual
|
||||||
|
```
|
||||||
|
|
||||||
|
Mock mode uses built-in simulated responses for the SkyWalker-1 and HMC472A. The NanoVNA prompts
|
||||||
|
are skipped. Useful for verifying command structure and CSV output format.
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Spectrum Analysis](/tools/spectrum-analysis/) — frequency sweep techniques
|
||||||
|
- [Hydrogen 21 cm](/tools/h21cm/) — direct L-band input mode (same RF path concept)
|
||||||
|
- [Signal Monitoring](/bcm4500/signal-monitoring/) — AGC and SNR register details
|
||||||
|
- [HMC472A Documentation](https://hmc472.l.zmesh.systems/) — attenuator module reference
|
||||||
|
- [Applications & Use Cases](/guides/applications/) — RF test and measurement context
|
||||||
95
tools/a8_autoclear_test.py
Normal file
95
tools/a8_autoclear_test.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Test whether BCM4500 register A8 auto-clears after write commands.
|
||||||
|
|
||||||
|
If A8 auto-clears to 0x00 (or 0x02) after writing 0x03, then
|
||||||
|
bcm_poll_ready() would always return TRUE, masking the fact that
|
||||||
|
the DSP isn't processing commands.
|
||||||
|
|
||||||
|
Uses enhanced 0xB6 diagnostic:
|
||||||
|
wValue = page, wIndex = A8 command (0 = default READ)
|
||||||
|
Returns 8 bytes:
|
||||||
|
[0] write_A6_ok
|
||||||
|
[1] A6 readback
|
||||||
|
[2] write_A8_ok
|
||||||
|
[3] A8 IMMEDIATE readback (no delay)
|
||||||
|
[4] A8 after 2ms delay
|
||||||
|
[5] A7 data result
|
||||||
|
[6] A6 final state
|
||||||
|
[7] echo: command sent
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
sys.path.insert(0, 'tools')
|
||||||
|
from skywalker_lib import SkyWalker1
|
||||||
|
|
||||||
|
CMD_I2C_DIAG = 0xB6
|
||||||
|
CMD_I2C_RAW_READ = 0xB5
|
||||||
|
BCM4500_ADDR = 0x08
|
||||||
|
|
||||||
|
sw = SkyWalker1()
|
||||||
|
sw.open()
|
||||||
|
print('=== A8 Auto-Clear Test ===')
|
||||||
|
print(f'Firmware: {sw.get_fw_version()}')
|
||||||
|
|
||||||
|
# Boot with full sequence
|
||||||
|
print('\n--- Booting BCM4500 (full boot) ---')
|
||||||
|
result = sw._vendor_in(0x89, value=1, index=0, length=3)
|
||||||
|
cfg, stage = result[0], result[1]
|
||||||
|
print(f' Config: 0x{cfg:02X}, Stage: 0x{stage:02X}')
|
||||||
|
|
||||||
|
# Read A8 default state before any commands
|
||||||
|
print('\n--- A8 default state (after boot, before diagnostic) ---')
|
||||||
|
a8_default = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR,
|
||||||
|
index=0xA8, length=1)
|
||||||
|
print(f' A8 default: 0x{a8_default[0]:02X} (bit0={a8_default[0] & 0x01})')
|
||||||
|
|
||||||
|
# Test 1: READ command (0x01) — this was the previous behavior
|
||||||
|
print('\n=== Test 1: A8 with READ command (0x01) ===')
|
||||||
|
diag1 = sw._vendor_in(CMD_I2C_DIAG, value=0x06, index=0x01, length=8)
|
||||||
|
print(f' A6 write ok: {diag1[0]}')
|
||||||
|
print(f' A6 readback: 0x{diag1[1]:02X}')
|
||||||
|
print(f' A8 write ok: {diag1[2]}')
|
||||||
|
print(f' A8 IMMEDIATE: 0x{diag1[3]:02X} (bit0={diag1[3] & 0x01})')
|
||||||
|
print(f' A8 after 2ms: 0x{diag1[4]:02X} (bit0={diag1[4] & 0x01})')
|
||||||
|
print(f' A7 data: 0x{diag1[5]:02X}')
|
||||||
|
print(f' A6 final: 0x{diag1[6]:02X}')
|
||||||
|
print(f' Command sent: 0x{diag1[7]:02X}')
|
||||||
|
|
||||||
|
# Reset A8 back to default by reading
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
# Test 2: WRITE command (0x03) — the one used by init blocks
|
||||||
|
print('\n=== Test 2: A8 with WRITE command (0x03) ===')
|
||||||
|
diag2 = sw._vendor_in(CMD_I2C_DIAG, value=0x06, index=0x03, length=8)
|
||||||
|
print(f' A6 write ok: {diag2[0]}')
|
||||||
|
print(f' A6 readback: 0x{diag2[1]:02X}')
|
||||||
|
print(f' A8 write ok: {diag2[2]}')
|
||||||
|
print(f' A8 IMMEDIATE: 0x{diag2[3]:02X} (bit0={diag2[3] & 0x01})')
|
||||||
|
print(f' A8 after 2ms: 0x{diag2[4]:02X} (bit0={diag2[4] & 0x01})')
|
||||||
|
print(f' A7 data: 0x{diag2[5]:02X}')
|
||||||
|
print(f' A6 final: 0x{diag2[6]:02X}')
|
||||||
|
print(f' Command sent: 0x{diag2[7]:02X}')
|
||||||
|
|
||||||
|
# Test 3: Try other A8 values
|
||||||
|
print('\n=== Test 3: Various A8 values ===')
|
||||||
|
for cmd in [0x00, 0x02, 0x04, 0xFF]:
|
||||||
|
time.sleep(0.01)
|
||||||
|
diag = sw._vendor_in(CMD_I2C_DIAG, value=0x00, index=cmd, length=8)
|
||||||
|
print(f' CMD=0x{cmd:02X}: A8_imm=0x{diag[3]:02X} A8_2ms=0x{diag[4]:02X} '
|
||||||
|
f'A7=0x{diag[5]:02X}')
|
||||||
|
|
||||||
|
# Analysis
|
||||||
|
print('\n=== Analysis ===')
|
||||||
|
if diag2[3] != 0x03 or diag2[4] != 0x03:
|
||||||
|
print(f' !! A8 AUTO-CLEARS after WRITE command!')
|
||||||
|
print(f' Wrote 0x03, immediate={diag2[3]:02X}, after_2ms={diag2[4]:02X}')
|
||||||
|
if (diag2[3] & 0x01) == 0 or (diag2[4] & 0x01) == 0:
|
||||||
|
print(f' bcm_poll_ready() sees bit0=0 → always returns TRUE')
|
||||||
|
print(f' Init blocks appear to succeed but DSP never processes them!')
|
||||||
|
else:
|
||||||
|
print(f' A8 retains WRITE command (0x03). No auto-clear.')
|
||||||
|
if (diag1[3] == 0x01) and (diag2[3] == 0x03):
|
||||||
|
print(f' Both READ and WRITE commands stick. DSP is genuinely not processing.')
|
||||||
|
|
||||||
|
sw.close()
|
||||||
|
print('\n=== Done ===')
|
||||||
128
tools/addr_gateway_test.py
Normal file
128
tools/addr_gateway_test.py
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""BCM4500 I2C address gateway verification test.
|
||||||
|
|
||||||
|
Compares register reads at address 0x08 (BCM4500 direct) vs 0x10
|
||||||
|
(BCM3440 tuner gateway) to confirm the stock firmware disassembly
|
||||||
|
finding: all BCM4500 register access goes through the tuner at 0x10.
|
||||||
|
|
||||||
|
HYPOTHESIS:
|
||||||
|
- Reads at 0x08 return the same status byte for all registers
|
||||||
|
- Reads at 0x10 return different, register-specific values
|
||||||
|
- Writes at 0x10 actually reach BCM4500 registers
|
||||||
|
- The indirect register protocol (A6/A7/A8) works at 0x10
|
||||||
|
|
||||||
|
Run with custom firmware v3.02+ (needs 0xB5 raw I2C read).
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, 'tools')
|
||||||
|
from skywalker_lib import SkyWalker1
|
||||||
|
|
||||||
|
CMD_I2C_RAW_READ = 0xB5
|
||||||
|
|
||||||
|
ADDR_DIRECT = 0x08 # BCM4500 direct
|
||||||
|
ADDR_GATEWAY = 0x10 # BCM3440 tuner gateway
|
||||||
|
|
||||||
|
REGS = [0xA0, 0xA2, 0xA4, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB]
|
||||||
|
|
||||||
|
sw = SkyWalker1()
|
||||||
|
sw.open()
|
||||||
|
print('=== BCM4500 I2C Address Gateway Test ===')
|
||||||
|
print(f'Firmware: {sw.get_fw_version()}')
|
||||||
|
|
||||||
|
# Boot first
|
||||||
|
print('\n--- Booting BCM4500 ---')
|
||||||
|
result = sw._vendor_in(0x89, value=1, index=0, length=3)
|
||||||
|
cfg, stage = result[0], result[1]
|
||||||
|
print(f' Config: 0x{cfg:02X}, Stage: 0x{stage:02X}')
|
||||||
|
|
||||||
|
# Test 1: Read registers via direct address (0x08)
|
||||||
|
print(f'\n=== Test 1: Registers via direct address 0x{ADDR_DIRECT:02X} (BCM4500) ===')
|
||||||
|
direct_vals = {}
|
||||||
|
for reg in REGS:
|
||||||
|
try:
|
||||||
|
data = sw._vendor_in(CMD_I2C_RAW_READ, value=ADDR_DIRECT, index=reg, length=1)
|
||||||
|
direct_vals[reg] = data[0]
|
||||||
|
print(f' 0x{reg:02X}: 0x{data[0]:02X}')
|
||||||
|
except Exception as e:
|
||||||
|
direct_vals[reg] = None
|
||||||
|
print(f' 0x{reg:02X}: FAILED ({e})')
|
||||||
|
|
||||||
|
# Test 2: Read registers via gateway address (0x10)
|
||||||
|
print(f'\n=== Test 2: Registers via gateway address 0x{ADDR_GATEWAY:02X} (BCM3440) ===')
|
||||||
|
gw_vals = {}
|
||||||
|
for reg in REGS:
|
||||||
|
try:
|
||||||
|
data = sw._vendor_in(CMD_I2C_RAW_READ, value=ADDR_GATEWAY, index=reg, length=1)
|
||||||
|
gw_vals[reg] = data[0]
|
||||||
|
print(f' 0x{reg:02X}: 0x{data[0]:02X}')
|
||||||
|
except Exception as e:
|
||||||
|
gw_vals[reg] = None
|
||||||
|
print(f' 0x{reg:02X}: FAILED ({e})')
|
||||||
|
|
||||||
|
# Test 3: Read tuner-specific registers (low range) at 0x10
|
||||||
|
print(f'\n=== Test 3: BCM3440 tuner registers (0x00-0x0F) at 0x{ADDR_GATEWAY:02X} ===')
|
||||||
|
for reg in range(0x00, 0x10):
|
||||||
|
try:
|
||||||
|
data = sw._vendor_in(CMD_I2C_RAW_READ, value=ADDR_GATEWAY, index=reg, length=1)
|
||||||
|
print(f' 0x{reg:02X}: 0x{data[0]:02X}', end='')
|
||||||
|
except Exception:
|
||||||
|
print(f' 0x{reg:02X}: FAIL', end='')
|
||||||
|
if (reg + 1) % 8 == 0:
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test 4: Write to A6 via gateway, then read back
|
||||||
|
print(f'\n=== Test 4: Write A6=0x42 via gateway, read back ===')
|
||||||
|
try:
|
||||||
|
# Write using vendor OUT (need to construct this)
|
||||||
|
# Use bcm_direct_write equivalent through 0xB2 (RAW_DEMOD_WRITE)
|
||||||
|
# Actually, we need a raw I2C write command...
|
||||||
|
# Let's use the B6 diagnostic which writes A6 and reads back
|
||||||
|
diag = sw._vendor_in(0xB6, value=0x42, index=0x01, length=8)
|
||||||
|
print(f' B6 diag (via current FW addr):')
|
||||||
|
print(f' A6 write ok: {diag[0]}')
|
||||||
|
print(f' A6 readback: 0x{diag[1]:02X}')
|
||||||
|
print(f' A8 write ok: {diag[2]}')
|
||||||
|
print(f' A8 immediate: 0x{diag[3]:02X}')
|
||||||
|
print(f' A8 after 2ms: 0x{diag[4]:02X}')
|
||||||
|
print(f' A7 data: 0x{diag[5]:02X}')
|
||||||
|
print(f' A6 final: 0x{diag[6]:02X}')
|
||||||
|
print(f' Cmd sent: 0x{diag[7]:02X}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' FAILED: {e}')
|
||||||
|
|
||||||
|
# Analysis
|
||||||
|
print('\n' + '=' * 60)
|
||||||
|
print('ANALYSIS')
|
||||||
|
print('=' * 60)
|
||||||
|
|
||||||
|
direct_unique = set(v for v in direct_vals.values() if v is not None)
|
||||||
|
gw_unique = set(v for v in gw_vals.values() if v is not None)
|
||||||
|
|
||||||
|
print(f'\nDirect (0x{ADDR_DIRECT:02X}): {len(direct_unique)} unique values: '
|
||||||
|
f'{", ".join(f"0x{v:02X}" for v in sorted(direct_unique))}')
|
||||||
|
print(f'Gateway (0x{ADDR_GATEWAY:02X}): {len(gw_unique)} unique values: '
|
||||||
|
f'{", ".join(f"0x{v:02X}" for v in sorted(gw_unique))}')
|
||||||
|
|
||||||
|
if len(direct_unique) <= 2 and len(gw_unique) > 2:
|
||||||
|
print('\n >> HYPOTHESIS CONFIRMED!')
|
||||||
|
print(' >> Direct 0x08 returns same status for all regs')
|
||||||
|
print(' >> Gateway 0x10 returns register-specific values')
|
||||||
|
print(' >> FIX: BCM4500_ADDR should be 0x10 (tuner gateway)')
|
||||||
|
elif len(gw_unique) <= 2:
|
||||||
|
print('\n >> Gateway also returns uniform values -- may need different timing')
|
||||||
|
print(' >> or the tuner gateway requires initialization first')
|
||||||
|
else:
|
||||||
|
print('\n >> Unexpected results -- check values above')
|
||||||
|
|
||||||
|
# Show side-by-side comparison
|
||||||
|
print('\n Reg Direct(0x08) Gateway(0x10) Different?')
|
||||||
|
for reg in REGS:
|
||||||
|
d = direct_vals.get(reg)
|
||||||
|
g = gw_vals.get(reg)
|
||||||
|
d_str = f'0x{d:02X}' if d is not None else 'FAIL'
|
||||||
|
g_str = f'0x{g:02X}' if g is not None else 'FAIL'
|
||||||
|
diff = ' <--' if d != g else ''
|
||||||
|
print(f' 0x{reg:02X} {d_str:>12} {g_str:>13} {diff}')
|
||||||
|
|
||||||
|
sw.close()
|
||||||
|
print('\n=== Done ===')
|
||||||
194
tools/boot_ab_test.py
Normal file
194
tools/boot_ab_test.py
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""A/B test: compare BCM4500 register state with and without firmware download.
|
||||||
|
|
||||||
|
Uses wIndex boot flags on BOOT_8PSK (0x89):
|
||||||
|
bit 0 (0x01): skip EEPROM firmware download
|
||||||
|
bit 1 (0x02): add 200ms DSP startup delay after download
|
||||||
|
bit 2 (0x04): skip register init blocks
|
||||||
|
|
||||||
|
Test matrix:
|
||||||
|
A) wIndex=0x01 — skip FW download, init blocks only
|
||||||
|
B) wIndex=0x00 — full boot (FW download + init blocks)
|
||||||
|
C) wIndex=0x02 — full boot + 200ms DSP delay
|
||||||
|
D) wIndex=0x04 — FW download only, skip init blocks
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
sys.path.insert(0, 'tools')
|
||||||
|
from skywalker_lib import SkyWalker1
|
||||||
|
|
||||||
|
CMD_RAW_DEMOD_READ = 0xB1
|
||||||
|
CMD_I2C_RAW_READ = 0xB5
|
||||||
|
CMD_GET_PLL_DIAG = 0xBF
|
||||||
|
BCM4500_ADDR = 0x08
|
||||||
|
|
||||||
|
# Key direct registers
|
||||||
|
KEY_REGS = [
|
||||||
|
(0xA0, 'CFG_MODE'),
|
||||||
|
(0xA2, 'STATUS'),
|
||||||
|
(0xA4, 'LOCK'),
|
||||||
|
(0xA6, 'PAGE'),
|
||||||
|
(0xA7, 'DATA'),
|
||||||
|
(0xA8, 'CMD'),
|
||||||
|
(0xA9, 'PLL_A9'),
|
||||||
|
(0xAA, 'PLL_AA'),
|
||||||
|
(0xAB, 'PLL_AB'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def read_key_regs(sw, label):
|
||||||
|
"""Read key BCM4500 registers via raw I2C (ground truth)."""
|
||||||
|
print(f' --- {label} ---')
|
||||||
|
results = {}
|
||||||
|
for reg, name in KEY_REGS:
|
||||||
|
try:
|
||||||
|
data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR,
|
||||||
|
index=reg, length=1)
|
||||||
|
val = data[0]
|
||||||
|
results[reg] = val
|
||||||
|
marker = ''
|
||||||
|
if val == 0x00:
|
||||||
|
marker = ' (zero)'
|
||||||
|
elif val == 0x02:
|
||||||
|
marker = ' << non-zero!'
|
||||||
|
elif val != 0x00:
|
||||||
|
marker = ' << non-zero!'
|
||||||
|
print(f' 0x{reg:02X} ({name:8s}): 0x{val:02X}{marker}')
|
||||||
|
except Exception as e:
|
||||||
|
results[reg] = None
|
||||||
|
print(f' 0x{reg:02X} ({name:8s}): FAILED ({e})')
|
||||||
|
nonzero = sum(1 for v in results.values() if v and v != 0)
|
||||||
|
print(f' Non-zero registers: {nonzero}/{len(results)}')
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def boot_with_flags(sw, wval=1, flags=0, label=''):
|
||||||
|
"""Send BOOT_8PSK with wValue and wIndex (boot flags)."""
|
||||||
|
print(f'\n{"="*60}')
|
||||||
|
print(f'BOOT: wValue={wval}, wIndex=0x{flags:02X} — {label}')
|
||||||
|
print(f'{"="*60}')
|
||||||
|
|
||||||
|
# Send boot command: wValue=boot mode, wIndex=flags
|
||||||
|
try:
|
||||||
|
result = sw._vendor_in(0x89, value=wval, index=flags, length=3)
|
||||||
|
cfg = result[0]
|
||||||
|
stage = result[1]
|
||||||
|
bits = []
|
||||||
|
if cfg & 0x01: bits.append('Started')
|
||||||
|
if cfg & 0x02: bits.append('FW_Loaded')
|
||||||
|
if cfg & 0x04: bits.append('Intersil')
|
||||||
|
if cfg & 0x08: bits.append('DVBmode')
|
||||||
|
print(f' Config: 0x{cfg:02X} ({" | ".join(bits) if bits else "none"})')
|
||||||
|
print(f' Boot stage: 0x{stage:02X}' +
|
||||||
|
(' (COMPLETE)' if stage == 0xFF else f' (stopped at {stage})'))
|
||||||
|
except Exception as e:
|
||||||
|
print(f' Boot command failed: {e}')
|
||||||
|
return None
|
||||||
|
|
||||||
|
# PLL diagnostics
|
||||||
|
try:
|
||||||
|
pd = sw._vendor_in(CMD_GET_PLL_DIAG, value=0, index=0, length=10)
|
||||||
|
print(f' PLL diag: eeprom={"Y" if pd[0] else "N"} '
|
||||||
|
f'blocks={pd[2]} '
|
||||||
|
f'exit={"OK" if pd[6]==1 else "FAIL" if pd[6]==0 else "skip"} '
|
||||||
|
f'result={"OK" if pd[7]==1 else "FAIL"}')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return read_key_regs(sw, 'Registers immediately after boot')
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sw = SkyWalker1()
|
||||||
|
sw.open()
|
||||||
|
print('=== BCM4500 Boot A/B Test ===')
|
||||||
|
print(f'Firmware: {sw.get_fw_version()}')
|
||||||
|
|
||||||
|
# Pre-boot register state (BCM4500 may still be in stock-firmware state)
|
||||||
|
print('\n' + '='*60)
|
||||||
|
print('PRE-BOOT: Register state before any boot command')
|
||||||
|
print('='*60)
|
||||||
|
pre = read_key_regs(sw, 'Before boot')
|
||||||
|
|
||||||
|
# Shutdown first to establish baseline
|
||||||
|
print('\n--- Shutting down BCM4500 ---')
|
||||||
|
sw._vendor_in(0x89, value=0, index=0, length=3)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# ===== TEST A: Init blocks only (no firmware download) =====
|
||||||
|
regs_a = boot_with_flags(sw, wval=1, flags=0x01,
|
||||||
|
label='Init blocks ONLY (skip FW download)')
|
||||||
|
|
||||||
|
# Read again after 200ms
|
||||||
|
time.sleep(0.2)
|
||||||
|
regs_a2 = read_key_regs(sw, 'After 200ms settle (test A)')
|
||||||
|
|
||||||
|
# Shutdown and wait
|
||||||
|
print('\n--- Shutting down BCM4500 ---')
|
||||||
|
sw._vendor_in(0x89, value=0, index=0, length=3)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# ===== TEST B: Full boot (firmware download + init blocks) =====
|
||||||
|
regs_b = boot_with_flags(sw, wval=1, flags=0x00,
|
||||||
|
label='Full boot (FW download + init blocks)')
|
||||||
|
|
||||||
|
# Read again after 200ms
|
||||||
|
time.sleep(0.2)
|
||||||
|
regs_b2 = read_key_regs(sw, 'After 200ms settle (test B)')
|
||||||
|
|
||||||
|
# Shutdown and wait
|
||||||
|
print('\n--- Shutting down BCM4500 ---')
|
||||||
|
sw._vendor_in(0x89, value=0, index=0, length=3)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# ===== TEST C: Full boot + 200ms DSP startup delay =====
|
||||||
|
regs_c = boot_with_flags(sw, wval=1, flags=0x02,
|
||||||
|
label='Full boot + 200ms DSP delay')
|
||||||
|
|
||||||
|
regs_c2 = read_key_regs(sw, 'Registers (test C)')
|
||||||
|
|
||||||
|
# Shutdown and wait
|
||||||
|
print('\n--- Shutting down BCM4500 ---')
|
||||||
|
sw._vendor_in(0x89, value=0, index=0, length=3)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# ===== TEST D: Firmware download only (no init blocks) =====
|
||||||
|
regs_d = boot_with_flags(sw, wval=1, flags=0x04,
|
||||||
|
label='FW download ONLY (skip init blocks)')
|
||||||
|
|
||||||
|
time.sleep(0.2)
|
||||||
|
regs_d2 = read_key_regs(sw, 'After 200ms settle (test D)')
|
||||||
|
|
||||||
|
# ===== SUMMARY =====
|
||||||
|
print('\n' + '='*60)
|
||||||
|
print('SUMMARY: Register 0xA2 (STATUS) across tests')
|
||||||
|
print('='*60)
|
||||||
|
|
||||||
|
def val_str(regs, reg=0xA2):
|
||||||
|
if regs is None:
|
||||||
|
return 'FAIL'
|
||||||
|
v = regs.get(reg)
|
||||||
|
if v is None:
|
||||||
|
return 'FAIL'
|
||||||
|
return f'0x{v:02X}'
|
||||||
|
|
||||||
|
print(f' Pre-boot (stock FW state): {val_str(pre)}')
|
||||||
|
print(f' A) Init only, immediate: {val_str(regs_a)}')
|
||||||
|
print(f' A) Init only, +200ms: {val_str(regs_a2)}')
|
||||||
|
print(f' B) Full boot, immediate: {val_str(regs_b)}')
|
||||||
|
print(f' B) Full boot, +200ms: {val_str(regs_b2)}')
|
||||||
|
print(f' C) Full boot + DSP delay: {val_str(regs_c)}')
|
||||||
|
print(f' D) FW only, +200ms: {val_str(regs_d2)}')
|
||||||
|
|
||||||
|
print()
|
||||||
|
print('Expected: A should show 0x02 (previous working state)')
|
||||||
|
print(' B should show 0x00 (current broken state)')
|
||||||
|
print(' C tests if DSP just needs more startup time')
|
||||||
|
print(' D isolates firmware download effect')
|
||||||
|
|
||||||
|
sw.close()
|
||||||
|
print('\n=== Done ===')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
154
tools/boot_deep_verify.py
Normal file
154
tools/boot_deep_verify.py
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Deep verification: full register dump + indirect register test.
|
||||||
|
|
||||||
|
Checks whether:
|
||||||
|
1. Direct registers (0xA0-0xBF) are truly all 0x02 or vary
|
||||||
|
2. Indirect registers respond (proves DSP core is running)
|
||||||
|
3. Signal monitoring works after boot
|
||||||
|
4. Boot with vs without FW download produces different indirect reg values
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
sys.path.insert(0, 'tools')
|
||||||
|
from skywalker_lib import SkyWalker1
|
||||||
|
|
||||||
|
CMD_RAW_DEMOD_READ = 0xB1
|
||||||
|
CMD_I2C_RAW_READ = 0xB5
|
||||||
|
BCM4500_ADDR = 0x08
|
||||||
|
|
||||||
|
|
||||||
|
def full_direct_dump(sw, label):
|
||||||
|
"""Read all direct registers 0xA0-0xBF via raw I2C."""
|
||||||
|
print(f'\n --- {label}: Direct Register Dump 0xA0-0xBF ---')
|
||||||
|
values = {}
|
||||||
|
for reg in range(0xA0, 0xC0):
|
||||||
|
try:
|
||||||
|
data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR,
|
||||||
|
index=reg, length=1)
|
||||||
|
values[reg] = data[0]
|
||||||
|
except Exception:
|
||||||
|
values[reg] = None
|
||||||
|
|
||||||
|
# Print in 2 rows of 16
|
||||||
|
for base in [0xA0, 0xB0]:
|
||||||
|
row = []
|
||||||
|
for reg in range(base, base + 16):
|
||||||
|
v = values.get(reg)
|
||||||
|
row.append(f'{v:02X}' if v is not None else '??')
|
||||||
|
print(f' {base:02X}: {" ".join(row)}')
|
||||||
|
|
||||||
|
unique = set(v for v in values.values() if v is not None)
|
||||||
|
print(f' Unique values: {sorted(f"0x{v:02X}" for v in unique)}')
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def indirect_reg_test(sw, label):
|
||||||
|
"""Read indirect registers to test DSP core responsiveness.
|
||||||
|
Uses 0xB1 with wIndex=0 (indirect mode): wValue=page."""
|
||||||
|
print(f'\n --- {label}: Indirect Register Test ---')
|
||||||
|
# Key DSP pages: 0x00 (config), 0x06 (acq), 0x07 (AGC),
|
||||||
|
# 0x0A (Viterbi), 0x0F (transport)
|
||||||
|
pages = [0x00, 0x01, 0x06, 0x07, 0x0A, 0x0F, 0x10, 0x20]
|
||||||
|
results = {}
|
||||||
|
for page in pages:
|
||||||
|
try:
|
||||||
|
data = sw._vendor_in(CMD_RAW_DEMOD_READ, value=page,
|
||||||
|
index=0, length=1)
|
||||||
|
val = data[0]
|
||||||
|
results[page] = val
|
||||||
|
marker = ' << DSP alive!' if val != 0 else ''
|
||||||
|
print(f' Page 0x{page:02X}: 0x{val:02X}{marker}')
|
||||||
|
except Exception as e:
|
||||||
|
results[page] = None
|
||||||
|
print(f' Page 0x{page:02X}: FAILED ({e})')
|
||||||
|
|
||||||
|
nonzero = sum(1 for v in results.values() if v and v != 0)
|
||||||
|
print(f' Non-zero pages: {nonzero}/{len(pages)}')
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def boot_and_test(sw, flags, label):
|
||||||
|
"""Boot with given flags and run full diagnostics."""
|
||||||
|
print(f'\n{"="*60}')
|
||||||
|
print(f'TEST: {label} (wIndex=0x{flags:02X})')
|
||||||
|
print(f'{"="*60}')
|
||||||
|
|
||||||
|
# Shutdown + wait
|
||||||
|
sw._vendor_in(0x89, value=0, index=0, length=3)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# Boot
|
||||||
|
result = sw._vendor_in(0x89, value=1, index=flags, length=3)
|
||||||
|
cfg, stage = result[0], result[1]
|
||||||
|
bits = []
|
||||||
|
if cfg & 0x01: bits.append('Started')
|
||||||
|
if cfg & 0x02: bits.append('FW_Loaded')
|
||||||
|
print(f' Config: 0x{cfg:02X} ({" | ".join(bits) if bits else "none"})')
|
||||||
|
print(f' Stage: 0x{stage:02X}' +
|
||||||
|
(' (COMPLETE)' if stage == 0xFF else f' (at {stage})'))
|
||||||
|
|
||||||
|
# Full register dumps
|
||||||
|
direct = full_direct_dump(sw, label)
|
||||||
|
indirect = indirect_reg_test(sw, label)
|
||||||
|
|
||||||
|
# Wait and re-test indirect (DSP may need startup time)
|
||||||
|
time.sleep(0.5)
|
||||||
|
indirect2 = indirect_reg_test(sw, f'{label} +500ms')
|
||||||
|
|
||||||
|
# Signal monitor
|
||||||
|
print(f'\n --- Signal Monitor ---')
|
||||||
|
try:
|
||||||
|
sig = sw.signal_monitor()
|
||||||
|
print(f' {sig}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' Failed: {e}')
|
||||||
|
|
||||||
|
return direct, indirect, indirect2
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sw = SkyWalker1()
|
||||||
|
sw.open()
|
||||||
|
print('=== BCM4500 Deep Boot Verification ===')
|
||||||
|
print(f'Firmware: {sw.get_fw_version()}')
|
||||||
|
|
||||||
|
# Test 1: Init blocks only (no FW download)
|
||||||
|
d1, i1, i1b = boot_and_test(sw, 0x01,
|
||||||
|
'Init blocks only (skip FW download)')
|
||||||
|
|
||||||
|
# Test 2: Full boot (FW download + init blocks)
|
||||||
|
d2, i2, i2b = boot_and_test(sw, 0x00,
|
||||||
|
'Full boot (FW download + init blocks)')
|
||||||
|
|
||||||
|
# Compare indirect registers between the two modes
|
||||||
|
print(f'\n{"="*60}')
|
||||||
|
print('COMPARISON: Indirect Registers (init-only vs full boot)')
|
||||||
|
print('='*60)
|
||||||
|
pages = [0x00, 0x01, 0x06, 0x07, 0x0A, 0x0F, 0x10, 0x20]
|
||||||
|
for page in pages:
|
||||||
|
v1 = i1b.get(page)
|
||||||
|
v2 = i2b.get(page)
|
||||||
|
v1s = f'0x{v1:02X}' if v1 is not None else '??'
|
||||||
|
v2s = f'0x{v2:02X}' if v2 is not None else '??'
|
||||||
|
diff = ' << DIFFERENT!' if v1 != v2 else ''
|
||||||
|
print(f' Page 0x{page:02X}: init-only={v1s} full={v2s}{diff}')
|
||||||
|
|
||||||
|
# Compare direct registers
|
||||||
|
print(f'\nDirect register comparison (0xA0-0xBF):')
|
||||||
|
diffs = []
|
||||||
|
for reg in range(0xA0, 0xC0):
|
||||||
|
v1 = d1.get(reg)
|
||||||
|
v2 = d2.get(reg)
|
||||||
|
if v1 != v2:
|
||||||
|
diffs.append(f' 0x{reg:02X}: init-only=0x{v1:02X} full=0x{v2:02X}')
|
||||||
|
if diffs:
|
||||||
|
print('\n'.join(diffs))
|
||||||
|
else:
|
||||||
|
print(' No differences (all registers identical)')
|
||||||
|
|
||||||
|
sw.close()
|
||||||
|
print('\n=== Done ===')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
189
tools/boot_reg_probe.py
Normal file
189
tools/boot_reg_probe.py
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Probe BCM4500 register behavior after boot.
|
||||||
|
|
||||||
|
1. Multi-byte reads via 0xB5 (do adjacent registers differ?)
|
||||||
|
2. Step-by-step indirect read via 0xB6 diagnostic
|
||||||
|
3. Write a register and read back (is the BCM alive or echoing?)
|
||||||
|
4. Try tuning to see if the signal path works
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
sys.path.insert(0, 'tools')
|
||||||
|
from skywalker_lib import SkyWalker1
|
||||||
|
|
||||||
|
CMD_I2C_RAW_READ = 0xB5
|
||||||
|
CMD_I2C_DIAG = 0xB6
|
||||||
|
CMD_RAW_DEMOD_READ = 0xB1
|
||||||
|
CMD_RAW_DEMOD_WRITE = 0xB2
|
||||||
|
BCM4500_ADDR = 0x08
|
||||||
|
|
||||||
|
sw = SkyWalker1()
|
||||||
|
sw.open()
|
||||||
|
print('=== BCM4500 Register Probe ===')
|
||||||
|
print(f'Firmware: {sw.get_fw_version()}')
|
||||||
|
|
||||||
|
# Boot with full sequence
|
||||||
|
print('\n--- Booting BCM4500 (full boot) ---')
|
||||||
|
result = sw._vendor_in(0x89, value=1, index=0, length=3)
|
||||||
|
cfg, stage = result[0], result[1]
|
||||||
|
print(f' Config: 0x{cfg:02X}, Stage: 0x{stage:02X}')
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# TEST 1: Multi-byte read — read 16 bytes starting at 0xA0
|
||||||
|
# ============================================================
|
||||||
|
print('\n=== Test 1: Multi-byte read (16 bytes from 0xA0) ===')
|
||||||
|
print('If BCM4500 truly maps different registers, bytes should differ.')
|
||||||
|
try:
|
||||||
|
data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR,
|
||||||
|
index=0xA0, length=16)
|
||||||
|
hex_str = ' '.join(f'{b:02X}' for b in data)
|
||||||
|
print(f' 0xA0-0xAF: {hex_str}')
|
||||||
|
unique = set(data)
|
||||||
|
print(f' Unique values: {sorted(f"0x{v:02X}" for v in unique)}')
|
||||||
|
if len(unique) == 1:
|
||||||
|
print(f' ALL SAME VALUE: 0x{data[0]:02X} — BCM4500 may be returning')
|
||||||
|
print(f' a fixed status byte, not true register contents.')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' Failed: {e}')
|
||||||
|
|
||||||
|
# Read second block 0xB0-0xBF
|
||||||
|
try:
|
||||||
|
data2 = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR,
|
||||||
|
index=0xB0, length=16)
|
||||||
|
hex_str = ' '.join(f'{b:02X}' for b in data2)
|
||||||
|
print(f' 0xB0-0xBF: {hex_str}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' Failed: {e}')
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# TEST 2: Write-then-read — does the BCM4500 retain writes?
|
||||||
|
# ============================================================
|
||||||
|
print('\n=== Test 2: Write-then-read (register 0xA6 PAGE) ===')
|
||||||
|
print('Write 0x42 to 0xA6, read back. If we get 0x42, register works.')
|
||||||
|
print('If we get 0x02, the chip may be ignoring writes.')
|
||||||
|
|
||||||
|
# Read current value
|
||||||
|
try:
|
||||||
|
before = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR,
|
||||||
|
index=0xA6, length=1)
|
||||||
|
print(f' Before write: 0xA6 = 0x{before[0]:02X}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' Read failed: {e}')
|
||||||
|
|
||||||
|
# Write 0x42 to 0xA6 via 0xB1 direct write
|
||||||
|
try:
|
||||||
|
# 0xB2: RAW_DEMOD_WRITE — wValue=register, wIndex=data
|
||||||
|
sw._vendor_in(CMD_RAW_DEMOD_WRITE, value=0xA6, index=0x42, length=0)
|
||||||
|
except Exception:
|
||||||
|
# 0xB2 might not return data
|
||||||
|
pass
|
||||||
|
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
# Read back
|
||||||
|
try:
|
||||||
|
after = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR,
|
||||||
|
index=0xA6, length=1)
|
||||||
|
print(f' After write 0x42: 0xA6 = 0x{after[0]:02X}')
|
||||||
|
if after[0] == 0x42:
|
||||||
|
print(f' >> REGISTER WORKS — BCM4500 is alive and accepting writes!')
|
||||||
|
elif after[0] == 0x02:
|
||||||
|
print(f' >> Still 0x02 — chip may be ignoring writes or returning status')
|
||||||
|
else:
|
||||||
|
print(f' >> Got 0x{after[0]:02X} — unexpected')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' Read failed: {e}')
|
||||||
|
|
||||||
|
# Restore 0xA6 to 0x00
|
||||||
|
try:
|
||||||
|
sw._vendor_in(CMD_RAW_DEMOD_WRITE, value=0xA6, index=0x00, length=0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# TEST 3: Step-by-step indirect read via 0xB6
|
||||||
|
# ============================================================
|
||||||
|
print('\n=== Test 3: Step-by-step indirect read (0xB6) ===')
|
||||||
|
print('Reading indirect register page 0x06 (acquisition config)')
|
||||||
|
try:
|
||||||
|
diag = sw._vendor_in(CMD_I2C_DIAG, value=0x06, index=0, length=8)
|
||||||
|
labels = [
|
||||||
|
'Write 0xA6 ok', # [0]
|
||||||
|
'Readback 0xA6', # [1]
|
||||||
|
'Write 0xA8 ok', # [2]
|
||||||
|
'Readback 0xA8', # [3]
|
||||||
|
'Readback 0xA7', # [4] — the indirect register data
|
||||||
|
'Direct read 0xA6', # [5]
|
||||||
|
'Direct read 0xA7', # [6]
|
||||||
|
'Direct read 0xA8', # [7]
|
||||||
|
]
|
||||||
|
for i, label in enumerate(labels):
|
||||||
|
ok_marker = ''
|
||||||
|
if i in [0, 2]:
|
||||||
|
ok_marker = ' (success)' if diag[i] == 0x01 else ' (FAILED!)' if diag[i] == 0x00 else ''
|
||||||
|
print(f' [{i}] {label:20s}: 0x{diag[i]:02X}{ok_marker}')
|
||||||
|
|
||||||
|
print()
|
||||||
|
if diag[0] == 0x01 and diag[2] == 0x01:
|
||||||
|
print(' Writes to A6/A8 succeeded.')
|
||||||
|
if diag[1] == 0x06:
|
||||||
|
print(f' A6 readback = 0x06 — page register WORKS.')
|
||||||
|
else:
|
||||||
|
print(f' A6 readback = 0x{diag[1]:02X} — expected 0x06!')
|
||||||
|
if diag[4] != 0x00:
|
||||||
|
print(f' A7 (indirect data) = 0x{diag[4]:02X} — DSP responding!')
|
||||||
|
else:
|
||||||
|
print(f' A7 (indirect data) = 0x00 — DSP may not be running.')
|
||||||
|
else:
|
||||||
|
print(' Write step FAILED — I2C issue.')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' Diagnostic failed: {e}')
|
||||||
|
|
||||||
|
# Try a few more pages
|
||||||
|
for page in [0x00, 0x07, 0x0F]:
|
||||||
|
try:
|
||||||
|
diag = sw._vendor_in(CMD_I2C_DIAG, value=page, index=0, length=8)
|
||||||
|
a6_ok = 'ok' if diag[0] == 1 else 'FAIL'
|
||||||
|
a8_ok = 'ok' if diag[2] == 1 else 'FAIL'
|
||||||
|
print(f' Page 0x{page:02X}: A6={a6_ok} A6_rb=0x{diag[1]:02X} '
|
||||||
|
f'A8={a8_ok} A8_rb=0x{diag[3]:02X} '
|
||||||
|
f'A7_data=0x{diag[4]:02X} '
|
||||||
|
f'direct=[A6=0x{diag[5]:02X} A7=0x{diag[6]:02X} A8=0x{diag[7]:02X}]')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' Page 0x{page:02X}: {e}')
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# TEST 4: Read registers 0x00-0x9F (below the A0 range)
|
||||||
|
# ============================================================
|
||||||
|
print('\n=== Test 4: Registers OUTSIDE 0xA0-0xBF range ===')
|
||||||
|
print('If BCM4500 returns 0x02 for everything, it might do so for ALL addresses.')
|
||||||
|
for reg in [0x00, 0x10, 0x50, 0x80, 0x90, 0x9F, 0xC0, 0xD0, 0xFF]:
|
||||||
|
try:
|
||||||
|
data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR,
|
||||||
|
index=reg, length=1)
|
||||||
|
print(f' Reg 0x{reg:02X}: 0x{data[0]:02X}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' Reg 0x{reg:02X}: FAILED ({e})')
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# TEST 5: Quick tune test (no dish needed — just check AGC)
|
||||||
|
# ============================================================
|
||||||
|
print('\n=== Test 5: Tune to 1200 MHz / 27500 ksps (AGC check) ===')
|
||||||
|
try:
|
||||||
|
# Enable Intersil (LNB controller)
|
||||||
|
sw._vendor_in(0x8A, value=1, index=0, length=1)
|
||||||
|
time.sleep(0.1)
|
||||||
|
print(' Intersil enabled')
|
||||||
|
|
||||||
|
# Tune: 1200 MHz, 27500 ksps, QPSK
|
||||||
|
result = sw.tune_monitor(freq_mhz=1200, sr_ksps=27500, mod_index=0, dwell_ms=200)
|
||||||
|
print(f' Tune result: {result}')
|
||||||
|
if result.get('agc1', 0) > 0 or result.get('agc2', 0) > 0:
|
||||||
|
print(' >> AGC non-zero — tuner + demod signal path is alive!')
|
||||||
|
else:
|
||||||
|
print(' >> AGC=0 — signal path not working (or tuner not configured)')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' Tune failed: {e}')
|
||||||
|
|
||||||
|
sw.close()
|
||||||
|
print('\n=== Done ===')
|
||||||
177
tools/boot_test.py
Normal file
177
tools/boot_test.py
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Quick boot test for PLL config firmware."""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, 'tools')
|
||||||
|
from skywalker_lib import SkyWalker1
|
||||||
|
|
||||||
|
CMD_RAW_DEMOD_READ = 0xB1
|
||||||
|
CMD_I2C_SCAN = 0xB4
|
||||||
|
CMD_GET_PLL_DIAG = 0xBF
|
||||||
|
|
||||||
|
sw = SkyWalker1()
|
||||||
|
sw.open()
|
||||||
|
print('=== SkyWalker-1 Custom Firmware Boot Test ===')
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Check firmware version
|
||||||
|
ver = sw.get_fw_version()
|
||||||
|
print(f'Firmware version: {ver}')
|
||||||
|
|
||||||
|
# Check config status
|
||||||
|
cfg = sw.get_config()
|
||||||
|
print(f'Config status: 0x{cfg:02X}')
|
||||||
|
bits = []
|
||||||
|
if cfg & 0x01: bits.append('Started')
|
||||||
|
if cfg & 0x02: bits.append('FW_Loaded')
|
||||||
|
if cfg & 0x04: bits.append('Intersil')
|
||||||
|
if cfg & 0x08: bits.append('DVBmode')
|
||||||
|
if cfg & 0x10: bits.append('22kHz')
|
||||||
|
if cfg & 0x20: bits.append('18V')
|
||||||
|
if cfg & 0x40: bits.append('DCtuned')
|
||||||
|
if cfg & 0x80: bits.append('Armed')
|
||||||
|
print(f' Flags: {" | ".join(bits) if bits else "(none)"}')
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Check last error before boot
|
||||||
|
err = sw.get_last_error()
|
||||||
|
print(f'Last error (pre-boot): 0x{err:02X}')
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Boot the BCM4500
|
||||||
|
print('--- Booting BCM4500 ---')
|
||||||
|
boot_result = sw.boot()
|
||||||
|
print(f'Boot result: {boot_result}')
|
||||||
|
cfg = sw.get_config()
|
||||||
|
print(f'Config after boot: 0x{cfg:02X}')
|
||||||
|
bits = []
|
||||||
|
if cfg & 0x01: bits.append('Started')
|
||||||
|
if cfg & 0x02: bits.append('FW_Loaded')
|
||||||
|
if cfg & 0x04: bits.append('Intersil')
|
||||||
|
if cfg & 0x08: bits.append('DVBmode')
|
||||||
|
if cfg & 0x10: bits.append('22kHz')
|
||||||
|
if cfg & 0x20: bits.append('18V')
|
||||||
|
if cfg & 0x40: bits.append('DCtuned')
|
||||||
|
if cfg & 0x80: bits.append('Armed')
|
||||||
|
print(f' Flags: {" | ".join(bits) if bits else "(none)"}')
|
||||||
|
|
||||||
|
err = sw.get_last_error()
|
||||||
|
ERR_NAMES = {
|
||||||
|
0x00: 'ERR_OK',
|
||||||
|
0x01: 'ERR_I2C_TIMEOUT',
|
||||||
|
0x02: 'ERR_I2C_NAK',
|
||||||
|
0x03: 'ERR_I2C_ARB_LOST',
|
||||||
|
0x04: 'ERR_BCM_NOT_READY (PLL config failed)',
|
||||||
|
0x05: 'ERR_BCM_TIMEOUT',
|
||||||
|
}
|
||||||
|
print(f'Last error after boot: 0x{err:02X} = {ERR_NAMES.get(err, "unknown")}')
|
||||||
|
print()
|
||||||
|
|
||||||
|
# PLL config diagnostics
|
||||||
|
print('--- PLL Config Diagnostics ---')
|
||||||
|
try:
|
||||||
|
pd = sw._vendor_in(CMD_GET_PLL_DIAG, value=0, index=0, length=10)
|
||||||
|
print(f' EEPROM present: {"YES" if pd[0] else "NO"}')
|
||||||
|
print(f' First block count: 0x{pd[1]:02X}' + (' (sentinel=0, no PLL data!)' if pd[1] == 0 else
|
||||||
|
(' (not reached)' if pd[1] == 0xFF else f' ({pd[1]} AB bytes)')))
|
||||||
|
print(f' Blocks written: {pd[2]}')
|
||||||
|
print(f' Last A9 value: 0x{pd[3]:02X}' + (' (none)' if pd[3] == 0xFF else ''))
|
||||||
|
print(f' Last AA value: 0x{pd[4]:02X}' + (' (none)' if pd[4] == 0xFF else ''))
|
||||||
|
print(f' Last AB count: 0x{pd[5]:02X}' + (' (none)' if pd[5] == 0xFF else ''))
|
||||||
|
print(f' Config mode exit: {"OK" if pd[6] == 1 else ("FAIL" if pd[6] == 0 else "not reached")}')
|
||||||
|
print(f' Overall PLL result: {"SUCCESS" if pd[7] == 1 else "FAILED"}')
|
||||||
|
print(f' Boot stage: 0x{pd[8]:02X}' + (' (all complete)' if pd[8] == 0xFF else f' (stopped at stage {pd[8]})'))
|
||||||
|
print(f' Last error: 0x{pd[9]:02X}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' PLL diag failed: {e}')
|
||||||
|
print()
|
||||||
|
|
||||||
|
# I2C bus scan
|
||||||
|
print('--- I2C Bus Scan ---')
|
||||||
|
try:
|
||||||
|
bitmap = sw._vendor_in(CMD_I2C_SCAN, value=0, index=0, length=16)
|
||||||
|
found = []
|
||||||
|
for byte_idx in range(16):
|
||||||
|
for bit_idx in range(8):
|
||||||
|
if bitmap[byte_idx] & (1 << bit_idx):
|
||||||
|
addr = byte_idx * 8 + bit_idx
|
||||||
|
found.append(addr)
|
||||||
|
labels = {0x08: 'BCM4500', 0x10: 'BCM3440', 0x51: 'EEPROM'}
|
||||||
|
for addr in found:
|
||||||
|
label = labels.get(addr, '')
|
||||||
|
print(f' 0x{addr:02X} {label}')
|
||||||
|
if not found:
|
||||||
|
print(' (no devices found!)')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' Scan failed: {e}')
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Try reading signal
|
||||||
|
print('--- Signal Check ---')
|
||||||
|
try:
|
||||||
|
sig = sw.signal_monitor()
|
||||||
|
print(f'Signal monitor: {sig}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Signal monitor failed: {e}')
|
||||||
|
|
||||||
|
# Read BCM4500 direct registers via 0xB1 vendor command
|
||||||
|
# wValue=register address, wIndex=1 for direct read mode
|
||||||
|
print()
|
||||||
|
print('--- BCM4500 Direct Register Reads ---')
|
||||||
|
key_regs = [
|
||||||
|
(0xA0, 'CFG_MODE'),
|
||||||
|
(0xA2, 'STATUS'),
|
||||||
|
(0xA4, 'LOCK'),
|
||||||
|
(0xA9, 'PLL_A9'),
|
||||||
|
(0xAA, 'PLL_AA'),
|
||||||
|
(0xAB, 'PLL_AB'),
|
||||||
|
]
|
||||||
|
for reg, label in key_regs:
|
||||||
|
try:
|
||||||
|
data = sw._vendor_in(CMD_RAW_DEMOD_READ, value=reg, index=1, length=1)
|
||||||
|
val = data[0]
|
||||||
|
print(f' 0x{reg:02X} ({label:8s}): 0x{val:02X}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' 0x{reg:02X} ({label:8s}): FAILED ({e})')
|
||||||
|
|
||||||
|
# Indirect register reads via 0xB1 with wIndex=0 (indirect mode)
|
||||||
|
# wValue=page, wIndex=0. Only meaningful if the DSP core is running.
|
||||||
|
print()
|
||||||
|
print('--- BCM4500 Indirect Register Reads (DSP core test) ---')
|
||||||
|
for page in [0x06, 0x07, 0x0F]:
|
||||||
|
try:
|
||||||
|
data = sw._vendor_in(CMD_RAW_DEMOD_READ, value=page, index=0, length=1)
|
||||||
|
val = data[0]
|
||||||
|
alive = ' << DSP responding' if val != 0 else ''
|
||||||
|
print(f' Page 0x{page:02X}: 0x{val:02X}{alive}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' Page 0x{page:02X}: FAILED ({e})')
|
||||||
|
|
||||||
|
# Cross-check: read key registers via 0xB5 (raw I2C, writes into EP0BUF directly)
|
||||||
|
# This bypasses the 0xB1 handler's val variable entirely
|
||||||
|
CMD_I2C_RAW_READ = 0xB5
|
||||||
|
BCM4500_ADDR = 0x08
|
||||||
|
print()
|
||||||
|
print('--- Cross-check via 0xB5 Raw I2C Read (BCM4500 @ 0x08) ---')
|
||||||
|
for reg, label in key_regs:
|
||||||
|
try:
|
||||||
|
data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR, index=reg, length=1)
|
||||||
|
val = data[0]
|
||||||
|
tag = ' ** I2C FAIL (0xFF) **' if val == 0xFF else ''
|
||||||
|
print(f' 0x{reg:02X} ({label:8s}): 0x{val:02X}{tag}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' 0x{reg:02X} ({label:8s}): FAILED ({e})')
|
||||||
|
|
||||||
|
# Full register dump 0xA0-0xBF via 0xB5 raw I2C (ground truth)
|
||||||
|
print()
|
||||||
|
print('--- Full Direct Register Dump 0xA0-0xBF (via 0xB5 raw I2C) ---')
|
||||||
|
for reg in range(0xA0, 0xC0):
|
||||||
|
try:
|
||||||
|
data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR, index=reg, length=1)
|
||||||
|
val = data[0]
|
||||||
|
print(f' 0x{reg:02X}: 0x{val:02X}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' 0x{reg:02X}: FAIL')
|
||||||
|
|
||||||
|
sw.close()
|
||||||
|
print()
|
||||||
|
print('=== Test Complete ===')
|
||||||
112
tools/eeprom_deep_scan.py
Normal file
112
tools/eeprom_deep_scan.py
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Deep EEPROM scan — find the real PLL data location."""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, 'tools')
|
||||||
|
from skywalker_lib import SkyWalker1
|
||||||
|
|
||||||
|
CMD_EEPROM_READ = 0xC0
|
||||||
|
|
||||||
|
sw = SkyWalker1()
|
||||||
|
sw.open()
|
||||||
|
|
||||||
|
|
||||||
|
def ee(addr, length):
|
||||||
|
return sw._vendor_in(CMD_EEPROM_READ, value=addr, index=length, length=length)
|
||||||
|
|
||||||
|
|
||||||
|
def hex_line(addr, data):
|
||||||
|
hex_str = ' '.join(f'{b:02X}' for b in data)
|
||||||
|
return f'{addr:04X}: {hex_str}'
|
||||||
|
|
||||||
|
|
||||||
|
# Dump 0x3F00-0x4100 (area around the boundary — zero gap between FX2 and BCM firmware?)
|
||||||
|
print('=== Region around FX2 firmware end (0x2530) ===')
|
||||||
|
for addr in range(0x2530, 0x2600, 16):
|
||||||
|
d = ee(addr, 16)
|
||||||
|
print(f' {hex_line(addr, d)}')
|
||||||
|
|
||||||
|
print()
|
||||||
|
print('=== Region 0x3F00-0x4080 (end of first 16KB, start of second) ===')
|
||||||
|
for addr in range(0x3F00, 0x4080, 16):
|
||||||
|
d = ee(addr, 16)
|
||||||
|
print(f' {hex_line(addr, d)}')
|
||||||
|
|
||||||
|
# Check the END of the AT24C256 (0x7F00-0x7FFF)
|
||||||
|
print()
|
||||||
|
print('=== End of EEPROM 0x7F00-0x7FFF ===')
|
||||||
|
for addr in range(0x7F00, 0x8000, 16):
|
||||||
|
d = ee(addr, 16)
|
||||||
|
print(f' {hex_line(addr, d)}')
|
||||||
|
|
||||||
|
# Parse the second image at 0x4000 to find its end
|
||||||
|
print()
|
||||||
|
print('=== Parsing 0x4000 as C2-like records ===')
|
||||||
|
offset = 0x4000
|
||||||
|
# First check if it's a C2 header
|
||||||
|
header = ee(0x4000, 4)
|
||||||
|
print(f' Header bytes: {header.hex(" ")}')
|
||||||
|
# Could be: [len_hi, len_lo, addr_hi, addr_lo]
|
||||||
|
rec_len = (header[0] << 8) | header[1]
|
||||||
|
rec_addr = (header[2] << 8) | header[3]
|
||||||
|
print(f' As record: len={rec_len} (0x{rec_len:04X}), addr=0x{rec_addr:04X}')
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Try parsing as load records (same format as C2 but without the 8-byte header)
|
||||||
|
offset = 0x4000
|
||||||
|
print(' Attempting record parse:')
|
||||||
|
total_size = 0
|
||||||
|
while offset < 0x7000:
|
||||||
|
hdr = ee(offset, 4)
|
||||||
|
rlen = (hdr[0] << 8) | hdr[1]
|
||||||
|
raddr = (hdr[2] << 8) | hdr[3]
|
||||||
|
|
||||||
|
if rlen == 0x8001:
|
||||||
|
print(f' [{total_size}] END MARKER at 0x{offset:04X} → entry=0x{raddr:04X}')
|
||||||
|
offset += 4
|
||||||
|
break
|
||||||
|
elif rlen == 0 or rlen > 0x4000:
|
||||||
|
print(f' [{total_size}] STOP at 0x{offset:04X}: len=0x{rlen:04X} addr=0x{raddr:04X}')
|
||||||
|
# Dump surrounding bytes
|
||||||
|
d = ee(offset, 32)
|
||||||
|
print(f' Bytes: {d.hex(" ")}')
|
||||||
|
offset += 4
|
||||||
|
break
|
||||||
|
|
||||||
|
end = raddr + rlen - 1
|
||||||
|
print(f' [{total_size}] {rlen:5d} bytes at EEPROM 0x{offset:04X} → 0x{raddr:04X}-0x{end:04X}')
|
||||||
|
offset += 4 + rlen
|
||||||
|
total_size += rlen
|
||||||
|
if total_size > 30000:
|
||||||
|
print(' (aborting, too much data)')
|
||||||
|
break
|
||||||
|
|
||||||
|
print(f' Second image ends at: 0x{offset:04X}')
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Check immediately after second image for PLL data
|
||||||
|
print(f'=== After second image: 0x{offset:04X}-0x{offset+256:04X} ===')
|
||||||
|
for addr in range(offset, offset + 256, 16):
|
||||||
|
d = ee(addr, 16)
|
||||||
|
print(f' {hex_line(addr, d)}')
|
||||||
|
|
||||||
|
# Also check for PLL blocks at this offset
|
||||||
|
print()
|
||||||
|
print(f'=== PLL block scan starting at 0x{offset:04X} ===')
|
||||||
|
for addr in range(offset, offset + 400, 20):
|
||||||
|
block = ee(addr, 20)
|
||||||
|
count = block[0]
|
||||||
|
if count == 0:
|
||||||
|
print(f' 0x{addr:04X}: [sentinel count=0]')
|
||||||
|
break
|
||||||
|
elif 1 <= count <= 16:
|
||||||
|
ab = block[4:4 + count]
|
||||||
|
print(f' 0x{addr:04X}: count={count} A9=0x{block[1]:02X} AA=0x{block[2]:02X} '
|
||||||
|
f'unused=0x{block[3]:02X} AB=[{ab.hex(" ")}]')
|
||||||
|
else:
|
||||||
|
print(f' 0x{addr:04X}: NOT PLL (first byte=0x{count:02X})')
|
||||||
|
# But continue scanning
|
||||||
|
pass
|
||||||
|
|
||||||
|
sw.close()
|
||||||
|
print()
|
||||||
|
print('=== Done ===')
|
||||||
@ -1,251 +1,157 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Genpix SkyWalker-1 EEPROM firmware dump tool.
|
Genpix SkyWalker-1 EEPROM exploration tool.
|
||||||
|
|
||||||
Reads the Cypress FX2 boot EEPROM via the I2C_READ vendor command.
|
Reads the calibration EEPROM (AT24Cxxx at I2C addr 0x51) via the custom
|
||||||
Protocol: I2C_READ (0x84), wValue=0x51, wIndex=offset, length=chunk_size
|
firmware's EEPROM_READ (0xC0) vendor command. This uses 16-bit addressing
|
||||||
|
directly, bypassing the stock firmware's single-byte I2C_READ protocol.
|
||||||
|
|
||||||
The EEPROM contains firmware in Cypress C2 IIC boot format:
|
Primary purpose: Find where PLL configuration data is stored so the
|
||||||
- Header: C2 VID_L VID_H PID_L PID_H DID_L DID_H CONFIG
|
bcm4500_load_pll_config() function reads from the correct address.
|
||||||
- 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
|
import sys
|
||||||
|
sys.path.insert(0, 'tools')
|
||||||
|
from skywalker_lib import SkyWalker1
|
||||||
|
|
||||||
VENDOR_ID = 0x09C0
|
CMD_EEPROM_READ = 0xC0
|
||||||
PRODUCT_ID = 0x0203
|
|
||||||
I2C_READ = 0x84
|
sw = SkyWalker1()
|
||||||
EEPROM_SLAVE = 0x51
|
sw.open()
|
||||||
|
|
||||||
|
|
||||||
def find_device():
|
def eeprom_read(addr, length):
|
||||||
dev = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID)
|
"""Read bytes from EEPROM at 16-bit address.
|
||||||
if dev is None:
|
wValue = address, wIndex = length."""
|
||||||
print("SkyWalker-1 not found")
|
return sw._vendor_in(CMD_EEPROM_READ, value=addr, index=length, length=length)
|
||||||
sys.exit(1)
|
|
||||||
return dev
|
|
||||||
|
|
||||||
|
|
||||||
def detach_driver(dev):
|
def hex_dump(addr, data):
|
||||||
intf_num = None
|
"""Print hex dump with ASCII sidebar."""
|
||||||
for cfg in dev:
|
for i in range(0, len(data), 16):
|
||||||
for intf in cfg:
|
chunk = data[i:i + 16]
|
||||||
if dev.is_kernel_driver_active(intf.bInterfaceNumber):
|
hex_str = ' '.join(f'{b:02X}' for b in chunk)
|
||||||
try:
|
ascii_str = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)
|
||||||
dev.detach_kernel_driver(intf.bInterfaceNumber)
|
print(f' {addr + i:04X}: {hex_str:<48s} |{ascii_str}|')
|
||||||
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):
|
print('=== EEPROM Exploration ===')
|
||||||
"""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()
|
print()
|
||||||
|
|
||||||
return {"vid": vid, "pid": pid, "did": did, "config": config}
|
# Step 1: Determine EEPROM size by aliasing detection
|
||||||
|
print('--- Size Detection ---')
|
||||||
|
data_0000 = eeprom_read(0x0000, 16)
|
||||||
def parse_records(data, offset=8):
|
data_4000 = eeprom_read(0x4000, 16)
|
||||||
"""Parse C2 load records from EEPROM data."""
|
data_8000 = eeprom_read(0x8000, 16)
|
||||||
records = []
|
print(f' 0x0000: {data_0000.hex(" ")}')
|
||||||
while offset < len(data) - 4:
|
print(f' 0x4000: {data_4000.hex(" ")}')
|
||||||
rec_len = (data[offset] << 8) | data[offset + 1]
|
print(f' 0x8000: {data_8000.hex(" ")}')
|
||||||
rec_addr = (data[offset + 2] << 8) | data[offset + 3]
|
if data_0000 == data_4000:
|
||||||
|
print(' Result: 0x4000 ALIASES to 0x0000 → AT24C128 (16KB)')
|
||||||
if rec_len == 0x8001:
|
eeprom_size = 16384
|
||||||
# End marker - rec_addr is the entry point (reset vector)
|
elif data_0000 == data_8000:
|
||||||
records.append({
|
print(' Result: 0x8000 aliases to 0x0000 → AT24C256 (32KB)')
|
||||||
"type": "end",
|
eeprom_size = 32768
|
||||||
"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:
|
else:
|
||||||
consecutive_ff = 0
|
print(' Result: All different → AT24C512+ (64KB+)')
|
||||||
|
eeprom_size = 65536
|
||||||
|
print()
|
||||||
|
|
||||||
if offset % 1024 == 0:
|
# Step 2: Dump first 512 bytes (FX2 boot firmware header + data)
|
||||||
print(f"\r 0x{offset:04X} / 0x{args.max_size:04X} ", end="", flush=True)
|
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()
|
||||||
|
|
||||||
print(f"\r Read {len(eeprom)} bytes total ")
|
# 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()
|
||||||
|
|
||||||
# Save raw EEPROM
|
# Scan the entire EEPROM in 20-byte strides
|
||||||
with open(args.output, 'wb') as f:
|
pll_candidates = []
|
||||||
f.write(eeprom)
|
for addr in range(0, min(eeprom_size, 0x4000), 20):
|
||||||
print(f" Saved raw EEPROM to: {args.output}")
|
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],
|
||||||
|
})
|
||||||
|
|
||||||
# Parse header
|
if pll_candidates:
|
||||||
print(f"\n{'=' * 40}")
|
print(' Found sentinel(s):')
|
||||||
print("EEPROM Header:")
|
for c in pll_candidates:
|
||||||
header = parse_c2_header(eeprom)
|
print(f' Sentinel at 0x{c["sentinel_addr"]:04X}')
|
||||||
|
print(f' Last block at 0x{c["last_block_addr"]:04X}: '
|
||||||
if header:
|
f'count={c["last_count"]} A9=0x{c["last_a9"]:02X} AA=0x{c["last_aa"]:02X}')
|
||||||
# Parse load records
|
# Walk backwards to find start of PLL data
|
||||||
print(f"\nLoad Records:")
|
start = c['last_block_addr']
|
||||||
records = parse_records(eeprom)
|
while start >= 20:
|
||||||
total_code = 0
|
prev = eeprom_read(start - 20, 20)
|
||||||
entry_point = None
|
if 1 <= prev[0] <= 16:
|
||||||
|
start -= 20
|
||||||
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:
|
else:
|
||||||
print(f" [{i}] INVALID (raw_len=0x{rec['raw_len']:04X}) "
|
break
|
||||||
f"at EEPROM offset 0x{rec['offset']:04X}")
|
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()
|
||||||
|
|
||||||
print(f"\n Total firmware: {total_code} bytes in "
|
# Step 4: Dump around the 16KB boundary (where our code expects PLL data)
|
||||||
f"{sum(1 for r in records if r['type'] == 'data')} records")
|
if eeprom_size > 16384:
|
||||||
if entry_point:
|
print('--- EEPROM 0x3FE0-0x4060 (16KB boundary) ---')
|
||||||
print(f" Entry point: 0x{entry_point:04X} (LJMP target after boot)")
|
for addr in range(0x3FE0, 0x4060, 64):
|
||||||
|
data = eeprom_read(addr, 64)
|
||||||
|
hex_dump(addr, data)
|
||||||
|
print()
|
||||||
|
|
||||||
# Extract flat binary
|
# Step 5: Check for 0xFF regions (empty/erased)
|
||||||
if args.extract and records:
|
print('--- Empty region scan ---')
|
||||||
# Build memory image
|
last_was_ff = False
|
||||||
mem = bytearray(0x10000) # 64KB address space
|
for addr in range(0, min(eeprom_size, 0x4000), 64):
|
||||||
for b in range(len(mem)):
|
data = eeprom_read(addr, 64)
|
||||||
mem[b] = 0xFF
|
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')
|
||||||
|
|
||||||
for rec in records:
|
sw.close()
|
||||||
if rec["type"] == "data":
|
print()
|
||||||
addr = rec["load_addr"]
|
print('=== Done ===')
|
||||||
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()
|
|
||||||
|
|||||||
736
tools/eeprom_flash_a0.py
Normal file
736
tools/eeprom_flash_a0.py
Normal file
@ -0,0 +1,736 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
EEPROM flash via host-side I2C orchestration (boot ROM 0xA0).
|
||||||
|
|
||||||
|
Writes C2 firmware images to the SkyWalker-1 EEPROM by driving the
|
||||||
|
FX2LP's I2C controller from the host while the CPU is halted.
|
||||||
|
|
||||||
|
Background:
|
||||||
|
- Stock firmware I2C proxy (0x83/0x84) returns pipe errors
|
||||||
|
- Custom firmware via RAM: I2CS BERR (0xF6) on CPUCS restart
|
||||||
|
- Pre-halt I2C flush (0x90) + halt → I2CS = 0x01 (clean/idle)
|
||||||
|
- Boot ROM 0xA0 can read/write I2C registers at 0xE678-0xE67A
|
||||||
|
- We drive the I2C controller from the host, one register write at
|
||||||
|
a time, to program the EEPROM
|
||||||
|
|
||||||
|
Strategy:
|
||||||
|
1. Pre-halt flush: GET_SIGNAL_LOCK (0x90) finishes stock firmware I2C
|
||||||
|
2. Halt CPU: CPUCS = 0x01 (I2C controller stays idle)
|
||||||
|
3. Write I2C registers via 0xA0 to orchestrate EEPROM write
|
||||||
|
4. Power-cycle to boot from new EEPROM image
|
||||||
|
"""
|
||||||
|
import usb.core, usb.util, sys, time, os, subprocess, argparse
|
||||||
|
|
||||||
|
# USB IDs
|
||||||
|
SKYWALKER_VID = 0x09C0
|
||||||
|
SKYWALKER_PID = 0x0203
|
||||||
|
|
||||||
|
# FX2LP register addresses (XDATA space)
|
||||||
|
CPUCS_ADDR = 0xE600
|
||||||
|
I2CS_ADDR = 0xE678
|
||||||
|
I2DAT_ADDR = 0xE679
|
||||||
|
I2CTL_ADDR = 0xE67A
|
||||||
|
|
||||||
|
# I2CS bit masks
|
||||||
|
bmSTART = 0x80
|
||||||
|
bmSTOP = 0x40
|
||||||
|
bmLASTRD = 0x20
|
||||||
|
bmID1 = 0x10
|
||||||
|
bmID0 = 0x08
|
||||||
|
bmBERR = 0x04
|
||||||
|
bmACK = 0x02
|
||||||
|
bmDONE = 0x01
|
||||||
|
|
||||||
|
# EEPROM parameters (24C128)
|
||||||
|
EEPROM_I2C_ADDR = 0x51 # 7-bit address
|
||||||
|
EEPROM_PAGE_SIZE = 64 # bytes per page write
|
||||||
|
EEPROM_SIZE = 16384 # 16KB total
|
||||||
|
EEPROM_WRITE_MS = 5 # max internal write cycle time
|
||||||
|
|
||||||
|
# Boot ROM vendor request
|
||||||
|
A0_REQUEST = 0xA0
|
||||||
|
|
||||||
|
|
||||||
|
def i2cs_str(val):
|
||||||
|
"""Human-readable I2CS register decode."""
|
||||||
|
flags = []
|
||||||
|
if val & bmSTART: flags.append('START')
|
||||||
|
if val & bmSTOP: flags.append('STOP')
|
||||||
|
if val & bmLASTRD: flags.append('LASTRD')
|
||||||
|
if val & bmBERR: flags.append('BERR')
|
||||||
|
if val & bmACK: flags.append('ACK')
|
||||||
|
if val & bmDONE: flags.append('DONE')
|
||||||
|
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 'clear'}] state={state}"
|
||||||
|
|
||||||
|
|
||||||
|
def find_device():
|
||||||
|
"""Find SkyWalker-1 on USB."""
|
||||||
|
dev = usb.core.find(idVendor=SKYWALKER_VID, idProduct=SKYWALKER_PID)
|
||||||
|
if dev is None:
|
||||||
|
print("ERROR: SkyWalker-1 not found on USB")
|
||||||
|
sys.exit(1)
|
||||||
|
print(f" Device: Bus {dev.bus} Addr {dev.address}")
|
||||||
|
return dev
|
||||||
|
|
||||||
|
|
||||||
|
def detach_driver(dev):
|
||||||
|
"""Detach kernel driver if attached."""
|
||||||
|
for cfg in dev:
|
||||||
|
for intf in cfg:
|
||||||
|
if dev.is_kernel_driver_active(intf.bInterfaceNumber):
|
||||||
|
try:
|
||||||
|
dev.detach_kernel_driver(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
|
||||||
|
|
||||||
|
|
||||||
|
# ── FX2 register access via 0xA0 ─────────────────────────────────
|
||||||
|
|
||||||
|
def a0_read(dev, addr, length=1):
|
||||||
|
"""Read from FX2 XDATA address space via boot ROM 0xA0."""
|
||||||
|
return bytes(dev.ctrl_transfer(
|
||||||
|
usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN,
|
||||||
|
A0_REQUEST, addr, 0, length, 2000))
|
||||||
|
|
||||||
|
|
||||||
|
def a0_write(dev, addr, data):
|
||||||
|
"""Write to FX2 XDATA address space via boot ROM 0xA0."""
|
||||||
|
if isinstance(data, int):
|
||||||
|
data = bytes([data])
|
||||||
|
dev.ctrl_transfer(
|
||||||
|
usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_OUT,
|
||||||
|
A0_REQUEST, addr, 0, data, 2000)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Host-side I2C primitives ─────────────────────────────────────
|
||||||
|
|
||||||
|
def i2c_read_status(dev):
|
||||||
|
"""Read I2CS register."""
|
||||||
|
return a0_read(dev, I2CS_ADDR, 1)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def i2c_wait_done(dev, timeout_ms=50):
|
||||||
|
"""Poll I2CS for DONE bit. Returns (success, i2cs_value)."""
|
||||||
|
deadline = time.time() + timeout_ms / 1000.0
|
||||||
|
while time.time() < deadline:
|
||||||
|
val = i2c_read_status(dev)
|
||||||
|
if val & bmDONE:
|
||||||
|
return True, val
|
||||||
|
if val & bmBERR:
|
||||||
|
return False, val
|
||||||
|
time.sleep(0.001) # 1ms between polls
|
||||||
|
return False, val
|
||||||
|
|
||||||
|
|
||||||
|
def i2c_start(dev):
|
||||||
|
"""Assert I2C START condition."""
|
||||||
|
a0_write(dev, I2CS_ADDR, bmSTART)
|
||||||
|
|
||||||
|
|
||||||
|
def i2c_stop(dev):
|
||||||
|
"""Assert I2C STOP condition and wait for completion."""
|
||||||
|
a0_write(dev, I2CS_ADDR, bmSTOP)
|
||||||
|
deadline = time.time() + 0.050
|
||||||
|
while time.time() < deadline:
|
||||||
|
val = i2c_read_status(dev)
|
||||||
|
if not (val & bmSTOP):
|
||||||
|
return True, val
|
||||||
|
time.sleep(0.001)
|
||||||
|
return False, val
|
||||||
|
|
||||||
|
|
||||||
|
def i2c_write_byte(dev, byte_val):
|
||||||
|
"""Write a byte to I2DAT, wait for DONE. Returns (ack, i2cs)."""
|
||||||
|
a0_write(dev, I2DAT_ADDR, byte_val)
|
||||||
|
ok, status = i2c_wait_done(dev)
|
||||||
|
if not ok:
|
||||||
|
return False, status
|
||||||
|
return bool(status & bmACK), status
|
||||||
|
|
||||||
|
|
||||||
|
def i2c_read_byte(dev, last=False):
|
||||||
|
"""Read a byte from I2DAT. Set last=True for NACK (last byte)."""
|
||||||
|
if last:
|
||||||
|
a0_write(dev, I2CS_ADDR, bmLASTRD)
|
||||||
|
# Reading I2DAT triggers the next SCL clock cycle
|
||||||
|
val = a0_read(dev, I2DAT_ADDR, 1)[0]
|
||||||
|
ok, status = i2c_wait_done(dev)
|
||||||
|
return val, ok, status
|
||||||
|
|
||||||
|
|
||||||
|
# ── Higher-level I2C operations ──────────────────────────────────
|
||||||
|
|
||||||
|
def i2c_probe(dev, addr_7bit):
|
||||||
|
"""Probe an I2C device. Returns True if it ACKs its address."""
|
||||||
|
i2c_start(dev)
|
||||||
|
ack, status = i2c_write_byte(dev, addr_7bit << 1)
|
||||||
|
i2c_stop(dev)
|
||||||
|
return ack
|
||||||
|
|
||||||
|
|
||||||
|
def eeprom_write_page(dev, mem_addr, data):
|
||||||
|
"""Write up to EEPROM_PAGE_SIZE bytes to EEPROM at mem_addr.
|
||||||
|
|
||||||
|
EEPROM protocol: START + slave_W + addr_H + addr_L + data... + STOP
|
||||||
|
Then wait for internal write cycle (ACK polling or fixed delay).
|
||||||
|
"""
|
||||||
|
if len(data) > EEPROM_PAGE_SIZE:
|
||||||
|
raise ValueError(f"Page write max {EEPROM_PAGE_SIZE} bytes, got {len(data)}")
|
||||||
|
|
||||||
|
addr_h = (mem_addr >> 8) & 0xFF
|
||||||
|
addr_l = mem_addr & 0xFF
|
||||||
|
|
||||||
|
# START
|
||||||
|
i2c_start(dev)
|
||||||
|
|
||||||
|
# Slave address (write)
|
||||||
|
ack, status = i2c_write_byte(dev, EEPROM_I2C_ADDR << 1)
|
||||||
|
if not ack:
|
||||||
|
i2c_stop(dev)
|
||||||
|
return False, "no ACK on slave address"
|
||||||
|
|
||||||
|
# Memory address high byte
|
||||||
|
ack, status = i2c_write_byte(dev, addr_h)
|
||||||
|
if not ack:
|
||||||
|
i2c_stop(dev)
|
||||||
|
return False, "no ACK on addr_H"
|
||||||
|
|
||||||
|
# Memory address low byte
|
||||||
|
ack, status = i2c_write_byte(dev, addr_l)
|
||||||
|
if not ack:
|
||||||
|
i2c_stop(dev)
|
||||||
|
return False, "no ACK on addr_L"
|
||||||
|
|
||||||
|
# Data bytes
|
||||||
|
for i, byte_val in enumerate(data):
|
||||||
|
ack, status = i2c_write_byte(dev, byte_val)
|
||||||
|
if not ack:
|
||||||
|
i2c_stop(dev)
|
||||||
|
return False, f"no ACK on data byte {i} (0x{byte_val:02X})"
|
||||||
|
|
||||||
|
# STOP (initiates EEPROM internal write cycle)
|
||||||
|
i2c_stop(dev)
|
||||||
|
|
||||||
|
# Wait for write cycle via ACK polling
|
||||||
|
# The EEPROM NACKs its address during the write cycle, then ACKs
|
||||||
|
# when the cycle completes. Timeout after 20ms.
|
||||||
|
deadline = time.time() + 0.020
|
||||||
|
while time.time() < deadline:
|
||||||
|
time.sleep(0.001)
|
||||||
|
i2c_start(dev)
|
||||||
|
ack, status = i2c_write_byte(dev, EEPROM_I2C_ADDR << 1)
|
||||||
|
if ack:
|
||||||
|
i2c_stop(dev)
|
||||||
|
return True, "ok"
|
||||||
|
i2c_stop(dev)
|
||||||
|
|
||||||
|
return False, "write cycle timeout (no ACK after 20ms)"
|
||||||
|
|
||||||
|
|
||||||
|
def eeprom_read_bytes(dev, mem_addr, length):
|
||||||
|
"""Read bytes from EEPROM.
|
||||||
|
|
||||||
|
Protocol: START + slave_W + addr_H + addr_L +
|
||||||
|
rSTART + slave_R + data[0] ... data[n-1] + NACK + STOP
|
||||||
|
"""
|
||||||
|
addr_h = (mem_addr >> 8) & 0xFF
|
||||||
|
addr_l = mem_addr & 0xFF
|
||||||
|
|
||||||
|
# Write phase: set address pointer
|
||||||
|
i2c_start(dev)
|
||||||
|
|
||||||
|
ack, status = i2c_write_byte(dev, EEPROM_I2C_ADDR << 1)
|
||||||
|
if not ack:
|
||||||
|
i2c_stop(dev)
|
||||||
|
return None, "no ACK on slave address (write phase)"
|
||||||
|
|
||||||
|
ack, status = i2c_write_byte(dev, addr_h)
|
||||||
|
if not ack:
|
||||||
|
i2c_stop(dev)
|
||||||
|
return None, "no ACK on addr_H"
|
||||||
|
|
||||||
|
ack, status = i2c_write_byte(dev, addr_l)
|
||||||
|
if not ack:
|
||||||
|
i2c_stop(dev)
|
||||||
|
return None, "no ACK on addr_L"
|
||||||
|
|
||||||
|
# Read phase: repeated START + slave_R
|
||||||
|
i2c_start(dev)
|
||||||
|
|
||||||
|
ack, status = i2c_write_byte(dev, (EEPROM_I2C_ADDR << 1) | 1)
|
||||||
|
if not ack:
|
||||||
|
i2c_stop(dev)
|
||||||
|
return None, "no ACK on slave address (read phase)"
|
||||||
|
|
||||||
|
# Read data bytes
|
||||||
|
result = bytearray()
|
||||||
|
for i in range(length):
|
||||||
|
is_last = (i == length - 1)
|
||||||
|
val, ok, status = i2c_read_byte(dev, last=is_last)
|
||||||
|
if not ok:
|
||||||
|
i2c_stop(dev)
|
||||||
|
return bytes(result), f"read failed at byte {i}"
|
||||||
|
result.append(val)
|
||||||
|
|
||||||
|
# Dummy read to complete the last byte cycle
|
||||||
|
_ = a0_read(dev, I2DAT_ADDR, 1)
|
||||||
|
|
||||||
|
# STOP
|
||||||
|
i2c_stop(dev)
|
||||||
|
|
||||||
|
return bytes(result), "ok"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Device preparation ───────────────────────────────────────────
|
||||||
|
|
||||||
|
def prepare_device(dev, verbose=False):
|
||||||
|
"""Pre-halt flush, halt CPU, verify I2C controller is clean.
|
||||||
|
|
||||||
|
Returns True if I2C controller is ready for host-side operations.
|
||||||
|
"""
|
||||||
|
print("\n Phase 1: Prepare I2C controller")
|
||||||
|
print(" " + "─" * 40)
|
||||||
|
|
||||||
|
# Step 1: Pre-halt I2C flush
|
||||||
|
print(" Sending GET_SIGNAL_LOCK (0x90) to flush I2C...")
|
||||||
|
try:
|
||||||
|
lock = dev.ctrl_transfer(
|
||||||
|
usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN,
|
||||||
|
0x90, 0, 0, 1, 2000)
|
||||||
|
print(f" Response: 0x{lock[0]:02X}")
|
||||||
|
except usb.core.USBError as e:
|
||||||
|
print(f" 0x90 failed: {e}")
|
||||||
|
# Try alternative flush commands
|
||||||
|
try:
|
||||||
|
cfg = dev.ctrl_transfer(
|
||||||
|
usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN,
|
||||||
|
0x80, 0, 0, 1, 2000)
|
||||||
|
print(f" Fallback 0x80 response: 0x{cfg[0]:02X}")
|
||||||
|
except usb.core.USBError:
|
||||||
|
print(" No flush commands worked — proceeding anyway")
|
||||||
|
|
||||||
|
# Step 2: Halt CPU
|
||||||
|
print(" Halting CPU (CPUCS = 0x01)...")
|
||||||
|
a0_write(dev, CPUCS_ADDR, 0x01)
|
||||||
|
time.sleep(0.010)
|
||||||
|
|
||||||
|
cpucs = a0_read(dev, CPUCS_ADDR, 1)[0]
|
||||||
|
if not (cpucs & 0x01):
|
||||||
|
print(f" CPUCS readback: 0x{cpucs:02X} — halt may have failed")
|
||||||
|
return False
|
||||||
|
print(f" CPU halted (CPUCS = 0x{cpucs:02X})")
|
||||||
|
|
||||||
|
# Step 3: Read I2C controller state
|
||||||
|
i2cs = i2c_read_status(dev)
|
||||||
|
i2ctl = a0_read(dev, I2CTL_ADDR, 1)[0]
|
||||||
|
print(f" I2CS: {i2cs_str(i2cs)}")
|
||||||
|
print(f" I2CTL: 0x{i2ctl:02X} ({'400kHz' if i2ctl & 1 else '100kHz'})")
|
||||||
|
|
||||||
|
if i2cs & bmBERR:
|
||||||
|
print(" BERR is set — I2C controller is stuck")
|
||||||
|
print(" This usually means the CPU was restarted after halt")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Step 4: Set I2C speed to 400kHz
|
||||||
|
if not (i2ctl & 0x01):
|
||||||
|
print(" Setting I2CTL = 0x01 (400kHz)...")
|
||||||
|
a0_write(dev, I2CTL_ADDR, 0x01)
|
||||||
|
i2ctl2 = a0_read(dev, I2CTL_ADDR, 1)[0]
|
||||||
|
print(f" I2CTL readback: 0x{i2ctl2:02X}")
|
||||||
|
|
||||||
|
# Step 5: Quick I2C bus probe
|
||||||
|
print("\n Phase 2: I2C bus probe")
|
||||||
|
print(" " + "─" * 40)
|
||||||
|
|
||||||
|
addrs_to_probe = [
|
||||||
|
(0x51, "EEPROM (24C128)"),
|
||||||
|
(0x50, "EEPROM alt addr"),
|
||||||
|
(0x08, "BCM4500 demod"),
|
||||||
|
(0x10, "BCM3440 tuner"),
|
||||||
|
]
|
||||||
|
|
||||||
|
found_eeprom = False
|
||||||
|
for addr, name in addrs_to_probe:
|
||||||
|
ack = i2c_probe(dev, addr)
|
||||||
|
status_after = i2c_read_status(dev)
|
||||||
|
result = "ACK" if ack else "NACK"
|
||||||
|
marker = " <--" if ack and addr in (0x50, 0x51) else ""
|
||||||
|
print(f" 0x{addr:02X} {name:20s}: {result:4s} "
|
||||||
|
f"(I2CS={i2cs_str(status_after)}){marker}")
|
||||||
|
if ack and addr in (0x50, 0x51):
|
||||||
|
found_eeprom = True
|
||||||
|
|
||||||
|
# Check for BERR after each probe
|
||||||
|
if status_after & bmBERR:
|
||||||
|
print(f" BERR after probe — host-side I2C may not work")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
# Read a few bytes from EEPROM to verify reads work
|
||||||
|
if found_eeprom:
|
||||||
|
print("\n Phase 2b: EEPROM read test")
|
||||||
|
print(" " + "─" * 40)
|
||||||
|
data, msg = eeprom_read_bytes(dev, 0x0000, 8)
|
||||||
|
if data:
|
||||||
|
print(f" EEPROM[0x0000..0x0007]: {data.hex(' ')}")
|
||||||
|
if data[0] == 0xC2:
|
||||||
|
print(f" First byte is 0xC2 — valid C2 boot header!")
|
||||||
|
else:
|
||||||
|
print(f" First byte is 0x{data[0]:02X} — not a C2 header")
|
||||||
|
else:
|
||||||
|
print(f" Read failed: {msg}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not found_eeprom:
|
||||||
|
print(" No EEPROM found at 0x50 or 0x51")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"\n I2C controller ready for EEPROM operations")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# ── Subcommands ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def cmd_probe(args):
|
||||||
|
"""Test host-side I2C by probing the bus."""
|
||||||
|
print("SkyWalker-1 Host-Side I2C Probe (0xA0)")
|
||||||
|
print("=" * 45)
|
||||||
|
|
||||||
|
dev = find_device()
|
||||||
|
detach_driver(dev)
|
||||||
|
|
||||||
|
ok = prepare_device(dev, verbose=True)
|
||||||
|
if not ok:
|
||||||
|
print("\n FAIL: Host-side I2C does not work")
|
||||||
|
print(" The 0xA0 vendor request may not trigger I2C hardware,")
|
||||||
|
print(" or the I2C bus is in a bad state.")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print("\n SUCCESS: Host-side I2C is working!")
|
||||||
|
print(" EEPROM detected and readable via boot ROM 0xA0")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_read(args):
|
||||||
|
"""Read EEPROM contents via host-side I2C."""
|
||||||
|
size = args.size
|
||||||
|
print("SkyWalker-1 EEPROM Read (host-side I2C)")
|
||||||
|
print("=" * 45)
|
||||||
|
|
||||||
|
dev = find_device()
|
||||||
|
detach_driver(dev)
|
||||||
|
|
||||||
|
ok = prepare_device(dev, verbose=False)
|
||||||
|
if not ok:
|
||||||
|
print("\n FAIL: Cannot prepare I2C")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"\n Reading {size} bytes from EEPROM...")
|
||||||
|
chunk_size = 32 # read in small chunks (each byte = 2+ USB transfers)
|
||||||
|
data = bytearray()
|
||||||
|
errors = 0
|
||||||
|
|
||||||
|
for offset in range(0, size, chunk_size):
|
||||||
|
remaining = min(chunk_size, size - offset)
|
||||||
|
chunk, msg = eeprom_read_bytes(dev, offset, remaining)
|
||||||
|
if chunk is None:
|
||||||
|
print(f"\n Read failed at 0x{offset:04X}: {msg}")
|
||||||
|
errors += 1
|
||||||
|
data.extend(b'\xff' * remaining)
|
||||||
|
else:
|
||||||
|
data.extend(chunk)
|
||||||
|
|
||||||
|
if offset % 256 == 0 or offset + remaining >= size:
|
||||||
|
pct = (offset + remaining) * 100 // size
|
||||||
|
print(f"\r Read: 0x{offset + remaining:04X}/{size:04X} [{pct:3d}%]",
|
||||||
|
end="", flush=True)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
print(f"\n {errors} read error(s)")
|
||||||
|
|
||||||
|
# Hex dump
|
||||||
|
if args.hex_dump:
|
||||||
|
print(f"\n EEPROM contents (first {min(256, len(data))} bytes):")
|
||||||
|
for i in range(0, min(256, 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" {i:04X}: {hex_part:<48s} {ascii_part}")
|
||||||
|
|
||||||
|
# Check C2 header
|
||||||
|
if len(data) >= 8 and data[0] == 0xC2:
|
||||||
|
vid = data[2] << 8 | data[1]
|
||||||
|
pid = data[4] << 8 | data[3]
|
||||||
|
print(f"\n C2 header: VID=0x{vid:04X} PID=0x{pid:04X} "
|
||||||
|
f"CONFIG=0x{data[7]:02X}")
|
||||||
|
|
||||||
|
if args.output:
|
||||||
|
with open(args.output, 'wb') as f:
|
||||||
|
f.write(data)
|
||||||
|
print(f"\n Saved to: {args.output}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_write(args):
|
||||||
|
"""Write a C2 firmware image to EEPROM via host-side I2C."""
|
||||||
|
image_path = args.file
|
||||||
|
if not os.path.exists(image_path):
|
||||||
|
print(f"File not found: {image_path}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
with open(image_path, 'rb') as f:
|
||||||
|
image = f.read()
|
||||||
|
|
||||||
|
print("SkyWalker-1 EEPROM Flash (host-side I2C)")
|
||||||
|
print("=" * 45)
|
||||||
|
|
||||||
|
# Validate image
|
||||||
|
if len(image) < 8 or image[0] != 0xC2:
|
||||||
|
print(f" Not a C2 image (first byte: 0x{image[0]:02X})")
|
||||||
|
sys.exit(1)
|
||||||
|
if len(image) > EEPROM_SIZE:
|
||||||
|
print(f" Image too large: {len(image)} > {EEPROM_SIZE}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
vid = image[2] << 8 | image[1]
|
||||||
|
pid = image[4] << 8 | image[3]
|
||||||
|
config = image[7]
|
||||||
|
print(f" Image: {image_path}")
|
||||||
|
print(f" Size: {len(image)} bytes ({len(image)*100//EEPROM_SIZE}% of EEPROM)")
|
||||||
|
print(f" VID: 0x{vid:04X} PID: 0x{pid:04X} CONFIG: 0x{config:02X}")
|
||||||
|
|
||||||
|
dev = find_device()
|
||||||
|
detach_driver(dev)
|
||||||
|
|
||||||
|
ok = prepare_device(dev, verbose=True)
|
||||||
|
if not ok:
|
||||||
|
print("\n FAIL: Cannot prepare I2C — aborting")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Backup current EEPROM first (just the header to be safe)
|
||||||
|
if not args.no_backup:
|
||||||
|
print(f"\n Backing up EEPROM header...")
|
||||||
|
hdr, msg = eeprom_read_bytes(dev, 0x0000, 8)
|
||||||
|
if hdr:
|
||||||
|
print(f" Current header: {hdr.hex(' ')}")
|
||||||
|
if hdr[0] == 0xC2:
|
||||||
|
cur_vid = hdr[2] << 8 | hdr[1]
|
||||||
|
cur_pid = hdr[4] << 8 | hdr[3]
|
||||||
|
print(f" Current: VID=0x{cur_vid:04X} PID=0x{cur_pid:04X}")
|
||||||
|
else:
|
||||||
|
print(f" Header read failed: {msg}")
|
||||||
|
if not args.force:
|
||||||
|
print(" Use --force to proceed without backup verification")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Dry run?
|
||||||
|
if args.dry_run:
|
||||||
|
pages = (len(image) + EEPROM_PAGE_SIZE - 1) // EEPROM_PAGE_SIZE
|
||||||
|
print(f"\n DRY RUN: would write {len(image)} bytes in {pages} pages")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Write confirmation
|
||||||
|
print(f"\n *** WRITING {len(image)} BYTES TO EEPROM ***")
|
||||||
|
print(f" This replaces the boot firmware. A bad write = bricked device.")
|
||||||
|
print(f" Press Ctrl+C within 3 seconds to abort.")
|
||||||
|
try:
|
||||||
|
for i in range(3, 0, -1):
|
||||||
|
print(f"\r Starting in {i}... ", end="", flush=True)
|
||||||
|
time.sleep(1)
|
||||||
|
print("\r Writing now... ")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n Aborted.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Write image in page-sized chunks
|
||||||
|
total_pages = (len(image) + EEPROM_PAGE_SIZE - 1) // EEPROM_PAGE_SIZE
|
||||||
|
write_errors = 0
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
for page_num in range(total_pages):
|
||||||
|
offset = page_num * EEPROM_PAGE_SIZE
|
||||||
|
end = min(offset + EEPROM_PAGE_SIZE, len(image))
|
||||||
|
chunk = image[offset:end]
|
||||||
|
|
||||||
|
pct = (page_num + 1) * 100 // total_pages
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
rate = (offset + len(chunk)) / elapsed if elapsed > 0 else 0
|
||||||
|
eta = (len(image) - offset - len(chunk)) / rate if rate > 0 else 0
|
||||||
|
print(f"\r Write: 0x{offset:04X}/0x{len(image):04X} "
|
||||||
|
f"[{pct:3d}%] {rate:.0f} B/s ETA {eta:.0f}s ",
|
||||||
|
end="", flush=True)
|
||||||
|
|
||||||
|
ok, msg = eeprom_write_page(dev, offset, chunk)
|
||||||
|
if not ok:
|
||||||
|
print(f"\n Write error at 0x{offset:04X}: {msg}")
|
||||||
|
write_errors += 1
|
||||||
|
if write_errors >= 3:
|
||||||
|
print("\n Too many errors — aborting")
|
||||||
|
print(" *** EEPROM STATE UNKNOWN ***")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
print(f"\r Write: 0x{len(image):04X}/0x{len(image):04X} "
|
||||||
|
f"[100%] done in {elapsed:.1f}s ")
|
||||||
|
|
||||||
|
if write_errors:
|
||||||
|
print(f"\n WARNING: {write_errors} write error(s)")
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
print(f"\n Verifying ({len(image)} bytes)...")
|
||||||
|
verify_chunk_size = 32
|
||||||
|
mismatches = 0
|
||||||
|
first_mismatch = None
|
||||||
|
|
||||||
|
for offset in range(0, len(image), verify_chunk_size):
|
||||||
|
remaining = min(verify_chunk_size, len(image) - offset)
|
||||||
|
chunk, msg = eeprom_read_bytes(dev, offset, remaining)
|
||||||
|
if chunk is None:
|
||||||
|
print(f"\n Verify read failed at 0x{offset:04X}: {msg}")
|
||||||
|
mismatches += remaining
|
||||||
|
continue
|
||||||
|
|
||||||
|
for i, (expected, got) in enumerate(zip(image[offset:offset+remaining], chunk)):
|
||||||
|
if expected != got:
|
||||||
|
if first_mismatch is None:
|
||||||
|
first_mismatch = offset + i
|
||||||
|
mismatches += 1
|
||||||
|
if mismatches <= 8:
|
||||||
|
print(f"\n Mismatch at 0x{offset+i:04X}: "
|
||||||
|
f"wrote 0x{expected:02X} read 0x{got:02X}")
|
||||||
|
|
||||||
|
if offset % 256 == 0 or offset + remaining >= len(image):
|
||||||
|
pct = (offset + remaining) * 100 // len(image)
|
||||||
|
print(f"\r Verify: 0x{offset+remaining:04X}/0x{len(image):04X} "
|
||||||
|
f"[{pct:3d}%]", end="", flush=True)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
if mismatches == 0:
|
||||||
|
print(f"\n VERIFIED: all {len(image)} bytes match")
|
||||||
|
print(f" Flash complete in {elapsed:.1f}s")
|
||||||
|
print(f"\n Power-cycle the device to boot the new firmware.")
|
||||||
|
if args.power_cycle:
|
||||||
|
do_power_cycle()
|
||||||
|
else:
|
||||||
|
print(f"\n VERIFY FAILED: {mismatches} byte(s) differ "
|
||||||
|
f"(first at 0x{first_mismatch:04X})")
|
||||||
|
if mismatches > 8:
|
||||||
|
print(f" ({mismatches - 8} additional mismatches not shown)")
|
||||||
|
print(f"\n *** DO NOT POWER CYCLE ***")
|
||||||
|
print(f" Re-run this tool to retry, or use an external programmer.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def do_power_cycle():
|
||||||
|
"""Power-cycle SkyWalker-1 via uhubctl."""
|
||||||
|
print(f"\n Power-cycling via uhubctl...")
|
||||||
|
try:
|
||||||
|
# Off
|
||||||
|
r = subprocess.run(
|
||||||
|
['sudo', 'uhubctl', '-l', '1-5.4.4', '-p', '3', '-a', 'off'],
|
||||||
|
capture_output=True, text=True, timeout=10)
|
||||||
|
if r.returncode != 0:
|
||||||
|
print(f" uhubctl off failed: {r.stderr.strip()}")
|
||||||
|
print(f" Manually unplug and replug the device.")
|
||||||
|
return
|
||||||
|
print(f" Port 3 powered off")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# On
|
||||||
|
r = subprocess.run(
|
||||||
|
['sudo', 'uhubctl', '-l', '1-5.4.4', '-p', '3', '-a', 'on'],
|
||||||
|
capture_output=True, text=True, timeout=10)
|
||||||
|
if r.returncode != 0:
|
||||||
|
print(f" uhubctl on failed: {r.stderr.strip()}")
|
||||||
|
return
|
||||||
|
print(f" Port 3 powered on")
|
||||||
|
print(f" Waiting for device to enumerate...")
|
||||||
|
|
||||||
|
time.sleep(3)
|
||||||
|
dev = usb.core.find(idVendor=SKYWALKER_VID, idProduct=SKYWALKER_PID)
|
||||||
|
if dev:
|
||||||
|
print(f" Device found: Bus {dev.bus} Addr {dev.address}")
|
||||||
|
print(f" New firmware is running!")
|
||||||
|
else:
|
||||||
|
print(f" Device not found yet — may need a few more seconds")
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f" uhubctl not found — manually power-cycle the device")
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
print(f" uhubctl timed out")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_power_cycle(args):
|
||||||
|
"""Power-cycle the SkyWalker-1 via uhubctl."""
|
||||||
|
print("SkyWalker-1 Power Cycle")
|
||||||
|
print("=" * 45)
|
||||||
|
do_power_cycle()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="SkyWalker-1 EEPROM flash via host-side I2C (boot ROM 0xA0)",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""\
|
||||||
|
This tool drives the FX2LP's I2C controller from the host while the
|
||||||
|
CPU is halted. It works around the stock firmware's broken I2C proxy
|
||||||
|
and the BERR bug triggered by CPUCS restart.
|
||||||
|
|
||||||
|
examples:
|
||||||
|
%(prog)s probe # test if host-side I2C works
|
||||||
|
%(prog)s read -o backup.bin # backup current EEPROM
|
||||||
|
%(prog)s write firmware_eeprom.bin # flash new C2 image
|
||||||
|
%(prog)s write firmware_eeprom.bin -P # flash + auto power-cycle
|
||||||
|
%(prog)s power-cycle # just power-cycle the device
|
||||||
|
""")
|
||||||
|
sub = parser.add_subparsers(dest='command', required=True)
|
||||||
|
|
||||||
|
# probe
|
||||||
|
sub.add_parser('probe', help='Test host-side I2C bus access')
|
||||||
|
|
||||||
|
# read
|
||||||
|
p_read = sub.add_parser('read', help='Read EEPROM contents')
|
||||||
|
p_read.add_argument('-o', '--output', help='Save to file')
|
||||||
|
p_read.add_argument('--size', type=int, default=EEPROM_SIZE,
|
||||||
|
help=f'Bytes to read (default: {EEPROM_SIZE})')
|
||||||
|
p_read.add_argument('--hex', dest='hex_dump', action='store_true',
|
||||||
|
help='Show hex dump')
|
||||||
|
|
||||||
|
# write
|
||||||
|
p_write = sub.add_parser('write', help='Flash C2 image to EEPROM')
|
||||||
|
p_write.add_argument('file', help='C2 firmware image (.bin)')
|
||||||
|
p_write.add_argument('--dry-run', action='store_true',
|
||||||
|
help='Show what would happen without writing')
|
||||||
|
p_write.add_argument('--no-backup', action='store_true',
|
||||||
|
help='Skip header backup check')
|
||||||
|
p_write.add_argument('--force', action='store_true',
|
||||||
|
help='Continue despite warnings')
|
||||||
|
p_write.add_argument('-P', '--power-cycle', action='store_true',
|
||||||
|
help='Auto power-cycle after successful flash')
|
||||||
|
|
||||||
|
# power-cycle
|
||||||
|
sub.add_parser('power-cycle', help='Power-cycle via uhubctl')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
dispatch = {
|
||||||
|
'probe': cmd_probe,
|
||||||
|
'read': cmd_read,
|
||||||
|
'write': cmd_write,
|
||||||
|
'power-cycle': cmd_power_cycle,
|
||||||
|
}
|
||||||
|
dispatch[args.command](args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
108
tools/eeprom_pll_find.py
Normal file
108
tools/eeprom_pll_find.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Find the exact EEPROM address where PLL config starts.
|
||||||
|
|
||||||
|
The stock firmware's FUN_CODE_10F2 presumably finds the PLL data by
|
||||||
|
computing an offset from the C2 boot firmware structure. This script
|
||||||
|
parses the C2 records to find where the firmware ends, then checks
|
||||||
|
if PLL data immediately follows.
|
||||||
|
"""
|
||||||
|
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):
|
||||||
|
return sw._vendor_in(CMD_EEPROM_READ, value=addr, index=length, length=length)
|
||||||
|
|
||||||
|
|
||||||
|
print('=== C2 Firmware Structure Analysis ===')
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Read C2 header (8 bytes)
|
||||||
|
header = eeprom_read(0x0000, 8)
|
||||||
|
print(f'Header: {header.hex(" ")}')
|
||||||
|
assert header[0] == 0xC2, 'Not a C2 EEPROM!'
|
||||||
|
vid = header[2] << 8 | header[1]
|
||||||
|
pid = header[4] << 8 | header[3]
|
||||||
|
print(f' VID: 0x{vid:04X} PID: 0x{pid:04X}')
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Parse C2 load records
|
||||||
|
offset = 8
|
||||||
|
records = []
|
||||||
|
print('C2 Load Records:')
|
||||||
|
while offset < 0x8000:
|
||||||
|
hdr = eeprom_read(offset, 4)
|
||||||
|
rec_len = (hdr[0] << 8) | hdr[1]
|
||||||
|
rec_addr = (hdr[2] << 8) | hdr[3]
|
||||||
|
|
||||||
|
if rec_len == 0x8001:
|
||||||
|
print(f' [{len(records)}] END MARKER at EEPROM 0x{offset:04X} → entry=0x{rec_addr:04X}')
|
||||||
|
records.append({'type': 'end', 'offset': offset, 'entry': rec_addr})
|
||||||
|
offset += 4
|
||||||
|
break
|
||||||
|
elif rec_len == 0 or rec_len > 0x4000:
|
||||||
|
print(f' [{len(records)}] INVALID at EEPROM 0x{offset:04X}: len=0x{rec_len:04X}')
|
||||||
|
records.append({'type': 'invalid', 'offset': offset})
|
||||||
|
offset += 4
|
||||||
|
break
|
||||||
|
|
||||||
|
end_addr = rec_addr + rec_len - 1
|
||||||
|
print(f' [{len(records)}] {rec_len:5d} bytes at EEPROM 0x{offset:04X} → RAM 0x{rec_addr:04X}-0x{end_addr:04X}')
|
||||||
|
records.append({'type': 'data', 'offset': offset, 'len': rec_len, 'addr': rec_addr})
|
||||||
|
offset += 4 + rec_len
|
||||||
|
|
||||||
|
print(f'\nFirmware ends at EEPROM offset: 0x{offset:04X}')
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Check what's right after the firmware
|
||||||
|
print(f'--- Data immediately after firmware (0x{offset:04X}) ---')
|
||||||
|
for addr in range(offset, offset + 128, 16):
|
||||||
|
data = eeprom_read(addr, 16)
|
||||||
|
hex_str = ' '.join(f'{b:02X}' for b in data)
|
||||||
|
print(f' {addr:04X}: {hex_str}')
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Now check: does PLL data start right after the C2 firmware?
|
||||||
|
print(f'--- PLL block check starting at 0x{offset:04X} ---')
|
||||||
|
for addr in range(offset, offset + 200, 20):
|
||||||
|
block = eeprom_read(addr, 20)
|
||||||
|
count = block[0]
|
||||||
|
if count == 0:
|
||||||
|
print(f' 0x{addr:04X}: [sentinel count=0]')
|
||||||
|
break
|
||||||
|
elif 1 <= count <= 16:
|
||||||
|
ab = block[4:4 + count]
|
||||||
|
print(f' 0x{addr:04X}: count={count} A9=0x{block[1]:02X} AA=0x{block[2]:02X} '
|
||||||
|
f'unused=0x{block[3]:02X} AB=[{ab.hex(" ")}]')
|
||||||
|
else:
|
||||||
|
print(f' 0x{addr:04X}: count=0x{count:02X} (invalid, not PLL data)')
|
||||||
|
break
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Also check the known PLL location from the scan
|
||||||
|
print('--- Confirmed PLL data at 0x125C (from EEPROM scan) ---')
|
||||||
|
for addr in range(0x125C, 0x12C0, 20):
|
||||||
|
block = eeprom_read(addr, 20)
|
||||||
|
count = block[0]
|
||||||
|
if count == 0:
|
||||||
|
print(f' 0x{addr:04X}: [sentinel count=0]')
|
||||||
|
break
|
||||||
|
elif 1 <= count <= 16:
|
||||||
|
ab = block[4:4 + count]
|
||||||
|
print(f' 0x{addr:04X}: count={count} A9=0x{block[1]:02X} AA=0x{block[2]:02X} '
|
||||||
|
f'unused=0x{block[3]:02X} AB=[{ab.hex(" ")}]')
|
||||||
|
else:
|
||||||
|
print(f' 0x{addr:04X}: NOT PLL (count=0x{count:02X})')
|
||||||
|
break
|
||||||
|
|
||||||
|
sw.close()
|
||||||
|
print()
|
||||||
|
print('=== Done ===')
|
||||||
77
tools/eeprom_sentinel_scan.py
Normal file
77
tools/eeprom_sentinel_scan.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Find the count=0 sentinel in the BCM4500 firmware data at 0x4000+."""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, 'tools')
|
||||||
|
from skywalker_lib import SkyWalker1
|
||||||
|
|
||||||
|
CMD_EEPROM_READ = 0xC0
|
||||||
|
sw = SkyWalker1()
|
||||||
|
sw.open()
|
||||||
|
|
||||||
|
|
||||||
|
def ee(addr, length):
|
||||||
|
return sw._vendor_in(CMD_EEPROM_READ, value=addr, index=length, length=length)
|
||||||
|
|
||||||
|
|
||||||
|
print('=== Scanning for count=0 sentinel from 0x4000 ===')
|
||||||
|
print()
|
||||||
|
|
||||||
|
blocks = 0
|
||||||
|
total_bytes = 0
|
||||||
|
addr = 0x4000
|
||||||
|
|
||||||
|
while addr < 0x8000:
|
||||||
|
data = ee(addr, 20)
|
||||||
|
count = data[0]
|
||||||
|
|
||||||
|
if count == 0:
|
||||||
|
print(f' SENTINEL FOUND at 0x{addr:04X} after {blocks} blocks ({total_bytes} payload bytes)')
|
||||||
|
break
|
||||||
|
|
||||||
|
if count > 16:
|
||||||
|
print(f' INVALID count=0x{count:02X} at 0x{addr:04X} after {blocks} blocks')
|
||||||
|
# Show context
|
||||||
|
for ctx_addr in range(max(0x4000, addr - 60), addr + 60, 20):
|
||||||
|
d = ee(ctx_addr, 20)
|
||||||
|
marker = ' ← INVALID' if ctx_addr == addr else ''
|
||||||
|
print(f' 0x{ctx_addr:04X}: count={d[0]:3d} A9=0x{d[1]:02X} AA=0x{d[2]:02X}{marker}')
|
||||||
|
break
|
||||||
|
|
||||||
|
# Valid block
|
||||||
|
a9 = data[1]
|
||||||
|
aa = data[2]
|
||||||
|
ab_bytes = count
|
||||||
|
total_bytes += ab_bytes
|
||||||
|
|
||||||
|
if blocks < 5 or blocks % 100 == 0:
|
||||||
|
ab = data[4:4 + min(count, 8)]
|
||||||
|
print(f' Block {blocks:4d} @ 0x{addr:04X}: count={count:2d} A9=0x{a9:02X} AA=0x{aa:02X} '
|
||||||
|
f'AB=[{ab.hex(" ")}{"..." if count > 8 else ""}]')
|
||||||
|
|
||||||
|
blocks += 1
|
||||||
|
addr += 20
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f' NO SENTINEL found before 0x8000 ({blocks} blocks scanned)')
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f'Summary: {blocks} blocks, {total_bytes} payload bytes')
|
||||||
|
print(f'Address range: 0x4000 - 0x{addr:04X}')
|
||||||
|
|
||||||
|
# Show the sentinel and what follows
|
||||||
|
if addr < 0x8000:
|
||||||
|
print()
|
||||||
|
print(f'--- Data around sentinel at 0x{addr:04X} ---')
|
||||||
|
for a in range(max(0x4000, addr - 40), addr + 60, 20):
|
||||||
|
d = ee(a, 20)
|
||||||
|
cnt = d[0]
|
||||||
|
if cnt == 0:
|
||||||
|
print(f' 0x{a:04X}: [SENTINEL count=0] rest: {d[1:].hex(" ")}')
|
||||||
|
elif 1 <= cnt <= 16:
|
||||||
|
print(f' 0x{a:04X}: count={cnt} A9=0x{d[1]:02X} AA=0x{d[2]:02X}')
|
||||||
|
else:
|
||||||
|
print(f' 0x{a:04X}: [non-PLL: 0x{cnt:02X}] {d[:16].hex(" ")}')
|
||||||
|
|
||||||
|
sw.close()
|
||||||
|
print()
|
||||||
|
print('=== Done ===')
|
||||||
@ -33,6 +33,132 @@ WRITE_CYCLE_MS = 10 # Max internal write cycle time per page
|
|||||||
MAX_EEPROM_SIZE = 16384 # 128Kbit / 16KB max addressable via wIndex
|
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():
|
def find_device():
|
||||||
dev = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID)
|
dev = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID)
|
||||||
if dev is None:
|
if dev is None:
|
||||||
@ -353,6 +479,72 @@ def cmd_verify(args):
|
|||||||
print("\nNote: run 'sudo modprobe dvb_usb_gp8psk' to reload")
|
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):
|
def cmd_flash(args):
|
||||||
"""Write a C2-format .bin file to the EEPROM."""
|
"""Write a C2-format .bin file to the EEPROM."""
|
||||||
if not os.path.exists(args.file):
|
if not os.path.exists(args.file):
|
||||||
@ -548,6 +740,21 @@ def main():
|
|||||||
help='Compare .bin file against EEPROM')
|
help='Compare .bin file against EEPROM')
|
||||||
p_verify.add_argument('file', help='C2 firmware image (.bin)')
|
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: <basename>_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
|
# flash
|
||||||
p_flash = sub.add_parser('flash',
|
p_flash = sub.add_parser('flash',
|
||||||
help='Write C2 firmware image to EEPROM')
|
help='Write C2 firmware image to EEPROM')
|
||||||
@ -561,12 +768,19 @@ def main():
|
|||||||
|
|
||||||
args = parser.parse_args()
|
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':
|
if args.command == 'info':
|
||||||
cmd_info(args)
|
cmd_info(args)
|
||||||
elif args.command == 'backup':
|
elif args.command == 'backup':
|
||||||
cmd_backup(args)
|
cmd_backup(args)
|
||||||
elif args.command == 'verify':
|
elif args.command == 'verify':
|
||||||
cmd_verify(args)
|
cmd_verify(args)
|
||||||
|
elif args.command == 'convert':
|
||||||
|
cmd_convert(args)
|
||||||
elif args.command == 'flash':
|
elif args.command == 'flash':
|
||||||
cmd_flash(args)
|
cmd_flash(args)
|
||||||
|
|
||||||
|
|||||||
368
tools/fw_load.py
368
tools/fw_load.py
@ -316,6 +316,312 @@ def write_segments(dev, segments, verbose=False):
|
|||||||
return total
|
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 --
|
# -- Subcommand handlers --
|
||||||
|
|
||||||
def cmd_load(args):
|
def cmd_load(args):
|
||||||
@ -372,9 +678,56 @@ def cmd_load(args):
|
|||||||
try:
|
try:
|
||||||
# Step 1: Halt CPU
|
# Step 1: Halt CPU
|
||||||
if not args.no_reset:
|
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)...")
|
print("\n[1/3] Halting CPU (CPUCS = 0x01)...")
|
||||||
cpu_halt(dev)
|
cpu_halt(dev)
|
||||||
time.sleep(0.05)
|
time.sleep(0.01) # minimal delay — I2C should be idle from flush
|
||||||
|
|
||||||
# Verify halt
|
# Verify halt
|
||||||
readback = fx2_ram_read(dev, CPUCS_ADDR, 1)
|
readback = fx2_ram_read(dev, CPUCS_ADDR, 1)
|
||||||
@ -387,7 +740,15 @@ def cmd_load(args):
|
|||||||
else:
|
else:
|
||||||
print("\n[1/3] Skipping CPU reset (--no-reset)")
|
print("\n[1/3] Skipping CPU reset (--no-reset)")
|
||||||
|
|
||||||
# Step 2: Load segments
|
# 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"
|
step = "2/3" if not args.no_reset else "2/2"
|
||||||
print(f"\n[{step}] Loading {len(segments)} segment(s) into RAM...")
|
print(f"\n[{step}] Loading {len(segments)} segment(s) into RAM...")
|
||||||
written = write_segments(dev, segments, verbose=args.verbose)
|
written = write_segments(dev, segments, verbose=args.verbose)
|
||||||
@ -563,6 +924,9 @@ Power-cycle the device to restore the factory-programmed firmware.
|
|||||||
help="Show detailed transfer progress")
|
help="Show detailed transfer progress")
|
||||||
p_load.add_argument('--force', action='store_true',
|
p_load.add_argument('--force', action='store_true',
|
||||||
help="Allow loading to unknown VID/PID devices")
|
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
|
# reset
|
||||||
p_reset = sub.add_parser('reset',
|
p_reset = sub.add_parser('reset',
|
||||||
|
|||||||
196
tools/i2c_host_test.py
Normal file
196
tools/i2c_host_test.py
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Test I2C communication from host side while FX2LP CPU is halted.
|
||||||
|
|
||||||
|
When the CPU is halted (CPUCS=1), I2CS=0x0A (clean state). When the CPU
|
||||||
|
runs (CPUCS=0), I2CS=0xF6 (stuck). This script tests whether the I2C
|
||||||
|
controller is functional during CPU halt by attempting to read the boot
|
||||||
|
EEPROM header (which MUST contain 0xC0 or 0xC2).
|
||||||
|
|
||||||
|
Uses USB vendor command 0xA0 to read/write XDATA registers directly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import usb.core
|
||||||
|
import usb.util
|
||||||
|
|
||||||
|
# XDATA register addresses
|
||||||
|
I2CS_ADDR = 0xE678
|
||||||
|
I2DAT_ADDR = 0xE679
|
||||||
|
I2CTL_ADDR = 0xE67A
|
||||||
|
CPUCS_ADDR = 0xE600
|
||||||
|
|
||||||
|
# I2CS bit masks
|
||||||
|
bmSTART = 0x80
|
||||||
|
bmSTOP = 0x40
|
||||||
|
bmLASTRD = 0x20
|
||||||
|
bmBERR = 0x04
|
||||||
|
bmACK = 0x02
|
||||||
|
bmDONE = 0x01
|
||||||
|
|
||||||
|
EEPROM_ADDR_W = 0xA2 # 0x51 << 1 | 0 (write)
|
||||||
|
EEPROM_ADDR_R = 0xA3 # 0x51 << 1 | 1 (read)
|
||||||
|
|
||||||
|
def fx2_read(dev, addr, length=1):
|
||||||
|
"""Read XDATA register(s) via USB vendor command 0xA0."""
|
||||||
|
return dev.ctrl_transfer(0xC0, 0xA0, addr, 0, length, timeout=1000)
|
||||||
|
|
||||||
|
def fx2_write(dev, addr, data):
|
||||||
|
"""Write XDATA register(s) via USB vendor command 0xA0."""
|
||||||
|
dev.ctrl_transfer(0x40, 0xA0, addr, 0, data, timeout=1000)
|
||||||
|
|
||||||
|
def i2cs_str(val):
|
||||||
|
"""Decode I2CS register value."""
|
||||||
|
flags = []
|
||||||
|
if val & 0x80: flags.append('START')
|
||||||
|
if val & 0x40: flags.append('STOP')
|
||||||
|
if val & 0x20: flags.append('LASTRD')
|
||||||
|
if val & 0x04: flags.append('BERR')
|
||||||
|
if val & 0x02: flags.append('ACK')
|
||||||
|
if val & 0x01: flags.append('DONE')
|
||||||
|
return f"0x{val:02X} ({' | '.join(flags) if flags else 'idle'})"
|
||||||
|
|
||||||
|
def wait_done(dev, timeout_ms=100):
|
||||||
|
"""Poll I2CS for DONE bit."""
|
||||||
|
deadline = time.monotonic() + timeout_ms / 1000.0
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
i2cs = fx2_read(dev, I2CS_ADDR, 1)[0]
|
||||||
|
if i2cs & bmDONE:
|
||||||
|
return True, i2cs
|
||||||
|
time.sleep(0.001)
|
||||||
|
return False, i2cs
|
||||||
|
|
||||||
|
def main():
|
||||||
|
dev = usb.core.find(idVendor=0x09C0, idProduct=0x0203)
|
||||||
|
if not dev:
|
||||||
|
print("SkyWalker-1 not found")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("SkyWalker-1 I2C Host-Side Test")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Step 1: Halt CPU
|
||||||
|
print("\n[1] Halting CPU (CPUCS=1)...")
|
||||||
|
fx2_write(dev, CPUCS_ADDR, bytes([0x01]))
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
# Step 2: Read I2C state
|
||||||
|
i2cs = fx2_read(dev, I2CS_ADDR, 1)[0]
|
||||||
|
i2ctl = fx2_read(dev, I2CTL_ADDR, 1)[0]
|
||||||
|
print(f" I2CS = {i2cs_str(i2cs)}")
|
||||||
|
print(f" I2CTL = 0x{i2ctl:02X}")
|
||||||
|
|
||||||
|
if i2cs == 0xF6:
|
||||||
|
print(" WARNING: I2CS stuck at 0xF6 even during CPU halt!")
|
||||||
|
print(" I2C test will likely fail.")
|
||||||
|
|
||||||
|
# Step 3: Try to write I2CTL and read back
|
||||||
|
print("\n[2] Testing register writability...")
|
||||||
|
fx2_write(dev, I2CTL_ADDR, bytes([0x01])) # 400kHz
|
||||||
|
time.sleep(0.01)
|
||||||
|
i2ctl_rb = fx2_read(dev, I2CTL_ADDR, 1)[0]
|
||||||
|
print(f" Wrote I2CTL=0x01, read back 0x{i2ctl_rb:02X}", end="")
|
||||||
|
if i2ctl_rb == 0x01:
|
||||||
|
print(" (write works!)")
|
||||||
|
else:
|
||||||
|
print(f" (write IGNORED — register is read-only during halt)")
|
||||||
|
|
||||||
|
# Step 4: Try hardware I2C — read EEPROM header byte at address 0x0000
|
||||||
|
print("\n[3] Attempting EEPROM read via hardware I2C...")
|
||||||
|
print(" Target: EEPROM 0x51, address 0x0000 (boot header)")
|
||||||
|
|
||||||
|
# Issue START
|
||||||
|
print("\n [START] Writing I2CS = 0x80...")
|
||||||
|
fx2_write(dev, I2CS_ADDR, bytes([bmSTART]))
|
||||||
|
done, i2cs = wait_done(dev, 200)
|
||||||
|
print(f" I2CS = {i2cs_str(i2cs)}, DONE={'YES' if done else 'NO'}")
|
||||||
|
|
||||||
|
if not done:
|
||||||
|
print(" START didn't complete. Trying alternative: write I2DAT first...")
|
||||||
|
# Some controllers need I2DAT written before START can proceed
|
||||||
|
# Write EEPROM address (0xA2 = 0x51 write)
|
||||||
|
fx2_write(dev, I2DAT_ADDR, bytes([EEPROM_ADDR_W]))
|
||||||
|
time.sleep(0.01)
|
||||||
|
i2cs = fx2_read(dev, I2CS_ADDR, 1)[0]
|
||||||
|
print(f" After I2DAT=0xA2: I2CS = {i2cs_str(i2cs)}")
|
||||||
|
|
||||||
|
if not done:
|
||||||
|
# Try the Cypress-documented sequence: START is issued by
|
||||||
|
# writing the slave address to I2DAT AFTER writing START to I2CS
|
||||||
|
print("\n Trying standard Cypress I2C sequence...")
|
||||||
|
# Re-issue START
|
||||||
|
fx2_write(dev, I2CS_ADDR, bytes([bmSTART]))
|
||||||
|
time.sleep(0.001)
|
||||||
|
# Write slave address — this should clock the address byte
|
||||||
|
fx2_write(dev, I2DAT_ADDR, bytes([EEPROM_ADDR_W]))
|
||||||
|
done, i2cs = wait_done(dev, 200)
|
||||||
|
print(f" After START + I2DAT=0xA2: I2CS = {i2cs_str(i2cs)}, DONE={'YES' if done else 'NO'}")
|
||||||
|
|
||||||
|
if done:
|
||||||
|
ack = 'ACK' if (i2cs & bmACK) else 'NAK'
|
||||||
|
print(f" Address phase: {ack}")
|
||||||
|
|
||||||
|
if done:
|
||||||
|
# Write EEPROM address bytes (16-bit address: 0x0000)
|
||||||
|
print("\n Writing address 0x0000...")
|
||||||
|
fx2_write(dev, I2DAT_ADDR, bytes([0x00])) # addr high
|
||||||
|
done, i2cs = wait_done(dev, 200)
|
||||||
|
ack = 'ACK' if (i2cs & bmACK) else 'NAK'
|
||||||
|
print(f" Addr high: {ack}, DONE={'YES' if done else 'NO'}")
|
||||||
|
|
||||||
|
if done:
|
||||||
|
fx2_write(dev, I2DAT_ADDR, bytes([0x00])) # addr low
|
||||||
|
done, i2cs = wait_done(dev, 200)
|
||||||
|
ack = 'ACK' if (i2cs & bmACK) else 'NAK'
|
||||||
|
print(f" Addr low: {ack}, DONE={'YES' if done else 'NO'}")
|
||||||
|
|
||||||
|
if done:
|
||||||
|
# Re-START for read phase
|
||||||
|
print("\n Re-START for read phase...")
|
||||||
|
fx2_write(dev, I2CS_ADDR, bytes([bmSTART]))
|
||||||
|
time.sleep(0.001)
|
||||||
|
fx2_write(dev, I2DAT_ADDR, bytes([EEPROM_ADDR_R]))
|
||||||
|
done, i2cs = wait_done(dev, 200)
|
||||||
|
ack = 'ACK' if (i2cs & bmACK) else 'NAK'
|
||||||
|
print(f" Read addr: {ack}, DONE={'YES' if done else 'NO'}")
|
||||||
|
|
||||||
|
if done:
|
||||||
|
# Read first byte (and only byte — set LASTRD + STOP)
|
||||||
|
print("\n Reading first byte (LASTRD + STOP)...")
|
||||||
|
fx2_write(dev, I2CS_ADDR, bytes([bmLASTRD]))
|
||||||
|
time.sleep(0.001)
|
||||||
|
# Dummy read to trigger byte transfer
|
||||||
|
dummy = fx2_read(dev, I2DAT_ADDR, 1)[0]
|
||||||
|
done, i2cs = wait_done(dev, 200)
|
||||||
|
if done:
|
||||||
|
data = fx2_read(dev, I2DAT_ADDR, 1)[0]
|
||||||
|
fx2_write(dev, I2CS_ADDR, bytes([bmSTOP]))
|
||||||
|
print(f" Boot header byte: 0x{data:02X}", end="")
|
||||||
|
if data == 0xC0:
|
||||||
|
print(" (C0 = no renumerate)")
|
||||||
|
elif data == 0xC2:
|
||||||
|
print(" (C2 = renumerate)")
|
||||||
|
else:
|
||||||
|
print(f" (unexpected!)")
|
||||||
|
print("\n *** EEPROM READ SUCCESSFUL! ***")
|
||||||
|
else:
|
||||||
|
print(f" Read DONE timeout. I2CS = {i2cs_str(i2cs)}")
|
||||||
|
|
||||||
|
# Final state
|
||||||
|
i2cs_final = fx2_read(dev, I2CS_ADDR, 1)[0]
|
||||||
|
print(f"\n[4] Final I2CS = {i2cs_str(i2cs_final)}")
|
||||||
|
|
||||||
|
# Try STOP to clean up
|
||||||
|
fx2_write(dev, I2CS_ADDR, bytes([bmSTOP]))
|
||||||
|
time.sleep(0.01)
|
||||||
|
i2cs_stop = fx2_read(dev, I2CS_ADDR, 1)[0]
|
||||||
|
print(f" After STOP: I2CS = {i2cs_str(i2cs_stop)}")
|
||||||
|
|
||||||
|
# Release CPU
|
||||||
|
print("\n[5] Releasing CPU (CPUCS=0)...")
|
||||||
|
fx2_write(dev, CPUCS_ADDR, bytes([0x00]))
|
||||||
|
time.sleep(0.5)
|
||||||
|
print(" CPU released. Device will re-enumerate.")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
259
tools/i2c_recovery_boot.py
Normal file
259
tools/i2c_recovery_boot.py
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Two-stage boot with I2C slave recovery.
|
||||||
|
|
||||||
|
The FX2LP I2C controller enters a stuck state (I2CS=0xF6) when the CPU
|
||||||
|
restarts after being halted mid-I2C-transaction. The stock firmware was
|
||||||
|
interrupted by fw_load.py's CPUCS halt while an I2C transfer was in
|
||||||
|
progress. The slave (BCM4500/BCM3440) is still driving SDA LOW, waiting
|
||||||
|
for clock pulses to finish its byte.
|
||||||
|
|
||||||
|
When CPUCS goes back to 0:
|
||||||
|
1. I2C pull-ups reconnect
|
||||||
|
2. Controller detects SDA LOW (slave still holding)
|
||||||
|
3. Controller enters permanent BERR state (0xF6)
|
||||||
|
4. All subsequent I2C operations fail
|
||||||
|
|
||||||
|
Fix: two-stage boot process:
|
||||||
|
Stage 1: Upload a tiny slave recovery stub, run it briefly to release
|
||||||
|
the stuck slave (32 SCL pulses @ ~10us + STOP), then halt again.
|
||||||
|
Stage 2: Upload the real firmware. The I2C bus is now clean, so when
|
||||||
|
CPUCS goes to 0, the controller initializes correctly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import usb.core
|
||||||
|
import usb.util
|
||||||
|
|
||||||
|
I2CS_ADDR = 0xE678
|
||||||
|
I2CTL_ADDR = 0xE67A
|
||||||
|
CPUCS_ADDR = 0xE600
|
||||||
|
|
||||||
|
def fx2_read(dev, addr, n=1):
|
||||||
|
return dev.ctrl_transfer(0xC0, 0xA0, addr, 0, n, timeout=1000)
|
||||||
|
|
||||||
|
def fx2_write(dev, addr, data):
|
||||||
|
dev.ctrl_transfer(0x40, 0xA0, addr, 0, data, timeout=1000)
|
||||||
|
|
||||||
|
def i2cs_str(v):
|
||||||
|
flags = []
|
||||||
|
for bit, name in [(7,'START'),(6,'STOP'),(5,'LASTRD'),
|
||||||
|
(4,'ID1'),(3,'ID0'),(2,'BERR'),(1,'ACK'),(0,'DONE')]:
|
||||||
|
if v & (1 << bit): flags.append(name)
|
||||||
|
return f"0x{v:02X} ({' | '.join(flags) if flags else 'idle'})"
|
||||||
|
|
||||||
|
def build_recovery_stub():
|
||||||
|
"""Build an 8051 stub that holds BCM4500 in reset to release the I2C bus.
|
||||||
|
|
||||||
|
The I2C controller (I2CS=0xF6 BERR) has exclusive control of PA0 (SDA)
|
||||||
|
and PA1 (SCL). GPIO operations on those pins are OVERRIDDEN by the I2C
|
||||||
|
engine — bit-bang SCL clocking has NO EFFECT on the actual bus.
|
||||||
|
|
||||||
|
Strategy: assert BCM4500 RESET (PA5 LOW) and KEEP it asserted. The
|
||||||
|
BCM4500's I2C interface goes high-impedance, releasing SDA. Then when
|
||||||
|
the host halts and loads real firmware, CPUCS restart will find SDA=HIGH
|
||||||
|
and the I2C controller will initialize cleanly (no BERR).
|
||||||
|
|
||||||
|
The real firmware handles BCM_RESET de-assertion during its normal init.
|
||||||
|
|
||||||
|
CRITICAL: Do NOT modify OEA bits 0-1 — the I2C controller owns those.
|
||||||
|
|
||||||
|
Diagnostics in XDATA:
|
||||||
|
0x3C00: 0xAA = stub started
|
||||||
|
0x3C01: initial OEA
|
||||||
|
0x3C02: initial IOA
|
||||||
|
0x3C03: I2CS at boot (via XDATA 0xE678)
|
||||||
|
0x3C04: IOA after asserting BCM4500 reset (~50ms hold)
|
||||||
|
0x3C05: 0xDD = stub complete, looping with BCM4500 held in reset
|
||||||
|
"""
|
||||||
|
code = []
|
||||||
|
|
||||||
|
# 0x0000: LJMP main_start (to 0x0010)
|
||||||
|
code += [0x02, 0x00, 0x10] # LJMP 0x0010
|
||||||
|
|
||||||
|
# 0x0003-0x000F: padding
|
||||||
|
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
|
||||||
|
|
||||||
|
# --- Capture initial OEA → 0x3C01 ---
|
||||||
|
code += [0xE5, 0xB2] # MOV A, OEA
|
||||||
|
code += [0x90, 0x3C, 0x01] # MOV DPTR, #0x3C01
|
||||||
|
code += [0xF0] # MOVX @DPTR, A
|
||||||
|
|
||||||
|
# --- Capture initial IOA → 0x3C02 ---
|
||||||
|
code += [0xE5, 0x80] # MOV A, IOA
|
||||||
|
code += [0x90, 0x3C, 0x02] # MOV DPTR, #0x3C02
|
||||||
|
code += [0xF0] # MOVX @DPTR, A
|
||||||
|
|
||||||
|
# --- Capture initial I2CS → 0x3C03 ---
|
||||||
|
code += [0x90, 0xE6, 0x78] # MOV DPTR, #0xE678
|
||||||
|
code += [0xE0] # MOVX A, @DPTR
|
||||||
|
code += [0x90, 0x3C, 0x03] # MOV DPTR, #0x3C03
|
||||||
|
code += [0xF0] # MOVX @DPTR, A
|
||||||
|
|
||||||
|
# --- Assert BCM4500 RESET: PA5 LOW, make output ---
|
||||||
|
# PA5 = IOA bit 5, bit address 0x85 (IOA base 0x80 + 5)
|
||||||
|
# ORL OEA with only bit 5 — do NOT touch bits 0-1 (I2C engine)
|
||||||
|
code += [0xC2, 0x85] # CLR PA5 (assert reset)
|
||||||
|
code += [0x43, 0xB2, 0x20] # ORL OEA, #0x20 (PA5 = output)
|
||||||
|
|
||||||
|
# Hold reset for ~50ms
|
||||||
|
# R2=250, R1=240: 250*240*3 = 180,000 cycles = ~3.75ms
|
||||||
|
# R0=15: 15 * 3.75ms = ~56ms
|
||||||
|
code += [0x78, 15] # MOV R0, #15
|
||||||
|
reset_outer = len(code)
|
||||||
|
code += [0x7A, 250] # MOV R2, #250
|
||||||
|
reset_mid = len(code)
|
||||||
|
code += [0x79, 240] # MOV R1, #240
|
||||||
|
code += [0xD9, 0xFE] # DJNZ R1, $-2
|
||||||
|
djnz2_pc = len(code) + 2
|
||||||
|
code += [0xDA, (reset_mid - djnz2_pc) & 0xFF] # DJNZ R2, reset_mid
|
||||||
|
djnz0_pc = len(code) + 2
|
||||||
|
code += [0xD8, (reset_outer - djnz0_pc) & 0xFF] # DJNZ R0, reset_outer
|
||||||
|
|
||||||
|
# --- Capture IOA during reset → 0x3C04 ---
|
||||||
|
code += [0xE5, 0x80] # MOV A, IOA
|
||||||
|
code += [0x90, 0x3C, 0x04] # MOV DPTR, #0x3C04
|
||||||
|
code += [0xF0] # MOVX @DPTR, A
|
||||||
|
|
||||||
|
# --- Marker: stub complete (0xDD → 0x3C05) ---
|
||||||
|
# BCM4500 stays in reset! Do NOT de-assert.
|
||||||
|
code += [0x74, 0xDD] # MOV A, #0xDD
|
||||||
|
code += [0x90, 0x3C, 0x05] # MOV DPTR, #0x3C05
|
||||||
|
code += [0xF0] # MOVX @DPTR, A
|
||||||
|
|
||||||
|
# Infinite loop — BCM4500 held in reset
|
||||||
|
code += [0x80, 0xFE] # SJMP $ (loop forever)
|
||||||
|
|
||||||
|
return bytes(code)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
dev = usb.core.find(idVendor=0x09C0, idProduct=0x0203)
|
||||||
|
if not dev:
|
||||||
|
print("SkyWalker-1 not found")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("Two-Stage Boot: I2C Slave Recovery (v2 — with markers)")
|
||||||
|
print("=" * 55)
|
||||||
|
|
||||||
|
# Stage 0: Initial state
|
||||||
|
print(f"\n[0] Device found: Bus {dev.bus} Addr {dev.address}")
|
||||||
|
|
||||||
|
# Stage 1: Halt CPU
|
||||||
|
print("\n[1] Halting CPU...")
|
||||||
|
fx2_write(dev, CPUCS_ADDR, bytes([0x01]))
|
||||||
|
time.sleep(0.05)
|
||||||
|
i2cs = fx2_read(dev, I2CS_ADDR, 1)[0]
|
||||||
|
print(f" I2CS during halt = {i2cs_str(i2cs)}")
|
||||||
|
|
||||||
|
# Clear XDATA diagnostic area first (so stale data doesn't confuse us)
|
||||||
|
print(" Clearing XDATA 0x3C00-0x3C0F...")
|
||||||
|
fx2_write(dev, 0x3C00, bytes([0x00] * 16))
|
||||||
|
|
||||||
|
# Stage 2: Upload stub
|
||||||
|
print("\n[2] Uploading slave recovery stub...")
|
||||||
|
stub = build_recovery_stub()
|
||||||
|
print(f" Stub size: {len(stub)} bytes")
|
||||||
|
fx2_write(dev, 0x0000, stub)
|
||||||
|
|
||||||
|
# Verify upload by reading back first 16 bytes
|
||||||
|
readback = fx2_read(dev, 0x0000, 16)
|
||||||
|
match = all(readback[i] == stub[i] for i in range(16))
|
||||||
|
print(f" Upload verify (first 16): {'MATCH' if match else 'MISMATCH'}")
|
||||||
|
if not match:
|
||||||
|
print(f" Expected: {' '.join(f'{b:02X}' for b in stub[:16])}")
|
||||||
|
print(f" Got: {' '.join(f'{b:02X}' for b in readback)}")
|
||||||
|
|
||||||
|
# Stage 3: Run stub (~50ms reset hold + ~100ms settle + margin)
|
||||||
|
print("\n[3] Running stub (500ms)...")
|
||||||
|
fx2_write(dev, CPUCS_ADDR, bytes([0x00]))
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# Stage 4: Halt and read diagnostics
|
||||||
|
print("\n[4] Halting CPU after recovery...")
|
||||||
|
fx2_write(dev, CPUCS_ADDR, bytes([0x01]))
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
# Read all diagnostic bytes
|
||||||
|
diag = fx2_read(dev, 0x3C00, 8)
|
||||||
|
i2cs_halt = fx2_read(dev, I2CS_ADDR, 1)[0]
|
||||||
|
|
||||||
|
print(f"\n Raw XDATA 0x3C00-0x3C07:")
|
||||||
|
print(f" {' '.join(f'{b:02X}' for b in diag)}")
|
||||||
|
|
||||||
|
# Decode diagnostics
|
||||||
|
marker_start = diag[0] # 0x3C00: expect 0xAA
|
||||||
|
oea_init = diag[1] # 0x3C01: initial OEA
|
||||||
|
ioa_init = diag[2] # 0x3C02: initial IOA
|
||||||
|
i2cs_init = diag[3] # 0x3C03: initial I2CS
|
||||||
|
ioa_during_rst = diag[4] # 0x3C04: IOA during BCM4500 reset
|
||||||
|
marker_done = diag[5] # 0x3C05: expect 0xDD
|
||||||
|
|
||||||
|
def bus_str(ioa):
|
||||||
|
sda = 'H' if ioa & 1 else 'L'
|
||||||
|
scl = 'H' if ioa & 2 else 'L'
|
||||||
|
rst = 'H' if ioa & 0x20 else 'L'
|
||||||
|
return f"SDA={sda} SCL={scl} RST={rst}"
|
||||||
|
|
||||||
|
print(f"\n Markers:")
|
||||||
|
print(f" Start (0xAA?): 0x{marker_start:02X} {'✓' if marker_start == 0xAA else '✗ STUB DID NOT EXECUTE'}")
|
||||||
|
print(f" Done (0xDD?): 0x{marker_done:02X} {'✓' if marker_done == 0xDD else '✗'}")
|
||||||
|
|
||||||
|
if marker_start != 0xAA:
|
||||||
|
print(f"\n FATAL: Stub never started!")
|
||||||
|
else:
|
||||||
|
print(f"\n Initial state:")
|
||||||
|
print(f" OEA = 0x{oea_init:02X}")
|
||||||
|
print(f" IOA = 0x{ioa_init:02X} {bus_str(ioa_init)}")
|
||||||
|
print(f" I2CS = {i2cs_str(i2cs_init)}")
|
||||||
|
|
||||||
|
print(f"\n BCM4500 held in RESET (~50ms):")
|
||||||
|
print(f" IOA = 0x{ioa_during_rst:02X} {bus_str(ioa_during_rst)}")
|
||||||
|
sda_rst = 'H' if ioa_during_rst & 1 else 'L'
|
||||||
|
if sda_rst == 'H':
|
||||||
|
print(f" ✓ SDA HIGH with BCM4500 in reset")
|
||||||
|
else:
|
||||||
|
print(f" ✗ SDA still LOW — not the BCM4500 holding it")
|
||||||
|
|
||||||
|
print(f"\n I2CS during halt: {i2cs_str(i2cs_halt)}")
|
||||||
|
print(f" BCM4500 is held in RESET (PA5 driven LOW)")
|
||||||
|
|
||||||
|
# Stage 5: Now load real firmware with BCM4500 still in reset
|
||||||
|
# The real firmware's init sequence will de-assert BCM_RESET.
|
||||||
|
# Since SDA is HIGH (slave in reset), the I2C controller should
|
||||||
|
# initialize cleanly when CPUCS restarts for the real firmware.
|
||||||
|
print(f"\n[5] BCM4500 held in reset. Loading real firmware...")
|
||||||
|
print(f" The CPUCS restart for firmware load will re-init I2C controller.")
|
||||||
|
print(f" With BCM4500 in reset (SDA=HIGH), I2CS should come up clean.")
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
fw_path = os.path.join(os.path.dirname(__file__), '..', 'firmware', 'build', 'skywalker1.ihx')
|
||||||
|
fw_path = os.path.abspath(fw_path)
|
||||||
|
fw_load = os.path.join(os.path.dirname(__file__), 'fw_load.py')
|
||||||
|
|
||||||
|
if os.path.exists(fw_path):
|
||||||
|
print(f"\n Running: python3 {fw_load} load {fw_path} --wait 5")
|
||||||
|
result = subprocess.run(
|
||||||
|
['python3', fw_load, 'load', fw_path, '--wait', '5'],
|
||||||
|
capture_output=True, text=True, timeout=30
|
||||||
|
)
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(f" stderr: {result.stderr}")
|
||||||
|
else:
|
||||||
|
print(f"\n Firmware not found: {fw_path}")
|
||||||
|
print(f" Build with: cd firmware && make")
|
||||||
|
print(f" Then run: python3 tools/fw_load.py load {fw_path} --wait 5")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
126
tools/i2c_register_test.py
Normal file
126
tools/i2c_register_test.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Quick diagnostic: test if 0xA0 writes to I2C registers trigger hardware."""
|
||||||
|
import usb.core, usb.util, time
|
||||||
|
|
||||||
|
VID, PID = 0x09C0, 0x0203
|
||||||
|
A0 = 0xA0
|
||||||
|
I2CS = 0xE678; I2DAT = 0xE679; I2CTL = 0xE67A; CPUCS = 0xE600
|
||||||
|
|
||||||
|
dev = usb.core.find(idVendor=VID, idProduct=PID)
|
||||||
|
for cfg in dev:
|
||||||
|
for intf in cfg:
|
||||||
|
if dev.is_kernel_driver_active(intf.bInterfaceNumber):
|
||||||
|
dev.detach_kernel_driver(intf.bInterfaceNumber)
|
||||||
|
try:
|
||||||
|
dev.set_configuration()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def a0r(addr, n=1):
|
||||||
|
return bytes(dev.ctrl_transfer(
|
||||||
|
usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, A0, addr, 0, n, 2000))
|
||||||
|
|
||||||
|
def a0w(addr, val):
|
||||||
|
dev.ctrl_transfer(
|
||||||
|
usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_OUT, A0, addr, 0, bytes([val]), 2000)
|
||||||
|
|
||||||
|
# Pre-halt flush
|
||||||
|
try:
|
||||||
|
lock = dev.ctrl_transfer(
|
||||||
|
usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, 0x90, 0, 0, 1, 2000)
|
||||||
|
print(f"Pre-halt flush (0x90): 0x{lock[0]:02X}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Flush failed: {e}")
|
||||||
|
|
||||||
|
# Halt
|
||||||
|
a0w(CPUCS, 0x01)
|
||||||
|
time.sleep(0.01)
|
||||||
|
print(f"CPUCS after halt: 0x{a0r(CPUCS)[0]:02X}")
|
||||||
|
|
||||||
|
print(f"\n=== Register State After Halt ===")
|
||||||
|
i2cs_orig = a0r(I2CS)[0]
|
||||||
|
i2ctl_orig = a0r(I2CTL)[0]
|
||||||
|
print(f"I2CS: 0x{i2cs_orig:02X}")
|
||||||
|
print(f"I2CTL: 0x{i2ctl_orig:02X}")
|
||||||
|
|
||||||
|
# Test 1: I2CTL writability
|
||||||
|
print(f"\n=== Test 1: I2CTL writability ===")
|
||||||
|
print(f"I2CTL before: 0x{a0r(I2CTL)[0]:02X}")
|
||||||
|
a0w(I2CTL, 0x00)
|
||||||
|
time.sleep(0.001)
|
||||||
|
v = a0r(I2CTL)[0]
|
||||||
|
print(f"I2CTL after 0x00 write: 0x{v:02X} {'CHANGED' if v == 0x00 else 'unchanged'}")
|
||||||
|
a0w(I2CTL, 0x01)
|
||||||
|
time.sleep(0.001)
|
||||||
|
v = a0r(I2CTL)[0]
|
||||||
|
print(f"I2CTL after 0x01 write: 0x{v:02X}")
|
||||||
|
|
||||||
|
# Test 2: Write START to I2CS, read back
|
||||||
|
print(f"\n=== Test 2: I2CS START ===")
|
||||||
|
print(f"I2CS before START: 0x{a0r(I2CS)[0]:02X}")
|
||||||
|
a0w(I2CS, 0x80) # bmSTART
|
||||||
|
for i in range(5):
|
||||||
|
val = a0r(I2CS)[0]
|
||||||
|
print(f" I2CS read {i}: 0x{val:02X}")
|
||||||
|
time.sleep(0.002)
|
||||||
|
|
||||||
|
# Test 3: Write to I2DAT
|
||||||
|
print(f"\n=== Test 3: I2DAT write (EEPROM addr) ===")
|
||||||
|
print(f"I2CS before I2DAT write: 0x{a0r(I2CS)[0]:02X}")
|
||||||
|
a0w(I2DAT, 0xA2) # EEPROM 0x51<<1
|
||||||
|
for i in range(5):
|
||||||
|
val = a0r(I2CS)[0]
|
||||||
|
print(f" I2CS read {i}: 0x{val:02X}")
|
||||||
|
time.sleep(0.002)
|
||||||
|
|
||||||
|
# Test 4: Read I2DAT
|
||||||
|
print(f"\n=== Test 4: I2DAT read ===")
|
||||||
|
dat = a0r(I2DAT)[0]
|
||||||
|
print(f"I2DAT read: 0x{dat:02X}")
|
||||||
|
print(f"I2CS after I2DAT read: 0x{a0r(I2CS)[0]:02X}")
|
||||||
|
|
||||||
|
# Test 5: Write STOP
|
||||||
|
print(f"\n=== Test 5: I2CS STOP ===")
|
||||||
|
a0w(I2CS, 0x40) # bmSTOP
|
||||||
|
for i in range(3):
|
||||||
|
val = a0r(I2CS)[0]
|
||||||
|
print(f" I2CS read {i}: 0x{val:02X}")
|
||||||
|
time.sleep(0.002)
|
||||||
|
|
||||||
|
# Test 6: Write arbitrary values to I2CS
|
||||||
|
print(f"\n=== Test 6: I2CS raw write/read ===")
|
||||||
|
for test_val in [0x00, 0xFF, 0x04, 0x80]:
|
||||||
|
a0w(I2CS, test_val)
|
||||||
|
time.sleep(0.001)
|
||||||
|
rb = a0r(I2CS)[0]
|
||||||
|
changed = "CHANGED" if rb != i2cs_orig else "unchanged"
|
||||||
|
print(f" Wrote 0x{test_val:02X}, read 0x{rb:02X} {changed}")
|
||||||
|
|
||||||
|
# Test 7: XDATA RAM write/read (control test)
|
||||||
|
print(f"\n=== Test 7: XDATA RAM control test ===")
|
||||||
|
TEST_ADDR = 0x3C00
|
||||||
|
orig = a0r(TEST_ADDR)[0]
|
||||||
|
a0w(TEST_ADDR, 0xBE)
|
||||||
|
time.sleep(0.001)
|
||||||
|
readback = a0r(TEST_ADDR)[0]
|
||||||
|
ok = "OK" if readback == 0xBE else "FAIL"
|
||||||
|
print(f"RAM[0x3C00]: wrote 0xBE, read 0x{readback:02X} {ok}")
|
||||||
|
|
||||||
|
# Test 8: Try writing 0x04 to I2CS (BERR clear bit)
|
||||||
|
print(f"\n=== Test 8: BERR clear bit ===")
|
||||||
|
a0w(I2CS, 0x04)
|
||||||
|
time.sleep(0.001)
|
||||||
|
v = a0r(I2CS)[0]
|
||||||
|
print(f"After writing 0x04: I2CS=0x{v:02X}")
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print(f"\n=== Summary ===")
|
||||||
|
final_i2cs = a0r(I2CS)[0]
|
||||||
|
print(f"Final I2CS: 0x{final_i2cs:02X} (started at 0x{i2cs_orig:02X})")
|
||||||
|
if final_i2cs == i2cs_orig:
|
||||||
|
print("I2CS NEVER CHANGED -- host-side I2C register writes ignored by hardware")
|
||||||
|
print("\nImplication: 0xA0 writes reach XDATA address space but the I2C")
|
||||||
|
print("controller only responds to 8051 MOVX instructions, not USB engine writes.")
|
||||||
|
print("The boot ROM's 0xA0 handler uses a different bus path for register access.")
|
||||||
|
else:
|
||||||
|
print("I2CS DID CHANGE -- host-side I2C register writes may work!")
|
||||||
176
tools/indirect_loopback_test.py
Normal file
176
tools/indirect_loopback_test.py
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""BCM4500 indirect register loopback test.
|
||||||
|
|
||||||
|
Write a known value via indirect write, then read it back.
|
||||||
|
Tests multiple approaches:
|
||||||
|
1. Multi-byte A6+A7+A8 in one transaction (0xB2 uses bcm_indirect_write)
|
||||||
|
2. Separate writes via 0xB6 diagnostic
|
||||||
|
3. Read via 0xB6 with various delays
|
||||||
|
4. Read using 0xB1 (bcm_indirect_read wrapper)
|
||||||
|
|
||||||
|
If we can write-then-read a value back, the DSP command processor works.
|
||||||
|
If not, we need to look at the I2C transaction structure more carefully.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
sys.path.insert(0, 'tools')
|
||||||
|
from skywalker_lib import SkyWalker1
|
||||||
|
|
||||||
|
CMD_RAW_DEMOD_READ = 0xB1
|
||||||
|
CMD_RAW_DEMOD_WRITE = 0xB2
|
||||||
|
CMD_I2C_RAW_READ = 0xB5
|
||||||
|
CMD_I2C_DIAG = 0xB6
|
||||||
|
BCM4500_ADDR = 0x08
|
||||||
|
|
||||||
|
sw = SkyWalker1()
|
||||||
|
sw.open()
|
||||||
|
print('=== BCM4500 Indirect Register Loopback Test ===')
|
||||||
|
print(f'Firmware: {sw.get_fw_version()}')
|
||||||
|
|
||||||
|
# Boot with full sequence
|
||||||
|
print('\n--- Booting BCM4500 (full boot) ---')
|
||||||
|
result = sw._vendor_in(0x89, value=1, index=0, length=3)
|
||||||
|
cfg, stage = result[0], result[1]
|
||||||
|
print(f' Config: 0x{cfg:02X}, Stage: 0x{stage:02X}')
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Test 1: Read default indirect register values
|
||||||
|
# ============================================================
|
||||||
|
print('\n=== Test 1: Default indirect register values (0xB1) ===')
|
||||||
|
for page in [0x00, 0x01, 0x06, 0x07, 0x0A, 0x0F]:
|
||||||
|
try:
|
||||||
|
data = sw._vendor_in(CMD_RAW_DEMOD_READ, value=page, index=0, length=1)
|
||||||
|
print(f' Page 0x{page:02X}: 0x{data[0]:02X}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' Page 0x{page:02X}: FAILED ({e})')
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Test 2: Write via 0xB2 (multi-byte A6+A7+A8), then read via 0xB1
|
||||||
|
# ============================================================
|
||||||
|
print('\n=== Test 2: Write 0x42 to page 0x00, read back (0xB2 write, 0xB1 read) ===')
|
||||||
|
try:
|
||||||
|
# 0xB2: bcm_indirect_write(page=0x00, val=0x42)
|
||||||
|
# Writes A6=0x00, A7=0x42, A8=0x03 in one 3-byte I2C transaction
|
||||||
|
sw._vendor_in(CMD_RAW_DEMOD_WRITE, value=0x00, index=0x42, length=0)
|
||||||
|
except Exception:
|
||||||
|
pass # May not return data
|
||||||
|
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
# Read back via 0xB1 (bcm_indirect_read with 1ms delay)
|
||||||
|
try:
|
||||||
|
data = sw._vendor_in(CMD_RAW_DEMOD_READ, value=0x00, index=0, length=1)
|
||||||
|
print(f' Read back page 0x00: 0x{data[0]:02X}')
|
||||||
|
if data[0] == 0x42:
|
||||||
|
print(' >> LOOPBACK SUCCESS! DSP is processing commands!')
|
||||||
|
elif data[0] == 0x00:
|
||||||
|
print(' >> Got 0x00 — either DSP not running or write/read protocol broken')
|
||||||
|
else:
|
||||||
|
print(f' >> Unexpected: 0x{data[0]:02X}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' Read failed: {e}')
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Test 3: Direct register reads before/after indirect write
|
||||||
|
# ============================================================
|
||||||
|
print('\n=== Test 3: Direct register state before/after indirect commands ===')
|
||||||
|
print(' Before indirect write:')
|
||||||
|
for reg in [0xA6, 0xA7, 0xA8]:
|
||||||
|
data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR,
|
||||||
|
index=reg, length=1)
|
||||||
|
print(f' 0x{reg:02X}: 0x{data[0]:02X}')
|
||||||
|
|
||||||
|
# Write via 0xB2
|
||||||
|
try:
|
||||||
|
sw._vendor_in(CMD_RAW_DEMOD_WRITE, value=0x06, index=0xAB, length=0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
time.sleep(0.01)
|
||||||
|
print(' After indirect write (page=0x06, data=0xAB):')
|
||||||
|
for reg in [0xA6, 0xA7, 0xA8]:
|
||||||
|
data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR,
|
||||||
|
index=reg, length=1)
|
||||||
|
print(f' 0x{reg:02X}: 0x{data[0]:02X}')
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Test 4: Manual step-by-step with individual I2C writes
|
||||||
|
# ============================================================
|
||||||
|
print('\n=== Test 4: Manual indirect read using individual raw I2C writes ===')
|
||||||
|
print(' Writing A6=0x06 via direct I2C write...')
|
||||||
|
|
||||||
|
# We don't have a raw I2C write command, but we can use the 0xB6 diagnostic
|
||||||
|
# which does individual writes and reads for us.
|
||||||
|
|
||||||
|
# Test 4a: 0xB6 with READ command (wIndex=0x01)
|
||||||
|
print('\n 4a: 0xB6 diagnostic — READ page 0x06:')
|
||||||
|
diag = sw._vendor_in(CMD_I2C_DIAG, value=0x06, index=0x01, length=8)
|
||||||
|
print(f' A6 write ok: {diag[0]}')
|
||||||
|
print(f' A6 readback: 0x{diag[1]:02X}')
|
||||||
|
print(f' A8 write ok: {diag[2]}')
|
||||||
|
print(f' A8 immediate: 0x{diag[3]:02X}')
|
||||||
|
print(f' A8 after 2ms: 0x{diag[4]:02X}')
|
||||||
|
print(f' A7 data: 0x{diag[5]:02X}')
|
||||||
|
print(f' A6 final: 0x{diag[6]:02X}')
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Test 5: Try reading A7 with longer delays
|
||||||
|
# ============================================================
|
||||||
|
print('\n=== Test 5: Indirect read with longer delays ===')
|
||||||
|
print(' Maybe the DSP needs more time to process the command...')
|
||||||
|
|
||||||
|
# Use 0xB6 to write A6=0x06 and A8=0x01
|
||||||
|
sw._vendor_in(CMD_I2C_DIAG, value=0x06, index=0x01, length=8)
|
||||||
|
|
||||||
|
for delay_ms in [10, 50, 100, 500]:
|
||||||
|
time.sleep(delay_ms / 1000.0)
|
||||||
|
# Read A7 via raw I2C
|
||||||
|
try:
|
||||||
|
data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR,
|
||||||
|
index=0xA7, length=1)
|
||||||
|
print(f' After {delay_ms:4d}ms: A7=0x{data[0]:02X}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' After {delay_ms:4d}ms: FAILED ({e})')
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Test 6: Direct register dump after all tests
|
||||||
|
# ============================================================
|
||||||
|
print('\n=== Test 6: Register state after all tests ===')
|
||||||
|
print(' A0-AF:')
|
||||||
|
for reg in range(0xA0, 0xB0):
|
||||||
|
data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR,
|
||||||
|
index=reg, length=1)
|
||||||
|
print(f' 0x{reg:02X}=0x{data[0]:02X}', end='')
|
||||||
|
if (reg % 8) == 7:
|
||||||
|
print()
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Test 7: Power cycle BCM4500 and check pre-command register state
|
||||||
|
# ============================================================
|
||||||
|
print('\n=== Test 7: Reboot and check BEFORE any indirect commands ===')
|
||||||
|
sw._vendor_in(0x89, value=0, index=0, length=3) # shutdown
|
||||||
|
time.sleep(0.5)
|
||||||
|
sw._vendor_in(0x89, value=1, index=0, length=3) # full boot
|
||||||
|
time.sleep(0.1)
|
||||||
|
print(' Fresh boot — direct reg reads (no indirect commands issued):')
|
||||||
|
for reg in [0xA0, 0xA2, 0xA4, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB]:
|
||||||
|
data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR,
|
||||||
|
index=reg, length=1)
|
||||||
|
print(f' 0x{reg:02X}: 0x{data[0]:02X}')
|
||||||
|
|
||||||
|
# Now issue ONE indirect read command and check if registers change
|
||||||
|
print('\n After ONE indirect read (page 0x06):')
|
||||||
|
diag = sw._vendor_in(CMD_I2C_DIAG, value=0x06, index=0x01, length=8)
|
||||||
|
print(f' A7 data: 0x{diag[5]:02X} (this is the indirect read result)')
|
||||||
|
|
||||||
|
# Check if direct registers changed
|
||||||
|
print(' Direct register check after indirect command:')
|
||||||
|
for reg in [0xA0, 0xA2, 0xA4, 0xA6, 0xA7, 0xA8]:
|
||||||
|
data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR,
|
||||||
|
index=reg, length=1)
|
||||||
|
print(f' 0x{reg:02X}: 0x{data[0]:02X}')
|
||||||
|
|
||||||
|
sw.close()
|
||||||
|
print('\n=== Done ===')
|
||||||
986
tools/rf_testbench.py
Normal file
986
tools/rf_testbench.py
Normal file
@ -0,0 +1,986 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
RF Test Bench — CW injection tests with NanoVNA + HMC472A + SkyWalker-1.
|
||||||
|
|
||||||
|
Injects CW signals from a NanoVNA-H through a programmable HMC472A digital
|
||||||
|
attenuator into the SkyWalker-1 receiver. Runs automated test sequences to
|
||||||
|
characterize AGC linearity, IF band flatness, frequency accuracy, minimum
|
||||||
|
detectable signal, and BPSK mode 9 behavior.
|
||||||
|
|
||||||
|
Hardware setup:
|
||||||
|
NanoVNA CH0 → DC Blocker → HMC472A → SMA-to-F → SkyWalker-1
|
||||||
|
|
||||||
|
The HMC472A (0-31.5 dB, 0.5 dB steps) is controlled via USB serial (preferred)
|
||||||
|
or REST API. The NanoVNA provides CW at a fixed frequency, controlled either
|
||||||
|
via mcnanovna or manually. The SkyWalker-1 measures AGC, SNR, and lock status.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python rf_testbench.py agc-linearity --freq 1200
|
||||||
|
python rf_testbench.py band-flatness --start 950 --stop 1500 --step 10
|
||||||
|
python rf_testbench.py freq-accuracy --freqs 1000,1200,1400
|
||||||
|
python rf_testbench.py mds --freq 1200
|
||||||
|
python rf_testbench.py bpsk-probe --freq 1200
|
||||||
|
python rf_testbench.py --help
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from urllib.request import urlopen, Request
|
||||||
|
from urllib.error import URLError
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from skywalker_lib import SkyWalker1, MODULATIONS
|
||||||
|
|
||||||
|
|
||||||
|
# --- HMC472A REST client ---
|
||||||
|
|
||||||
|
class HMC472A:
|
||||||
|
"""Control the HMC472A digital attenuator via its ESP32-S2 REST API."""
|
||||||
|
|
||||||
|
def __init__(self, base_url: str = "http://attenuator.local"):
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
|
||||||
|
def _get(self, path: str, retries: int = 3) -> dict:
|
||||||
|
url = f"{self.base_url}{path}"
|
||||||
|
req = Request(url)
|
||||||
|
last_err: OSError = OSError("no attempts made")
|
||||||
|
for attempt in range(retries):
|
||||||
|
try:
|
||||||
|
with urlopen(req, timeout=5) as resp:
|
||||||
|
return json.loads(resp.read())
|
||||||
|
except (URLError, OSError) as e:
|
||||||
|
last_err = e
|
||||||
|
if attempt < retries - 1:
|
||||||
|
time.sleep(0.2 * (attempt + 1))
|
||||||
|
raise last_err
|
||||||
|
|
||||||
|
def _post(self, path: str, data: dict, retries: int = 3) -> dict:
|
||||||
|
url = f"{self.base_url}{path}"
|
||||||
|
body = json.dumps(data).encode()
|
||||||
|
req = Request(url, data=body, method="POST",
|
||||||
|
headers={"Content-Type": "application/json"})
|
||||||
|
last_err: OSError = OSError("no attempts made")
|
||||||
|
for attempt in range(retries):
|
||||||
|
try:
|
||||||
|
with urlopen(req, timeout=5) as resp:
|
||||||
|
return json.loads(resp.read())
|
||||||
|
except (URLError, OSError) as e:
|
||||||
|
last_err = e
|
||||||
|
if attempt < retries - 1:
|
||||||
|
time.sleep(0.2 * (attempt + 1))
|
||||||
|
raise last_err
|
||||||
|
|
||||||
|
def status(self) -> dict:
|
||||||
|
return self._get("/status")
|
||||||
|
|
||||||
|
def set_db(self, attenuation_db: float) -> dict:
|
||||||
|
clamped = max(0.0, min(31.5, attenuation_db))
|
||||||
|
rounded = round(clamped * 2) / 2 # Snap to 0.5 dB steps
|
||||||
|
return self._post("/set", {"attenuation_db": rounded})
|
||||||
|
|
||||||
|
def config(self) -> dict:
|
||||||
|
return self._get("/config")
|
||||||
|
|
||||||
|
|
||||||
|
# --- HMC472A USB serial client ---
|
||||||
|
|
||||||
|
class HMC472ASerial:
|
||||||
|
"""Control the HMC472A digital attenuator via USB CDC serial.
|
||||||
|
|
||||||
|
Uses the usb-serial-json-v1 protocol: one JSON object per newline-
|
||||||
|
terminated line in each direction. Requires pyserial.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, port: str, baudrate: int = 115200, timeout: float = 1.0):
|
||||||
|
import serial
|
||||||
|
self.ser = serial.Serial(port, baudrate, timeout=timeout)
|
||||||
|
self.ser.reset_input_buffer()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.ser and self.ser.is_open:
|
||||||
|
self.ser.close()
|
||||||
|
|
||||||
|
def _cmd(self, command: dict) -> dict:
|
||||||
|
line = json.dumps(command, separators=(",", ":")) + "\n"
|
||||||
|
self.ser.write(line.encode())
|
||||||
|
self.ser.flush()
|
||||||
|
resp_line = self.ser.readline()
|
||||||
|
if not resp_line:
|
||||||
|
raise TimeoutError("no response from HMC472A")
|
||||||
|
resp = json.loads(resp_line)
|
||||||
|
if not resp.get("ok"):
|
||||||
|
raise RuntimeError(resp.get("error", "unknown error"))
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def status(self) -> dict:
|
||||||
|
return self._cmd({"cmd": "status"})
|
||||||
|
|
||||||
|
def set_db(self, attenuation_db: float) -> dict:
|
||||||
|
clamped = max(0.0, min(31.5, attenuation_db))
|
||||||
|
rounded = round(clamped * 2) / 2
|
||||||
|
return self._cmd({"cmd": "set", "db": rounded})
|
||||||
|
|
||||||
|
def config(self) -> dict:
|
||||||
|
return self._cmd({"cmd": "config"})
|
||||||
|
|
||||||
|
def identify(self) -> dict:
|
||||||
|
return self._cmd({"cmd": "identify"})
|
||||||
|
|
||||||
|
|
||||||
|
def detect_hmc472a_serial() -> str | None:
|
||||||
|
"""Scan /dev/ttyACM* ports for an HMC472A responding to identify.
|
||||||
|
|
||||||
|
Returns the port path if found, None otherwise.
|
||||||
|
"""
|
||||||
|
import glob
|
||||||
|
try:
|
||||||
|
import serial
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
ports = sorted(glob.glob("/dev/ttyACM*"))
|
||||||
|
for port in ports:
|
||||||
|
try:
|
||||||
|
ser = serial.Serial(port, 115200, timeout=0.5)
|
||||||
|
ser.reset_input_buffer()
|
||||||
|
ser.write(b'{"cmd":"identify"}\n')
|
||||||
|
ser.flush()
|
||||||
|
resp_line = ser.readline()
|
||||||
|
ser.close()
|
||||||
|
if resp_line:
|
||||||
|
resp = json.loads(resp_line)
|
||||||
|
if resp.get("device") == "hmc472a-attenuator":
|
||||||
|
return port
|
||||||
|
except (OSError, json.JSONDecodeError, ValueError):
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class MockSkyWalker1:
|
||||||
|
"""Lightweight mock SkyWalker-1 for rf_testbench testing."""
|
||||||
|
|
||||||
|
def __init__(self, verbose=False):
|
||||||
|
self.verbose = verbose
|
||||||
|
self._freq_khz = 0
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def ensure_booted(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def start_intersil(self, on=True):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def tune_monitor(self, symbol_rate_sps=1000000, freq_khz=1200000,
|
||||||
|
mod_index=0, fec_index=5, dwell_ms=10):
|
||||||
|
self._freq_khz = freq_khz
|
||||||
|
# Simulate AGC response: higher freq → slightly lower power
|
||||||
|
base_agc1 = 1200 + (freq_khz - 1200000) // 100
|
||||||
|
return {
|
||||||
|
"snr_raw": 180, "snr_db": 7.8, "snr_pct": 39.0,
|
||||||
|
"agc1": max(100, base_agc1), "agc2": 750,
|
||||||
|
"power_db": -46.1 - (freq_khz - 1200000) / 500000,
|
||||||
|
"locked": False, "lock": 0x00, "status": 0x01,
|
||||||
|
"dwell_ms": dwell_ms,
|
||||||
|
}
|
||||||
|
|
||||||
|
def signal_monitor(self):
|
||||||
|
return {
|
||||||
|
"snr_raw": 200, "snr_db": 8.5, "snr_pct": 42.5,
|
||||||
|
"agc1": 1200, "agc2": 800, "power_db": -45.3,
|
||||||
|
"locked": False, "lock": 0x00, "status": 0x01,
|
||||||
|
}
|
||||||
|
|
||||||
|
def sweep_spectrum(self, start_mhz, stop_mhz, step_mhz=5.0,
|
||||||
|
dwell_ms=15, sr_ksps=1000, mod_index=0, fec_index=5):
|
||||||
|
n = int((stop_mhz - start_mhz) / step_mhz) + 1
|
||||||
|
freqs = [start_mhz + i * step_mhz for i in range(n)]
|
||||||
|
powers = [-50.0 + 3.0 * (1.0 - abs(f - 1200) / 300) for f in freqs]
|
||||||
|
raw = [{"agc1": 1200, "agc2": 750, "power_db": p,
|
||||||
|
"snr_raw": 0, "snr_db": 0, "locked": False,
|
||||||
|
"lock": 0, "status": 0} for p in powers]
|
||||||
|
return freqs, powers, raw
|
||||||
|
|
||||||
|
|
||||||
|
def _make_mock_skywalker(verbose=False):
|
||||||
|
sw = MockSkyWalker1(verbose=verbose)
|
||||||
|
sw.open()
|
||||||
|
sw.ensure_booted()
|
||||||
|
return sw
|
||||||
|
|
||||||
|
|
||||||
|
class FixedAttenuator:
|
||||||
|
"""Stand-in for a fixed (non-programmable) inline attenuator.
|
||||||
|
|
||||||
|
Reports the declared fixed attenuation for every set_db() call.
|
||||||
|
Used when testing with a fixed pad instead of the HMC472A.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, fixed_db: float = 20.0):
|
||||||
|
self._fixed_db = fixed_db
|
||||||
|
|
||||||
|
def status(self) -> dict:
|
||||||
|
return {"attenuation_db": self._fixed_db, "step": 0,
|
||||||
|
"version": "fixed-pad", "note": "non-programmable"}
|
||||||
|
|
||||||
|
def set_db(self, attenuation_db: float) -> dict:
|
||||||
|
# Can't change a fixed pad — just return what it is
|
||||||
|
return self.status()
|
||||||
|
|
||||||
|
def config(self) -> dict:
|
||||||
|
return {"db_min": self._fixed_db, "db_max": self._fixed_db,
|
||||||
|
"db_step": 0, "version": "fixed-pad",
|
||||||
|
"hostname": f"fixed-{self._fixed_db:.1f}dB"}
|
||||||
|
|
||||||
|
|
||||||
|
class MockHMC472A:
|
||||||
|
"""Mock attenuator for testing without hardware."""
|
||||||
|
|
||||||
|
def __init__(self, base_url: str = "http://mock.local"):
|
||||||
|
self.base_url = base_url
|
||||||
|
self._db = 0.0
|
||||||
|
|
||||||
|
def status(self) -> dict:
|
||||||
|
step = int(self._db * 2)
|
||||||
|
return {"attenuation_db": self._db, "step": step, "version": "mock"}
|
||||||
|
|
||||||
|
def set_db(self, attenuation_db: float) -> dict:
|
||||||
|
self._db = max(0.0, min(31.5, round(attenuation_db * 2) / 2))
|
||||||
|
return self.status()
|
||||||
|
|
||||||
|
def config(self) -> dict:
|
||||||
|
return {"db_min": 0.0, "db_max": 31.5, "db_step": 0.5,
|
||||||
|
"version": "mock", "hostname": "mock-attenuator"}
|
||||||
|
|
||||||
|
|
||||||
|
# --- NanoVNA control ---
|
||||||
|
|
||||||
|
def try_import_nanovna():
|
||||||
|
"""Try to import mcnanovna for automated NanoVNA control."""
|
||||||
|
try:
|
||||||
|
from mcnanovna.nanovna import NanoVNA
|
||||||
|
return NanoVNA
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class MockNanoVNA:
|
||||||
|
"""Mock NanoVNA for testing without hardware."""
|
||||||
|
|
||||||
|
def cw(self, frequency_hz: int = 0, power: int = 3):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def manual_nanovna_set(freq_mhz: float, power: int = 3) -> None:
|
||||||
|
"""Prompt the user to manually set NanoVNA CW frequency."""
|
||||||
|
print(f"\n >>> Set NanoVNA to CW at {freq_mhz:.3f} MHz, power={power}")
|
||||||
|
input(" Press Enter when ready...")
|
||||||
|
|
||||||
|
|
||||||
|
# --- CSV output ---
|
||||||
|
|
||||||
|
CSV_COLUMNS = [
|
||||||
|
"timestamp", "test_name", "freq_mhz", "atten_db",
|
||||||
|
"agc1", "agc2", "power_db", "snr_raw", "snr_db",
|
||||||
|
"locked", "lock_raw", "status", "notes",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def open_csv(path: str):
|
||||||
|
f = open(path, "w", newline="")
|
||||||
|
writer = csv.DictWriter(f, fieldnames=CSV_COLUMNS)
|
||||||
|
writer.writeheader()
|
||||||
|
return f, writer
|
||||||
|
|
||||||
|
|
||||||
|
def write_row(writer, csv_file, test_name: str, freq_mhz: float,
|
||||||
|
atten_db: float, result: dict, notes: str = ""):
|
||||||
|
writer.writerow({
|
||||||
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"test_name": test_name,
|
||||||
|
"freq_mhz": f"{freq_mhz:.3f}",
|
||||||
|
"atten_db": f"{atten_db:.1f}",
|
||||||
|
"agc1": result.get("agc1", 0),
|
||||||
|
"agc2": result.get("agc2", 0),
|
||||||
|
"power_db": f"{result.get('power_db', 0):.2f}",
|
||||||
|
"snr_raw": result.get("snr_raw", 0),
|
||||||
|
"snr_db": f"{result.get('snr_db', 0):.2f}",
|
||||||
|
"locked": result.get("locked", False),
|
||||||
|
"lock_raw": f"0x{result.get('lock', 0):02X}",
|
||||||
|
"status": f"0x{result.get('status', 0):02X}",
|
||||||
|
"notes": notes,
|
||||||
|
})
|
||||||
|
if csv_file:
|
||||||
|
csv_file.flush()
|
||||||
|
|
||||||
|
|
||||||
|
# --- Calibration ---
|
||||||
|
|
||||||
|
def load_cal_file(path: str) -> dict:
|
||||||
|
"""Load a NanoVNA S21 path-loss calibration CSV.
|
||||||
|
|
||||||
|
Expects columns: frequency_hz (or freq_mhz), s21_db (or loss_db).
|
||||||
|
Returns dict mapping freq_mhz -> loss_db (positive = loss).
|
||||||
|
"""
|
||||||
|
cal = {}
|
||||||
|
with open(path) as f:
|
||||||
|
reader = csv.DictReader(f)
|
||||||
|
for row in reader:
|
||||||
|
if "freq_mhz" in row:
|
||||||
|
freq = float(row["freq_mhz"])
|
||||||
|
elif "frequency_hz" in row:
|
||||||
|
freq = float(row["frequency_hz"]) / 1e6
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "loss_db" in row:
|
||||||
|
loss = float(row["loss_db"])
|
||||||
|
elif "s21_db" in row:
|
||||||
|
loss = -float(row["s21_db"]) # S21 is negative, loss is positive
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
cal[freq] = loss
|
||||||
|
return cal
|
||||||
|
|
||||||
|
|
||||||
|
def interpolate_loss(cal: dict, freq_mhz: float) -> float:
|
||||||
|
"""Interpolate path loss at a frequency from cal data."""
|
||||||
|
if not cal:
|
||||||
|
return 0.0
|
||||||
|
freqs = sorted(cal.keys())
|
||||||
|
if freq_mhz <= freqs[0]:
|
||||||
|
return cal[freqs[0]]
|
||||||
|
if freq_mhz >= freqs[-1]:
|
||||||
|
return cal[freqs[-1]]
|
||||||
|
for i in range(len(freqs) - 1):
|
||||||
|
if freqs[i] <= freq_mhz <= freqs[i + 1]:
|
||||||
|
f0, f1 = freqs[i], freqs[i + 1]
|
||||||
|
t = (freq_mhz - f0) / (f1 - f0)
|
||||||
|
return cal[f0] + t * (cal[f1] - cal[f0])
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
# --- Test: AGC Power Linearity ---
|
||||||
|
|
||||||
|
def test_agc_linearity(sw, atten, nanovna, freq_mhz: float,
|
||||||
|
writer, csv_file, cal: dict, settle_ms: int) -> list:
|
||||||
|
"""Sweep attenuator from 0 to 31.5 dB and record AGC at each step.
|
||||||
|
|
||||||
|
Maps the AGC transfer function: how AGC register values respond to
|
||||||
|
known changes in input power.
|
||||||
|
"""
|
||||||
|
print(f"\n=== AGC Linearity Test at {freq_mhz:.1f} MHz ===")
|
||||||
|
results = []
|
||||||
|
path_loss = interpolate_loss(cal, freq_mhz)
|
||||||
|
if path_loss > 0:
|
||||||
|
print(f" Calibrated path loss: {path_loss:.1f} dB")
|
||||||
|
|
||||||
|
# Set NanoVNA to CW at the test frequency
|
||||||
|
if nanovna:
|
||||||
|
nanovna.cw(frequency_hz=int(freq_mhz * 1e6), power=3)
|
||||||
|
print(f" NanoVNA CW: {freq_mhz:.3f} MHz, power=3")
|
||||||
|
else:
|
||||||
|
manual_nanovna_set(freq_mhz, power=3)
|
||||||
|
|
||||||
|
# Tune SkyWalker-1 to the frequency
|
||||||
|
freq_khz = int(freq_mhz * 1000)
|
||||||
|
|
||||||
|
print(f"\n {'Atten dB':>9} {'AGC1':>6} {'AGC2':>6} {'Power dB':>9} "
|
||||||
|
f"{'SNR raw':>8} {'Lock':>5}")
|
||||||
|
print(f" {'─' * 9} {'─' * 6} {'─' * 6} {'─' * 9} {'─' * 8} {'─' * 5}")
|
||||||
|
|
||||||
|
# Sweep in 0.5 dB steps from 0 to 31.5 dB (64 steps)
|
||||||
|
# Use integer step counter to avoid IEEE 754 float accumulation drift
|
||||||
|
for step in range(64): # 0, 1, 2, ... 63 → 0.0, 0.5, 1.0, ... 31.5
|
||||||
|
atten_db = step * 0.5
|
||||||
|
atten.set_db(atten_db)
|
||||||
|
time.sleep(settle_ms / 1000.0)
|
||||||
|
|
||||||
|
result = sw.tune_monitor(
|
||||||
|
symbol_rate_sps=1000000, freq_khz=freq_khz,
|
||||||
|
mod_index=0, fec_index=5, dwell_ms=50
|
||||||
|
)
|
||||||
|
|
||||||
|
locked = "Y" if result.get("locked") else "N"
|
||||||
|
print(f" {atten_db:9.1f} {result['agc1']:6d} {result['agc2']:6d} "
|
||||||
|
f"{result['power_db']:9.2f} {result['snr_raw']:8d} {locked:>5}")
|
||||||
|
|
||||||
|
effective_atten = atten_db + path_loss
|
||||||
|
note = f"effective_atten={effective_atten:.1f}dB"
|
||||||
|
if writer:
|
||||||
|
write_row(writer, csv_file, "agc_linearity", freq_mhz, atten_db,
|
||||||
|
result, note)
|
||||||
|
|
||||||
|
results.append({"atten_db": atten_db, **result})
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
if results:
|
||||||
|
agc1_min = min(r["agc1"] for r in results)
|
||||||
|
agc1_max = max(r["agc1"] for r in results)
|
||||||
|
print(f"\n AGC1 range: {agc1_min} - {agc1_max} "
|
||||||
|
f"(delta={agc1_max - agc1_min}) over 31.5 dB sweep")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# --- Test: IF Band Flatness ---
|
||||||
|
|
||||||
|
def test_band_flatness(sw, atten, nanovna, start_mhz: float,
|
||||||
|
stop_mhz: float, step_mhz: float,
|
||||||
|
writer, csv_file, cal: dict, settle_ms: int) -> list:
|
||||||
|
"""Sweep CW across the IF band and record AGC at each frequency.
|
||||||
|
|
||||||
|
Reveals tuner gain slope, passband ripple, and the IF filter response.
|
||||||
|
"""
|
||||||
|
atten_db = 10.0 # Fixed attenuation — mid-range for good dynamic range
|
||||||
|
print(f"\n=== IF Band Flatness: {start_mhz:.0f}-{stop_mhz:.0f} MHz, "
|
||||||
|
f"step={step_mhz:.1f} MHz ===")
|
||||||
|
print(f" HMC472A fixed at {atten_db:.1f} dB")
|
||||||
|
|
||||||
|
atten.set_db(atten_db)
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# Use integer step counter to avoid float accumulation drift
|
||||||
|
n_steps = int(round((stop_mhz - start_mhz) / step_mhz)) + 1
|
||||||
|
|
||||||
|
print(f"\n {'Step':>5} {'Freq MHz':>9} {'AGC1':>6} {'AGC2':>6} "
|
||||||
|
f"{'Power dB':>9} {'PathLoss':>9} {'Corr dB':>8}")
|
||||||
|
print(f" {'─' * 5} {'─' * 9} {'─' * 6} {'─' * 6} "
|
||||||
|
f"{'─' * 9} {'─' * 9} {'─' * 8}")
|
||||||
|
|
||||||
|
for step_num in range(n_steps):
|
||||||
|
freq_mhz = start_mhz + step_num * step_mhz
|
||||||
|
|
||||||
|
# Set NanoVNA CW
|
||||||
|
if nanovna:
|
||||||
|
nanovna.cw(frequency_hz=int(freq_mhz * 1e6), power=3)
|
||||||
|
else:
|
||||||
|
manual_nanovna_set(freq_mhz, power=3)
|
||||||
|
|
||||||
|
time.sleep(settle_ms / 1000.0)
|
||||||
|
|
||||||
|
# Tune SkyWalker-1
|
||||||
|
freq_khz = int(freq_mhz * 1000)
|
||||||
|
result = sw.tune_monitor(
|
||||||
|
symbol_rate_sps=1000000, freq_khz=freq_khz,
|
||||||
|
mod_index=0, fec_index=5, dwell_ms=50
|
||||||
|
)
|
||||||
|
|
||||||
|
path_loss = interpolate_loss(cal, freq_mhz)
|
||||||
|
corrected = result["power_db"] + path_loss
|
||||||
|
|
||||||
|
print(f" {step_num + 1:5d} {freq_mhz:9.1f} {result['agc1']:6d} "
|
||||||
|
f"{result['agc2']:6d} {result['power_db']:9.2f} "
|
||||||
|
f"{path_loss:9.1f} {corrected:8.2f}")
|
||||||
|
|
||||||
|
note = f"path_loss={path_loss:.1f}dB corrected={corrected:.2f}dB"
|
||||||
|
if writer:
|
||||||
|
write_row(writer, csv_file, "band_flatness", freq_mhz, atten_db,
|
||||||
|
result, note)
|
||||||
|
|
||||||
|
results.append({"freq_mhz": freq_mhz, "corrected_db": corrected, **result})
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
if results:
|
||||||
|
powers = [r["corrected_db"] for r in results]
|
||||||
|
ripple = max(powers) - min(powers)
|
||||||
|
print(f"\n Band flatness: {ripple:.2f} dB ripple "
|
||||||
|
f"(min={min(powers):.2f}, max={max(powers):.2f})")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# --- Test: Frequency Accuracy ---
|
||||||
|
|
||||||
|
def test_freq_accuracy(sw, atten, nanovna, test_freqs: list,
|
||||||
|
writer, csv_file, settle_ms: int) -> list:
|
||||||
|
"""Inject CW at known frequencies, sweep SkyWalker-1 around each one.
|
||||||
|
|
||||||
|
Compares detected peak vs. injected frequency to characterize the
|
||||||
|
BCM3440 tuner's frequency accuracy.
|
||||||
|
"""
|
||||||
|
print(f"\n=== Frequency Accuracy Test ===")
|
||||||
|
atten_db = 10.0
|
||||||
|
atten.set_db(atten_db)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
sweep_span_mhz = 10.0 # Sweep +/- 5 MHz around each test freq
|
||||||
|
sweep_step_mhz = 1.0
|
||||||
|
|
||||||
|
for inject_freq in test_freqs:
|
||||||
|
print(f"\n Injecting CW at {inject_freq:.3f} MHz...")
|
||||||
|
if nanovna:
|
||||||
|
nanovna.cw(frequency_hz=int(inject_freq * 1e6), power=3)
|
||||||
|
else:
|
||||||
|
manual_nanovna_set(inject_freq, power=3)
|
||||||
|
|
||||||
|
time.sleep(settle_ms / 1000.0)
|
||||||
|
|
||||||
|
# Sweep around the expected frequency
|
||||||
|
sweep_start = inject_freq - sweep_span_mhz / 2
|
||||||
|
sweep_stop = inject_freq + sweep_span_mhz / 2
|
||||||
|
|
||||||
|
freqs, powers, raw = sw.sweep_spectrum(
|
||||||
|
sweep_start, sweep_stop,
|
||||||
|
step_mhz=sweep_step_mhz, dwell_ms=50,
|
||||||
|
sr_ksps=1000, mod_index=0, fec_index=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find peak
|
||||||
|
if powers:
|
||||||
|
peak_idx = max(range(len(powers)), key=lambda i: powers[i])
|
||||||
|
peak_freq = freqs[peak_idx]
|
||||||
|
peak_power = powers[peak_idx]
|
||||||
|
error_mhz = peak_freq - inject_freq
|
||||||
|
error_khz = error_mhz * 1000
|
||||||
|
|
||||||
|
print(f" Injected: {inject_freq:.3f} MHz "
|
||||||
|
f"Detected peak: {peak_freq:.3f} MHz "
|
||||||
|
f"Error: {error_khz:+.0f} kHz")
|
||||||
|
|
||||||
|
result_entry = {
|
||||||
|
"inject_freq_mhz": inject_freq,
|
||||||
|
"peak_freq_mhz": peak_freq,
|
||||||
|
"error_khz": error_khz,
|
||||||
|
"peak_power_db": peak_power,
|
||||||
|
}
|
||||||
|
results.append(result_entry)
|
||||||
|
|
||||||
|
if writer:
|
||||||
|
peak_result = raw[peak_idx] if isinstance(raw[peak_idx], dict) else {
|
||||||
|
"agc1": 0, "agc2": 0, "power_db": peak_power,
|
||||||
|
"snr_raw": 0, "snr_db": 0,
|
||||||
|
"locked": False, "lock": 0, "status": 0,
|
||||||
|
}
|
||||||
|
write_row(writer, csv_file, "freq_accuracy", inject_freq,
|
||||||
|
atten_db, peak_result,
|
||||||
|
f"peak={peak_freq:.3f}MHz error={error_khz:+.0f}kHz")
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
if results:
|
||||||
|
errors = [r["error_khz"] for r in results]
|
||||||
|
mean_err = sum(errors) / len(errors)
|
||||||
|
max_err = max(abs(e) for e in errors)
|
||||||
|
print(f"\n Mean frequency error: {mean_err:+.0f} kHz")
|
||||||
|
print(f" Max absolute error: {max_err:.0f} kHz")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# --- Test: Minimum Detectable Signal ---
|
||||||
|
|
||||||
|
def test_mds(sw, atten, nanovna, freq_mhz: float,
|
||||||
|
writer, csv_file, settle_ms: int) -> dict:
|
||||||
|
"""Find the minimum detectable signal level.
|
||||||
|
|
||||||
|
Measures noise floor with NanoVNA off (or max attenuation), then
|
||||||
|
increases attenuation from 0 until the CW signal is indistinguishable
|
||||||
|
from noise.
|
||||||
|
"""
|
||||||
|
print(f"\n=== Minimum Detectable Signal at {freq_mhz:.1f} MHz ===")
|
||||||
|
freq_khz = int(freq_mhz * 1000)
|
||||||
|
|
||||||
|
# Step 1: measure noise floor (max attenuation)
|
||||||
|
print(" Measuring noise floor (31.5 dB attenuation)...")
|
||||||
|
atten.set_db(31.5)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
noise_readings = []
|
||||||
|
for _ in range(10):
|
||||||
|
r = sw.tune_monitor(1000000, freq_khz, 0, 5, 50)
|
||||||
|
noise_readings.append(r["power_db"])
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
noise_floor = sum(noise_readings) / len(noise_readings)
|
||||||
|
noise_std = (sum((x - noise_floor) ** 2 for x in noise_readings)
|
||||||
|
/ len(noise_readings)) ** 0.5
|
||||||
|
threshold = noise_floor + max(3.0 * noise_std, 1.0) # 3-sigma above noise
|
||||||
|
print(f" Noise floor: {noise_floor:.2f} dB (std={noise_std:.3f})")
|
||||||
|
print(f" Detection threshold: {threshold:.2f} dB (noise + 3sigma)")
|
||||||
|
|
||||||
|
# Step 2: inject CW and increase attenuation until signal disappears
|
||||||
|
if nanovna:
|
||||||
|
nanovna.cw(frequency_hz=int(freq_mhz * 1e6), power=3)
|
||||||
|
print(f" NanoVNA CW: {freq_mhz:.3f} MHz, power=3")
|
||||||
|
else:
|
||||||
|
manual_nanovna_set(freq_mhz, power=3)
|
||||||
|
|
||||||
|
print(f"\n {'Atten dB':>9} {'Power dB':>9} {'Above noise':>12} {'Detected':>9}")
|
||||||
|
print(f" {'─' * 9} {'─' * 9} {'─' * 12} {'─' * 9}")
|
||||||
|
|
||||||
|
mds_atten = None
|
||||||
|
# 1 dB steps: 0, 1, 2, ... 31 (32 steps)
|
||||||
|
for step in range(32):
|
||||||
|
atten_db = float(step)
|
||||||
|
atten.set_db(atten_db)
|
||||||
|
time.sleep(settle_ms / 1000.0)
|
||||||
|
|
||||||
|
# Average 5 readings for stability
|
||||||
|
readings = []
|
||||||
|
r = sw.tune_monitor(1000000, freq_khz, 0, 5, 50)
|
||||||
|
readings.append(r["power_db"])
|
||||||
|
for _ in range(4):
|
||||||
|
time.sleep(0.02)
|
||||||
|
r = sw.tune_monitor(1000000, freq_khz, 0, 5, 50)
|
||||||
|
readings.append(r["power_db"])
|
||||||
|
|
||||||
|
avg_power = sum(readings) / len(readings)
|
||||||
|
above_noise = avg_power - noise_floor
|
||||||
|
detected = avg_power > threshold
|
||||||
|
|
||||||
|
marker = "YES" if detected else "---"
|
||||||
|
print(f" {atten_db:9.1f} {avg_power:9.2f} {above_noise:+12.2f} "
|
||||||
|
f"{marker:>9}")
|
||||||
|
|
||||||
|
if writer:
|
||||||
|
# Use averaged power instead of last single reading
|
||||||
|
avg_result = dict(r)
|
||||||
|
avg_result["power_db"] = avg_power
|
||||||
|
write_row(writer, csv_file, "mds", freq_mhz, atten_db,
|
||||||
|
avg_result,
|
||||||
|
f"avg={avg_power:.2f} noise={noise_floor:.2f} "
|
||||||
|
f"detected={'Y' if detected else 'N'}")
|
||||||
|
|
||||||
|
if not detected and mds_atten is None:
|
||||||
|
mds_atten = atten_db
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"freq_mhz": freq_mhz,
|
||||||
|
"noise_floor_db": noise_floor,
|
||||||
|
"noise_std": noise_std,
|
||||||
|
"threshold_db": threshold,
|
||||||
|
"mds_atten_db": mds_atten,
|
||||||
|
}
|
||||||
|
|
||||||
|
if mds_atten is not None:
|
||||||
|
print(f"\n Signal lost at {mds_atten:.1f} dB attenuation")
|
||||||
|
print(f" (NanoVNA output ~-15 dBm minus {mds_atten:.1f} dB path = "
|
||||||
|
f"~{-15 - mds_atten:.0f} dBm at receiver)")
|
||||||
|
else:
|
||||||
|
print(f"\n Signal detected at all attenuation levels (0-31.5 dB)")
|
||||||
|
print(f" Need more attenuation to find MDS")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# --- Test: BPSK Mode 9 CW Probe ---
|
||||||
|
|
||||||
|
def test_bpsk_probe(sw, atten, nanovna, freq_mhz: float,
|
||||||
|
writer, csv_file, settle_ms: int) -> dict:
|
||||||
|
"""Probe BPSK mode 9 response to an unmodulated CW carrier.
|
||||||
|
|
||||||
|
BPSK mode (index 9) uses Viterbi rate 1/2 K=7 — the same inner FEC
|
||||||
|
as GOES LRIT. A CW carrier has no modulation, so the demodulator
|
||||||
|
shouldn't lock, but the AGC and carrier recovery behavior reveals
|
||||||
|
how mode 9 handles a clean carrier.
|
||||||
|
"""
|
||||||
|
print(f"\n=== BPSK Mode 9 CW Probe at {freq_mhz:.1f} MHz ===")
|
||||||
|
bpsk_index = MODULATIONS["bpsk"][0] # Mode 9
|
||||||
|
freq_khz = int(freq_mhz * 1000)
|
||||||
|
atten_db = 10.0
|
||||||
|
atten.set_db(atten_db)
|
||||||
|
|
||||||
|
if nanovna:
|
||||||
|
nanovna.cw(frequency_hz=int(freq_mhz * 1e6), power=3)
|
||||||
|
else:
|
||||||
|
manual_nanovna_set(freq_mhz, power=3)
|
||||||
|
|
||||||
|
time.sleep(settle_ms / 1000.0)
|
||||||
|
|
||||||
|
# Test with different symbol rates typical of LRIT-like signals
|
||||||
|
test_rates = [293883, 500000, 1000000, 5000000]
|
||||||
|
|
||||||
|
print(f"\n {'SR (sps)':>10} {'AGC1':>6} {'AGC2':>6} {'Power dB':>9} "
|
||||||
|
f"{'SNR raw':>8} {'SNR dB':>7} {'Lock':>6} {'Status':>8}")
|
||||||
|
print(f" {'─' * 10} {'─' * 6} {'─' * 6} {'─' * 9} "
|
||||||
|
f"{'─' * 8} {'─' * 7} {'─' * 6} {'─' * 8}")
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for sr in test_rates:
|
||||||
|
# FEC 1/2 (index 0) for BPSK mode
|
||||||
|
result = sw.tune_monitor(sr, freq_khz, bpsk_index, 0, dwell_ms=100)
|
||||||
|
|
||||||
|
locked = "Y" if result.get("locked") else "N"
|
||||||
|
print(f" {sr:10d} {result['agc1']:6d} {result['agc2']:6d} "
|
||||||
|
f"{result['power_db']:9.2f} {result['snr_raw']:8d} "
|
||||||
|
f"{result['snr_db']:7.2f} {locked:>6} "
|
||||||
|
f"0x{result.get('status', 0):02X}")
|
||||||
|
|
||||||
|
if writer:
|
||||||
|
write_row(writer, csv_file, "bpsk_probe", freq_mhz, atten_db,
|
||||||
|
result, f"mode=bpsk sr={sr} fec=1/2")
|
||||||
|
|
||||||
|
results.append({"symbol_rate": sr, **result})
|
||||||
|
|
||||||
|
# Compare with QPSK mode 0 at same settings
|
||||||
|
print(f"\n Reference: QPSK mode 0 at same frequency")
|
||||||
|
ref = sw.tune_monitor(1000000, freq_khz, 0, 5, dwell_ms=100)
|
||||||
|
ref_locked = "Y" if ref.get("locked") else "N"
|
||||||
|
print(f" {'1000000':>10} {ref['agc1']:6d} {ref['agc2']:6d} "
|
||||||
|
f"{ref['power_db']:9.2f} {ref['snr_raw']:8d} "
|
||||||
|
f"{ref['snr_db']:7.2f} {ref_locked:>6} "
|
||||||
|
f"0x{ref.get('status', 0):02X}")
|
||||||
|
|
||||||
|
return {"bpsk_results": results, "qpsk_reference": ref}
|
||||||
|
|
||||||
|
|
||||||
|
# --- Main ---
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="rf_testbench.py",
|
||||||
|
description="CW injection test bench: NanoVNA + HMC472A + SkyWalker-1",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""\
|
||||||
|
examples:
|
||||||
|
%(prog)s agc-linearity --freq 1200
|
||||||
|
%(prog)s band-flatness --start 950 --stop 1500 --step 10
|
||||||
|
%(prog)s freq-accuracy --freqs 1000,1200,1400
|
||||||
|
%(prog)s mds --freq 1200
|
||||||
|
%(prog)s bpsk-probe --freq 1200
|
||||||
|
%(prog)s band-flatness --attenuator /dev/ttyACM1
|
||||||
|
%(prog)s agc-linearity --attenuator http://attenuator.local --freq 1200
|
||||||
|
|
||||||
|
hardware setup:
|
||||||
|
NanoVNA CH0 → DC Blocker → HMC472A (0-31.5 dB) → SMA-to-F → SkyWalker-1
|
||||||
|
|
||||||
|
The HMC472A is controlled via USB serial (preferred) or HTTP REST API.
|
||||||
|
Use --attenuator auto (default) to auto-detect USB, falling back to HTTP.
|
||||||
|
The NanoVNA provides CW, controlled via mcnanovna or manually.
|
||||||
|
LNB power is disabled (direct L-band input mode).
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument("-v", "--verbose", action="store_true",
|
||||||
|
help="Show raw USB traffic")
|
||||||
|
parser.add_argument("-o", "--output", type=str, default=None,
|
||||||
|
help="CSV output file path")
|
||||||
|
parser.add_argument("--cal", type=str, default=None,
|
||||||
|
help="Path loss calibration CSV (NanoVNA S21 sweep)")
|
||||||
|
parser.add_argument("--attenuator", type=str, default="auto",
|
||||||
|
help="HMC472A connection: 'auto' (USB then HTTP), "
|
||||||
|
"/dev/ttyACMx (USB serial), http://host (REST), "
|
||||||
|
"or 'fixed:20' for a non-programmable pad "
|
||||||
|
"(default: auto)")
|
||||||
|
parser.add_argument("--nanovna", choices=["auto", "manual"],
|
||||||
|
default="auto",
|
||||||
|
help="NanoVNA control mode (default: auto via mcnanovna)")
|
||||||
|
parser.add_argument("--settle", type=int, default=200,
|
||||||
|
help="Settle time in ms after changing attenuation "
|
||||||
|
"(default: 200)")
|
||||||
|
|
||||||
|
sub = parser.add_subparsers(dest="test", required=True)
|
||||||
|
|
||||||
|
# AGC linearity
|
||||||
|
p_agc = sub.add_parser("agc-linearity",
|
||||||
|
help="Sweep attenuation at fixed freq, map AGC curve")
|
||||||
|
p_agc.add_argument("--freq", type=float, default=1200.0,
|
||||||
|
help="Test frequency in MHz (default: 1200)")
|
||||||
|
|
||||||
|
# Band flatness
|
||||||
|
p_band = sub.add_parser("band-flatness",
|
||||||
|
help="Sweep CW across IF band, measure AGC response")
|
||||||
|
p_band.add_argument("--start", type=float, default=950.0,
|
||||||
|
help="Start frequency in MHz (default: 950)")
|
||||||
|
p_band.add_argument("--stop", type=float, default=1500.0,
|
||||||
|
help="Stop frequency in MHz (default: 1500)")
|
||||||
|
p_band.add_argument("--step", type=float, default=10.0,
|
||||||
|
help="Frequency step in MHz (default: 10)")
|
||||||
|
|
||||||
|
# Frequency accuracy
|
||||||
|
p_freq = sub.add_parser("freq-accuracy",
|
||||||
|
help="Inject CW at known freqs, measure error")
|
||||||
|
p_freq.add_argument("--freqs", type=str, default="1000,1100,1200,1300,1400",
|
||||||
|
help="Comma-separated test frequencies in MHz")
|
||||||
|
|
||||||
|
# Minimum detectable signal
|
||||||
|
p_mds = sub.add_parser("mds",
|
||||||
|
help="Find minimum detectable signal level")
|
||||||
|
p_mds.add_argument("--freq", type=float, default=1200.0,
|
||||||
|
help="Test frequency in MHz (default: 1200)")
|
||||||
|
|
||||||
|
# BPSK mode 9 probe
|
||||||
|
p_bpsk = sub.add_parser("bpsk-probe",
|
||||||
|
help="Probe BPSK mode 9 with CW carrier")
|
||||||
|
p_bpsk.add_argument("--freq", type=float, default=1200.0,
|
||||||
|
help="Test frequency in MHz (default: 1200)")
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def _connect_attenuator(target: str):
|
||||||
|
"""Connect to HMC472A via auto-detect, USB serial, HTTP REST, or fixed pad.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target: "auto", "fixed:XX" (dB), /dev/ttyACM* (USB), or http://... (REST)
|
||||||
|
"""
|
||||||
|
# Fixed attenuator mode (non-programmable inline pad)
|
||||||
|
if target.startswith("fixed:"):
|
||||||
|
try:
|
||||||
|
fixed_db = float(target.split(":", 1)[1])
|
||||||
|
except ValueError:
|
||||||
|
print(f"HMC472A: invalid fixed value '{target}' (use fixed:20)")
|
||||||
|
sys.exit(1)
|
||||||
|
atten = FixedAttenuator(fixed_db)
|
||||||
|
print(f"HMC472A: fixed {fixed_db:.1f} dB pad (non-programmable)")
|
||||||
|
return atten
|
||||||
|
|
||||||
|
# Auto-detect: try USB serial first, fall back to HTTP
|
||||||
|
if target == "auto":
|
||||||
|
port = detect_hmc472a_serial()
|
||||||
|
if port:
|
||||||
|
print(f"HMC472A: auto-detected USB serial on {port}")
|
||||||
|
target = port
|
||||||
|
else:
|
||||||
|
print("HMC472A: no USB device found, trying HTTP...")
|
||||||
|
target = "http://attenuator.local"
|
||||||
|
|
||||||
|
# USB serial path
|
||||||
|
if target.startswith("/dev/"):
|
||||||
|
try:
|
||||||
|
atten = HMC472ASerial(target)
|
||||||
|
info = atten.identify()
|
||||||
|
print(f"HMC472A: USB serial on {target} "
|
||||||
|
f"(v{info.get('version', '?')}, "
|
||||||
|
f"protocol {info.get('protocol', '?')})")
|
||||||
|
return atten
|
||||||
|
except ImportError:
|
||||||
|
print("HMC472A: pyserial not installed (pip install pyserial)")
|
||||||
|
sys.exit(1)
|
||||||
|
except (OSError, TimeoutError) as e:
|
||||||
|
print(f"HMC472A: cannot open {target} ({e})")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# HTTP REST API
|
||||||
|
atten = HMC472A(target)
|
||||||
|
try:
|
||||||
|
cfg = atten.config()
|
||||||
|
print(f"HMC472A: HTTP on {target} ({cfg.get('hostname', '?')}, "
|
||||||
|
f"v{cfg.get('version', '?')})")
|
||||||
|
return atten
|
||||||
|
except (URLError, OSError) as e:
|
||||||
|
print(f"HMC472A: cannot reach {target} ({e})")
|
||||||
|
print(" Use --attenuator /dev/ttyACMx (USB) or http://host (HTTP)")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = build_parser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Mock mode for testing without hardware
|
||||||
|
mock_mode = os.environ.get("SKYWALKER_MOCK")
|
||||||
|
|
||||||
|
# Set up HMC472A attenuator
|
||||||
|
if mock_mode:
|
||||||
|
atten = MockHMC472A()
|
||||||
|
print("HMC472A: mock mode")
|
||||||
|
else:
|
||||||
|
atten = _connect_attenuator(args.attenuator)
|
||||||
|
|
||||||
|
# Set up NanoVNA
|
||||||
|
nanovna = None
|
||||||
|
if mock_mode:
|
||||||
|
nanovna = MockNanoVNA()
|
||||||
|
print("NanoVNA: mock mode")
|
||||||
|
elif args.nanovna == "auto":
|
||||||
|
NanoVNA = try_import_nanovna()
|
||||||
|
if NanoVNA:
|
||||||
|
try:
|
||||||
|
nanovna = NanoVNA()
|
||||||
|
print(f"NanoVNA: auto mode (mcnanovna)")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"NanoVNA: mcnanovna failed ({e}), falling back to manual")
|
||||||
|
else:
|
||||||
|
print("NanoVNA: mcnanovna not installed, using manual mode")
|
||||||
|
print(" Install: uv pip install -e /path/to/mcnanovna")
|
||||||
|
else:
|
||||||
|
print("NanoVNA: manual mode (you'll be prompted to set frequencies)")
|
||||||
|
|
||||||
|
# Load calibration
|
||||||
|
cal = {}
|
||||||
|
if args.cal:
|
||||||
|
cal = load_cal_file(args.cal)
|
||||||
|
print(f"Calibration: loaded {len(cal)} points from {args.cal}")
|
||||||
|
|
||||||
|
# Open CSV output
|
||||||
|
csv_file = None
|
||||||
|
writer = None
|
||||||
|
if args.output:
|
||||||
|
csv_file, writer = open_csv(args.output)
|
||||||
|
|
||||||
|
# Open SkyWalker-1
|
||||||
|
# SAFETY: Boot demodulator WITHOUT enabling LNB power. ensure_booted()
|
||||||
|
# transiently enables LNB voltage (13-18V on the F-connector), which
|
||||||
|
# would travel backward through the attenuator toward the NanoVNA.
|
||||||
|
# The DC blocker protects against this, but code should never rely on
|
||||||
|
# external protection it cannot verify.
|
||||||
|
if mock_mode:
|
||||||
|
sw = _make_mock_skywalker(args.verbose)
|
||||||
|
print("SkyWalker-1: mock mode")
|
||||||
|
else:
|
||||||
|
sw = SkyWalker1(verbose=args.verbose)
|
||||||
|
sw.open()
|
||||||
|
# Ensure LNB power is OFF before booting demodulator
|
||||||
|
sw.start_intersil(on=False)
|
||||||
|
status = sw.get_config()
|
||||||
|
if not (status & 0x01):
|
||||||
|
sw.boot(on=True)
|
||||||
|
time.sleep(0.5)
|
||||||
|
status = sw.get_config()
|
||||||
|
if not (status & 0x01):
|
||||||
|
print("ERROR: Device failed to start")
|
||||||
|
sys.exit(1)
|
||||||
|
print("SkyWalker-1: booted (LNB power kept OFF)")
|
||||||
|
|
||||||
|
# Confirm LNB power disabled — direct input mode
|
||||||
|
sw.start_intersil(on=False)
|
||||||
|
print("LNB power disabled (direct input mode)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if args.test == "agc-linearity":
|
||||||
|
test_agc_linearity(sw, atten, nanovna, args.freq,
|
||||||
|
writer, csv_file, cal, args.settle)
|
||||||
|
elif args.test == "band-flatness":
|
||||||
|
test_band_flatness(sw, atten, nanovna, args.start, args.stop,
|
||||||
|
args.step, writer, csv_file, cal, args.settle)
|
||||||
|
elif args.test == "freq-accuracy":
|
||||||
|
freqs = [float(f) for f in args.freqs.split(",")]
|
||||||
|
test_freq_accuracy(sw, atten, nanovna, freqs,
|
||||||
|
writer, csv_file, args.settle)
|
||||||
|
elif args.test == "mds":
|
||||||
|
test_mds(sw, atten, nanovna, args.freq,
|
||||||
|
writer, csv_file, args.settle)
|
||||||
|
elif args.test == "bpsk-probe":
|
||||||
|
test_bpsk_probe(sw, atten, nanovna, args.freq,
|
||||||
|
writer, csv_file, args.settle)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\nInterrupted by operator.")
|
||||||
|
finally:
|
||||||
|
# Safe state: maximum attenuation before releasing hardware
|
||||||
|
try:
|
||||||
|
atten.set_db(31.5)
|
||||||
|
print("Attenuator set to 31.5 dB (safe state)")
|
||||||
|
except Exception:
|
||||||
|
pass # best-effort on cleanup path
|
||||||
|
if csv_file:
|
||||||
|
csv_file.flush()
|
||||||
|
csv_file.close()
|
||||||
|
print(f"\nData saved to {args.output}")
|
||||||
|
if not mock_mode:
|
||||||
|
sw.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -377,8 +377,12 @@ class SkyWalker1:
|
|||||||
# -- Power and boot --
|
# -- Power and boot --
|
||||||
|
|
||||||
def boot(self, on: bool = True) -> int:
|
def boot(self, on: bool = True) -> int:
|
||||||
"""Power on/off the 8PSK demodulator."""
|
"""Power on/off the 8PSK demodulator.
|
||||||
data = self._vendor_in(CMD_BOOT_8PSK, value=int(on), length=1)
|
|
||||||
|
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]
|
return data[0]
|
||||||
|
|
||||||
def start_intersil(self, on: bool = True) -> int:
|
def start_intersil(self, on: bool = True) -> int:
|
||||||
|
|||||||
210
tools/stock_fw_compare.py
Normal file
210
tools/stock_fw_compare.py
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compare BCM4500 register behavior: stock EEPROM firmware vs custom firmware.
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
1. Power cycle the SkyWalker-1 (unplug/replug USB)
|
||||||
|
2. Run this script IMMEDIATELY (before loading custom FW)
|
||||||
|
3. The script tests registers under stock firmware first,
|
||||||
|
then loads custom firmware and tests again.
|
||||||
|
|
||||||
|
If stock firmware shows different register behavior, the issue is
|
||||||
|
in our custom firmware's boot sequence.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import usb.core
|
||||||
|
sys.path.insert(0, 'tools')
|
||||||
|
|
||||||
|
VID = 0x09C0
|
||||||
|
PID = 0x0203
|
||||||
|
BCM4500_ADDR = 0x08
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Raw USB helpers (work with any firmware)
|
||||||
|
# ============================================================
|
||||||
|
def vendor_in(dev, cmd, value=0, index=0, length=1):
|
||||||
|
"""Send a vendor IN request and return the response bytes."""
|
||||||
|
return dev.ctrl_transfer(
|
||||||
|
0xC0, # bmRequestType: vendor, device-to-host
|
||||||
|
cmd, # bRequest
|
||||||
|
value, # wValue
|
||||||
|
index, # wIndex
|
||||||
|
length # wLength
|
||||||
|
)
|
||||||
|
|
||||||
|
def vendor_out(dev, cmd, value=0, index=0, data=None):
|
||||||
|
"""Send a vendor OUT request."""
|
||||||
|
dev.ctrl_transfer(
|
||||||
|
0x40, # bmRequestType: vendor, host-to-device
|
||||||
|
cmd, # bRequest
|
||||||
|
value, # wValue
|
||||||
|
index, # wIndex
|
||||||
|
data if data else b''
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_bcm_reg(dev, reg):
|
||||||
|
"""Read one BCM4500 register via stock-compatible I2C read (0xB5).
|
||||||
|
This might not exist on stock firmware, so we use the
|
||||||
|
stock READ_8PSK_REG (0x81) as a fallback."""
|
||||||
|
try:
|
||||||
|
data = vendor_in(dev, 0xB5, value=BCM4500_ADDR, index=reg, length=1)
|
||||||
|
return data[0]
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def read_bcm_reg_stock(dev, reg):
|
||||||
|
"""Read BCM4500 register via stock READ_8PSK_REG (0x81).
|
||||||
|
wValue = register address, returns 1 byte."""
|
||||||
|
try:
|
||||||
|
data = vendor_in(dev, 0x81, value=reg, index=0, length=1)
|
||||||
|
return data[0]
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def read_all_key_regs(dev, method='0xB5'):
|
||||||
|
"""Read key BCM4500 registers."""
|
||||||
|
regs = [0xA0, 0xA2, 0xA4, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB]
|
||||||
|
results = {}
|
||||||
|
for reg in regs:
|
||||||
|
if method == '0xB5':
|
||||||
|
val = read_bcm_reg(dev, reg)
|
||||||
|
else:
|
||||||
|
val = read_bcm_reg_stock(dev, reg)
|
||||||
|
results[reg] = val
|
||||||
|
v_str = f'0x{val:02X}' if val is not None else 'FAIL'
|
||||||
|
print(f' 0x{reg:02X}: {v_str}')
|
||||||
|
return results
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# MAIN
|
||||||
|
# ============================================================
|
||||||
|
print('=== Stock vs Custom Firmware BCM4500 Comparison ===')
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Find the device
|
||||||
|
dev = usb.core.find(idVendor=VID, idProduct=PID)
|
||||||
|
if dev is None:
|
||||||
|
print('ERROR: SkyWalker-1 not found!')
|
||||||
|
print('Make sure to power cycle the device first (stock firmware must be running)')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f'Found SkyWalker-1: Bus {dev.bus} Addr {dev.address}')
|
||||||
|
print(f' VID=0x{dev.idVendor:04X} PID=0x{dev.idProduct:04X}')
|
||||||
|
|
||||||
|
# Try to get firmware version (our custom command)
|
||||||
|
try:
|
||||||
|
fw = vendor_in(dev, 0x80, value=0, index=0, length=3)
|
||||||
|
print(f' Firmware response (0x80): {list(fw)}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' Firmware version (0x80): {e}')
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Phase 1: Test under stock firmware (before boot command)
|
||||||
|
# ============================================================
|
||||||
|
print('\n' + '='*60)
|
||||||
|
print('PHASE 1: Stock firmware — BEFORE boot command')
|
||||||
|
print('='*60)
|
||||||
|
|
||||||
|
print('\n Registers via 0x81 (READ_8PSK_REG):')
|
||||||
|
regs_stock_pre_81 = {}
|
||||||
|
for reg in [0xA0, 0xA2, 0xA4, 0xA6, 0xA7, 0xA8]:
|
||||||
|
val = read_bcm_reg_stock(dev, reg)
|
||||||
|
regs_stock_pre_81[reg] = val
|
||||||
|
v_str = f'0x{val:02X}' if val is not None else 'FAIL'
|
||||||
|
print(f' 0x{reg:02X}: {v_str}')
|
||||||
|
|
||||||
|
print('\n Registers via 0xB5 (I2C_RAW_READ) — may fail on stock FW:')
|
||||||
|
regs_stock_pre_b5 = {}
|
||||||
|
for reg in [0xA0, 0xA2, 0xA4, 0xA6, 0xA7, 0xA8]:
|
||||||
|
val = read_bcm_reg(dev, reg)
|
||||||
|
regs_stock_pre_b5[reg] = val
|
||||||
|
v_str = f'0x{val:02X}' if val is not None else 'N/A (stock FW lacks 0xB5)'
|
||||||
|
print(f' 0x{reg:02X}: {v_str}')
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Phase 2: Boot BCM4500 under stock firmware
|
||||||
|
# ============================================================
|
||||||
|
print('\n' + '='*60)
|
||||||
|
print('PHASE 2: Stock firmware — boot BCM4500 (0x89)')
|
||||||
|
print('='*60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = vendor_in(dev, 0x89, value=1, index=0, length=3)
|
||||||
|
print(f' Boot result: [{result[0]:02X}, {result[1]:02X}, {result[2]:02X}]')
|
||||||
|
cfg = result[0]
|
||||||
|
bits = []
|
||||||
|
if cfg & 0x01: bits.append('Started')
|
||||||
|
if cfg & 0x02: bits.append('FW_Loaded')
|
||||||
|
if cfg & 0x04: bits.append('Intersil')
|
||||||
|
if cfg & 0x08: bits.append('DVBmode')
|
||||||
|
print(f' Config: 0x{cfg:02X} ({" | ".join(bits) if bits else "none"})')
|
||||||
|
print(f' Stage: 0x{result[1]:02X}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' Boot failed: {e}')
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
print('\n Registers via 0x81 after boot:')
|
||||||
|
regs_stock_post_81 = {}
|
||||||
|
for reg in [0xA0, 0xA2, 0xA4, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB]:
|
||||||
|
val = read_bcm_reg_stock(dev, reg)
|
||||||
|
regs_stock_post_81[reg] = val
|
||||||
|
v_str = f'0x{val:02X}' if val is not None else 'FAIL'
|
||||||
|
marker = ''
|
||||||
|
if val is not None and reg in regs_stock_pre_81:
|
||||||
|
if regs_stock_pre_81.get(reg) != val:
|
||||||
|
marker = f' (was 0x{regs_stock_pre_81[reg]:02X})' if regs_stock_pre_81[reg] is not None else ''
|
||||||
|
print(f' 0x{reg:02X}: {v_str}{marker}')
|
||||||
|
|
||||||
|
# Try indirect read via stock firmware's 0x81 with indirect addressing
|
||||||
|
# (0x81 might support indirect reads differently)
|
||||||
|
print('\n Testing indirect register reads via 0x81:')
|
||||||
|
for page in [0x00, 0x06, 0x07, 0x0F]:
|
||||||
|
# Stock firmware might use a different convention for indirect reads
|
||||||
|
# Try reading page register values
|
||||||
|
val = read_bcm_reg_stock(dev, page)
|
||||||
|
v_str = f'0x{val:02X}' if val is not None else 'FAIL'
|
||||||
|
print(f' Page 0x{page:02X} via 0x81: {v_str}')
|
||||||
|
|
||||||
|
# Try signal monitor
|
||||||
|
print('\n Signal status:')
|
||||||
|
try:
|
||||||
|
sig = vendor_in(dev, 0x82, value=0, index=0, length=4)
|
||||||
|
print(f' GET_8PSK_SIGNAL (0x82): {list(sig)}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' 0x82: {e}')
|
||||||
|
|
||||||
|
try:
|
||||||
|
lock = vendor_in(dev, 0x83, value=0, index=0, length=1)
|
||||||
|
print(f' GET_8PSK_LOCK (0x83): 0x{lock[0]:02X}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' 0x83: {e}')
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Summary
|
||||||
|
# ============================================================
|
||||||
|
print('\n' + '='*60)
|
||||||
|
print('SUMMARY')
|
||||||
|
print('='*60)
|
||||||
|
|
||||||
|
def compare_regs(label, regs):
|
||||||
|
vals = set(v for v in regs.values() if v is not None)
|
||||||
|
if len(vals) == 1:
|
||||||
|
print(f' {label}: ALL = 0x{list(vals)[0]:02X}')
|
||||||
|
elif len(vals) == 0:
|
||||||
|
print(f' {label}: ALL FAILED')
|
||||||
|
else:
|
||||||
|
print(f' {label}: Mixed: {", ".join(f"0x{v:02X}" for v in sorted(vals))}')
|
||||||
|
|
||||||
|
compare_regs('Stock FW before boot (0x81)', regs_stock_pre_81)
|
||||||
|
compare_regs('Stock FW after boot (0x81)', regs_stock_post_81)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print('If stock FW shows DIFFERENT register values (not all 0x02),')
|
||||||
|
print('then the BCM4500 is truly functional under stock FW and our')
|
||||||
|
print('custom boot sequence is missing something.')
|
||||||
|
print()
|
||||||
|
print('If stock FW also shows all 0x02, then the register behavior')
|
||||||
|
print('is normal and the 0x02 IS the legitimate power-on value.')
|
||||||
|
|
||||||
|
print('\n=== Done ===')
|
||||||
342
tools/stock_fw_test.py
Normal file
342
tools/stock_fw_test.py
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Stock firmware BCM4500 boot diagnostic.
|
||||||
|
|
||||||
|
Runs the stock v2.06 firmware boot sequence (same as kernel gp8psk driver),
|
||||||
|
then performs comprehensive register dumps to capture the BCM4500 state —
|
||||||
|
including the critical A9/AA/AB PLL registers written by FUN_CODE_10F2
|
||||||
|
(which our custom firmware currently skips).
|
||||||
|
|
||||||
|
Also scans the I2C bus to find device 0x51 (calibration EEPROM) that the
|
||||||
|
stock firmware reads PLL configuration data from.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python tools/stock_fw_test.py [--dump-all] [--i2c-scan]
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- SkyWalker-1 connected via USB (stock firmware loaded)
|
||||||
|
- pyusb installed
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import struct
|
||||||
|
|
||||||
|
try:
|
||||||
|
import usb.core
|
||||||
|
import usb.util
|
||||||
|
except ImportError:
|
||||||
|
print("pyusb required: pip install pyusb")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# USB IDs
|
||||||
|
VID = 0x09C0
|
||||||
|
PID = 0x0203
|
||||||
|
|
||||||
|
# Stock firmware vendor commands (same as kernel driver)
|
||||||
|
GET_8PSK_CONFIG = 0x80
|
||||||
|
I2C_WRITE = 0x83
|
||||||
|
I2C_READ = 0x84
|
||||||
|
ARM_TRANSFER = 0x85
|
||||||
|
TUNE_8PSK = 0x86
|
||||||
|
GET_SIGNAL_STRENGTH = 0x87
|
||||||
|
BOOT_8PSK = 0x89
|
||||||
|
START_INTERSIL = 0x8A
|
||||||
|
SET_LNB_VOLTAGE = 0x8B
|
||||||
|
GET_SIGNAL_LOCK = 0x90
|
||||||
|
GET_FW_VERS = 0x92
|
||||||
|
|
||||||
|
# Config status bits
|
||||||
|
BM_STARTED = 0x01
|
||||||
|
BM_FW_LOADED = 0x02
|
||||||
|
BM_INTERSIL = 0x04
|
||||||
|
|
||||||
|
# I2C addresses (7-bit)
|
||||||
|
BCM4500_ADDR = 0x08
|
||||||
|
BCM3440_ADDR = 0x10
|
||||||
|
EEPROM_ADDR = 0x51 # Calibration EEPROM found in FUN_CODE_10F2 disassembly
|
||||||
|
|
||||||
|
|
||||||
|
def vendor_in(dev, cmd, value=0, index=0, length=1):
|
||||||
|
"""Send a vendor IN control transfer (device -> host)."""
|
||||||
|
return bytes(dev.ctrl_transfer(
|
||||||
|
usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN,
|
||||||
|
cmd, value, index, length, 2000))
|
||||||
|
|
||||||
|
|
||||||
|
def vendor_out(dev, cmd, value=0, index=0, data=None):
|
||||||
|
"""Send a vendor OUT control transfer (host -> device)."""
|
||||||
|
dev.ctrl_transfer(
|
||||||
|
usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_OUT,
|
||||||
|
cmd, value, index, data or b'', 2000)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_config(cfg):
|
||||||
|
"""Decode config status byte to human-readable string."""
|
||||||
|
bits = []
|
||||||
|
names = [
|
||||||
|
(0x01, "Started"), (0x02, "FW_Loaded"), (0x04, "Intersil"),
|
||||||
|
(0x08, "DVBmode"), (0x10, "22kHz"), (0x20, "18V"),
|
||||||
|
(0x40, "DCtuned"), (0x80, "Armed"),
|
||||||
|
]
|
||||||
|
for mask, name in names:
|
||||||
|
if cfg & mask:
|
||||||
|
bits.append(name)
|
||||||
|
return " | ".join(bits) if bits else "(none)"
|
||||||
|
|
||||||
|
|
||||||
|
def i2c_read(dev, addr, reg, length=1):
|
||||||
|
"""Read from I2C device via stock firmware 0x84 command."""
|
||||||
|
return vendor_in(dev, I2C_READ, value=addr, index=reg, length=length)
|
||||||
|
|
||||||
|
|
||||||
|
def i2c_scan(dev, start=0x03, end=0x77):
|
||||||
|
"""Scan I2C bus for responding devices using stock firmware I2C_READ."""
|
||||||
|
found = []
|
||||||
|
for addr in range(start, end + 1):
|
||||||
|
try:
|
||||||
|
val = i2c_read(dev, addr, 0x00, length=1)
|
||||||
|
found.append((addr, val[0]))
|
||||||
|
except usb.core.USBError:
|
||||||
|
pass
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def dump_bcm4500_direct_regs(dev):
|
||||||
|
"""Read all BCM4500 direct registers 0xA0-0xBF after boot.
|
||||||
|
|
||||||
|
These are the PLL/config registers. FUN_CODE_10F2 writes to A0, A9, AA, AB
|
||||||
|
during boot from calibration EEPROM data. Our custom firmware skips this.
|
||||||
|
"""
|
||||||
|
regs = {}
|
||||||
|
for reg in range(0xA0, 0xC0):
|
||||||
|
try:
|
||||||
|
val = i2c_read(dev, BCM4500_ADDR, reg, length=1)
|
||||||
|
regs[reg] = val[0]
|
||||||
|
except usb.core.USBError:
|
||||||
|
regs[reg] = None
|
||||||
|
return regs
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
dump_all = "--dump-all" in sys.argv
|
||||||
|
do_scan = "--i2c-scan" in sys.argv or dump_all
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("Stock Firmware BCM4500 Boot Diagnostic")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# --- Step 0: Find device ---
|
||||||
|
dev = usb.core.find(idVendor=VID, idProduct=PID)
|
||||||
|
if dev is None:
|
||||||
|
print("\nERROR: SkyWalker-1 not found on USB")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"\nFound SkyWalker-1: Bus {dev.bus} Addr {dev.address}")
|
||||||
|
product = usb.util.get_string(dev, dev.iProduct) if dev.iProduct else "?"
|
||||||
|
serial = usb.util.get_string(dev, dev.iSerialNumber) if dev.iSerialNumber else "?"
|
||||||
|
print(f" Product: {product}")
|
||||||
|
print(f" Serial: {serial}")
|
||||||
|
|
||||||
|
# Detach kernel driver if attached
|
||||||
|
for cfg in dev:
|
||||||
|
for intf in cfg:
|
||||||
|
if dev.is_kernel_driver_active(intf.bInterfaceNumber):
|
||||||
|
dev.detach_kernel_driver(intf.bInterfaceNumber)
|
||||||
|
try:
|
||||||
|
dev.set_configuration()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# --- Step 1: Pre-boot state ---
|
||||||
|
print("\n--- Step 1: Pre-boot state ---")
|
||||||
|
cfg = vendor_in(dev, GET_8PSK_CONFIG, length=1)
|
||||||
|
print(f" Config: 0x{cfg[0]:02X} = {decode_config(cfg[0])}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
ver = vendor_in(dev, GET_FW_VERS, length=6)
|
||||||
|
print(f" FW Version: {ver[2]}.{ver[0]:02d}.{ver[1]}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" FW Version: read failed ({e})")
|
||||||
|
|
||||||
|
# --- Step 2: Boot BCM4500 (kernel driver sequence) ---
|
||||||
|
print("\n--- Step 2: BOOT_8PSK (0x89, wValue=1) ---")
|
||||||
|
try:
|
||||||
|
result = vendor_in(dev, BOOT_8PSK, value=1, length=1)
|
||||||
|
print(f" Boot response: 0x{result[0]:02X} = {decode_config(result[0])}")
|
||||||
|
except usb.core.USBError as e:
|
||||||
|
print(f" Boot FAILED: {e}")
|
||||||
|
try:
|
||||||
|
result = vendor_in(dev, BOOT_8PSK, value=1, length=3)
|
||||||
|
print(f" Boot response (3 bytes): {result.hex(' ')}")
|
||||||
|
except usb.core.USBError as e2:
|
||||||
|
print(f" Boot also failed with 3 bytes: {e2}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
cfg = vendor_in(dev, GET_8PSK_CONFIG, length=1)
|
||||||
|
started = bool(cfg[0] & BM_STARTED)
|
||||||
|
fw_loaded = bool(cfg[0] & BM_FW_LOADED)
|
||||||
|
print(f" Config after boot: 0x{cfg[0]:02X} = {decode_config(cfg[0])}")
|
||||||
|
print(f" Started: {started}, FW Loaded: {fw_loaded}")
|
||||||
|
|
||||||
|
# --- Step 3: Enable LNB power supply (Intersil) ---
|
||||||
|
print("\n--- Step 3: START_INTERSIL (0x8A, wValue=1) ---")
|
||||||
|
try:
|
||||||
|
result = vendor_in(dev, START_INTERSIL, value=1, length=1)
|
||||||
|
print(f" Intersil response: 0x{result[0]:02X} = {decode_config(result[0])}")
|
||||||
|
except usb.core.USBError as e:
|
||||||
|
print(f" Intersil FAILED: {e}")
|
||||||
|
|
||||||
|
# --- Step 4: Cancel pending MPEG transfers ---
|
||||||
|
print("\n--- Step 4: ARM_TRANSFER (0x85, wValue=0) ---")
|
||||||
|
try:
|
||||||
|
vendor_out(dev, ARM_TRANSFER, value=0)
|
||||||
|
print(" OK")
|
||||||
|
except usb.core.USBError as e:
|
||||||
|
print(f" Failed: {e}")
|
||||||
|
|
||||||
|
cfg = vendor_in(dev, GET_8PSK_CONFIG, length=1)
|
||||||
|
print(f" Final config: 0x{cfg[0]:02X} = {decode_config(cfg[0])}")
|
||||||
|
|
||||||
|
# --- Step 5: Signal strength (indirect register reads) ---
|
||||||
|
print("\n--- Step 5: Signal reads ---")
|
||||||
|
sig_all_zero = True
|
||||||
|
try:
|
||||||
|
sig = vendor_in(dev, GET_SIGNAL_STRENGTH, length=6)
|
||||||
|
snr_raw = struct.unpack_from('<H', sig, 0)[0]
|
||||||
|
sig_all_zero = all(b == 0 for b in sig)
|
||||||
|
print(f" Signal strength (0x87): {sig.hex(' ')}")
|
||||||
|
print(f" SNR raw: 0x{snr_raw:04X} ({snr_raw}), all_zero: {sig_all_zero}")
|
||||||
|
except usb.core.USBError as e:
|
||||||
|
print(f" Signal strength read failed: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
lock = vendor_in(dev, GET_SIGNAL_LOCK, length=1)
|
||||||
|
print(f" Lock status (0x90): 0x{lock[0]:02X} {'LOCKED' if lock[0] else 'no lock'}")
|
||||||
|
except usb.core.USBError as e:
|
||||||
|
print(f" Lock status read failed: {e}")
|
||||||
|
|
||||||
|
# --- Step 6: FULL BCM4500 direct register dump (0xA0-0xBF) ---
|
||||||
|
# This captures the PLL/clock values written by FUN_CODE_10F2
|
||||||
|
print("\n--- Step 6: BCM4500 direct registers 0xA0-0xBF (post-boot) ---")
|
||||||
|
print(" (A0=config_mode, A9/AA/AB=PLL from EEPROM, A6/A7/A8=indirect)")
|
||||||
|
regs = dump_bcm4500_direct_regs(dev)
|
||||||
|
for reg in sorted(regs.keys()):
|
||||||
|
val = regs[reg]
|
||||||
|
if val is None:
|
||||||
|
print(f" 0x{reg:02X}: FAILED")
|
||||||
|
continue
|
||||||
|
# Check for echo pattern (dead core symptom)
|
||||||
|
echo_expected = min(reg & 0xFE, 0x7E) if reg > 0x7E else (reg & 0xFE)
|
||||||
|
markers = []
|
||||||
|
if val == echo_expected:
|
||||||
|
markers.append("ECHO")
|
||||||
|
if reg in (0xA9, 0xAA, 0xAB):
|
||||||
|
markers.append("★ PLL")
|
||||||
|
if reg == 0xA0:
|
||||||
|
markers.append("CONFIG_MODE")
|
||||||
|
tag = f" ({', '.join(markers)})" if markers else ""
|
||||||
|
print(f" 0x{reg:02X}: 0x{val:02X}{tag}")
|
||||||
|
|
||||||
|
# Highlight the critical PLL values
|
||||||
|
a9 = regs.get(0xA9)
|
||||||
|
aa = regs.get(0xAA)
|
||||||
|
ab = regs.get(0xAB)
|
||||||
|
a0 = regs.get(0xA0)
|
||||||
|
print(f"\n *** Critical PLL registers ***")
|
||||||
|
print(f" A0 (config mode): 0x{a0:02X}" if a0 is not None else " A0: FAILED")
|
||||||
|
print(f" A9 (PLL div?): 0x{a9:02X}" if a9 is not None else " A9: FAILED")
|
||||||
|
print(f" AA (PLL div?): 0x{aa:02X}" if aa is not None else " AA: FAILED")
|
||||||
|
print(f" AB (PLL cfg?): 0x{ab:02X}" if ab is not None else " AB: FAILED")
|
||||||
|
|
||||||
|
# Try multi-byte read of AB (FUN_CODE_10F2 writes variable-length data here)
|
||||||
|
print("\n AB multi-byte read (up to 8 bytes):")
|
||||||
|
try:
|
||||||
|
ab_multi = i2c_read(dev, BCM4500_ADDR, 0xAB, length=8)
|
||||||
|
print(f" {ab_multi.hex(' ')}")
|
||||||
|
except usb.core.USBError as e:
|
||||||
|
print(f" FAILED: {e}")
|
||||||
|
|
||||||
|
# --- Step 7: Other known registers ---
|
||||||
|
print("\n--- Step 7: Other BCM4500 registers ---")
|
||||||
|
other_regs = [(0xF0, "F0"), (0xF8, "F8"), (0xF9, "F9")]
|
||||||
|
for reg, name in other_regs:
|
||||||
|
try:
|
||||||
|
val = i2c_read(dev, BCM4500_ADDR, reg, length=1)
|
||||||
|
echo = min(reg & 0xFE, 0x7E)
|
||||||
|
marker = " (ECHO)" if val[0] == echo else ""
|
||||||
|
print(f" Reg 0x{name}: 0x{val[0]:02X}{marker}")
|
||||||
|
except usb.core.USBError as e:
|
||||||
|
print(f" Reg 0x{name}: FAILED ({e})")
|
||||||
|
|
||||||
|
# --- Step 8: BCM3440 tuner control read ---
|
||||||
|
print("\n--- Step 8: BCM3440 tuner control read (@ 0x10) ---")
|
||||||
|
tuner_ok = False
|
||||||
|
try:
|
||||||
|
val = i2c_read(dev, BCM3440_ADDR, 0x00, length=4)
|
||||||
|
tuner_ok = not all(b == 0 for b in val)
|
||||||
|
print(f" Tuner regs 0x00-0x03: {val.hex(' ')} {'(OK)' if tuner_ok else '(all zero!)'}")
|
||||||
|
except usb.core.USBError as e:
|
||||||
|
print(f" Tuner read failed: {e}")
|
||||||
|
|
||||||
|
# --- Step 9: I2C bus scan ---
|
||||||
|
if do_scan:
|
||||||
|
print("\n--- Step 9: I2C bus scan (0x03-0x77) ---")
|
||||||
|
print(" Scanning for all responding devices...")
|
||||||
|
devices = i2c_scan(dev)
|
||||||
|
if devices:
|
||||||
|
for addr, first_byte in devices:
|
||||||
|
label = ""
|
||||||
|
if addr == BCM4500_ADDR:
|
||||||
|
label = " ← BCM4500 demod"
|
||||||
|
elif addr == BCM3440_ADDR:
|
||||||
|
label = " ← BCM3440 tuner"
|
||||||
|
elif addr == 0x28:
|
||||||
|
label = " ← EEPROM? (0x51 wire >> 1)"
|
||||||
|
elif 0x50 <= addr <= 0x57:
|
||||||
|
label = " ← EEPROM range"
|
||||||
|
elif addr == EEPROM_ADDR:
|
||||||
|
label = " ← device 0x51 from FUN_CODE_10F2!"
|
||||||
|
print(f" 0x{addr:02X} (7-bit) = 0x{addr << 1:02X}/{addr << 1 | 1:02X} (wire) "
|
||||||
|
f"first byte: 0x{first_byte:02X}{label}")
|
||||||
|
else:
|
||||||
|
print(" No devices found!")
|
||||||
|
|
||||||
|
# Try reading from device 0x51 specifically (calibration EEPROM)
|
||||||
|
print("\n --- Device 0x51 probe (calibration EEPROM) ---")
|
||||||
|
for try_addr in [EEPROM_ADDR, 0x50, 0x28, 0x29]:
|
||||||
|
try:
|
||||||
|
val = i2c_read(dev, try_addr, 0x00, length=16)
|
||||||
|
print(f" Addr 0x{try_addr:02X} reg 0x00 (16 bytes): {val.hex(' ')}")
|
||||||
|
# If we get data, try reading more
|
||||||
|
if not all(b == 0xFF for b in val):
|
||||||
|
val2 = i2c_read(dev, try_addr, 0x00, length=64)
|
||||||
|
print(f" Addr 0x{try_addr:02X} reg 0x00 (64 bytes):")
|
||||||
|
for row in range(0, len(val2), 16):
|
||||||
|
chunk = val2[row:row+16]
|
||||||
|
print(f" +{row:02X}: {chunk.hex(' ')}")
|
||||||
|
except usb.core.USBError:
|
||||||
|
print(f" Addr 0x{try_addr:02X}: no response")
|
||||||
|
else:
|
||||||
|
print("\n (use --i2c-scan or --dump-all for I2C bus scan)")
|
||||||
|
|
||||||
|
# --- Summary ---
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
# Check for echo pattern on critical registers
|
||||||
|
pll_echo = (a9 is not None and a9 == 0xA8) or (aa is not None and aa == 0xAA)
|
||||||
|
if not sig_all_zero:
|
||||||
|
print("BCM4500 CORE IS ALIVE under stock firmware!")
|
||||||
|
print(" → Problem is in our custom firmware boot sequence")
|
||||||
|
if a9 is not None and aa is not None:
|
||||||
|
print(f" → PLL values to replicate: A9=0x{a9:02X} AA=0x{aa:02X} AB=0x{ab:02X}")
|
||||||
|
elif pll_echo:
|
||||||
|
print("BCM4500 CORE IS DEAD (echo pattern on PLL registers)")
|
||||||
|
print(" → FUN_CODE_10F2 may not have run, or EEPROM missing")
|
||||||
|
else:
|
||||||
|
print("BCM4500 CORE STATUS UNCERTAIN")
|
||||||
|
print(" → Check register values above for non-echo data")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
24
udev/99-rf-testbench.rules
Normal file
24
udev/99-rf-testbench.rules
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# RF Test Bench udev rules
|
||||||
|
# Install: sudo cp udev/99-rf-testbench.rules /etc/udev/rules.d/ && sudo udevadm control --reload-rules
|
||||||
|
#
|
||||||
|
# Provides non-root access and stable /dev symlinks for:
|
||||||
|
# /dev/skywalker1 - Genpix SkyWalker-1 DVB-S receiver (USB bulk device, not serial)
|
||||||
|
# /dev/nanovna - NanoVNA-H vector network analyzer (ttyACM)
|
||||||
|
# /dev/attenuator - HMC472A digital attenuator on ESP32-S3 (ttyACM)
|
||||||
|
|
||||||
|
# --- Genpix SkyWalker-1 (09c0:0203) ---
|
||||||
|
# Custom firmware: Product="SkyWalker-1 Custom", Serial="0001"
|
||||||
|
# Stock firmware: Product="Genpix SkyWalker-1", Serial="00857"
|
||||||
|
# Kernel dvb_usb_gp8psk driver blacklisted in /etc/modprobe.d/blacklist-gp8psk.conf
|
||||||
|
SUBSYSTEM=="usb", ATTR{idVendor}=="09c0", ATTR{idProduct}=="0203", MODE="0666", SYMLINK+="skywalker1"
|
||||||
|
|
||||||
|
# Cypress FX2 bare/unprogrammed (04b4:8613) - for recovery/development
|
||||||
|
SUBSYSTEM=="usb", ATTR{idVendor}=="04b4", ATTR{idProduct}=="8613", MODE="0666"
|
||||||
|
|
||||||
|
# --- NanoVNA-H (0483:5740) ---
|
||||||
|
# Match on model to avoid hitting other STM32 CDC devices
|
||||||
|
SUBSYSTEM=="tty", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="5740", ATTRS{product}=="NanoVNA-H", MODE="0666", SYMLINK+="nanovna"
|
||||||
|
|
||||||
|
# --- HMC472A attenuator on ESP32-S3 native USB CDC ---
|
||||||
|
# Espressif VID=303a, PID=1001 (USB JTAG/serial), match on product string
|
||||||
|
SUBSYSTEM=="tty", ATTRS{idVendor}=="303a", ATTRS{product}=="hmc472a-attenuator", MODE="0666", SYMLINK+="attenuator"
|
||||||
Loading…
x
Reference in New Issue
Block a user