-
Notifications
You must be signed in to change notification settings - Fork 22
/
pymcuprog_main.py
566 lines (487 loc) · 21.8 KB
/
pymcuprog_main.py
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
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
"""
Python MCU programmer, CLI main program
"""
# Python 3 compatibility for Python 2
from __future__ import print_function
# utils
import time
import os
from copy import copy
from logging import getLogger
from .backend import Backend, SessionConfig
from .toolconnection import ToolUsbHidConnection, ToolSerialConnection
from .deviceinfo.memorynames import MemoryNameAliases, MemoryNames
from .deviceinfo.eraseflags import ChiperaseEffect
from .deviceinfo.deviceinfo import get_supported_devices
from .deviceinfo.deviceinfokeys import DeviceMemoryInfoKeys
from .utils import print_tool_info, showdata, verify_from_bin, compare
from .hexfileutils import write_memories_to_hex, write_memory_to_hex, read_memories_from_hex
from .pymcuprog_errors import PymcuprogNotSupportedError, PymcuprogSessionConfigError, \
PymcuprogToolConnectionError, PymcuprogDeviceLockedError, PymcuprogError
try:
from .version import VERSION, BUILD_DATE, COMMIT_ID
except ImportError:
VERSION = "0.0.0"
COMMIT_ID = "N/A"
BUILD_DATE = "N/A"
STATUS_SUCCESS = 0
STATUS_FAILURE = 1
# Only include memories that can be written when writing memories to hex file
WRITE_TO_HEX_MEMORIES = [MemoryNames.EEPROM, MemoryNames.FLASH, MemoryNames.FUSES, MemoryNames.CONFIG_WORD, MemoryNames.USER_ROW]
def pymcuprog(args):
"""
Main program
"""
logger = getLogger(__name__)
if args.version or args.release_info:
print("pymcuprog version {}".format(VERSION))
if args.release_info:
print("Build date: {}".format(BUILD_DATE))
print("Commit ID: {}".format(COMMIT_ID))
return STATUS_SUCCESS
backend = Backend()
toolconnection = _setup_tool_connection(args)
try:
backend.connect_to_tool(toolconnection)
except PymcuprogToolConnectionError as error:
print(error)
return STATUS_FAILURE
status = None
if args.tool not in ['uart']:
# This section can initialise all features requiring non-UART transports
# DAP info only available on native CMSIS-DAP
dap_info = backend.read_tool_info()
print_tool_info(dap_info)
# Targetless actions, only available on HID tools
status = _debugger_actions(backend, args)
if status is not None:
backend.disconnect_from_tool()
return status
device_selected = _select_target_device(backend, args)
if device_selected is None:
backend.disconnect_from_tool()
return STATUS_FAILURE
status = _start_session(backend, device_selected, args)
if status != STATUS_SUCCESS:
backend.disconnect_from_tool()
return status
# -x timer argument
time_start = None
if args.timing:
print("Starting timer")
time_start = time.time()
try:
_programming_actions(backend, args)
except PymcuprogError as exc:
logger.error("%s", exc)
status = STATUS_FAILURE
except Exception as exc:
logger.error("Operation failed with %s: %s", type(exc).__name__, exc)
logger.debug(exc, exc_info=True) # get traceback if debug loglevel
status = STATUS_FAILURE
backend.end_session()
backend.disconnect_from_tool()
if args.timing:
time_stop = time.time()
print("Operation took {0:.03f}s".format(time_stop - time_start))
print("Done.")
return status
def _action_getvoltage(backend):
voltage = backend.read_target_voltage()
print("Measured voltage: {0:0.2f}V".format(voltage))
return STATUS_SUCCESS
def _action_getsupplyvoltage(backend):
voltage = backend.read_supply_voltage_setpoint()
print("Supply voltage set to {0:0.2f}V".format(voltage))
return STATUS_SUCCESS
def _action_getusbvoltage(backend):
voltage = backend.read_usb_voltage()
print("USB voltage is {0:0.2f}V".format(voltage))
return STATUS_SUCCESS
def _action_setsupplyvoltage(backend, literal):
voltage = backend.read_supply_voltage_setpoint()
print("Supply voltage is currently set to {0:0.2f}V".format(voltage))
if literal is None:
print("Specify voltage in Volts using -l <literal>")
else:
setvoltage = literal[0]
if setvoltage == voltage:
print("Voltage is already right where you want it.")
else:
print("Setting supply voltage to {0:0.2f}V".format(setvoltage))
backend.set_supply_voltage_setpoint(setvoltage)
voltage = backend.read_supply_voltage_setpoint()
print("Supply voltage is now set to {0:0.2f}V".format(voltage))
# Static delay to let the target voltage settle before reading it out
# Alternatively a retry loop could be used, but it is difficult to know when to terminate
# the loop as sometimes the final voltage is not known, for example if setting the voltage
# to 5.5V the actual voltage will depend upon the USB voltage. If the USB voltage is only
# 4.9V the target voltage will never reach more than 4.9V
time.sleep(0.5)
voltage = backend.read_target_voltage()
print("Measured voltage: {0:0.2f}V".format(voltage))
return STATUS_SUCCESS
def _action_reboot_debugger(backend):
print("Rebooting tool...")
backend.reboot_tool()
return STATUS_SUCCESS
def _action_ping(backend):
print("Pinging device...")
response = backend.read_device_id()
idstring = ''
for idbyte in response:
idstring = '{:02X}'.format(idbyte) + idstring
print("Ping response: {}".format(idstring))
return STATUS_SUCCESS
def _action_erase(backend, args):
if args.memory is None or args.memory == MemoryNameAliases.ALL:
print("Chip/Bulk erase:")
for memname in backend.device_memory_info.mem_by_name:
effect = backend.get_chiperase_effect(memname)
if effect != ChiperaseEffect.NOT_ERASED:
print("- Memory type {} is {}".format(memname, effect))
else:
if backend.is_isolated_erase_possible(args.memory):
print("Erasing {}...".format(args.memory))
else:
print("ERROR: {} memory can't be erased or "
"can't be erased without affecting other memories".format(args.memory))
chiperase_effect = backend.get_chiperase_effect(args.memory)
if chiperase_effect != ChiperaseEffect.NOT_ERASED:
print("{} memory is {} by a chip/bulk erase".format(args.memory, chiperase_effect))
print("Use erase without -m option to erase this memory")
return STATUS_FAILURE
backend.erase(args.memory, address=None)
print("Erased.")
return STATUS_SUCCESS
def _action_read(backend, args):
# Reading with bytes argument requires that memory type is specified
if args.bytes != 0 and args.memory == MemoryNameAliases.ALL:
print("Memory area must be specified when number of bytes is specified.")
return STATUS_FAILURE
print("Reading...")
result = backend.read_memory(args.memory, args.offset, args.bytes)
# If a filename is specified, write to it
hexfile = False
binary = False
filepath = None
if args.filename is not None:
filepath = os.path.normpath(args.filename)
prefix, postfix = _get_file_prefix_and_postfix(filepath)
# If it ends in hex, use intel hex format, else binary
if postfix == 'hex':
hexfile = True
else:
binary = True
# Print the data or save it to a file
if hexfile:
if args.memory == MemoryNameAliases.ALL:
# Only memories that can be written should go into the hex file
result_to_write = _extract_writeable_memories(result)
write_memories_to_hex(filepath, result_to_write)
else:
write_memory_to_hex(filepath, result[0], args.offset)
print("Data written to hex file: '{0:s}'".format(filepath))
elif binary:
for item in result:
memory_name = item.memory_info[DeviceMemoryInfoKeys.NAME]
data = item.data
filepath = "{}_{}.{}".format(prefix, memory_name, postfix)
# Binary files does not have addressing, and needs a split on memory type
with open(filepath, "wb") as binfile:
binfile.write(data)
print("Data written to binary file: '{0:s}'".format(filepath))
else:
for item in result:
memory_info = item.memory_info
memory_name = memory_info[DeviceMemoryInfoKeys.NAME]
memory_hexfile_size = memory_info[DeviceMemoryInfoKeys.HEXFILE_SIZE]
memory_size = memory_info[DeviceMemoryInfoKeys.SIZE]
print("Memory type: {}".format(memory_name))
showdata(item.data,
args.offset + memory_info[DeviceMemoryInfoKeys.ADDRESS],
memory_info[DeviceMemoryInfoKeys.PAGE_SIZE],
# PIC16 is word (16-bit) addressed, but each word address only contains one byte of actual data
# for EEPROM, the other byte is a phantom byte
phantom_bytes= 1 if memory_hexfile_size == 2*memory_size else 0)
print("\n")
return STATUS_SUCCESS
def _action_verify(backend, args):
hexfile = False
binary = False
literal = False
filepath = None
if args.filename is not None:
filepath = os.path.normpath(args.filename)
_, postfix = _get_file_prefix_and_postfix(filepath)
# If it ends in hex, use intel hex format, else binary
if postfix == 'hex':
hexfile = True
else:
binary = True
if args.literal is not None:
literal = True
if args.filename is not None:
print("Both file and literal value was specified. Literal verify will be ignored in favor of file verify")
literal = False
if hexfile:
print("Verifying...")
verify_status = backend.verify_hex(args.filename)
if verify_status is True:
print("Verify successful. Data in device matches data in specified hex-file")
elif binary:
print("Verifying...")
verify_status = verify_from_bin(args.filename, backend, args.offset, args.memory)
if verify_status is True:
print("Verify successful. Data in {} matches data in specified bin-file".format(args.memory))
elif literal:
print("Verifying...")
data_read = backend.read_memory(args.memory, args.offset, len(args.literal))[0].data
compare(data_read, args.literal, args.offset)
print("Verify successful. Data in {} matches literal data specified".format(args.memory))
else:
raise Exception('No file or literal specified for verify')
return STATUS_SUCCESS
def _get_file_prefix_and_postfix(filepath):
"""
Get file prefix and postfix from the filepath
If the file name in the filepath has not file extension the file is supposed to be a binary file
:param filepath: File name and full path
:return: prefix, postfix
"""
prefix = filepath.split('.')[0]
postfix = filepath.split('.')[-1].lower()
# If no "." is found in the filepath
if postfix == prefix:
postfix = "bin"
return prefix, postfix
def _extract_writeable_memories(memory_segments):
"""
Take a list of memory segments and return the segments that can be written
:param memory_segments: List of namedtuples with two fields: data and memory_info. data contains a byte array of
raw data bytes and memory_info is a dictionary with memory information (as defined in
deviceinfo.deviceinfo.DeviceMemoryInfo).
:return: List of namedtuples (a subset of the memory_segments input parameter) only containing memory segments
that can be written
"""
writeable_segments = []
for segment in memory_segments:
if segment.memory_info[DeviceMemoryInfoKeys.NAME] in WRITE_TO_HEX_MEMORIES:
writeable_segments.append(segment)
return writeable_segments
def _action_write(backend, args):
# If a filename is specified, read from it
if args.filename is not None:
filepath = os.path.normpath(args.filename)
_, postfix = _get_file_prefix_and_postfix(filepath)
# If it ends in hex, use intel hex format, else binary
if postfix == 'hex':
# Hexfiles contain addressing information that cannot be remapped, so offset/memory are not allowed here
if args.offset:
print("Offset cannot be specified when writing hex file")
return STATUS_FAILURE
if args.memory != MemoryNameAliases.ALL:
print("Memory area cannot be specified when writing hex file")
return STATUS_FAILURE
result = read_memories_from_hex(args.filename, backend.device_memory_info)
print("Writing from hex file...")
_write_memory_segments(backend, result, args.verify)
else:
with open(filepath, "rb") as binfile:
data_from_file = bytearray(binfile.read())
# Prepare and write data
print("Writing from binary file...")
# When writing data to target the data might be pagealigned so we make a copy to avoid verifying
# more than needed (in case verify option is enabled)
data_to_write = copy(data_from_file)
backend.write_memory(data_to_write, args.memory, args.offset)
if args.verify:
print("Verifying from binary file...")
# Verify content, an exception is thrown on mismatch
backend.verify_memory(data_from_file, args.memory, args.offset)
elif args.literal:
# Prepare and write data
print("Writing literal values...")
backend.write_memory(bytearray(args.literal), args.memory, args.offset)
if args.verify:
print("Verifying literal values...")
# Verify content, an exception is thrown on mismatch
backend.verify_memory(bytearray(args.literal), args.memory, args.offset)
else:
print("Error: for writing use either -f <file> or -l <literal>")
return STATUS_SUCCESS
def _write_memory_segments(backend, memory_segments, verify):
"""
Write content of list of memory segments
:param backend: pymcuprog Backend instance
:param memory_segments: List of namedtuples with two fields: data and memory_info. data contains a byte array of
raw data bytes and memory_info is a dictionary with memory information (as defined in
deviceinfo.deviceinfo.DeviceMemoryInfo).
:param verify: If True verify the written data by reading it back and compare
"""
for segment in memory_segments:
memory_name = segment.memory_info[DeviceMemoryInfoKeys.NAME]
print("Writing {}...".format(memory_name))
backend.write_memory(segment.data, memory_name, segment.offset)
if verify:
print("Verifying {}...".format(memory_name))
verify_ok = backend.verify_memory(segment.data, memory_name, segment.offset)
if verify_ok:
print("OK")
else:
print("Verification failed!")
def _action_reset(backend):
backend.hold_in_reset()
# Wait a bit to make sure the device has entered reset
# If needed this sleep could be made configurable by a CLI parameter,
# but for now a hardcoded value is assumed to be sufficient
time.sleep(0.1)
backend.release_from_reset()
return STATUS_SUCCESS
def _debugger_actions(backend, args):
"""
Debugger related actions
Targetless actions only involving the debugger. Only available on HID tools
"""
status = None
logger = getLogger(__name__)
try:
if args.action == 'getvoltage':
status = _action_getvoltage(backend)
if args.action == 'getsupplyvoltage':
status = _action_getsupplyvoltage(backend)
if args.action == 'getusbvoltage':
status = _action_getusbvoltage(backend)
if args.action == 'setsupplyvoltage':
status = _action_setsupplyvoltage(backend, args.literal)
if args.action == 'reboot-debugger':
status = _action_reboot_debugger(backend)
except (PymcuprogNotSupportedError, ValueError) as error:
print("ERROR: {}".format(error))
return STATUS_FAILURE
except Exception as exc:
logger.error("Operation failed with %s: %s", type(exc).__name__, exc)
logger.debug(exc, exc_info=True) # get traceback if debug loglevel
status = STATUS_FAILURE
return status
def _programming_actions(backend, args):
status = None
# Ping: checks that the device is there by reading its ID, or equivalent
# Always ping the device first before continuing. This guarantees connectivity and
# that the device matches the one expected
if not args.user_row_locked_device:
status = _action_ping(backend)
if status != STATUS_SUCCESS:
return status
# Already pinged
if args.action == "ping":
return status
# Erase: perform a full chip erase, or memtype-only erase if specified
if args.action == "erase":
status = _action_erase(backend, args)
# Reading data:
elif args.action == "read":
status = _action_read(backend, args)
elif args.action == "write":
status = _action_write(backend, args)
elif args.action == "reset":
status = _action_reset(backend)
elif args.action == "verify":
status = _action_verify(backend, args)
else:
print("Unknown command '{0:s}'".format(args.action))
status = STATUS_FAILURE
return status
def _setup_tool_connection(args):
toolconnection = None
# Parse the requested tool from the CLI
if args.tool == "uart":
# Embedded GPIO/UART tool (eg: raspberry pi) => no USB connection
toolconnection = ToolSerialConnection(serialport=args.uart)
else:
usb_serial = args.serialnumber
product = args.tool
if usb_serial and product:
print("Connecting to {0:s} ({1:s})'".format(product, usb_serial))
else:
if usb_serial:
print("Connecting to any tool with USB serial number '{0:s}'".format(usb_serial))
elif product:
print("Connecting to any {0:s}".format(product))
else:
print("Connecting to anything possible")
toolconnection = ToolUsbHidConnection(serialnumber=usb_serial, tool_name=product)
return toolconnection
def _select_target_device(backend, args):
device_mounted = None
device_selected = None
if args.tool not in ['uart']:
# Find out from the board (kit) if a device is mounted
device_mounted = backend.read_kit_device()
if device_mounted is not None:
device_mounted = device_mounted.lower()
print("Device mounted: '{0:s}'".format(device_mounted))
# Parse device field. If unspecified, use the board's device
if args.device:
device_selected = args.device.lower()
else:
if device_mounted is None:
print("Unable to determine on-board target! Please specify device using -d <device>")
else:
print("No device specified. Using on-board target ({0:s})".format(device_mounted))
device_selected = device_mounted
# Mismatch. Allow user to proceed at own risk.
if device_mounted is not None and device_selected != device_mounted:
print("Warning: you are attempting to use a device which is not the one which was mounted on the kit!")
print("Cut all straps between the debugger and the on-board target when accessing an external device!")
return device_selected
def _start_session(backend, device, args):
"""
Setup the session and try to build the stack for this device
"""
sessionconfig = SessionConfig(device)
# -c clock argument
# allow Hz, or kHz ending in 'k' (eg: 100k) or MHz ending in 'M' eg (1M)
if args.clk:
if args.clk[-1] == 'k':
clk = int(args.clk.strip('k')) * 1000
elif args.clk[-1] == 'M':
clk = int(args.clk.strip('M')) * 1000000
else:
clk = int(args.clk)
sessionconfig.interface_speed = clk
# Translate args into "special_options" to pass down the stack
sessionconfig.special_options = {}
if args.high_voltage:
sessionconfig.special_options['high-voltage'] = args.high_voltage
if args.user_row_locked_device:
sessionconfig.special_options['user-row-locked-device'] = args.user_row_locked_device
if args.chip_erase_locked_device:
sessionconfig.special_options['chip-erase-locked-device'] = args.chip_erase_locked_device
# Programming user row on locked parts and erasing to unlock are mutually exclusive
if args.chip_erase_locked_device and args.user_row_locked_device:
print("User row cannot be written on a locked device while erasing and unlocking.")
return STATUS_FAILURE
if args.interface:
sessionconfig.interface = args.interface
if args.packpath:
sessionconfig.packpath = args.packpath
status = STATUS_SUCCESS
try:
backend.start_session(sessionconfig)
except PymcuprogDeviceLockedError:
print("The device is in a locked state and is not accessible; a chip erase is required.")
print("Locked AVR UPDI devices can:")
print(" - be unlocked using command: erase --chip-erase-locked-device")
print(" - write user row values using command: write -m user_row --user-row-locked-device")
status = STATUS_FAILURE
except PymcuprogNotSupportedError:
print("Unable to setup stack for device {0:s}".format(sessionconfig.device))
print("Currently supported devices (in 'devices' folder):")
device_list = get_supported_devices()
print(', '.join(map(str, device_list)))
status = STATUS_FAILURE
except PymcuprogSessionConfigError as error:
print("Unable to start session: {}".format(error))
status = STATUS_FAILURE
return status