Skip to content

Commit

Permalink
Switch HEX-file parser to bincopy
Browse files Browse the repository at this point in the history
  • Loading branch information
bessman committed Sep 19, 2023
1 parent 5848b67 commit 5a3085c
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 64 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ requires-python = ">=3.7"
license = {file = "LICENSE"}
dependencies = [
"pyserial",
"intelhex",
"bincopy",
]
classifiers = [
"Programming Language :: Python :: 3",
Expand Down
121 changes: 58 additions & 63 deletions src/mcbootflash/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
import logging
import sys
from struct import error as structerror
from typing import Any, Dict, Generator, Tuple, Type, Union
from typing import Any, Dict, Tuple, Type, Union

from intelhex import IntelHex # type: ignore[import]
import bincopy # type: ignore[import]
from serial import Serial # type: ignore[import]

from mcbootflash.error import (
Expand Down Expand Up @@ -63,7 +63,7 @@ def __init__(self, port: str, **kwargs: Any):
self._erase_size,
self._write_size,
) = self._read_version()
self._memory_range = range(*self._get_memory_address_range())
self._memory_range = self._get_memory_address_range()
except structerror as exc:
raise BootloaderError("No response from bootloader") from exc
_logger.info("Connected")
Expand Down Expand Up @@ -91,50 +91,54 @@ def flash(
If HEX-file cannot be flashed.
"""
path = hexfile
# Since the MCU uses 16-bit instructions, each "address" in the (8-bit) hex file
# is actually only half an address. Therefore, we need to multiply addresses
# received from MCC by two to get the corresponding address in the hex file.
hexdata = IntelHex(path)[
2 * self._memory_range[0] : 2 * (self._memory_range[-1] + 1)
]
segments = [hexdata[u:l] for u, l in hexdata.segments()]

if not segments:
hexdata = bincopy.BinFile(path)

# Each address in a Microchip HEX-file is twice the machine address.
hexdata.word_size_bits = 16
hexdata.word_size_bytes = 2
hexdata._segments.word_size_bits = 16 # pylint: disable=protected-access
hexdata._segments.word_size_bytes = 2 # pylint: disable=protected-access

for segment in hexdata.segments:
segment._word_size_bits = 16 # pylint: disable=protected-access
segment._word_size_bytes = 2 # pylint: disable=protected-access

hexdata.crop(*self._memory_range)

if not hexdata.segments:
raise BootloaderError(
"HEX file contains no data that fits entirely within program memory"
)

self.erase_flash(self._memory_range)
self.erase_flash(range(*self._memory_range))
_logger.info(f"Flashing {path}")
chunk_size = self._max_packet_length - Command.get_size()
chunk_size -= chunk_size % self._write_size
total_bytes = sum(len(segment) for segment in segments)
chunk_size //= hexdata.word_size_bytes
total_bytes = len(hexdata) * hexdata.word_size_bytes
written_bytes = 0
alignment = self._write_size // hexdata.word_size_bytes

for segment in segments:
chunks = self._chunk(segment, chunk_size)
_logger.debug(f"Flashing segment {segments.index(segment)}")

for chunk in chunks:
self._write_flash(chunk, self._write_size)
written_bytes += len(chunk)
_logger.debug(
f"{written_bytes} bytes written of {total_bytes} "
f"({written_bytes / total_bytes * 100:.2f}%)"
)
self._checksum(chunk)
for chunk in hexdata.segments.chunks(chunk_size, alignment):
self._write_flash(chunk)
written_bytes += len(chunk.data)
_logger.debug(
f"{written_bytes} bytes written of {total_bytes} "
f"({written_bytes / total_bytes * 100:.2f}%)"
)
segment = bincopy.Segment(
chunk.address * hexdata.word_size_bytes,
(chunk.address + len(chunk.data)) * hexdata.word_size_bytes,
data=chunk.data,
word_size_bytes=hexdata.word_size_bytes,
)
self._checksum(segment)

if progress_callback:
progress_callback(written_bytes, total_bytes)
if progress_callback:
progress_callback(written_bytes, total_bytes)

self._self_verify()

@staticmethod
def _chunk(hexdata: IntelHex, size: int) -> Generator[IntelHex, None, None]:
start = hexdata.minaddr()
stop = hexdata.maxaddr()
return (hexdata[i : i + size] for i in range(start, stop, size))

def _send_and_receive(self, command: Command, data: bytes = b"") -> ResponseBase:
self.interface.write(bytes(command) + data)
response = get_response(self.interface)
Expand Down Expand Up @@ -282,25 +286,24 @@ def _detect_program(self) -> bool:
_logger.disabled = False
return True

def _write_flash(self, data: IntelHex, align: int) -> None:
def _write_flash(self, chunk: bincopy.Segment) -> None:
"""Write data to bootloader.
Parameters
----------
data : intelhex.IntelHex
An IntelHex instance of length no greater than the bootloader's
chunk : bincopy.Segment
A bincopy.Segment instance of length no greater than the bootloader's
max_packet_length attribute.
"""
padding = bytes([data.padding] * ((align - (len(data) % align)) % align))
_logger.debug(f"Writing {len(data)} bytes to {data.minaddr():#08x}")
_logger.debug(f"Writing {len(chunk.data)} bytes to {chunk.address:#08x}")
self._send_and_receive(
Command(
command=CommandCode.WRITE_FLASH,
data_length=len(data) + len(padding),
data_length=len(chunk.data),
unlock_sequence=self._FLASH_UNLOCK_KEY,
address=data.minaddr() >> 1,
address=chunk.address,
),
data.tobinstr() + padding,
chunk.data,
)

def _self_verify(self) -> None:
Expand All @@ -319,43 +322,35 @@ def _get_remote_checksum(self, address: int, length: int) -> int:
return checksum_response.checksum

@staticmethod
def _get_local_checksum(data: IntelHex) -> int:
checksum = 0
start = data.minaddr()
stop = start + len(data)
step = 4
def _get_local_checksum(chunk: bincopy.Segment) -> int:
chksum = 0

for i in range(start, stop, step):
databytes = data[i : i + step].tobinstr()
checksum += int.from_bytes(databytes, byteorder="little") & 0xFFFF
checksum += (int.from_bytes(databytes, byteorder="little") >> 16) & 0xFF
for piece in chunk.chunks(
size=4 // chunk._word_size_bytes, # pylint: disable=protected-access
):
chksum += piece.data[0] + (piece.data[1] << 8) + piece.data[2]

return checksum & 0xFFFF
return chksum & 0xFFFF

def _checksum(self, hexdata: IntelHex) -> None:
def _checksum(self, chunk: bincopy.Segment) -> None:
"""Compare checksums calculated locally and onboard device.
Parameters
----------
address : int
Address from which to start checksum.
length : int
Number of bytes to checksum.
segment : bincopy.Segment
HEX segment to checksum.
"""
upper_legal_address = self._memory_range[-1] + 1
lower_data_address = hexdata.minaddr() >> 1

# Workaround for bug in bootloader. The bootloader incorrectly raises
# BAD_ADDRESS when trying to calculate checksums close to the upper bound of the
# program memory range.
if (upper_legal_address - lower_data_address) < len(hexdata):
if (self._memory_range[1] - chunk.address) < len(chunk.data):
_logger.debug(
"Too close to upper memory bound, skipping checksum calculation"
)
return

checksum1 = self._get_local_checksum(hexdata)
checksum2 = self._get_remote_checksum(hexdata.minaddr() >> 1, len(hexdata))
checksum1 = self._get_local_checksum(chunk)
checksum2 = self._get_remote_checksum(chunk.address, len(chunk.data))

if checksum1 != checksum2:
_logger.debug(f"Checksum mismatch: {checksum1} != {checksum2}")
Expand Down

0 comments on commit 5a3085c

Please sign in to comment.