mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-12 23:43:44 +08:00
250 lines
10 KiB
Python
250 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""Direct Navi31 ROM_SW access for GD25LQ16E-class 2 MiB SPI flash."""
|
|
from __future__ import annotations
|
|
import argparse, hashlib, sys, time
|
|
from pathlib import Path
|
|
from common import MMIO, open_gpu, wait_until
|
|
|
|
FLASH_SIZE, SECTOR_SIZE, PAGE_SIZE, MAX_DATA = 0x200000, 0x1000, 0x100, 0x100
|
|
ROM_CNTL, PAGE_MIRROR_CNTL = 0x5A380, 0x5A384
|
|
ROM_SW_CNTL, ROM_SW_STATUS, ROM_SW_COMMAND, ROM_SW_DATA = 0x5A3A0, 0x5A3A4, 0x5A3A8, 0x5A3B0
|
|
GPIO_PAD_MASK, GPIO_PAD_A, GPIO_PAD_EN = 0x5A504, 0x5A508, 0x5A510
|
|
SPI_GPIO_BITS, RETURN_DATA_EN = 0x780, 0x80000
|
|
EXPECTED_JEDEC = b'\xc8\x60\x15'
|
|
|
|
|
|
class Navi31SPI:
|
|
def __init__(self, pci_dev, prescale: int = 8):
|
|
if not 0 <= prescale <= 15: raise ValueError("prescale must be 0..15")
|
|
self.mmio = MMIO(pci_dev)
|
|
rc = self.mmio.read32(ROM_CNTL)
|
|
# Select the prescaler instead of inheriting a potentially unusable BL value.
|
|
self.mmio.write32(ROM_CNTL, (rc & 0xE0FFFFFF) | (1 << 28) | (prescale << 24) | 1)
|
|
|
|
def transfer(self, opcode: int, *, address: int = 0, address_len: int = 0,
|
|
data_out: bytes = b'', data_in: int = 0, timeout: float = 2.0) -> bytes:
|
|
if data_out and data_in: raise ValueError("simultaneous TX and RX is unsupported")
|
|
if not 0 <= address_len <= 3: raise ValueError("address_len must be 0..3")
|
|
count = len(data_out) if data_out else data_in
|
|
if not 0 <= count <= MAX_DATA: raise ValueError(f"transfer data must be <= {MAX_DATA} bytes")
|
|
ncmd = 1 + address_len
|
|
m = self.mmio
|
|
gpio_mask, gpio_a, gpio_en = m.read32(GPIO_PAD_MASK), m.read32(GPIO_PAD_A), m.read32(GPIO_PAD_EN)
|
|
page_mirror, rom_cntl = m.read32(PAGE_MIRROR_CNTL), m.read32(ROM_CNTL)
|
|
try:
|
|
m.write32(GPIO_PAD_MASK, gpio_mask & ~SPI_GPIO_BITS)
|
|
m.write32(GPIO_PAD_A, gpio_a & ~SPI_GPIO_BITS)
|
|
m.write32(GPIO_PAD_EN, gpio_en & ~SPI_GPIO_BITS)
|
|
m.write32(PAGE_MIRROR_CNTL, (page_mirror & 0xF1FFFFFF) | 0x06000000)
|
|
m.write32(ROM_CNTL, (rom_cntl & ~0xF) | 8)
|
|
m.write32(ROM_SW_CNTL, 0)
|
|
m.write32(ROM_SW_STATUS, 0)
|
|
if m.read32(ROM_SW_STATUS) != 0: raise RuntimeError("ROM_SW_STATUS did not clear")
|
|
|
|
# Navi31 serializes the low instruction byte first, followed by ADDRESS[23:0].
|
|
m.write32(ROM_SW_COMMAND, ((address & 0xFFFFFF) << 8) | (opcode & 0xFF))
|
|
for offset in range(0, len(data_out), 4):
|
|
word = data_out[offset:offset+4].ljust(4, b'\0')
|
|
m.write32(ROM_SW_DATA + offset, int.from_bytes(word, 'big'))
|
|
|
|
control = ((ncmd - 1) << 16) | (RETURN_DATA_EN if data_in else 0) | count
|
|
m.write32(ROM_SW_CNTL, control)
|
|
m.read32(ROM_SW_CNTL) # posted-write flush
|
|
wait_until(lambda: m.read32(ROM_SW_STATUS) & 1, timeout,
|
|
f"ROM_SW transaction timeout (status={m.read32(ROM_SW_STATUS):#x}); engine may be gated after SOS boot")
|
|
return m.read(ROM_SW_DATA, (data_in + 3) & ~3)[:data_in] if data_in else b''
|
|
finally:
|
|
m.write32(ROM_SW_CNTL, 0)
|
|
m.write32(ROM_SW_STATUS, 0)
|
|
m.write32(ROM_CNTL, rom_cntl)
|
|
m.write32(PAGE_MIRROR_CNTL, page_mirror)
|
|
m.write32(GPIO_PAD_A, gpio_a)
|
|
m.write32(GPIO_PAD_EN, gpio_en)
|
|
m.write32(GPIO_PAD_MASK, gpio_mask)
|
|
|
|
|
|
class GD25LQ16E:
|
|
def __init__(self, spi: Navi31SPI): self.spi = spi
|
|
|
|
def read_register(self, opcode: int, count: int = 1) -> bytes:
|
|
# Navi31 exposes the preceding transaction's RX capture. Prime identically.
|
|
self.spi.transfer(opcode, data_in=max(2, count))
|
|
return self.spi.transfer(opcode, data_in=count)
|
|
|
|
def status(self, opcode: int = 0x05) -> int: return self.read_register(opcode)[0]
|
|
def rdid(self) -> bytes: return self.read_register(0x9F, 4)
|
|
|
|
def sfdp(self, count: int = 20) -> bytes:
|
|
# 5Ah has one dummy byte after its 24-bit address; retain it for diagnostics.
|
|
self.spi.transfer(0x5A, address_len=3, data_in=count)
|
|
return self.spi.transfer(0x5A, address_len=3, data_in=count)
|
|
|
|
def wait_idle(self, timeout: float = 2.0) -> int:
|
|
end = time.monotonic() + timeout
|
|
while time.monotonic() < end:
|
|
sr1 = self.status()
|
|
if not sr1 & 1: return sr1
|
|
time.sleep(0.002)
|
|
raise TimeoutError(f"flash remained busy for {timeout}s")
|
|
|
|
def write_enable(self):
|
|
self.spi.transfer(0x06)
|
|
sr1 = self.status()
|
|
if not sr1 & 2: raise RuntimeError(f"WREN failed (SR1={sr1:#04x})")
|
|
|
|
def clear_cmp(self):
|
|
sr1, sr2 = self.status(), self.status(0x35)
|
|
if not sr2 & 0x40: return False
|
|
self.write_enable()
|
|
# BUSY/WEL are not writable; preserve all protection/QE fields except CMP.
|
|
self.spi.transfer(0x01, data_out=bytes((sr1 & 0xFC, sr2 & ~0x40)))
|
|
self.wait_idle(1.0)
|
|
new_sr2 = self.status(0x35)
|
|
if new_sr2 & 0x40: raise RuntimeError(f"failed to clear CMP (SR2={new_sr2:#04x})")
|
|
return True
|
|
|
|
def erase_sector(self, address: int):
|
|
if address & (SECTOR_SIZE - 1): raise ValueError("sector address is not 4 KiB aligned")
|
|
self.write_enable()
|
|
self.spi.transfer(0x20, address=address, address_len=3)
|
|
self.wait_idle(2.0)
|
|
|
|
def program_page(self, address: int, data: bytes):
|
|
if not data or len(data) > PAGE_SIZE or (address & 0xFF) + len(data) > PAGE_SIZE:
|
|
raise ValueError("page program crosses a 256-byte boundary")
|
|
self.write_enable()
|
|
self.spi.transfer(0x02, address=address, address_len=3, data_out=data)
|
|
self.wait_idle(1.0)
|
|
|
|
def read(self, address: int, count: int) -> bytes:
|
|
if address < 0 or count < 0 or address + count > FLASH_SIZE: raise ValueError("read outside 2 MiB flash")
|
|
output = bytearray()
|
|
while count:
|
|
size = min(count, MAX_DATA)
|
|
self.spi.transfer(0x03, address=address, address_len=3, data_in=size)
|
|
output += self.spi.transfer(0x03, address=address, address_len=3, data_in=size)
|
|
address, count = address + size, count - size
|
|
return bytes(output)
|
|
|
|
|
|
def has_jedec(raw: bytes) -> bool:
|
|
return EXPECTED_JEDEC in raw + raw[:2]
|
|
|
|
|
|
def open_flash(args) -> GD25LQ16E:
|
|
flash = GD25LQ16E(Navi31SPI(open_gpu(args.device, 'usb'), args.prescale))
|
|
raw = flash.rdid()
|
|
if not has_jedec(raw): raise RuntimeError(f"unexpected GD25LQ16E JEDEC capture: {raw.hex()}")
|
|
return flash
|
|
|
|
|
|
def cmd_info(args):
|
|
f = open_flash(args)
|
|
sr1, sr2, sr3 = f.status(), f.status(0x35), f.status(0x15)
|
|
sfdp = f.sfdp(24)
|
|
pos = sfdp.find(b'SFDP')
|
|
print(f"JEDEC capture: {f.rdid().hex()} (C8 60 15 detected)")
|
|
print(f"SR1/SR2/SR3: {sr1:02x}/{sr2:02x}/{sr3:02x} CMP={'set' if sr2 & 0x40 else 'clear'}")
|
|
print(f"SFDP capture: {sfdp.hex()} signature_offset={pos}")
|
|
|
|
|
|
def cmd_read(args):
|
|
data = open_flash(args).read(args.address, args.size)
|
|
if args.output: Path(args.output).write_bytes(data)
|
|
else: print(data.hex())
|
|
|
|
|
|
def cmd_dump(args):
|
|
f = open_flash(args)
|
|
out = Path(args.output)
|
|
digest = hashlib.sha256()
|
|
with out.open('wb') as file:
|
|
for address in range(0, FLASH_SIZE, SECTOR_SIZE):
|
|
data = f.read(address, SECTOR_SIZE)
|
|
file.write(data)
|
|
digest.update(data)
|
|
if not (address & 0xFFFF): print(f"{address + SECTOR_SIZE:#08x}/{FLASH_SIZE:#08x}", flush=True)
|
|
print(f"wrote {out} sha256={digest.hexdigest()}")
|
|
|
|
|
|
def cmd_verify(args):
|
|
expected = Path(args.image).read_bytes()
|
|
if len(expected) != FLASH_SIZE: raise ValueError(f"image must be exactly {FLASH_SIZE:#x} bytes")
|
|
f = open_flash(args)
|
|
digest = hashlib.sha256()
|
|
for address in range(0, FLASH_SIZE, SECTOR_SIZE):
|
|
got, wanted = f.read(address, SECTOR_SIZE), expected[address:address+SECTOR_SIZE]
|
|
digest.update(got)
|
|
if got != wanted:
|
|
index = next(i for i, (a, b) in enumerate(zip(got, wanted)) if a != b)
|
|
raise RuntimeError(f"verify mismatch at {address+index:#x}: flash={got[index]:02x} image={wanted[index]:02x}")
|
|
print(f"verified {FLASH_SIZE:#x} bytes sha256={digest.hexdigest()}")
|
|
|
|
|
|
def cmd_flash(args):
|
|
if not args.yes: raise RuntimeError("refusing to write without --yes")
|
|
image = Path(args.image).read_bytes()
|
|
if len(image) != FLASH_SIZE: raise ValueError(f"image must be exactly {FLASH_SIZE:#x} bytes")
|
|
total_sectors = FLASH_SIZE // SECTOR_SIZE
|
|
start, count = args.start_sector, args.sector_count if args.sector_count is not None else total_sectors - args.start_sector
|
|
if not 0 <= start < total_sectors or not 1 <= count <= total_sectors - start: raise ValueError("invalid sector range")
|
|
f = open_flash(args)
|
|
if f.status(0x35) & 0x40:
|
|
if not args.clear_cmp: raise RuntimeError("CMP protects the full array; rerun with --clear-cmp")
|
|
f.clear_cmp()
|
|
print("cleared SR2.CMP", flush=True)
|
|
begin = time.monotonic()
|
|
for sector in range(start, start + count):
|
|
address = sector * SECTOR_SIZE
|
|
wanted = image[address:address+SECTOR_SIZE]
|
|
f.erase_sector(address)
|
|
for offset in range(0, SECTOR_SIZE, PAGE_SIZE):
|
|
page = wanted[offset:offset+PAGE_SIZE]
|
|
if page != b'\xff' * PAGE_SIZE: f.program_page(address + offset, page)
|
|
got = f.read(address, SECTOR_SIZE)
|
|
if got != wanted:
|
|
index = next(i for i, (a, b) in enumerate(zip(got, wanted)) if a != b)
|
|
raise RuntimeError(f"verify mismatch at {address+index:#x}: flash={got[index]:02x} image={wanted[index]:02x}")
|
|
print(f"OK sector {sector:03d}/{total_sectors-1} @{address:#07x} elapsed={time.monotonic()-begin:.1f}s", flush=True)
|
|
|
|
|
|
def parser():
|
|
p = argparse.ArgumentParser(description=__doc__)
|
|
p.add_argument('--device', type=int, default=0, help='USB bridge device index')
|
|
p.add_argument('--prescale', type=int, default=8, help='SCK prescaler 0..15 (default: 8)')
|
|
sub = p.add_subparsers(dest='command', required=True)
|
|
sub.add_parser('info', help='read JEDEC, status and SFDP').set_defaults(func=cmd_info)
|
|
|
|
r = sub.add_parser('read', help='read a flash range')
|
|
r.add_argument('address', type=lambda x:int(x, 0))
|
|
r.add_argument('size', type=lambda x:int(x, 0))
|
|
r.add_argument('-o', '--output')
|
|
r.set_defaults(func=cmd_read)
|
|
|
|
d = sub.add_parser('dump', help='dump the complete 2 MiB flash')
|
|
d.add_argument('output')
|
|
d.set_defaults(func=cmd_dump)
|
|
|
|
v = sub.add_parser('verify', help='compare the complete flash with an image')
|
|
v.add_argument('image')
|
|
v.set_defaults(func=cmd_verify)
|
|
|
|
w = sub.add_parser('flash', help='erase, program, and verify one or more sectors')
|
|
w.add_argument('image')
|
|
w.add_argument('--start-sector', type=lambda x:int(x, 0), default=0)
|
|
w.add_argument('--sector-count', type=lambda x:int(x, 0))
|
|
w.add_argument('--clear-cmp', action='store_true')
|
|
w.add_argument('--yes', action='store_true')
|
|
w.set_defaults(func=cmd_flash)
|
|
return p
|
|
|
|
|
|
def main():
|
|
args = parser().parse_args()
|
|
try: args.func(args)
|
|
except (RuntimeError, TimeoutError, ValueError, OSError) as error:
|
|
print(f"error: {error}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
|
|
if __name__ == '__main__': main()
|