-
Notifications
You must be signed in to change notification settings - Fork 55
Expand file tree
/
Copy pathdfu.py
More file actions
263 lines (216 loc) · 9.07 KB
/
dfu.py
File metadata and controls
263 lines (216 loc) · 9.07 KB
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
"""
Usage:
dfu.py --node_id=ID [--bin=PATH | --recovery] [--no-reset] [--force] [--bus=<bus>] [--chan=<chan>] [--bitrate=<bitrate>]
Options:
--node_id=ID The CAN Node ID of the device in DFU mode.
--bin=PATH The path of the .bin file to upload.
--recovery Perform recovery procedure for inaccessible DFU bootloader.
--no-reset Do not perform a reset following successful flashing.
--force Force flashing even if device memory matches the .bin file.
--bus=<bus> One or more interfaces to use, first available is used [default: canine,slcan_disco].
--chan=<chan> The bus device "channel".
--bitrate=<bitrate> CAN bitrate [default: 1000000].
"""
import sys
import os
import time
import inspect
from pathlib import Path
import can
import yaml
from rich.progress import Progress
import IPython
from traitlets.config import Config
from docopt import docopt
from tinymovr.bus_router import init_router, destroy_router
from tinymovr.config import get_bus_config, create_device, configure_logging
from tinymovr.channel import ResponseError
"""
Tinymovr DFU Module
Copyright 2020-2026 MotionLayer P.C.
The Tinymovr Studio IPython-based command line interface
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
this program. If not, see <http://www.gnu.org/licenses/>.
"""
# Define constants
BIN_CHUNK_SIZE = 4
FLASH_START_ADDR = 0x00001000
# NVM config region: pages 120-127 (8 KB at end of flash)
NVM_START_ADDR = 0x0001E000
NVM_SIZE = 8 * 1024
def compare_bin_w_device(device, bin_path, string="Comparing"):
"""
Compare .bin file and device non-volatile memory
"""
with Progress() as progress:
total_size = os.path.getsize(bin_path)
task1 = progress.add_task("[green]{}...".format(string), total=total_size)
with open(bin_path, "rb") as bin_file:
chunk = bin_file.read(BIN_CHUNK_SIZE * 4)
flash_addr = FLASH_START_ADDR
while chunk:
for i in range(BIN_CHUNK_SIZE):
file_value = int.from_bytes(
chunk[i * 4 : (i + 1) * 4], byteorder="little"
)
device_value = device.read_flash_32(flash_addr + i * 4)
if file_value != device_value:
return False
chunk = bin_file.read(BIN_CHUNK_SIZE * 4)
flash_addr += BIN_CHUNK_SIZE * 4
progress.update(task1, advance=BIN_CHUNK_SIZE * 4)
return True
def calculate_local_checksum(chunk):
"""Calculate the checksum for a chunk of data"""
checksum = sum(
int.from_bytes(chunk[i * 4 : (i + 1) * 4], byteorder="little")
for i in range(BIN_CHUNK_SIZE)
)
return checksum & 0xFFFFFFFF # To ensure we get a 32-bit value
def read_nvm_region(device):
"""
Read the NVM config region from flash before erasing.
Returns the region as bytes, or None if it is entirely erased (0xFF).
"""
print("Backing up NVM config region...")
data = bytearray()
word_count = NVM_SIZE // 4
with Progress() as progress:
task = progress.add_task("[cyan]Reading NVM...", total=word_count)
for i in range(word_count):
value = device.read_flash_32(NVM_START_ADDR + i * 4)
data.extend(value.to_bytes(4, byteorder="little"))
progress.update(task, advance=1)
if all(b == 0xFF for b in data):
print("NVM region is empty, nothing to preserve.")
return None
print("NVM config region backed up.")
return bytes(data)
def write_nvm_region(device, data):
"""
Write a previously backed-up NVM config region back to flash.
Uses the scratchpad + commit mechanism (16 bytes per commit).
"""
if data is None:
return
print("Restoring NVM config region...")
chunk_size = BIN_CHUNK_SIZE * 4 # 16 bytes per commit
num_chunks = NVM_SIZE // chunk_size
with Progress() as progress:
task = progress.add_task("[cyan]Writing NVM...", total=num_chunks)
for chunk_idx in range(num_chunks):
offset = chunk_idx * chunk_size
chunk = data[offset : offset + chunk_size]
# Skip chunks that are all 0xFF (already erased)
if all(b == 0xFF for b in chunk):
progress.update(task, advance=1)
continue
for i in range(BIN_CHUNK_SIZE):
value = int.from_bytes(
chunk[i * 4 : (i + 1) * 4], byteorder="little"
)
device.write_scratch_32(i, value)
time.sleep(1e-5)
flash_addr = NVM_START_ADDR + offset
try:
device.commit(flash_addr, device.hash_uint32)
except TypeError:
device.commit(flash_addr)
progress.update(task, advance=1)
print("NVM config region restored.")
def upload_bin(device, bin_path):
"""
Upload a binary file to the device, preserving the NVM config region.
"""
total_size = os.path.getsize(bin_path)
# Back up NVM config region before erasing
nvm_data = read_nvm_region(device)
print("\nErasing flash...")
try:
result = device.erase_all(device.hash_uint32)
except TypeError:
result = device.erase_all()
if result != 0:
print("\nError while erasing!")
return
print("Done.")
with Progress() as progress:
task2 = progress.add_task("[orange]Flashing...", total=total_size)
with open(bin_path, "rb") as bin_file:
chunk = bin_file.read(BIN_CHUNK_SIZE * 4)
flash_addr = FLASH_START_ADDR
while chunk:
for i in range(BIN_CHUNK_SIZE):
value = int.from_bytes(
chunk[i * 4 : (i + 1) * 4], byteorder="little"
)
device.write_scratch_32(i, value)
time.sleep(1e-5)
try:
device_checksum = device.commit(flash_addr, device.hash_uint32)
except TypeError:
device_checksum = device.commit(flash_addr)
local_checksum = calculate_local_checksum(chunk)
if device_checksum != local_checksum:
print(f"Checksum mismatch at address {flash_addr:08X}. Exiting...")
sys.exit(1)
chunk = bin_file.read(BIN_CHUNK_SIZE * 4)
flash_addr += BIN_CHUNK_SIZE * 4
progress.update(task2, advance=BIN_CHUNK_SIZE * 4)
# Restore NVM config region after firmware write
write_nvm_region(device, nvm_data)
def spawn():
# Parse command line arguments
arguments = docopt(__doc__)
node_id = int(arguments["--node_id"])
bin_path = arguments["--bin"]
# Set up the device
buses = arguments["--bus"].rsplit(sep=",")
channel = arguments["--chan"]
bitrate = int(arguments["--bitrate"])
logger = configure_logging()
if not channel:
params = get_bus_config(buses, bitrate=bitrate)
else:
params = {"bustype": buses[0], "channel": channel, "bitrate": bitrate}
if arguments["--recovery"]:
input("Please power off the device and then press any key to continue...")
print("Now power on the device.")
init_router(can.Bus, params, logger=logger, timeout=1.0)
while True:
try:
device = create_device(node_id=node_id)
print("The device is now in DFU mode.")
break
except ResponseError:
pass
else:
init_router(can.Bus, params, logger=logger, timeout=1.0)
device = create_device(node_id=node_id)
if not bin_path:
raise FileNotFoundError(f"No bin file specified!")
# If a non-existing .bin file is specified, raise error
elif bin_path and not Path(bin_path).is_file():
raise FileNotFoundError(f"Bin file {bin_path} not found!")
# If an existing .bin file is specified, upload it to the device
elif bin_path:
if (not arguments["--force"]) and compare_bin_w_device(device, bin_path):
print("Device firmware matches the bin file. Skipping flash.")
else:
if not arguments["--force"]:
print("Device firmware differs from bin file. Proceeding with flash.")
upload_bin(device, bin_path)
compare_bin_w_device(device, bin_path, string="Verifying")
if not arguments["--no-reset"]:
print("Resetting device...")
device.reset()
destroy_router()
if __name__ == "__main__":
spawn()