|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +""" |
| 4 | +Add a flash layout table to a hex firmware for MicroPython on the micro:bit. |
| 5 | +
|
| 6 | +Usage: ./addlayouttable.py <firmware.hex> <firmware.map> [-o <combined.hex>] |
| 7 | +
|
| 8 | +Output goes to stdout if no filename is given. |
| 9 | +
|
| 10 | +The layout table is a sequence of 16-byte entries. The last entry contains the |
| 11 | +header (including magic numbers) and is aligned to the end of a page such that |
| 12 | +the final byte of the layout table is the final byte of the page it resides in. |
| 13 | +This is so it can be quickly and easily searched for. |
| 14 | +
|
| 15 | +The layout table has the following format. All integer values are unsigned and |
| 16 | +store little endian. |
| 17 | +
|
| 18 | +0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f |
| 19 | +
|
| 20 | +ID HT REG_PAGE REG_LEN HASH_DATA |
| 21 | +(additional regions) |
| 22 | +... |
| 23 | +MAGIC1 VERSION TABLE_LEN NUM_REG PSIZE_LOG2 MAGIC2 |
| 24 | +
|
| 25 | +The values are: |
| 26 | +
|
| 27 | +ID - 1 byte - region id for this entry, defined by the region |
| 28 | +HT - 1 byte - hash type of the region hash data |
| 29 | +REG_PAGE - 2 bytes - starting page number of the region |
| 30 | +REG_LEN - 4 bytes - length in bytes of the region |
| 31 | +HASH_DATA - 8 bytes - data for the hash of this region |
| 32 | + HT=0: hash data is empty |
| 33 | + HT=1: hash data contains 8 bytes of verbatim data |
| 34 | + HT=2: hash data contains a 4-byte pointer to a string |
| 35 | +
|
| 36 | +MAGIC1 - 4 bytes - 0x597F30FE |
| 37 | +VERSION - 2 bytes - table version (currently 1) |
| 38 | +TABLE_LEN - 2 bytes - length in bytes of the table excluding this header row |
| 39 | +NUM_REG - 2 bytes - number of regions |
| 40 | +PSIZE_LOG2 - 2 bytes - native page size of the flash, log-2 |
| 41 | +MAGIC2 - 4 bytes - 0xC1B1D79D |
| 42 | +
|
| 43 | +""" |
| 44 | + |
| 45 | +import argparse |
| 46 | +import binascii |
| 47 | +import struct |
| 48 | +import sys |
| 49 | + |
| 50 | +IHEX_TYPE_DATA = 0 |
| 51 | +IHEX_TYPE_EXT_LIN_ADDR = 4 |
| 52 | + |
| 53 | +NRF_PAGE_SIZE_LOG2 = 12 |
| 54 | +NRF_PAGE_SIZE = 1 << NRF_PAGE_SIZE_LOG2 |
| 55 | + |
| 56 | + |
| 57 | +class FlashLayout: |
| 58 | + MAGIC1 = 0x597F30FE |
| 59 | + MAGIC2 = 0xC1B1D79D |
| 60 | + VERSION = 1 |
| 61 | + |
| 62 | + REGION_HASH_NONE = 0 |
| 63 | + REGION_HASH_DATA = 1 |
| 64 | + REGION_HASH_PTR = 2 |
| 65 | + |
| 66 | + def __init__(self): |
| 67 | + self.data = b"" |
| 68 | + self.num_regions = 0 |
| 69 | + |
| 70 | + def add_region( |
| 71 | + self, region_id, region_addr, region_len, region_hash_type, region_hash=None |
| 72 | + ): |
| 73 | + # Compute/validate the hash data. |
| 74 | + if region_addr % NRF_PAGE_SIZE != 0: |
| 75 | + assert 0, region_addr |
| 76 | + if region_hash_type == FlashLayout.REGION_HASH_NONE: |
| 77 | + assert region_hash is None |
| 78 | + region_hash = b"\x00" * 8 |
| 79 | + elif region_hash_type == FlashLayout.REGION_HASH_DATA: |
| 80 | + assert len(region_hash) == 8 |
| 81 | + elif region_hash_type == FlashLayout.REGION_HASH_PTR: |
| 82 | + region_hash = struct.pack("<II", region_hash, 0) |
| 83 | + |
| 84 | + # Increase number of regions. |
| 85 | + self.num_regions += 1 |
| 86 | + |
| 87 | + # Add the region data. |
| 88 | + self.data += struct.pack( |
| 89 | + "<BBHI8s", |
| 90 | + region_id, |
| 91 | + region_hash_type, |
| 92 | + region_addr // NRF_PAGE_SIZE, |
| 93 | + region_len, |
| 94 | + region_hash, |
| 95 | + ) |
| 96 | + |
| 97 | + def finalise(self): |
| 98 | + # Add padding to data to align it to 16 bytes. |
| 99 | + if len(self.data) % 16 != 0: |
| 100 | + self.data += b"\xff" * 16 - len(self.data) % 16 |
| 101 | + |
| 102 | + # Add 16-byte "header" at the end with magic numbers and meta data. |
| 103 | + self.data += struct.pack( |
| 104 | + "<IHHHHI", |
| 105 | + FlashLayout.MAGIC1, |
| 106 | + FlashLayout.VERSION, |
| 107 | + len(self.data), |
| 108 | + self.num_regions, |
| 109 | + NRF_PAGE_SIZE_LOG2, |
| 110 | + FlashLayout.MAGIC2, |
| 111 | + ) |
| 112 | + |
| 113 | + |
| 114 | +def make_ihex_record(addr, type, data): |
| 115 | + record = struct.pack(">BHB", len(data), addr & 0xFFFF, type) + data |
| 116 | + checksum = (-(sum(record))) & 0xFF |
| 117 | + return ":%s%02X" % (str(binascii.hexlify(record), "utf8").upper(), checksum) |
| 118 | + |
| 119 | + |
| 120 | +def parse_map_file(filename, symbols): |
| 121 | + parse_symbols = False |
| 122 | + with open(filename) as f: |
| 123 | + for line in f: |
| 124 | + line = line.strip() |
| 125 | + if line == "Linker script and memory map": |
| 126 | + parse_symbols = True |
| 127 | + elif parse_symbols and line.startswith("0x00"): |
| 128 | + line = line.split() |
| 129 | + if len(line) >= 2 and line[1] in symbols: |
| 130 | + symbols[line[1]] = int(line[0], 16) |
| 131 | + |
| 132 | + |
| 133 | +def output_firmware(dest, firmware, layout_addr, layout_data): |
| 134 | + # Output head of firmware. |
| 135 | + for line in firmware[:-2]: |
| 136 | + print(line, end="", file=dest) |
| 137 | + |
| 138 | + # Output layout data. |
| 139 | + print( |
| 140 | + make_ihex_record( |
| 141 | + 0, |
| 142 | + IHEX_TYPE_EXT_LIN_ADDR, |
| 143 | + struct.pack(">H", layout_addr >> 16), |
| 144 | + ), |
| 145 | + file=dest, |
| 146 | + ) |
| 147 | + for i in range(0, len(layout_data), 16): |
| 148 | + chunk = layout_data[i : min(i + 16, len(layout_data))] |
| 149 | + print( |
| 150 | + make_ihex_record(layout_addr + i, IHEX_TYPE_DATA, chunk), |
| 151 | + file=dest, |
| 152 | + ) |
| 153 | + |
| 154 | + # Output tail of firmware. |
| 155 | + print(firmware[-2], end="", file=dest) |
| 156 | + print(firmware[-1], end="", file=dest) |
| 157 | + |
| 158 | + |
| 159 | +def main(): |
| 160 | + arg_parser = argparse.ArgumentParser( |
| 161 | + description="Add UICR region to hex firmware for the micro:bit." |
| 162 | + ) |
| 163 | + arg_parser.add_argument( |
| 164 | + "-o", |
| 165 | + "--output", |
| 166 | + default=sys.stdout, |
| 167 | + type=argparse.FileType("wt"), |
| 168 | + help="output file (default is stdout)", |
| 169 | + ) |
| 170 | + arg_parser.add_argument("firmware", nargs=1, help="input MicroPython firmware") |
| 171 | + arg_parser.add_argument( |
| 172 | + "mapfile", |
| 173 | + nargs=1, |
| 174 | + help="input map file", |
| 175 | + ) |
| 176 | + args = arg_parser.parse_args() |
| 177 | + |
| 178 | + # Read in the firmware from the given hex file. |
| 179 | + with open(args.firmware[0], "rt") as f: |
| 180 | + firmware = f.readlines() |
| 181 | + |
| 182 | + # Parse the linker map file, looking for the following symbols. |
| 183 | + symbols = { |
| 184 | + key: None |
| 185 | + for key in [ |
| 186 | + "_binary_softdevice_bin_start", |
| 187 | + "__isr_vector", |
| 188 | + "__etext", |
| 189 | + "__data_start__", |
| 190 | + "__data_end__", |
| 191 | + "_fs_start", |
| 192 | + "_fs_end", |
| 193 | + "microbit_version_string", |
| 194 | + ] |
| 195 | + } |
| 196 | + parse_map_file(args.mapfile[0], symbols) |
| 197 | + |
| 198 | + # Get the required symbol addresses. |
| 199 | + sd_start = symbols["_binary_softdevice_bin_start"] |
| 200 | + sd_end = symbols["__isr_vector"] |
| 201 | + mp_start = symbols["__isr_vector"] |
| 202 | + data_len = symbols["__data_end__"] - symbols["__data_start__"] |
| 203 | + mp_end = symbols["__etext"] + data_len |
| 204 | + mp_version = symbols["microbit_version_string"] |
| 205 | + fs_start = symbols["_fs_start"] |
| 206 | + fs_end = symbols["_fs_end"] |
| 207 | + |
| 208 | + # Make the flash layout information table. |
| 209 | + layout = FlashLayout() |
| 210 | + layout.add_region(1, sd_start, sd_end - sd_start, FlashLayout.REGION_HASH_NONE) |
| 211 | + layout.add_region( |
| 212 | + 2, mp_start, mp_end - mp_start, FlashLayout.REGION_HASH_PTR, mp_version |
| 213 | + ) |
| 214 | + layout.add_region(3, fs_start, fs_end - fs_start, FlashLayout.REGION_HASH_NONE) |
| 215 | + layout.finalise() |
| 216 | + |
| 217 | + # Compute layout address. |
| 218 | + layout_addr = ( |
| 219 | + ((mp_end >> NRF_PAGE_SIZE_LOG2) << NRF_PAGE_SIZE_LOG2) |
| 220 | + + NRF_PAGE_SIZE |
| 221 | + - len(layout.data) |
| 222 | + ) |
| 223 | + if layout_addr < mp_end: |
| 224 | + layout_addr += NRF_PAGE_SIZE |
| 225 | + if layout_addr >= fs_start: |
| 226 | + print("ERROR: Flash layout information overlaps with filesystem") |
| 227 | + sys.exit(1) |
| 228 | + |
| 229 | + # Print information. |
| 230 | + if args.output is not sys.stdout: |
| 231 | + fmt = "{:13} 0x{:05x}..0x{:05x}" |
| 232 | + print(fmt.format("SoftDevice", sd_start, sd_end)) |
| 233 | + print(fmt.format("MicroPython", mp_start, mp_end)) |
| 234 | + print(fmt.format("Layout table", layout_addr, layout_addr + len(layout.data))) |
| 235 | + print(fmt.format("Filesystem", fs_start, fs_end)) |
| 236 | + |
| 237 | + # Output the new firmware as a hex file. |
| 238 | + output_firmware(args.output, firmware, layout_addr, layout.data) |
| 239 | + |
| 240 | + |
| 241 | +if __name__ == "__main__": |
| 242 | + main() |
0 commit comments