diff --git a/CMakeLists.txt b/CMakeLists.txt index 28dc075..9bf6dec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) find_package(argparse QUIET) find_package(magic_enum QUIET) find_package(picosha2 QUIET) +find_package(elfio QUIET) # Check if nix is not providing these dependencies. if (NOT argparse_FOUND) include(FetchContent) @@ -36,12 +37,24 @@ if (NOT argparse_FOUND) ) FetchContent_MakeAvailable(picosha2) + FetchContent_Declare( + elfio + GIT_REPOSITORY https://github.com/serge1/ELFIO + GIT_TAG Release_3.12 + ) + FetchContent_MakeAvailable(elfio) + else() # This is a hack becase picosha2 does not define an install phase in CMakefile. add_library(picosha2 INTERFACE) target_include_directories(picosha2 INTERFACE ${CMAKE_PREFIX_PATH}/include ) + # This is a hack becase elfio does not define an install phase in CMakefile. + add_library(elfio INTERFACE) + target_include_directories(elfio INTERFACE + ${CMAKE_PREFIX_PATH}/elfio + ) endif() @@ -53,6 +66,5 @@ add_subdirectory(lib/visuals) add_executable(ftditool app/main.cc) target_include_directories(ftditool PUBLIC "./src/lib" "./src/") -target_link_libraries(ftditool PRIVATE visuals picosha2 argparse::argparse flash ftdipp) +target_link_libraries(ftditool PRIVATE visuals picosha2 elfio argparse::argparse flash ftdipp) install(TARGETS ftditool DESTINATION bin) - diff --git a/app/commands.hh b/app/commands.hh index 0c4d320..2b66d1d 100644 --- a/app/commands.hh +++ b/app/commands.hh @@ -12,6 +12,7 @@ #include #include #include +#include namespace commands { @@ -283,6 +284,178 @@ struct LoadFile : public Commands { } }; +template +struct LoadFileElf : public Commands { + std::string& filename; + bool quad; + bool bootstrap; + bool skip_erase; + LoadFileElf(flash::Generic f, std::string& filename, bool bootstrap = false, + bool skip_erase = false, bool quad = false) + : Commands(f), + filename(filename), + quad(quad), + bootstrap(bootstrap), + skip_erase(skip_erase) {} + + int run() override { + ELFIO::elfio reader; + if (!reader.load(filename)) { + std::println("Invalid ELF file"); + return 0; + } + + std::vector load_segments; + for (auto& segment : reader.segments) { + if (segment->get_type() == ELFIO::PT_LOAD) { + load_segments.push_back(std::to_address(segment)); + } + } + + // Given a start and end interval, return the interval in sectors containing them, + // by rounding the start address down and end address up to the next sector-aligned + // address. + auto containing_sectors = [](auto start, auto end) { + return std::make_pair(start & ~(flash::SectorSize - 1), + (end + flash::SectorSize - 1) & ~(flash::SectorSize - 1)); + }; + + bool addr4b = false; + auto erase_size = 0; + auto load_size = 0; + for (auto segment : load_segments) { + auto phys_start = segment->get_physical_address(); + auto phys_end = phys_start + segment->get_memory_size(); + auto sectors = containing_sectors(phys_start, phys_end); + load_size += segment->get_file_size(); + erase_size += (sectors.second - sectors.first); + if (phys_start > 0xFFFFFF || phys_end > 0xFFFFFF) { + addr4b = true; + } + } + + if (!bootstrap) { + this->flash.reset(); + } + if (addr4b && !this->flash.enter_4b_addr()) { + std::println("Enter 4-byte address mode failed"); + return 0; + } + if (quad && !this->flash.enable_quad(true)) { + std::println("enable quad failed"); + return 0; + } + + this->flash.write_enable(true); + + std::optional res; + + // The length of the data to be loaded into memory (file size) may be less than + // the size of the segment in memory (memory size), in which case the excess represents + // zero-initialised data (e.g .bss section). Therefore, the memory size is used for + // erasing the flash, and then the file resident data is loaded into it, which may be + // shorter. + + // Erase sectors containing segment data. + if (!skip_erase) { + auto erased = 0; + auto erase_progress = ProgressBar(erase_size, 50, "Erasing").with_throughput(); + for (auto segment : load_segments) { + auto phys_start = segment->get_physical_address(); + auto phys_end = phys_start + segment->get_memory_size(); + auto sectors = containing_sectors(phys_start, phys_end); + + auto addr = sectors.first; + while (addr < sectors.second) { + res = addr4b ? this->flash.template erase<4, flash::Opcode::SectorErase4b>(addr) + : this->flash.erase(addr); + if (!res) { + std::println("Failed to erase block {:#x}", addr); + return 0; + } + + addr += flash::SectorSize; + erased += flash::SectorSize; + erase_progress.update(erased); + } + } + } + + this->flash.wait_not_busy(); + + // Then, load the segment data from the file. + auto loaded = 0; + auto load_progress = ProgressBar(load_size, 50, "Loading").with_throughput(); + for (auto segment : load_segments) { + auto addr = segment->get_physical_address(); + std::span data(reinterpret_cast(segment->get_data()), + segment->get_file_size()); + + // Segments may start at an address that is not aligned to the flash page size. + // Page program commands may start within a page, but may wrap-around or be invalid + // if going over the page boundary, so only write until we are aligned to the page size. + if ((addr % flash::PageSize) != 0) { + auto to_next = flash::PageSize - (addr % flash::PageSize); + auto n = std::min(to_next, data.size()); + std::vector page(data.first(n).begin(), data.first(n).end()); + + if (addr4b) { + res = this->flash.template single_page_program_non_blocking<4>(addr, page); + } else if (quad) { + res = this->flash.quad_page_program(addr, page); + } else { + res = this->flash.single_page_program_non_blocking(addr, page); + } + if (!res) { + std::println("Program page {:#x} failed.", addr); + return 0; + } + + this->flash.wait_not_busy(); + + addr += n; + loaded += n; + data = data.subspan(n); + load_progress.update(loaded); + } + + // Now we are aligned to the flash page size, write pages as normal. + while (data.size() > 0) { + auto n = std::min(data.size(), std::size_t{flash::PageSize}); + std::vector page(data.first(n).begin(), data.first(n).end()); + + this->flash.wait_not_busy(); + + if (addr4b) { + res = this->flash.template single_page_program_non_blocking<4>(addr, page); + } else if (quad) { + res = this->flash.quad_page_program(addr, page); + } else { + res = this->flash.single_page_program_non_blocking(addr, page); + } + if (!res) { + std::println("Program page {:#x} failed.", addr); + return 0; + } + + addr += n; + loaded += n; + data = data.subspan(n); + load_progress.update(loaded); + } + } + + this->flash.wait_not_busy(); + this->flash.write_enable(false); + + if (bootstrap) { + this->flash.reset(); + } + + return 1; + } +}; + template requires embeddedpp::Gpio struct GpioWrite { diff --git a/app/main.cc b/app/main.cc index 8339116..2cff4dc 100644 --- a/app/main.cc +++ b/app/main.cc @@ -241,6 +241,30 @@ int main(int argc, char* argv[]) { return 0; }; + auto bootstrap_elf_cmd = new_flash_command( + "bootstrap-elf", "Write the loadable contents of an ELF file and reset the target."); + bootstrap_elf_cmd->add_argument("filename").help("The file path."); + bootstrap_elf_cmd->add_argument("--quad") + .help("Use qSPI") + .default_value(false) + .implicit_value(true); + bootstrap_elf_cmd->add_argument("--skip-erase") + .help("Don't issue erase commands") + .default_value(false) + .implicit_value(true); + program.add_subparser(*bootstrap_elf_cmd); + commands["bootstrap-elf"] = [&]() -> int { + auto pid = program.get("--pid"); + auto filename = bootstrap_elf_cmd->get("filename"); + auto quad = bootstrap_elf_cmd->get("--quad"); + auto skip_erase = bootstrap_elf_cmd->get("--skip-erase"); + auto spih = handle_flash_command(bootstrap_elf_cmd, pid); + commands::LoadFileElf(flash::Generic(*spih), filename, true, skip_erase, quad).run(); + + spih->close(); + return 0; + }; + auto gpio_write_cmd = new_command("gpio-write", "Write a value to an FTDI GPIO pin."); gpio_write_cmd->add_argument("pin").help("GPIO pin number (0-3)").required().scan<'d', int>(); gpio_write_cmd->add_argument("value").help("Value to write (0 or 1)").required().scan<'d', int>(); diff --git a/doc/img/deps.png b/doc/img/deps.png index 2f47a43..8698f94 100644 Binary files a/doc/img/deps.png and b/doc/img/deps.png differ diff --git a/lib/flash/flash.hh b/lib/flash/flash.hh index fe829a2..51aee5e 100644 --- a/lib/flash/flash.hh +++ b/lib/flash/flash.hh @@ -8,6 +8,7 @@ #include "jedec.hh" #include "sfdp.hh" #include +#include namespace flash { @@ -135,8 +136,9 @@ class Generic { Option quad_read_sector(uint32_t address) { return quad_read<4096>(address); } - Option - quad_page_program(uint32_t address, std::span data, uint8_t upcode = 0x32) { + Option quad_page_program(uint32_t address, std::span data, uint8_t upcode = 0x32) { + // Page program commands should not cross page boundaries. + assert((address % flash::PageSize) + data.size() <= flash::PageSize); write_enable(); std::array cmd = {upcode}; @@ -250,7 +252,9 @@ class Generic { } template - Option single_page_program_non_blocking(uint32_t address, std::span data) { + Option single_page_program_non_blocking(uint32_t address, std::span data) { + // Page program commands should not cross page boundaries. + assert((address % flash::PageSize) + data.size() <= flash::PageSize); static_assert(ADDR_SIZE == 3 || ADDR_SIZE == 4, "Only 3 or 4 byte addresses supported"); Opcode op; @@ -274,7 +278,9 @@ class Generic { } template - Option single_page_program(uint32_t address, std::span data) { + Option single_page_program(uint32_t address, std::span data) { + // Page program commands should not cross page boundaries. + assert((address % flash::PageSize) + data.size() <= flash::PageSize); write_enable(); single_page_program_non_blocking(address, data); wait_not_busy(); diff --git a/nix/elfio.nix b/nix/elfio.nix new file mode 100644 index 0000000..6cf2771 --- /dev/null +++ b/nix/elfio.nix @@ -0,0 +1,34 @@ +# Copyright lowRISC contributors. +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 + +{ + stdenv, + lib, + fetchFromGitHub, +}: +stdenv.mkDerivation rec { + pname = "elfio"; + version = "Release_3.12"; + + src = fetchFromGitHub { + owner = "serge1"; + repo = "ELFIO"; + rev = "${version}"; + sha256 = "sha256-tDRBscs2L/3gYgLQvb1+8nNxqkr8v1xBkeDXuOqShX4="; + }; + + dontBuild = true; + + installPhase = '' + mkdir -p $out/include + cp -r elfio $out/include + ''; + + meta = with lib; { + description = "Header-only ELF implementation for C++"; + homepage = "https://github.com/serge1/ELFIO"; + license = licenses.mit; + platforms = platforms.all; + }; +} diff --git a/nix/ftditool.nix b/nix/ftditool.nix index 3fdcf4a..704344b 100644 --- a/nix/ftditool.nix +++ b/nix/ftditool.nix @@ -12,6 +12,7 @@ argparse, magic-enum, picosha2, + elfio, libusb1, systemd, makeWrapper, @@ -34,6 +35,7 @@ stdenv.mkDerivation { argparse magic-enum picosha2 + elfio ftd2xx libusb1 systemd