Skip to content

Commit 63fe3bd

Browse files
committed
Initial commit.
0 parents  commit 63fe3bd

File tree

10 files changed

+514
-0
lines changed

10 files changed

+514
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
dist/
2+
*egg-info/
3+
__pycache__

LICENSE

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Copyright © 2022 Alexander Bessman
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4+
5+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# mcbootflash
2+
3+
## Overview
4+
5+
mcbootflash is a tool for flashing firmware to devices running Microchip's
6+
[MCC 16-bit bootloader](https://www.microchip.com/en-us/software-library/16-bit-bootloader).
7+
Microchip provides an official GUI tool for this purpose, called the
8+
Unified Bootloader Host Application (UBHA). Compared to UBHA, mcbootflash:
9+
10+
- Provides no GUI.
11+
- Can be automated.
12+
- Can be used as a library.
13+
- Is free and open source.
14+
- Is written in Python instead of Java.
15+
16+
mcbootflash is affiliated with neither Microchip nor McDonald's.
17+
18+
MIT License, (C) 2022 Alexander Bessman <alexander.bessman@gmail.com>
19+
20+
## Installation
21+
22+
`pip install mcbootflash`
23+
24+
Once installed, mcbootflash can be run from the command line:
25+
26+
```bash
27+
$ mcbootflash --port=/dev/ttyUSB0 --baudrate=460800 firmware.hex
28+
```

mcbootflash/__init__.py

Whitespace-only changes.

mcbootflash/connection.py

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
import logging
2+
3+
import intelhex
4+
from serial import Serial
5+
6+
from mcbootflash.error import (
7+
BootloaderError,
8+
FlashEraseError,
9+
FlashWriteError,
10+
ChecksumError,
11+
)
12+
from mcbootflash.protocol import (
13+
FLASH_UNLOCK_KEY,
14+
BootCommand,
15+
BootResponseCode,
16+
CommandPacket,
17+
ResponsePacket,
18+
VersionResponsePacket,
19+
MemoryRangePacket,
20+
)
21+
22+
logger = logging.getLogger(__name__)
23+
24+
25+
class BootloaderConnection(Serial):
26+
"""Communication interface to device running MCC 16-bit bootloader."""
27+
28+
def flash(self, hexfile: str):
29+
"""Flash application firmware.
30+
31+
Parameters
32+
----------
33+
hexfile : str
34+
An Intel HEX-file containing application firmware.
35+
36+
Raises
37+
------
38+
FlashEraseError
39+
FlashWriteError
40+
ChecksumError
41+
BootloaderError
42+
"""
43+
self.hexfile = intelhex.IntelHex(hexfile)
44+
(
45+
self.version,
46+
self.max_packet_length,
47+
self.device_id,
48+
self.erase_size,
49+
self.write_size,
50+
) = self.read_version()
51+
legal_range = self._get_memory_address_range()
52+
53+
self._erase_flash(*legal_range, self.erase_size)
54+
55+
for segment in self.hexfile.segments():
56+
# Since the MCU uses 16-bit instructions, each "address" in the
57+
# (8-bit) hex file is actually only half an address. Therefore, we
58+
# need to divide by two to get the actual address.
59+
if (segment[0] // 2 in range(*legal_range)) and (
60+
segment[1] // 2 in range(*legal_range)
61+
):
62+
logger.info(
63+
"Flashing segment "
64+
f"{self.hexfile.segments().index(segment)}, "
65+
f"[{segment[0]:#08x}:{segment[1]:#08x}]."
66+
)
67+
self._flash_segment(segment)
68+
else:
69+
logger.info(
70+
f"Segment {self.hexfile.segments().index(segment)} "
71+
"ignored; not in legal range "
72+
f"([{segment[0]:#08x}:{segment[1]:#08x}] vs. "
73+
f"[{legal_range[0]:#08x}:{legal_range[1]:#08x}])."
74+
)
75+
76+
self._self_verify()
77+
78+
def _flash_segment(self, segment):
79+
chunk_size = self.max_packet_length - CommandPacket.size
80+
chunk_size -= chunk_size % self.write_size
81+
chunk_size //= 2
82+
total_bytes = segment[1] - segment[0]
83+
written_bytes = 0
84+
# If (segment[1] - segment[0]) % write_size != 0, writing the final
85+
# chunk will fail. However, I have seen no example where it's not,
86+
# so not adding code to check for now (YAGNI).
87+
for addr in range(segment[0] // 2, segment[1] // 2, chunk_size):
88+
chunk = self.hexfile[addr * 2 : (addr + chunk_size) * 2]
89+
self._write_flash(addr, chunk.tobinstr())
90+
self._checksum(addr, len(chunk))
91+
written_bytes += len(chunk)
92+
logger.info(
93+
f"{written_bytes} bytes written of {total_bytes} "
94+
f"({written_bytes / total_bytes * 100:.2f}%)."
95+
)
96+
97+
def read_version(self) -> tuple:
98+
"""Read bootloader version and some other useful information.
99+
100+
Returns
101+
-------
102+
version : int
103+
max_packet_length : int
104+
The maximum size of a single packet sent to the bootloader,
105+
including both the command and associated data.
106+
device_id : int
107+
erase_size : int
108+
Flash page size. When erasing flash memory, the number of bytes to
109+
be erased must align with a flash page.
110+
write_size : int
111+
Write block size. When writing to flash, the number of bytes to be
112+
written must align with a write block.
113+
"""
114+
read_version_command = CommandPacket(
115+
command=BootCommand.READ_VERSION.value
116+
)
117+
self.write(bytes(read_version_command))
118+
read_version_response = VersionResponsePacket.from_serial(self)
119+
return (
120+
read_version_response.version,
121+
read_version_response.max_packet_pength,
122+
read_version_response.device_id,
123+
read_version_response.erase_size,
124+
read_version_response.write_size,
125+
)
126+
127+
def _get_memory_address_range(self) -> tuple:
128+
mem_range_command = CommandPacket(
129+
command=BootCommand.GET_MEMORY_ADDRESS_RANGE.value
130+
)
131+
self.write(bytes(mem_range_command))
132+
mem_range_response = MemoryRangePacket.from_serial(self)
133+
134+
if mem_range_response.success != BootResponseCode.SUCCESS.value:
135+
logger.error(
136+
"Failed to get program memory range: "
137+
f"{BootResponseCode(mem_range_response.success).name}"
138+
)
139+
raise BootloaderError(
140+
BootResponseCode(mem_range_response.success).name
141+
)
142+
else:
143+
logger.info(
144+
"Got program memory range: "
145+
f"{mem_range_response.program_start:#08x}:"
146+
f"{mem_range_response.program_end:#08x}."
147+
)
148+
149+
return mem_range_response.program_start, mem_range_response.program_end
150+
151+
def _erase_flash(
152+
self, start_address: int, end_address: int, erase_size: int
153+
):
154+
erase_flash_command = CommandPacket(
155+
command=BootCommand.ERASE_FLASH.value,
156+
data_length=int((end_address - start_address) / erase_size),
157+
unlock_sequence=FLASH_UNLOCK_KEY,
158+
address=start_address,
159+
)
160+
self.write(bytes(erase_flash_command))
161+
erase_flash_response = ResponsePacket.from_serial(self)
162+
163+
if erase_flash_response.success != BootResponseCode.SUCCESS.value:
164+
logger.error(
165+
"Flash erase failed: "
166+
f"{BootResponseCode(erase_flash_response.success).name}."
167+
)
168+
raise FlashEraseError(
169+
BootResponseCode(erase_flash_response.success).name
170+
)
171+
else:
172+
logger.info(
173+
f"Erased flash area {start_address:#08x}:{end_address:#08x}."
174+
)
175+
176+
def _write_flash(self, address: int, data: bytes):
177+
write_flash_command = CommandPacket(
178+
command=BootCommand.WRITE_FLASH.value,
179+
data_length=len(data),
180+
unlock_sequence=FLASH_UNLOCK_KEY,
181+
address=address,
182+
)
183+
self.write(bytes(write_flash_command) + data)
184+
write_flash_response = ResponsePacket.from_serial(self)
185+
186+
if write_flash_response.success != BootResponseCode.SUCCESS.value:
187+
logger.error(
188+
f"Failed to write {len(bytes)} bytes to {address:#08x}: "
189+
f"{BootResponseCode(write_flash_response.success).name}."
190+
)
191+
raise FlashWriteError(
192+
BootResponseCode(write_flash_response.success).name
193+
)
194+
else:
195+
logger.debug(f"Wrote {len(data)} bytes to {address:#08x}.")
196+
197+
def _self_verify(self):
198+
self_verify_command = CommandPacket(
199+
command=BootCommand.SELF_VERIFY.value
200+
)
201+
self.write(bytes(self_verify_command))
202+
self_verify_response = ResponsePacket.from_serial(self)
203+
204+
if self_verify_response.success != BootResponseCode.SUCCESS.value:
205+
logger.error(
206+
"Self verify failed: "
207+
f"{BootResponseCode(self_verify_response.success).name}."
208+
)
209+
raise BootloaderError(
210+
BootResponseCode(self_verify_response.success).name
211+
)
212+
else:
213+
logger.info("Self verify OK.")
214+
215+
def _get_checksum(self, address: int, length: int):
216+
calculcate_checksum_command = CommandPacket(
217+
command=BootCommand.CALC_CHECKSUM.value,
218+
data_length=length,
219+
address=address,
220+
)
221+
self.write(bytes(calculcate_checksum_command))
222+
calculate_checksum_response = ResponsePacket.from_serial(self)
223+
224+
if (
225+
calculate_checksum_response.success
226+
!= BootResponseCode.SUCCESS.value
227+
):
228+
logger.error(
229+
"Failed to get checksum: "
230+
f"{BootResponseCode(calculate_checksum_response.success).name}"
231+
)
232+
raise BootloaderError(
233+
BootResponseCode(calculate_checksum_response.success).name
234+
)
235+
236+
checksum = int.from_bytes(self.read(2), byteorder="little")
237+
238+
return checksum
239+
240+
def _calculate_checksum(self, address: int, length: int):
241+
checksum = 0
242+
for i in range(address, address + length, 4):
243+
data = self.hexfile[i : i + 4].tobinstr()
244+
checksum += int.from_bytes(data, byteorder="little") & 0xFFFF
245+
checksum += (int.from_bytes(data, byteorder="little") >> 16) & 0xFF
246+
return checksum & 0xFFFF
247+
248+
def _checksum(self, address: int, length: int):
249+
"""Compare checksums calculated locally and onboard device.
250+
251+
Parameters
252+
----------
253+
address : int
254+
Address from which to start checksum.
255+
length : int
256+
Number of bytes to checksum.
257+
"""
258+
checksum1 = self._calculate_checksum(address * 2, length)
259+
checksum2 = self._get_checksum(address, length)
260+
if checksum1 != checksum2:
261+
logger.error(f"Checksum mismatch: {checksum1} != {checksum2}.")
262+
raise ChecksumError
263+
else:
264+
logger.debug(f"Checksum OK: {checksum1}.")

mcbootflash/error.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
class BootloaderError(Exception):
2+
"""Base class for mccflash exceptions.
3+
4+
Raised when a negative response code is received and no derived exception
5+
applies.
6+
"""
7+
8+
pass
9+
10+
11+
class FlashEraseError(BootloaderError):
12+
"""Raised if an attempt to erase flash memory failed."""
13+
14+
pass
15+
16+
17+
class FlashWriteError(BootloaderError):
18+
"""Raised if an attempt to write to flash failed."""
19+
20+
pass
21+
22+
23+
class ChecksumError(FlashWriteError):
24+
"""Raised if the device checksum does not match the written hex file."""
25+
26+
pass

mcbootflash/flash.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Command line tool for flashing firmware."""
2+
3+
import argparse
4+
import logging
5+
6+
from mcbootflash.connection import BootloaderConnection
7+
8+
9+
def flash():
10+
"""Entry point for console_script."""
11+
parser = argparse.ArgumentParser(prog="mccbootflash")
12+
parser.add_argument(
13+
"file",
14+
type=str,
15+
help="An Intel HEX file containing application firmware.",
16+
)
17+
parser.add_argument(
18+
"-p",
19+
"--port",
20+
type=str,
21+
required=True,
22+
help="Serial port connected to device to flash.",
23+
)
24+
parser.add_argument(
25+
"-b",
26+
"--baudrate",
27+
type=int,
28+
required=True,
29+
help="Symbol rate of device's serial bus.",
30+
)
31+
parser.add_argument(
32+
"-t",
33+
"--timeout",
34+
type=float,
35+
default=5,
36+
help=(
37+
"Try to read data from the serial port for this many seconds "
38+
"before giving up."
39+
),
40+
)
41+
parser.add_argument("-v", "--verbose", action="count", default=0)
42+
args = parser.parse_args()
43+
logging.basicConfig()
44+
45+
if not args.verbose:
46+
logging.getLogger().setLevel(logging.CRITICAL)
47+
elif args.verbose == 1:
48+
logging.getLogger().setLevel(logging.ERROR)
49+
elif args.verbose == 2:
50+
logging.getLogger().setLevel(logging.WARNING)
51+
elif args.verbose == 3:
52+
logging.getLogger().setLevel(logging.INFO)
53+
else:
54+
logging.getLogger().setLevel(logging.DEBUG)
55+
56+
boot = BootloaderConnection(
57+
port=args.port, baudrate=args.baudrate, timeout=args.timeout
58+
)
59+
boot.flash(hexfile=args.file)
60+
61+
62+
if __name__ == "__main__":
63+
flash()

0 commit comments

Comments
 (0)