diff --git a/src/modm/board/feather_m4/module.md b/src/modm/board/feather_m4/module.md index 1de3c10b2c..402fb1d4ee 100644 --- a/src/modm/board/feather_m4/module.md +++ b/src/modm/board/feather_m4/module.md @@ -65,12 +65,16 @@ The Feather M4 has a [UF2](https://github.com/microsoft/uf2) bootloader. By using a utility to convert the '.elf' files (generated by scons or make) to '.uf2' files, it is then possible to copy the uf2 file directly to the board. -One of the more common tools for this conversion is ['uf2conv.py'](https://github.com/microsoft/uf2/tree/master/utils) (although the elf file needs to be converted to a -binary or hex file first). The converter that is supplied with modm, 'elf2uf2.py', -is ONLY for the rp2040, and will not work here. - -However, the bootloader on the Feather is compatible with [BOSSA](https://www.shumatech.com/web/products/bossa); so the easiest method when working with modm is to use -'scons program-bossac' or 'make program-bossac' to load your program into -flash. (Be sure to press the reset button twice to get the board into bootloader -mode before flashing.) +In modm, the conversion utility is called 'elf2uf2.py' (in tools/modm_tools), +and is incorporated into the build system. Use 'scons uf2' or 'make uf2' to +create the uf2 file. + +Alternatively, the bootloader on the Feather is compatible with [BOSSA](https://www.shumatech.com/web/products/bossa); +so another method is to use 'scons program-bossac' or 'make program-bossac' to +load your program into flash. + +With either method, you must press the reset button twice to get the board into +bootloader mode before flashing with BOSSA or UF2 copying. (In bootloader mode, +a volume named FEATHERBOOT will be mounted; and that is where the UF2 file +should be copied.) diff --git a/tools/modm_tools/elf2uf2.py b/tools/modm_tools/elf2uf2.py index b6cfec10b8..f36087a0da 100755 --- a/tools/modm_tools/elf2uf2.py +++ b/tools/modm_tools/elf2uf2.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # # Copyright (c) 2022, Andrey Kunitsyn +# Copyright (c) 2023, Thomas Rush # # This file is part of the modm project. # @@ -25,32 +26,58 @@ (\* *only ARM Cortex-M targets*) """ -import os import struct from pathlib import Path - verbose = False +warned = False + +# uf2_config is a dictionary with the family name (which is basically just the +# start of the part name) as the key, and a two item tuple as the value. +# The first value of the tuple is the family id (a 32-bit number that was +# randomly assigned and agreed upon), and the second is a boolean indicating +# whether or not an FPU is available for this part. + +# IMPORTANT: The search for the family name using the target is straightforward, +# EXCEPT for three names which overlap: stm32f407vg, stm32f407, and stm32f4. +# Using 'startswith()' requires that they MUST be checked in that order. +# If target was 'stm32f407vgt6', for example, and the entry 'stm32f4' came +# first in the name list, then 'target.startswith(entry)' would be true and +# the search would end at that point, not reaching the correct entry of +# 'stm32f407vg'. Therefore, if any entries are added to this dictionary, the +# order of those three must NOT be changed. + +uf2_config = { + "rp2040": ( 0xe48bff56, False ), "samd21": ( 0x68ed2b88, False ), + # same5x shares its id with samd51 + "samd51": ( 0x55114460, True ), "same5": ( 0x55114460, True ), + "saml21": ( 0x1851780a, False ), "stm32f0": ( 0x647824b6, False ), + "stm32f1": ( 0x5ee21072, False ), "stm32f2": ( 0x5d1a0a2e, False ), + "stm32f3": ( 0x6b846188, True ), "stm32f407vg": ( 0x8fb060fe, True ), + "stm32f407": ( 0x6d0922fa, True ), "stm32f4": ( 0x57755a57, True ), + "stm32f7": ( 0x53b80f00, True ), "stm32g0": ( 0x300f5633, False ), + "stm32g4": ( 0x4c71240a, True ), "stm32h7": ( 0x6db66082, True ), + "stm32l0": ( 0x202e3a91, False ), "stm32l1": ( 0x1e1f432d, False ), + "stm32l4": ( 0x00ff6919, True ), "stm32l5": ( 0x04240bdf, True ), + "stm32wb": ( 0x70d16653, True ), "stm32wl": ( 0x21460ff0, True ) +} + +# UF2 constants +UF2_MAGIC_START0 = b'UF2\n' +UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected +UF2_MAGIC_END = 0x0AB16F30 # Ditto UF2_FLAG_NOT_MAIN_FLASH = 0x00000001 UF2_FLAG_FILE_CONTAINER = 0x00001000 UF2_FLAG_FAMILY_ID_PRESENT = 0x00002000 UF2_FLAG_MD5_PRESENT = 0x00004000 -uf2_config = { - "rp2040": { - "MAGIC_START0": 0x0A324655, - "MAGIC_START1": 0x9E5D5157, - "MAGIC_END": 0x0AB16F30, - "FAMILY_ID": 0xe48bff56, - "PAGE_SIZE": (1 << 8), - } -} +UF2_PAGE_SIZE = (1 << 8) #struct uf2_block { # // 32 byte header -# uint32_t magic_start0; +# uint32_t magic_start0; encoded as a byte string '4s' for python # uint32_t magic_start1; # uint32_t flags; # uint32_t target_addr; @@ -58,114 +85,133 @@ # uint32_t block_no; # uint32_t num_blocks; # uint32_t file_size; // or familyID; -uf2_block = "= (addr+size): - if range["type"] == "NO_CONTENTS" and not uninitialized: - raise Exception("ELF contains memory contents for uninitialized memory") + if is_phys_addr and range["type"] == "NO_CONTENTS" and size != 0: + err = "ELF has contents for uninitialized memory at {:08x}->{:08x}".format(addr,addr+size) + raise Exception(err) + return + + # No range was found. With some targets, external memory is added, or a contiguous region is split + # into 2 or more regions in the device file while the linkerscript provides a way to lump it into + # one. These added memory ranges (typically RAM) do not show up in the build system commands for + # this program, and strict adherence to the command-provided ranges can cause unnecessary failure. + # If 'size' is zero, the conversion continues without any message. When 'size' is non-zero, then if + # 'addr' is a paddr (physical address, meaning there is content to be loaded), an exception is + # raised and the program aborts; otherwise a warning is printed and the conversion continues. + # + # (As a side effect of continuing with missing ranges: when executing from the command line, the + # only range that MUST be provided is for the flash, with a type of 'CONTENTS'.) + + global warned + if size != 0: + err = "Memory segment {:08x}->{:08x} is outside of supplied address ranges.".format(addr, addr+size) + if is_phys_addr: + raise Exception(err) + if not warned: + # This message is printed once; the range message is printed for each offending segment. + print("Warning: ranges supplied don't match or don't include all those in ELF file.") + warned = True + print(err) + + +def read_and_check_elf32_ph_entries(buffer, ph_offset, ph_num, valid_ranges, pages, page_size): + for ph in range(ph_num): + type, offset, vaddr, paddr, filesz, memsz = struct.unpack_from(elf32_ph_entry,buffer,ph_offset+ph*elf32_ph_entry_size) + if type == PT_LOAD and memsz !=0: + check_address_range(valid_ranges, paddr, filesz, True) + check_address_range(valid_ranges, vaddr, memsz, False) if verbose: - print(("{} segment {:08x}->{:08x} ({:08x}->{:08x})").format(uninitialized and "Uninitialized" or "Mapped", addr, addr + size, vaddr, vaddr+size)) - return range - raise Exception("Memory segment {:08x}->{:08x} is outside of valid address range for device".format(addr, addr+size)) - - -def read_and_check_elf32_ph_entries(buffer, eh, valid_ranges, pages, page_size): - for i in range(eh[6]): - entry = struct.unpack_from(elf32_ph_entry,buffer,eh[1]+i*elf32_ph_entry_size) - if entry[0] == PT_LOAD and entry[5] !=0: - mapped_size = min(entry[5],entry[4]) - if mapped_size != 0: - ar = check_address_range(valid_ranges,entry[3],entry[2],mapped_size,False) - # we don"t download uninitialized, generally it is BSS and should be zero-ed by crt0.S, or it may be COPY areas which are undefined - if ar["type"] != "CONTENTS": - if verbose: - print(" ignored"); - continue - addr = entry[3]; - remaining = mapped_size; - file_offset = entry[1]; + print(("{} segment {:08x}->{:08x} ({:08x}->{:08x})").format(memsz>filesz \ + and "Uninitialized" or "Mapped",paddr,paddr+filesz,vaddr,vaddr+memsz)) + if filesz != 0: + addr = paddr + remaining = filesz + file_offset = offset while remaining > 0: off = addr & (page_size - 1) chlen = min(remaining, page_size - off) @@ -174,62 +220,59 @@ def read_and_check_elf32_ph_entries(buffer, eh, valid_ranges, pages, page_size): if key in pages: fragments = pages[key] for fragment in fragments: - if (off < fragment["page_offset"] + fragment["bytes"]) != ((off + chlen) <= fragment["page_offset"]): + page_offset, byte_count = fragment[1:] + if (off < page_offset + byte_count) != ((off + chlen) <= page_offset): raise Exception("In memory segments overlap") - - fragments.append({"file_offset":file_offset,"page_offset":off,"bytes":chlen}) + fragments.append(tuple([file_offset, off, chlen])) addr += chlen file_offset += chlen remaining -= chlen pages[key] = fragments - if entry[5] > entry[4]: - # we have some uninitialized data too - check_address_range(valid_ranges, entry[3] + entry[4], entry[2] + entry[4], entry[5] - entry[4], True); - -def realize_page(buffer, fragments): +def realize_page(buffer, page): result = bytes(476) - for frag in fragments: - data = buffer[frag["file_offset"]:frag["file_offset"]+frag["bytes"]] - if len(data) != frag["bytes"]: + for frag in page: + file_offset, page_offset, byte_count = frag + data = buffer[file_offset:file_offset+byte_count] + if len(data) != byte_count: raise Exception("failed extract") - if frag["page_offset"] == 0: - result = data + result[frag["page_offset"]+frag["bytes"]:] + if page_offset == 0: + result = data + result[byte_count:] else: - result = result[:frag["page_offset"]] + data + result[frag["page_offset"]+frag["bytes"]:] + result = result[:page_offset] + data + result[page_offset+byte_count:] if len(result) != 476: raise Exception("failed concat") return result -def convert_data(source_bytes,target,ranges): - eh = read_header(source_bytes) - config = uf2_config[target] +def convert_data(source_bytes, target, family, ranges): + family_id, has_fpu = uf2_config[family] + ph_offset, ph_num = read_header(source_bytes, has_fpu) if verbose: - print("Build for chip:{}".format(target)) + print("Build for chip {} in UF2 family {}".format(target, family)) pages = {} - read_and_check_elf32_ph_entries(source_bytes,eh,ranges,pages,config["PAGE_SIZE"]) + read_and_check_elf32_ph_entries(source_bytes, ph_offset, ph_num, ranges, pages, UF2_PAGE_SIZE) if len(pages) == 0: raise Exception("The input file has no memory pages") num_blocks = len(pages) page_num = 0 file_content = bytes(0) - for target_addr,pages in pages.items(): + for target_addr, page in pages.items(): if verbose: - print("Page {} / {} {:08x}".format( page_num, num_blocks, target_addr)) + print("Page {} / {} {:08x}".format(page_num, num_blocks, target_addr)) - data = realize_page(source_bytes,pages) + data = realize_page(source_bytes,page) block = struct.pack(uf2_block, - config["MAGIC_START0"], - config["MAGIC_START1"], + UF2_MAGIC_START0, + UF2_MAGIC_START1, UF2_FLAG_FAMILY_ID_PRESENT, target_addr, - config["PAGE_SIZE"], + UF2_PAGE_SIZE, page_num, num_blocks, - config["FAMILY_ID"]) + data + struct.pack("= range["end"]: + err += "Supplied memory range {:08x}->{:08x} has length <= 0\n".format(range["start"],range["end"]) + if range["type"] == "CONTENTS": + no_content = False + if no_content: + err += "No ranges with type 'CONTENTS'\n" + if len(err) > 0: + raise Exception(err) + + def convert(source, output, target, ranges): + family = check_target(target) + check_valid_range(ranges) source_bytes = Path(source).read_bytes() - uf2 = convert_data(source_bytes,target, ranges) + uf2 = convert_data(source_bytes, target, family, ranges) Path(output).write_bytes(uf2) def parse_range(strval): if strval.startswith("0x"): - return int(strval[2:],16) + return int(strval,16) return int(strval) @@ -258,20 +326,17 @@ def parse_range(strval): dest="source", metavar="ELF", help="Input ELF binary") - parser.add_argument( - "-o", "--output", - dest="output", - required=True, - help="Destination UF2 image") parser.add_argument( "--verbose", - dest="verbose", action="store_true", help="Verbose output") + parser.add_argument( + "-o", "--output", + required=True, + help="Destination UF2 image") parser.add_argument( "--target", - dest="target", - default="rp2040", + required=True, help="Target chip") parser.add_argument( "--range", @@ -282,6 +347,7 @@ def parse_range(strval): args = parser.parse_args() verbose = args.verbose + target = args.target.lower() ranges = [] for r in args.ranges: start, end, t = r.split(":") @@ -291,4 +357,5 @@ def parse_range(strval): "type": t }) - convert(args.source, args.output, args.target, ranges) + + convert(args.source, args.output, target, ranges)