aboutsummaryrefslogtreecommitdiffstats
path: root/bridge/e4_flash.py
blob: 9677f2db9874c1154b0c5bba24767f6c2bf11202 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
#!/usr/bin/env python3
"""
SPI Flash programmer via 0xE4/0xE5 XDATA register pokes.

All flash operations done purely from the host by poking registers.
No firmware changes needed.

Register sequences matched from emulator trace (trace/flash).

Key discoveries:
  - CA81 bit 0 must be set for flash controller to work
  - CC33=0x04, C805=0x02 also needed for init
  - Address registers are: C8A1=addr[15:8], C8A2=addr[23:16], C8AB=addr[7:0]
  - DATA_LEN_HI=0x80 (32KB) needed for read DMA to execute
  - Flash read data lands at 0x7000, readable via E4
  - Flash write data comes from 0x7000, loaded via bulk OUT (ARM_OUT)

Usage:
  python3 e4_flash.py firmware.bin               # flash and verify
  python3 e4_flash.py --read 0x0000 256           # read flash
  python3 e4_flash.py --id                        # JEDEC ID
"""
import ctypes, time, sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'pcie'))
from tinygrad.runtime.autogen import libusb
from pcie_probe import xdata_read, xdata_write

VID, PID = 0x3801, 0x0001
EP_OUT = 0x02

def flash_init(h):
  """Initialize flash controller — required before any SPI operation."""
  xdata_write(h, 0xCC33, 0x04)
  v = xdata_read(h, 0xCA81, 1)[0]
  xdata_write(h, 0xCA81, v | 0x01)
  xdata_write(h, 0xC805, 0x02)
  xdata_write(h, 0xC8A6, 0x04)  # SPI clock divider

  # Clear block protection bits — retry until SR shows BP=0
  for attempt in range(5):
    flash_wren(h)
    bulk_out(h, bytes([0x00] * 4))
    flash_transaction(h, cmd=0x01, data_len=1, addr_len=0x04, mode=0x01)
    time.sleep(0.01)
    sr = flash_read_status(h)
    if not (sr & 0x1C):
      break
  else:
    raise RuntimeError(f"Failed to clear block protection after 5 attempts: SR=0x{sr:02X}")

def flash_poll_busy(h):
  for _ in range(100000):
    if not (xdata_read(h, 0xC8A9, 1)[0] & 0x01): return
  raise TimeoutError("Flash CSR busy timeout")

def flash_transaction(h, cmd, addr=0, data_len=0, addr_len=0x07, mode=0x00):
  """Run a flash transaction matching the stock bootloader sequence."""
  xdata_write(h, 0xC8AD, mode)
  xdata_write(h, 0xC8AE, 0x00)
  xdata_write(h, 0xC8AF, 0x00)
  xdata_write(h, 0xC8AA, cmd)
  xdata_write(h, 0xC8AC, addr_len)
  # Address: C8A1=addr[7:0], C8A2=addr[15:8], C8AB=addr[23:16]
  xdata_write(h, 0xC8A1, addr & 0xFF)
  xdata_write(h, 0xC8A2, (addr >> 8) & 0xFF)
  xdata_write(h, 0xC8AB, (addr >> 16) & 0xFF)
  # DATA_LEN is big-endian: C8A3=high byte, C8A4=low byte
  xdata_write(h, 0xC8A3, (data_len >> 8) & 0xFF)
  xdata_write(h, 0xC8A4, data_len & 0xFF)
  xdata_write(h, 0xC8A9, 0x01)
  flash_poll_busy(h)
  xdata_write(h, 0xC8AD, 0x00)
  xdata_write(h, 0xC8AD, 0x00)
  xdata_write(h, 0xC8AD, 0x00)
  xdata_write(h, 0xC8AD, 0x00)

def flash_wren(h):
  """WREN — write enable. Does NOT use flash_transaction to avoid clobbering 0x7000 buffer."""
  xdata_write(h, 0xC8AD, 0x00)
  xdata_write(h, 0xC8AA, 0x06)
  xdata_write(h, 0xC8AC, 0x04)
  xdata_write(h, 0xC8A3, 0x00)
  xdata_write(h, 0xC8A4, 0x00)
  xdata_write(h, 0xC8A9, 0x01)
  flash_poll_busy(h)

def flash_read_status(h):
  flash_transaction(h, cmd=0x05, data_len=1, addr_len=0x04, mode=0x00)
  return xdata_read(h, 0x7000, 1)[0]

def flash_wait_wip(h, timeout=10.0):
  t0 = time.monotonic()
  while time.monotonic() - t0 < timeout:
    if not (flash_read_status(h) & 0x01): return
    time.sleep(0.01)
  raise TimeoutError("Flash WIP timeout")

