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