From 51d83031fa0398dfee4248be3ffa67e99d2cfc3a Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Mon, 12 Aug 2024 14:07:57 +0100 Subject: [PATCH 01/67] Improve family ID auto-detection * If no block loop, then check the checksum to detect RP2040 binaries * If no IMAGE_DEF, then assume partition table * Check for partition table, when throwing error that file cannot be loaded onto the device --- bintool/bintool.cpp | 10 ++++ bintool/bintool.h | 1 + bintool/mbedtls_wrapper.c | 104 ++++++++++++++++++++++++++++++++++++++ bintool/mbedtls_wrapper.h | 2 + main.cpp | 36 +++++++++++-- 5 files changed, 148 insertions(+), 5 deletions(-) diff --git a/bintool/bintool.cpp b/bintool/bintool.cpp index c5462d2..ab605c3 100644 --- a/bintool/bintool.cpp +++ b/bintool/bintool.cpp @@ -747,6 +747,16 @@ void verify_block(std::vector bin, uint32_t storage_addr, uint32_t runt } +uint32_t calc_checksum(std::vector bin) { + assert(bin.size() == 252); + + uint32_t checksum = 0; + crc32(bin.data(), bin.size(), &checksum); + + return checksum; +} + + int encrypt(elf_file *elf, block *new_block, const private_t aes_key, const public_t public_key, const private_t private_key, bool hash_value, bool sign) { std::vector to_enc = get_lm_hash_data(elf, new_block); diff --git a/bintool/bintool.h b/bintool/bintool.h index b2c6499..49935e2 100644 --- a/bintool/bintool.h +++ b/bintool/bintool.h @@ -38,4 +38,5 @@ block place_new_block(std::vector &bin, uint32_t storage_addr, std::uni std::vector hash_andor_sign(std::vector bin, uint32_t storage_addr, uint32_t runtime_addr, block *new_block, const public_t public_key, const private_t private_key, bool hash_value, bool sign, bool clear_sram = false); std::vector encrypt(std::vector bin, uint32_t storage_addr, uint32_t runtime_addr, block *new_block, const private_t aes_key, const public_t public_key, const private_t private_key, bool hash_value, bool sign); void verify_block(std::vector bin, uint32_t storage_addr, uint32_t runtime_addr, block *block, verified_t &hash_verified, verified_t &sig_verified); + uint32_t calc_checksum(std::vector bin); #endif diff --git a/bintool/mbedtls_wrapper.c b/bintool/mbedtls_wrapper.c index 1d2e115..b622da6 100644 --- a/bintool/mbedtls_wrapper.c +++ b/bintool/mbedtls_wrapper.c @@ -240,3 +240,107 @@ uint32_t mb_verify_signature_secp256k1( return 0; } + +// Not actually mbedtls, but leaving in here anyway +uint32_t poly8_lookup[256] = +{ + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, + 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, + 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, + 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, + 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, + 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, + 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, + 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, + 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, + 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, + 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, + 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, + 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, + 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, + 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, + 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, + 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, + 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, + 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, + 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, + 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, + 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, + 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, + 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, + 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, + 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, + 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, + 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, + 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, + 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, + 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, + 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, + 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, + 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, + 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, + 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, + 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, + 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, + 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, + 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, + 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, + 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, + 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, + 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, + 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, + 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, + 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, + 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, + 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, + 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, + 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, + 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, + 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, + 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, + 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, + 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, + 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, + 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, + 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, + 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, + 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D +}; + +uint32_t crc32_byte(const uint8_t *p, uint32_t bytelength) +{ + uint32_t crc = 0xffffffff; + while (bytelength-- !=0) crc = poly8_lookup[((uint8_t) crc ^ *(p++))] ^ (crc >> 8); + return crc; +} + +uint8_t rev_8(uint8_t b) { + b = (b & 0xF0) >> 4 | (b & 0x0F) << 4; + b = (b & 0xCC) >> 2 | (b & 0x33) << 2; + b = (b & 0xAA) >> 1 | (b & 0x55) << 1; + return b; +} + +uint32_t rev_32(uint32_t b) { + uint8_t b0 = rev_8(b >> 24); + uint8_t b1 = rev_8(b >> 16); + uint8_t b2 = rev_8(b >> 8); + uint8_t b3 = rev_8(b); + return (b3 << 24) | (b2 << 16) | (b1 << 8) | b0; +} + +void mb_crc32(const uint8_t *data, size_t len, uint32_t* cs_out) { + uint8_t rev_data[252] = {}; + for (size_t i=0; i < sizeof(rev_data); i++) { + rev_data[i] = rev_8(data[i]); + } + + uint32_t crc = crc32_byte(rev_data, sizeof(rev_data)); + + crc = rev_32(crc); + + *cs_out = crc; +} diff --git a/bintool/mbedtls_wrapper.h b/bintool/mbedtls_wrapper.h index c4e2b77..de36d1b 100644 --- a/bintool/mbedtls_wrapper.h +++ b/bintool/mbedtls_wrapper.h @@ -44,6 +44,7 @@ typedef message_digest_t private_t; void mb_sha256_buffer(const uint8_t *data, size_t len, message_digest_t *digest_out); void mb_aes256_buffer(const uint8_t *data, size_t len, uint8_t *data_out, const private_t *key, iv_t *iv); void mb_sign_sha256(const uint8_t *entropy, size_t entropy_size, const message_digest_t *m, const public_t *p, const private_t *d, signature_t *out); +void mb_crc32(const uint8_t *data, size_t len, uint32_t* cs_out); uint32_t mb_verify_signature_secp256k1( signature_t signature[1], @@ -54,6 +55,7 @@ uint32_t mb_verify_signature_secp256k1( #define aes256_buffer mb_aes256_buffer #define sign_sha256 mb_sign_sha256 #define verify_signature_secp256k1 mb_verify_signature_secp256k1 +#define crc32 mb_crc32 #ifdef __cplusplus }; diff --git a/main.cpp b/main.cpp index 4f5b2fe..9963359 100644 --- a/main.cpp +++ b/main.cpp @@ -3467,12 +3467,32 @@ uint32_t get_family_id(uint8_t file_idx) { vector bin; std::unique_ptr best_block = find_best_block(file_access, bin); if (best_block == NULL) { - // No block, so RP2040 - DEBUG_LOG("Detected family id %s\n", family_name(RP2040_FAMILY_ID).c_str()); - return RP2040_FAMILY_ID; + // No block, so RP2040 or absolute + if (file_access.get_binary_start() == FLASH_START) { + vector checksum_data = {}; + file_access.read_into_vector(FLASH_START, 252, checksum_data); + uint32_t checksum = file_access.read_int(FLASH_START + 252); + if (checksum == calc_checksum(checksum_data)) { + // Checksum is correct, so RP2040 + DEBUG_LOG("Detected family id %s due to boot2 checksum\n", family_name(RP2040_FAMILY_ID).c_str()); + return RP2040_FAMILY_ID; + } else { + // Checksum incorrect, so absolute + DEBUG_LOG("Assumed family id %s\n", family_name(ABSOLUTE_FAMILY_ID).c_str()); + return ABSOLUTE_FAMILY_ID; + } + } else { + // no_flash RP2040 binaries have no checksum + DEBUG_LOG("Assumed family id %s\n", family_name(RP2040_FAMILY_ID).c_str()); + return RP2040_FAMILY_ID; + } } auto first_item = best_block->items[0].get(); - if (first_item->type() != PICOBIN_BLOCK_ITEM_1BS_IMAGE_TYPE) fail(ERROR_INCOMPATIBLE, "Cannot autodetect UF2 family: No IMAGE_DEF found\n"); + if (first_item->type() != PICOBIN_BLOCK_ITEM_1BS_IMAGE_TYPE) { + // This will apply for partition tables + DEBUG_LOG("Assumed family id %s due to block with no IMAGE_DEF\n", family_name(ABSOLUTE_FAMILY_ID).c_str()); + return ABSOLUTE_FAMILY_ID; + } auto image_def = dynamic_cast(first_item); if (image_def->image_type() == type_exe) { if (image_def->chip() == chip_rp2040) { @@ -4162,7 +4182,13 @@ bool load_command::execute(device_map &devices) { settings.offset_set = true; settings.partition_size = end - start; } else { - fail(ERROR_NOT_POSSIBLE, "This file cannot be loaded into the partition table on the device"); + // Check if partition table is present, for correct error message + auto partitions = get_partitions(con); + if (!partitions) { + fail(ERROR_NOT_POSSIBLE, "This file cannot be loaded onto a device with no partition table"); + } else { + fail(ERROR_NOT_POSSIBLE, "This file cannot be loaded into the partition table on the device"); + } } } } From 3c457839d16668f73af603c44368a3cd38eede7c Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Mon, 12 Aug 2024 11:44:40 +0100 Subject: [PATCH 02/67] Display embedded block info if there is no binary info Useful for encrypted binaries and data --- main.cpp | 228 +++++++++++++++++++++++++++---------------------------- 1 file changed, 114 insertions(+), 114 deletions(-) diff --git a/main.cpp b/main.cpp index 9963359..0bc999b 100644 --- a/main.cpp +++ b/main.cpp @@ -2948,120 +2948,6 @@ void info_guts(memory_access &raw_access, void *con) { if (binary_end) info_pair("binary end", hex_string(binary_end)); - vector bin; - std::unique_ptr best_block = find_best_block(raw_access, bin); - if (best_block) { - verified_t hash_verified = none; - verified_t sig_verified = none; - #if HAS_MBEDTLS - verify_block(bin, raw_access.get_binary_start(), raw_access.get_binary_start(), best_block.get(), hash_verified, sig_verified); - #endif - - // Image Def - auto image_def = best_block->get_item(); - if (image_def != nullptr) { - if (image_def->image_type() == type_exe) { - switch (image_def->chip()) { - case chip_rp2040: - info_pair("target chip", "RP2040"); - break; - case chip_rp2350: - info_pair("target chip", "RP2350"); - switch (image_def->cpu()) { - case cpu_riscv: - info_pair("image type", "RISC-V"); - break; - case cpu_varmulet: - info_pair("image type", "Varmulet"); - break; - case cpu_arm: - if (image_def->security() == sec_s) { - info_pair("image type", "ARM Secure"); - } else if (image_def->security() == sec_ns) { - info_pair("image type", "ARM Non-Secure"); - } else if (image_def->security() == sec_unspecified) { - info_pair("image type", "ARM"); - } - } - break; - default: - break; - } - } else if (image_def->image_type() == type_data) { - info_pair("image type", "data"); - } - } - - // Version - auto version = best_block->get_item(); - if (version != nullptr) { - info_pair("version", std::to_string(version->major) + "." + std::to_string(version->minor)); - if (version->otp_rows.size() > 0) { - info_pair("rollback version", std::to_string(version->rollback)); - std::stringstream rows; - for (const auto row : version->otp_rows) { rows << hex_string(row, 3) << " "; } - info_pair("rollback rows", rows.str()); - } - } - - // Load Map - // todo what should this really report - auto load_map = best_block->get_item(); - if (load_map != nullptr) { - for (unsigned int i=0; i < load_map->entries.size(); i++) { - std::stringstream ss; - auto e = load_map->entries[i]; - if (e.storage_address == 0) { - ss << "Clear 0x" << std::hex << e.runtime_address; - ss << "+0x" << std::hex << e.size; - } else if (e.storage_address != e.runtime_address) { - if (is_address_initialized(rp2350_address_ranges_flash, e.runtime_address)) { - ss << "ERROR: COPY TO FLASH NOT PERMITTED "; - } - ss << "Copy 0x" << std::hex << e.storage_address; - ss << "+0x" << std::hex << e.size; - ss << " to 0x" << std::hex << e.runtime_address; - } else { - ss << "Load 0x" << std::hex << e.storage_address; - ss << "+0x" << std::hex << e.size; - } - info_pair("load map entry " + std::to_string(i), ss.str()); - } - } - - // Rolling Window Delta - auto rwd = best_block->get_item(); - if (rwd != nullptr) { - info_pair("rolling window delta", hex_string(rwd->addr)); - } - - // Vector Table - auto vtor = best_block->get_item(); - if (vtor != nullptr) { - info_pair("vector table", hex_string(vtor->addr)); - } - - // Entry Point - auto entry_point = best_block->get_item(); - if (entry_point != nullptr) { - std::stringstream ss; - ss << "EP " << hex_string(entry_point->ep); - ss << ", SP " << hex_string(entry_point->sp); - if (entry_point->splim_set) ss << ", SPLIM " << hex_string(entry_point->splim); - info_pair("entry point", ss.str()); - } - - // Hash and Sig - if (hash_verified != none) { - info_pair("hash value", hash_verified == passed ? "verified" : "incorrect"); - } - if (sig_verified != none) { - info_pair("signature", sig_verified == passed ? "verified" : "incorrect"); - } - } else if (get_model(raw_access) == rp2350) { - fos << "WARNING: Binary on RP2350 device does not contain a block loop - this binary will not boot\n"; - } - for (auto &f: deferred) { f(); } @@ -3096,6 +2982,120 @@ void info_guts(memory_access &raw_access, void *con) { info_pair("build attributes", cli::join(build_attributes, "\n")); } } + vector bin; + std::unique_ptr best_block = find_best_block(raw_access, bin); + if (best_block && (settings.info.show_basic || settings.info.all)) { + select_group(program_info); + verified_t hash_verified = none; + verified_t sig_verified = none; + #if HAS_MBEDTLS + verify_block(bin, raw_access.get_binary_start(), raw_access.get_binary_start(), best_block.get(), hash_verified, sig_verified); + #endif + + // Image Def + auto image_def = best_block->get_item(); + if (image_def != nullptr) { + if (image_def->image_type() == type_exe) { + switch (image_def->chip()) { + case chip_rp2040: + info_pair("target chip", "RP2040"); + break; + case chip_rp2350: + info_pair("target chip", "RP2350"); + switch (image_def->cpu()) { + case cpu_riscv: + info_pair("image type", "RISC-V"); + break; + case cpu_varmulet: + info_pair("image type", "Varmulet"); + break; + case cpu_arm: + if (image_def->security() == sec_s) { + info_pair("image type", "ARM Secure"); + } else if (image_def->security() == sec_ns) { + info_pair("image type", "ARM Non-Secure"); + } else if (image_def->security() == sec_unspecified) { + info_pair("image type", "ARM"); + } + } + break; + default: + break; + } + } else if (image_def->image_type() == type_data) { + info_pair("image type", "data"); + } + } + + // Version + auto version = best_block->get_item(); + if (version != nullptr) { + info_pair("version", std::to_string(version->major) + "." + std::to_string(version->minor)); + if (version->otp_rows.size() > 0) { + info_pair("rollback version", std::to_string(version->rollback)); + std::stringstream rows; + for (const auto row : version->otp_rows) { rows << hex_string(row, 3) << " "; } + info_pair("rollback rows", rows.str()); + } + } + + // Load Map + // todo what should this really report + auto load_map = best_block->get_item(); + if (load_map != nullptr) { + for (unsigned int i=0; i < load_map->entries.size(); i++) { + std::stringstream ss; + auto e = load_map->entries[i]; + if (e.storage_address == 0) { + ss << "Clear 0x" << std::hex << e.runtime_address; + ss << "+0x" << std::hex << e.size; + } else if (e.storage_address != e.runtime_address) { + if (is_address_initialized(rp2350_address_ranges_flash, e.runtime_address)) { + ss << "ERROR: COPY TO FLASH NOT PERMITTED "; + } + ss << "Copy 0x" << std::hex << e.storage_address; + ss << "+0x" << std::hex << e.size; + ss << " to 0x" << std::hex << e.runtime_address; + } else { + ss << "Load 0x" << std::hex << e.storage_address; + ss << "+0x" << std::hex << e.size; + } + info_pair("load map entry " + std::to_string(i), ss.str()); + } + } + + // Rolling Window Delta + auto rwd = best_block->get_item(); + if (rwd != nullptr) { + info_pair("rolling window delta", hex_string(rwd->addr)); + } + + // Vector Table + auto vtor = best_block->get_item(); + if (vtor != nullptr) { + info_pair("vector table", hex_string(vtor->addr)); + } + + // Entry Point + auto entry_point = best_block->get_item(); + if (entry_point != nullptr) { + std::stringstream ss; + ss << "EP " << hex_string(entry_point->ep); + ss << ", SP " << hex_string(entry_point->sp); + if (entry_point->splim_set) ss << ", SPLIM " << hex_string(entry_point->splim); + info_pair("entry point", ss.str()); + } + + // Hash and Sig + if (hash_verified != none) { + info_pair("hash value", hash_verified == passed ? "verified" : "incorrect"); + } + if (sig_verified != none) { + info_pair("signature", sig_verified == passed ? "verified" : "incorrect"); + } + } else if (get_model(raw_access) == rp2350) { + fos << "WARNING: Binary on RP2350 device does not contain a block loop - this binary will not boot\n"; + } } catch (std::invalid_argument &e) { fos << "Error reading binary info\n"; #if HAS_LIBUSB From 3783fb42c0cb7dfe0fae2787b0a42d415df45455 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Tue, 13 Aug 2024 11:16:52 +0100 Subject: [PATCH 03/67] Fix #110 - Improve message when PICOTOOL_NO_LIBUSB is set --- CMakeLists.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d75982e..8e86298 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -242,7 +242,11 @@ else() endif() if (NOT LIBUSB_FOUND) - message("libUSB is not found - no USB support will be built") + if (PICOTOOL_NO_LIBUSB) + message("PICOTOOL_NO_LIBUSB is set - no USB support will be built") + else() + message("libUSB is not found - no USB support will be built") + endif() target_compile_definitions(picotool PRIVATE HAS_LIBUSB=0) target_link_libraries(picotool picoboot_connection_header) From 0c8dcc16e395d2de7bfa5618721997b41e312644 Mon Sep 17 00:00:00 2001 From: Ferdinand Bachmann Date: Tue, 13 Aug 2024 12:36:32 +0200 Subject: [PATCH 04/67] Compile picoboot_flash_id_cmd from source and add objdump (#107) Add full compilation of picoboot_flash_id_cmd from source and add objdump Uses same mechanism as xip_ram_perms --------- Co-authored-by: William Vinnicombe Co-authored-by: will-v-pi <108662275+will-v-pi@users.noreply.github.com> --- CMakeLists.txt | 36 +++++++- picoboot_connection/picoboot_connection.c | 100 +++++++++++++++------- picoboot_flash_id/CMakeLists.txt | 25 ++++++ picoboot_flash_id/flash_id.bin | Bin 0 -> 152 bytes picoboot_flash_id/flash_id.c | 70 +++++++++++++++ xip_ram_perms/CMakeLists.txt | 2 +- xip_ram_perms/pico_sdk_import.cmake | 62 -------------- 7 files changed, 199 insertions(+), 96 deletions(-) create mode 100644 picoboot_flash_id/CMakeLists.txt create mode 100755 picoboot_flash_id/flash_id.bin create mode 100644 picoboot_flash_id/flash_id.c delete mode 100644 xip_ram_perms/pico_sdk_import.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e86298..5ca5246 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,6 +84,29 @@ if (NOT PICOTOOL_NO_LIBUSB) DEPENDS xip_ram_perms ) + # compile flash_id + ExternalProject_Add(flash_id + PREFIX picoboot_flash_id + SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/picoboot_flash_id + BINARY_DIR ${CMAKE_BINARY_DIR}/picoboot_flash_id + CMAKE_ARGS + "-DCMAKE_MAKE_PROGRAM:FILEPATH=${CMAKE_MAKE_PROGRAM}" + "-DPICO_SDK_PATH:FILEPATH=${PICO_SDK_PATH}" + "-DUSE_PRECOMPILED:BOOL=${USE_PRECOMPILED}" + BUILD_ALWAYS 1 # todo remove this + INSTALL_COMMAND "" + ) + + set(FLASH_ID_BIN ${CMAKE_BINARY_DIR}/picoboot_flash_id/flash_id.bin) + add_executable(flash_id_bin IMPORTED) + add_dependencies(flash_id_bin flash_id) + set_property(TARGET flash_id_bin PROPERTY IMPORTED_LOCATION ${FLASH_ID_BIN}) + # copy flash_id.bin into build directory + add_custom_command(TARGET flash_id + COMMAND ${CMAKE_COMMAND} -E copy ${FLASH_ID_BIN} ${CMAKE_BINARY_DIR}/flash_id.bin + DEPENDS flash_id + ) + # We want to generate headers from WELCOME.HTM etc. ExternalProject_Add(otp_header_parser PREFIX otp_header_parser @@ -144,7 +167,10 @@ if (NOT PICOTOOL_NO_LIBUSB) endif() endif() -add_custom_target(binary_data DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/rp2350.rom.h ${CMAKE_CURRENT_BINARY_DIR}/xip_ram_perms_elf.h) +add_custom_target(binary_data DEPENDS + ${CMAKE_CURRENT_BINARY_DIR}/rp2350.rom.h + ${CMAKE_CURRENT_BINARY_DIR}/xip_ram_perms_elf.h + ${CMAKE_CURRENT_BINARY_DIR}/flash_id_bin.h) add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/rp2350.rom.h COMMAND ${CMAKE_COMMAND} -D BINARY_FILE=${CMAKE_CURRENT_LIST_DIR}/bootrom.end.bin @@ -160,6 +186,14 @@ add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/xip_ram_perms_elf.h DEPENDS xip_ram_perms COMMENT "Configuring xip_ram_perms_elf.h" VERBATIM) +add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/flash_id_bin.h + COMMAND ${CMAKE_COMMAND} + -D BINARY_FILE=${FLASH_ID_BIN} + -D OUTPUT_NAME=flash_id_bin + -P ${CMAKE_CURRENT_LIST_DIR}/cmake/binh.cmake + DEPENDS flash_id + COMMENT "Configuring flash_id_bin.h" + VERBATIM) add_subdirectory(errors) diff --git a/picoboot_connection/picoboot_connection.c b/picoboot_connection/picoboot_connection.c index 7a519d1..8b08a0a 100644 --- a/picoboot_connection/picoboot_connection.c +++ b/picoboot_connection/picoboot_connection.c @@ -579,36 +579,71 @@ static const uint8_t picoboot_peek_cmd[] = { }; #define PICOBOOT_PEEK_CMD_PROG_SIZE (size_t)(12 + 4) -// todo - compile this - currently taken from github PR #86 -static const size_t picoboot_flash_id_cmd_len = 152; -static const uint8_t picoboot_flash_id_cmd[] = { - // void flash_get_unique_id(void) - 0x02, 0xa0, 0x06, 0xa1, 0x00, 0x4a, 0x11, 0xe0, - // int buflen - 0x0d, 0x00, 0x00, 0x00, - // char txbuf[13] - 0x4b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // char rxbuf[13] - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // void flash_do_cmd(txbuf, rxbuf, buflen) - 0x80, 0x23, 0xf0, 0xb5, 0x17, 0x4e, 0x9b, 0x00, - 0x34, 0x68, 0x63, 0x40, 0xc0, 0x24, 0xa4, 0x00, - 0x23, 0x40, 0x15, 0x4c, 0x23, 0x60, 0xc0, 0x24, - 0x13, 0x00, 0x64, 0x05, 0x17, 0x00, 0x1f, 0x43, - 0x06, 0xd1, 0xc0, 0x23, 0x32, 0x68, 0x9b, 0x00, - 0x93, 0x43, 0x0f, 0x4a, 0x13, 0x60, 0xf0, 0xbd, - 0x08, 0x25, 0xa7, 0x6a, 0x3d, 0x40, 0xac, 0x46, - 0x02, 0x25, 0x2f, 0x42, 0x08, 0xd0, 0x00, 0x2a, - 0x06, 0xd0, 0x9f, 0x1a, 0x0d, 0x2f, 0x03, 0xd8, - 0x07, 0x78, 0x01, 0x3a, 0x27, 0x66, 0x01, 0x30, - 0x65, 0x46, 0x00, 0x2d, 0xe2, 0xd0, 0x00, 0x2b, - 0xe0, 0xd0, 0x27, 0x6e, 0x01, 0x3b, 0x0f, 0x70, - 0x01, 0x31, 0xdb, 0xe7, 0x0c, 0x80, 0x01, 0x40, - 0x0c, 0x90, 0x01, 0x40, -}; -#define PICOBOOT_FLASH_ID_CMD_PROG_SIZE (size_t)(152) +// 00000000 : +// 0: a002 add r0, pc, #8 @ (adr r0, c ) +// 2: a106 add r1, pc, #24 @ (adr r1, 1c ) +// 4: 4a00 ldr r2, [pc, #0] @ (8 ) +// 6: e011 b.n 2c +// 8: 0000000d .word 0x0000000d +// c: 0000004b .word 0x0000004b +// ... +// +// 0000002c : +// 2c: 2380 movs r3, #128 @ 0x80 +// 2e: b5f0 push {r4, r5, r6, r7, lr} +// 30: 4e17 ldr r6, [pc, #92] @ (90 ) +// 32: 009b lsls r3, r3, #2 +// 34: 6834 ldr r4, [r6, #0] +// 36: 4063 eors r3, r4 +// 38: 24c0 movs r4, #192 @ 0xc0 +// 3a: 00a4 lsls r4, r4, #2 +// 3c: 4023 ands r3, r4 +// 3e: 4c15 ldr r4, [pc, #84] @ (94 ) +// 40: 6023 str r3, [r4, #0] +// 42: 24c0 movs r4, #192 @ 0xc0 +// 44: 0013 movs r3, r2 +// 46: 0564 lsls r4, r4, #21 +// 48: 0017 movs r7, r2 +// 4a: 431f orrs r7, r3 +// 4c: d106 bne.n 5c +// 4e: 23c0 movs r3, #192 @ 0xc0 +// 50: 6832 ldr r2, [r6, #0] +// 52: 009b lsls r3, r3, #2 +// 54: 4393 bics r3, r2 +// 56: 4a0f ldr r2, [pc, #60] @ (94 ) +// 58: 6013 str r3, [r2, #0] +// 5a: bdf0 pop {r4, r5, r6, r7, pc} +// 5c: 2508 movs r5, #8 +// 5e: 6aa7 ldr r7, [r4, #40] @ 0x28 +// 60: 403d ands r5, r7 +// 62: 46ac mov ip, r5 +// 64: 2502 movs r5, #2 +// 66: 422f tst r7, r5 +// 68: d008 beq.n 7c +// 6a: 2a00 cmp r2, #0 +// 6c: d006 beq.n 7c +// 6e: 1a9f subs r7, r3, r2 +// 70: 2f0d cmp r7, #13 +// 72: d803 bhi.n 7c +// 74: 7807 ldrb r7, [r0, #0] +// 76: 3a01 subs r2, #1 +// 78: 6627 str r7, [r4, #96] @ 0x60 +// 7a: 3001 adds r0, #1 +// 7c: 4665 mov r5, ip +// 7e: 2d00 cmp r5, #0 +// 80: d0e2 beq.n 48 +// 82: 2b00 cmp r3, #0 +// 84: d0e0 beq.n 48 +// 86: 6e27 ldr r7, [r4, #96] @ 0x60 +// 88: 3b01 subs r3, #1 +// 8a: 700f strb r7, [r1, #0] +// 8c: 3101 adds r1, #1 +// 8e: e7db b.n 48 +// 90: 4001800c .word 0x4001800c +// 94: 4001900c .word 0x4001900c + +#include "flash_id_bin.h" +#define PICOBOOT_FLASH_ID_CMD_PROG_SIZE (const size_t)(152) // TODO better place for this e.g. the USB DPRAM location the controller has already put it in #define PEEK_POKE_CODE_LOC 0x20000000u @@ -647,10 +682,11 @@ int picoboot_peek(libusb_device_handle *usb_device, uint32_t addr, uint32_t *dat int picoboot_flash_id(libusb_device_handle *usb_device, uint64_t *data) { picoboot_exclusive_access(usb_device, 1); + assert(PICOBOOT_FLASH_ID_CMD_PROG_SIZE == flash_id_bin_SIZE); uint8_t prog[PICOBOOT_FLASH_ID_CMD_PROG_SIZE]; uint64_t id; output("GET FLASH ID\n"); - memcpy(prog, picoboot_flash_id_cmd, picoboot_flash_id_cmd_len); + memcpy(prog, flash_id_bin, flash_id_bin_SIZE); // ensure XIP is exited before executing int ret = picoboot_exit_xip(usb_device); @@ -676,4 +712,4 @@ flash_id_return: picoboot_exclusive_access(usb_device, 0); return ret; } -#endif \ No newline at end of file +#endif diff --git a/picoboot_flash_id/CMakeLists.txt b/picoboot_flash_id/CMakeLists.txt new file mode 100644 index 0000000..9110f8d --- /dev/null +++ b/picoboot_flash_id/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.12) + +if (NOT USE_PRECOMPILED) + # default build type + set(CMAKE_BUILD_TYPE "MinSizeRel" CACHE STRING "build type") + + include(${PICO_SDK_PATH}/external/pico_sdk_import.cmake) + project(flash_id C CXX ASM) + pico_sdk_init() + + add_executable(flash_id flash_id.c) + target_link_libraries(flash_id PRIVATE + hardware_regs hardware_structs hardware_flash_headers + ) + target_link_options(flash_id PRIVATE -nostartfiles -nodefaultlibs -Ttext=0) + pico_add_bin_output(flash_id) + pico_add_dis_output(flash_id) +else() + project(flash_id C CXX ASM) + message("Using precompiled flash_id.bin") + configure_file(${CMAKE_CURRENT_LIST_DIR}/flash_id.bin ${CMAKE_CURRENT_BINARY_DIR}/flash_id.bin COPYONLY) + # Use manually specified variables + set(NULL ${CMAKE_MAKE_PROGRAM}) + set(NULL ${PICO_SDK_PATH}) +endif() diff --git a/picoboot_flash_id/flash_id.bin b/picoboot_flash_id/flash_id.bin new file mode 100755 index 0000000000000000000000000000000000000000..4143381f6c9da072bdcc80e1f8eb11620aee73c1 GIT binary patch literal 152 zcmZQtz_yUVOYi|N0|SFM5aR<4${)6h`^{!B$w+oMpt6KP*+JAtIpKhcFhdHfID@=1 z+rio[1].ctrl, + field_val << IO_QSPI_GPIO_QSPI_SS_CTRL_OUTOVER_LSB, + IO_QSPI_GPIO_QSPI_SS_CTRL_OUTOVER_BITS + ); +} + +void flash_do_cmd(const uint8_t * txbuf, uint8_t *rxbuf, size_t count) { + flash_cs_force(0); + size_t tx_remaining = count; + size_t rx_remaining = count; + // We may be interrupted -- don't want FIFO to overflow if we're distracted. + const size_t max_in_flight = 16 - 2; + while (tx_remaining || rx_remaining) { + uint32_t flags = ssi_hw->sr; + bool can_put = !!(flags & SSI_SR_TFNF_BITS); + bool can_get = !!(flags & SSI_SR_RFNE_BITS); + if (can_put && tx_remaining && rx_remaining - tx_remaining < max_in_flight) { + ssi_hw->dr0 = *txbuf++; + --tx_remaining; + } + if (can_get && rx_remaining) { + *rxbuf++ = (uint8_t)ssi_hw->dr0; + --rx_remaining; + } + } + flash_cs_force(1); +} diff --git a/xip_ram_perms/CMakeLists.txt b/xip_ram_perms/CMakeLists.txt index 01975dc..524921f 100644 --- a/xip_ram_perms/CMakeLists.txt +++ b/xip_ram_perms/CMakeLists.txt @@ -4,7 +4,7 @@ if (NOT USE_PRECOMPILED) set(PICO_PLATFORM rp2350-arm-s) # Pull in SDK (must be before project) - include(pico_sdk_import.cmake) + include(${PICO_SDK_PATH}/external/pico_sdk_import.cmake) project(xip_ram_perms C CXX ASM) set(CMAKE_C_STANDARD 11) diff --git a/xip_ram_perms/pico_sdk_import.cmake b/xip_ram_perms/pico_sdk_import.cmake deleted file mode 100644 index 5613261..0000000 --- a/xip_ram_perms/pico_sdk_import.cmake +++ /dev/null @@ -1,62 +0,0 @@ -# This is a copy of /external/pico_sdk_import.cmake - -# This can be dropped into an external project to help locate this SDK -# It should be include()ed prior to project() - -if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) - set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) - message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") -endif () - -if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) - set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) - message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") -endif () - -if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) - set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) - message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") -endif () - -set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") -set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") -set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") - -if (NOT PICO_SDK_PATH) - if (PICO_SDK_FETCH_FROM_GIT) - include(FetchContent) - set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) - if (PICO_SDK_FETCH_FROM_GIT_PATH) - get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") - endif () - FetchContent_Declare( - pico_sdk - GIT_REPOSITORY git@asic-git.pitowers.org:amethyst/pico-sdk.git - GIT_TAG use-picotool - ) - if (NOT pico_sdk) - message("Downloading Raspberry Pi Pico SDK") - FetchContent_Populate(pico_sdk) - set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) - endif () - set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) - else () - message(FATAL_ERROR - "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." - ) - endif () -endif () - -get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") -if (NOT EXISTS ${PICO_SDK_PATH}) - message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") -endif () - -set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) -if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) - message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") -endif () - -set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) - -include(${PICO_SDK_INIT_CMAKE_FILE}) From 6b63c0636b59abdb9b83e3ff8b4234fdea2e5242 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Tue, 13 Aug 2024 13:57:56 +0100 Subject: [PATCH 05/67] Only warn about no block loop if binary info found Improves 3c45783, by removing warning with empty flash --- main.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/main.cpp b/main.cpp index 0bc999b..2b348bd 100644 --- a/main.cpp +++ b/main.cpp @@ -2836,7 +2836,8 @@ void info_guts(memory_access &raw_access, void *con) { select_group(device_info); binary_info_header hdr; try { - if (find_binary_info(raw_access, hdr)) { + bool has_binary_info = find_binary_info(raw_access, hdr); + if (has_binary_info) { auto access = remapped_memory_access(raw_access, hdr.reverse_copy_mapping); auto visitor = bi_visitor{}; map output; @@ -3093,7 +3094,7 @@ void info_guts(memory_access &raw_access, void *con) { if (sig_verified != none) { info_pair("signature", sig_verified == passed ? "verified" : "incorrect"); } - } else if (get_model(raw_access) == rp2350) { + } else if (has_binary_info && get_model(raw_access) == rp2350) { fos << "WARNING: Binary on RP2350 device does not contain a block loop - this binary will not boot\n"; } } catch (std::invalid_argument &e) { From 468ba2f5c467c0781a17f6719732ded910a7dcf3 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Tue, 13 Aug 2024 15:05:24 +0100 Subject: [PATCH 06/67] Errata E9 was changed to E10 --- README.md | 4 ++-- elf2uf2/elf2uf2.cpp | 2 +- main.cpp | 16 ++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 29231b9..b397203 100644 --- a/README.md +++ b/README.md @@ -709,7 +709,7 @@ OPTIONS: Don't hash the partition table --singleton Singleton partition table - Errata RP2350-E9 Fix + Errata RP2350-E10 Fix --abs-block Enforce support for an absolute block @@ -756,7 +756,7 @@ OPTIONS: UF2 Family options family id for UF2 - Errata RP2350-E9 Fix + Errata RP2350-E10 Fix --abs-block Add an absolute block diff --git a/elf2uf2/elf2uf2.cpp b/elf2uf2/elf2uf2.cpp index 5a0e495..c2464de 100644 --- a/elf2uf2/elf2uf2.cpp +++ b/elf2uf2/elf2uf2.cpp @@ -154,7 +154,7 @@ bool check_abs_block(uf2_block block) { } int pages2uf2(std::map>& pages, std::shared_ptr in, std::shared_ptr out, uint32_t family_id, uint32_t abs_block_loc=0) { - // RP2350-E9: add absolute block to start, targeting end of flash by default + // RP2350-E10: add absolute block to start, targeting end of flash by default if (family_id != ABSOLUTE_FAMILY_ID && family_id != RP2040_FAMILY_ID && abs_block_loc) { uf2_block block = gen_abs_block(abs_block_loc); out->write((char*)&block, sizeof(uf2_block)); diff --git a/main.cpp b/main.cpp index 2b348bd..9fbb7fa 100644 --- a/main.cpp +++ b/main.cpp @@ -857,7 +857,7 @@ struct partition_create_command : public cmd { + ( option("--abs-block").set(settings.uf2.abs_block) % "Enforce support for 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-E9 Fix" + ).force_expand_help(true).min(0) % "Errata RP2350-E10 Fix" #endif ); } @@ -1141,7 +1141,7 @@ struct uf2_convert_command : public cmd { + ( 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-E9 Fix" + ).force_expand_help(true).min(0) % "Errata RP2350-E10 Fix" #endif ); } @@ -2611,7 +2611,7 @@ void build_rmap_uf2(std::shared_ptrfile, range_map& rmap) #if SUPPORT_A2 // ignore the absolute block if (check_abs_block(block)) { - DEBUG_LOG("Ignoring RP2350-E9 absolute block\n"); + DEBUG_LOG("Ignoring RP2350-E10 absolute block\n"); } else { rmap.insert(range(block.target_addr, block.target_addr + PAGE_SIZE), pos + offsetof(uf2_block, data[0])); } @@ -3527,7 +3527,7 @@ uint32_t get_family_id(uint8_t file_idx) { #if SUPPORT_A2 // ignore the absolute block if (check_abs_block(block)) { - DEBUG_LOG("Ignoring RP2350-E9 absolute block\n"); + DEBUG_LOG("Ignoring RP2350-E10 absolute block\n"); file->read((char*)&block, sizeof(block)); } #endif @@ -5406,7 +5406,7 @@ bool partition_create_command::execute(device_map &devices) { #if SUPPORT_A2 if (!(unpartitioned_flags & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_ABSOLUTE_BITS)) { - fail(ERROR_INCOMPATIBLE, "Unpartitioned space must accept the absolute family, for the RP2350-E9 fix to work"); + fail(ERROR_INCOMPATIBLE, "Unpartitioned space must accept the absolute family, for the RP2350-E10 fix to work"); } #endif @@ -5430,7 +5430,7 @@ bool partition_create_command::execute(device_map &devices) { cur_pos = start + size; #if SUPPORT_A2 if (start <= (settings.uf2.abs_block_loc - FLASH_START)/0x1000 && start + size > (settings.uf2.abs_block_loc - FLASH_START)/0x1000) { - fail(ERROR_INCOMPATIBLE, "The address %lx cannot be in a partition for the RP2350-E9 fix to work", settings.uf2.abs_block_loc); + fail(ERROR_INCOMPATIBLE, "The address %lx cannot be in a partition for the RP2350-E10 fix to work", settings.uf2.abs_block_loc); } #endif new_p.first_sector = start; @@ -5607,9 +5607,9 @@ bool uf2_convert_command::execute(device_map &devices) { auto in = get_file(ios::in|ios::binary); auto out = get_file_idx(ios::out|ios::binary, 1); #if SUPPORT_A2 - // RP2350-E9 : add absolute block + // RP2350-E10 : add absolute block if (settings.uf2.abs_block) { - fos << "RP2350-E9: Adding absolute block to UF2 targeting " << hex_string(settings.uf2.abs_block_loc) << "\n"; + fos << "RP2350-E10: Adding absolute block to UF2 targeting " << hex_string(settings.uf2.abs_block_loc) << "\n"; } else { settings.uf2.abs_block_loc = 0; } From deecc43c5f3d9871dec320de0f0a8426c0588b31 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Tue, 13 Aug 2024 15:42:38 +0100 Subject: [PATCH 07/67] Errata E10 absolute block is only required for flash UF2s --- elf2uf2/elf2uf2.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/elf2uf2/elf2uf2.cpp b/elf2uf2/elf2uf2.cpp index c2464de..edc5ee5 100644 --- a/elf2uf2/elf2uf2.cpp +++ b/elf2uf2/elf2uf2.cpp @@ -154,12 +154,16 @@ bool check_abs_block(uf2_block block) { } int pages2uf2(std::map>& pages, std::shared_ptr in, std::shared_ptr out, uint32_t family_id, uint32_t abs_block_loc=0) { - // RP2350-E10: add absolute block to start, targeting end of flash by default + // RP2350-E10: add absolute block to start of flash UF2s, targeting end of flash by default if (family_id != ABSOLUTE_FAMILY_ID && family_id != RP2040_FAMILY_ID && abs_block_loc) { - uf2_block block = gen_abs_block(abs_block_loc); - out->write((char*)&block, sizeof(uf2_block)); - if (out->fail()) { - fail_write_error(); + uint32_t base_addr = pages.begin()->first; + address_ranges flash_range = rp2350_address_ranges_flash; + if (is_address_initialized(flash_range, base_addr)) { + uf2_block block = gen_abs_block(abs_block_loc); + out->write((char*)&block, sizeof(uf2_block)); + if (out->fail()) { + fail_write_error(); + } } } uf2_block block; From 4ea7e634d48b98d7bd610e59c0da3d46175f2918 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Wed, 14 Aug 2024 10:54:00 +0100 Subject: [PATCH 08/67] Support UF2s with arbitrary family IDs Don't check fo a valid family ID when loading a UF2 file --- main.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/main.cpp b/main.cpp index 9fbb7fa..ec24efc 100644 --- a/main.cpp +++ b/main.cpp @@ -2589,10 +2589,6 @@ void build_rmap_elf(std::shared_ptrfile, range_map& rmap) } } -bool is_valid_family_id(uint32_t family_id) { - return family_id >= RP2040_FAMILY_ID && family_id <= FAMILY_ID_MAX; -} - void build_rmap_uf2(std::shared_ptrfile, range_map& rmap) { file->seekg(0, ios::beg); uf2_block block; @@ -2606,7 +2602,6 @@ void build_rmap_uf2(std::shared_ptrfile, range_map& rmap) 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 && - is_valid_family_id(block.file_size) && !(block.flags & UF2_FLAG_NOT_MAIN_FLASH) && block.payload_size == PAGE_SIZE) { #if SUPPORT_A2 // ignore the absolute block From e1bfb364971551e0bbe544dc59471e258308343d Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Wed, 14 Aug 2024 11:01:19 +0100 Subject: [PATCH 09/67] Support hex partition IDs in JSON --- main.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/main.cpp b/main.cpp index ec24efc..6011617 100644 --- a/main.cpp +++ b/main.cpp @@ -1683,11 +1683,13 @@ static model_t get_model(memory_access &raw_access) { } } -bool get_int(const std::string& s, int& out) { +template +bool get_int(const std::string& s, T& out) { return integer::parse_string(s, out).empty(); } -bool get_json_int(json value, int& out) { +template +bool get_json_int(json value, T& out) { if (value.is_string()) { string str = value; if (str.back() == 'k' || str.back() == 'K') { @@ -5410,7 +5412,7 @@ bool partition_create_command::execute(device_map &devices) { for (auto p : partitions) { partition_table_item::partition new_p; uint32_t start = cur_pos; - if (p.contains("start")) get_json_int(p["start"], (int&)start); + if (p.contains("start")) get_json_int(p["start"], start); int size; get_json_int(p["size"], size); if (start >= 4096 || size >= 4096) { @@ -5467,7 +5469,7 @@ bool partition_create_command::execute(device_map &devices) { new_p.flags |= (link_value << PICOBIN_PARTITION_FLAGS_LINK_VALUE_LSB) & PICOBIN_PARTITION_FLAGS_LINK_VALUE_BITS; } if (p.contains("name")) { new_p.name = p["name"]; new_p.flags |= PICOBIN_PARTITION_FLAGS_HAS_NAME_BITS; } - if (p.contains("id")) { new_p.id = p["id"]; new_p.flags |= PICOBIN_PARTITION_FLAGS_HAS_ID_BITS; } + if (p.contains("id")) { get_json_int(p["id"], new_p.id); new_p.flags |= PICOBIN_PARTITION_FLAGS_HAS_ID_BITS; } if(p.contains("no_reboot_on_uf2_download")) new_p.flags |= PICOBIN_PARTITION_FLAGS_UF2_DOWNLOAD_NO_REBOOT_BITS; if(p.contains("ab_non_bootable_owner_affinity")) new_p.flags |= PICOBIN_PARTITION_FLAGS_UF2_DOWNLOAD_AB_NON_BOOTABLE_OWNER_AFFINITY; From ef03bd2d90c4987f59bec09ba03d8dbcd881c612 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Wed, 14 Aug 2024 11:56:40 +0100 Subject: [PATCH 10/67] Target abs-block at CS1 by default This still works around E10, but without actually writing to CS0 flash --- main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.cpp b/main.cpp index 6011617..b2fc0e3 100644 --- a/main.cpp +++ b/main.cpp @@ -471,7 +471,7 @@ struct _settings { struct { bool abs_block = false; #if SUPPORT_A2 - uint32_t abs_block_loc = 0x11000000 - UF2_PAGE_SIZE; + uint32_t abs_block_loc = 0x12000000 - UF2_PAGE_SIZE; #else uint32_t abs_block_loc = 0; #endif From f98850637b04269c37723d6cf156f2779aa507d9 Mon Sep 17 00:00:00 2001 From: Darwin Date: Wed, 14 Aug 2024 07:09:15 -0400 Subject: [PATCH 11/67] Clarify picotool builds when libusb is not present. (#109) Add note to README about picotool builds without libusb missing all the usb interface commands. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b397203..e793111 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ Use your favorite package tool to install dependencies. For example, on Ubuntu: sudo apt install build-essential pkg-config libusb-1.0-0-dev cmake ``` +> If libusb-1.0-0-dev is not installed, picotool still builds, but it omits all options that deal with managing a pico via USB (load, save, verify, reboot). Builds that do not include USB support can be recognized because these commands also do not appear in the help command. The build output message 'libUSB is not found - no USB support will be built' also appears in the build logs. + Then simply build like a normal CMake project: ```console From 29c54310920060b12dd3998a41361d4458048bbd Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Wed, 14 Aug 2024 17:47:43 +0100 Subject: [PATCH 12/67] Add picotool erase command Defaults to erasing all of flash, with options for erasing a range or a partition --- README.md | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- main.cpp | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e793111..7acb0dd 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Use your favorite package tool to install dependencies. For example, on Ubuntu: sudo apt install build-essential pkg-config libusb-1.0-0-dev cmake ``` -> If libusb-1.0-0-dev is not installed, picotool still builds, but it omits all options that deal with managing a pico via USB (load, save, verify, reboot). Builds that do not include USB support can be recognized because these commands also do not appear in the help command. The build output message 'libUSB is not found - no USB support will be built' also appears in the build logs. +> If libusb-1.0-0-dev is not installed, picotool still builds, but it omits all options that deal with managing a pico via USB (load, save, erase, verify, reboot). Builds that do not include USB support can be recognized because these commands also do not appear in the help command. The build output message 'libUSB is not found - no USB support will be built' also appears in the build logs. Then simply build like a normal CMake project: @@ -118,6 +118,9 @@ SYNOPSIS: picotool save [-p] [device-selection] picotool save -a [device-selection] picotool save -r [device-selection] + picotool erase [-a] [device-selection] + picotool erase [-p ] [device-selection] + picotool erase -r [device-selection] picotool verify [device-selection] picotool reboot [-a] [-u] [-g ] [-c ] [device-selection] picotool otp list|get|set|load|dump|permissions|white-label @@ -136,6 +139,7 @@ COMMANDS: seal Add final metadata to a binary, optionally including a hash and/or signature. link Link multiple binaries into one block loop. save Save the program / memory stored in flash on the device to a file. + erase Erase the program / memory stored in flash on the device. verify Check that the device contents match those in the file. reboot Reboot the device otp Commands related to the RP2350 OTP (One-Time-Programmable) Memory @@ -497,6 +501,95 @@ name: lcd_1602_i2c web site: https://github.com/raspberrypi/pico-examples/tree/HEAD/i2c/lcd_1602_i2c ``` +## erase + +`erase` allows you to erase all of flash, a partition of flash, or an explicit range of flash on the device. +It defaults to erasing all of flash. + +```text +$ picotool help erase +ERASE: + Erase the program / memory stored in flash on the device. + +SYNOPSIS: + picotool erase [-a] [device-selection] + picotool erase [-p ] [device-selection] + picotool erase -r [device-selection] + +OPTIONS: + Selection of data to erase + -a, --all + Erase all of flash memory. This is the default + -p, --partition + Erase a partition + + Partition number to erase + -r, --range + Erase a range of memory. Note that erases must be 4096 byte-aligned, so the range is expanded accordingly + + The lower address bound in hex + + The upper address bound in hex + Source device selection + --bus + Filter devices by USB bus number + --address + Filter devices by USB device address + --vid + Filter by vendor id + --pid + Filter by product id + --ser + Filter by serial number + -f, --force + Force a device not in BOOTSEL mode but running compatible code to reset so the command can be executed. After executing the + command (unless the command itself is a 'reboot') the device will be rebooted back to application mode + -F, --force-no-reboot + Force a device not in BOOTSEL mode but running compatible code to reset so the command can be executed. After executing the + command (unless the command itself is a 'reboot') the device will be left connected and accessible to picotool, but without the + RPI-RP2 drive mounted +``` + +e.g. first looking at what is on the device... + +```text +$ picotool info +Partition 0 + Program Information + none + +Partition 1 + Program Information + name: blink + web site: https://github.com/raspberrypi/pico-examples/tree/HEAD/blink + features: UART stdin / stdout + binary start: 0x10000000 + binary end: 0x1000a934 + target chip: RP2350 + image type: ARM Secure +``` + +... then erase partition 1 ... +```text +$ picotool erase -p 1 +Erasing partition 1: + 0007f000->000fc000 +Erasing: [==============================] 100% +Erased 512000 bytes +``` + +... and looking at the device again: +```text +$ picotool info +Partition 0 + Program Information + none + +Partition 1 + Program Information + none +``` + ## seal `seal` allows you to sign and/or hash a binary to run on RP2350. diff --git a/main.cpp b/main.cpp index b2fc0e3..a0e6a53 100644 --- a/main.cpp +++ b/main.cpp @@ -707,6 +707,34 @@ struct load_command : public cmd { return "Load the program / memory range stored in a file onto the device."; } }; + +struct erase_command : public cmd { + erase_command() : cmd("erase") {} + bool execute(device_map &devices) override; + + group get_cli() override { + return ( + ( + option('a', "--all") % "Erase all of flash memory. This is the default" | + ( + option('p', "--partition") % "Erase a partition" & + integer("partition").set(settings.load.partition) % "Partition number to erase" + ).min(0).doc_non_optional(true) | + ( + option('r', "--range").set(settings.range_set) % "Erase a range of memory. Note that erases must be 4096 byte-aligned, so the range is expanded accordingly" & + hex("from").set(settings.from) % "The lower address bound in hex" & + hex("to").set(settings.to) % "The upper address bound in hex" + ).min(0).doc_non_optional(true) + ).min(0).doc_non_optional(true).no_match_beats_error(false) % "Selection of data to erase" + + ( // note this parenthesis seems to help with error messages for say erase --foo + device_selection % "Source device selection" + ) + ); + } + string get_doc() const override { + return "Erase the program / memory stored in flash on the device."; + } +}; #endif #if HAS_MBEDTLS @@ -1289,6 +1317,7 @@ vector> commands { std::shared_ptr(new link_command()), #if HAS_LIBUSB std::shared_ptr(new save_command()), + std::shared_ptr(new erase_command()), std::shared_ptr(new verify_command()), reboot_cmd, #endif @@ -3895,6 +3924,64 @@ bool save_command::execute(device_map &devices) { } return false; } + +bool erase_command::execute(device_map &devices) { + auto con = get_single_bootsel_device_connection(devices); + picoboot_memory_access raw_access(con); + + uint32_t end = 0; + uint32_t binary_end = 0; + binary_info_header hdr; + uint32_t start = FLASH_START; + if (settings.load.partition >= 0) { + auto partitions = get_partitions(con); + if (!partitions) { + fail(ERROR_NOT_POSSIBLE, "There is no partition table on the device"); + } + if (settings.load.partition >= partitions->size()) { + fail(ERROR_NOT_POSSIBLE, "There are only %d partitions on the device", partitions->size()); + } + start = std::get<0>((*partitions)[settings.load.partition]); + end = std::get<1>((*partitions)[settings.load.partition]); + printf("Erasing partition %d:\n", settings.load.partition); + printf(" %08x->%08x\n", start, end); + start += FLASH_START; + end += FLASH_START; + if (end <= start) { + fail(ERROR_ARGS, "Erase range is invalid/empty"); + } + } else if (settings.range_set) { + start = settings.from & ~(FLASH_SECTOR_ERASE_SIZE - 1); + end = (settings.to + (FLASH_SECTOR_ERASE_SIZE - 1)) & ~(FLASH_SECTOR_ERASE_SIZE - 1); + if (end <= start) { + fail(ERROR_ARGS, "Erase range is invalid/empty"); + } + } else { + end = FLASH_START + guess_flash_size(raw_access); + if (end <= FLASH_START) { + fail(ERROR_NOT_POSSIBLE, "Cannot determine the flash size, so cannot erase the entirety of flash, try --range."); + } + } + + model_t model = get_model(raw_access); + enum memory_type t1 = get_memory_type(start , model); + enum memory_type t2 = get_memory_type(end, model); + if (t1 != flash || t1 != t2) { + fail(ERROR_NOT_POSSIBLE, "Erase range not all in flash"); + } + uint32_t size = end - start; + + { + progress_bar bar("Erasing: "); + for (uint32_t addr = start; addr < end; addr += FLASH_SECTOR_ERASE_SIZE) { + bar.progress(addr-start, end-start); + con.flash_erase(addr, FLASH_SECTOR_ERASE_SIZE); + } + bar.progress(100); + } + std::cout << "Erased " << size << " bytes\n"; + return false; +} #endif #if HAS_LIBUSB From 408691022639df1f3c5b5bc216a986ebf03efab0 Mon Sep 17 00:00:00 2001 From: will-v-pi <108662275+will-v-pi@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:42:48 +0100 Subject: [PATCH 13/67] Add note to README about picotool_DIR environment variable Also add link to the find_package documentation for more details --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7acb0dd..37206a6 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ Alternatively you can install in a custom path via: cmake -DCMAKE_INSTALL_PREFIX=$MY_INSTALL_DIR -DPICOTOOL_FLAT_INSTALL=1 .. ``` -In order for the SDK to find `picotool` in this custom path, you will need to set the `picotool_DIR` variable in your project, either by passing to `-Dpicotool_DIR=$MY_INSTALL_DIR/picotool` to your SDK `cmake` command, or by adding +In order for the SDK to find `picotool` in this custom path, you will need to set the `picotool_DIR` variable in your project. This can be achieved either by setting the `picotool_DIR` environment variable to `$MY_INSTALL_DIR/picotool`, by passing `-Dpicotool_DIR=$MY_INSTALL_DIR/picotool` to your SDK `cmake` command, or by adding ```CMake set(picotool_DIR $MY_INSTALL_DIR/picotool) @@ -95,6 +95,8 @@ set(picotool_DIR $MY_INSTALL_DIR/picotool) to your CMakeLists.txt file. +> See the [find_package documentation](https://cmake.org/cmake/help/latest/command/find_package.html#config-mode-search-procedure) for more details - although note that SDK builds will not have the `CMAKE_SYSTEM_XXX` variables set, as the the Platform files do not set up these paths + ## Overview `picotool` is a tool for working with RP2040/RP2350 binaries, and interacting with RP2040/RP2350 devices when they are in BOOTSEL mode. (As of version 1.1 of `picotool` it is also possible to interact with devices that are not in BOOTSEL mode, but are using USB stdio support from the Raspberry Pi Pico SDK by using the `-f` argument of `picotool`). From 9bb1ab41efad9a7e66c5594bb6f64811052899c6 Mon Sep 17 00:00:00 2001 From: Wyatt Hepler <255@users.noreply.github.com> Date: Thu, 15 Aug 2024 03:01:26 -0700 Subject: [PATCH 14/67] Accept ELFs with an OS/ABI of ELFOSABI_GNU (#111) RP2 ELF files typically have their EI_OSABI field set to ELFOSABI_NONE. However, when Clang uses GNU extensions, it sets EI_OSABI to ELFOSABI_GNU. The binary is still fully compatible with picotool; ELFOSABI_GNU just indicates that GNU extensions are used in the ELF. --- elf/elf_file.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/elf/elf_file.cpp b/elf/elf_file.cpp index 436e8fe..08988a1 100644 --- a/elf/elf_file.cpp +++ b/elf/elf_file.cpp @@ -150,7 +150,10 @@ int rp_check_elf_header(const elf32_header &eh) { if (eh.common.machine != EM_ARM && eh.common.machine != EM_RISCV) { fail(ERROR_FORMAT, "Not an Arm or RISC-V executable"); } - if (eh.common.abi != 0) { + // Accept either ELFOSABI_NONE or ELFOSABI_GNU for EI_OSABI. Compilers may + // set the OS/ABI field to ELFOSABI_GNU when they use GNU features, such as + // the SHF_GNU_RETAIN section flag, but the binary is still compatible. + if (eh.common.abi != 0 /* NONE */ && eh.common.abi != 3 /* GNU */) { fail(ERROR_INCOMPATIBLE, "Unrecognized ABI"); } // todo amy not sure if this should be expected or not - we have HARD float in clang only for now From 29f5b0bec7d4f420c7a7cfaa8410cb861d04f0a9 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Thu, 15 Aug 2024 11:19:21 +0100 Subject: [PATCH 15/67] Add signing of UF2s Also fix picotool info bug for files with unknown families --- main.cpp | 45 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/main.cpp b/main.cpp index a0e6a53..0756cfc 100644 --- a/main.cpp +++ b/main.cpp @@ -1627,6 +1627,7 @@ SAFE_MAPPING(binary_info_named_group_t); #define BOOTROM_MAGIC_RP2040 0x01754d #define BOOTROM_MAGIC_RP2350 0x02754d +#define BOOTROM_MAGIC_UNKNOWN 0x000000 #define BOOTROM_MAGIC_ADDR 0x00000010 static inline uint32_t rom_table_code(char c1, char c2) { @@ -1956,7 +1957,7 @@ struct iostream_memory_access : public memory_access { } void read(uint32_t address, uint8_t *buffer, uint32_t size, bool zero_fill) override { - if (model != unknown && address == BOOTROM_MAGIC_ADDR && size == 4) { + if (address == BOOTROM_MAGIC_ADDR && size == 4) { // return the memory model if (model == rp2040) { *(uint32_t*)buffer = BOOTROM_MAGIC_RP2040; @@ -1964,6 +1965,9 @@ struct iostream_memory_access : public memory_access { } else if (model == rp2350) { *(uint32_t*)buffer = BOOTROM_MAGIC_RP2350; return; + } else { + *(uint32_t*)buffer = BOOTROM_MAGIC_UNKNOWN; + return; } } while (size) { @@ -2635,9 +2639,10 @@ void build_rmap_uf2(std::shared_ptrfile, range_map& rmap) if (block.flags & UF2_FLAG_FAMILY_ID_PRESENT && !(block.flags & UF2_FLAG_NOT_MAIN_FLASH) && block.payload_size == PAGE_SIZE) { #if SUPPORT_A2 - // ignore the absolute block + // ignore the absolute block, but save the address if (check_abs_block(block)) { DEBUG_LOG("Ignoring RP2350-E10 absolute block\n"); + settings.uf2.abs_block_loc = block.target_addr; } else { rmap.insert(range(block.target_addr, block.target_addr + PAGE_SIZE), pos + offsetof(uf2_block, data[0])); } @@ -4408,13 +4413,7 @@ void sign_guts_elf(elf_file* elf, private_t private_key, public_t public_key) { ); } -vector sign_guts_bin(iostream_memory_access in, private_t private_key, public_t public_key) { - auto rmap = in.get_rmap(); - auto ranges = rmap.ranges(); - assert(ranges.size() == 1); - auto bin_start = ranges[0].from; - auto bin_size = ranges[0].len(); - +vector sign_guts_bin(iostream_memory_access in, private_t private_key, public_t public_key, uint32_t bin_start, uint32_t bin_size) { vector bin = in.read_vector(bin_start, bin_size, false); std::unique_ptr first_block = find_first_block(bin, bin_start); @@ -4457,12 +4456,15 @@ vector sign_guts_bin(iostream_memory_access in, private_t private_key, bool seal_command::execute(device_map &devices) { bool isElf = false; bool isBin = false; + bool isUf2 = false; if (get_file_type() == filetype::elf) { isElf = true; } else if (get_file_type() == filetype::bin) { isBin = true; + } else if (get_file_type() == filetype::uf2) { + isUf2 = true; } else { - fail(ERROR_ARGS, "Can only sign ELFs or BINs"); + fail(ERROR_ARGS, "Can only sign ELFs, BINs or UF2s"); } if (get_file_type_idx(1) != get_file_type()) { @@ -4519,10 +4521,31 @@ bool seal_command::execute(device_map &devices) { elf->write(out); out->close(); } else if (isBin) { - auto sig_data = sign_guts_bin(get_file_memory_access(0), private_key, public_key); + auto access = get_file_memory_access(0); + auto rmap = access.get_rmap(); + auto ranges = rmap.ranges(); + assert(ranges.size() == 1); + auto bin_start = ranges[0].from; + auto bin_size = ranges[0].len(); + + auto sig_data = sign_guts_bin(access, private_key, public_key, bin_start, bin_size); auto out = get_file_idx(ios::out|ios::binary, 1); out->write((const char *)sig_data.data(), sig_data.size()); out->close(); + } else if (isUf2) { + auto access = get_file_memory_access(0); + auto rmap = access.get_rmap(); + auto ranges = rmap.ranges(); + auto bin_start = ranges.front().from; + auto bin_size = ranges.back().to - bin_start; + auto family_id = get_family_id(0); + + auto sig_data = sign_guts_bin(access, private_key, public_key, bin_start, bin_size); + auto tmp = std::make_shared(); + tmp->write(reinterpret_cast(sig_data.data()), sig_data.size()); + auto out = get_file_idx(ios::out|ios::binary, 1); + bin2uf2(tmp, out, bin_start, family_id, settings.uf2.abs_block_loc); + out->close(); } else { fail(ERROR_ARGS, "Must be ELF or BIN"); } From e66f13bb2f28ab6d7756b4c5e1b1528d835f2327 Mon Sep 17 00:00:00 2001 From: will-v-pi <108662275+will-v-pi@users.noreply.github.com> Date: Fri, 16 Aug 2024 10:30:01 +0100 Subject: [PATCH 16/67] Improve Usage by the SDK section in README.md Reword and add some clarifying notes. Also mention `~/.local` install option on Linux, which fixes raspberrypi/pico-sdk#1827 --- README.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 37206a6..a2c35b3 100644 --- a/README.md +++ b/README.md @@ -75,19 +75,27 @@ make install DESTDIR=/ # optional ## Usage by the Raspberry Pi Pico SDK -The Raspberry Pi Pico SDK ([pico-sdk](https://github.com/raspberrypi/pico-sdk)) version 2.0.0 and above, uses `picotool` to do the ELF->UF2 conversion previously handled by the `elf2uf2` tool in the SDK. `picootol` is also used by the SDK for hashing and/or signing binaries. +The Raspberry Pi Pico SDK ([pico-sdk](https://github.com/raspberrypi/pico-sdk)) version 2.0.0 and above, uses `picotool` to do the ELF->UF2 conversion previously handled by the `elf2uf2` tool in the SDK. `picotool` is also used by the SDK for hashing and/or signing binaries. Whilst the SDK can download picotool on its own per project, if you have multiple projects or build configurations, it is preferable to install a single copy of `picotool` locally. -This can be done most simply with `make install`; the SDK will use this installed version by default. +This can be done most simply with `make install`, using `sudo` if required; the SDK will use this installed version by default. -Alternatively you can install in a custom path via: +> On some Linux systems, the `~/.local` prefix may be used for an install without `sudo`; from your build directory simply run +> ```console +> cmake -DCMAKE_INSTALL_PREFIX=~/.local .. +> make install +> ``` +> This will only work if `~/.local` is included in your `PATH` -``` +Alternatively you can install into any custom folder via: + +```console cmake -DCMAKE_INSTALL_PREFIX=$MY_INSTALL_DIR -DPICOTOOL_FLAT_INSTALL=1 .. +make install ``` -In order for the SDK to find `picotool` in this custom path, you will need to set the `picotool_DIR` variable in your project. This can be achieved either by setting the `picotool_DIR` environment variable to `$MY_INSTALL_DIR/picotool`, by passing `-Dpicotool_DIR=$MY_INSTALL_DIR/picotool` to your SDK `cmake` command, or by adding +In order for the SDK to find `picotool` in this custom folder, you will usually need to set the `picotool_DIR` variable in your project. This can be achieved either by setting the `picotool_DIR` environment variable to `$MY_INSTALL_DIR/picotool`, by passing `-Dpicotool_DIR=$MY_INSTALL_DIR/picotool` to your SDK `cmake` command, or by adding ```CMake set(picotool_DIR $MY_INSTALL_DIR/picotool) @@ -95,7 +103,7 @@ set(picotool_DIR $MY_INSTALL_DIR/picotool) to your CMakeLists.txt file. -> See the [find_package documentation](https://cmake.org/cmake/help/latest/command/find_package.html#config-mode-search-procedure) for more details - although note that SDK builds will not have the `CMAKE_SYSTEM_XXX` variables set, as the the Platform files do not set up these paths +> See the [find_package documentation](https://cmake.org/cmake/help/latest/command/find_package.html#config-mode-search-procedure) for more details ## Overview From 4a523ede0c67104700158cf24b679176034c755e Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Mon, 19 Aug 2024 09:09:35 +0100 Subject: [PATCH 17/67] Fix hashing a pre-existing load_map with clearing --- bintool/bintool.cpp | 36 ++++++++++++++++++++++-------------- bintool/metadata.h | 6 +++++- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/bintool/bintool.cpp b/bintool/bintool.cpp index ab605c3..eb235f8 100644 --- a/bintool/bintool.cpp +++ b/bintool/bintool.cpp @@ -554,21 +554,29 @@ std::vector get_lm_hash_data(elf_file *elf, block *new_block, bool clea for(const auto &entry : load_map->entries) { std::vector data; uint32_t current_storage_address = entry.storage_address; - while (data.size() < entry.size) { - auto seg = elf->segment_from_physical_address(current_storage_address); - if (seg == nullptr) { - fail(ERROR_NOT_POSSIBLE, "The ELF file does not contain the storage address %x", current_storage_address); + if (current_storage_address == 0) { + std::copy( + (uint8_t*)&entry.size, + (uint8_t*)&entry.size + sizeof(entry.size), + std::back_inserter(to_hash)); + DEBUG_LOG("CLEAR %08x + %08x\n", (int)entry.runtime_address, (int)entry.size); + } else { + while (data.size() < entry.size) { + auto seg = elf->segment_from_physical_address(current_storage_address); + if (seg == nullptr) { + fail(ERROR_NOT_POSSIBLE, "The ELF file does not contain the storage address %x", current_storage_address); + } + const auto new_data = elf->content(*seg); + + uint32_t offset = current_storage_address - seg->physical_address(); + + std::copy(new_data.begin()+offset, new_data.end(), std::back_inserter(data)); + current_storage_address += new_data.size(); } - const auto new_data = elf->content(*seg); - - uint32_t offset = current_storage_address - seg->physical_address(); - - std::copy(new_data.begin()+offset, new_data.end(), std::back_inserter(data)); - current_storage_address += new_data.size(); + data.resize(entry.size); + std::copy(data.begin(), data.end(), std::back_inserter(to_hash)); + DEBUG_LOG("HASH %08x + %08x\n", (int)entry.storage_address, (int)data.size()); } - data.resize(entry.size); - std::copy(data.begin(), data.end(), std::back_inserter(to_hash)); - DEBUG_LOG("HASH %08x + %08x\n", (int)entry.storage_address, (int)data.size()); } } @@ -613,7 +621,7 @@ std::vector get_lm_hash_data(std::vector bin, uint32_t storage (uint8_t*)&entry.size, (uint8_t*)&entry.size + sizeof(entry.size), std::back_inserter(to_hash)); - DEBUG_LOG("HASH CLEAR %08x + %08x\n", (int)entry.storage_address, (int)entry.size); + DEBUG_LOG("CLEAR %08x + %08x\n", (int)entry.runtime_address, (int)entry.size); } else { uint32_t rel_addr = entry.storage_address - storage_addr; std::copy( diff --git a/bintool/metadata.h b/bintool/metadata.h index 2a3d6cb..eb45984 100644 --- a/bintool/metadata.h +++ b/bintool/metadata.h @@ -367,7 +367,11 @@ struct load_map_item : public item { if (absolute) { rc.push_back(entry.storage_address); rc.push_back(entry.runtime_address); - rc.push_back(entry.runtime_address + entry.size); + if (entry.storage_address != 0) { + rc.push_back(entry.runtime_address + entry.size); + } else { + rc.push_back(entry.size); + } } else { if (entry.storage_address != 0) { rc.push_back(entry.storage_address - ctx.base_addr - ctx.word_offset * 4); From a98a52e5d76376a765e64445d14f0c3781aa06c6 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Mon, 19 Aug 2024 10:11:48 +0100 Subject: [PATCH 18/67] Fixes #114 - Allow specifying family_id when saving Auto-detects from the binary if not specified Also extend save range to cover metadata blocks at the end --- main.cpp | 152 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 94 insertions(+), 58 deletions(-) diff --git a/main.cpp b/main.cpp index 0756cfc..e75e9af 100644 --- a/main.cpp +++ b/main.cpp @@ -665,6 +665,8 @@ struct save_command : public cmd { hex("to").set(settings.to) % "The upper address bound in hex" ).min(0).doc_non_optional(true) ).min(0).doc_non_optional(true).no_match_beats_error(false) % "Selection of data to save" + + (option("--family") % "Specify the family ID to save the file as" & + family_id("family_id").set(settings.family_id) % "family id to save file as").force_expand_help(true) + ( // note this parenthesis seems to help with error messages for say save --foo device_selection % "Source device selection" + file_selection % "File to save to" @@ -2819,6 +2821,26 @@ std::unique_ptr find_best_block(memory_access &raw_access, vector find_last_block(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 new_size) { + DEBUG_LOG("Now reading from %x size %x\n", raw_access.get_binary_start(), new_size); + bin = raw_access.read_vector(raw_access.get_binary_start(), new_size, true); + }; + auto last_block = get_last_block(bin, raw_access.get_binary_start(), first_block, more_cb); + return last_block; + } + + return nullptr; +} + #if HAS_LIBUSB void info_guts(memory_access &raw_access, picoboot::connection *con) { #else @@ -3490,67 +3512,74 @@ bool help_command::execute(device_map &devices) { return false; } +uint32_t get_access_family_id(memory_access &file_access) { + uint32_t family_id = 0; + vector bin; + std::unique_ptr best_block = find_best_block(file_access, bin); + if (best_block == NULL) { + // No block, so RP2040 or absolute + if (file_access.get_binary_start() == FLASH_START) { + vector checksum_data = {}; + file_access.read_into_vector(FLASH_START, 252, checksum_data); + uint32_t checksum = file_access.read_int(FLASH_START + 252); + if (checksum == calc_checksum(checksum_data)) { + // Checksum is correct, so RP2040 + DEBUG_LOG("Detected family id %s due to boot2 checksum\n", family_name(RP2040_FAMILY_ID).c_str()); + return RP2040_FAMILY_ID; + } else { + // Checksum incorrect, so absolute + DEBUG_LOG("Assumed family id %s\n", family_name(ABSOLUTE_FAMILY_ID).c_str()); + return ABSOLUTE_FAMILY_ID; + } + } else { + // no_flash RP2040 binaries have no checksum + DEBUG_LOG("Assumed family id %s\n", family_name(RP2040_FAMILY_ID).c_str()); + return RP2040_FAMILY_ID; + } + } + auto first_item = best_block->items[0].get(); + if (first_item->type() != PICOBIN_BLOCK_ITEM_1BS_IMAGE_TYPE) { + // This will apply for partition tables + DEBUG_LOG("Assumed family id %s due to block with no IMAGE_DEF\n", family_name(ABSOLUTE_FAMILY_ID).c_str()); + return ABSOLUTE_FAMILY_ID; + } + auto image_def = dynamic_cast(first_item); + if (image_def->image_type() == type_exe) { + if (image_def->chip() == chip_rp2040) { + family_id = RP2040_FAMILY_ID; + } else if (image_def->chip() == chip_rp2350) { + if (image_def->cpu() == cpu_riscv) { + family_id = RP2350_RISCV_FAMILY_ID; + } else if (image_def->cpu() == cpu_arm) { + if (image_def->security() == sec_s) { + family_id = RP2350_ARM_S_FAMILY_ID; + } else if (image_def->security() == sec_ns) { + family_id = RP2350_ARM_NS_FAMILY_ID; + } else { + fail(ERROR_INCOMPATIBLE, "Cannot autodetect UF2 family: Unsupported security level %x\n", image_def->security()); + } + } else { + fail(ERROR_INCOMPATIBLE, "Cannot autodetect UF2 family: Unsupported cpu %x\n", image_def->cpu()); + } + } else { + fail(ERROR_INCOMPATIBLE, "Cannot autodetect UF2 family: Unsupported chip %x\n", image_def->chip()); + } + } else if (image_def->image_type() == type_data) { + family_id = DATA_FAMILY_ID; + } else { + fail(ERROR_INCOMPATIBLE, "Cannot autodetect UF2 family: Unsupported image type %x\n", image_def->image_type()); + } + + return family_id; +} + uint32_t get_family_id(uint8_t file_idx) { uint32_t family_id = 0; if (settings.family_id) { family_id = settings.family_id; } else if (get_file_type_idx(file_idx) == filetype::elf || get_file_type_idx(file_idx) == filetype::bin) { auto file_access = get_file_memory_access(file_idx); - vector bin; - std::unique_ptr best_block = find_best_block(file_access, bin); - if (best_block == NULL) { - // No block, so RP2040 or absolute - if (file_access.get_binary_start() == FLASH_START) { - vector checksum_data = {}; - file_access.read_into_vector(FLASH_START, 252, checksum_data); - uint32_t checksum = file_access.read_int(FLASH_START + 252); - if (checksum == calc_checksum(checksum_data)) { - // Checksum is correct, so RP2040 - DEBUG_LOG("Detected family id %s due to boot2 checksum\n", family_name(RP2040_FAMILY_ID).c_str()); - return RP2040_FAMILY_ID; - } else { - // Checksum incorrect, so absolute - DEBUG_LOG("Assumed family id %s\n", family_name(ABSOLUTE_FAMILY_ID).c_str()); - return ABSOLUTE_FAMILY_ID; - } - } else { - // no_flash RP2040 binaries have no checksum - DEBUG_LOG("Assumed family id %s\n", family_name(RP2040_FAMILY_ID).c_str()); - return RP2040_FAMILY_ID; - } - } - auto first_item = best_block->items[0].get(); - if (first_item->type() != PICOBIN_BLOCK_ITEM_1BS_IMAGE_TYPE) { - // This will apply for partition tables - DEBUG_LOG("Assumed family id %s due to block with no IMAGE_DEF\n", family_name(ABSOLUTE_FAMILY_ID).c_str()); - return ABSOLUTE_FAMILY_ID; - } - auto image_def = dynamic_cast(first_item); - if (image_def->image_type() == type_exe) { - if (image_def->chip() == chip_rp2040) { - family_id = RP2040_FAMILY_ID; - } else if (image_def->chip() == chip_rp2350) { - if (image_def->cpu() == cpu_riscv) { - family_id = RP2350_RISCV_FAMILY_ID; - } else if (image_def->cpu() == cpu_arm) { - if (image_def->security() == sec_s) { - family_id = RP2350_ARM_S_FAMILY_ID; - } else if (image_def->security() == sec_ns) { - family_id = RP2350_ARM_NS_FAMILY_ID; - } else { - fail(ERROR_INCOMPATIBLE, "Cannot autodetect UF2 family: Unsupported security level %x\n", image_def->security()); - } - } else { - fail(ERROR_INCOMPATIBLE, "Cannot autodetect UF2 family: Unsupported cpu %x\n", image_def->cpu()); - } - } else { - fail(ERROR_INCOMPATIBLE, "Cannot autodetect UF2 family: Unsupported chip %x\n", image_def->chip()); - } - } else if (image_def->image_type() == type_data) { - family_id = DATA_FAMILY_ID; - } else { - fail(ERROR_INCOMPATIBLE, "Cannot autodetect UF2 family: Unsupported image type %x\n", image_def->image_type()); - } + family_id = get_access_family_id(file_access); } else if (get_file_type_idx(file_idx) == filetype::uf2) { auto file = get_file_idx(ios::in|ios::binary, file_idx); uf2_block block; @@ -3843,11 +3872,18 @@ bool save_command::execute(device_map &devices) { }); visitor.visit(access, hdr); } - if (binary_end == 0) { + end = binary_end; + vector bin; + std::unique_ptr last_block = find_last_block(raw_access, bin); + if (last_block != nullptr) { + uint32_t new_end = last_block->physical_addr + (last_block->to_words().size())*4; + DEBUG_LOG("Adjusting end to max of %x %x\n", end, new_end); + end = MAX(end, new_end); + } + if (end == 0) { fail(ERROR_NOT_POSSIBLE, "Cannot determine the binary size, so cannot save the program only, try --all."); } - end = binary_end; } } else { end = FLASH_START + guess_flash_size(raw_access); @@ -3888,7 +3924,7 @@ bool save_command::execute(device_map &devices) { block.flags = UF2_FLAG_FAMILY_ID_PRESENT; block.payload_size = PAGE_SIZE; block.num_blocks = (size + PAGE_SIZE - 1)/PAGE_SIZE; - block.file_size = RP2040_FAMILY_ID; + block.file_size = settings.family_id ? settings.family_id : get_access_family_id(raw_access); block.magic_end = UF2_MAGIC_END; writer256 = [&](FILE *out, const uint8_t *buffer, unsigned int size, unsigned int offset) { static_assert(512 == sizeof(block), ""); From 4545546271e8623533f6aff11cbe7dcacb320b50 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Tue, 20 Aug 2024 11:09:45 +0100 Subject: [PATCH 19/67] Revert "Target abs-block at CS1 by default" Bug discovered where larger UF2 files with an abs-block targeting CS1 fail to download - this is not seen when it targets CS0 so revert this change for now This reverts commit ef03bd2d90c4987f59bec09ba03d8dbcd881c612. --- main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.cpp b/main.cpp index e75e9af..44b1fa3 100644 --- a/main.cpp +++ b/main.cpp @@ -471,7 +471,7 @@ struct _settings { struct { bool abs_block = false; #if SUPPORT_A2 - uint32_t abs_block_loc = 0x12000000 - UF2_PAGE_SIZE; + uint32_t abs_block_loc = 0x11000000 - UF2_PAGE_SIZE; #else uint32_t abs_block_loc = 0; #endif From f5064f76fdc3ce337e403dd7e20bc7b8760e0533 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Tue, 20 Aug 2024 12:01:06 +0100 Subject: [PATCH 20/67] Fix compilation with HAS_MBEDTLS=0 Move checksum calculation into bintool --- bintool/bintool.cpp | 124 +++++++++++++++++++++++++++++++++++--- bintool/bintool.h | 2 +- bintool/mbedtls_wrapper.c | 104 -------------------------------- bintool/mbedtls_wrapper.h | 2 - 4 files changed, 115 insertions(+), 117 deletions(-) diff --git a/bintool/bintool.cpp b/bintool/bintool.cpp index eb235f8..c8fa8dc 100644 --- a/bintool/bintool.cpp +++ b/bintool/bintool.cpp @@ -453,6 +453,120 @@ block place_new_block(std::vector &bin, uint32_t storage_addr, std::uni } +// Checksum stuff +uint32_t poly8_lookup[256] = +{ + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, + 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, + 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, + 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, + 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, + 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, + 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, + 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, + 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, + 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, + 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, + 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, + 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, + 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, + 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, + 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, + 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, + 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, + 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, + 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, + 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, + 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, + 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, + 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, + 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, + 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, + 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, + 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, + 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, + 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, + 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, + 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, + 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, + 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, + 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, + 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, + 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, + 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, + 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, + 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, + 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, + 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, + 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, + 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, + 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, + 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, + 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, + 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, + 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, + 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, + 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, + 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, + 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, + 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, + 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, + 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, + 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, + 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, + 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, + 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, + 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D +}; + +uint32_t crc32_byte(const uint8_t *p, uint32_t bytelength) +{ + uint32_t crc = 0xffffffff; + while (bytelength-- !=0) crc = poly8_lookup[((uint8_t) crc ^ *(p++))] ^ (crc >> 8); + return crc; +} + +uint8_t rev_8(uint8_t b) { + b = (b & 0xF0) >> 4 | (b & 0x0F) << 4; + b = (b & 0xCC) >> 2 | (b & 0x33) << 2; + b = (b & 0xAA) >> 1 | (b & 0x55) << 1; + return b; +} + +uint32_t rev_32(uint32_t b) { + uint8_t b0 = rev_8(b >> 24); + uint8_t b1 = rev_8(b >> 16); + uint8_t b2 = rev_8(b >> 8); + uint8_t b3 = rev_8(b); + return (b3 << 24) | (b2 << 16) | (b1 << 8) | b0; +} + +void crc32(const uint8_t *data, size_t len, uint32_t* cs_out) { + uint8_t rev_data[252] = {}; + for (size_t i=0; i < sizeof(rev_data); i++) { + rev_data[i] = rev_8(data[i]); + } + + uint32_t crc = crc32_byte(rev_data, sizeof(rev_data)); + + crc = rev_32(crc); + + *cs_out = crc; +} + +uint32_t calc_checksum(std::vector bin) { + assert(bin.size() == 252); + + uint32_t checksum = 0; + crc32(bin.data(), bin.size(), &checksum); + + return checksum; +} + + #if HAS_MBEDTLS void hash_andor_sign_block(block *new_block, const public_t public_key, const private_t private_key, bool hash_value, bool sign, std::vector to_hash) { std::shared_ptr hash_def = std::make_shared(PICOBIN_HASH_SHA256); @@ -755,16 +869,6 @@ void verify_block(std::vector bin, uint32_t storage_addr, uint32_t runt } -uint32_t calc_checksum(std::vector bin) { - assert(bin.size() == 252); - - uint32_t checksum = 0; - crc32(bin.data(), bin.size(), &checksum); - - return checksum; -} - - int encrypt(elf_file *elf, block *new_block, const private_t aes_key, const public_t public_key, const private_t private_key, bool hash_value, bool sign) { std::vector to_enc = get_lm_hash_data(elf, new_block); diff --git a/bintool/bintool.h b/bintool/bintool.h index 49935e2..77e89c8 100644 --- a/bintool/bintool.h +++ b/bintool/bintool.h @@ -34,9 +34,9 @@ std::unique_ptr find_first_block(std::vector bin, uint32_t stora std::unique_ptr get_last_block(std::vector &bin, uint32_t storage_addr, std::unique_ptr &first_block, get_more_bin_cb more_cb = nullptr); std::vector> get_all_blocks(std::vector &bin, uint32_t storage_addr, std::unique_ptr &first_block, get_more_bin_cb more_cb = nullptr); block place_new_block(std::vector &bin, uint32_t storage_addr, std::unique_ptr &first_block); +uint32_t calc_checksum(std::vector bin); #if HAS_MBEDTLS std::vector hash_andor_sign(std::vector bin, uint32_t storage_addr, uint32_t runtime_addr, block *new_block, const public_t public_key, const private_t private_key, bool hash_value, bool sign, bool clear_sram = false); std::vector encrypt(std::vector bin, uint32_t storage_addr, uint32_t runtime_addr, block *new_block, const private_t aes_key, const public_t public_key, const private_t private_key, bool hash_value, bool sign); void verify_block(std::vector bin, uint32_t storage_addr, uint32_t runtime_addr, block *block, verified_t &hash_verified, verified_t &sig_verified); - uint32_t calc_checksum(std::vector bin); #endif diff --git a/bintool/mbedtls_wrapper.c b/bintool/mbedtls_wrapper.c index b622da6..1d2e115 100644 --- a/bintool/mbedtls_wrapper.c +++ b/bintool/mbedtls_wrapper.c @@ -240,107 +240,3 @@ uint32_t mb_verify_signature_secp256k1( return 0; } - -// Not actually mbedtls, but leaving in here anyway -uint32_t poly8_lookup[256] = -{ - 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, - 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, - 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, - 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, - 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, - 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, - 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, - 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, - 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, - 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, - 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, - 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, - 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, - 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, - 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, - 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, - 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, - 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, - 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, - 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, - 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, - 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, - 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, - 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, - 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, - 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, - 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, - 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, - 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, - 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, - 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, - 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, - 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, - 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, - 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, - 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, - 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, - 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, - 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, - 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, - 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, - 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, - 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, - 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, - 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, - 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, - 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, - 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, - 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, - 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, - 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, - 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, - 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, - 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, - 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, - 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, - 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, - 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, - 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, - 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, - 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, - 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, - 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, - 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D -}; - -uint32_t crc32_byte(const uint8_t *p, uint32_t bytelength) -{ - uint32_t crc = 0xffffffff; - while (bytelength-- !=0) crc = poly8_lookup[((uint8_t) crc ^ *(p++))] ^ (crc >> 8); - return crc; -} - -uint8_t rev_8(uint8_t b) { - b = (b & 0xF0) >> 4 | (b & 0x0F) << 4; - b = (b & 0xCC) >> 2 | (b & 0x33) << 2; - b = (b & 0xAA) >> 1 | (b & 0x55) << 1; - return b; -} - -uint32_t rev_32(uint32_t b) { - uint8_t b0 = rev_8(b >> 24); - uint8_t b1 = rev_8(b >> 16); - uint8_t b2 = rev_8(b >> 8); - uint8_t b3 = rev_8(b); - return (b3 << 24) | (b2 << 16) | (b1 << 8) | b0; -} - -void mb_crc32(const uint8_t *data, size_t len, uint32_t* cs_out) { - uint8_t rev_data[252] = {}; - for (size_t i=0; i < sizeof(rev_data); i++) { - rev_data[i] = rev_8(data[i]); - } - - uint32_t crc = crc32_byte(rev_data, sizeof(rev_data)); - - crc = rev_32(crc); - - *cs_out = crc; -} diff --git a/bintool/mbedtls_wrapper.h b/bintool/mbedtls_wrapper.h index de36d1b..c4e2b77 100644 --- a/bintool/mbedtls_wrapper.h +++ b/bintool/mbedtls_wrapper.h @@ -44,7 +44,6 @@ typedef message_digest_t private_t; void mb_sha256_buffer(const uint8_t *data, size_t len, message_digest_t *digest_out); void mb_aes256_buffer(const uint8_t *data, size_t len, uint8_t *data_out, const private_t *key, iv_t *iv); void mb_sign_sha256(const uint8_t *entropy, size_t entropy_size, const message_digest_t *m, const public_t *p, const private_t *d, signature_t *out); -void mb_crc32(const uint8_t *data, size_t len, uint32_t* cs_out); uint32_t mb_verify_signature_secp256k1( signature_t signature[1], @@ -55,7 +54,6 @@ uint32_t mb_verify_signature_secp256k1( #define aes256_buffer mb_aes256_buffer #define sign_sha256 mb_sign_sha256 #define verify_signature_secp256k1 mb_verify_signature_secp256k1 -#define crc32 mb_crc32 #ifdef __cplusplus }; From 3027a54423647c468c2db93651ea12882eb97d36 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Tue, 20 Aug 2024 15:09:21 +0100 Subject: [PATCH 21/67] Don't hash pts by default with no mbedtls --- main.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/main.cpp b/main.cpp index 44b1fa3..59396b6 100644 --- a/main.cpp +++ b/main.cpp @@ -463,7 +463,11 @@ struct _settings { } version; struct { + #if HAS_MBEDTLS bool hash = true; + #else + bool hash = false; + #endif bool sign = false; bool singleton = false; } partition; @@ -875,12 +879,14 @@ struct partition_create_command : public cmd { ).min(0).force_expand_help(true) % "UF2 output options") + optional_typed_file_selection_x("bootloader", 2, "elf") % "embed partition table into bootloader ELF" + ( + #if HAS_MBEDTLS // todo why doesn't this set settings.partition.sign? ((option("--sign").set(settings.partition.sign) & value("keyfile").with_exclusion_filter([](const string &value) { return value.find_first_of('-') == 0; }).set(settings.filenames[3])) % "The file name" + named_file_types_x("pem", 3)) % "Sign the partition table" + (option("--no-hash").clear(settings.partition.hash) % "Don't hash the partition table") + + #endif (option("--singleton").set(settings.partition.singleton) % "Singleton partition table") ).min(0).force_expand_help(true) % "Partition Table Options" #if SUPPORT_A2 From 6ad9c2352cf90b4abf183143e3cbd9ec933b9ad1 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Fri, 23 Aug 2024 15:45:23 +0100 Subject: [PATCH 22/67] Use lib instead of CMAKE_INSTALL_LIBDIR Fixes #117 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5ca5246..fd716be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,7 +35,7 @@ if (PICOTOOL_FLAT_INSTALL) set(INSTALL_DATADIR picotool) set(INSTALL_BINDIR picotool) else() - set(INSTALL_CONFIGDIR ${CMAKE_INSTALL_LIBDIR}/cmake/picotool) + set(INSTALL_CONFIGDIR lib/cmake/picotool) set(INSTALL_DATADIR ${CMAKE_INSTALL_DATADIR}/picotool) set(INSTALL_BINDIR ${CMAKE_INSTALL_BINDIR}) endif() From b4590fa43bafc1d2df4c348ee1aa38bb9f02fc84 Mon Sep 17 00:00:00 2001 From: Andrew Scheller Date: Wed, 28 Aug 2024 16:31:22 +0100 Subject: [PATCH 23/67] Use string constants for the family names (#123) Instead of having hard-coded strings scattered through the code --- main.cpp | 57 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/main.cpp b/main.cpp index 59396b6..340cc39 100644 --- a/main.cpp +++ b/main.cpp @@ -101,7 +101,14 @@ auto memory_names = map{ {memory_type::rom, "ROM"} }; -static string tool_name = "picotool"; +static const string tool_name = "picotool"; + +static const string data_family_name = "data"; +static const string absolute_family_name = "absolute"; +static const string rp2040_family_name = "rp2040"; +static const string rp2350_arm_s_family_name = "rp2350-arm-s"; +static const string rp2350_arm_ns_family_name = "rp2350-arm-ns"; +static const string rp2350_riscv_family_name = "rp2350-riscv"; static string hex_string(int64_t value, int width=8, bool prefix=true) { std::stringstream ss; @@ -295,17 +302,17 @@ struct family_id : public cli::value_base { // note we cannot capture "this" on_action([&t, nm](string value) { auto ovalue = value; - if (value == "data") { + if (value == data_family_name) { t = DATA_FAMILY_ID; - } else if (value == "absolute") { + } else if (value == absolute_family_name) { t = ABSOLUTE_FAMILY_ID; - } else if (value == "rp2040") { + } else if (value == rp2040_family_name) { t = RP2040_FAMILY_ID; - } else if (value == "rp2350-arm-s") { + } else if (value == rp2350_arm_s_family_name) { t = RP2350_ARM_S_FAMILY_ID; - } else if (value == "rp2350-arm-ns") { + } else if (value == rp2350_arm_s_family_name) { t = RP2350_ARM_NS_FAMILY_ID; - } else if (value == "rp2350-riscv") { + } else if (value == rp2350_riscv_family_name) { t = RP2350_RISCV_FAMILY_ID; } else { if (value.find("0x") == 0) value = value.substr(2); @@ -332,12 +339,12 @@ struct family_id : public cli::value_base { }; string family_name(unsigned int family_id) { - if (family_id == DATA_FAMILY_ID) return "'data'"; - if (family_id == ABSOLUTE_FAMILY_ID) return "'absolute'"; - if (family_id == RP2040_FAMILY_ID) return "'rp2040'"; - if (family_id == RP2350_ARM_S_FAMILY_ID) return "'rp2350-arm-s'"; - if (family_id == RP2350_ARM_NS_FAMILY_ID) return "'rp2350-arm-ns'"; - if (family_id == RP2350_RISCV_FAMILY_ID) return "'rp2350-riscv'"; + if (family_id == DATA_FAMILY_ID) return "'" + data_family_name + "'"; + if (family_id == ABSOLUTE_FAMILY_ID) return "'" + absolute_family_name + "'"; + if (family_id == RP2040_FAMILY_ID) return "'" + rp2040_family_name + "'"; + if (family_id == RP2350_ARM_S_FAMILY_ID) return "'" + rp2350_arm_s_family_name + "'"; + if (family_id == RP2350_ARM_NS_FAMILY_ID) return "'" + rp2350_arm_ns_family_name + "'"; + if (family_id == RP2350_RISCV_FAMILY_ID) return "'" + rp2350_riscv_family_name + "'"; if (!family_id) return "none"; return hex_string(family_id); } @@ -5496,17 +5503,17 @@ uint32_t permissions_to_flags(json permissions) { uint32_t families_to_flags(std::vector families) { uint32_t ret = 0; for (auto family : families) { - if (family == "data") { + if (family == data_family_name) { ret |= PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_DATA_BITS; - } else if (family == "absolute") { + } else if (family == absolute_family_name) { ret |= PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_ABSOLUTE_BITS; - } else if (family == "rp2040") { + } else if (family == rp2040_family_name) { ret |= PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_RP2040_BITS; - } else if (family == "rp2350-arm-s") { + } else if (family == rp2350_arm_s_family_name) { ret |= PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_RP2350_ARM_S_BITS; - } else if (family == "rp2350-arm-ns") { + } else if (family == rp2350_arm_ns_family_name) { ret |= PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_RP2350_ARM_NS_BITS; - } else if (family == "rp2350-riscv") { + } else if (family == rp2350_riscv_family_name) { ret |= PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_RP2350_RISCV_BITS; } } @@ -6498,12 +6505,12 @@ bool coprodis_command::execute(device_map &devices) { #if HAS_LIBUSB void partition_info_command::insert_default_families(uint32_t flags_and_permissions, vector &family_ids) const { - if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_ABSOLUTE_BITS) family_ids.emplace_back("absolute"); - if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_RP2040_BITS) family_ids.emplace_back("rp2040"); - if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_RP2350_ARM_S_BITS) family_ids.emplace_back("rp2350-arm-s"); - if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_RP2350_ARM_NS_BITS) family_ids.emplace_back("rp2350-arm-ns"); - if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_RP2350_RISCV_BITS) family_ids.emplace_back("rp2350-riscv"); - if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_DATA_BITS) family_ids.emplace_back("data"); + if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_ABSOLUTE_BITS) family_ids.emplace_back(absolute_family_name); + if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_RP2040_BITS) family_ids.emplace_back(rp2040_family_name); + if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_RP2350_ARM_S_BITS) family_ids.emplace_back(rp2350_arm_s_family_name); + if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_RP2350_ARM_NS_BITS) family_ids.emplace_back(rp2350_arm_ns_family_name); + if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_RP2350_RISCV_BITS) family_ids.emplace_back(rp2350_riscv_family_name); + if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_DATA_BITS) family_ids.emplace_back(data_family_name); } void partition_info_command::print_permissions(unsigned int p) const { From 05ae05532ad175c3cd3570a833166bf230d9ffd0 Mon Sep 17 00:00:00 2001 From: will-v-pi <108662275+will-v-pi@users.noreply.github.com> Date: Wed, 28 Aug 2024 16:47:33 +0100 Subject: [PATCH 24/67] Support multiple tries when using -f/F (#116) This is necessary for WSL, or other cases where it takes more time to detect the device after reboot --- main.cpp | 152 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 85 insertions(+), 67 deletions(-) diff --git a/main.cpp b/main.cpp index 340cc39..290a3f9 100644 --- a/main.cpp +++ b/main.cpp @@ -75,6 +75,8 @@ static __forceinline int __builtin_ctz(unsigned x) { #define _CRT_SECURE_NO_WARNINGS #endif +#define MAX_REBOOT_TRIES 5 + #define OTP_PAGE_COUNT 64 #define OTP_PAGE_ROWS 64 #define OTP_ROW_COUNT (OTP_PAGE_COUNT * OTP_PAGE_ROWS) @@ -7434,7 +7436,7 @@ int main(int argc, char **argv) { } // we only loop a second time if we want to reboot some devices (which may cause device - for (int tries = 0; !rc && tries < 2; tries++) { + for (int tries = 0; !rc && tries <= MAX_REBOOT_TRIES; tries++) { if (ctx) { if (libusb_get_device_list(ctx, &devs) < 0) { fail(ERROR_USB, "Failed to enumerate USB devices\n"); @@ -7461,49 +7463,57 @@ int main(int argc, char **argv) { case cmd::device_support::one: if (devices[dr_vidpid_bootrom_ok].empty() && (!settings.force || devices[dr_vidpid_stdio_usb].empty())) { - bool had_note = false; - fos << missing_device_string(tries>0, selected_cmd->requires_rp2350()); - if (tries > 0) { - fos << " It is possible the device is not responding, and will have to be manually entered into BOOTSEL mode.\n"; - had_note = true; // suppress "but:" in this case - } - fos << "\n"; - fos.first_column(0); - fos.hanging_indent(4); - auto printer = [&](enum picoboot_device_result r, const string &description) { - if (!had_note && !devices[r].empty()) { - fos << "\nbut:\n\n"; - had_note = true; + if (tries == 0 || tries == MAX_REBOOT_TRIES) { + if (tries) { + fos << "\n\n"; } - for (auto d : devices[r]) { - fos << bus_device_string(std::get<1>(d)) << description << "\n"; + bool had_note = false; + fos << missing_device_string(tries>0, selected_cmd->requires_rp2350()); + if (tries) { + fos << " It is possible the device is not responding, and will have to be manually entered into BOOTSEL mode.\n"; + had_note = true; // suppress "but:" in this case } - }; -#if defined(__linux__) || defined(__APPLE__) - printer(dr_vidpid_bootrom_cant_connect, - " appears to be a RP2040 device in BOOTSEL mode, but picotool was unable to connect. Maybe try 'sudo' or check your permissions."); -#else - printer(dr_vidpid_bootrom_cant_connect, - " appears to be a RP2040 device in BOOTSEL mode, but picotool was unable to connect. You may need to install a driver via Zadig. See \"Getting started with Raspberry Pi Pico\" for more information"); -#endif - printer(dr_vidpid_picoprobe, - " appears to be a RP2040 PicoProbe device not in BOOTSEL mode."); - printer(dr_vidpid_micropython, - " appears to be a RP2040 MicroPython device not in BOOTSEL mode."); - if (selected_cmd->force_requires_pre_reboot()) { -#if defined(_WIN32) - printer(dr_vidpid_stdio_usb, - " appears to be a RP2040 device with a USB serial connection, not in BOOTSEL mode. You can force reboot into BOOTSEL mode via 'picotool reboot -f -u' first."); -#else - printer(dr_vidpid_stdio_usb, - " appears to be a RP2040 device with a USB serial connection, so consider -f (or -F) to force reboot in order to run the command."); -#endif + fos << "\n"; + fos.first_column(0); + fos.hanging_indent(4); + auto printer = [&](enum picoboot_device_result r, const string &description) { + if (!had_note && !devices[r].empty()) { + fos << "\nbut:\n\n"; + had_note = true; + } + for (auto d : devices[r]) { + fos << bus_device_string(std::get<1>(d)) << description << "\n"; + } + }; + #if defined(__linux__) || defined(__APPLE__) + printer(dr_vidpid_bootrom_cant_connect, + " appears to be a RP2040 device in BOOTSEL mode, but picotool was unable to connect. Maybe try 'sudo' or check your permissions."); + #else + printer(dr_vidpid_bootrom_cant_connect, + " appears to be a RP2040 device in BOOTSEL mode, but picotool was unable to connect. You may need to install a driver via Zadig. See \"Getting started with Raspberry Pi Pico\" for more information"); + #endif + printer(dr_vidpid_picoprobe, + " appears to be a RP2040 PicoProbe device not in BOOTSEL mode."); + printer(dr_vidpid_micropython, + " appears to be a RP2040 MicroPython device not in BOOTSEL mode."); + if (selected_cmd->force_requires_pre_reboot()) { + #if defined(_WIN32) + printer(dr_vidpid_stdio_usb, + " appears to be a RP2040 device with a USB serial connection, not in BOOTSEL mode. You can force reboot into BOOTSEL mode via 'picotool reboot -f -u' first."); + #else + printer(dr_vidpid_stdio_usb, + " appears to be a RP2040 device with a USB serial connection, so consider -f (or -F) to force reboot in order to run the command."); + #endif + } else { + // special case message for what is actually just reboot (the only command that doesn't require reboot first) + printer(dr_vidpid_stdio_usb, + " appears to be a RP2040 device with a USB serial connection, so consider -f to force the reboot."); + } + rc = ERROR_NO_DEVICE; } else { - // special case message for what is actually just reboot (the only command that doesn't require reboot first) - printer(dr_vidpid_stdio_usb, - " appears to be a RP2040 device with a USB serial connection, so consider -f to force the reboot."); + // waiting for rebooted device to show up + break; } - rc = ERROR_NO_DEVICE; } else if (supported == cmd::device_support::one) { if (devices[dr_vidpid_bootrom_ok].size() > 1 || (devices[dr_vidpid_bootrom_ok].empty() && devices[dr_vidpid_stdio_usb].size() > 1)) { @@ -7524,36 +7534,43 @@ int main(int argc, char **argv) { } if (!rc) { if (settings.force && ctx) { // actually ctx should never be null as we are targeting device if force is set, but still - if (devices[dr_vidpid_stdio_usb].size() != 1) { + if (devices[dr_vidpid_stdio_usb].size() != 1 && !tries) { fail(ERROR_NOT_POSSIBLE, "Forced command requires a single rebootable RP2040 device to be targeted."); } if (selected_cmd->force_requires_pre_reboot()) { - // we reboot into BOOTSEL mode and disable MSC interface (the 1 here) - auto &to_reboot = std::get<1>(devices[dr_vidpid_stdio_usb][0]); - auto &to_reboot_handle = std::get<2>(devices[dr_vidpid_stdio_usb][0]); -#if defined(_WIN32) - { - struct libusb_device_descriptor desc; - libusb_get_device_descriptor(to_reboot, &desc); - if (desc.idProduct == PRODUCT_ID_RP2040_STDIO_USB) { - fail(ERROR_NOT_POSSIBLE, - "Forced commands do not work with RP2040 on Windows - you can force reboot into BOOTSEL mode via 'picotool reboot -f -u' instead."); + if (!tries) { + // we reboot into BOOTSEL mode and disable MSC interface (the 1 here) + auto &to_reboot = std::get<1>(devices[dr_vidpid_stdio_usb][0]); + auto &to_reboot_handle = std::get<2>(devices[dr_vidpid_stdio_usb][0]); + #if defined(_WIN32) + { + struct libusb_device_descriptor desc; + libusb_get_device_descriptor(to_reboot, &desc); + if (desc.idProduct == PRODUCT_ID_RP2040_STDIO_USB) { + fail(ERROR_NOT_POSSIBLE, + "Forced commands do not work with RP2040 on Windows - you can force reboot into BOOTSEL mode via 'picotool reboot -f -u' instead."); + } + } + #endif + if (settings.ser.empty() && to_reboot_handle) { + // store USB serial number, to pick correct device after reboot + struct libusb_device_descriptor desc; + libusb_get_device_descriptor(to_reboot, &desc); + char ser_str[128]; + libusb_get_string_descriptor_ascii(to_reboot_handle, desc.iSerialNumber, (unsigned char*)ser_str, sizeof(ser_str)); + settings.ser = ser_str; + fos << "Tracking device serial number " << ser_str << " for reboot\n"; } - } -#endif - if (settings.ser.empty() && to_reboot_handle) { - // store USB serial number, to pick correct device after reboot - struct libusb_device_descriptor desc; - libusb_get_device_descriptor(to_reboot, &desc); - char ser_str[128]; - libusb_get_string_descriptor_ascii(to_reboot_handle, desc.iSerialNumber, (unsigned char*)ser_str, sizeof(ser_str)); - settings.ser = ser_str; - fos << "Tracking device serial number " << ser_str << " for reboot\n"; - } - reboot_device(to_reboot, to_reboot_handle, true, 1); - fos << "The device was asked to reboot into BOOTSEL mode so the command can be executed.\n\n"; + reboot_device(to_reboot, to_reboot_handle, true, 1); + fos << "The device was asked to reboot into BOOTSEL mode so the command can be executed."; + } else if (tries == 1) { + fos << "\nWaiting for device to reboot"; + } else { + fos << "..."; + } + fos.flush(); for (const auto &handle : to_close) { libusb_close(handle); } @@ -7563,15 +7580,16 @@ int main(int argc, char **argv) { devices.clear(); sleep_ms(1200); - // we now clear settings.force, because we expect the device to have rebooted and be available. - // we also clear bus/address filters, because the device may have moved, so the only way we can find it + // we now clear bus/address filters, because the device may have moved, so the only way we can find it // again is to assume it has the same serial number. - settings.force = false; settings.address = -1; settings.bus = -1; continue; } } + if (tries) { + fos << "\n\n"; + } if (!selected_cmd->execute(devices) && tries) { if (settings.force_no_reboot) { fos << "\nThe device has been left accessible, but without the drive mounted; use 'picotool reboot' to reboot into regular BOOTSEL mode or application mode.\n"; From 94a96f3af14f75a11f9cb4bb1840e2250adbec46 Mon Sep 17 00:00:00 2001 From: Andrew Scheller Date: Wed, 28 Aug 2024 17:49:52 +0100 Subject: [PATCH 25/67] Small refactoring of the hex_string functions (#124) --- main.cpp | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/main.cpp b/main.cpp index 290a3f9..338ef90 100644 --- a/main.cpp +++ b/main.cpp @@ -112,17 +112,12 @@ static const string rp2350_arm_s_family_name = "rp2350-arm-s"; static const string rp2350_arm_ns_family_name = "rp2350-arm-ns"; static const string rp2350_riscv_family_name = "rp2350-riscv"; -static string hex_string(int64_t value, int width=8, bool prefix=true) { +static string hex_string(int64_t value, int width=8, bool prefix=true, bool uppercase=false) { std::stringstream ss; if (prefix) ss << "0x"; - ss << std::setfill('0') << std::setw(width) << std::hex << value; - return ss.str(); -} - -static string HEX_string(int64_t value, int width=8, bool prefix=true) { - std::stringstream ss; - if (prefix) ss << "0x"; - ss << std::setfill('0') << std::setw(width) << std::uppercase << std::hex << value; + ss << std::setfill('0') << std::setw(width); + if (uppercase) ss << std::uppercase; + ss << std::hex << value; return ss.str(); } @@ -3326,7 +3321,7 @@ void info_guts(memory_access &raw_access, void *con) { if (model == rp2040) { uint64_t flash_id = 0; con->flash_id(flash_id); - info_pair("flash id", HEX_string(flash_id, 16)); + info_pair("flash id", hex_string(flash_id, 16, true, true)); } } } catch (picoboot::command_failure &e) { From 930fcb610835d96c870fe3d7d06d58239ded5434 Mon Sep 17 00:00:00 2001 From: graham sanderson Date: Thu, 29 Aug 2024 14:20:20 -0500 Subject: [PATCH 26/67] fixup for latest pico-sdk develop/ --- CMakeLists.txt | 5 ++--- main.cpp | 2 +- picoboot_connection/picoboot_connection.c | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fd716be..cd9537b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -222,10 +222,9 @@ add_subdirectory(${PICO_SDK_PATH}/src/common/boot_uf2_headers boot_uf2_headers) add_subdirectory(${PICO_SDK_PATH}/src/common/boot_picoboot_headers boot_picoboot_headers) add_subdirectory(${PICO_SDK_PATH}/src/common/boot_picobin_headers boot_picobin_headers) add_subdirectory(${PICO_SDK_PATH}/src/common/pico_usb_reset_interface_headers pico_usb_reset_interface_headers) +add_subdirectory(${PICO_SDK_PATH}/src/rp2_common/boot_bootrom_headers boot_bootrom_headers) add_subdirectory(${PICO_SDK_PATH}/src/host/pico_platform pico_platform) -add_library(pico_bootrom_headers INTERFACE) -target_include_directories(pico_bootrom_headers INTERFACE ${PICO_SDK_PATH}/src/rp2_common/pico_bootrom/include) add_library(regs_headers INTERFACE) target_include_directories(regs_headers INTERFACE ${PICO_SDK_PATH}/src/rp2350/hardware_regs/include) @@ -258,7 +257,7 @@ target_link_libraries(picotool boot_uf2_headers boot_picoboot_headers boot_picobin_headers - pico_bootrom_headers + boot_bootrom_headers pico_platform_headers pico_usb_reset_interface_headers regs_headers diff --git a/main.cpp b/main.cpp index 338ef90..9caaae8 100644 --- a/main.cpp +++ b/main.cpp @@ -42,7 +42,7 @@ #endif #include "bintool.h" #include "elf2uf2.h" -#include "pico/bootrom_constants.h" +#include "boot/bootrom_constants.h" #include "pico/binary_info.h" #include "pico/stdio_usb/reset_interface.h" #include "elf.h" diff --git a/picoboot_connection/picoboot_connection.c b/picoboot_connection/picoboot_connection.c index 8b08a0a..f71ea89 100644 --- a/picoboot_connection/picoboot_connection.c +++ b/picoboot_connection/picoboot_connection.c @@ -10,7 +10,7 @@ #include #include "picoboot_connection.h" -#include "pico/bootrom_constants.h" +#include "boot/bootrom_constants.h" #if ENABLE_DEBUG_LOG #include From f0232cd544ceb3c46167f93b4f7f30dcd8c5dd19 Mon Sep 17 00:00:00 2001 From: will-v-pi <108662275+will-v-pi@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:30:49 +0100 Subject: [PATCH 27/67] Fix loading into PSRAM (#121) Skip the flash size checks --- main.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main.cpp b/main.cpp index 9caaae8..9220905 100644 --- a/main.cpp +++ b/main.cpp @@ -4150,7 +4150,8 @@ bool load_guts(picoboot::connection con, iostream_memory_access &file_access) { uint32_t flash_start_offset = flash_min - FLASH_START; uint32_t size_guess = guess_flash_size(raw_access); if (size_guess > 0) { - if ((flash_start_offset + flash_data_size) > size_guess) { + // Skip check when targeting PSRAM, which is anything above 0x11000000 + if (flash_start_offset < FLASH_END_RP2040 && (flash_start_offset + flash_data_size) > size_guess) { if (flash_start_offset) { fail(ERROR_NOT_POSSIBLE, "File size 0x%x starting at 0x%x is too big to fit in flash size 0x%x", flash_data_size, flash_start_offset, size_guess); } else { From fb2e6b9b97fb25ebf3f70f6fc48ccc929b4df307 Mon Sep 17 00:00:00 2001 From: will-v-pi <108662275+will-v-pi@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:31:27 +0100 Subject: [PATCH 28/67] Add support for multi-family UF2s to picotool info (#122) --- README.md | 8 ++--- main.cpp | 89 +++++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 71 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index a2c35b3..3366195 100644 --- a/README.md +++ b/README.md @@ -380,7 +380,7 @@ OPTIONS: --family Specify the family ID of the file to load - family id to use for load + family ID to use for load -p, --partition Specify the partition to load into @@ -758,7 +758,7 @@ un-partitioned_space : S(rw) NSBOOT(rw) NS(rw), uf2 { absolute } partitions: 0(A) 00002000->00201000 S(rw) NSBOOT(rw) NS(rw), id=0000000000000000, "A", uf2 { rp2350-arm-s, rp2350-riscv }, arm_boot 1, riscv_boot 1 1(B w/ 0) 00201000->00400000 S(rw) NSBOOT(rw) NS(rw), id=0000000000000001, "B", uf2 { rp2350-arm-s, rp2350-riscv }, arm_boot 1, riscv_boot 1 -Family id 'rp2350-arm-s' can be downloaded in partition 0: +Family ID 'rp2350-arm-s' can be downloaded in partition 0: 00002000->00201000 ``` @@ -799,7 +799,7 @@ OPTIONS: --family Specify the family if for UF2 file output - family id for UF2 (default absolute) + family ID for UF2 (default absolute) embed partition table into bootloader ELF The file name @@ -860,7 +860,7 @@ OPTIONS: Load offset (memory address; default 0x10000000 for BIN file) UF2 Family options - family id for UF2 + family ID for UF2 Errata RP2350-E10 Fix --abs-block Add an absolute block diff --git a/main.cpp b/main.cpp index 9220905..9616184 100644 --- a/main.cpp +++ b/main.cpp @@ -674,7 +674,7 @@ struct save_command : public cmd { ).min(0).doc_non_optional(true) ).min(0).doc_non_optional(true).no_match_beats_error(false) % "Selection of data to save" + (option("--family") % "Specify the family ID to save the file as" & - family_id("family_id").set(settings.family_id) % "family id to save file as").force_expand_help(true) + + family_id("family_id").set(settings.family_id) % "family ID to save file as").force_expand_help(true) + ( // note this parenthesis seems to help with error messages for say save --foo device_selection % "Source device selection" + file_selection % "File to save to" @@ -695,7 +695,7 @@ struct load_command : public cmd { ( option("--ignore-partitions").set(settings.load.ignore_pt) % "When writing flash data, ignore the partition table and write to absolute space" + (option("--family") % "Specify the family ID of the file to load" & - family_id("family_id").set(settings.family_id) % "family id to use for load").force_expand_help(true) + + family_id("family_id").set(settings.family_id) % "family ID to use for load").force_expand_help(true) + (option('p', "--partition") % "Specify the partition to load into" & integer("partition").set(settings.load.partition) % "partition to load into").force_expand_help(true) + option('n', "--no-overwrite").set(settings.load.no_overwrite) % "When writing flash data, do not overwrite an existing program in flash. If picotool cannot determine the size/presence of the program in flash, the command fails" + @@ -850,7 +850,7 @@ struct partition_info_command : public cmd { group get_cli() override { return ( - (option('m', "--family") & family_id("family_id").set(settings.family_id)) % "family id (will show target partition for said family)" + + (option('m', "--family") & family_id("family_id").set(settings.family_id)) % "family ID (will show target partition for said family)" + device_selection % "Target device selection" ); } @@ -879,7 +879,7 @@ struct partition_create_command : public cmd { (option('o', "--offset").set(settings.offset_set) % "Specify the load address for UF2 file output" & hex("offset").set(settings.offset) % "Load offset (memory address; default 0x10000000)").force_expand_help(true) + (option("--family") % "Specify the family if for UF2 file output" & - family_id("family_id").set(settings.family_id) % "family id for UF2 (default absolute)").force_expand_help(true) + family_id("family_id").set(settings.family_id) % "family ID for UF2 (default absolute)").force_expand_help(true) ).min(0).force_expand_help(true) % "UF2 output options") + optional_typed_file_selection_x("bootloader", 2, "elf") % "embed partition table into bootloader ELF" + ( @@ -1175,7 +1175,7 @@ struct uf2_convert_command : public cmd { hex("offset").set(settings.offset) % "Load offset (memory address; default 0x10000000 for BIN file)" ).force_expand_help(true) % "Packaging Options" + ( - option("--family") & family_id("family_id").set(settings.family_id) % "family id for UF2" + option("--family") & family_id("family_id").set(settings.family_id) % "family ID for UF2" ).force_expand_help(true) % "UF2 Family options" #if SUPPORT_A2 + ( @@ -2636,10 +2636,11 @@ void build_rmap_elf(std::shared_ptrfile, range_map& rmap) } } -void build_rmap_uf2(std::shared_ptrfile, range_map& rmap) { +uint32_t build_rmap_uf2(std::shared_ptrfile, range_map& rmap, uint32_t family_id=0) { 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()) { @@ -2649,7 +2650,8 @@ void build_rmap_uf2(std::shared_ptrfile, range_map& rmap) 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) { + !(block.flags & UF2_FLAG_NOT_MAIN_FLASH) && block.payload_size == PAGE_SIZE && + (!family_id || block.file_size == family_id)) { #if SUPPORT_A2 // ignore the absolute block, but save the address if (check_abs_block(block)) { @@ -2657,14 +2659,28 @@ void build_rmap_uf2(std::shared_ptrfile, range_map& rmap) settings.uf2.abs_block_loc = block.target_addr; } else { rmap.insert(range(block.target_addr, block.target_addr + PAGE_SIZE), pos + offsetof(uf2_block, data[0])); + family_id = block.file_size; + next_family_id = 0; } #else rmap.insert(range(block.target_addr, block.target_addr + PAGE_SIZE), pos + offsetof(uf2_block, data[0])); + family_id = block.file_size; + next_family_id = 0; + #endif + } else if (block.file_size != family_id && family_id && !next_family_id) { + #if SUPPORT_A2 + if (!check_abs_block(block)) { + #endif + next_family_id = block.file_size; + #if SUPPORT_A2 + } #endif } } pos += sizeof(uf2_block); } while (true); + + return next_family_id; } uint32_t find_binary_start(range_map& rmap) { @@ -2688,9 +2704,11 @@ uint32_t find_binary_start(range_map& rmap) { return binary_start; } -template ACCESS get_iostream_memory_access(std::shared_ptr file, filetype type, bool writeable = false) { +template ACCESS get_iostream_memory_access(std::shared_ptr file, filetype type, bool writeable = false, uint32_t *next_family_id=nullptr) { range_map rmap; uint32_t binary_start = 0; + uint32_t tmp = 0; + if (next_family_id != nullptr) tmp = *next_family_id; switch (type) { case filetype::bin: file->seekg(0, std::ios::end); @@ -2702,7 +2720,12 @@ template ACCESS get_iostream_memory_access(st binary_start = find_binary_start(rmap); break; case filetype::uf2: - build_rmap_uf2(file, rmap); + tmp = build_rmap_uf2(file, rmap, tmp); + if (next_family_id != nullptr) { + *next_family_id = tmp; + } else if (tmp) { + fos << "WARNING: Multiple family IDs in a single UF2 file - only using first one\n"; + } binary_start = find_binary_start(rmap); break; default: @@ -2717,11 +2740,11 @@ template ACCESS get_iostream_memory_access(st return ACCESS(file, rmap, binary_start); } -file_memory_access get_file_memory_access(uint8_t idx, bool writeable = false) { +file_memory_access get_file_memory_access(uint8_t idx, bool writeable = false, uint32_t *next_family_id=nullptr) { ios::openmode mode = (writeable ? ios::out|ios::in : ios::in)|ios::binary; auto file = get_file_idx(mode, idx); try { - return get_iostream_memory_access(file, get_file_type_idx(idx), writeable); + return get_iostream_memory_access(file, get_file_type_idx(idx), writeable, next_family_id); } catch (std::exception&) { file->close(); throw; @@ -3534,23 +3557,23 @@ uint32_t get_access_family_id(memory_access &file_access) { uint32_t checksum = file_access.read_int(FLASH_START + 252); if (checksum == calc_checksum(checksum_data)) { // Checksum is correct, so RP2040 - DEBUG_LOG("Detected family id %s due to boot2 checksum\n", family_name(RP2040_FAMILY_ID).c_str()); + DEBUG_LOG("Detected family ID %s due to boot2 checksum\n", family_name(RP2040_FAMILY_ID).c_str()); return RP2040_FAMILY_ID; } else { // Checksum incorrect, so absolute - DEBUG_LOG("Assumed family id %s\n", family_name(ABSOLUTE_FAMILY_ID).c_str()); + DEBUG_LOG("Assumed family ID %s\n", family_name(ABSOLUTE_FAMILY_ID).c_str()); return ABSOLUTE_FAMILY_ID; } } else { // no_flash RP2040 binaries have no checksum - DEBUG_LOG("Assumed family id %s\n", family_name(RP2040_FAMILY_ID).c_str()); + DEBUG_LOG("Assumed family ID %s\n", family_name(RP2040_FAMILY_ID).c_str()); return RP2040_FAMILY_ID; } } auto first_item = best_block->items[0].get(); if (first_item->type() != PICOBIN_BLOCK_ITEM_1BS_IMAGE_TYPE) { // This will apply for partition tables - DEBUG_LOG("Assumed family id %s due to block with no IMAGE_DEF\n", family_name(ABSOLUTE_FAMILY_ID).c_str()); + DEBUG_LOG("Assumed family ID %s due to block with no IMAGE_DEF\n", family_name(ABSOLUTE_FAMILY_ID).c_str()); return ABSOLUTE_FAMILY_ID; } auto image_def = dynamic_cast(first_item); @@ -3606,7 +3629,7 @@ uint32_t get_family_id(uint8_t file_idx) { // todo this can be done - need to add block search for bin files fail(ERROR_FORMAT, "Cannot autodetect UF2 family - must specify the family\n"); } - DEBUG_LOG("Detected family id %s\n", family_name(family_id).c_str());; + DEBUG_LOG("Detected family ID %s\n", family_name(family_id).c_str());; return family_id; } @@ -3728,7 +3751,8 @@ bool config_command::execute(device_map &devices) { bool info_command::execute(device_map &devices) { fos.first_column(0); fos.hanging_indent(0); if (!settings.filenames[0].empty()) { - auto access = get_file_memory_access(0); + uint32_t next_id = 0; + auto access = get_file_memory_access(0, false, &next_id); uint32_t id = 0; id = get_family_id(0); if (id == RP2040_FAMILY_ID) { @@ -3736,8 +3760,29 @@ bool info_command::execute(device_map &devices) { } else if (id >= RP2350_ARM_S_FAMILY_ID && id <= RP2350_ARM_NS_FAMILY_ID) { access.set_model(rp2350); } - fos << "File " << settings.filenames[0] << ":\n\n"; - info_guts(access, nullptr); + if (next_id) { + next_id = id; + while (next_id) { + fos.first_column(0); fos.hanging_indent(0); + std::stringstream s; + s << "File " << settings.filenames[0] << " family ID " << family_name(next_id) << ":"; + if (next_id != id) { + string dashes; + std::generate_n(std::back_inserter(dashes), s.str().length() + 1, [] { return '-'; }); + fos << "\n" << dashes << "\n"; + } + fos << s.str() << "\n\n"; + auto tmp_access = get_file_memory_access(0, false, &next_id); + info_guts(tmp_access, nullptr); + } + } else { + if (get_file_type() == filetype::uf2) { + fos << "File " << settings.filenames[0] << " family ID " << family_name(id) << ":\n\n"; + } else { + fos << "File " << settings.filenames[0] << ":\n\n"; + } + info_guts(access, nullptr); + } return false; } #if HAS_LIBUSB @@ -4076,13 +4121,13 @@ bool get_target_partition(picoboot::connection &con, uint32_t* start = nullptr, con.get_info(&cmd, loc_flags_id_buf, sizeof(loc_flags_id_buf)); assert(loc_flags_id_buf_32[0] == 3); if ((int)loc_flags_id_buf_32[1] < 0) { - printf("Family id %s cannot be downloaded anywhere\n", family_name(settings.family_id).c_str()); + printf("Family ID %s cannot be downloaded anywhere\n", family_name(settings.family_id).c_str()); return false; } else { if (loc_flags_id_buf_32[1] == PARTITION_TABLE_NO_PARTITION_INDEX) { - printf("Family id %s can be downloaded in absolute space:\n", family_name(settings.family_id).c_str()); + printf("Family ID %s can be downloaded in absolute space:\n", family_name(settings.family_id).c_str()); } else { - printf("Family id %s can be downloaded in partition %d:\n", family_name(settings.family_id).c_str(), loc_flags_id_buf_32[1]); + printf("Family ID %s can be downloaded in partition %d:\n", family_name(settings.family_id).c_str(), loc_flags_id_buf_32[1]); } uint32_t location_and_permissions = loc_flags_id_buf_32[2]; uint32_t saddr = ((location_and_permissions >> PICOBIN_PARTITION_LOCATION_FIRST_SECTOR_LSB) & 0x1fffu) * 4096; From 818d3bcf51b63ec40908442a7987e07b70352e03 Mon Sep 17 00:00:00 2001 From: will-v-pi <108662275+will-v-pi@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:33:09 +0100 Subject: [PATCH 29/67] Add verify option to picotool save (#125) Fixes #113 --- README.md | 19 +++++++---- main.cpp | 100 +++++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 86 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 3366195..877e260 100644 --- a/README.md +++ b/README.md @@ -125,9 +125,9 @@ SYNOPSIS: picotool encrypt [--quiet] [--verbose] [--hash] [--sign] [-t ] [-o ] [-t ] [-t ] [] [-t ] picotool seal [--quiet] [--verbose] [--hash] [--sign] [--clear] [-t ] [-o ] [-t ] [] [-t ] [] [-t ] [--major ] [--minor ] [--rollback [..]] picotool link [--quiet] [--verbose] [-t ] [-t ] [-t ] [] [-t ] [-p] - picotool save [-p] [device-selection] - picotool save -a [device-selection] - picotool save -r [device-selection] + picotool save [-p] [-v] [--family ] [device-selection] + picotool save -a [-v] [--family ] [device-selection] + picotool save -r [-v] [--family ] [device-selection] picotool erase [-a] [device-selection] picotool erase [-p ] [device-selection] picotool erase -r [device-selection] @@ -444,9 +444,9 @@ SAVE: Save the program / memory stored in flash on the device to a file. SYNOPSIS: - picotool save [-p] [device-selection] - picotool save -a [device-selection] - picotool save -r [device-selection] + picotool save [-p] [-v] [--family ] [device-selection] + picotool save -a [-v] [--family ] [device-selection] + picotool save -r [-v] [--family ] [device-selection] OPTIONS: Selection of data to save @@ -461,6 +461,13 @@ OPTIONS: The lower address bound in hex The upper address bound in hex + Other + -v, --verify + Verify the data was saved correctly + --family + Specify the family ID to save the file as + + family id to save file as Source device selection --bus Filter devices by USB bus number diff --git a/main.cpp b/main.cpp index 9616184..b12e5cf 100644 --- a/main.cpp +++ b/main.cpp @@ -459,6 +459,7 @@ struct _settings { struct { bool all = false; + bool verify = false; } save; struct { @@ -673,6 +674,7 @@ struct save_command : public cmd { hex("to").set(settings.to) % "The upper address bound in hex" ).min(0).doc_non_optional(true) ).min(0).doc_non_optional(true).no_match_beats_error(false) % "Selection of data to save" + + option('v', "--verify").set(settings.save.verify) % "Verify the data was saved correctly" + (option("--family") % "Specify the family ID to save the file as" & family_id("family_id").set(settings.family_id) % "family ID to save file as").force_expand_help(true) + ( // note this parenthesis seems to help with error messages for say save --foo @@ -3896,6 +3898,33 @@ struct progress_bar { }; #if HAS_LIBUSB +vector get_coalesced_ranges(iostream_memory_access &file_access, model_t model) { + auto rmap = file_access.get_rmap(); + auto ranges = rmap.ranges(); + std::sort(ranges.begin(), ranges.end(), [](const range& a, const range &b) { + return a.from < b.from; + }); + // coalesce all the contiguous ranges + for(auto i = ranges.begin(); i < ranges.end(); ) { + if (i != ranges.end() - 1) { + uint32_t erase_size; + // we want to coalesce flash sectors together (this ends up creating ranges that may have holes) + if( get_memory_type(i->from, model) == flash ) { + erase_size = FLASH_SECTOR_ERASE_SIZE; + } else { + erase_size = 1; + } + if (i->to / erase_size == (i+1)->from / erase_size) { + i->to = (i+1)->to; + i = ranges.erase(i+1) - 1; + continue; + } + } + i++; + } + return ranges; +} + bool save_command::execute(device_map &devices) { auto con = get_single_bootsel_device_connection(devices); picoboot_memory_access raw_access(con); @@ -4018,6 +4047,50 @@ bool save_command::execute(device_map &devices) { throw; } } + + if (settings.save.verify) { + auto file_access = get_file_memory_access(0); + model_t model = get_model(raw_access); + auto ranges = get_coalesced_ranges(file_access, model); + for (auto mem_range : ranges) { + enum memory_type type = get_memory_type(mem_range.from, model); + bool ok = true; + { + progress_bar bar("Verifying " + memory_names[type] + ": "); + uint32_t batch_size = FLASH_SECTOR_ERASE_SIZE; + vector file_buf; + vector device_buf; + uint32_t pos = mem_range.from; + for (uint32_t base = mem_range.from; base < mem_range.to && ok; base += batch_size) { + uint32_t this_batch = std::min(std::min(mem_range.to, end) - base, batch_size); + // note we pass zero_fill = true in case the file has holes, but this does + // mean that the verification will fail if those holes are not filled with zeros + // on the device + file_access.read_into_vector(base, this_batch, file_buf, true); + raw_access.read_into_vector(base, this_batch, device_buf); + assert(file_buf.size() == device_buf.size()); + for (unsigned int i = 0; i < this_batch; i++) { + if (file_buf[i] != device_buf[i]) { + pos = base + i; + printf("Unmatch file %x, device %x, pos %x\n", file_buf[i], device_buf[i], pos); + ok = false; + break; + } + } + if (ok) { + pos = base + this_batch; + } + bar.progress(pos - mem_range.from, mem_range.to - mem_range.from); + } + } + if (ok) { + std::cout << " OK\n"; + } else { + std::cout << " FAILED\n"; + fail(ERROR_VERIFICATION_FAILED, "The device contents did not match the saved file"); + } + } + } return false; } @@ -4081,33 +4154,6 @@ bool erase_command::execute(device_map &devices) { #endif #if HAS_LIBUSB -vector get_coalesced_ranges(iostream_memory_access &file_access, model_t model) { - auto rmap = file_access.get_rmap(); - auto ranges = rmap.ranges(); - std::sort(ranges.begin(), ranges.end(), [](const range& a, const range &b) { - return a.from < b.from; - }); - // coalesce all the contiguous ranges - for(auto i = ranges.begin(); i < ranges.end(); ) { - if (i != ranges.end() - 1) { - uint32_t erase_size; - // we want to coalesce flash sectors together (this ends up creating ranges that may have holes) - if( get_memory_type(i->from, model) == flash ) { - erase_size = FLASH_SECTOR_ERASE_SIZE; - } else { - erase_size = 1; - } - if (i->to / erase_size == (i+1)->from / erase_size) { - i->to = (i+1)->to; - i = ranges.erase(i+1) - 1; - continue; - } - } - i++; - } - return ranges; -} - bool get_target_partition(picoboot::connection &con, uint32_t* start = nullptr, uint32_t* end = nullptr) { #if SUPPORT_A2 con.exit_xip(); From 9fa08571cb9f010a5bd4492e59a1a6fa30fedaa3 Mon Sep 17 00:00:00 2001 From: will-v-pi <108662275+will-v-pi@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:33:37 +0100 Subject: [PATCH 30/67] Replace .dll.a libusb with .a (#126) --- cmake/FindLIBUSB.cmake | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmake/FindLIBUSB.cmake b/cmake/FindLIBUSB.cmake index ab0d303..85825aa 100644 --- a/cmake/FindLIBUSB.cmake +++ b/cmake/FindLIBUSB.cmake @@ -56,5 +56,10 @@ else (LIBUSB_INCLUDE_DIR AND LIBUSB_LIBRARIES) include(FindPackageHandleStandardArgs) FIND_PACKAGE_HANDLE_STANDARD_ARGS(LIBUSB DEFAULT_MSG LIBUSB_LIBRARIES LIBUSB_INCLUDE_DIR) + + # Don't use .dll.a libraries, as they require the .dll file to be in the correct location + # Replace with .a for static linking instead + string(REPLACE ".dll.a" ".a" LIBUSB_LIBRARIES ${LIBUSB_LIBRARIES}) + MARK_AS_ADVANCED(LIBUSB_INCLUDE_DIR LIBUSB_LIBRARIES) endif (LIBUSB_INCLUDE_DIR AND LIBUSB_LIBRARIES) From bf33c6ddd756c3961c7a13b000859b71e825ee74 Mon Sep 17 00:00:00 2001 From: David Grayson Date: Mon, 2 Sep 2024 03:39:45 -0700 Subject: [PATCH 31/67] README.md: Improve the MSYS2 instructions (#128) It's better to let MSYS2 use its default CMake generator, which is Ninja. The instructions assumed that Makefiles were being used, because they said "make install", but we don't want to use Make on MSYS2, so I fixed that. --- README.md | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 29231b9..6b4d384 100644 --- a/README.md +++ b/README.md @@ -66,32 +66,23 @@ No need to download libusb separately or set `LIBUSB_ROOT`. pacman -S $MINGW_PACKAGE_PREFIX-{toolchain,cmake,libusb} mkdir build cd build -MSYS2_ARG_CONV_EXCL=- cmake .. -G"MSYS Makefiles" -DCMAKE_INSTALL_PREFIX=$MINGW_PREFIX -make -make install DESTDIR=/ # optional +cmake .. -DCMAKE_INSTALL_PREFIX=$MINGW_PREFIX +cmake --build . ``` ## Usage by the Raspberry Pi Pico SDK -The Raspberry Pi Pico SDK ([pico-sdk](https://github.com/raspberrypi/pico-sdk)) version 2.0.0 and above, uses `picotool` to do the ELF->UF2 conversion previously handled by the `elf2uf2` tool in the SDK. `picootol` is also used by the SDK for hashing and/or signing binaries. +The Raspberry Pi Pico SDK ([pico-sdk](https://github.com/raspberrypi/pico-sdk)) version 2.0.0 and above uses `picotool` to do the ELF-to-UF2 conversion previously handled by the `elf2uf2` tool in the SDK. The SDK also uses `picotool` to hash and sign binaries. -Whilst the SDK can download picotool on its own per project, if you have multiple projects or build configurations, it is preferable to install a single copy of `picotool` locally. +Whilst the SDK can download picotool on its own per project, if you have multiple projects or build configurations, it is preferable to install a single copy of `picotool` locally. This can be done most simply with `make install` or `cmake --install .`; the SDK will use this installed version by default. -This can be done most simply with `make install`; the SDK will use this installed version by default. - -Alternatively you can install in a custom path via: +Alternatively, you can install to a custom path via: ``` cmake -DCMAKE_INSTALL_PREFIX=$MY_INSTALL_DIR -DPICOTOOL_FLAT_INSTALL=1 .. ``` -In order for the SDK to find `picotool` in this custom path, you will need to set the `picotool_DIR` variable in your project, either by passing to `-Dpicotool_DIR=$MY_INSTALL_DIR/picotool` to your SDK `cmake` command, or by adding - -```CMake -set(picotool_DIR $MY_INSTALL_DIR/picotool) -``` - -to your CMakeLists.txt file. +In order for the SDK to find `picotool` in this custom path, you will need to set the `picotool_DIR` variable in your project, either by setting the `picotool_DIR` environment variable, by passing `-Dpicotool_DIR=$MY_INSTALL_DIR/picotool` to your `cmake` command, or by adding `set(picotool_DIR $MY_INSTALL_DIR/picotool)` to your CMakeLists.txt file. ## Overview From 1721716c5e75f4e5fa7e946e8dd2d859ad71688e Mon Sep 17 00:00:00 2001 From: Koji KITAYAMA <45088311+kkitayam@users.noreply.github.com> Date: Mon, 2 Sep 2024 19:44:10 +0900 Subject: [PATCH 32/67] Fix compile errors when using clang-x86_64-pc-windows-msvc (#129) --- main.cpp | 2 +- otp_header_parser/otp_header_parse.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/main.cpp b/main.cpp index b12e5cf..1c927b6 100644 --- a/main.cpp +++ b/main.cpp @@ -57,7 +57,7 @@ #endif // missing __builtins on windows -#ifdef _MSC_VER +#if defined(_MSC_VER) && !defined(__clang__) # include # define __builtin_popcount __popcnt static __forceinline int __builtin_ctz(unsigned x) { diff --git a/otp_header_parser/otp_header_parse.cpp b/otp_header_parser/otp_header_parse.cpp index 04ead13..00e924c 100644 --- a/otp_header_parser/otp_header_parse.cpp +++ b/otp_header_parser/otp_header_parse.cpp @@ -15,7 +15,7 @@ #include "nlohmann/json.hpp" // missing __builtins on windows -#ifdef _MSC_VER +#if defined(_MSC_VER) && !defined(__clang__) # include # define __builtin_popcount __popcnt static __forceinline int __builtin_ctz(unsigned x) { From 971ee85176dd17a3bf43713cc96cd52e24b449f6 Mon Sep 17 00:00:00 2001 From: Andrew Scheller Date: Tue, 3 Sep 2024 22:20:39 +0100 Subject: [PATCH 33/67] Use 4-space indent instead of 8-space indent (#132) For commonality with other CMakeLists.txt files --- xip_ram_perms/CMakeLists.txt | 60 ++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/xip_ram_perms/CMakeLists.txt b/xip_ram_perms/CMakeLists.txt index 524921f..9031289 100644 --- a/xip_ram_perms/CMakeLists.txt +++ b/xip_ram_perms/CMakeLists.txt @@ -1,42 +1,42 @@ cmake_minimum_required(VERSION 3.12) if (NOT USE_PRECOMPILED) - set(PICO_PLATFORM rp2350-arm-s) + set(PICO_PLATFORM rp2350-arm-s) - # Pull in SDK (must be before project) - include(${PICO_SDK_PATH}/external/pico_sdk_import.cmake) + # Pull in SDK (must be before project) + include(${PICO_SDK_PATH}/external/pico_sdk_import.cmake) - project(xip_ram_perms C CXX ASM) - set(CMAKE_C_STANDARD 11) - set(CMAKE_CXX_STANDARD 17) + project(xip_ram_perms C CXX ASM) + set(CMAKE_C_STANDARD 11) + set(CMAKE_CXX_STANDARD 17) - if (PICO_SDK_VERSION_STRING VERSION_LESS "2.0.0") - message(FATAL_ERROR "Raspberry Pi Pico SDK version 2.0.0 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}") - endif() + if (PICO_SDK_VERSION_STRING VERSION_LESS "2.0.0") + message(FATAL_ERROR "Raspberry Pi Pico SDK version 2.0.0 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}") + endif() - # Initialize the SDK - pico_sdk_init() + # Initialize the SDK + pico_sdk_init() - # XIP Ram OTP Perm Setter - add_executable(xip_ram_perms - set_perms.c - ) + # XIP Ram OTP Perm Setter + add_executable(xip_ram_perms + set_perms.c + ) - target_link_libraries(xip_ram_perms - pico_stdlib - ) + target_link_libraries(xip_ram_perms + pico_stdlib + ) - pico_set_binary_type(xip_ram_perms no_flash) - # create linker script to run from 0x20070000 - file(READ ${PICO_LINKER_SCRIPT_PATH}/memmap_no_flash.ld LINKER_SCRIPT) - string(REPLACE "RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 512k" "RAM(rwx) : ORIGIN = 0x13ffc000, LENGTH = 16k" LINKER_SCRIPT "${LINKER_SCRIPT}") - file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/memmap_xip_ram.ld "${LINKER_SCRIPT}") - pico_set_linker_script(xip_ram_perms ${CMAKE_CURRENT_BINARY_DIR}/memmap_xip_ram.ld) + pico_set_binary_type(xip_ram_perms no_flash) + # create linker script to run from 0x20070000 + file(READ ${PICO_LINKER_SCRIPT_PATH}/memmap_no_flash.ld LINKER_SCRIPT) + string(REPLACE "RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 512k" "RAM(rwx) : ORIGIN = 0x13ffc000, LENGTH = 16k" LINKER_SCRIPT "${LINKER_SCRIPT}") + file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/memmap_xip_ram.ld "${LINKER_SCRIPT}") + pico_set_linker_script(xip_ram_perms ${CMAKE_CURRENT_BINARY_DIR}/memmap_xip_ram.ld) else() - project(xip_ram_perms C CXX ASM) - message("Using precompiled xip_ram_perms.elf") - configure_file(${CMAKE_CURRENT_LIST_DIR}/xip_ram_perms.elf ${CMAKE_CURRENT_BINARY_DIR}/xip_ram_perms.elf COPYONLY) - # Use manually specified variables - set(NULL ${CMAKE_MAKE_PROGRAM}) - set(NULL ${PICO_SDK_PATH}) + project(xip_ram_perms C CXX ASM) + message("Using precompiled xip_ram_perms.elf") + configure_file(${CMAKE_CURRENT_LIST_DIR}/xip_ram_perms.elf ${CMAKE_CURRENT_BINARY_DIR}/xip_ram_perms.elf COPYONLY) + # Use manually specified variables + set(NULL ${CMAKE_MAKE_PROGRAM}) + set(NULL ${PICO_SDK_PATH}) endif() From 3ff7c3e710f990bac96adb70d7e8c06b94ac0c63 Mon Sep 17 00:00:00 2001 From: will-v-pi <108662275+will-v-pi@users.noreply.github.com> Date: Mon, 9 Sep 2024 11:08:26 +0100 Subject: [PATCH 34/67] Add github actions compile test (#131) * Add build & test workflow * Ensure no picotool needed for xip_ram_perms and flash_id compilation * Still check LIBUSB_ROOT if pkgconfig libusb not found --- .github/workflows/choco_packages.config | 7 +++ .github/workflows/test.yml | 73 +++++++++++++++++++++++++ cmake/FindLIBUSB.cmake | 4 +- picoboot_flash_id/CMakeLists.txt | 2 + xip_ram_perms/CMakeLists.txt | 2 + 5 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/choco_packages.config create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/choco_packages.config b/.github/workflows/choco_packages.config new file mode 100644 index 0000000..b2d3db1 --- /dev/null +++ b/.github/workflows/choco_packages.config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..e79a342 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,73 @@ +on: + push: + pull_request: + +jobs: + build: + # Prevent running twice for PRs from same repo + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + name: Build & Test + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + generator: ["Ninja", "Unix Makefiles"] + mbedtls: ["mbedtls", ""] + libusb: ["libusb", ""] + compile: ["compile", ""] + exclude: + - os: 'windows-latest' + generator: "Unix Makefiles" + - libusb: "" + compile: "compile" + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install dependencies (Windows) + if: runner.os == 'Windows' + run: | + choco install -y .github/workflows/choco_packages.config + curl -L https://github.com/libusb/libusb/releases/download/v1.0.27/libusb-1.0.27.7z -o libusb.7z + 7z x libusb.7z -olibusb + - name: Set LIBUSB_ROOT (Windows) + if: runner.os == 'Windows' + shell: bash + run: echo "LIBUSB_ROOT=$(pwd)/libusb" >> "$GITHUB_ENV" + + - name: Install dependencies (macOS) + if: runner.os == 'macOS' + run: | + brew install cmake libusb pkg-config ninja + brew install --cask gcc-arm-embedded + + - name: Install dependencies (Linux) + if: runner.os == 'Linux' + run: sudo apt install cmake ninja-build python3 build-essential gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib libusb-1.0-0-dev + - name: Checkout Pico SDK + uses: actions/checkout@v4 + with: + repository: raspberrypi/pico-sdk + ref: develop + path: pico-sdk + submodules: ${{ !(!matrix.mbedtls) }} + + - name: Build and Install + run: | + cmake -S . -B build -G "${{ matrix.generator }}" -D PICO_SDK_PATH="${{ github.workspace }}/pico-sdk" ${{ !matrix.libusb && '-D PICOTOOL_NO_LIBUSB=1' || '' }} ${{ matrix.compile && '-D USE_PRECOMPILED=false' || '' }} + cmake --build build + ${{ runner.os != 'Windows' && 'sudo' || '' }} cmake --install build + - name: Add to path (Windows) + if: runner.os == 'Windows' + run: echo "C:\Program Files (x86)\picotool\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Test + run: | + picotool help + curl -L https://datasheets.raspberrypi.com/soft/blink.uf2 -o blink.uf2 + curl -L https://datasheets.raspberrypi.com/soft/hello_world.uf2 -o hello_world.uf2 + curl -L https://datasheets.raspberrypi.com/soft/flash_nuke.uf2 -o flash_nuke.uf2 + picotool info -a blink.uf2 + picotool info -a hello_world.uf2 + picotool info -a flash_nuke.uf2 diff --git a/cmake/FindLIBUSB.cmake b/cmake/FindLIBUSB.cmake index 85825aa..e5defef 100644 --- a/cmake/FindLIBUSB.cmake +++ b/cmake/FindLIBUSB.cmake @@ -25,7 +25,9 @@ else (LIBUSB_INCLUDE_DIR AND LIBUSB_LIBRARIES) if (PKG_CONFIG_FOUND) pkg_check_modules(PC_LIBUSB libusb-1.0) - else () + endif() + + if (NOT PC_LIBUSB_FOUND) # As the pkg-config was not found we are probably building under windows. # Determine the architecture of the host, to choose right library if (NOT DEFINED ARCHITECTURE) diff --git a/picoboot_flash_id/CMakeLists.txt b/picoboot_flash_id/CMakeLists.txt index 9110f8d..efbaafb 100644 --- a/picoboot_flash_id/CMakeLists.txt +++ b/picoboot_flash_id/CMakeLists.txt @@ -1,6 +1,8 @@ cmake_minimum_required(VERSION 3.12) if (NOT USE_PRECOMPILED) + set(PICO_NO_PICOTOOL 1) + # default build type set(CMAKE_BUILD_TYPE "MinSizeRel" CACHE STRING "build type") diff --git a/xip_ram_perms/CMakeLists.txt b/xip_ram_perms/CMakeLists.txt index 9031289..15477c0 100644 --- a/xip_ram_perms/CMakeLists.txt +++ b/xip_ram_perms/CMakeLists.txt @@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.12) if (NOT USE_PRECOMPILED) set(PICO_PLATFORM rp2350-arm-s) + + set(PICO_NO_PICOTOOL 1) # Pull in SDK (must be before project) include(${PICO_SDK_PATH}/external/pico_sdk_import.cmake) From d0d0f29af3745cce17b509c7ae22ec86d19a785c Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Mon, 9 Sep 2024 16:58:32 +0100 Subject: [PATCH 35/67] Remove debug info from xip_ram_perms.elf (#139) This embedded local paths of the build machine into the elf file, which was undesirable behaviour --- CMakeLists.txt | 2 ++ xip_ram_perms/xip_ram_perms.elf | Bin 342480 -> 35716 bytes 2 files changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index cd9537b..bd98861 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,6 +70,7 @@ if (NOT PICOTOOL_NO_LIBUSB) "-DCMAKE_MAKE_PROGRAM:FILEPATH=${CMAKE_MAKE_PROGRAM}" "-DPICO_SDK_PATH:FILEPATH=${PICO_SDK_PATH}" "-DUSE_PRECOMPILED:BOOL=${USE_PRECOMPILED}" + "-DPICO_DEBUG_INFO_IN_RELEASE=OFF" BUILD_ALWAYS 1 # todo remove this INSTALL_COMMAND "" ) @@ -93,6 +94,7 @@ if (NOT PICOTOOL_NO_LIBUSB) "-DCMAKE_MAKE_PROGRAM:FILEPATH=${CMAKE_MAKE_PROGRAM}" "-DPICO_SDK_PATH:FILEPATH=${PICO_SDK_PATH}" "-DUSE_PRECOMPILED:BOOL=${USE_PRECOMPILED}" + "-DPICO_DEBUG_INFO_IN_RELEASE=OFF" BUILD_ALWAYS 1 # todo remove this INSTALL_COMMAND "" ) diff --git a/xip_ram_perms/xip_ram_perms.elf b/xip_ram_perms/xip_ram_perms.elf index ae083cd62c1984f36c936081c7fb5dbd267b9c76..5fd950ea0a8901cb5c9e129156c03fac1a34d8e2 100755 GIT binary patch delta 11986 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

tjiw^Y874r&r%V2iPm}y;h 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 G1^FLz!4LNU From 08bbcf7b427b950e70db731037591dd31002bf86 Mon Sep 17 00:00:00 2001 From: armandomontanez Date: Tue, 10 Sep 2024 16:44:02 -0700 Subject: [PATCH 36/67] Fix Bazel build breakages (#136) * Fix Bazel build breakages Fixes some build breakages related to changes to the Pico SDK structure and the addition of picoboot_flash_id. * Add TODO for building flash_id.bin from source in Bazel --- BUILD.bazel | 7 +++++++ bazel/mbedtls.BUILD | 2 +- picoboot_connection/BUILD.bazel | 2 ++ picoboot_flash_id/BUILD.bazel | 8 ++++++++ 4 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 picoboot_flash_id/BUILD.bazel diff --git a/BUILD.bazel b/BUILD.bazel index 2bc5b2a..05b355f 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -15,6 +15,13 @@ picotool_binary_data_header( out = "xip_ram_perms_elf.h", ) +# TODO: Make it possible to build the prebuilt from source. +picotool_binary_data_header( + name = "flash_id_bin", + src = "//picoboot_flash_id:picoboot_flash_id_prebuilt", + out = "flash_id_bin.h", +) + cc_library( name = "xip_ram_perms", srcs = ["xip_ram_perms.cpp"], diff --git a/bazel/mbedtls.BUILD b/bazel/mbedtls.BUILD index c3342b1..1c9fa43 100644 --- a/bazel/mbedtls.BUILD +++ b/bazel/mbedtls.BUILD @@ -9,10 +9,10 @@ cc_library( "library/*.h", ], ), + includes = ["include"], linkopts = select({ "@rules_cc//cc/compiler:msvc-cl": ["-DEFAULTLIB:AdvAPI32.Lib"], "//conditions:default": [], }), - includes = ["include"], deps = ["@picotool//lib:mbedtls_config"], ) diff --git a/picoboot_connection/BUILD.bazel b/picoboot_connection/BUILD.bazel index 20437b4..fd8b15b 100644 --- a/picoboot_connection/BUILD.bazel +++ b/picoboot_connection/BUILD.bazel @@ -9,6 +9,7 @@ cc_library( hdrs = [ "picoboot_connection.h", "picoboot_connection_cxx.h", + "//:flash_id_bin.h", ], defines = ["HAS_LIBUSB=1"], # Bazel build always has libusb. includes = ["."], @@ -16,6 +17,7 @@ cc_library( "//elf", "@libusb", "@pico-sdk//src/common/boot_picoboot_headers", + "@pico-sdk//src/rp2_common/boot_bootrom_headers", "@pico-sdk//src/rp2_common/pico_bootrom:pico_bootrom_headers", ], ) diff --git a/picoboot_flash_id/BUILD.bazel b/picoboot_flash_id/BUILD.bazel new file mode 100644 index 0000000..ace33ad --- /dev/null +++ b/picoboot_flash_id/BUILD.bazel @@ -0,0 +1,8 @@ +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "picoboot_flash_id_prebuilt", + srcs = ["flash_id.bin"], +) + +# TODO: Make it possible to build flash_id.bin from source. From fb9a4f0d307bb8ba1d7f589b5c64049e65db49a4 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Fri, 13 Sep 2024 13:48:41 +0100 Subject: [PATCH 37/67] Fix "Manually-specified variables were not used by the project" warning Add PICO_DEBUG_INFO_IN_RELEASE to the used variables in xip_ram_perms and picoboot_flash_id --- picoboot_flash_id/CMakeLists.txt | 1 + xip_ram_perms/CMakeLists.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/picoboot_flash_id/CMakeLists.txt b/picoboot_flash_id/CMakeLists.txt index efbaafb..199fdfc 100644 --- a/picoboot_flash_id/CMakeLists.txt +++ b/picoboot_flash_id/CMakeLists.txt @@ -24,4 +24,5 @@ else() # Use manually specified variables set(NULL ${CMAKE_MAKE_PROGRAM}) set(NULL ${PICO_SDK_PATH}) + set(NULL ${PICO_DEBUG_INFO_IN_RELEASE}) endif() diff --git a/xip_ram_perms/CMakeLists.txt b/xip_ram_perms/CMakeLists.txt index 15477c0..1faeb05 100644 --- a/xip_ram_perms/CMakeLists.txt +++ b/xip_ram_perms/CMakeLists.txt @@ -41,4 +41,5 @@ else() # Use manually specified variables set(NULL ${CMAKE_MAKE_PROGRAM}) set(NULL ${PICO_SDK_PATH}) + set(NULL ${PICO_DEBUG_INFO_IN_RELEASE}) endif() From 333a03b8196c706f3bcddf6e46781ef5457dad69 Mon Sep 17 00:00:00 2001 From: Andrew Gordon <47662932+arg08@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:38:33 +0100 Subject: [PATCH 38/67] Fix compilation on FreeBSD 13.2 and later. (#133) The main change is to portable_endian.h which seems to be out-of-date and not maintained upstream. This change also impacts OpenBSD, but a check of current OpenBSD git repository suggests it is correct there also. Finally, is excluded in main.cpp (as it already was for __Apple__); this will probably not be needed in later FreeBSD releases once has been picked up from more recent llvm. --- elf/portable_endian.h | 25 ++++++++++--------------- main.cpp | 2 +- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/elf/portable_endian.h b/elf/portable_endian.h index 4286f91..347dd50 100644 --- a/elf/portable_endian.h +++ b/elf/portable_endian.h @@ -43,27 +43,22 @@ # define __LITTLE_ENDIAN LITTLE_ENDIAN # define __PDP_ENDIAN PDP_ENDIAN -#elif defined(__OpenBSD__) - -# include - -# define __BYTE_ORDER BYTE_ORDER -# define __BIG_ENDIAN BIG_ENDIAN -# define __LITTLE_ENDIAN LITTLE_ENDIAN -# define __PDP_ENDIAN PDP_ENDIAN - -#elif defined(__NetBSD__) || defined(__FreeBSD__) || defined(__DragonFly__) +#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__FreeBSD__) || defined(__DragonFly__) # include +#ifndef be16toh # define be16toh(x) betoh16(x) +#endif +#ifndef le16toh # define le16toh(x) letoh16(x) - +#endif +#ifndef be32toh # define be32toh(x) betoh32(x) +#endif +#ifndef le32toh # define le32toh(x) letoh32(x) - -# define be64toh(x) betoh64(x) -# define le64toh(x) letoh64(x) +#endif #elif defined(__WINDOWS__) @@ -169,4 +164,4 @@ #endif -#endif \ No newline at end of file +#endif diff --git a/main.cpp b/main.cpp index 1c927b6..de16e53 100644 --- a/main.cpp +++ b/main.cpp @@ -14,7 +14,7 @@ #include #include #include -#ifndef __APPLE__ +#if !defined(__APPLE__) && !defined(__FreeBSD__) #include #endif #include From 7e2f756a0071d72c88e5590b8cd3b05d0512e007 Mon Sep 17 00:00:00 2001 From: josch Date: Wed, 18 Sep 2024 17:40:02 +0100 Subject: [PATCH 39/67] {xip_ram_perms,picoboot_flash_id}/CMakeLists.txt: unset environment variables CFLAGS, CXXFLAGS and LDFLAGS (#140) If the user set these environment variables to influence the picotool build, unset them here so that they do not influence the pico-sdk build. This is especially required for flags that are not supported by arm-none-eabi compilers. --- picoboot_flash_id/CMakeLists.txt | 8 ++++++++ xip_ram_perms/CMakeLists.txt | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/picoboot_flash_id/CMakeLists.txt b/picoboot_flash_id/CMakeLists.txt index 199fdfc..1de78ae 100644 --- a/picoboot_flash_id/CMakeLists.txt +++ b/picoboot_flash_id/CMakeLists.txt @@ -6,6 +6,14 @@ if (NOT USE_PRECOMPILED) # default build type set(CMAKE_BUILD_TYPE "MinSizeRel" CACHE STRING "build type") + # If the user set these environment variables to influence the picotool + # build, unset them here so that they do not influence the pico-sdk + # build. This is especially required for flags that are not supported + # by arm-none-eabi compilers. + unset(ENV{CFLAGS}) + unset(ENV{CXXFLAGS}) + unset(ENV{LDFLAGS}) + include(${PICO_SDK_PATH}/external/pico_sdk_import.cmake) project(flash_id C CXX ASM) pico_sdk_init() diff --git a/xip_ram_perms/CMakeLists.txt b/xip_ram_perms/CMakeLists.txt index 1faeb05..601dc2c 100644 --- a/xip_ram_perms/CMakeLists.txt +++ b/xip_ram_perms/CMakeLists.txt @@ -5,6 +5,14 @@ if (NOT USE_PRECOMPILED) set(PICO_NO_PICOTOOL 1) + # If the user set these environment variables to influence the picotool + # build, unset them here so that they do not influence the pico-sdk + # build. This is especially required for flags that are not supported + # by arm-none-eabi compilers. + unset(ENV{CFLAGS}) + unset(ENV{CXXFLAGS}) + unset(ENV{LDFLAGS}) + # Pull in SDK (must be before project) include(${PICO_SDK_PATH}/external/pico_sdk_import.cmake) From afdfa928d2f409bdb549bfaf92d9ccf7ee720401 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Wed, 18 Sep 2024 18:09:31 +0100 Subject: [PATCH 40/67] Don't track RP2040 no_flash serial number when rebooting The RP2040 USB serial number is all EEs when running a no_flash binary, which will then change once booted into bootsel mode, so don't track it for the reboot Fixes #144 --- main.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/main.cpp b/main.cpp index de16e53..876bf27 100644 --- a/main.cpp +++ b/main.cpp @@ -7646,8 +7646,11 @@ int main(int argc, char **argv) { libusb_get_device_descriptor(to_reboot, &desc); char ser_str[128]; libusb_get_string_descriptor_ascii(to_reboot_handle, desc.iSerialNumber, (unsigned char*)ser_str, sizeof(ser_str)); - settings.ser = ser_str; - fos << "Tracking device serial number " << ser_str << " for reboot\n"; + if (strcmp(ser_str, "EEEEEEEEEEEEEEEE") != 0) { + // don't store EEs serial number, as that is an RP2040 running a no_flash binary + settings.ser = ser_str; + fos << "Tracking device serial number " << ser_str << " for reboot\n"; + } } reboot_device(to_reboot, to_reboot_handle, true, 1); From b62ead341f499532302b8e9f957fa0480fb09a9e Mon Sep 17 00:00:00 2001 From: will-v-pi <108662275+will-v-pi@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:44:21 +0100 Subject: [PATCH 41/67] Add support for encrypting elfs with section holes within segments (#150) --- elf/elf_file.cpp | 25 +++++++++++++++++++++++++ elf/elf_file.h | 2 ++ 2 files changed, 27 insertions(+) diff --git a/elf/elf_file.cpp b/elf/elf_file.cpp index 08988a1..febb318 100644 --- a/elf/elf_file.cpp +++ b/elf/elf_file.cpp @@ -230,6 +230,15 @@ void elf_file::flatten(void) { } idx++; } + + idx = 0; + for (const auto &ph : ph_entries) { + if (ph.filez) { + elf_bytes.resize(std::max(ph.offset + ph.filez, (uint32_t)elf_bytes.size())); + memcpy(&elf_bytes[ph.offset], &ph_data[idx][0], ph.filez); + } + idx++; + } if (verbose) printf("Elf file size %zu\n", elf_bytes.size()); } @@ -265,6 +274,18 @@ void elf_file::read_sh_data(void) { } } +void elf_file::read_ph_data(void) { + int ph_idx = 0; + ph_data.resize(eh.ph_num); + for (const auto &ph: ph_entries) { + if (ph.filez) { + ph_data[ph_idx].resize(ph.filez); + read_bytes(ph.offset, ph.filez, &ph_data[ph_idx][0]); + } + ph_idx++; + } +} + const std::string elf_file::section_name(uint32_t sh_name) const { if (!eh.sh_str_index || eh.sh_str_index > eh.sh_num) return ""; @@ -372,6 +393,7 @@ int elf_file::read_file(std::shared_ptr file) { read_sh(); } read_sh_data(); + read_ph_data(); } catch (const std::ios_base::failure &e) { std::cerr << "Failed to read elf file" << std::endl; @@ -418,6 +440,7 @@ void elf_file::content(const elf32_ph_entry &ph, const std::vector &con if (verbose) printf("Update segment content offset %x content size %zx physical size %x\n", ph.offset, content.size(), ph.filez); memcpy(&elf_bytes[ph.offset], &content[0], std::min(content.size(), (size_t) ph.filez)); read_sh_data(); // Extract the sections after modifying the content + read_ph_data(); } void elf_file::content(const elf32_sh_entry &sh, const std::vector &content) { @@ -426,6 +449,7 @@ void elf_file::content(const elf32_sh_entry &sh, const std::vector &con if (verbose) printf("Update section content offset %x content size %zx section size %x\n", sh.offset, content.size(), sh.size); memcpy(&elf_bytes[sh.offset], &content[0], std::min(content.size(), (size_t) sh.size)); read_sh_data(); // Extract the sections after modifying the content + read_ph_data(); } const elf32_ph_entry* elf_file::segment_from_physical_address(uint32_t paddr) { @@ -503,6 +527,7 @@ const elf32_ph_entry& elf_file::append_segment(uint32_t vaddr, uint32_t paddr, u sh_entries.push_back(sh); sh_data.push_back(std::vector(size)); ph_entries.back().offset = sh.offset; + ph_data.push_back(std::vector(size)); eh.sh_offset = sh.offset + sh.size; eh.sh_num++; diff --git a/elf/elf_file.h b/elf/elf_file.h index 03b3364..cd4e1f3 100644 --- a/elf/elf_file.h +++ b/elf/elf_file.h @@ -55,6 +55,7 @@ private: void read_ph(void); void read_sh(void); void read_sh_data(void); + void read_ph_data(void); void read_bytes(unsigned offset, unsigned length, void *dest); uint32_t append_section_name(const std::string &sh_name_str); void flatten(void); @@ -65,6 +66,7 @@ private: std::vector ph_entries; std::vector sh_entries; std::vector> sh_data; + std::vector> ph_data; bool verbose; }; int rp_check_elf_header(const elf32_header &eh); From 0d259e7f76fdd566349c3ab7674deb17c5c70fbe Mon Sep 17 00:00:00 2001 From: will-v-pi <108662275+will-v-pi@users.noreply.github.com> Date: Mon, 14 Oct 2024 17:33:50 +0100 Subject: [PATCH 42/67] Fix otp dump command, and otp get for lock rows (#153) --- main.cpp | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/main.cpp b/main.cpp index 876bf27..d342251 100644 --- a/main.cpp +++ b/main.cpp @@ -5281,14 +5281,21 @@ bool otp_get_command::execute(device_map &devices) { uint32_t corrected_val = 0; if (m.reg_row / OTP_PAGE_ROWS != last_page) { // todo pre-check page lock - // todo this is a bit inefficient; we should probably read a page at a time - // struct picoboot_otp_cmd otp_cmd = {0}; - last_page = m.reg_row / OTP_PAGE_ROWS; struct picoboot_otp_cmd otp_cmd; - otp_cmd.wRow = last_page * OTP_PAGE_ROWS; - otp_cmd.wRowCount = OTP_PAGE_ROWS; - otp_cmd.bEcc = 0; - con.otp_read(&otp_cmd, (uint8_t *)raw_buffer, sizeof(raw_buffer)); + if (m.reg_row / OTP_PAGE_ROWS >= 62) { + // Read individual rows for lock words + otp_cmd.wRow = m.reg_row; + otp_cmd.wRowCount = 1; + otp_cmd.bEcc = 0; + con.otp_read(&otp_cmd, (uint8_t *)&(raw_buffer[m.reg_row % OTP_PAGE_ROWS]), sizeof(raw_buffer[0])); + } else { + // Otherwise read a page at a time + last_page = m.reg_row / OTP_PAGE_ROWS; + otp_cmd.wRow = last_page * OTP_PAGE_ROWS; + otp_cmd.wRowCount = OTP_PAGE_ROWS; + otp_cmd.bEcc = 0; + con.otp_read(&otp_cmd, (uint8_t *)raw_buffer, sizeof(raw_buffer)); + } } if (m.reg_row != last_reg_row) { last_reg_row = m.reg_row; @@ -5432,10 +5439,10 @@ bool otp_dump_command::execute(device_map &devices) { otp_cmd.wRow = 0; otp_cmd.wRowCount = OTP_ROW_COUNT; otp_cmd.bEcc = settings.otp.ecc && !settings.otp.raw; - std::unique_ptr unique_raw_buffer(new uint32_t[otp_cmd.wRowCount / (otp_cmd.bEcc ? 2 : 1)]()); - uint32_t* raw_buffer = unique_raw_buffer.get(); + vector raw_buffer; + raw_buffer.resize(otp_cmd.wRowCount * (otp_cmd.bEcc ? 2 : 4)); picoboot_memory_access raw_access(con); - con.otp_read(&otp_cmd, (uint8_t *)raw_buffer, sizeof(raw_buffer)); + con.otp_read(&otp_cmd, raw_buffer.data(), raw_buffer.size()); fos.first_column(0); char buf[256]; for(int i=0;i Date: Thu, 17 Oct 2024 09:32:50 +0100 Subject: [PATCH 43/67] Use RP-series when referring to RP2040/RP2350 (#154) * Use correct device name when known, and RP-series if not known --- main.cpp | 75 +++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 25 deletions(-) diff --git a/main.cpp b/main.cpp index d342251..300115a 100644 --- a/main.cpp +++ b/main.cpp @@ -151,8 +151,16 @@ std::array, 12> pin_functions_rp2350{{ std::map otp_regs; #if HAS_LIBUSB -auto bus_device_string = [](struct libusb_device *device) { - return string("Device at bus ") + std::to_string(libusb_get_bus_number(device)) + ", address " + std::to_string(libusb_get_device_address(device)); +auto bus_device_string = [](struct libusb_device *device, model_t model) { + string bus_device; + if (model == rp2040) { + bus_device = string("RP2040 device at bus "); + } else if (model == rp2350) { + bus_device = string("RP2350 device at bus "); + } else { + bus_device = string("Device at bus "); + } + return bus_device + std::to_string(libusb_get_bus_number(device)) + ", address " + std::to_string(libusb_get_device_address(device)); }; #endif @@ -488,6 +496,7 @@ struct _settings { }; _settings settings; std::shared_ptr selected_cmd; +model_t selected_model = unknown; auto device_selection = ( @@ -592,7 +601,7 @@ struct info_command : public cmd { ).min(0).doc_non_optional(true) % "Information to display" + ( #if HAS_LIBUSB - device_selection % "To target one or more connected RP2040 device(s) in BOOTSEL mode (the default)" | + device_selection % "To target one or more connected RP-series device(s) in BOOTSEL mode (the default)" | #endif file_selection % "To target a file" ).major_group("TARGET SELECTION").min(0).doc_non_optional(true) @@ -600,7 +609,7 @@ struct info_command : public cmd { } string get_doc() const override { - return "Display information from the target device(s) or file.\nWithout any arguments, this will display basic information for all connected RP2040 devices in BOOTSEL mode"; + return "Display information from the target device(s) or file.\nWithout any arguments, this will display basic information for all connected RP-series devices in BOOTSEL mode"; } }; @@ -623,7 +632,7 @@ struct config_command : public cmd { (option('g', "--group") & value("group").set(settings.config.group)) % "Filter by feature group" + ( #if HAS_LIBUSB - device_selection % "To target one or more connected RP2040 device(s) in BOOTSEL mode (the default)" | + device_selection % "To target one or more connected RP-series device(s) in BOOTSEL mode (the default)" | #endif file_selection % "To target a file" ).major_group("TARGET SELECTION").min(0).doc_non_optional(true) @@ -1396,9 +1405,9 @@ int parse(const int argc, char **argv) { section_header(tool_name); fos.first_column(tab); #if HAS_LIBUSB - fos << "Tool for interacting with RP2040/RP2350 device(s) in BOOTSEL mode, or with an RP2040/RP2350 binary" << "\n"; + fos << "Tool for interacting with RP-series device(s) in BOOTSEL mode, or with an RP-series binary" << "\n"; #else - fos << "Tool for interacting with an RP2040/RP2350 binary" << "\n"; + fos << "Tool for interacting with an RP-series binary" << "\n"; #endif } vector synopsis; @@ -1779,7 +1788,7 @@ uint32_t bootrom_func_lookup(memory_access& access, uint16_t tag) { uint32_t bootrom_table_lookup_rp2350(memory_access& access, uint16_t tag, uint16_t flags) { model_t model = get_model(access); - // we are only used on RP2040 + // we are only used on RP2350 if (model != rp2350) { fail(ERROR_INCOMPATIBLE, "RP2350 BOOT ROM not found"); } @@ -3518,7 +3527,7 @@ void config_guts(memory_access &raw_access) { string missing_device_string(bool wasRetry, bool requires_rp2350 = false) { char b[256]; - const char* device_name = requires_rp2350 ? "RP2350" : "RP2040/RP2350"; + const char* device_name = requires_rp2350 ? "RP2350" : "RP-series"; if (wasRetry) { strcpy(b, "Despite the reboot attempt, no "); } else { @@ -3711,12 +3720,13 @@ bool config_command::execute(device_map &devices) { int size = devices[dr_vidpid_bootrom_ok].size(); if (size) { if (size > 1) { - fos << "Multiple RP2040 devices in BOOTSEL mode found:\n"; + fos << "Multiple RP-series devices in BOOTSEL mode found:\n"; } for (auto handles : devices[dr_vidpid_bootrom_ok]) { + selected_model = std::get<0>(handles); fos.first_column(0); fos.hanging_indent(0); if (size > 1) { - auto s = bus_device_string(std::get<1>(handles)); + auto s = bus_device_string(std::get<1>(handles), std::get<0>(handles)); string dashes; std::generate_n(std::back_inserter(dashes), s.length() + 1, [] { return '-'; }); fos << "\n" << s << ":\n" << dashes << "\n"; @@ -3791,12 +3801,13 @@ bool info_command::execute(device_map &devices) { int size = devices[dr_vidpid_bootrom_ok].size(); if (size) { if (size > 1) { - fos << "Multiple RP2040 devices in BOOTSEL mode found:\n"; + fos << "Multiple RP-series devices in BOOTSEL mode found:\n"; } for (auto handles : devices[dr_vidpid_bootrom_ok]) { + selected_model = std::get<0>(handles); fos.first_column(0); fos.hanging_indent(0); if (size > 1) { - auto s = bus_device_string(std::get<1>(handles)); + auto s = bus_device_string(std::get<1>(handles), std::get<0>(handles)); string dashes; std::generate_n(std::back_inserter(dashes), s.length() + 1, [] { return '-'; }); fos << "\n" << s << ":\n" << dashes << "\n"; @@ -3855,6 +3866,7 @@ bool info_command::execute(device_map &devices) { static picoboot::connection get_single_bootsel_device_connection(device_map& devices, bool exclusive = true) { assert(devices[dr_vidpid_bootrom_ok].size() == 1); auto device = devices[dr_vidpid_bootrom_ok][0]; + selected_model = std::get<0>(device); libusb_device_handle *rc = std::get<2>(device); if (!rc) fail(ERROR_USB, "Unable to connect to device"); return picoboot::connection(rc, std::get<0>(device), exclusive); @@ -7369,6 +7381,7 @@ bool reboot_command::execute(device_map &devices) { if (!settings.switch_cpu.empty()) { fail(ERROR_ARGS, "--cpu may not be specified for forced reboot"); } + selected_model = std::get<0>(devices[dr_vidpid_stdio_usb][0]); reboot_device(std::get<1>(devices[dr_vidpid_stdio_usb][0]), std::get<2>(devices[dr_vidpid_stdio_usb][0]), settings.reboot_usb); if (!quiet) { if (settings.reboot_usb) { @@ -7576,32 +7589,32 @@ int main(int argc, char **argv) { had_note = true; } for (auto d : devices[r]) { - fos << bus_device_string(std::get<1>(d)) << description << "\n"; + fos << bus_device_string(std::get<1>(d), std::get<0>(d)) << description << "\n"; } }; #if defined(__linux__) || defined(__APPLE__) printer(dr_vidpid_bootrom_cant_connect, - " appears to be a RP2040 device in BOOTSEL mode, but picotool was unable to connect. Maybe try 'sudo' or check your permissions."); + " appears to be in BOOTSEL mode, but picotool was unable to connect. Maybe try 'sudo' or check your permissions."); #else printer(dr_vidpid_bootrom_cant_connect, - " appears to be a RP2040 device in BOOTSEL mode, but picotool was unable to connect. You may need to install a driver via Zadig. See \"Getting started with Raspberry Pi Pico\" for more information"); + " appears to be in BOOTSEL mode, but picotool was unable to connect. You may need to install a driver via Zadig. See \"Getting started with Raspberry Pi Pico\" for more information"); #endif printer(dr_vidpid_picoprobe, - " appears to be a RP2040 PicoProbe device not in BOOTSEL mode."); + " appears to be an RP-series PicoProbe device not in BOOTSEL mode."); printer(dr_vidpid_micropython, - " appears to be a RP2040 MicroPython device not in BOOTSEL mode."); + " appears to be an RP-series MicroPython device not in BOOTSEL mode."); if (selected_cmd->force_requires_pre_reboot()) { #if defined(_WIN32) printer(dr_vidpid_stdio_usb, - " appears to be a RP2040 device with a USB serial connection, not in BOOTSEL mode. You can force reboot into BOOTSEL mode via 'picotool reboot -f -u' first."); + " appears to have a USB serial connection, not in BOOTSEL mode. You can force reboot into BOOTSEL mode via 'picotool reboot -f -u' first."); #else printer(dr_vidpid_stdio_usb, - " appears to be a RP2040 device with a USB serial connection, so consider -f (or -F) to force reboot in order to run the command."); + " appears to have a USB serial connection, so consider -f (or -F) to force reboot in order to run the command."); #endif } else { // special case message for what is actually just reboot (the only command that doesn't require reboot first) printer(dr_vidpid_stdio_usb, - " appears to be a RP2040 device with a USB serial connection, so consider -f to force the reboot."); + " appears to have a USB serial connection, so consider -f to force the reboot."); } rc = ERROR_NO_DEVICE; } else { @@ -7611,7 +7624,7 @@ int main(int argc, char **argv) { } else if (supported == cmd::device_support::one) { if (devices[dr_vidpid_bootrom_ok].size() > 1 || (devices[dr_vidpid_bootrom_ok].empty() && devices[dr_vidpid_stdio_usb].size() > 1)) { - fail(ERROR_NOT_POSSIBLE, "Command requires a single RP2040 device to be targeted."); + fail(ERROR_NOT_POSSIBLE, "Command requires a single RP-series device to be targeted."); } if (!devices[dr_vidpid_bootrom_ok].empty()) { settings.force = false; // we have a device, so we're not forcing @@ -7630,7 +7643,7 @@ int main(int argc, char **argv) { if (settings.force && ctx) { // actually ctx should never be null as we are targeting device if force is set, but still if (devices[dr_vidpid_stdio_usb].size() != 1 && !tries) { fail(ERROR_NOT_POSSIBLE, - "Forced command requires a single rebootable RP2040 device to be targeted."); + "Forced command requires a single rebootable RP-series device to be targeted."); } if (selected_cmd->force_requires_pre_reboot()) { if (!tries) { @@ -7707,11 +7720,23 @@ int main(int argc, char **argv) { rc = e.code(); } catch (picoboot::command_failure& e) { // todo rp2350/rp2040 - std::cout << "ERROR: The RP2040 device returned an error: " << e.what() << "\n"; + string device = "RP-series"; + if (selected_model == rp2040) { + device = "RP2040"; + } else if (selected_model == rp2350) { + device = "RP2350"; + } + std::cout << "ERROR: The " << device << " device returned an error: " << e.what() << "\n"; rc = ERROR_UNKNOWN; } catch (picoboot::connection_error&) { // todo rp2350/rp2040 - std::cout << "ERROR: Communication with RP2040 device failed\n"; + string device = "RP-series"; + if (selected_model == rp2040) { + device = "RP2040"; + } else if (selected_model == rp2350) { + device = "RP2350"; + } + std::cout << "ERROR: Communication with " << device << " device failed\n"; rc = ERROR_CONNECTION; } catch (cancelled_exception&) { rc = ERROR_CANCELLED; From c4550ad0a72089a41c808e9124a1e58637bc9cf4 Mon Sep 17 00:00:00 2001 From: will-v-pi <108662275+will-v-pi@users.noreply.github.com> Date: Tue, 22 Oct 2024 14:55:19 +0100 Subject: [PATCH 44/67] Fix segfault when unable to connect to stdio_usb device (#155) Add dr_vidpid_stdio_usb_cant_connect when searching for devices, to detect USB devices that failed to open separately from ones that did Fixes #151 --- main.cpp | 4 ++++ picoboot_connection/picoboot_connection.c | 10 +++++++--- picoboot_connection/picoboot_connection.h | 1 + 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/main.cpp b/main.cpp index 300115a..a9d23b8 100644 --- a/main.cpp +++ b/main.cpp @@ -7595,9 +7595,13 @@ int main(int argc, char **argv) { #if defined(__linux__) || defined(__APPLE__) printer(dr_vidpid_bootrom_cant_connect, " appears to be in BOOTSEL mode, but picotool was unable to connect. Maybe try 'sudo' or check your permissions."); + printer(dr_vidpid_stdio_usb_cant_connect, + " appears to have a USB serial connection, but picotool was unable to connect. Maybe try 'sudo' or check your permissions."); #else printer(dr_vidpid_bootrom_cant_connect, " appears to be in BOOTSEL mode, but picotool was unable to connect. You may need to install a driver via Zadig. See \"Getting started with Raspberry Pi Pico\" for more information"); + printer(dr_vidpid_stdio_usb_cant_connect, + " appears to have a USB serial connection, but picotool was unable to connect."); #endif printer(dr_vidpid_picoprobe, " appears to be an RP-series PicoProbe device not in BOOTSEL mode."); diff --git a/picoboot_connection/picoboot_connection.c b/picoboot_connection/picoboot_connection.c index f71ea89..0c6c35f 100644 --- a/picoboot_connection/picoboot_connection.c +++ b/picoboot_connection/picoboot_connection.c @@ -87,7 +87,11 @@ enum picoboot_device_result picoboot_open_device(libusb_device *device, libusb_d case PRODUCT_ID_PICOPROBE: return dr_vidpid_picoprobe; case PRODUCT_ID_RP2040_STDIO_USB: + *model = rp2040; + res = dr_vidpid_stdio_usb; + break; case PRODUCT_ID_STDIO_USB: + *model = rp2350; res = dr_vidpid_stdio_usb; break; case PRODUCT_ID_RP2040_USBBOOT: @@ -115,15 +119,15 @@ enum picoboot_device_result picoboot_open_device(libusb_device *device, libusb_d if (vid == 0 || strlen(ser) != 0) { // didn't check vid or ser, so treat as unknown return dr_vidpid_unknown; - } else if (res != dr_vidpid_unknown) { - return res; + } else if (res == dr_vidpid_stdio_usb) { + return dr_vidpid_stdio_usb_cant_connect; } else { return dr_vidpid_bootrom_cant_connect; } } } - if (res == dr_vidpid_stdio_usb) { + if (!ret && res == dr_vidpid_stdio_usb) { if (strlen(ser) != 0) { // Check USB serial number char ser_str[128]; diff --git a/picoboot_connection/picoboot_connection.h b/picoboot_connection/picoboot_connection.h index 875c35c..436943f 100644 --- a/picoboot_connection/picoboot_connection.h +++ b/picoboot_connection/picoboot_connection.h @@ -38,6 +38,7 @@ enum picoboot_device_result { dr_vidpid_unknown, dr_error, dr_vidpid_stdio_usb, + dr_vidpid_stdio_usb_cant_connect, }; typedef enum { From c2fca5a6a7cee3591f739e9cc81f021e04350753 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Thu, 24 Oct 2024 17:30:22 +0100 Subject: [PATCH 45/67] Fix RP2350 higher pin functions The pin_functions_rp2350 array was missing a PIO2 row, causing the higher functions to be displayed incorrectly by picotool info Fix this, and add an "Unknown pin function" printout if pin function is not known --- main.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/main.cpp b/main.cpp index a9d23b8..48b1c0f 100644 --- a/main.cpp +++ b/main.cpp @@ -143,6 +143,7 @@ std::array, 12> pin_functions_rp2350{{ {"SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO", "SIO"}, {"PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0", "PIO0"}, {"PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1", "PIO1"}, + {"PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2", "PIO2"}, {"XIP CS1", "CORESIGHT TRACECLK","CORESIGHT TRACEDATA0","CORESIGHT TDATA1","CORESIGHT TDATA2","CORESIGHT TDATA3","","","XIP CS1", "", "","", "CLK GPIN", "CLK GPOUT","CLK GPIN", "CLK GPOUT","", "", "", "XIP CS1", "CLK GPIN", "CLK GPOUT","CLK GPIN", "CLK GPOUT","CLK GPOUT","CLK GPOUT","", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "XIP CS1"}, {"USB OVCUR DET", "USB VBUS DET", "USB VBUS EN", "USB OVCUR DET", "USB VBUS DET", "USB VBUS EN", "USB OVCUR DET", "USB VBUS DET", "USB VBUS EN", "USB OVCUR DET", "USB VBUS DET", "USB VBUS EN", "USB OVCUR DET", "USB VBUS DET", "USB VBUS EN", "USB OVCUR DET", "USB VBUS DET", "USB VBUS EN", "USB OVCUR DET", "USB VBUS DET", "USB VBUS EN", "USB OVCUR DET", "USB VBUS DET", "USB VBUS EN", "USB OVCUR DET", "USB VBUS DET", "USB VBUS EN", "USB OVCUR DET", "USB VBUS DET", "USB VBUS EN", "USB OVCUR DET", "USB VBUS DET", "USB VBUS EN", "USB OVCUR DET", "USB VBUS DET", "USB VBUS EN", "USB OVCUR DET", "USB VBUS DET", "USB VBUS EN", "USB OVCUR DET", "USB VBUS DET", "USB VBUS EN", "USB OVCUR DET", "USB VBUS DET", "USB VBUS EN", "USB OVCUR DET", "USB VBUS DET", "USB VBUS EN"}, {"", "", "UART0 TX", "UART0 RX", "", "", "UART1 TX", "UART1 RX", "", "", "UART1 TX", "UART1 RX", "", "", "UART0 TX", "UART0 RX", "", "", "UART0 TX", "UART0 RX", "", "", "UART1 TX", "UART1 RX", "", "", "UART1 TX", "UART1 RX", "", "", "UART0 TX", "UART0 RX", "", "", "UART0 TX", "UART0 RX", "", "", "UART1 TX", "UART1 RX", "", "", "UART1 TX", "UART1 RX", "", "", "UART0 TX", "UART0 RX"} @@ -2379,7 +2380,13 @@ struct bi_visitor_base { for(unsigned int i=0; i<64; i++) { if (pin_mask & (1ull << i)) { if (func != -1) { - pin(i, pin_functions[func][i]); + if (pin_functions[func][i].empty()) { + std::stringstream sstream; + sstream << "Unknown pin function " << func; + pin(i, sstream.str()); + } else { + pin(i, pin_functions[func][i]); + } } else { auto sep = name.find_first_of('|'); auto cur = name.substr(0, sep); From afb5f26532e4682d38c10503a2e018535cf29260 Mon Sep 17 00:00:00 2001 From: graham sanderson Date: Sat, 26 Oct 2024 15:56:00 -0500 Subject: [PATCH 46/67] fix otp_load_command --- main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.cpp b/main.cpp index 48b1c0f..3c54640 100644 --- a/main.cpp +++ b/main.cpp @@ -6838,7 +6838,7 @@ bool otp_load_command::execute(device_map &devices) { uint8_t* file_buffer = unique_file_buffer.get(); file->read((char*)file_buffer, file_size); try { - con.otp_write(&otp_cmd, (uint8_t *)file_buffer, sizeof(file_buffer)); + con.otp_write(&otp_cmd, (uint8_t *)file_buffer, file_size); } catch (picoboot::command_failure &e) { check_otp_write_error(e, otp_cmd.bEcc); throw e; From 0dcea9c2bbdfe72c50f983cf4bc4201268999110 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Tue, 29 Oct 2024 14:41:43 +0000 Subject: [PATCH 47/67] Require 0x before hexadecimal family IDs Fixes #161 --- main.cpp | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/main.cpp b/main.cpp index 3c54640..9123281 100644 --- a/main.cpp +++ b/main.cpp @@ -321,22 +321,26 @@ struct family_id : public cli::value_base { } else if (value == rp2350_riscv_family_name) { t = RP2350_RISCV_FAMILY_ID; } else { - if (value.find("0x") == 0) value = value.substr(2); - size_t pos = 0; - long lvalue = std::numeric_limits::max(); - try { - lvalue = std::stoul(value, &pos, 16); - if (pos != value.length()) { - return "Garbage after hex value: " + value.substr(pos); + if (value.find("0x") == 0) { + value = value.substr(2); + size_t pos = 0; + long lvalue = std::numeric_limits::max(); + try { + lvalue = std::stoul(value, &pos, 16); + if (pos != value.length()) { + return "Garbage after hex value: " + value.substr(pos); + } + } catch (std::invalid_argument &) { + return ovalue + " is not a valid hex value"; + } catch (std::out_of_range &) { } - } catch (std::invalid_argument &) { - return ovalue + " is not a valid hex value"; - } catch (std::out_of_range &) { + if (lvalue != (unsigned int) lvalue) { + return value + " is not a valid 32 bit value"; + } + t = (unsigned int) lvalue; + } else { + return value + " is not a valid family ID"; } - if (lvalue != (unsigned int) lvalue) { - return value + " is not a valid 32 bit value"; - } - t = (unsigned int) lvalue; } return string(""); }); From 19226d169be5fbdc25ce24d897131115d834afe6 Mon Sep 17 00:00:00 2001 From: will-v-pi <108662275+will-v-pi@users.noreply.github.com> Date: Tue, 29 Oct 2024 18:31:34 +0000 Subject: [PATCH 48/67] Fix info and config commands for packaged binaries (#158) Remap according to the load_map before searching for the binary info --- main.cpp | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/main.cpp b/main.cpp index 9123281..a03afd9 100644 --- a/main.cpp +++ b/main.cpp @@ -1914,13 +1914,13 @@ struct picoboot_memory_access : public memory_access { } } - // note this does not automatically erase flash + // note this does not automatically erase flash unless erase is set void write(uint32_t address, uint8_t *buffer, unsigned int size) override { + vector write_data; // used when erasing flash if (flash == get_memory_type(address, model)) { connection.exit_xip(); if (erase) { // Do automatically erase flash, and make it aligned - vector write_data; // we have to erase in whole pages range aligned_range(address & ~(FLASH_SECTOR_ERASE_SIZE - 1), ((address + size) & ~(FLASH_SECTOR_ERASE_SIZE - 1)) + FLASH_SECTOR_ERASE_SIZE); @@ -2705,6 +2705,15 @@ uint32_t build_rmap_uf2(std::shared_ptrfile, range_map& r return next_family_id; } +void build_rmap_load_map(std::shared_ptrload_map, range_map& rmap) { + for (unsigned int i=0; i < load_map->entries.size(); i++) { + auto e = load_map->entries[i]; + if (e.storage_address != 0) { + rmap.insert(range(e.runtime_address, e.runtime_address + e.size), e.storage_address); + } + } +} + uint32_t find_binary_start(range_map& rmap) { range flash(FLASH_START, FLASH_END_RP2350); // pick biggest (rp2350) here for now range sram(SRAM_START, SRAM_END_RP2350); // pick biggest (rp2350) here for now @@ -2896,6 +2905,21 @@ std::unique_ptr find_last_block(memory_access &raw_access, vector get_bi_access(memory_access &raw_access) { + vector bin; + std::unique_ptr best_block = find_best_block(raw_access, bin); + range_map rmap; + if (best_block) { + auto load_map = best_block->get_item(); + if (load_map != nullptr) { + // Remap for find_binary_info + build_rmap_load_map(load_map, rmap); + } + } + + return std::make_shared(raw_access, rmap); +} + #if HAS_LIBUSB void info_guts(memory_access &raw_access, picoboot::connection *con) { #else @@ -2944,9 +2968,10 @@ void info_guts(memory_access &raw_access, void *con) { select_group(device_info); binary_info_header hdr; try { - bool has_binary_info = find_binary_info(raw_access, hdr); + auto bi_access = get_bi_access(raw_access); + bool has_binary_info = find_binary_info(*bi_access, hdr); if (has_binary_info) { - auto access = remapped_memory_access(raw_access, hdr.reverse_copy_mapping); + auto access = remapped_memory_access(*bi_access, hdr.reverse_copy_mapping); auto visitor = bi_visitor{}; map output; map> pins; @@ -3202,7 +3227,7 @@ void info_guts(memory_access &raw_access, void *con) { if (sig_verified != none) { info_pair("signature", sig_verified == passed ? "verified" : "incorrect"); } - } else if (has_binary_info && get_model(raw_access) == rp2350) { + } else if (!best_block && has_binary_info && get_model(raw_access) == rp2350) { fos << "WARNING: Binary on RP2350 device does not contain a block loop - this binary will not boot\n"; } } catch (std::invalid_argument &e) { @@ -3430,8 +3455,9 @@ void config_guts(memory_access &raw_access) { } } - if (find_binary_info(raw_access, hdr)) { - auto access = remapped_memory_access(raw_access, hdr.reverse_copy_mapping); + auto bi_access = get_bi_access(raw_access); + if (find_binary_info(*bi_access, hdr)) { + auto access = remapped_memory_access(*bi_access, hdr.reverse_copy_mapping); auto visitor = bi_visitor{}; map, pair> named_feature_groups; @@ -3487,6 +3513,7 @@ void config_guts(memory_access &raw_access) { for (auto n : group_names) { auto ints = named_feature_group_ints[n]; auto strings = named_feature_group_strings[n]; + fos.first_column(fr_col); if (!n.empty()) { fos << n << ":\n"; fos.first_column(fr_col + 1); From 439062512e08d0ebb5f3bb00e2de0c356c4ad927 Mon Sep 17 00:00:00 2001 From: Charlie Birks Date: Tue, 29 Oct 2024 18:34:20 +0000 Subject: [PATCH 49/67] Fix coprodis to not drop the last instruction and the rest of the file (#159) --- main.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/main.cpp b/main.cpp index a03afd9..c881e0a 100644 --- a/main.cpp +++ b/main.cpp @@ -6623,7 +6623,7 @@ bool coprodis_command::execute(device_map &devices) { string line; fos << "Replacing " << proc_insts.size() << " instructions\n"; while (getline(buffer, line)) { - if (line == std::get<0>(proc_insts[0])) { + if (!proc_insts.empty() && line == std::get<0>(proc_insts[0])) { fos << "\nFound instruction\n"; fos << line; fos << "\n"; @@ -6636,7 +6636,6 @@ bool coprodis_command::execute(device_map &devices) { fos << line; fos << "\n"; proc_insts.erase(proc_insts.begin()); - if (proc_insts.size() == 0) break; } *out << line << "\n"; } From 3ea1bb5d3e6de66b161ea9b386d2f374cd147201 Mon Sep 17 00:00:00 2001 From: armandomontanez Date: Wed, 30 Oct 2024 03:57:17 -0700 Subject: [PATCH 50/67] [Bazel] Infer PICOTOOL_VERSION define from module version (#156) Makes the PICOTOOL_VERSION define use the value of module_version() to reduce duplication of version strings in the Bazel build. --- BUILD.bazel | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/BUILD.bazel b/BUILD.bazel index 05b355f..2f9aa94 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -2,6 +2,8 @@ load("//bazel:defs.bzl", "otp_header_parse", "picotool_binary_data_header") package(default_visibility = ["//visibility:public"]) +PICOTOOL_SDK_VERSION_STRING = module_version() if module_version() != None else "0.0.1-WORKSPACE" + picotool_binary_data_header( name = "rp2350_rom", src = "bootrom.end.bin", @@ -71,8 +73,7 @@ cc_binary( ], }), defines = [ - # TODO: There's probably a nicer way to share the version with CMake. - 'PICOTOOL_VERSION=\\"2.0.0\\"', + 'PICOTOOL_VERSION=\\"{}\\"'.format(PICOTOOL_VERSION_STRING), 'SYSTEM_VERSION=\\"host\\"', 'COMPILER_INFO=\\"local\\"', "SUPPORT_A0=0", From f41f7fa450620e5a1f561e7867090693228698c0 Mon Sep 17 00:00:00 2001 From: armandomontanez Date: Wed, 30 Oct 2024 03:58:29 -0700 Subject: [PATCH 51/67] [Bazel] Get MSVC working (#157) Fixes the Windows MSVC build for Picotool. --- BUILD.bazel | 19 ++++++++++++++++--- bintool/BUILD.bazel | 1 + otp_header_parser/BUILD.bazel | 6 ++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/BUILD.bazel b/BUILD.bazel index 2f9aa94..a5c0411 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -46,6 +46,10 @@ otp_header_parse( name = "otp_header", src = "@pico-sdk//src/rp2350/hardware_regs:otp_data_header", out = "rp2350.json.h", + target_compatible_with = select({ + "@rules_cc//cc/compiler:msvc-cl": ["@platforms//:incompatible"], + "//conditions:default": [], + }), ) cc_binary( @@ -56,10 +60,14 @@ cc_binary( "main.cpp", "otp.cpp", "otp.h", - "rp2350.json.h", "rp2350.rom.h", "xip_ram_perms.cpp", - ], + ] + select({ + # MSVC can't handle long strings, so use this manually generated + # header instead. + "@rules_cc//cc/compiler:msvc-cl": [], + "//conditions:default": ["rp2350.json.h"], + }), copts = select({ "@rules_cc//cc/compiler:msvc-cl": [ "/std:c++20", @@ -106,5 +114,10 @@ cc_binary( "@pico-sdk//src/rp2350/hardware_regs:otp_data", "@pico-sdk//src/rp2_common/pico_bootrom:pico_bootrom_headers", "@pico-sdk//src/rp2_common/pico_stdio_usb:reset_interface_headers", - ], + ] + select({ + # MSVC can't handle long strings, so use this manually generated + # header instead. + "@rules_cc//cc/compiler:msvc-cl": ["//otp_header_parser:pre_generated_otp_header"], + "//conditions:default": [], + }), ) diff --git a/bintool/BUILD.bazel b/bintool/BUILD.bazel index 82ede3b..a9f4961 100644 --- a/bintool/BUILD.bazel +++ b/bintool/BUILD.bazel @@ -12,6 +12,7 @@ cc_library( "metadata.h", ], copts = select({ + "@rules_cc//cc/compiler:msvc-cl": ["/std:c++20"], "@platforms//os:windows": [], "//conditions:default": [ "-Wno-unused-variable", diff --git a/otp_header_parser/BUILD.bazel b/otp_header_parser/BUILD.bazel index ae191c0..fdf04f1 100644 --- a/otp_header_parser/BUILD.bazel +++ b/otp_header_parser/BUILD.bazel @@ -1,5 +1,11 @@ package(default_visibility = ["//visibility:public"]) +cc_library( + name = "pre_generated_otp_header", + includes = ["."], + hdrs = ["rp2350.json.h"], +) + cc_binary( name = "otp_header_parser", srcs = ["otp_header_parse.cpp"], From a86abb73b589c7a74781e82e45c871e598551f88 Mon Sep 17 00:00:00 2001 From: armandomontanez Date: Tue, 5 Nov 2024 10:42:41 -0800 Subject: [PATCH 52/67] Fixes and add presubmit (#166) * Fix Bazel build and add presubmit checks Fixes a variable name and adds Bazel presubmit checks to ensure the Bazel build stays healthy. * Remove Windows from presubmit checks for now MSVC is tripping up on statement expressions in timer.h. --- .bazelignore | 1 + .github/workflows/bazel_build.yml | 36 +++++++++++++++++++++++++++++++ BUILD.bazel | 2 +- 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 .bazelignore create mode 100644 .github/workflows/bazel_build.yml diff --git a/.bazelignore b/.bazelignore new file mode 100644 index 0000000..dd2e8d2 --- /dev/null +++ b/.bazelignore @@ -0,0 +1 @@ +lib/pico-sdk diff --git a/.github/workflows/bazel_build.yml b/.github/workflows/bazel_build.yml new file mode 100644 index 0000000..2e775c7 --- /dev/null +++ b/.github/workflows/bazel_build.yml @@ -0,0 +1,36 @@ +name: Bazel presubmit checks + +on: + push: + pull_request: + +jobs: + bazel-build-check: + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + fail-fast: false + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Get Bazel + uses: bazel-contrib/setup-bazel@0.9.0 + with: + # Avoid downloading Bazel every time. + bazelisk-cache: true + # Store build cache per workflow. + disk-cache: ${{ github.workflow }} + # Share repository cache between workflows. + repository-cache: true + - name: Fetch latest Pico SDK + uses: actions/checkout@v4 + with: + repository: raspberrypi/pico-sdk + ref: develop + fetch-depth: 0 + path: lib/pico-sdk + - name: Bazel Picotool with develop pico-sdk + run: bazel build @picotool//:picotool --override_module=pico-sdk=lib/pico-sdk diff --git a/BUILD.bazel b/BUILD.bazel index a5c0411..d120f50 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -81,7 +81,7 @@ cc_binary( ], }), defines = [ - 'PICOTOOL_VERSION=\\"{}\\"'.format(PICOTOOL_VERSION_STRING), + 'PICOTOOL_VERSION=\\"{}\\"'.format(PICOTOOL_SDK_VERSION_STRING), 'SYSTEM_VERSION=\\"host\\"', 'COMPILER_INFO=\\"local\\"', "SUPPORT_A0=0", From 0f9977ea7139052dae65bf4a0a21a493aaca41d4 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Thu, 7 Nov 2024 13:45:40 +0000 Subject: [PATCH 53/67] Fix parsing of partition IDs Partition IDs are unsigned 64-bit integers, but were being parsed as signed integers, so were out of range if the first bit was set. --- cli.h | 7 ++++++- main.cpp | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/cli.h b/cli.h index 1bc766b..6db9b4f 100644 --- a/cli.h +++ b/cli.h @@ -432,13 +432,18 @@ namespace cli { base = 2; } try { - lvalue = std::stoll(value, &pos, base); + if (std::is_signed()) { + lvalue = std::stoll(value, &pos, base); + } else { + lvalue = std::stoull(value, &pos, base); + } if (pos != value.length()) { return "Garbage after integer value: " + value.substr(pos); } } catch (std::invalid_argument&) { return value + " is not a valid integer"; } catch (std::out_of_range&) { + return value + " is out of range"; } if (lvalue != (int64_t)lvalue) { return value + " is too big"; diff --git a/main.cpp b/main.cpp index c881e0a..f551ebd 100644 --- a/main.cpp +++ b/main.cpp @@ -5774,7 +5774,10 @@ bool partition_create_command::execute(device_map &devices) { new_p.flags |= (link_value << PICOBIN_PARTITION_FLAGS_LINK_VALUE_LSB) & PICOBIN_PARTITION_FLAGS_LINK_VALUE_BITS; } if (p.contains("name")) { new_p.name = p["name"]; new_p.flags |= PICOBIN_PARTITION_FLAGS_HAS_NAME_BITS; } - if (p.contains("id")) { get_json_int(p["id"], new_p.id); new_p.flags |= PICOBIN_PARTITION_FLAGS_HAS_ID_BITS; } + if (p.contains("id")) { + if (get_json_int(p["id"], new_p.id)) {new_p.flags |= PICOBIN_PARTITION_FLAGS_HAS_ID_BITS;} + else {string p_id = p["id"]; fail(ERROR_INCOMPATIBLE, "Partition ID \"%s\" is not a valid 64bit integer\n", p_id.c_str());} + } if(p.contains("no_reboot_on_uf2_download")) new_p.flags |= PICOBIN_PARTITION_FLAGS_UF2_DOWNLOAD_NO_REBOOT_BITS; if(p.contains("ab_non_bootable_owner_affinity")) new_p.flags |= PICOBIN_PARTITION_FLAGS_UF2_DOWNLOAD_AB_NON_BOOTABLE_OWNER_AFFINITY; From dc9b5494fe78e3fe52fd11d7427d2be72e55f37a Mon Sep 17 00:00:00 2001 From: will-v-pi <108662275+will-v-pi@users.noreply.github.com> Date: Thu, 7 Nov 2024 14:37:08 +0000 Subject: [PATCH 54/67] Always print serial number when unable to find a device with specific serial number (#164) Previously it would not print the serial number if a device wasn't found after a -f reboot (see https://forums.raspberrypi.com/viewtopic.php?t=378682) --- main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.cpp b/main.cpp index f551ebd..64a0ae8 100644 --- a/main.cpp +++ b/main.cpp @@ -3581,7 +3581,7 @@ string missing_device_string(bool wasRetry, bool requires_rp2350 = false) { } } else if (settings.bus != -1) { snprintf(buf, buf_len, "accessible %s devices in BOOTSEL mode were found found on bus %d.", device_name, settings.bus); - } else if (!settings.ser.empty() && !wasRetry) { + } else if (!settings.ser.empty()) { snprintf(buf, buf_len, "accessible %s devices in BOOTSEL mode were found found with serial number %s.", device_name, settings.ser.c_str()); } else { snprintf(buf, buf_len, "accessible %s devices in BOOTSEL mode were found.", device_name); From ae9a188b4dcb05797115f4d8d73fd6c668bf83c4 Mon Sep 17 00:00:00 2001 From: Tobias Simetsreiter Date: Tue, 12 Nov 2024 15:08:44 +0100 Subject: [PATCH 55/67] lowercase 'Windows.h' in main.cpp for mingw32 support (#168) --- main.cpp | 2 +- picoboot_connection/picoboot_connection.h | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/main.cpp b/main.cpp index 64a0ae8..0bc722d 100644 --- a/main.cpp +++ b/main.cpp @@ -7496,7 +7496,7 @@ bool reboot_command::execute(device_map &devices) { #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN #define VC_EXTRALEAN -#include +#include #elif defined(__linux__) || defined(__APPLE__) #include #endif diff --git a/picoboot_connection/picoboot_connection.h b/picoboot_connection/picoboot_connection.h index 436943f..8c2ca22 100644 --- a/picoboot_connection/picoboot_connection.h +++ b/picoboot_connection/picoboot_connection.h @@ -81,6 +81,9 @@ int picoboot_flash_id(libusb_device_handle *usb_device, uint64_t *data); // we require 256 (as this is the page size supported by the device) #define LOG2_PAGE_SIZE 8u +#ifdef PAGE_SIZE +#undef PAGE_SIZE +#endif #define PAGE_SIZE (1u << LOG2_PAGE_SIZE) #define FLASH_SECTOR_ERASE_SIZE 4096u From 78c9bd121b09399823b67ee7ea89003ca0d3315f Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Tue, 12 Nov 2024 17:26:33 +0000 Subject: [PATCH 56/67] Update abs_block Add RP2 ignored extension to errata E10 abs_block, to make it more identifiable --- elf2uf2/elf2uf2.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/elf2uf2/elf2uf2.cpp b/elf2uf2/elf2uf2.cpp index edc5ee5..63ce372 100644 --- a/elf2uf2/elf2uf2.cpp +++ b/elf2uf2/elf2uf2.cpp @@ -129,7 +129,7 @@ uf2_block gen_abs_block(uint32_t abs_block_loc) { uf2_block block; block.magic_start0 = UF2_MAGIC_START0; block.magic_start1 = UF2_MAGIC_START1; - block.flags = UF2_FLAG_FAMILY_ID_PRESENT; + block.flags = UF2_FLAG_FAMILY_ID_PRESENT | UF2_FLAG_EXTENSION_FLAGS_PRESENT; block.payload_size = UF2_PAGE_SIZE; block.num_blocks = 2; block.file_size = ABSOLUTE_FAMILY_ID; @@ -138,6 +138,7 @@ uf2_block gen_abs_block(uint32_t abs_block_loc) { block.block_no = 0; memset(block.data, 0, sizeof(block.data)); memset(block.data, 0xef, UF2_PAGE_SIZE); + *(uint32_t*)&(block.data[UF2_PAGE_SIZE]) = UF2_EXTENSION_RP2_IGNORE_BLOCK; return block; } @@ -145,12 +146,13 @@ bool check_abs_block(uf2_block block) { return std::all_of(block.data, block.data + UF2_PAGE_SIZE, [](uint8_t i) { return i == 0xef; }) && block.magic_start0 == UF2_MAGIC_START0 && block.magic_start1 == UF2_MAGIC_START1 && - block.flags == UF2_FLAG_FAMILY_ID_PRESENT && + (block.flags & ~UF2_FLAG_EXTENSION_FLAGS_PRESENT) == UF2_FLAG_FAMILY_ID_PRESENT && block.payload_size == UF2_PAGE_SIZE && block.num_blocks == 2 && block.file_size == ABSOLUTE_FAMILY_ID && block.magic_end == UF2_MAGIC_END && - block.block_no == 0; + block.block_no == 0 && + !(block.flags & UF2_FLAG_EXTENSION_FLAGS_PRESENT && *(uint32_t*)&(block.data[UF2_PAGE_SIZE]) != UF2_EXTENSION_RP2_IGNORE_BLOCK); } int pages2uf2(std::map>& pages, std::shared_ptr in, std::shared_ptr out, uint32_t family_id, uint32_t abs_block_loc=0) { From 7350867a238e20b6b411d670f0c795bd766cd80b Mon Sep 17 00:00:00 2001 From: will-v-pi <108662275+will-v-pi@users.noreply.github.com> Date: Tue, 19 Nov 2024 17:51:19 +0000 Subject: [PATCH 57/67] Align Saving, Loading and Verifying progress bars (#170) * Align all progress bars * Prevent saving/loading from unstriped SRAM * Fix saving/verifying range to bin file --- main.cpp | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/main.cpp b/main.cpp index 0bc722d..f6a2f98 100644 --- a/main.cpp +++ b/main.cpp @@ -97,7 +97,6 @@ typedef map>> auto memory_names = map{ {memory_type::sram, "RAM"}, - {memory_type::sram_unstriped, "Unstriped RAM"}, {memory_type::flash, "Flash"}, {memory_type::xip_sram, "XIP RAM"}, {memory_type::rom, "ROM"} @@ -3922,7 +3921,16 @@ static picoboot::connection get_single_rp2350_bootsel_device_connection(device_m #endif struct progress_bar { - explicit progress_bar(string prefix, int width = 30) : prefix(std::move(prefix)), width(width) { + explicit progress_bar(string new_prefix, int width = 30) : width(width) { + // Align all bars with the longest possible prefix string + auto longest_mem = std::max_element( + std::begin(memory_names), std::end(memory_names), + [] (const auto & p1, const auto & p2) { + return p1.second.length() < p2.second.length(); + } + ); + string extra_space(string("Loading into " + longest_mem->second + ": ").length() - new_prefix.length(), ' '); + prefix = new_prefix + extra_space; progress(0); } @@ -3991,6 +3999,9 @@ bool save_command::execute(device_map &devices) { } else { start = settings.from; end = settings.to; + // Set offset for verifying + settings.offset = start; + settings.offset_set = true; } if (end <= start) { fail(ERROR_ARGS, "Save range is invalid/empty"); @@ -4029,7 +4040,7 @@ bool save_command::execute(device_map &devices) { model_t model = get_model(raw_access); enum memory_type t1 = get_memory_type(start , model); enum memory_type t2 = get_memory_type(end, model); - if (t1 == invalid || t1 != t2) { + if (t1 != t2 || t1 == invalid || t1 == sram_unstriped) { fail(ERROR_NOT_POSSIBLE, "Save range crosses unmapped memory"); } uint32_t size = end - start; @@ -4106,7 +4117,7 @@ bool save_command::execute(device_map &devices) { enum memory_type type = get_memory_type(mem_range.from, model); bool ok = true; { - progress_bar bar("Verifying " + memory_names[type] + ": "); + progress_bar bar("Verifying " + memory_names[type] + ": "); uint32_t batch_size = FLASH_SECTOR_ERASE_SIZE; vector file_buf; vector device_buf; @@ -4264,7 +4275,7 @@ bool load_guts(picoboot::connection con, iostream_memory_access &file_access) { for (auto mem_range : ranges) { enum memory_type t1 = get_memory_type(mem_range.from, model); enum memory_type t2 = get_memory_type(mem_range.to, model); - if (t1 != t2 || t1 == invalid || t1 == rom) { + if (t1 != t2 || t1 == invalid || t1 == rom || t1 == sram_unstriped) { fail(ERROR_FORMAT, "File to load contained an invalid memory range 0x%08x-0x%08x", mem_range.from, mem_range.to); } @@ -4355,7 +4366,7 @@ bool load_guts(picoboot::connection con, iostream_memory_access &file_access) { if (settings.load.verify) { bool ok = true; { - progress_bar bar("Verifying " + memory_names[type] + ": "); + progress_bar bar("Verifying " + memory_names[type] + ": "); uint32_t batch_size = FLASH_SECTOR_ERASE_SIZE; vector file_buf; vector device_buf; @@ -4926,7 +4937,7 @@ bool verify_command::execute(device_map &devices) { for (auto mem_range : ranges) { enum memory_type t1 = get_memory_type(mem_range.from, model); enum memory_type t2 = get_memory_type(mem_range.to, model); - if (t1 != t2 || t1 == invalid) { + if (t1 != t2 || t1 == invalid || t1 == sram_unstriped) { fail(ERROR_NOT_POSSIBLE, "invalid memory range for verification %08x-%08x", mem_range.from, mem_range.to); } else { bool ok = true; From 877282d19d420340fdaac8674d4f93a6310e2254 Mon Sep 17 00:00:00 2001 From: Andrew Scheller Date: Wed, 20 Nov 2024 01:14:04 +0000 Subject: [PATCH 58/67] Fix macOS CI builds (#178) By removing cmake and pkg-config from the dependencies --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e79a342..85decb5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,7 +39,7 @@ jobs: - name: Install dependencies (macOS) if: runner.os == 'macOS' run: | - brew install cmake libusb pkg-config ninja + brew install libusb ninja brew install --cask gcc-arm-embedded - name: Install dependencies (Linux) From 2f2e8dffdb56714036fbe429552d09081d8d545f Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Wed, 20 Nov 2024 18:25:22 +0800 Subject: [PATCH 59/67] Allow using thirdparty VID for reboot interface (#177) With this change you can reboot a device with third party VID/PID from application firmware into bootloader with: ``` picotool reboot --vid 0x32ac --pid 0x001f -f -u ``` Signed-off-by: Daniel Schaefer --- picoboot_connection/BUILD.bazel | 1 + picoboot_connection/picoboot_connection.c | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/picoboot_connection/BUILD.bazel b/picoboot_connection/BUILD.bazel index fd8b15b..b2fac4d 100644 --- a/picoboot_connection/BUILD.bazel +++ b/picoboot_connection/BUILD.bazel @@ -19,5 +19,6 @@ cc_library( "@pico-sdk//src/common/boot_picoboot_headers", "@pico-sdk//src/rp2_common/boot_bootrom_headers", "@pico-sdk//src/rp2_common/pico_bootrom:pico_bootrom_headers", + "@pico-sdk//src/rp2_common/pico_stdio_usb:reset_interface_headers", ], ) diff --git a/picoboot_connection/picoboot_connection.c b/picoboot_connection/picoboot_connection.c index 0c6c35f..265608c 100644 --- a/picoboot_connection/picoboot_connection.c +++ b/picoboot_connection/picoboot_connection.c @@ -11,6 +11,7 @@ #include "picoboot_connection.h" #include "boot/bootrom_constants.h" +#include "pico/stdio_usb/reset_interface.h" #if ENABLE_DEBUG_LOG #include @@ -142,6 +143,17 @@ enum picoboot_device_result picoboot_open_device(libusb_device *device, libusb_d } } + // Runtime reset interface with thirdparty VID + if (!ret) { + for (int i = 0; i < config->bNumInterfaces; i++) { + if (config->interface[i].altsetting[0].bInterfaceClass == 0xff && + config->interface[i].altsetting[0].bInterfaceSubClass == RESET_INTERFACE_SUBCLASS && + config->interface[i].altsetting[0].bInterfaceProtocol == RESET_INTERFACE_PROTOCOL) { + return dr_vidpid_stdio_usb; + } + } + } + if (!ret) { if (config->bNumInterfaces == 1) { interface = 0; From dcff4d08d1e4ca606b7967149a83f06f9bc61134 Mon Sep 17 00:00:00 2001 From: will-v-pi <108662275+will-v-pi@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:23:49 +0000 Subject: [PATCH 60/67] List field desciptions if field is matching (#174) * List field desciptions if field matching (fixes #134) --- main.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/main.cpp b/main.cpp index f6a2f98..4000168 100644 --- a/main.cpp +++ b/main.cpp @@ -425,6 +425,7 @@ struct _settings { std::vector pages; bool list_pages = false; bool list_no_descriptions = false; + bool list_field_descriptions = false; std::vector selectors; uint32_t row = 0; std::vector extra_files; @@ -951,6 +952,7 @@ struct otp_list_command : public cmd { ( option('p', "--pages").set(settings.otp.list_pages) % "Show page number/page row number" + option('n', "--no-descriptions").set(settings.otp.list_no_descriptions) % "Don't show descriptions" + + option('f', "--field-descriptions").set(settings.otp.list_field_descriptions) % "Show all field descriptions" + (option('i', "--include") & value("filename").add_to(settings.otp.extra_files)).min(0).max(1) % "Include extra otp definition" + // todo more than 1 (value("selector").add_to(settings.otp.selectors) % "The row/field selector, each of which can select a whole row:\n\n" \ @@ -6974,6 +6976,14 @@ bool otp_list_command::execute(device_map &devices) { } else { fos << " (bits " << low << "-" << high << ")\n"; } + if ((m.field || settings.otp.list_field_descriptions) && !settings.otp.list_no_descriptions && !f.description.empty()) { + // Only print field descriptors if matching a field, or if list_field_descriptions is set + fos.first_column(indent0); + fos.hanging_indent(0); + fos << "\"" << f.description << "\""; + fos.first_column(0); + fos << "\n"; + } } } } From fb85aca4cf49e2ad998db4fd9f9160b00893b663 Mon Sep 17 00:00:00 2001 From: will-v-pi <108662275+will-v-pi@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:58:47 +0000 Subject: [PATCH 61/67] Add JSON schemas (#176) * Add partition table; and otp permissions, whitelabel, contents and settings JSON schemas * Move example json files into json folder, with schemas in json/schemas --- README.md | 14 +- default-pt.json | 36 ---- json/default-pt.json | 37 ++++ .../sample-permissions.json | 1 + sample-wl.json => json/sample-wl.json | 3 +- json/schemas/otp-contents-schema.json | 77 +++++++++ json/schemas/otp-schema.json | 50 ++++++ json/schemas/partition-table-schema.json | 158 ++++++++++++++++++ json/schemas/permissions-schema.json | 52 ++++++ json/schemas/whitelabel-schema.json | 124 ++++++++++++++ main.cpp | 7 +- 11 files changed, 512 insertions(+), 47 deletions(-) delete mode 100644 default-pt.json create mode 100644 json/default-pt.json rename sample-permissions.json => json/sample-permissions.json (79%) rename sample-wl.json => json/sample-wl.json (84%) create mode 100644 json/schemas/otp-contents-schema.json create mode 100644 json/schemas/otp-schema.json create mode 100644 json/schemas/partition-table-schema.json create mode 100644 json/schemas/permissions-schema.json create mode 100644 json/schemas/whitelabel-schema.json diff --git a/README.md b/README.md index bf10a3e..902dfbc 100644 --- a/README.md +++ b/README.md @@ -763,7 +763,7 @@ Family ID 'rp2350-arm-s' can be downloaded in partition 0: ### create This command allows you to create partition tables, and additionally embed them into the block loop if ELF files (for example, for bootloaders). -By default, all partition tables are hashed, and you can also sign them. +By default, all partition tables are hashed, and you can also sign them. The schema for this JSON file is [here](json/schemas/partition-table-schema.json). ```text $ picotool help partition create @@ -907,7 +907,7 @@ The `otp` commands are for interacting with the RP2350 OTP Memory. They are not Note that the OTP Memory is One-Time-Programmable, which means that once a bit has been changed from 0 to 1, it cannot be changed back. Therefore, caution should be used when using these commands, as they risk bricking your RP2350 device. For example, if you set SECURE_BOOT_ENABLE but don't set a boot key, and disable the PICOBOOT interface, then your device will be unusable. -For the `list`, `set`, `get` and `load` commands, you can define your own OTP layout in a JSON file and pass that in with the `-i` argument. These rows will be added to the default rows when parsing. +For the `list`, `set`, `get` and `load` commands, you can define your own OTP layout in a JSON file and pass that in with the `-i` argument. These rows will be added to the default rows when parsing. The schema for this JSON file is [here](json/schemas/otp-contents-schema.json) ```text $ picotool help otp @@ -940,7 +940,7 @@ These commands will set/get specific rows of OTP. By default, they will write/re ### load -This command allows loading of a range of OTP rows onto the device. The source can be a binary file, or a JSON file such as the one output by `picotool sign`. +This command allows loading of a range of OTP rows onto the device. The source can be a binary file, or a JSON file such as the one output by `picotool sign`. The schema for this JSON file is [here](json/schemas/otp-schema.json) For example, if you wish to sign a binary and then test secure boot with it, you can run the following set of commands: ```text $ picotool sign hello_world.elf hello_world.signed.elf private.pem otp.json @@ -952,7 +952,7 @@ $ picotool reboot ### white-label This command allows for OTP white-labelling, which sets the USB configuration used by the device in BOOTSEL mode. -This can be configured from a JSON file, an example of which is in [sample-wl.json](sample-wl.json). +This can be configured from a JSON file, an example of which is in [sample-wl.json](json/sample-wl.json). The schema for this JSON file is [here](json/schemas/whitelabel-schema.json) ```text $ picotool help otp white-label @@ -990,7 +990,7 @@ OPTIONS: ``` ```text -$ picotool otp white-label -s 0x100 ../sample-wl.json +$ picotool otp white-label -s 0x100 sample-wl.json Setting attributes 20e0 0x2e8b, 0x000e, 0x0215, 0x0c09, 0x1090, 0x200c, 0x2615, 0x20e0, 0x310b, 0x3706, 0x3a04, 0x3c04, 0x3e21, 0x4f15, 0x5a0a, 0x5f0a, 0x007a, 0x00df, 0x6c34, 0xd83c, 0xdf4c, 0x0020, 0x0054, 0x0065, 0x0073, 0x0074, 0x0027, 0x0073, 0x0020, 0x0050, 0x0069, 0x0073, 0x6554, 0x7473, 0x5220, 0x3250, 0x3533, 0x3f30, 0x6f6e, 0x6e74, 0x6365, 0x7365, 0x6173, 0x6972, 0x796c, 0x6e61, 0x6d75, 0x6562, 0x0072, 0x6554, 0x7473, 0x6950, 0x4220, 0x6f6f, 0x0074, 0x6554, @@ -1032,7 +1032,7 @@ Device Descriptor: This command will run a binary on your device in order to set the OTP permissions, as these are not directly accessible from `picotool` on due to the default permissions settings required to fix errata XXX on RP2350. Because it runs a binary, the binary needs to be sign it if secure boot is enabled. The binary will print what it is doing over uart, which can be configured using the UART Configuration arguments. You can define your OTP permissions in a json file, an example of which -is in [sample-permissions.json](sample-permissions.json). +is in [sample-permissions.json](json/sample-permissions.json). The schema for this JSON file is [here](json/schemas/permissions-schema.json) ```text $ picotool help otp permissions @@ -1081,7 +1081,7 @@ OPTIONS: ``` ```text -$ picotool otp permissions --sign private.pem --tx 46 ../sample-permissions.json +$ picotool otp permissions --sign private.pem --tx 46 sample-permissions.json Picking file ./xip_ram_perms.elf page10 page10 = 0 diff --git a/default-pt.json b/default-pt.json deleted file mode 100644 index b057d7a..0000000 --- a/default-pt.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "version": [1, 0], - "unpartitioned": { - "families": ["absolute"], - "permissions": { - "secure": "rw", - "nonsecure": "rw", - "bootloader": "rw" - } - }, - "partitions": [ - { - "name": "A", - "id": 0, - "size": "2044K", - "families": ["rp2350-arm-s", "rp2350-riscv"], - "permissions": { - "secure": "rw", - "nonsecure": "rw", - "bootloader": "rw" - } - }, - { - "name": "B", - "id": 1, - "size": "2044K", - "families": ["rp2350-arm-s", "rp2350-riscv"], - "permissions": { - "secure": "rw", - "nonsecure": "rw", - "bootloader": "rw" - }, - "link": ["a", 0] - } - ] -} \ No newline at end of file diff --git a/json/default-pt.json b/json/default-pt.json new file mode 100644 index 0000000..83be3dc --- /dev/null +++ b/json/default-pt.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://raw.githubusercontent.com/raspberrypi/picotool/develop/json/schemas/partition-table-schema.json", + "version": [1, 0], + "unpartitioned": { + "families": ["absolute"], + "permissions": { + "secure": "rw", + "nonsecure": "rw", + "bootloader": "rw" + } + }, + "partitions": [ + { + "name": "A", + "id": 0, + "size": "2044K", + "families": ["rp2350-arm-s", "rp2350-riscv"], + "permissions": { + "secure": "rw", + "nonsecure": "rw", + "bootloader": "rw" + } + }, + { + "name": "B", + "id": 1, + "size": "2044K", + "families": ["rp2350-arm-s", "rp2350-riscv"], + "permissions": { + "secure": "rw", + "nonsecure": "rw", + "bootloader": "rw" + }, + "link": ["a", 0] + } + ] +} diff --git a/sample-permissions.json b/json/sample-permissions.json similarity index 79% rename from sample-permissions.json rename to json/sample-permissions.json index fb125d1..0d01ff1 100644 --- a/sample-permissions.json +++ b/json/sample-permissions.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/raspberrypi/picotool/develop/json/schemas/permissions-schema.json", "10": { "no_key_state": 0, "key_r": 0, diff --git a/sample-wl.json b/json/sample-wl.json similarity index 84% rename from sample-wl.json rename to json/sample-wl.json index 4a74dfd..d48274a 100644 --- a/sample-wl.json +++ b/json/sample-wl.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/raspberrypi/picotool/develop/json/schemas/whitelabel-schema.json", "device": { "vid": "0x2e8b", "pid": "0x000e", @@ -22,4 +23,4 @@ "model": "My Test Pi", "board_id": "TPI-RP2350" } -} \ No newline at end of file +} diff --git a/json/schemas/otp-contents-schema.json b/json/schemas/otp-contents-schema.json new file mode 100644 index 0000000..aa4f7ac --- /dev/null +++ b/json/schemas/otp-contents-schema.json @@ -0,0 +1,77 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "OTP Contents", + "description": "Defined contents of the RP-series device OTP", + "type": "array", + "items": { + "description": "OTP Row", + "type": "object", + "properties": { + "crit": { + "description": "Critical Row (use three-of-eight vote encoding)", + "type": "boolean" + }, + "description": { + "description": "Row Description", + "type": "string" + }, + "ecc": { + "description": "ECC Row", + "type": "boolean" + }, + "fields": { + "description": "Fields within row", + "type": "array", + "items": { + "type": "object", + "properties": { + "description": { + "description": "Field Description", + "type": "string" + }, + "mask": { + "description": "Field Bit Mask", + "type": "integer" + }, + "name": { + "description": "Field Name", + "type": "string" + } + }, + "required": ["description", "mask", "name"], + "additionalProperties": false + } + }, + "mask": { + "description": "Row Bit Mask", + "type": "integer" + }, + "name": { + "description": "Row Name", + "type": "string" + }, + "redundancy": { + "description": "Number of redundant rows", + "type": "integer" + }, + "row": { + "description": "OTP Row", + "type": "integer" + }, + "seq_index": { + "description": "Sequence Index", + "type": "integer" + }, + "seq_length": { + "description": "Sequence Length", + "type": "integer" + }, + "seq_prefix": { + "description": "Sequence Prefix", + "type": "string" + } + }, + "required": ["crit", "description"], + "additionalProperties": false + } +} diff --git a/json/schemas/otp-schema.json b/json/schemas/otp-schema.json new file mode 100644 index 0000000..d59bc3a --- /dev/null +++ b/json/schemas/otp-schema.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "OTP Settings", + "description": "OTP Settings", + "type": "object", + "properties": {"$schema": {}}, + "patternProperties": { + "^\\d{1,2}:\\d{1,2}$": { + "description": "Generic OTP Row", + "type": "object", + "properties": { + "ecc": { + "description": "Protect with ECC", + "type": "boolean" + }, + "value": { + "description": "Value to write", + "type": ["array", "string", "integer"], + "pattern": "^0x[0-9a-fA-F]{1,6}$", + "items": { + "description": "Data Byte", + "type": ["string", "integer"], + "pattern": "^0x[0-9a-fA-F]{1,2}$" + } + } + }, + "additionalProperties": false, + "required": ["ecc", "value"] + }, + "^[\\d\\w_]+$": { + "description": "Defined OTP Row", + "type": ["object", "array", "string", "integer"], + "pattern": "^0x[0-9a-fA-F]{1,6}$", + "items": { + "description": "Data Byte", + "type": ["string", "integer"], + "pattern": "^0x[0-9a-fA-F]{1,2}$" + }, + "patternProperties": { + "^[\\d\\w_]+$": { + "description": "OTP Field", + "type": ["string", "integer"], + "pattern": "^0x[0-9a-fA-F]{1,6}$" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/json/schemas/partition-table-schema.json b/json/schemas/partition-table-schema.json new file mode 100644 index 0000000..92eadcd --- /dev/null +++ b/json/schemas/partition-table-schema.json @@ -0,0 +1,158 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Partition Table", + "description": "Layout of the partition table", + "type": "object", + "properties": { + "$schema": {}, + "version": { + "description": "Partition Table Version", + "type": "array", + "prefixItems": [ + { + "description": "Major Version", + "type": "integer", + "minimum": 0 + }, + { + "description": "Minor Version", + "type": "integer", + "minimum": 0 + } + ] + }, + "unpartitioned": { + "description": "Unpartitioned space UF2 families and permissions", + "type": "object", + "properties": { + "families": { + "description": "UF2 families accepted", + "type": "array", + "items": { + "enum": [ + "data", + "absolute", + "rp2040", + "rp2350-arm-s", + "rp2350-arm-ns", + "rp2350-riscv" + ] + } + }, + "permissions": {"$ref": "#/$defs/permissions"} + }, + "required": ["permissions", "families"], + "additionalProperties": false + }, + "partitions": { + "description": "Partitions", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "description": "Partition Name", + "type": "string" + }, + "id": { + "description": "Partition ID", + "type": ["integer", "string"], + "minimum": 0, + "exclusiveMaximum": 18446744073709551616, + "pattern": "^0x[0-9a-fA-F]{1,16}$", + "examples": [ + "0xDED3FFFF01234567", + 29, + "0xdeadbeef" + ] + }, + "start": { + "description": "Partition Start", + "type": ["integer", "string"], + "minimum": 0, + "pattern": "^\\d+(k|K)$" + }, + "size": { + "description": "Partition Size", + "type": ["integer", "string"], + "minimum": 0, + "pattern": "^\\d+(k|K)$" + }, + "families": { + "description": "UF2 families accepted", + "type": "array", + "items": { + "type": "string", + "pattern": "^data|absolute|rp2040|rp2350-arm-s|rp2350-arm-ns|rp2350-riscv|0x[0-9a-fA-F]{1,8}$", + "examples": [ + "data", + "absolute", + "rp2040", + "rp2350-arm-s", + "rp2350-arm-ns", + "rp2350-riscv" + ] + } + }, + "permissions": {"$ref": "#/$defs/permissions"}, + "link": { + "type": "array", + "prefixItems": [ + { + "description": "Link Type", + "enum": ["a", "owner" , "none"] + }, + { + "description": "Link Value", + "type": "integer" + } + ] + }, + "no_reboot_on_uf2_download": { + "description": "Don't reboot after UF2 is downloaded", + "type": "boolean" + }, + "ab_non_bootable_owner_affinity": { + "description": "Pick the non-bootable owner instead", + "type": "boolean" + }, + "ignored_during_riscv_boot": { + "description": "Ignore this partition during Risc-V boot", + "type": "boolean" + }, + "ignored_during_arm_boot": { + "description": "Ignore this partition during Arm boot", + "type": "boolean" + } + }, + "required": ["size", "permissions", "families"], + "additionalProperties": false + } + } + }, + "required": ["unpartitioned", "partitions"], + "additionalProperties": false, + "$defs": { + "permissions": { + "description": "Permissions", + "type": "object", + "properties": { + "secure": { + "description": "Secure Permissions", + "type": "string", + "pattern": "^(r|w){0,2}$" + }, + "nonsecure": { + "description": "Non-Secure Permissions", + "type": "string", + "pattern": "^(r|w){0,2}$" + }, + "bootloader": { + "description": "Bootloader Permissions", + "type": "string", + "pattern": "^(r|w){0,2}$" + } + } + } + } +} diff --git a/json/schemas/permissions-schema.json b/json/schemas/permissions-schema.json new file mode 100644 index 0000000..7956c62 --- /dev/null +++ b/json/schemas/permissions-schema.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "OTP Permissions", + "description": "Setup of OTP page permissions", + "type": "object", + "properties": {"$schema": {}}, + "patternProperties": { + "^[0-6][0-9]$": { + "description": "OTP Page Permissions", + "type": "object", + "properties": { + "no_key_state": { + "description": "State when at least one key is registered for this page and no matching key has been entered: 0 -> read_only, 1 -> inaccessible", + "type": "integer", + "minimum": 0, + "maximum": 1 + }, + "key_r": { + "description": "Index 1-6 of a hardware key which must be entered to grant read access, or 0 if no such key is required", + "type": "integer", + "minimum": 0, + "maximum": 6 + }, + "key_w": { + "description": "Index 1-6 of a hardware key which must be entered to grant write access, or 0 if no such key is required", + "type": "integer", + "minimum": 0, + "maximum": 6 + }, + "lock_bl": { + "description": "Dummy lock bits reserved for bootloaders (including the RP2350 USB bootloader) to store their own OTP access permissions: 0 -> read_write, 1 -> read_only, 2 -> Do not use (behaves the same as incaccessible), 3 -> inaccessible", + "type": "integer", + "minimum": 0, + "maximum": 3 + }, + "lock_ns": { + "description": "Lock state for Non-secure accesses to this page: 0 -> read_write, 1 -> read_only, 2 -> Do not use (behaves the same as incaccessible), 3 -> inaccessible", + "type": "integer", + "minimum": 0, + "maximum": 3 + }, + "lock_s": { + "description": "Lock state for Secure accesses to this page: 0 -> read_write, 1 -> read_only, 2 -> Do not use (behaves the same as incaccessible), 3 -> inaccessible", + "type": "integer", + "minimum": 0, + "maximum": 3 + } + } + } + }, + "additionalProperties": false +} diff --git a/json/schemas/whitelabel-schema.json b/json/schemas/whitelabel-schema.json new file mode 100644 index 0000000..d96011a --- /dev/null +++ b/json/schemas/whitelabel-schema.json @@ -0,0 +1,124 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "White Labelling", + "description": "White Labelling Configuration, see section 5.7 in the RP2350 datasheet for more details", + "type": "object", + "properties": { + "$schema": {}, + "device": { + "description": "Device Properties", + "type": "object", + "properties": { + "vid": { + "description": "Vendor ID", + "type": "string", + "pattern": "^0x[0-9a-fA-F]{4}$" + }, + "pid": { + "description": "Product ID", + "type": "string", + "pattern": "^0x[0-9a-fA-F]{4}$" + }, + "bcd": { + "description": "Device Revision", + "type": "number", + "minimum": 0, + "maximum": 99 + }, + "lang_id": { + "description": "Language ID", + "type": "string", + "pattern": "^0x[0-9a-fA-F]{4}$" + }, + "manufacturer": { + "description": "Manufacturer Name (can contain unicode)", + "type": "string", + "maxLength": 30 + }, + "product": { + "description": "Product Name (can contain unicode)", + "type": "string", + "maxLength": 30 + }, + "serial_number": { + "description": "Serial Number (can contain unicode)", + "type": "string", + "maxLength": 30 + }, + "max_power": { + "description": "Max power consumption, in 2mA units", + "type": ["integer", "string"], + "maximum": 255, + "pattern": "^0x[0-9a-fA-F]{1,2}$" + }, + "attributes": { + "description": "Device attributes: bit 7 must be 1, bit 6 is self-powered, bit 5 is remote wakeup, bits 0-4 must be 0", + "type": ["integer", "string"], + "minimum": 128, + "maximum": 224, + "pattern": "^0x[8aceACE]{1}0$" + } + }, + "dependentRequired": { + "max_power": ["attributes"], + "attributes": ["max_power"] + }, + "additionalProperties": false + }, + "scsi": { + "description": "SCSI Inquiry Values", + "type": "object", + "properties": { + "vendor": { + "description": "SCSI Vendor", + "type": "string", + "maxLength": 8 + }, + "product": { + "description": "SCSI Product", + "type": "string", + "maxLength": 16 + }, + "version": { + "description": "SCSI Version", + "type": "string", + "maxLength": 4 + } + }, + "additionalProperties": false + }, + "volume": { + "description": "MSD Volume Configuration", + "type": "object", + "properties": { + "label": { + "description": "Volume Label", + "type": "string", + "maxLength": 11 + }, + "redirect_url": { + "description": "INDEX.HTM Redirect URL", + "type": "string", + "maxLength": 127 + }, + "redirect_name": { + "description": "INDEX.HTM Redirect Name", + "type": "string", + "maxLength": 127 + }, + "model": { + "description": "INFO_UF2.TXT Model Name", + "type": "string", + "maxLength": 127 + }, + "board_id": { + "description": "INFO_UF2.TXT Board ID", + "type": "string", + "maxLength": 127 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/main.cpp b/main.cpp index 4000168..78ff0a1 100644 --- a/main.cpp +++ b/main.cpp @@ -7299,19 +7299,20 @@ bool otp_white_label_command::execute(device_map &devices) { // Check for separate max_power and attributes uint16_t val = 0; int hex_val = 0; - if (wl_json["device"].contains("max_power")) { + if (wl_json["device"].contains("max_power") && wl_json["device"].contains("attributes")) { if (!get_json_int(wl_json["device"]["max_power"], hex_val)) { fail(ERROR_FORMAT, "MaxPower must be an integer"); } val |= (hex_val << 8); - } - if (wl_json["device"].contains("attributes")) { + if (!get_json_int(wl_json["device"]["attributes"], hex_val)) { fail(ERROR_FORMAT, "Device Attributes must be an integer"); } else if (hex_val & 0b11111 || ~hex_val & 0x80) { fail(ERROR_FORMAT, "Device Attributes must have bit 7 set (0x80), and bits 4-0 clear"); } val |= hex_val; + } else if (wl_json["device"].contains("max_power") || wl_json["device"].contains("attributes")) { + fail(ERROR_INCOMPATIBLE, "Must specify both max_power and attributes in the JSON file"); } if (val) { fos << "Setting attributes " << hex_string(val, 4) << "\n"; From 081a386153a58d83dcc2a8f0bd11580be6fb4436 Mon Sep 17 00:00:00 2001 From: will-v-pi <108662275+will-v-pi@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:09:17 +0000 Subject: [PATCH 62/67] Add an entry point when signing Arm images (#163) Reads the entry point and stack pointer from the vector table --- main.cpp | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/main.cpp b/main.cpp index 78ff0a1..505d27b 100644 --- a/main.cpp +++ b/main.cpp @@ -4607,6 +4607,41 @@ void sign_guts_elf(elf_file* elf, private_t private_key, public_t public_key) { new_block.items.push_back(version); } + // Add entry point when signing Arm images + std::shared_ptr image_type = new_block.get_item(); + if (settings.seal.sign && image_type != nullptr && image_type->image_type() == type_exe && image_type->cpu() == cpu_arm) { + std::shared_ptr entry_point = new_block.get_item(); + if (entry_point == nullptr) { + std::shared_ptr vtor = new_block.get_item(); + uint32_t vtor_loc = 0x10000000; + if (vtor != nullptr) { + vtor_loc = vtor->addr; + } else { + if (elf->header().entry >= SRAM_START) { + vtor_loc = 0x20000000; + } else if (elf->header().entry >= XIP_SRAM_START_RP2350) { + vtor_loc = 0x13ffc000; + } else { + vtor_loc = 0x10000000; + std::shared_ptr rwd = new_block.get_item(); + if (rwd != nullptr) { + vtor_loc += rwd->addr; + } + } + } + auto segment = elf->segment_from_physical_address(vtor_loc); + auto content = elf->content(*segment); + auto offset = vtor_loc - segment->physical_address(); + uint32_t ep; + memcpy(&ep, content.data() + offset + 4, sizeof(ep)); + uint32_t sp; + memcpy(&sp, content.data() + offset, sizeof(sp)); + DEBUG_LOG("Adding entry_point_item: ep %08x, sp %08x\n", ep, sp); + entry_point = std::make_shared(ep, sp); + new_block.items.push_back(entry_point); + } + } + hash_andor_sign( elf, &new_block, public_key, private_key, settings.seal.hash, settings.seal.sign, @@ -4643,6 +4678,27 @@ vector sign_guts_bin(iostream_memory_access in, private_t private_key, new_block.items.push_back(version); } + // Add entry point when signing Arm images + std::shared_ptr image_type = new_block.get_item(); + if (settings.seal.sign && image_type != nullptr && image_type->image_type() == type_exe && image_type->cpu() == cpu_arm) { + std::shared_ptr entry_point = new_block.get_item(); + if (entry_point == nullptr) { + std::shared_ptr vtor = new_block.get_item(); + uint32_t vtor_loc = bin_start; + if (vtor != nullptr) { + vtor_loc = vtor->addr; + } + auto offset = vtor_loc - bin_start; + uint32_t ep; + memcpy(&ep, bin.data() + offset + 4, sizeof(ep)); + uint32_t sp; + memcpy(&sp, bin.data() + offset, sizeof(sp)); + DEBUG_LOG("Adding entry_point_item: ep %08x, sp %08x\n", ep, sp); + entry_point = std::make_shared(ep, sp); + new_block.items.push_back(entry_point); + } + } + auto sig_data = hash_andor_sign( bin, bin_start, bin_start, &new_block, public_key, private_key, From 4a403bb27795bff005d49531fa77107fd33ca35e Mon Sep 17 00:00:00 2001 From: will-v-pi <108662275+will-v-pi@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:10:27 +0000 Subject: [PATCH 63/67] Add all metadata blocks to info (#173) * Add info for all metadata blocks with -m * Add printing of bootloader info --- bintool/metadata.h | 53 +++++- main.cpp | 461 ++++++++++++++++++++++++++++++--------------- 2 files changed, 360 insertions(+), 154 deletions(-) diff --git a/bintool/metadata.h b/bintool/metadata.h index eb45984..605bf63 100644 --- a/bintool/metadata.h +++ b/bintool/metadata.h @@ -225,8 +225,55 @@ struct partition_table_item : public single_byte_size_item { partition_table_item() = default; explicit partition_table_item(uint32_t unpartitioned_flags, bool singleton) : unpartitioned_flags(unpartitioned_flags), singleton(singleton) {} - template static std::shared_ptr parse(I it, I end, uint32_t header) { - return nullptr; + template static std::shared_ptr parse(I& it, I end, uint32_t header) { + uint32_t size = decode_size(header); + uint8_t singleton_count = header >> 24; + bool singleton = singleton_count & 0x80; + uint8_t partition_count = singleton_count & 0x0f; + uint32_t unpartitioned_flags = *it++; + + auto pt = std::make_shared(unpartitioned_flags, singleton); + + std::vector data; + for (unsigned int i=2; i < size; i++) { + data.push_back(*it++); + } + int i=0; + while (i < data.size()) { + partition new_p; + uint32_t permissions_locations = data[i++]; + new_p.permissions = (permissions_locations & PICOBIN_PARTITION_PERMISSIONS_BITS) >> PICOBIN_PARTITION_PERMISSIONS_LSB; + new_p.first_sector = (permissions_locations & PICOBIN_PARTITION_LOCATION_FIRST_SECTOR_BITS) >> PICOBIN_PARTITION_LOCATION_FIRST_SECTOR_LSB; + new_p.last_sector = (permissions_locations & PICOBIN_PARTITION_LOCATION_LAST_SECTOR_BITS) >> PICOBIN_PARTITION_LOCATION_LAST_SECTOR_LSB; + + uint32_t permissions_flags = data[i++]; + uint8_t permissions2 = (permissions_flags & PICOBIN_PARTITION_PERMISSIONS_BITS) >> PICOBIN_PARTITION_PERMISSIONS_LSB; + if (new_p.permissions != permissions2) { + printf("Permissions mismatch %02x %02x\n", new_p.permissions, permissions2); + assert(false); + } + new_p.flags = permissions_flags & (~PICOBIN_PARTITION_PERMISSIONS_BITS); + + if (new_p.flags & PICOBIN_PARTITION_FLAGS_HAS_ID_BITS) { + new_p.id = (uint64_t)data[i++] | ((uint64_t)data[i++] << 32); + } + + uint8_t num_extra_families = (new_p.flags & PICOBIN_PARTITION_FLAGS_ACCEPTS_NUM_EXTRA_FAMILIES_BITS) >> PICOBIN_PARTITION_FLAGS_ACCEPTS_NUM_EXTRA_FAMILIES_LSB; + for (int fam=0; fam < num_extra_families; fam++) { + new_p.extra_families.push_back(data[i++]); + } + + if (new_p.flags & PICOBIN_PARTITION_FLAGS_HAS_NAME_BITS) { + auto bytes = words_to_lsb_bytes(data.begin() + i++, data.end()); + int name_size = bytes[0]; + // This works neatly - accounts for the size byte at the start + i += name_size / 4; + new_p.name = std::string((char*)(bytes.data() + 1), name_size); + } + + pt->partitions.push_back(new_p); + } + return pt; } std::vector to_words(item_writer_context& ctx) const override { @@ -574,7 +621,7 @@ struct block { i = image_type_item::parse(it, end, header); break; case PICOBIN_BLOCK_ITEM_PARTITION_TABLE: - i = ignored_item::parse(it, end, header); + i = partition_table_item::parse(it, end, header); break; case PICOBIN_BLOCK_ITEM_1BS_VECTOR_TABLE: i = vector_table_item::parse(it, end, header); diff --git a/main.cpp b/main.cpp index 505d27b..e769afb 100644 --- a/main.cpp +++ b/main.cpp @@ -434,6 +434,7 @@ struct _settings { struct { bool show_basic = false; bool all = false; + bool show_metadata = false; bool show_pins = false; bool show_device = false; bool show_debug = false; @@ -598,6 +599,7 @@ struct info_command : public cmd { return ( ( option('b', "--basic").set(settings.info.show_basic) % "Include basic information. This is the default" + + option('m', "--metadata").set(settings.info.show_metadata) % "Include all metadata blocks" + option('p', "--pins").set(settings.info.show_pins) % "Include pin information" + option('d', "--device").set(settings.info.show_device) % "Include device information" + option("--debug").set(settings.info.show_debug) % "Include device debug information" + @@ -874,9 +876,6 @@ struct partition_info_command : public cmd { string get_doc() const override { return "Print the device's partition table."; } - - void print_permissions(unsigned int p) const; - void insert_default_families(uint32_t flags_and_permissions, vector &family_ids) const; }; #endif @@ -921,9 +920,6 @@ struct partition_create_command : public cmd { string get_doc() const override { return "Create a partition table from json"; } - - void print_permissions(unsigned int p) const; - void insert_default_families(uint32_t flags_and_permissions, vector &family_ids) const; }; @@ -2921,6 +2917,39 @@ std::shared_ptr get_bi_access(memory_access &raw_access) { return std::make_shared(raw_access, rmap); } +string str_permissions(unsigned int p) { + static_assert(PICOBIN_PARTITION_PERMISSION_S_R_BITS == (1u << 26), ""); + static_assert(PICOBIN_PARTITION_PERMISSION_S_W_BITS == (1u << 27), ""); + static_assert(PICOBIN_PARTITION_PERMISSION_NS_W_BITS == (1u << 29), ""); + static_assert(PICOBIN_PARTITION_PERMISSION_NSBOOT_W_BITS == (1u << 31), ""); + + std::stringstream ss; + ss << " S("; + unsigned int r = (p >> 26) & 3; + if (r & 1) ss << "r"; + if (r & 2) ss << "w"; else if (!r) ss << "-"; + ss << ") NSBOOT("; + r = (p >> 30) & 3; + if (r & 1) ss << "r"; + if (r & 2) ss << "w"; else if (!r) ss << "-"; + ss << ") NS("; + r = (p >> 28) & 3; + if (r & 1) ss << "r"; + if (r & 2) ss << "w"; else if (!r) ss << "-"; + ss << ")"; + + return ss.str(); +} + +void insert_default_families(uint32_t flags_and_permissions, vector &family_ids) { + if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_ABSOLUTE_BITS) family_ids.emplace_back(absolute_family_name); + if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_RP2040_BITS) family_ids.emplace_back(rp2040_family_name); + if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_RP2350_ARM_S_BITS) family_ids.emplace_back(rp2350_arm_s_family_name); + if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_RP2350_ARM_NS_BITS) family_ids.emplace_back(rp2350_arm_ns_family_name); + if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_RP2350_RISCV_BITS) family_ids.emplace_back(rp2350_riscv_family_name); + if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_DATA_BITS) family_ids.emplace_back(data_family_name); +} + #if HAS_LIBUSB void info_guts(memory_access &raw_access, picoboot::connection *con) { #else @@ -2936,12 +2965,19 @@ void info_guts(memory_access &raw_access, void *con) { vector groups; string current_group; map>> infos; - auto select_group = [&](const group &g1, bool enabled = true) { + // Set enable to true to enable the selected group + auto select_group = [&](const group &g1, bool enable = false) { if (std::find_if(groups.begin(), groups.end(), [&](const group &g2) { return g1.name == g2.name; }) == groups.end()) { groups.push_back(g1); } + auto enable_changed = std::find_if(groups.begin(), groups.end(), [&](const group &g2) { + return g1.name == g2.name && (enable && !g2.enabled); + }); + if (enable_changed != groups.end()) { + enable_changed->enabled = true; + } current_group = g1.name; }; auto info_pair = [&](const string &name, const string &value) { @@ -2950,15 +2986,221 @@ void info_guts(memory_access &raw_access, void *con) { infos[current_group].emplace_back(std::make_pair(name, value)); } }; + auto info_metadata = [&](std::vector bin, block *current_block, bool verbose_metadata = false) { + verified_t hash_verified = none; + verified_t sig_verified = none; + #if HAS_MBEDTLS + verify_block(bin, raw_access.get_binary_start(), raw_access.get_binary_start(), current_block, hash_verified, sig_verified); + #endif + + // Addresses + if (verbose_metadata) { + info_pair("address", hex_string(current_block->physical_addr)); + info_pair("next block address", hex_string(current_block->next_block_rel + current_block->physical_addr)); + if (current_block->get_item() != nullptr) info_pair("block type", "ignored"); + } + + // Image Def + auto image_def = current_block->get_item(); + if (image_def != nullptr) { + if (verbose_metadata) info_pair("block type", "image def"); + if (image_def->image_type() == type_exe) { + switch (image_def->chip()) { + case chip_rp2040: + info_pair("target chip", "RP2040"); + break; + case chip_rp2350: + info_pair("target chip", "RP2350"); + switch (image_def->cpu()) { + case cpu_riscv: + info_pair("image type", "RISC-V"); + break; + case cpu_varmulet: + info_pair("image type", "Varmulet"); + break; + case cpu_arm: + if (image_def->security() == sec_s) { + info_pair("image type", "ARM Secure"); + } else if (image_def->security() == sec_ns) { + info_pair("image type", "ARM Non-Secure"); + } else if (image_def->security() == sec_unspecified) { + info_pair("image type", "ARM"); + } + } + break; + default: + break; + } + } else if (image_def->image_type() == type_data) { + info_pair("image type", "data"); + } + } + + // Partition Table + auto partition_table = current_block->get_item(); + if (partition_table != nullptr) { + if (verbose_metadata) info_pair("block type", "partition table"); + info_pair("partition table", partition_table->singleton ? "singleton" : "non-singleton"); + std::stringstream unpartitioned; + unpartitioned << str_permissions(partition_table->unpartitioned_flags); + std::vector family_ids; + insert_default_families(partition_table->unpartitioned_flags, family_ids); + unpartitioned << ", uf2 { " << cli::join(family_ids, ", ") << " }"; + info_pair("un-partitioned space", unpartitioned.str()); + + for (int i=0; i < partition_table->partitions.size(); i++) { + std::stringstream pstring; + std::stringstream pname; + auto partition = partition_table->partitions[i]; + uint32_t flags = partition.flags; + uint64_t id = partition.id; + pname << "partition " << i; + if ((flags & PICOBIN_PARTITION_FLAGS_LINK_TYPE_BITS) == + PICOBIN_PARTITION_FLAGS_LINK_TYPE_AS_BITS(A_PARTITION)) { + pname << " (B w/ " << ((flags & PICOBIN_PARTITION_FLAGS_LINK_VALUE_BITS) + >> PICOBIN_PARTITION_FLAGS_LINK_VALUE_LSB) + << ")"; + } else if ((flags & PICOBIN_PARTITION_FLAGS_LINK_TYPE_BITS) == + PICOBIN_PARTITION_FLAGS_LINK_TYPE_AS_BITS(OWNER_PARTITION)) { + pname << " (A ob/ " << ((flags & PICOBIN_PARTITION_FLAGS_LINK_VALUE_BITS) + >> PICOBIN_PARTITION_FLAGS_LINK_VALUE_LSB) + << ")"; + } else { + pname << " (A)"; + } + pstring << hex_string(partition.first_sector * 4096, 8, false) << "->" << hex_string((partition.last_sector + 1) * 4096, 8, false); + unsigned int p = partition.permissions; + pstring << str_permissions(p << PICOBIN_PARTITION_PERMISSIONS_LSB); + if (flags & PICOBIN_PARTITION_FLAGS_HAS_ID_BITS) { + pstring << ", id=" << hex_string(id, 16, false); + } + uint32_t num_extra_families = partition.extra_families.size(); + family_ids.clear(); + insert_default_families(flags, family_ids); + for (auto family : partition.extra_families) { + family_ids.emplace_back(hex_string(family)); + } + if (flags & PICOBIN_PARTITION_FLAGS_HAS_NAME_BITS) { + pstring << ", \""; + pstring << partition.name; + pstring << '"'; + } + pstring << ", uf2 { " << cli::join(family_ids, ", ") << " }"; + pstring << ", arm_boot " << !(flags & PICOBIN_PARTITION_FLAGS_IGNORED_DURING_ARM_BOOT_BITS); + pstring << ", riscv_boot " << !(flags & PICOBIN_PARTITION_FLAGS_IGNORED_DURING_RISCV_BOOT_BITS); + info_pair(pname.str(), pstring.str()); + } + } + + // Version + auto version = current_block->get_item(); + if (version != nullptr) { + info_pair("version", std::to_string(version->major) + "." + std::to_string(version->minor)); + if (version->otp_rows.size() > 0) { + info_pair("rollback version", std::to_string(version->rollback)); + std::stringstream rows; + for (const auto row : version->otp_rows) { rows << hex_string(row, 3) << " "; } + info_pair("rollback rows", rows.str()); + } + } + + if (verbose_metadata) { + // Load Map + // todo what should this really report + auto load_map = current_block->get_item(); + if (load_map != nullptr) { + for (unsigned int i=0; i < load_map->entries.size(); i++) { + std::stringstream ss; + auto e = load_map->entries[i]; + if (e.storage_address == 0) { + ss << "Clear 0x" << std::hex << e.runtime_address; + ss << "->0x" << std::hex << e.runtime_address + e.size; + } else if (e.storage_address != e.runtime_address) { + if (is_address_initialized(rp2350_address_ranges_flash, e.runtime_address)) { + ss << "ERROR: COPY TO FLASH NOT PERMITTED "; + } + ss << "Copy 0x" << std::hex << e.storage_address; + ss << "->0x" << std::hex << e.storage_address + e.size; + ss << " to 0x" << std::hex << e.runtime_address; + ss << "->0x" << std::hex << e.runtime_address + e.size; + } else { + ss << "Load 0x" << std::hex << e.storage_address; + ss << "->0x" << std::hex << e.storage_address + e.size; + } + info_pair("load map entry " + std::to_string(i), ss.str()); + } + } + + // Rolling Window Delta + auto rwd = current_block->get_item(); + if (rwd != nullptr) { + info_pair("rolling window delta", hex_string(rwd->addr)); + } + + // Vector Table + auto vtor = current_block->get_item(); + if (vtor != nullptr) { + info_pair("vector table", hex_string(vtor->addr)); + } + + // Entry Point + auto entry_point = current_block->get_item(); + if (entry_point != nullptr) { + std::stringstream ss; + ss << "EP " << hex_string(entry_point->ep); + ss << ", SP " << hex_string(entry_point->sp); + if (entry_point->splim_set) ss << ", SPLIM " << hex_string(entry_point->splim); + info_pair("entry point", ss.str()); + } + } + + // Hash and Sig + if (hash_verified != none) { + info_pair("hash", hash_verified == passed ? "verified" : "incorrect"); + if (verbose_metadata) { + std::shared_ptr hash_value = current_block->get_item(); + assert(hash_value != nullptr); // verify_block would return none if it's not present + std::stringstream val; + for(uint8_t i : hash_value->hash_bytes) { + val << hex_string(i, 2, false, true); + } + info_pair("hash value", val.str()); + } + } + if (sig_verified != none) { + info_pair("signature", sig_verified == passed ? "verified" : "incorrect"); + if (verbose_metadata) { + std::shared_ptr signature = current_block->get_item(); + assert(signature != nullptr); // verify_block would return none if it's not present + std::stringstream sig; + for(uint8_t i : signature->signature_bytes) { + sig << hex_string(i, 2, false, true); + } + info_pair("signature value", sig.str()); + std::stringstream pkey; + for(uint8_t i : signature->public_key_bytes) { + pkey << hex_string(i, 2, false, true); + } + info_pair("public key", pkey.str()); + } + } + }; // establish core groups and their order - if (!settings.info.show_basic && !settings.info.all && !settings.info.show_pins && !settings.info.show_device && !settings.info.show_debug && !settings.info.show_build) { + if (!settings.info.show_basic && !settings.info.all && !settings.info.show_metadata && !settings.info.show_pins && !settings.info.show_device && !settings.info.show_debug && !settings.info.show_build) { settings.info.show_basic = true; } if (settings.info.show_debug && !settings.info.show_device) { settings.info.show_device = true; } auto program_info = group("Program Information", settings.info.show_basic || settings.info.all); + auto no_metadata_info = group("Metadata Blocks", false); + vector metadata_info; + #define MAX_METADATA_BLOCKS 10 + for (int i=1; i <= MAX_METADATA_BLOCKS; i++) { + // These groups are enabled later, depending on how many metadata blocks the binary has + metadata_info.push_back(group("Metadata Block " + std::to_string(i), false)); + } auto pin_info = group("Fixed Pin Information", settings.info.show_pins || settings.info.all); auto build_info = group("Build Information", settings.info.show_build || settings.info.all); auto device_info = group("Device Information", (settings.info.show_device || settings.info.all) & raw_access.is_device()); @@ -2966,6 +3208,9 @@ void info_guts(memory_access &raw_access, void *con) { select_group(program_info); select_group(pin_info); select_group(build_info); + for (auto mb : metadata_info) { + select_group(mb); + } select_group(device_info); binary_info_header hdr; try { @@ -3118,116 +3363,35 @@ void info_guts(memory_access &raw_access, void *con) { } } vector bin; + if (settings.info.show_metadata || settings.info.all) { + 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 new_size) { + DEBUG_LOG("Now reading from %x size %x\n", raw_access.get_binary_start(), new_size); + bin = raw_access.read_vector(raw_access.get_binary_start(), new_size, true); + }; + auto all_blocks = get_all_blocks(bin, raw_access.get_binary_start(), first_block, more_cb); + + int block_i = 0; + select_group(metadata_info[block_i++], true); + info_metadata(bin, first_block.get(), true); + for (auto &block : all_blocks) { + select_group(metadata_info[block_i++], true); + info_metadata(bin, block.get(), true); + } + } else { + // This displays that there are no metadata blocks + select_group(no_metadata_info, true); + } + } std::unique_ptr best_block = find_best_block(raw_access, bin); if (best_block && (settings.info.show_basic || settings.info.all)) { select_group(program_info); - verified_t hash_verified = none; - verified_t sig_verified = none; - #if HAS_MBEDTLS - verify_block(bin, raw_access.get_binary_start(), raw_access.get_binary_start(), best_block.get(), hash_verified, sig_verified); - #endif - - // Image Def - auto image_def = best_block->get_item(); - if (image_def != nullptr) { - if (image_def->image_type() == type_exe) { - switch (image_def->chip()) { - case chip_rp2040: - info_pair("target chip", "RP2040"); - break; - case chip_rp2350: - info_pair("target chip", "RP2350"); - switch (image_def->cpu()) { - case cpu_riscv: - info_pair("image type", "RISC-V"); - break; - case cpu_varmulet: - info_pair("image type", "Varmulet"); - break; - case cpu_arm: - if (image_def->security() == sec_s) { - info_pair("image type", "ARM Secure"); - } else if (image_def->security() == sec_ns) { - info_pair("image type", "ARM Non-Secure"); - } else if (image_def->security() == sec_unspecified) { - info_pair("image type", "ARM"); - } - } - break; - default: - break; - } - } else if (image_def->image_type() == type_data) { - info_pair("image type", "data"); - } - } - - // Version - auto version = best_block->get_item(); - if (version != nullptr) { - info_pair("version", std::to_string(version->major) + "." + std::to_string(version->minor)); - if (version->otp_rows.size() > 0) { - info_pair("rollback version", std::to_string(version->rollback)); - std::stringstream rows; - for (const auto row : version->otp_rows) { rows << hex_string(row, 3) << " "; } - info_pair("rollback rows", rows.str()); - } - } - - // Load Map - // todo what should this really report - auto load_map = best_block->get_item(); - if (load_map != nullptr) { - for (unsigned int i=0; i < load_map->entries.size(); i++) { - std::stringstream ss; - auto e = load_map->entries[i]; - if (e.storage_address == 0) { - ss << "Clear 0x" << std::hex << e.runtime_address; - ss << "+0x" << std::hex << e.size; - } else if (e.storage_address != e.runtime_address) { - if (is_address_initialized(rp2350_address_ranges_flash, e.runtime_address)) { - ss << "ERROR: COPY TO FLASH NOT PERMITTED "; - } - ss << "Copy 0x" << std::hex << e.storage_address; - ss << "+0x" << std::hex << e.size; - ss << " to 0x" << std::hex << e.runtime_address; - } else { - ss << "Load 0x" << std::hex << e.storage_address; - ss << "+0x" << std::hex << e.size; - } - info_pair("load map entry " + std::to_string(i), ss.str()); - } - } - - // Rolling Window Delta - auto rwd = best_block->get_item(); - if (rwd != nullptr) { - info_pair("rolling window delta", hex_string(rwd->addr)); - } - - // Vector Table - auto vtor = best_block->get_item(); - if (vtor != nullptr) { - info_pair("vector table", hex_string(vtor->addr)); - } - - // Entry Point - auto entry_point = best_block->get_item(); - if (entry_point != nullptr) { - std::stringstream ss; - ss << "EP " << hex_string(entry_point->ep); - ss << ", SP " << hex_string(entry_point->sp); - if (entry_point->splim_set) ss << ", SPLIM " << hex_string(entry_point->splim); - info_pair("entry point", ss.str()); - } - - // Hash and Sig - if (hash_verified != none) { - info_pair("hash value", hash_verified == passed ? "verified" : "incorrect"); - } - if (sig_verified != none) { - info_pair("signature", sig_verified == passed ? "verified" : "incorrect"); - } + info_metadata(bin, best_block.get()); } else if (!best_block && has_binary_info && get_model(raw_access) == rp2350) { fos << "WARNING: Binary on RP2350 device does not contain a block loop - this binary will not boot\n"; } @@ -3409,6 +3573,19 @@ void info_guts(memory_access &raw_access, void *con) { #endif bool first = true; int fr_col = fos.first_column(); + // Standardise indent for whole info printout + int tab = 0; + for(const auto& group : groups) { + if (group.enabled) { + const auto& info = infos[group.name]; + if (!info.empty()) { + tab = std::max(tab, group.min_tab); + for(const auto& item : info) { + tab = std::max(tab, 3 + (int)item.first.length()); // +3 for ": " + } + } + } + } for(const auto& group : groups) { if (group.enabled) { const auto& info = infos[group.name]; @@ -3424,10 +3601,6 @@ void info_guts(memory_access &raw_access, void *con) { if (info.empty()) { fos << "none\n"; } else { - int tab = group.min_tab; - for(const auto& item : info) { - tab = std::max(tab, 3 + (int)item.first.length()); // +3 for ": " - } for(const auto& item : info) { fos.first_column(fr_col + 1); fos << (item.first + ":"); @@ -3856,6 +4029,11 @@ bool info_command::execute(device_map &devices) { auto partitions = get_partitions(connection); vector starts; if (partitions) { + // Check if bootloader is present, based on presence of binary info + binary_info_header hdr; + auto bi_access = get_bi_access(access); + bool has_bootloader = find_binary_info(*bi_access, hdr); + // Don't show device, until all partitions done bool device = settings.info.show_device || settings.info.all; bool debug = settings.info.show_debug || settings.info.all; @@ -3863,14 +4041,23 @@ bool info_command::execute(device_map &devices) { settings.info.show_basic = true; settings.info.show_pins = true; settings.info.show_build = true; + settings.info.show_metadata = true; settings.info.all = false; } - if ((settings.info.show_basic || settings.info.show_pins || settings.info.show_build) || !(settings.info.show_device || settings.info.show_debug)) { + if ((settings.info.show_basic || settings.info.show_pins || settings.info.show_build || settings.info.show_metadata) || !(settings.info.show_device || settings.info.show_debug)) { settings.info.show_device = false; settings.info.show_debug = false; for (auto range : *partitions) { starts.push_back(std::get<0>(range)); } + if (has_bootloader && std::none_of(starts.cbegin(), starts.cend(), [](int i) { return i == 0; })) { + // Print bootloader info, only if bootloader is present and not in a partition + fos.first_column(0); fos.hanging_indent(0); + fos << "\nBootloader\n"; + fos.first_column(1); + partition_memory_access part_access(access, 0); + info_guts(part_access, &connection); + } for (unsigned int i=0; i < starts.size(); i++) { uint32_t start = starts[i]; fos.first_column(0); fos.hanging_indent(0); @@ -3886,6 +4073,7 @@ bool info_command::execute(device_map &devices) { settings.info.show_basic = false; settings.info.show_pins = false; settings.info.show_build = false; + settings.info.show_metadata = false; settings.info.show_device = device; settings.info.show_debug = debug; info_guts(access, &connection); @@ -5613,7 +5801,7 @@ bool partition_info_command::execute(device_map &devices) { printf("the partition table is empty\n"); } printf("un-partitioned_space : "); - print_permissions(unpartitioned.permissions_and_flags); + fos << str_permissions(unpartitioned.permissions_and_flags); std::vector family_ids; insert_default_families(unpartitioned.permissions_and_flags, family_ids); printf(", uf2 { %s }\n", cli::join(family_ids, ", ").c_str()); @@ -5653,7 +5841,7 @@ bool partition_info_command::execute(device_map &devices) { return -1; } unsigned int p = location_and_permissions & flags_and_permissions; - print_permissions(p); + fos << str_permissions(p); if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_HAS_ID_BITS) { printf(", id=%016" PRIx64, id); } @@ -6721,35 +6909,6 @@ bool coprodis_command::execute(device_map &devices) { #if HAS_LIBUSB -void partition_info_command::insert_default_families(uint32_t flags_and_permissions, vector &family_ids) const { - if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_ABSOLUTE_BITS) family_ids.emplace_back(absolute_family_name); - if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_RP2040_BITS) family_ids.emplace_back(rp2040_family_name); - if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_RP2350_ARM_S_BITS) family_ids.emplace_back(rp2350_arm_s_family_name); - if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_RP2350_ARM_NS_BITS) family_ids.emplace_back(rp2350_arm_ns_family_name); - if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_RP2350_RISCV_BITS) family_ids.emplace_back(rp2350_riscv_family_name); - if (flags_and_permissions & PICOBIN_PARTITION_FLAGS_ACCEPTS_DEFAULT_FAMILY_DATA_BITS) family_ids.emplace_back(data_family_name); -} - -void partition_info_command::print_permissions(unsigned int p) const { - static_assert(PICOBIN_PARTITION_PERMISSION_S_R_BITS == (1u << 26), ""); - static_assert(PICOBIN_PARTITION_PERMISSION_S_W_BITS == (1u << 27), ""); - static_assert(PICOBIN_PARTITION_PERMISSION_NS_W_BITS == (1u << 29), ""); - static_assert(PICOBIN_PARTITION_PERMISSION_NSBOOT_W_BITS == (1u << 31), ""); - printf(" S("); - unsigned int r = (p >> 26) & 3; - if (r & 1) printf("r"); - if (r & 2) printf("w"); else if (!r) printf("-"); - printf(") NSBOOT("); - r = (p >> 30) & 3; - if (r & 1) printf("r"); - if (r & 2) printf("w"); else if (!r) printf("-"); - printf(") NS("); - r = (p >> 28) & 3; - if (r & 1) printf("r"); - if (r & 2) printf("w"); else if (!r) printf("-"); - printf(")"); -} - static void check_otp_write_error(picoboot::command_failure &e, bool ecc) { if (e.get_code() == PICOBOOT_UNSUPPORTED_MODIFICATION) { if (ecc) fail(ERROR_NOT_POSSIBLE, "Attempted to modify OTP ECC row(s)\n"); From fa69a49bfb92fa56d44e29739b05340a8e2d2ee2 Mon Sep 17 00:00:00 2001 From: will-v-pi <108662275+will-v-pi@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:13:11 +0000 Subject: [PATCH 64/67] Add option to ignore already-set bits in otp set command (#175) Adds -s, --set-bits option to otp set command --- main.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/main.cpp b/main.cpp index e769afb..1f75cd3 100644 --- a/main.cpp +++ b/main.cpp @@ -417,6 +417,7 @@ struct _settings { int redundancy = -1; bool raw = false; bool ecc = false; + bool ignore_set = false; bool fuzzy = false; uint32_t value = 0; uint8_t lock0 = 0; @@ -1067,6 +1068,7 @@ struct otp_set_command : public cmd { (option('c', "--copies") & integer("copies").min(1).set(settings.otp.redundancy)) % "Read multiple redundant values" + option('r', "--raw").set(settings.otp.raw) % "Set raw 24 bit values" + option('e', "--ecc").set(settings.otp.ecc) % "Use error correction" + + option('s', "--set-bits").set(settings.otp.ignore_set) % "Set bits only" + (option('i', "--include") & value("filename").add_to(settings.otp.extra_files)).min(0).max(1) % "Include extra otp definition" // todo more than 1 ).min(0).doc_non_optional(true) % "Redundancy/Error Correction Overrides" + ( @@ -7318,6 +7320,10 @@ bool otp_set_command::execute(device_map &devices) { settings.otp.value &= field->mask; settings.otp.value |= old_raw_value & ~field->mask; } + if (settings.otp.ignore_set) { + // OR with current value, to ignore any already-set bits + settings.otp.value |= old_raw_value; + } // todo check for clearing bits if (old_raw_value && settings.otp.ecc) { fail(ERROR_NOT_POSSIBLE, "Cannot modify OTP ECC row(s)\n"); From 6c3f0901c48f586a9471572e0c6601d0a7ecefd3 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Thu, 21 Nov 2024 16:34:43 +0000 Subject: [PATCH 65/67] Update help commands in the README to show their current output --- README.md | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 902dfbc..3f79cc9 100644 --- a/README.md +++ b/README.md @@ -105,14 +105,14 @@ Note for additional documentation see https://rptl.io/pico-get-started ``` $ picotool help PICOTOOL: -Tool for interacting with RP2040/RP2350 device(s) in BOOTSEL mode, or with an RP2040/RP2350 binary + Tool for interacting with RP-series device(s) in BOOTSEL mode, or with an RP-series binary SYNOPSIS: - picotool info [-b] [-p] [-d] [--debug] [-l] [-a] [device-selection] - picotool info [-b] [-p] [-d] [--debug] [-l] [-a] [-t ] + picotool info [-b] [-m] [-p] [-d] [--debug] [-l] [-a] [device-selection] + picotool info [-b] [-m] [-p] [-d] [--debug] [-l] [-a] [-t ] picotool config [-s ] [-g ] [device-selection] picotool config [-s ] [-g ] [-t ] - picotool load [-p] [-n] [-N] [-u] [-v] [-x] [-t ] [-o ] [device-selection] + picotool load [--ignore-partitions] [--family ] [-p ] [-n] [-N] [-u] [-v] [-x] [-t ] [-o ] [device-selection] picotool encrypt [--quiet] [--verbose] [--hash] [--sign] [-t ] [-o ] [-t ] [-t ] [] [-t ] picotool seal [--quiet] [--verbose] [--hash] [--sign] [--clear] [-t ] [-o ] [-t ] [] [-t ] [] [-t ] [--major ] [--minor ] [--rollback [..]] picotool link [--quiet] [--verbose] [-t ] [-t ] [-t ] [] [-t ] [-p] @@ -120,7 +120,7 @@ SYNOPSIS: picotool save -a [-v] [--family ] [device-selection] picotool save -r [-v] [--family ] [device-selection] picotool erase [-a] [device-selection] - picotool erase [-p ] [device-selection] + picotool erase -p [device-selection] picotool erase -r [device-selection] picotool verify [device-selection] picotool reboot [-a] [-u] [-g ] [-c ] [device-selection] @@ -133,7 +133,7 @@ SYNOPSIS: COMMANDS: info Display information from the target device(s) or file. - Without any arguments, this will display basic information for all connected RP2040 devices in BOOTSEL mode + Without any arguments, this will display basic information for all connected RP-series devices in BOOTSEL mode config Display or change program configuration settings from the target device(s) or file. load Load the program / memory range stored in a file onto the device. encrypt Encrypt the program. @@ -147,7 +147,7 @@ COMMANDS: partition Commands related to RP2350 Partition Tables uf2 Commands related to UF2 creation and status version Display picotool version - coprodis Post-process coprocessor instructions in dissassembly files. + coprodis Post-process coprocessor instructions in disassembly files. help Show general help or help for a specific command Use "picotool help " for more info @@ -167,16 +167,18 @@ a file. This file can be an ELF, a UF2 or a BIN file. $ picotool help info INFO: Display information from the target device(s) or file. - Without any arguments, this will display basic information for all connected RP2040 devices in BOOTSEL mode + Without any arguments, this will display basic information for all connected RP-series devices in BOOTSEL mode SYNOPSIS: - picotool info [-b] [-p] [-d] [--debug] [-l] [-a] [device-selection] - picotool info [-b] [-p] [-d] [--debug] [-l] [-a] [-t ] + picotool info [-b] [-m] [-p] [-d] [--debug] [-l] [-a] [device-selection] + picotool info [-b] [-m] [-p] [-d] [--debug] [-l] [-a] [-t ] OPTIONS: Information to display -b, --basic Include basic information. This is the default + -m, --metadata + Include all metadata blocks -p, --pins Include pin information -d, --device @@ -189,7 +191,7 @@ OPTIONS: Include all information TARGET SELECTION: - To target one or more connected RP2040 device(s) in BOOTSEL mode (the default) + To target one or more connected RP-series device(s) in BOOTSEL mode (the default) --bus Filter devices by USB bus number --address @@ -297,7 +299,7 @@ OPTIONS: Filter by feature group TARGET SELECTION: - To target one or more connected RP2040 device(s) in BOOTSEL mode (the default) + To target one or more connected RP-series device(s) in BOOTSEL mode (the default) --bus Filter devices by USB bus number --address @@ -915,9 +917,9 @@ OTP: Commands related to the RP2350 OTP (One-Time-Programmable) Memory SYNOPSIS: - picotool otp list [-p] [-n] [-i ] [..] + picotool otp list [-p] [-n] [-f] [-i ] [..] picotool otp get [-c ] [-r] [-e] [-n] [-i ] [device-selection] [-z] [..] - picotool otp set [-c ] [-r] [-e] [-i ] [-z] [device-selection] + picotool otp set [-c ] [-r] [-e] [-s] [-i ] [-z] [device-selection] picotool otp load [-r] [-e] [-s ] [-i ] [-t ] [device-selection] picotool otp dump [-r] [-e] [device-selection] picotool otp permissions [-t ] [--led ] [--hash] [--sign] [] [-t ] [device-selection] From 3a476024adb333e22036d6f7eb9629099b3e6b75 Mon Sep 17 00:00:00 2001 From: Chris Burton Date: Fri, 22 Nov 2024 18:17:08 +0000 Subject: [PATCH 66/67] fix typo (#179) * fix typo * fix grammar --------- Co-authored-by: Graham Sanderson --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3f79cc9..625259f 100644 --- a/README.md +++ b/README.md @@ -823,7 +823,7 @@ OPTIONS: ## uf2 -The `uf2` commands allow for creation of UF2s, and cam provide information when if a UF2 download has failed. +The `uf2` commands allow for creation of UF2s, and can provide information if a UF2 download has failed. ### convert From df21059f7ca6f1babc7f1f3b92122cacffc85951 Mon Sep 17 00:00:00 2001 From: graham sanderson Date: Sun, 24 Nov 2024 19:46:57 -0600 Subject: [PATCH 67/67] bump picotool version and SDK dependency to 2.1.0 --- CMakeLists.txt | 4 ++-- MODULE.bazel | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bd98861..adb47ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -239,8 +239,8 @@ if (NOT PICOTOOL_NO_LIBUSB) target_sources(picotool PRIVATE xip_ram_perms.cpp) add_dependencies(picotool generate_otp_header xip_ram_perms_elf binary_data) endif() -set(PROJECT_VERSION 2.0.0) -set(PICOTOOL_VERSION 2.0.0) +set(PROJECT_VERSION 2.1.0) +set(PICOTOOL_VERSION 2.1.0) set(SYSTEM_VERSION "${CMAKE_SYSTEM_NAME}") set(COMPILER_INFO "${CMAKE_C_COMPILER_ID}-${CMAKE_C_COMPILER_VERSION}, ${CMAKE_BUILD_TYPE}") target_compile_definitions(picotool PRIVATE diff --git a/MODULE.bazel b/MODULE.bazel index 2c22158..eedd667 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,10 +1,10 @@ module( name = "picotool", - version = "2.0.0", + version = "2.1.0", ) bazel_dep(name = "rules_libusb", version = "0.1.0-rc1") -bazel_dep(name = "pico-sdk", version = "2.0.0") +bazel_dep(name = "pico-sdk", version = "2.1.0") bazel_dep(name = "rules_cc", version = "0.0.9") bazel_dep(name = "bazel_skylib", version = "1.6.1") bazel_dep(name = "rules_python", version = "0.22.1")