Skip to content

Commit 29ca637

Browse files
committed
pybricksdev.dfu: add support for new SPIKE Prime hardware
Add a check for the bcdDevice when flashing SPIKE Prime, and use the bootloader size to calculate the firmware address/size.
1 parent 9653be3 commit 29ca637

File tree

1 file changed

+52
-13
lines changed

1 file changed

+52
-13
lines changed

pybricksdev/dfu.py

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424
SPIKE_PRIME_DFU_USB_PID,
2525
)
2626

27-
FIRMWARE_ADDRESS = 0x08008000
28-
FIRMWARE_SIZE = 1 * 1024 * 1024 - 32 * 1024 # 1MiB - 32KiB
27+
BOOTLOADER_SIZE_32K = 32 * 1024
28+
BOOTLOADER_SIZE_64K = 64 * 1024
29+
FLASH_BASE_ADDRESS = 0x08000000
30+
FLASH_SIZE = 1 * 1024 * 1024
2931

3032

3133
ALL_PIDS = {
@@ -36,6 +38,20 @@
3638
ALL_DEVICES = [f"{LEGO_USB_VID:04x}:{pid:04x}" for pid in ALL_PIDS.keys()]
3739

3840

41+
def _get_bootloader_size(pid: int, bcd_device: int | None) -> int:
42+
"""Gets bootloader size for the connected DFU device."""
43+
# New hardware revision of SPIKE Prime released in 2026 has a larger bootloader.
44+
if pid == SPIKE_PRIME_DFU_USB_PID and bcd_device == 0x0300:
45+
return BOOTLOADER_SIZE_64K
46+
47+
return BOOTLOADER_SIZE_32K
48+
49+
50+
def _get_firmware_region(bootloader_size: int) -> tuple[int, int]:
51+
"""Gets firmware flash address and size from bootloader size."""
52+
return FLASH_BASE_ADDRESS + bootloader_size, FLASH_SIZE - bootloader_size
53+
54+
3955
def _get_dfu_util() -> ContextManager[os.PathLike[str] | str]:
4056
"""Gets ``dfu-util`` command line tool path.
4157
@@ -62,9 +78,9 @@ def _get_dfu_util() -> ContextManager[os.PathLike[str] | str]:
6278
return nullcontext(dfu_util)
6379

6480

65-
def _get_vid_pid(dfu_util: os.PathLike[str] | str) -> str:
81+
def _get_dfu_device_info(dfu_util: os.PathLike[str] | str) -> tuple[str, int]:
6682
"""
67-
Gets the VID and PID of a connected LEGO DFU device.
83+
Gets the VID:PID and bootloader size of a connected LEGO DFU device.
6884
6985
Returns: The first matching LEGO DFU device from ``dfu-util --list``
7086
@@ -79,7 +95,16 @@ def _get_vid_pid(dfu_util: os.PathLike[str] | str) -> str:
7995
dev_id = line[line.index(b"[") + 1 : line.index(b"]")].decode()
8096

8197
if dev_id in ALL_DEVICES:
82-
return dev_id
98+
pid = int(dev_id.split(":", maxsplit=1)[1], 16)
99+
bcd_device = None
100+
101+
try:
102+
i = line.index(b"ver=") + 4
103+
bcd_device = int(line[i : i + 4].decode(), 16)
104+
except (ValueError, IndexError):
105+
pass
106+
107+
return dev_id, _get_bootloader_size(pid, bcd_device)
83108

84109
raise RuntimeError("No LEGO DFU USB device found")
85110

@@ -98,6 +123,8 @@ def backup_dfu(file: BinaryIO) -> None:
98123
# if libusb was not found, try using dfu-util command line tool
99124

100125
with _get_dfu_util() as dfu_util:
126+
dev_id, bootloader_size = _get_dfu_device_info(dfu_util)
127+
firmware_address, firmware_size = _get_firmware_region(bootloader_size)
101128
file.close()
102129

103130
# dfu-util won't overwrite existing files so we have to do that first
@@ -108,11 +135,11 @@ def backup_dfu(file: BinaryIO) -> None:
108135
[
109136
dfu_util,
110137
"--device",
111-
f",{_get_vid_pid(dfu_util)}",
138+
f",{dev_id}",
112139
"--alt",
113140
"0",
114141
"--dfuse-address",
115-
f"{FIRMWARE_ADDRESS}:{FIRMWARE_SIZE}",
142+
f"{firmware_address}:{firmware_size}",
116143
"--upload",
117144
file.name,
118145
]
@@ -140,18 +167,20 @@ def restore_dfu(file: BinaryIO) -> None:
140167
# if libusb was not found, try using dfu-util command line tool
141168

142169
with _get_dfu_util() as dfu_util:
170+
dev_id, bootloader_size = _get_dfu_device_info(dfu_util)
171+
firmware_address, _ = _get_firmware_region(bootloader_size)
143172
file.close()
144173

145174
exit(
146175
call(
147176
[
148177
dfu_util,
149178
"--device",
150-
f",{_get_vid_pid(dfu_util)}",
179+
f",{dev_id}",
151180
"--alt",
152181
"0",
153182
"--dfuse-address",
154-
f"{FIRMWARE_ADDRESS}:leave",
183+
f"{firmware_address}:leave",
155184
"--download",
156185
file.name,
157186
]
@@ -164,7 +193,6 @@ def flash_dfu(firmware_bin: bytes, metadata: AnyFirmwareMetadata) -> None:
164193

165194
with TemporaryDirectory() as out_dir:
166195
outfile = os.path.join(out_dir, "firmware.dfu")
167-
target: dfu_create.Image = {"address": FIRMWARE_ADDRESS, "data": firmware_bin}
168196

169197
try:
170198
# Determine correct product ID
@@ -178,7 +206,8 @@ def flash_dfu(firmware_bin: bytes, metadata: AnyFirmwareMetadata) -> None:
178206
)
179207
exit(1)
180208

181-
product_id = devices[0].idProduct
209+
product_id = int(devices[0].idProduct)
210+
bcd_device = int(devices[0].bcdDevice)
182211
if product_id not in ALL_PIDS:
183212
print(f"Unknown USB product ID: {product_id:04X}", file=sys.stderr)
184213
exit(1)
@@ -187,6 +216,13 @@ def flash_dfu(firmware_bin: bytes, metadata: AnyFirmwareMetadata) -> None:
187216
print("Incorrect firmware type for this hub", file=sys.stderr)
188217
exit(1)
189218

219+
bootloader_size = _get_bootloader_size(product_id, bcd_device)
220+
firmware_address, _ = _get_firmware_region(bootloader_size)
221+
target: dfu_create.Image = {
222+
"address": firmware_address,
223+
"data": firmware_bin,
224+
}
225+
190226
# Create dfu file
191227
device = "0x{0:04x}:0x{1:04x}".format(LEGO_USB_VID, product_id)
192228
dfu_create.build(outfile, [[target]], device)
@@ -225,6 +261,9 @@ def flash_dfu(firmware_bin: bytes, metadata: AnyFirmwareMetadata) -> None:
225261
# if libusb was not found, try using dfu-util command line tool
226262

227263
with _get_dfu_util() as dfu_util:
264+
dev_id, bootloader_size = _get_dfu_device_info(dfu_util)
265+
firmware_address, _ = _get_firmware_region(bootloader_size)
266+
228267
with open(os.path.join(out_dir, "firmware.bin"), "wb") as bin_file:
229268
bin_file.write(firmware_bin)
230269

@@ -233,14 +272,14 @@ def flash_dfu(firmware_bin: bytes, metadata: AnyFirmwareMetadata) -> None:
233272
[
234273
dfu_util,
235274
"--device",
236-
f",{_get_vid_pid(dfu_util)}",
275+
f",{dev_id}",
237276
"--alt",
238277
"0",
239278
# We have to use dfuse option to be able to use
240279
# "leave" to exit DFU mode after flashing. --reset
241280
# doesn't work on Windows, so we can't use a .dfu file
242281
"--dfuse-address",
243-
f"{target['address']}:leave",
282+
f"{firmware_address}:leave",
244283
"--download",
245284
bin_file.name,
246285
]

0 commit comments

Comments
 (0)