From 05b8d074af13901e6b64522f33f3320044e3273b Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Thu, 4 Dec 2025 18:26:43 +0000 Subject: [PATCH 1/4] Add UF2 combine command Combines 2 UF2s into 1, modifying the block numbers (and giving them same family ID) --- elf2uf2/elf2uf2.h | 1 + main.cpp | 110 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/elf2uf2/elf2uf2.h b/elf2uf2/elf2uf2.h index 8239a0b1..818ba63b 100644 --- a/elf2uf2/elf2uf2.h +++ b/elf2uf2/elf2uf2.h @@ -21,6 +21,7 @@ #define UF2_PAGE_SIZE (1u << LOG2_PAGE_SIZE) +uf2_block gen_abs_block(uint32_t abs_block_loc); bool check_abs_block(uf2_block block); int bin2uf2(std::shared_ptr in, std::shared_ptr out, uint32_t address, uint32_t family_id, model_t model, uint32_t abs_block_loc=0, bool verbose=false); int elf2uf2(std::shared_ptr in, std::shared_ptr out, uint32_t family_id, model_t model, uint32_t package_addr=0, uint32_t abs_block_loc=0, bool verbose=false); diff --git a/main.cpp b/main.cpp index f11cb3df..db17faa4 100644 --- a/main.cpp +++ b/main.cpp @@ -1370,11 +1370,41 @@ struct uf2_convert_command : public cmd { } }; +struct uf2_combine_command : public cmd { + uf2_combine_command() : cmd("combine") {} + bool execute(device_map &devices) override; + virtual device_support get_device_support() override { return none; } + + group get_cli() override { + return ( + option("--quiet").set(settings.quiet) % "Don't print any output" + + option("--verbose").set(settings.verbose) % "Print verbose output" + + named_typed_file_selection_x("infile1", 0, "uf2") % "First file to combine" + + named_typed_file_selection_x("infile1", 1, "uf2") % "Second file to combine" + + named_typed_file_selection_x("outfile", 2, "uf2") % "File to save output to" + + ( + option("--family") & family_id("family_id").set(settings.family_id) % "family ID for combined UF2 (defaults to first one)" + ).force_expand_help(true) % "UF2 Family options" + #if SUPPORT_RP2350_A2 + + ( + option("--abs-block").set(settings.uf2.abs_block) % "Add an absolute block" + + hex("abs_block_loc").set(settings.uf2.abs_block_loc).min(0) % "absolute block location (default to 0x10ffff00)" + ).force_expand_help(true).min(0) % "Errata RP2350-E10 Fix" + #endif + ); + } + + string get_doc() const override { + return "Combine multiple UF2 files."; + } +}; + vector> uf2_sub_commands { #if HAS_LIBUSB std::shared_ptr(new uf2_info_command()), #endif std::shared_ptr(new uf2_convert_command()), + std::shared_ptr(new uf2_combine_command()), }; struct uf2_command : public multi_cmd { @@ -6775,6 +6805,86 @@ bool uf2_convert_command::execute(device_map &devices) { return false; } +bool uf2_combine_command::execute(device_map &devices) { + if (get_file_type_idx(0) != filetype::uf2 || get_file_type_idx(1) != filetype::uf2 || get_file_type_idx(0) != filetype::uf2) { + fail(ERROR_ARGS, "All files must be UF2 files\n"); + } + + auto file1 = get_file_idx(ios::in|ios::binary, 0); + auto file2 = get_file_idx(ios::in|ios::binary, 1); + auto out = get_file_idx(ios::out|ios::binary, 2); + + out->seekp(0, ios::beg); + + unsigned int num_blocks = 0; + for (auto file : {file1, file2}) { + uf2_block block; + // Seek to 2nd block, in case 1st is abs_block + file->seekg(0, ios::beg); + file->read((char*)&block, sizeof(uf2_block)); + if (file->fail()) { + fail(ERROR_READ_FAILED, "unexpected end of input file"); + } + + #if SUPPORT_RP2350_A2 + if (check_abs_block(block)) { + // save abs block address + settings.uf2.abs_block = true; + settings.uf2.abs_block_loc = block.target_addr; + file->read((char*)&block, sizeof(uf2_block)); + } + #endif + + num_blocks += block.num_blocks; + + if (!settings.family_id) { + settings.family_id = block.file_size; + } + } + +#if SUPPORT_RP2350_A2 + if (settings.uf2.abs_block) { + uf2_block block = gen_abs_block(settings.uf2.abs_block_loc); + out->write((char*)&block, sizeof(uf2_block)); + } +#endif + + unsigned int block_no = 0; + for (auto file : {file1, file2}) { + file->seekg(0, ios::beg); + uf2_block block; + unsigned int pos = 0; + uint32_t next_family_id = 0; + do { + file->read((char*)&block, sizeof(uf2_block)); + if (file->fail()) { + if (file->eof()) { file->clear(); break; } + fail(ERROR_READ_FAILED, "unexpected end of input file"); + } + if (block.magic_start0 == UF2_MAGIC_START0 && block.magic_start1 == UF2_MAGIC_START1 && + block.magic_end == UF2_MAGIC_END) { + if (block.flags & UF2_FLAG_FAMILY_ID_PRESENT && + !(block.flags & UF2_FLAG_NOT_MAIN_FLASH) && block.payload_size == PAGE_SIZE) { + // ignore the absolute block + if (check_abs_block(block)) { + DEBUG_LOG("Ignoring RP2350-E10 absolute block\n"); + } else { + block.block_no = block_no; block_no++; + block.num_blocks = num_blocks; + block.file_size = settings.family_id; + out->write((char*)&block, sizeof(uf2_block)); + } + } + } + pos += sizeof(uf2_block); + } while (true); + } + + out->close(); + + return false; +} + // Dissassembly helpers string gpiodir(int val) { From f25aa9c7501bde998a85660ad00c1d1056302421 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Thu, 5 Feb 2026 12:58:19 +0000 Subject: [PATCH 2/4] Add partition and offset options to uf2 combine Allows placing second uf2 at offset, or in a partition defined in the first uf2 --- main.cpp | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/main.cpp b/main.cpp index db17faa4..588a9237 100644 --- a/main.cpp +++ b/main.cpp @@ -597,6 +597,10 @@ struct _settings { #else uint32_t abs_block_loc = 0; #endif + uint32_t offset = 0; + bool offset_set = false; + int partition = -1; + bool partition_set = false; } uf2; }; _settings settings; @@ -1384,7 +1388,15 @@ struct uf2_combine_command : public cmd { named_typed_file_selection_x("outfile", 2, "uf2") % "File to save output to" + ( option("--family") & family_id("family_id").set(settings.family_id) % "family ID for combined UF2 (defaults to first one)" - ).force_expand_help(true) % "UF2 Family options" + ).force_expand_help(true) % "UF2 Family options" + + ( + option("--offset").set(settings.uf2.offset_set) % "Offset second UF2 by amount" & + hex("offset").set(settings.uf2.offset) % "offset amount (default to 0)" + ).force_expand_help(true) % "Offset options" + + ( + option("--partition").set(settings.uf2.partition_set) % "Place second UF2 in partition (first UF2 must contain a partition table)" & + integer("partition").min_value(0).max_value(PARTITION_TABLE_MAX_PARTITIONS-1).set(settings.uf2.partition) % "partition number (default to 0)" + ).force_expand_help(true) % "Partition options" #if SUPPORT_RP2350_A2 + ( option("--abs-block").set(settings.uf2.abs_block) % "Add an absolute block" + @@ -3269,6 +3281,25 @@ std::unique_ptr find_last_block(memory_access &raw_access, vector> find_all_blocks(memory_access &raw_access, vector &bin) { + // todo read the right amount + uint32_t read_size = 0x1000; + DEBUG_LOG("Reading from %x size %x\n", raw_access.get_binary_start(), read_size); + bin = raw_access.read_vector(raw_access.get_binary_start(), read_size, true); + + std::unique_ptr first_block = find_first_block(bin, raw_access.get_binary_start()); + if (first_block) { + // verify stuff + get_more_bin_cb more_cb = [&raw_access](std::vector &bin, uint32_t offset, uint32_t size) { + DEBUG_LOG("Now reading from %x size %x\n", offset, size); + bin = raw_access.read_vector(offset, size, true); + }; + return get_all_blocks(bin, raw_access.get_binary_start(), first_block, more_cb); + } + + return std::vector>(); +} + std::shared_ptr get_bi_access(memory_access &raw_access) { vector bin; std::unique_ptr best_block = find_best_block(raw_access, bin); @@ -6810,13 +6841,42 @@ bool uf2_combine_command::execute(device_map &devices) { fail(ERROR_ARGS, "All files must be UF2 files\n"); } + if (settings.uf2.partition_set && settings.uf2.offset_set) { + fail(ERROR_ARGS, "Cannot use both partition and offset options together\n"); + } + auto file1 = get_file_idx(ios::in|ios::binary, 0); auto file2 = get_file_idx(ios::in|ios::binary, 1); auto out = get_file_idx(ios::out|ios::binary, 2); + if (settings.uf2.partition_set) { + auto access = get_file_memory_access(0); + + vector bin; + auto blocks = find_all_blocks(access, bin); + for (auto &block : blocks) { + auto partition_table = block->get_item(); + if (partition_table == nullptr) { + continue; + } + + if (settings.uf2.partition < 0 || settings.uf2.partition >= partition_table->partitions.size()) { + fail(ERROR_ARGS, "Partition table only contains partitions 0 -> %d\n", partition_table->partitions.size() - 1); + } + + settings.uf2.offset = partition_table->partitions[settings.uf2.partition].first_sector * 4096; + settings.uf2.offset_set = true; + break; + } + + if (!settings.uf2.offset_set) { + fail(ERROR_ARGS, "No partition table found in first UF2\n"); + } + } + out->seekp(0, ios::beg); - unsigned int num_blocks = 0; + unsigned int num_blocks = 0; for (auto file : {file1, file2}) { uf2_block block; // Seek to 2nd block, in case 1st is abs_block @@ -6850,6 +6910,7 @@ bool uf2_combine_command::execute(device_map &devices) { #endif unsigned int block_no = 0; + unsigned int file_no = 0; for (auto file : {file1, file2}) { file->seekg(0, ios::beg); uf2_block block; @@ -6872,12 +6933,16 @@ bool uf2_combine_command::execute(device_map &devices) { block.block_no = block_no; block_no++; block.num_blocks = num_blocks; block.file_size = settings.family_id; + if (settings.uf2.offset_set && file_no == 1) { + block.target_addr += settings.uf2.offset; + } out->write((char*)&block, sizeof(uf2_block)); } } } pos += sizeof(uf2_block); } while (true); + file_no++; } out->close(); From 3380646bcee8201d81cf42fbe2fc2e117b8c7026 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Wed, 6 May 2026 15:06:59 +0100 Subject: [PATCH 3/4] Change file numbers so outfile is 0 --- main.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/main.cpp b/main.cpp index 588a9237..ff3c8786 100644 --- a/main.cpp +++ b/main.cpp @@ -1383,9 +1383,9 @@ struct uf2_combine_command : public cmd { return ( option("--quiet").set(settings.quiet) % "Don't print any output" + option("--verbose").set(settings.verbose) % "Print verbose output" + - named_typed_file_selection_x("infile1", 0, "uf2") % "First file to combine" + - named_typed_file_selection_x("infile1", 1, "uf2") % "Second file to combine" + - named_typed_file_selection_x("outfile", 2, "uf2") % "File to save output to" + + named_typed_file_selection_x("infile1", 1, "uf2") % "First file to combine" + + named_typed_file_selection_x("infile2", 2, "uf2") % "Second file to combine" + + named_typed_file_selection_x("outfile", 0, "uf2") % "File to save output to" + ( option("--family") & family_id("family_id").set(settings.family_id) % "family ID for combined UF2 (defaults to first one)" ).force_expand_help(true) % "UF2 Family options" + @@ -6845,12 +6845,12 @@ bool uf2_combine_command::execute(device_map &devices) { fail(ERROR_ARGS, "Cannot use both partition and offset options together\n"); } - auto file1 = get_file_idx(ios::in|ios::binary, 0); - auto file2 = get_file_idx(ios::in|ios::binary, 1); - auto out = get_file_idx(ios::out|ios::binary, 2); + auto out = get_file_idx(ios::out|ios::binary, 0); + auto file1 = get_file_idx(ios::in|ios::binary, 1); + auto file2 = get_file_idx(ios::in|ios::binary, 2); if (settings.uf2.partition_set) { - auto access = get_file_memory_access(0); + auto access = get_file_memory_access(1); vector bin; auto blocks = find_all_blocks(access, bin); From d9ea78cc6ff0e7b04c8d94a6f76714b82f84de7e Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Thu, 14 May 2026 15:34:17 +0100 Subject: [PATCH 4/4] Add uf2 combine to readme --- README.md | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b80f0ba..4a78f9dc 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ SYNOPSIS: picotool reboot [-a] [-u] [-g ] [-c ] [device-selection] picotool otp list|get|set|load|dump|permissions|white-label picotool partition info|create - picotool uf2 info|convert + picotool uf2 info|convert|combine picotool version [-s] [] picotool coprodis [--quiet] [--verbose] picotool help [] @@ -906,6 +906,101 @@ OPTIONS: absolute block location (default to 0x10ffff00) ``` +### combine + +This command is used to combine multiple UF2 files (possibly with different family IDs) into a single file with the same family ID. This can be useful for loading a partition table and multiple partitions onto a device using a single file. + +```text +$ picotool help uf2 combine +UF2 COMBINE: + Combine multiple UF2 files. + +SYNOPSIS: + picotool uf2 combine [--quiet] [--verbose] [-t ] [-t ] [-t ] [--family ] + [--offset ] [--partition ] [[--abs-block] []] + +OPTIONS: + --quiet + Don't print any output + --verbose + Print verbose output + First file to combine + + The file name + -t + Specify file type (uf2) explicitly, ignoring file extension + Second file to combine + + The file name + -t + Specify file type (uf2) explicitly, ignoring file extension + File to save output to + + The file name + -t + Specify file type (uf2) explicitly, ignoring file extension + UF2 Family options + + family ID for combined UF2 (defaults to first one) + Offset options + --offset + Offset second UF2 by amount + + offset amount (default to 0) + Partition options + --partition + Place second UF2 in partition (first UF2 must contain a partition table) + + partition number (default to 0) + Errata RP2350-E10 Fix + --abs-block + Add an absolute block + + absolute block location (default to 0x10ffff00) +``` + +The `--partition` argument can be used to place the second file in a partition number, provided that there is a partition table in the first file. For example, take this `pt.json` +```json +{ + "unpartitioned": { + "families": ["absolute"], + "permissions": { + "secure": "rw", + "nonsecure": "rw", + "bootloader": "rw" + } + }, + "partitions": [ + { + "size": "1024K", + "families": ["rp2350-arm-s", "rp2350-riscv"], + "permissions": { + "secure": "rw", + "nonsecure": "rw", + "bootloader": "rw" + } + }, + { + "size": "1024K", + "families": ["data"], + "permissions": { + "secure": "rw", + "nonsecure": "rw", + "bootloader": "rw" + } + } + ] +} +``` + +If you want to load this partition table, and put `code.uf2` and `data.uf2` into the partitions, all using a single UF2, you would run: +```text +$ picotool partition create pt.json pt.uf2 +$ picotool uf2 combine pt.uf2 code.uf2 tmp.uf2 --partition 0 +$ picotool uf2 combine tmp.uf2 data.uf2 combined.uf2 --partition 1 +$ picotool load -x combined.uf2 +``` + ### info This command reads the information on a device about why a UF2 download has failed. It will only give information if the most recent download has failed.