def flash_jedec_id(h):
  flash_transaction(h, cmd=0x9F, data_len=3, addr_len=0x04, mode=0x00)
  return xdata_read(h, 0x7000, 3)

def flash_read(h, addr, size):
  """Read from SPI flash in 4KB DMA chunks, return requested bytes."""
  result = bytearray()
  offset = 0
  while offset < size:
    want = min(4096, size - offset)
    # DMA needs at least 4KB to fill buffer reliably
    dma_len = max(4096, want)
    flash_transaction(h, cmd=0x03, addr=addr + offset, data_len=dma_len, addr_len=0x07, mode=0x00)
    read = 0
    while read < want:
      n = min(255, want - read)
      result.extend(xdata_read(h, 0x7000 + read, n))
      read += n
    offset += want
  return bytes(result)

def flash_block_erase(h, addr):
  """Erase 64KB block."""
  assert (addr & 0xFFFF) == 0
  flash_wren(h)
  flash_transaction(h, cmd=0xD8, addr=addr, data_len=0, addr_len=0x07, mode=0x00)
  flash_wait_wip(h)

def flash_page_program(h, addr, size):
  """Write size bytes from 0x7000 buffer to flash."""
  flash_wren(h)
  flash_transaction(h, cmd=0x02, addr=addr, data_len=size, addr_len=0x07, mode=0x01)
  flash_wait_wip(h)

def bulk_out(h, data):
  """Send data to 0x7000 via bulk OUT."""
  xdata_write(h, 0x9094, 0x10)  # ARM_OUT
  buf = (ctypes.c_ubyte * len(data))(*data)
  transferred = ctypes.c_int()
  ret = libusb.libusb_bulk_transfer(h, EP_OUT, buf, len(data), ctypes.byref(transferred), 5000)
  assert ret == 0, f"Bulk OUT failed: {ret}"
  assert transferred.value == len(data), f"Short write: {transferred.value}/{len(data)}"
  time.sleep(0.001)

def flash_write(h, addr, firmware):
  """Write firmware to flash: save config, erase blocks, restore config, write firmware."""
  total = len(firmware)

  # Save config (first 256 bytes) if erase would destroy it
  config = None
  erase_start = addr & ~0xFFFF
  if erase_start == 0:
    print("  Saving config (0x000-0x0FF)...")
    config = flash_read(h, 0, 256)

  # Erase needed 64KB blocks
  erase_end = (addr + total + 0xFFFF) & ~0xFFFF
  for block in range(erase_start, erase_end, 0x10000):
    print(f"  Erasing block at 0x{block:05X}...")
    flash_block_erase(h, block)

  # Restore config if we saved it
  if config is not None:
    print("  Restoring config...")
    for off in range(0, 256, 128):
      chunk = config[off:off+128]
      if not all(b == 0xFF for b in chunk):
        bulk_out(h, chunk)
        flash_page_program(h, off, 128)

  # Write in 128-byte chunks — 0x7000 buffer only reliably holds 128 bytes
  # (upper half gets overwritten by firmware code/ISR)
  written = 0
  while written < total:
    chunk_size = min(128, total - written)
    chunk = firmware[written:written + chunk_size]
    padded = chunk + bytes((-len(chunk)) % 4)
    bulk_out(h, padded)
    flash_page_program(h, addr + written, chunk_size)
    written += chunk_size
    pct = written * 100 // total
    print(f"\r  Writing: {written}/{total} ({pct}%)", end="", flush=True)
  print()

def flash_verify(h, addr, firmware):
  """Read back flash and compare."""
  total = len(firmware)
  errors = 0
  verified = 0
  while verified < total:
    chunk = min(255, total - verified)
    got = flash_read(h, addr + verified, chunk)
    expected = firmware[verified:verified + chunk]
    for i in range(min(len(got), len(expected))):
      if got[i] != expected[i]:
        if errors < 10:
          print(f"  MISMATCH at 0x{addr + verified + i:05X}: got 0x{got[i]:02X}, expected 0x{expected[i]:02X}")
        errors += 1
    verified += chunk
    pct = verified * 100 // total
    print(f"\r  Verifying: {verified}/{total} ({pct}%)", end="", flush=True)
  print()
  return errors

