# Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Host side programmer for the ZX0 bootloader. The bootloader implements the HF2 protocol. See https://github.com/microsoft/uf2/blob/master/hf2.md for the definition including the protocol level values. """ import enum from dataclasses import dataclass import random import struct from typing import List, Optional, Sequence import click import smbus import intelhex from crccheck.crc import Crc16Xmodem FILL = 0xFF class Command(enum.IntEnum): BININFO = 0x0001 INFO = 0x0002 RESET_INTO_APP = 0x0003 RESET_INTO_BOOTLOADER = 0x0004 START_FLASH = 0x0005 WRITE_FLASH_PAGE = 0x0006 CHKSUM_PAGES = 0x0007 READ_WORDS = 0x0008 WRITE_WORDS = 0x0009 DMESG = 0x0010 class Flag(enum.IntFlag): SERIAL_OUT = 0x80 SERIAL_ERR = 0xC0 CMDPKT_LAST = 0x40 CMDPKT_BODY = 0x00 FLAG_MASK = 0xC0 SIZE_MASK = 63 class Status(enum.IntEnum): OK = 0 INVALID_CMD = 1 ERROR = 2 @dataclass class BinInfo: mode: int flash_page_size: int flash_num_pages: int max_message_size: int family_id: int class Error(Exception): pass class HF2: """Implements the HF2 flashing protocol over I2C.""" # Maximum number of bytes per packet. Must fit within the 32 bytes of a Linux write. MAX_PACKET = 31 def __init__(self, bus: smbus.SMBus, address: int, command: int): self._bus = bus self._address = address self._command = command self._tag = random.randint(0, 30000) def send(self, command: Command, args: Optional[bytes] = None) -> None: self._tag = (self._tag + 1) & 0xFFFF msg = struct.pack('= len(msg)) if final: header |= Flag.CMDPKT_LAST packet = [header] + list(msg[i:i + take]) self._bus.write_i2c_block_data(self._address, self._command, packet) def exec(self, command: Command, args: Optional[bytes] = None) -> bytes: self.send(command, args) got = bytes(self._bus.read_block_data(self._address, self._command)) header, tag, status, status_info = struct.unpack_from(' BinInfo: data = self.exec(Command.BININFO) return BinInfo(*struct.unpack_from(' None: args = struct.pack(' Sequence[int]: args = struct.pack(' Sequence[int]: args = struct.pack(' None: self.send(Command.RESET_INTO_APP) def _show_address(v) -> str: if v is None: return '' return '%x' % v @click.command() @click.option('--bus', default=3, help='I2C bus') @click.option('--address', default=0x22, help='Device address') @click.option('--command', default=0xFE, help='HF2 command') @click.option('--program', type=click.File(), help='Hex file to program to the device') @click.option('--start', is_flag=True, help='Start the application running after any other operations') def main(bus: int, address: int, command: int, program: str, start: bool): b = smbus.SMBus(bus) h = HF2(b, address, command) info = h.get_bininfo() click.echo(f'Found a device with {info.flash_num_pages} pages of ' f'{info.flash_page_size} bytes') if program: hex = intelhex.IntelHex() hex.fromfile(program, format='hex') page = info.flash_page_size addresses: List[int] = [] for start, end in hex.segments(): addresses.extend(range(start, end, page)) with click.progressbar(addresses, label='Program', item_show_func=_show_address) as bar: for address in bar: data = [ hex[x] for x in range(address, min(address + page, end)) ] data += [FILL] * (page - len(data)) assert len(data) == page h.write_flash_page(address, bytes(data)) got, want = h.chksum_pages(address)[0], Crc16Xmodem.calc(data) if got != want and address >= 16384: raise Error(f'Program error at address {address:x}. ' f'Want CRC {want:x}, got {got:x}') if start: click.echo('Starting app') h.reset_into_app() if __name__ == '__main__': main()