def flash_test(h):
  """Write a test block to a safe area, read back and verify."""
  import random
  TEST_ADDR = 0x10000  # block 1, safe area (not config or firmware)
  TEST_SIZE = 4096

  print(f"Flash write/read test at 0x{TEST_ADDR:05X} ({TEST_SIZE} bytes)")

  # Generate random test pattern
  rng = random.Random(42)
  pattern = bytes(rng.randint(0, 255) for _ in range(TEST_SIZE))

  # Erase
  print("  Erasing...")
  flash_block_erase(h, TEST_ADDR)
  erased = flash_read(h, TEST_ADDR, 16)
  assert all(b == 0xFF for b in erased), f"Erase failed: {erased.hex()}"

  # Write in 128-byte pages
  print("  Writing...")
  for off in range(0, TEST_SIZE, 128):
    chunk = pattern[off:off+128]
    bulk_out(h, chunk)
    flash_page_program(h, TEST_ADDR + off, 128)

  # Read back and verify
  print("  Verifying...")
  got = flash_read(h, TEST_ADDR, TEST_SIZE)
  errors = 0
  for i in range(TEST_SIZE):
    if got[i] != pattern[i]:
      if errors < 5:
        print(f"    MISMATCH at +0x{i:04X}: got 0x{got[i]:02X}, expected 0x{pattern[i]:02X}")
      errors += 1

  # Clean up: re-erase the test block
  flash_block_erase(h, TEST_ADDR)

  if errors == 0:
    print(f"  PASS: {TEST_SIZE} bytes verified")
  else:
    print(f"  FAIL: {errors} mismatches")
  return errors

def main():
  ctx = ctypes.POINTER(libusb.libusb_context)()
  libusb.libusb_init(ctypes.byref(ctx))
  h = libusb.libusb_open_device_with_vid_pid(ctx, VID, PID)
  assert h, "Device not found"
  libusb.libusb_claim_interface(h, 0)
  flash_init(h)

  if '--test' in sys.argv:
    errors = flash_test(h)
    libusb.libusb_release_interface(h, 0)
    libusb.libusb_close(h)
    libusb.libusb_exit(ctx)
    sys.exit(1 if errors else 0)
  elif '--id' in sys.argv:
    jedec = flash_jedec_id(h)
    print(f"JEDEC ID: {jedec.hex()} (mfr=0x{jedec[0]:02X} type=0x{jedec[1]:02X} cap=0x{jedec[2]:02X})")
  elif '--read' in sys.argv:
    idx = sys.argv.index('--read')
    addr = int(sys.argv[idx + 1], 0)
    size = int(sys.argv[idx + 2], 0) if idx + 2 < len(sys.argv) else 256
    data = flash_read(h, addr, size)
    for off in range(0, len(data), 16):
      line = data[off:off+16]
      hex_str = ' '.join(f'{b:02X}' for b in line)
      ascii_str = ''.join(chr(b) if 32 <= b < 127 else '.' for b in line)
      print(f"  {addr + off:05X}: {hex_str:<48s} {ascii_str}")
  elif len(sys.argv) >= 2 and not sys.argv[1].startswith('-'):
    fw_path = sys.argv[1]
    flash_addr = int(sys.argv[sys.argv.index('--addr') + 1], 0) if '--addr' in sys.argv else 0x100
    with open(fw_path, 'rb') as f:
      fw = f.read()
    print(f"Firmware: {fw_path} ({len(fw)} bytes)")
    print(f"Flash address: 0x{flash_addr:05X}")
    print(f"\nWriting {len(fw)} bytes to flash at 0x{flash_addr:05X}...")
    t0 = time.monotonic()
    flash_write(h, flash_addr, fw)
    print(f"Write done in {time.monotonic() - t0:.2f}s")
    print(f"\nVerifying...")
    t0 = time.monotonic()
    errors = flash_verify(h, flash_addr, fw)
    if errors == 0:
      print(f"PASS: {len(fw)} bytes verified in {time.monotonic() - t0:.2f}s")
    else:
      print(f"FAIL: {errors} byte mismatches")
      sys.exit(1)
    # Reboot device to run new firmware
    if '--no-reset' not in sys.argv:
      print("\nResetting device...")
      libusb.libusb_release_interface(h, 0)
      libusb.libusb_close(h)
      libusb.libusb_exit(ctx)
      import subprocess
      subprocess.run([sys.executable, os.path.join(os.path.dirname(__file__), '..', 'ftdi_debug.py'), '-rn'],
                     capture_output=True)
      print("Done! Device rebooted with new firmware.")
      return
  else:
    print(f"Usage: {sys.argv[0]} <firmware.bin>")
    print(f"       {sys.argv[0]} --read <addr> [size]")
    print(f"       {sys.argv[0]} --id")

  libusb.libusb_release_interface(h, 0)
  libusb.libusb_close(h)
  libusb.libusb_exit(ctx)

if __name__ == '__main__':
  main()