From f9915fc6b499c78087a9f54e0e9a2c957fc1e2f6 Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Wed, 28 Oct 2026 21:36:01 +0000 Subject: [PATCH 01/14] selector: the portable core - choice of the bank, trial and its confirmation, store of the state and of the persistent items The code of the first sector of the flash. Portable C with a port made of five functions (the flash as memory, erase, program, jump, log) and a layout; nothing of the chip is in the core (2.8 kB of code on Cortex-M). * Images: a header of up to 512 bytes at the start of a bank (magic, version, size, CRC of the image, offset of the vectors - a multiple of 512, as VTOR wants), verified before every start. * Choice: the selected bank, then the one to go back to, then the others; a bank on trial gets max_tries starts and is dropped when it does not confirm; a lost state is rebuilt from the banks (the newest good image). * The store (one sector, S1): a log of CRC-protected records - the state of the banks and the persistent items (Dmod_WritePersistency / Dmod_ReadPersistency); a full store is compacted in RAM. Payload first, header last, so that a record cut short is not a record. * The functions behind Dmod_FirmwareGetBankCount / GetRunningBank / GetBankInfo / VerifyBank / SelectBank / Ready. Tests on the flash in RAM that keeps the rules of the real one (programming only on erased words; a power cut at any program): 1202 checks, run with the sanitizers in the CI. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/build.yml | 17 +- selector/CMakeLists.txt | 24 + selector/include/dmsel.h | 146 ++++++ selector/port/host/dmsel_port_host.c | 79 ++++ selector/port/host/dmsel_port_host.h | 29 ++ selector/src/dmsel.c | 674 +++++++++++++++++++++++++++ selector/tests/test_selector.c | 400 ++++++++++++++++ 7 files changed, 1368 insertions(+), 1 deletion(-) create mode 100644 selector/CMakeLists.txt create mode 100644 selector/include/dmsel.h create mode 100644 selector/port/host/dmsel_port_host.c create mode 100644 selector/port/host/dmsel_port_host.h create mode 100644 selector/src/dmsel.c create mode 100644 selector/tests/test_selector.c diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0a669c0..f2371b1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -145,6 +145,21 @@ jobs: build/dmboot.map retention-days: 30 + test-selector: + name: Selector tests (host, sanitizers) + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build and run the tests of the selector + run: | + cmake -S selector -B build-selector -DDMSEL_SANITIZE=ON -DCMAKE_BUILD_TYPE=Debug + cmake --build build-selector + ctest --test-dir build-selector --output-on-failure + test-renode: name: Test Renode Simulation runs-on: ubuntu-latest @@ -190,7 +205,7 @@ jobs: build-all: name: All builds completed runs-on: ubuntu-latest - needs: [build-cmake, test-renode] + needs: [build-cmake, test-renode, test-selector] permissions: contents: read diff --git a/selector/CMakeLists.txt b/selector/CMakeLists.txt new file mode 100644 index 0000000..5de0271 --- /dev/null +++ b/selector/CMakeLists.txt @@ -0,0 +1,24 @@ +# The selector: portable code (src/), the flash in RAM for its tests (port/host/) +# Standalone: cmake -S selector -B build-selector && cmake --build build-selector && ctest --test-dir build-selector +cmake_minimum_required(VERSION 3.16) +project(dmsel LANGUAGES C) +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) +option(DMSEL_SANITIZE "AddressSanitizer + UBSanitizer" OFF) + +add_library(dmsel STATIC src/dmsel.c) +target_include_directories(dmsel PUBLIC include) +target_compile_options(dmsel PRIVATE -Wall -Wextra -Wshadow -Wpedantic -Werror) + +enable_testing() +add_executable(test_selector tests/test_selector.c port/host/dmsel_port_host.c) +target_include_directories(test_selector PRIVATE port/host) +target_link_libraries(test_selector PRIVATE dmsel) +target_compile_options(test_selector PRIVATE -Wall -Wextra) +if(DMSEL_SANITIZE) + foreach(t dmsel test_selector) + target_compile_options(${t} PRIVATE -fsanitize=address,undefined -fno-sanitize-recover=undefined) + target_link_options(${t} PRIVATE -fsanitize=address,undefined) + endforeach() +endif() +add_test(NAME test_selector COMMAND test_selector) diff --git a/selector/include/dmsel.h b/selector/include/dmsel.h new file mode 100644 index 0000000..2d727ba --- /dev/null +++ b/selector/include/dmsel.h @@ -0,0 +1,146 @@ +/** + * @file dmsel.h + * @brief The selector: the code of the first sector that picks the firmware bank to start, keeps the persistent data + * + * Portable C. Everything that depends on the chip is behind dmsel_port_t (read the flash, erase, program, jump); the + * layout of the flash is a dmsel_layout_t. The selector owns one sector of the flash (the store): a log of records - + * the state of the banks and the persistent items of Dmod_WritePersistency(). + */ +#ifndef DMSEL_H +#define DMSEL_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define DMSEL_MAX_BANKS 4 +#define DMSEL_HEADER_MAGIC 0x4B424D44u //!< "DMBK" +#define DMSEL_HEADER_VERSION 1 +#define DMSEL_HEADER_MAX_SIZE 512 //!< The header is at the start of a bank, the vectors follow at 512 (VTOR) + +/** Results: 0, or negative */ +typedef enum +{ + DMSEL_OK = 0, + DMSEL_ERR_PARAM = -1, //!< Wrong argument (no such bank, ...) + DMSEL_ERR_NOENT = -2, //!< No such persistent item + DMSEL_ERR_NOSPACE = -3, //!< The store is full + DMSEL_ERR_IO = -4, //!< The flash failed + DMSEL_ERR_EMPTY = -5, //!< The bank is empty + DMSEL_ERR_BAD = -6, //!< The image in the bank is damaged +} dmsel_result_t; + +/** State of a bank - the numbers are those of Dmod_FirmwareBankState_t */ +typedef enum +{ + DMSEL_BANK_EMPTY = 0, + DMSEL_BANK_INVALID = 1, + DMSEL_BANK_VALID = 2, + DMSEL_BANK_TRIAL = 3, + DMSEL_BANK_CONFIRMED = 4, +} dmsel_bank_state_t; + +/** + * Header of an image, in the first bytes of a bank (the tool that makes the image writes it after the linking). + * The rest of the 512 bytes is 0xFF. The image is [bank start, bank start + image_size). + */ +typedef struct +{ + uint32_t magic; //!< DMSEL_HEADER_MAGIC + uint16_t header_version; //!< DMSEL_HEADER_VERSION + uint16_t header_size; //!< Where the code starts (the vectors): at least sizeof(dmsel_header_t), at most 512 + uint32_t image_size; //!< Of the whole image, the header included + uint32_t version; //!< The bigger the newer + uint32_t image_crc; //!< CRC-32 of [header_size, image_size) + uint32_t entry_offset; //!< Of the vector table from the start of the bank (a multiple of 512) + uint32_t flags; //!< 0 + uint32_t header_crc; //!< CRC-32 of the bytes above +} dmsel_header_t; + +/** What dmsel_get_info() tells (the layout is that of Dmod_FirmwareBankInfo_t) */ +typedef struct +{ + uint32_t struct_size; + uint32_t state; + uint32_t version; + uint32_t size; + uint32_t tries; + uint32_t reserved; + uint64_t address; + uint64_t capacity; +} dmsel_bank_info_t; + +/** Where things are in the flash (offsets from the beginning of the flash) */ +typedef struct +{ + uint32_t offset; + uint32_t capacity; +} dmsel_bank_t; + +typedef struct +{ + uint32_t flash_address; //!< Address of the offset 0 in the memory of the chip (0x08000000) + uint32_t store_offset; //!< The sector of the selector's store + uint32_t store_size; + uint32_t bank_count; + dmsel_bank_t banks[DMSEL_MAX_BANKS]; + uint32_t max_tries; //!< Starts of an image on trial that does not confirm, before it is dropped + uint8_t* scratch; //!< store_size bytes of RAM for the compaction of the store +} dmsel_layout_t; + +/** The chip */ +typedef struct +{ + const uint8_t* flash; //!< The flash as memory: offset 0 first + int (*erase)( void* user, uint32_t offset, uint32_t size ); //!< Whole sectors; 0 or negative + int (*program)( void* user, uint32_t offset, const void* data, uint32_t size ); //!< Words, only on erased flash + void (*jump)( void* user, uint32_t vector_address ); //!< Does not return + void (*log)( void* user, const char* text ); //!< May be NULL + void* user; +} dmsel_port_t; + +typedef struct +{ + const dmsel_layout_t* layout; + const dmsel_port_t* port; + uint8_t selected; //!< Bank that is started (DMSEL_MAX_BANKS: none) + uint8_t previous; //!< Bank to go back to when the one on trial does not confirm (DMSEL_MAX_BANKS: none) + uint8_t running; //!< Bank that was started (DMSEL_MAX_BANKS: none) + uint8_t state_valid; //!< A state record was found in the store + uint8_t state[DMSEL_MAX_BANKS]; + uint8_t tries[DMSEL_MAX_BANKS]; + uint32_t store_end; //!< Offset in the store of the first free byte +} dmsel_t; + +uint32_t dmsel_crc32( uint32_t crc, const void* data, size_t size ); + +/** Reads the state from the store (a lost state is rebuilt from the banks at the next dmsel_choose) */ +int dmsel_init( dmsel_t* s, const dmsel_layout_t* layout, const dmsel_port_t* port ); + +/** + * Decides which bank to start, stores what has to be remembered (a try of an image on trial) and returns the bank. + * @return bank number, or a negative result when there is nothing to start + */ +int dmsel_choose( dmsel_t* s ); + +/** dmsel_choose() and the jump into the image; when nothing can be started it logs and returns */ +void dmsel_boot( dmsel_t* s ); + +// --- What the running firmware asks for (Dmod_Firmware*, Dmod_*Persistency) +int dmsel_bank_count( const dmsel_t* s ); +int dmsel_running_bank( const dmsel_t* s ); +int dmsel_get_info( dmsel_t* s, int bank, dmsel_bank_info_t* info ); +int dmsel_verify( dmsel_t* s, int bank ); +int dmsel_select( dmsel_t* s, int bank, int trial ); +int dmsel_ready( dmsel_t* s ); +int dmsel_persist_write( dmsel_t* s, uint32_t id, const void* data, size_t size ); +int dmsel_persist_read( dmsel_t* s, uint32_t id, void* buffer, size_t buffer_size ); + +#ifdef __cplusplus +} +#endif + +#endif /* DMSEL_H */ diff --git a/selector/port/host/dmsel_port_host.c b/selector/port/host/dmsel_port_host.c new file mode 100644 index 0000000..2bf0b01 --- /dev/null +++ b/selector/port/host/dmsel_port_host.c @@ -0,0 +1,79 @@ +#include +#include "dmsel_port_host.h" + +static int h_erase( void* user, uint32_t offset, uint32_t size ) +{ + host_flash_t* f = (host_flash_t*)user; + if( offset + size > HOST_FLASH_SIZE ) + { + return -1; + } + memset( f->flash + offset, 0xFF, size ); + f->erases++; + return 0; +} + +static int h_program( void* user, uint32_t offset, const void* data, uint32_t size ) +{ + host_flash_t* f = (host_flash_t*)user; + uint32_t i; + const uint8_t* d = (const uint8_t*)data; + + if( offset % 4 != 0 || size % 4 != 0 || offset + size > HOST_FLASH_SIZE ) + { + return -1; + } + if( f->fail_after >= 0 && f->programs >= f->fail_after ) + { + // The power goes in the middle: a half of the words are written + for( i = 0; i < size / 2 / 4 * 4; i++ ) + { + f->flash[offset + i] &= d[i]; + } + f->programs++; + return -1; + } + for( i = 0; i < size; i += 4 ) + { + uint32_t k; + for( k = 0; k < 4; k++ ) + { + if( f->flash[offset + i + k] != 0xFF ) + { + f->program_errors++; + return -1; + } + } + } + memcpy( f->flash + offset, d, size ); + f->programs++; + return 0; +} + +static void h_jump( void* user, uint32_t vector_address ) +{ + ((host_flash_t*)user)->jumped_to = vector_address; +} + +static void h_log( void* user, const char* text ) +{ + host_flash_t* f = (host_flash_t*)user; + size_t n = strlen( f->log ); + if( n + strlen( text ) + 1 < sizeof(f->log) ) + { + strcpy( f->log + n, text ); + } +} + +void host_flash_init( host_flash_t* f ) +{ + memset( f, 0, sizeof(*f) ); + memset( f->flash, 0xFF, sizeof(f->flash) ); + f->fail_after = -1; + f->port.flash = f->flash; + f->port.erase = h_erase; + f->port.program = h_program; + f->port.jump = h_jump; + f->port.log = h_log; + f->port.user = f; +} diff --git a/selector/port/host/dmsel_port_host.h b/selector/port/host/dmsel_port_host.h new file mode 100644 index 0000000..721b5b5 --- /dev/null +++ b/selector/port/host/dmsel_port_host.h @@ -0,0 +1,29 @@ +/** + * @file dmsel_port_host.h + * @brief The flash in RAM, for the tests of the selector on the host + * + * It keeps the rules of the real one: erased bytes are 0xFF, programming only on erased words (it fails otherwise), the erase + * clears whole ranges. fail_after makes the n-th next program (and everything after it) fail half way: a power cut. + */ +#ifndef DMSEL_PORT_HOST_H +#define DMSEL_PORT_HOST_H + +#include "dmsel.h" + +#define HOST_FLASH_SIZE (1024u * 1024u) + +typedef struct +{ + uint8_t flash[HOST_FLASH_SIZE]; + int fail_after; //!< -1: never; n: the program number n (0 - the next one) is cut and all after it fail + int programs; + int erases; + int program_errors; //!< Programs refused because the flash was not erased + uint32_t jumped_to; //!< Address of the last jump (0: none) + char log[4096]; + dmsel_port_t port; +} host_flash_t; + +void host_flash_init( host_flash_t* f ); + +#endif diff --git a/selector/src/dmsel.c b/selector/src/dmsel.c new file mode 100644 index 0000000..975393f --- /dev/null +++ b/selector/src/dmsel.c @@ -0,0 +1,674 @@ +/** + * @file dmsel.c + * @brief The selector - choice of the bank, the store of the state and of the persistent items + * + * The store is one sector of the flash used as a log: records are appended, a record is valid when its CRC is, the latest + * valid record of a kind wins. A full store is compacted in RAM (the sector is erased and written again: a power cut in + * that moment loses the persistent items, and the state is rebuilt from the banks: the newest good image). + */ +#include +#include "dmsel.h" + +#define NO_BANK DMSEL_MAX_BANKS +#define REC_HDR 8u +enum { REC_STATE = 1, REC_PERSIST = 2, REC_DELETE = 3 }; +#define STATE_SIZE (2u + 2u + DMSEL_MAX_BANKS + DMSEL_MAX_BANKS) + +typedef struct +{ + uint16_t type; + uint16_t len; //!< Of the payload, without the padding + uint32_t crc; //!< Of type, len and the payload +} rec_t; + +// --------------------------------------------------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------------------------------------------------- + +uint32_t dmsel_crc32( uint32_t crc, const void* data, size_t size ) +{ + static const uint32_t table[16] = + { + 0x00000000, 0x1DB71064, 0x3B6E20C8, 0x26D930AC, 0x76DC4190, 0x6B6B51F4, 0x4DB26158, 0x5005713C, + 0xEDB88320, 0xF00F9344, 0xD6D6A3E8, 0xCB61B38C, 0x9B64C2B0, 0x86D3D2D4, 0xA00AE278, 0xBDBDF21C + }; + const uint8_t* p = (const uint8_t*)data; + + crc = ~crc; + while( size-- > 0 ) + { + crc ^= *p++; + crc = (crc >> 4) ^ table[crc & 15u]; + crc = (crc >> 4) ^ table[crc & 15u]; + } + return ~crc; +} + +static uint32_t pad4( uint32_t n ) +{ + return (n + 3u) & ~3u; +} + +static void say( const dmsel_t* s, const char* text, int value, int with_value ) +{ + char buf[64]; + size_t n = 0, i; + char digits[12]; + unsigned v; + int d = 0; + + if( s->port->log == NULL ) + { + return; + } + for( i = 0; text[i] != '\0' && n < 40; i++ ) + { + buf[n++] = text[i]; + } + if( with_value ) + { + if( value < 0 ) + { + buf[n++] = '-'; + v = (unsigned)(-value); + } + else + { + v = (unsigned)value; + } + do + { + digits[d++] = (char)('0' + v % 10u); + v /= 10u; + } + while( v != 0 ); + while( d > 0 ) + { + buf[n++] = digits[--d]; + } + } + buf[n++] = '\n'; + buf[n] = '\0'; + s->port->log( s->port->user, buf ); +} + +static int is_blank( const uint8_t* p, uint32_t n ) +{ + while( n-- > 0 ) + { + if( *p++ != 0xFF ) + { + return 0; + } + } + return 1; +} + +static const uint8_t* store_base( const dmsel_t* s ) +{ + return s->port->flash + s->layout->store_offset; +} + +// --------------------------------------------------------------------------------------------------------------------- +// The store +// --------------------------------------------------------------------------------------------------------------------- + +/** 1: a valid record, 0: the end (blank), -1: garbage (a record that was not finished) - both end the log */ +static int rec_read( const dmsel_t* s, uint32_t off, rec_t* r, const uint8_t** payload ) +{ + const uint8_t* p = store_base( s ) + off; + uint32_t crc; + + if( off + REC_HDR > s->layout->store_size ) + { + return 0; + } + memcpy( r, p, REC_HDR ); + if( r->type == 0xFFFF && r->len == 0xFFFF && r->crc == 0xFFFFFFFFu ) + { + return 0; + } + if( off + REC_HDR + pad4( r->len ) > s->layout->store_size ) + { + return -1; + } + crc = dmsel_crc32( 0, p, 4 ); + crc = dmsel_crc32( crc, p + REC_HDR, r->len ); + if( crc != r->crc ) + { + return -1; + } + *payload = p + REC_HDR; + return 1; +} + +static uint32_t le32( const uint8_t* p ) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +/** Is there a record after @p from_off that sets or deletes the item @p id? */ +static int later_item( const dmsel_t* s, uint32_t from_off, uint32_t id ) +{ + rec_t r; + const uint8_t* payload; + uint32_t off = from_off; + + while( rec_read( s, off, &r, &payload ) == 1 ) + { + if( (r.type == REC_PERSIST || r.type == REC_DELETE) && r.len >= 4 && le32( payload ) == id ) + { + return 1; + } + off += REC_HDR + pad4( r.len ); + } + return 0; +} + +/** Reads the log: the latest state, the end of the log */ +static void load_state( dmsel_t* s ) +{ + rec_t r; + const uint8_t* payload; + uint32_t off = 0; + unsigned i; + + s->state_valid = 0; + while( rec_read( s, off, &r, &payload ) == 1 ) + { + if( r.type == REC_STATE && r.len == STATE_SIZE ) + { + s->selected = payload[0]; + s->previous = payload[1]; + for( i = 0; i < DMSEL_MAX_BANKS; i++ ) + { + s->state[i] = payload[4 + i]; + s->tries[i] = payload[4 + DMSEL_MAX_BANKS + i]; + } + s->state_valid = 1; + } + off += REC_HDR + pad4( r.len ); + } + s->store_end = off; +} + +static int write_record( dmsel_t* s, uint32_t off, uint16_t type, const void* a, uint32_t na, const void* b, uint32_t nb ) +{ + uint8_t* buf = s->layout->scratch; + uint32_t len = na + nb, padded = pad4( len ); + rec_t r; + + memset( buf, 0xFF, padded ); + if( na ) memcpy( buf, a, na ); + if( nb ) memcpy( buf + na, b, nb ); + r.type = type; + r.len = (uint16_t)len; + r.crc = dmsel_crc32( 0, &r, 4 ); + r.crc = dmsel_crc32( r.crc, buf, len ); + // The payload first, the header last: a record is there only when its header is + if( padded != 0 && s->port->program( s->port->user, s->layout->store_offset + off + REC_HDR, buf, padded ) != 0 ) + { + return DMSEL_ERR_IO; + } + return s->port->program( s->port->user, s->layout->store_offset + off, &r, REC_HDR ) == 0 ? DMSEL_OK : DMSEL_ERR_IO; +} + +static void state_payload( const dmsel_t* s, uint8_t* out ) +{ + unsigned i; + out[0] = s->selected; + out[1] = s->previous; + out[2] = 0; + out[3] = 0; + for( i = 0; i < DMSEL_MAX_BANKS; i++ ) + { + out[4 + i] = s->state[i]; + out[4 + DMSEL_MAX_BANKS + i] = s->tries[i]; + } +} + +/** Erases the store and writes the state and the latest value of every item again */ +static int compact( dmsel_t* s ) +{ + uint8_t* out = s->layout->scratch; + uint8_t payload[STATE_SIZE]; + rec_t r, hdr; + const uint8_t* p; + uint32_t o = 0, off = 0; + + state_payload( s, payload ); + hdr.type = REC_STATE; + hdr.len = STATE_SIZE; + hdr.crc = dmsel_crc32( dmsel_crc32( 0, &hdr, 4 ), payload, STATE_SIZE ); + memcpy( out, &hdr, REC_HDR ); + memcpy( out + REC_HDR, payload, STATE_SIZE ); + o = REC_HDR + pad4( STATE_SIZE ); + + while( rec_read( s, off, &r, &p ) == 1 ) + { + uint32_t total = REC_HDR + pad4( r.len ); + if( r.type == REC_PERSIST && r.len >= 4 && !later_item( s, off + total, le32( p ) ) ) + { + memcpy( out + o, store_base( s ) + off, total ); + o += total; + } + off += total; + } + + say( s, "dmsel: store compacted", 0, 0 ); + if( s->port->erase( s->port->user, s->layout->store_offset, s->layout->store_size ) != 0 || + s->port->program( s->port->user, s->layout->store_offset, out, o ) != 0 ) + { + return DMSEL_ERR_IO; + } + s->store_end = o; + return DMSEL_OK; +} + +static int append( dmsel_t* s, uint16_t type, const void* a, uint32_t na, const void* b, uint32_t nb ) +{ + uint32_t total = REC_HDR + pad4( na + nb ); + int attempt; + + if( total > s->layout->store_size ) + { + return DMSEL_ERR_NOSPACE; + } + for( attempt = 0; attempt < 2; attempt++ ) + { + if( s->store_end + total <= s->layout->store_size && + is_blank( store_base( s ) + s->store_end, total ) ) + { + int r = write_record( s, s->store_end, type, a, na, b, nb ); + if( r == DMSEL_OK ) + { + s->store_end += total; + } + return r; + } + if( attempt == 0 ) + { + int r = compact( s ); + if( r != DMSEL_OK ) + { + return r; + } + } + } + return DMSEL_ERR_NOSPACE; +} + +static int save_state( dmsel_t* s ) +{ + uint8_t payload[STATE_SIZE]; + + state_payload( s, payload ); + s->state_valid = 1; + return append( s, REC_STATE, payload, STATE_SIZE, NULL, 0 ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Images +// --------------------------------------------------------------------------------------------------------------------- + +/** 0: a good image; DMSEL_ERR_EMPTY: nothing there; DMSEL_ERR_BAD: damaged */ +static int check_image( const dmsel_t* s, unsigned bank, dmsel_header_t* out ) +{ + const dmsel_bank_t* b = &s->layout->banks[bank]; + const uint8_t* p = s->port->flash + b->offset; + dmsel_header_t h; + + memcpy( &h, p, sizeof(h) ); + if( h.magic != DMSEL_HEADER_MAGIC ) + { + return is_blank( p, 16 ) ? DMSEL_ERR_EMPTY : DMSEL_ERR_BAD; + } + if( dmsel_crc32( 0, &h, sizeof(h) - 4 ) != h.header_crc || h.header_version != DMSEL_HEADER_VERSION || + h.header_size < sizeof(h) || h.header_size > DMSEL_HEADER_MAX_SIZE || h.image_size <= h.header_size || + h.image_size > b->capacity || h.entry_offset % 512u != 0 || h.entry_offset >= h.image_size ) + { + return DMSEL_ERR_BAD; + } + if( dmsel_crc32( 0, p + h.header_size, h.image_size - h.header_size ) != h.image_crc ) + { + return DMSEL_ERR_BAD; + } + if( out != NULL ) + { + *out = h; + } + return DMSEL_OK; +} + +/** The state is lost: the banks tell what there is - the newest good image is started */ +static void rebuild( dmsel_t* s ) +{ + unsigned i; + uint32_t best_version = 0; + int best = -1; + + for( i = 0; i < DMSEL_MAX_BANKS; i++ ) + { + s->state[i] = DMSEL_BANK_EMPTY; + s->tries[i] = 0; + } + for( i = 0; i < s->layout->bank_count; i++ ) + { + dmsel_header_t h; + int r = check_image( s, i, &h ); + if( r == DMSEL_OK ) + { + s->state[i] = DMSEL_BANK_VALID; + if( best < 0 || h.version > best_version ) + { + best = (int)i; + best_version = h.version; + } + } + else + { + s->state[i] = r == DMSEL_ERR_EMPTY ? DMSEL_BANK_EMPTY : DMSEL_BANK_INVALID; + } + } + s->selected = best < 0 ? NO_BANK : (uint8_t)best; + s->previous = NO_BANK; + say( s, "dmsel: state rebuilt from the banks, newest: ", best, 1 ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public +// --------------------------------------------------------------------------------------------------------------------- + +int dmsel_init( dmsel_t* s, const dmsel_layout_t* layout, const dmsel_port_t* port ) +{ + unsigned i; + + if( s == NULL || layout == NULL || port == NULL || layout->bank_count > DMSEL_MAX_BANKS || layout->scratch == NULL ) + { + return DMSEL_ERR_PARAM; + } + memset( s, 0, sizeof(*s) ); + s->layout = layout; + s->port = port; + s->selected = s->previous = s->running = NO_BANK; + for( i = 0; i < DMSEL_MAX_BANKS; i++ ) + { + s->state[i] = DMSEL_BANK_EMPTY; + } + load_state( s ); + return DMSEL_OK; +} + +int dmsel_choose( dmsel_t* s ) +{ + int pass; + + for( pass = 0; pass < 2; pass++ ) + { + unsigned order[2 + DMSEL_MAX_BANKS], n = 0, k, i; + + if( pass == 1 || !s->state_valid ) + { + rebuild( s ); + } + order[n++] = s->selected; + order[n++] = s->previous; + for( i = 0; i < s->layout->bank_count; i++ ) + { + order[n++] = i; + } + for( k = 0; k < n; k++ ) + { + unsigned c = order[k], j, seen = 0; + dmsel_header_t h; + int r; + + if( c >= s->layout->bank_count ) + { + continue; + } + for( j = 0; j < k; j++ ) + { + seen |= (order[j] == c); + } + if( seen || s->state[c] == DMSEL_BANK_EMPTY || s->state[c] == DMSEL_BANK_INVALID ) + { + continue; + } + r = check_image( s, c, &h ); + if( r != DMSEL_OK ) + { + say( s, "dmsel: image damaged or missing in bank ", (int)c, 1 ); + s->state[c] = r == DMSEL_ERR_EMPTY ? DMSEL_BANK_EMPTY : DMSEL_BANK_INVALID; + save_state( s ); + continue; + } + if( s->state[c] == DMSEL_BANK_TRIAL ) + { + if( s->tries[c] >= s->layout->max_tries ) + { + say( s, "dmsel: bank did not confirm, dropped: ", (int)c, 1 ); + s->state[c] = DMSEL_BANK_INVALID; + save_state( s ); + continue; + } + s->tries[c]++; + say( s, "dmsel: trial start, try ", s->tries[c], 1 ); + } + else + { + s->previous = NO_BANK; + } + s->selected = s->running = (uint8_t)c; + save_state( s ); + say( s, "dmsel: bank ", (int)c, 1 ); + return (int)c; + } + } + say( s, "dmsel: nothing to start", 0, 0 ); + return DMSEL_ERR_EMPTY; +} + +void dmsel_boot( dmsel_t* s ) +{ + dmsel_header_t h; + int bank = dmsel_choose( s ); + + if( bank >= 0 && check_image( s, (unsigned)bank, &h ) == DMSEL_OK ) + { + s->port->jump( s->port->user, + s->layout->flash_address + s->layout->banks[bank].offset + h.entry_offset ); + } +} + +int dmsel_bank_count( const dmsel_t* s ) +{ + return (int)s->layout->bank_count; +} + +int dmsel_running_bank( const dmsel_t* s ) +{ + return s->running < s->layout->bank_count ? (int)s->running : -1; +} + +int dmsel_get_info( dmsel_t* s, int bank, dmsel_bank_info_t* info ) +{ + dmsel_bank_info_t full; + dmsel_header_t h; + + if( bank < 0 || (unsigned)bank >= s->layout->bank_count || info == NULL || info->struct_size == 0 ) + { + return DMSEL_ERR_PARAM; + } + memset( &full, 0, sizeof(full) ); + full.struct_size = sizeof(full); + full.state = s->state[bank]; + full.tries = s->tries[bank]; + full.address = (uint64_t)s->layout->flash_address + s->layout->banks[bank].offset; + full.capacity = s->layout->banks[bank].capacity; + memcpy( &h, s->port->flash + s->layout->banks[bank].offset, sizeof(h) ); + if( s->state[bank] != DMSEL_BANK_EMPTY && h.magic == DMSEL_HEADER_MAGIC ) + { + full.version = h.version; + full.size = h.image_size; + } + { + uint32_t wanted = info->struct_size; // The caller's, not ours + memcpy( info, &full, wanted < sizeof(full) ? wanted : sizeof(full) ); + info->struct_size = wanted; + } + return DMSEL_OK; +} + +int dmsel_verify( dmsel_t* s, int bank ) +{ + int r; + uint8_t wanted; + + if( bank < 0 || (unsigned)bank >= s->layout->bank_count ) + { + return DMSEL_ERR_PARAM; + } + r = check_image( s, (unsigned)bank, NULL ); + if( r == DMSEL_OK ) + { + wanted = (s->state[bank] == DMSEL_BANK_EMPTY || s->state[bank] == DMSEL_BANK_INVALID) ? DMSEL_BANK_VALID : s->state[bank]; + } + else + { + wanted = r == DMSEL_ERR_EMPTY ? DMSEL_BANK_EMPTY : DMSEL_BANK_INVALID; + } + if( wanted != s->state[bank] ) + { + s->state[bank] = wanted; + s->tries[bank] = 0; + if( save_state( s ) != DMSEL_OK ) + { + return DMSEL_ERR_IO; + } + } + return r; +} + +int dmsel_select( dmsel_t* s, int bank, int trial ) +{ + int r = dmsel_verify( s, bank ); + + if( r != DMSEL_OK ) + { + return r; + } + if( trial ) + { + // Back to the bank that runs now - or to the one that was to be started, when this is run from nowhere + uint8_t back = s->running < s->layout->bank_count ? s->running : s->selected; + if( back == (uint8_t)bank || back >= s->layout->bank_count ) + { + back = NO_BANK; + } + else if( s->state[back] == DMSEL_BANK_TRIAL ) + { + back = s->previous; // The running one is not confirmed itself: the one before it + } + s->previous = back; + s->state[bank] = DMSEL_BANK_TRIAL; + } + else + { + s->previous = NO_BANK; + s->state[bank] = DMSEL_BANK_CONFIRMED; + } + s->tries[bank] = 0; + s->selected = (uint8_t)bank; + return save_state( s ); +} + +int dmsel_ready( dmsel_t* s ) +{ + if( s->running >= s->layout->bank_count ) + { + return DMSEL_ERR_PARAM; + } + if( s->state[s->running] != DMSEL_BANK_TRIAL ) + { + return DMSEL_OK; + } + s->state[s->running] = DMSEL_BANK_CONFIRMED; + s->tries[s->running] = 0; + s->previous = NO_BANK; + return save_state( s ); +} + +int dmsel_persist_read( dmsel_t* s, uint32_t id, void* buffer, size_t buffer_size ) +{ + rec_t r; + const uint8_t* p; + const uint8_t* found = NULL; + uint32_t found_len = 0, off = 0; + + if( buffer == NULL && buffer_size != 0 ) + { + return DMSEL_ERR_PARAM; + } + while( rec_read( s, off, &r, &p ) == 1 ) + { + if( (r.type == REC_PERSIST || r.type == REC_DELETE) && r.len >= 4 && le32( p ) == id ) + { + found = r.type == REC_PERSIST ? p + 4 : NULL; + found_len = r.len - 4; + } + off += REC_HDR + pad4( r.len ); + } + if( found == NULL ) + { + return DMSEL_ERR_NOENT; + } + if( buffer_size != 0 ) + { + memcpy( buffer, found, found_len < buffer_size ? found_len : buffer_size ); + } + return (int)found_len; +} + +int dmsel_persist_write( dmsel_t* s, uint32_t id, const void* data, size_t size ) +{ + uint8_t idle[4]; + int current; + + if( (data == NULL && size != 0) || size > 0xFFF0u ) + { + return DMSEL_ERR_PARAM; + } + idle[0] = (uint8_t)id; + idle[1] = (uint8_t)(id >> 8); + idle[2] = (uint8_t)(id >> 16); + idle[3] = (uint8_t)(id >> 24); + current = dmsel_persist_read( s, id, NULL, 0 ); + + if( data == NULL ) + { + return current < 0 ? DMSEL_OK : append( s, REC_DELETE, idle, 4, NULL, 0 ); + } + if( current == (int)size && size > 0 ) + { + // Already there: nothing to wear the flash for + const uint8_t* p; + rec_t r; + uint32_t off = 0; + const uint8_t* value = NULL; + + while( rec_read( s, off, &r, &p ) == 1 ) + { + if( (r.type == REC_PERSIST || r.type == REC_DELETE) && r.len >= 4 && le32( p ) == id ) + { + value = r.type == REC_PERSIST ? p + 4 : NULL; + } + off += REC_HDR + pad4( r.len ); + } + if( value != NULL && memcmp( value, data, size ) == 0 ) + { + return DMSEL_OK; + } + } + return append( s, REC_PERSIST, idle, 4, data, (uint32_t)size ); +} diff --git a/selector/tests/test_selector.c b/selector/tests/test_selector.c new file mode 100644 index 0000000..1115db9 --- /dev/null +++ b/selector/tests/test_selector.c @@ -0,0 +1,400 @@ +/** + * @file test_selector.c + * @brief The selector on the flash in RAM: the choice of the bank, the trial and its confirmation, damaged images, a lost + * state, the persistent items, the compaction of the store, power cuts + */ +#include +#include +#include +#include "dmsel.h" +#include "dmsel_port_host.h" + +static int g_checks, g_failures; +#define CHECK_EQ( a, e ) do { long long a_ = (long long)(a), e_ = (long long)(e); g_checks++; \ + if( a_ != e_ ) { g_failures++; printf( " FAIL %s:%d: %s == %s (got %lld, expected %lld)\n", __FILE__, __LINE__, #a, #e, a_, e_ ); } } while( 0 ) +#define CHECK( c ) do { g_checks++; if( !(c) ) { g_failures++; printf( " FAIL %s:%d: %s\n", __FILE__, __LINE__, #c ); } } while( 0 ) +#define RUN( t ) do { int b_ = g_failures; t(); printf( "%-4s %s\n", g_failures == b_ ? "ok" : "FAIL", #t ); } while( 0 ) + +#define FLASH_ADDRESS 0x08000000u +#define STORE_OFFSET 0x8000u +#define STORE_SIZE 0x8000u +#define BANK_A 0x20000u +#define BANK_B 0x80000u +#define BANK_SIZE 0x60000u + +static host_flash_t g_f; +static uint8_t g_scratch[STORE_SIZE]; +static dmsel_layout_t g_layout; +static dmsel_t g_s; + +static void setup( void ) +{ + host_flash_init( &g_f ); + memset( &g_layout, 0, sizeof(g_layout) ); + g_layout.flash_address = FLASH_ADDRESS; + g_layout.store_offset = STORE_OFFSET; + g_layout.store_size = STORE_SIZE; + g_layout.bank_count = 2; + g_layout.banks[0].offset = BANK_A; + g_layout.banks[0].capacity = BANK_SIZE; + g_layout.banks[1].offset = BANK_B; + g_layout.banks[1].capacity = BANK_SIZE; + g_layout.max_tries = 3; + g_layout.scratch = g_scratch; +} + +/** The selector starts again after a reset: the flash stays, the RAM does not */ +static void reset( void ) +{ + g_f.jumped_to = 0; + g_f.fail_after = -1; + CHECK_EQ( dmsel_init( &g_s, &g_layout, &g_f.port ), DMSEL_OK ); +} + +/** An image: header, vectors at 512, a body; written straight into the flash (as the update tool would) */ +static void put_image( unsigned bank, uint32_t version, uint32_t size, uint8_t fill ) +{ + uint8_t* base = g_f.flash + g_layout.banks[bank].offset; + dmsel_header_t h; + + memset( base, 0xFF, size ); + memset( base + 512, fill, size - 512 ); + memset( &h, 0xFF, sizeof(h) ); + h.magic = DMSEL_HEADER_MAGIC; + h.header_version = DMSEL_HEADER_VERSION; + h.header_size = sizeof(h); + h.image_size = size; + h.version = version; + h.image_crc = dmsel_crc32( 0, base + h.header_size, size - h.header_size ); + h.entry_offset = 512; + h.flags = 0; + h.header_crc = dmsel_crc32( 0, &h, sizeof(h) - 4 ); + memcpy( base, &h, sizeof(h) ); +} + +static uint32_t vector_of( unsigned bank ) +{ + return FLASH_ADDRESS + g_layout.banks[bank].offset + 512; +} + +static void test_crc( void ) +{ + CHECK_EQ( dmsel_crc32( 0, "123456789", 9 ), 0xCBF43926u ); + CHECK_EQ( dmsel_crc32( dmsel_crc32( 0, "12345", 5 ), "6789", 4 ), 0xCBF43926u ); +} + +static void test_nothing_to_start( void ) +{ + setup(); reset(); + CHECK_EQ( dmsel_choose( &g_s ), DMSEL_ERR_EMPTY ); + dmsel_boot( &g_s ); + CHECK_EQ( g_f.jumped_to, 0 ); + CHECK( strstr( g_f.log, "nothing to start" ) != NULL ); +} + +static void test_single_image_starts( void ) +{ + setup(); put_image( 0, 5, 0x4000, 0xA5 ); reset(); + dmsel_boot( &g_s ); + CHECK_EQ( g_f.jumped_to, vector_of( 0 ) ); + CHECK_EQ( dmsel_running_bank( &g_s ), 0 ); + // And again after a reset: the state is in the store now + reset(); + dmsel_boot( &g_s ); + CHECK_EQ( g_f.jumped_to, vector_of( 0 ) ); + CHECK( g_s.state_valid ); +} + +static void test_newest_image_without_a_state( void ) +{ + setup(); put_image( 0, 5, 0x4000, 1 ); put_image( 1, 9, 0x5000, 2 ); reset(); + CHECK_EQ( dmsel_choose( &g_s ), 1 ); + setup(); put_image( 0, 12, 0x4000, 1 ); put_image( 1, 9, 0x5000, 2 ); reset(); + CHECK_EQ( dmsel_choose( &g_s ), 0 ); +} + +static void test_trial_confirmed( void ) +{ + dmsel_bank_info_t info; + + setup(); put_image( 0, 1, 0x4000, 1 ); reset(); + CHECK_EQ( dmsel_choose( &g_s ), 0 ); + // The update: an image into B, verified, selected on trial + put_image( 1, 2, 0x5000, 2 ); + CHECK_EQ( dmsel_select( &g_s, 1, 1 ), DMSEL_OK ); + reset(); + CHECK_EQ( dmsel_choose( &g_s ), 1 ); + info.struct_size = sizeof(info); + CHECK_EQ( dmsel_get_info( &g_s, 1, &info ), DMSEL_OK ); + CHECK_EQ( info.state, DMSEL_BANK_TRIAL ); + CHECK_EQ( info.tries, 1 ); + CHECK_EQ( info.version, 2 ); + CHECK_EQ( info.address, FLASH_ADDRESS + BANK_B ); + // The firmware says it runs properly + CHECK_EQ( dmsel_ready( &g_s ), DMSEL_OK ); + reset(); + CHECK_EQ( dmsel_choose( &g_s ), 1 ); + CHECK_EQ( dmsel_get_info( &g_s, 1, &info ), DMSEL_OK ); + CHECK_EQ( info.state, DMSEL_BANK_CONFIRMED ); + CHECK_EQ( info.tries, 0 ); +} + +static void test_trial_not_confirmed_goes_back( void ) +{ + int i; + + setup(); put_image( 0, 1, 0x4000, 1 ); reset(); + CHECK_EQ( dmsel_choose( &g_s ), 0 ); + CHECK_EQ( dmsel_ready( &g_s ), DMSEL_OK ); + put_image( 1, 2, 0x5000, 2 ); + CHECK_EQ( dmsel_select( &g_s, 1, 1 ), DMSEL_OK ); + // Three starts without a word + for( i = 0; i < 3; i++ ) + { + reset(); + CHECK_EQ( dmsel_choose( &g_s ), 1 ); + } + // The fourth: the image is dropped, the previous one runs + reset(); + CHECK_EQ( dmsel_choose( &g_s ), 0 ); + CHECK_EQ( g_s.state[1], DMSEL_BANK_INVALID ); + reset(); + CHECK_EQ( dmsel_choose( &g_s ), 0 ); + CHECK_EQ( g_s.state[1], DMSEL_BANK_INVALID ); +} + +static void test_permanent_select( void ) +{ + setup(); put_image( 0, 1, 0x4000, 1 ); put_image( 1, 2, 0x4000, 2 ); reset(); + CHECK_EQ( dmsel_choose( &g_s ), 1 ); + CHECK_EQ( dmsel_select( &g_s, 0, 0 ), DMSEL_OK ); // back to A for good + reset(); + CHECK_EQ( dmsel_choose( &g_s ), 0 ); + CHECK_EQ( g_s.state[0], DMSEL_BANK_CONFIRMED ); +} + +static void test_damaged_image_falls_back( void ) +{ + setup(); put_image( 0, 1, 0x4000, 1 ); reset(); + CHECK_EQ( dmsel_choose( &g_s ), 0 ); + CHECK_EQ( dmsel_ready( &g_s ), DMSEL_OK ); + put_image( 1, 2, 0x5000, 2 ); + CHECK_EQ( dmsel_select( &g_s, 1, 1 ), DMSEL_OK ); + g_f.flash[BANK_B + 0x1234] ^= 0x10; // a bit flips in the selected image + reset(); + CHECK_EQ( dmsel_choose( &g_s ), 0 ); + CHECK_EQ( g_s.state[1], DMSEL_BANK_INVALID ); + + // select() and verify() refuse such an image, and an empty bank + CHECK_EQ( dmsel_verify( &g_s, 1 ), DMSEL_ERR_BAD ); + CHECK_EQ( dmsel_select( &g_s, 1, 1 ), DMSEL_ERR_BAD ); + memset( g_f.flash + BANK_B, 0xFF, 0x6000 ); + CHECK_EQ( dmsel_verify( &g_s, 1 ), DMSEL_ERR_EMPTY ); + CHECK_EQ( dmsel_select( &g_s, 1, 0 ), DMSEL_ERR_EMPTY ); + CHECK_EQ( dmsel_select( &g_s, 2, 0 ), DMSEL_ERR_PARAM ); +} + +static void test_header_checks( void ) +{ + dmsel_header_t h; + + setup(); reset(); + put_image( 0, 1, 0x4000, 7 ); + CHECK_EQ( dmsel_verify( &g_s, 0 ), DMSEL_OK ); + // A header that was changed: its CRC fails + memcpy( &h, g_f.flash + BANK_A, sizeof(h) ); + h.version = 99; + memcpy( g_f.flash + BANK_A, &h, sizeof(h) ); + CHECK_EQ( dmsel_verify( &g_s, 0 ), DMSEL_ERR_BAD ); + // An image bigger than the bank + put_image( 0, 1, 0x4000, 7 ); + memcpy( &h, g_f.flash + BANK_A, sizeof(h) ); + h.image_size = BANK_SIZE + 4; + h.header_crc = dmsel_crc32( 0, &h, sizeof(h) - 4 ); + memcpy( g_f.flash + BANK_A, &h, sizeof(h) ); + CHECK_EQ( dmsel_verify( &g_s, 0 ), DMSEL_ERR_BAD ); + // Vectors not at a multiple of 512 + put_image( 0, 1, 0x4000, 7 ); + memcpy( &h, g_f.flash + BANK_A, sizeof(h) ); + h.entry_offset = 100; + h.header_crc = dmsel_crc32( 0, &h, sizeof(h) - 4 ); + memcpy( g_f.flash + BANK_A, &h, sizeof(h) ); + CHECK_EQ( dmsel_verify( &g_s, 0 ), DMSEL_ERR_BAD ); +} + +static void test_lost_state_is_rebuilt( void ) +{ + setup(); put_image( 0, 1, 0x4000, 1 ); put_image( 1, 2, 0x4000, 2 ); reset(); + CHECK_EQ( dmsel_choose( &g_s ), 1 ); + memset( g_f.flash + STORE_OFFSET, 0xFF, STORE_SIZE ); // the store is gone + reset(); + CHECK( !g_s.state_valid ); + CHECK_EQ( dmsel_choose( &g_s ), 1 ); +} + +static void test_persistency( void ) +{ + uint8_t buf[64]; + const char a[] = "first value", b[] = "second"; + + setup(); reset(); + CHECK_EQ( dmsel_persist_read( &g_s, 7, NULL, 0 ), DMSEL_ERR_NOENT ); + CHECK_EQ( dmsel_persist_write( &g_s, 7, a, sizeof(a) ), DMSEL_OK ); + CHECK_EQ( dmsel_persist_read( &g_s, 7, NULL, 0 ), sizeof(a) ); + memset( buf, 0, sizeof(buf) ); + CHECK_EQ( dmsel_persist_read( &g_s, 7, buf, sizeof(buf) ), sizeof(a) ); + CHECK( memcmp( buf, a, sizeof(a) ) == 0 ); + // A smaller buffer: the size of the whole item, a part of the data + memset( buf, 0, sizeof(buf) ); + CHECK_EQ( dmsel_persist_read( &g_s, 7, buf, 5 ), sizeof(a) ); + CHECK( memcmp( buf, a, 5 ) == 0 && buf[5] == 0 ); + // Replaced, another item beside it, both survive a reset + CHECK_EQ( dmsel_persist_write( &g_s, 7, b, sizeof(b) ), DMSEL_OK ); + CHECK_EQ( dmsel_persist_write( &g_s, 8, a, sizeof(a) ), DMSEL_OK ); + reset(); + CHECK_EQ( dmsel_persist_read( &g_s, 7, buf, sizeof(buf) ), sizeof(b) ); + CHECK( memcmp( buf, b, sizeof(b) ) == 0 ); + CHECK_EQ( dmsel_persist_read( &g_s, 8, NULL, 0 ), sizeof(a) ); + // The same value again does not touch the flash + { + int before = g_f.programs; + CHECK_EQ( dmsel_persist_write( &g_s, 7, b, sizeof(b) ), DMSEL_OK ); + CHECK_EQ( g_f.programs, before ); + } + // Removed + CHECK_EQ( dmsel_persist_write( &g_s, 7, NULL, 0 ), DMSEL_OK ); + CHECK_EQ( dmsel_persist_read( &g_s, 7, NULL, 0 ), DMSEL_ERR_NOENT ); + CHECK_EQ( dmsel_persist_write( &g_s, 99, NULL, 0 ), DMSEL_OK ); // nothing to remove: not an error + // An empty item is an item + CHECK_EQ( dmsel_persist_write( &g_s, 5, "", 0 ), DMSEL_OK ); + CHECK_EQ( dmsel_persist_read( &g_s, 5, NULL, 0 ), 0 ); + // Wrong arguments + CHECK_EQ( dmsel_persist_write( &g_s, 1, NULL, 4 ), DMSEL_ERR_PARAM ); + CHECK_EQ( dmsel_persist_read( &g_s, 8, NULL, 4 ), DMSEL_ERR_PARAM ); +} + +static void test_store_is_compacted( void ) +{ + uint8_t value[100], buf[100]; + int i, erases_before; + + setup(); put_image( 0, 1, 0x4000, 1 ); reset(); + CHECK_EQ( dmsel_choose( &g_s ), 0 ); + CHECK_EQ( dmsel_persist_write( &g_s, 1000, "keep", 5 ), DMSEL_OK ); + erases_before = g_f.erases; + // Far more than the sector holds: the same item over and over + for( i = 0; i < 1000; i++ ) + { + memset( value, i & 0xFF, sizeof(value) ); + value[0] = (uint8_t)i; value[1] = (uint8_t)(i >> 8); + CHECK_EQ( dmsel_persist_write( &g_s, 1, value, sizeof(value) ), DMSEL_OK ); + } + CHECK( g_f.erases > erases_before ); + reset(); + CHECK_EQ( dmsel_persist_read( &g_s, 1, buf, sizeof(buf) ), sizeof(value) ); + CHECK( memcmp( buf, value, sizeof(value) ) == 0 ); + CHECK_EQ( dmsel_persist_read( &g_s, 1000, buf, sizeof(buf) ), 5 ); + CHECK( memcmp( buf, "keep", 5 ) == 0 ); + // The state of the banks went through the compactions too + CHECK_EQ( dmsel_choose( &g_s ), 0 ); + CHECK_EQ( g_s.state[0], DMSEL_BANK_VALID ); + CHECK_EQ( g_f.program_errors, 0 ); +} + +static void test_item_too_big( void ) +{ + static uint8_t big[STORE_SIZE]; + + setup(); reset(); + CHECK_EQ( dmsel_persist_write( &g_s, 1, big, STORE_SIZE ), DMSEL_ERR_NOSPACE ); + CHECK_EQ( dmsel_persist_write( &g_s, 1, big, STORE_SIZE - 100 ), DMSEL_OK ); // fits with a short state record +} + +static void test_power_cut_while_writing( void ) +{ + int cut; + uint8_t buf[32]; + + // A cut at every program of an update of the store: the older values stay readable, the store goes on working + for( cut = 0; cut < 6; cut++ ) + { + setup(); put_image( 0, 1, 0x4000, 1 ); reset(); + CHECK_EQ( dmsel_choose( &g_s ), 0 ); + CHECK_EQ( dmsel_persist_write( &g_s, 1, "old", 4 ), DMSEL_OK ); + g_f.fail_after = g_f.programs + cut; + (void)dmsel_persist_write( &g_s, 1, "new!!", 6 ); + reset(); + { + int n = dmsel_persist_read( &g_s, 1, buf, sizeof(buf) ); + CHECK( n == 4 || n == 6 ); // the old one, or the new one - not a mix + CHECK( (n == 4 && memcmp( buf, "old", 4 ) == 0) || (n == 6 && memcmp( buf, "new!!", 6 ) == 0) ); + } + CHECK_EQ( dmsel_persist_write( &g_s, 2, "after", 6 ), DMSEL_OK ); + CHECK_EQ( dmsel_persist_read( &g_s, 2, buf, sizeof(buf) ), 6 ); + CHECK_EQ( dmsel_choose( &g_s ), 0 ); + } +} + +static void test_power_cut_while_choosing( void ) +{ + int cut; + + // The cut when the selector writes a try: the start must still happen from a good bank + for( cut = 0; cut < 4; cut++ ) + { + setup(); put_image( 0, 1, 0x4000, 1 ); put_image( 1, 2, 0x4000, 2 ); reset(); + CHECK_EQ( dmsel_choose( &g_s ), 1 ); + CHECK_EQ( dmsel_select( &g_s, 0, 1 ), DMSEL_OK ); // A on trial, B ran before + g_f.fail_after = g_f.programs + cut; + reset(); + g_f.fail_after = g_f.programs + cut; + (void)dmsel_choose( &g_s ); + reset(); + { + int bank = dmsel_choose( &g_s ); + CHECK( bank == 0 || bank == 1 ); + } + } +} + +static void test_get_info_extensible( void ) +{ + dmsel_bank_info_t info; + uint32_t small[2]; + + setup(); put_image( 0, 3, 0x4000, 1 ); reset(); + CHECK_EQ( dmsel_choose( &g_s ), 0 ); + // A caller built with a smaller structure gets the part that fits and nothing beyond it + small[0] = 8; small[1] = 0xDEADBEEF; + CHECK_EQ( dmsel_get_info( &g_s, 0, (dmsel_bank_info_t*)small ), DMSEL_OK ); + CHECK_EQ( small[0], 8 ); + CHECK_EQ( small[1], DMSEL_BANK_VALID ); + info.struct_size = sizeof(info); + CHECK_EQ( dmsel_get_info( &g_s, 5, &info ), DMSEL_ERR_PARAM ); + CHECK_EQ( dmsel_get_info( &g_s, 1, &info ), DMSEL_OK ); + CHECK_EQ( info.state, DMSEL_BANK_EMPTY ); + CHECK_EQ( info.version, 0 ); + CHECK_EQ( info.capacity, BANK_SIZE ); +} + +int main( void ) +{ + RUN( test_crc ); + RUN( test_nothing_to_start ); + RUN( test_single_image_starts ); + RUN( test_newest_image_without_a_state ); + RUN( test_trial_confirmed ); + RUN( test_trial_not_confirmed_goes_back ); + RUN( test_permanent_select ); + RUN( test_damaged_image_falls_back ); + RUN( test_header_checks ); + RUN( test_lost_state_is_rebuilt ); + RUN( test_persistency ); + RUN( test_store_is_compacted ); + RUN( test_item_too_big ); + RUN( test_power_cut_while_writing ); + RUN( test_power_cut_while_choosing ); + RUN( test_get_info_extensible ); + printf( "test_selector: %d checks, %d failures\n", g_checks, g_failures ); + return g_failures ? 1 : 0; +} From 8044fa7a00d4444c1b1460b4ab51ec5dfa39f220 Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Thu, 29 Oct 2026 05:59:03 +0000 Subject: [PATCH 02/14] selector: the port for the STM32F7, the firmware of the first sector, the table for the firmware, the image tool * selector/fw: the selector as firmware (5.4 kB of the 32 kB sector): vectors and the reset handler, the linker script (the DTCM for the RAM, the table at 0x08000100), the port (flash registers of the F7 - erase and program only in the store sector -, jump with VTOR and MSP, the independent watchdog for an image on trial), dmlog for the log (dmod's symbols that it takes are stubs), a minimal libc. * include/dmsel_table.h: the table of functions that the firmware finds at DMSEL_TABLE_ADDRESS; the state is the firmware's memory (the RAM of the selector is gone when it starts), the scratch for the store is given at init. * tools/dmsel_mkimage.py: the 512 byte header (magic, version, size, CRC) in front of the code linked at the start of the bank + 0x200; a ctest checks that the core accepts what the tool makes. * The core: the scratch is given at dmsel_init; an optional watchdog is started when an image on trial is started. * fw/testbank: a test image of a bank that reports what the table says into the RAM. Tested on the STM32F746G-DISCO (selector in S0, two test images): the newest good image is started; the persistent item is written and read back; the other bank on trial starts with tries 1, 2, 3 and at the fourth start is dropped and the first one runs again; Dmod_FirmwareReady confirms; a permanent choice holds across resets; the record is in S1. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/build.yml | 12 ++ selector/CMakeLists.txt | 10 ++ selector/fw/CMakeLists.txt | 56 +++++++++ selector/fw/dmod_stubs.c | 28 +++++ selector/fw/dmsel_f7.h | 15 +++ selector/fw/dmsel_f7.ld | 20 ++++ selector/fw/dmsel_port_f7.c | 166 +++++++++++++++++++++++++++ selector/fw/libc_min.c | 52 +++++++++ selector/fw/main.c | 87 ++++++++++++++ selector/fw/startup.c | 34 ++++++ selector/fw/testbank/bank.ld | 17 +++ selector/fw/testbank/build.sh | 14 +++ selector/fw/testbank/libc.c | 6 + selector/fw/testbank/main.c | 89 ++++++++++++++ selector/fw/testbank/start.c | 13 +++ selector/include/dmsel.h | 11 +- selector/include/dmsel_table.h | 50 ++++++++ selector/port/host/dmsel_port_host.c | 6 + selector/port/host/dmsel_port_host.h | 1 + selector/src/dmsel.c | 14 ++- selector/tests/mkimage.cmake | 9 ++ selector/tests/test_selector.c | 47 +++++++- selector/tools/dmsel_mkimage.py | 37 ++++++ 23 files changed, 784 insertions(+), 10 deletions(-) create mode 100644 selector/fw/CMakeLists.txt create mode 100644 selector/fw/dmod_stubs.c create mode 100644 selector/fw/dmsel_f7.h create mode 100644 selector/fw/dmsel_f7.ld create mode 100644 selector/fw/dmsel_port_f7.c create mode 100644 selector/fw/libc_min.c create mode 100644 selector/fw/main.c create mode 100644 selector/fw/startup.c create mode 100644 selector/fw/testbank/bank.ld create mode 100755 selector/fw/testbank/build.sh create mode 100644 selector/fw/testbank/libc.c create mode 100644 selector/fw/testbank/main.c create mode 100644 selector/fw/testbank/start.c create mode 100644 selector/include/dmsel_table.h create mode 100644 selector/tests/mkimage.cmake create mode 100644 selector/tools/dmsel_mkimage.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f2371b1..1d4776f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -160,6 +160,18 @@ jobs: cmake --build build-selector ctest --test-dir build-selector --output-on-failure + - name: Install the ARM toolchain + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends gcc-arm-none-eabi libnewlib-arm-none-eabi binutils-arm-none-eabi + + - name: Build the selector for the STM32F7 (it has to fit 32 kB; the linker checks) + run: | + cmake -S selector/fw -B build-selector-fw + cmake --build build-selector-fw + ls -l build-selector-fw/dmsel.bin + + - name: Build the test images of the banks + run: selector/fw/testbank/build.sh build-selector-banks 1 2 + test-renode: name: Test Renode Simulation runs-on: ubuntu-latest diff --git a/selector/CMakeLists.txt b/selector/CMakeLists.txt index 5de0271..9373524 100644 --- a/selector/CMakeLists.txt +++ b/selector/CMakeLists.txt @@ -22,3 +22,13 @@ if(DMSEL_SANITIZE) endforeach() endif() add_test(NAME test_selector COMMAND test_selector) + +# The tool that makes the images (python) and the core have to agree on the header and on the CRC +find_package(Python3 COMPONENTS Interpreter) +if(Python3_FOUND) + file(WRITE ${CMAKE_BINARY_DIR}/code.bin "vectors and code of a test image - 0123456789") + add_test(NAME mkimage COMMAND ${CMAKE_COMMAND} + -DPY=${Python3_EXECUTABLE} -DTOOL=${CMAKE_CURRENT_SOURCE_DIR}/tools/dmsel_mkimage.py + -DIN=${CMAKE_BINARY_DIR}/code.bin -DOUT=${CMAKE_BINARY_DIR}/image.bin -DCHECK=$ + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/mkimage.cmake) +endif() diff --git a/selector/fw/CMakeLists.txt b/selector/fw/CMakeLists.txt new file mode 100644 index 0000000..09febd7 --- /dev/null +++ b/selector/fw/CMakeLists.txt @@ -0,0 +1,56 @@ +# The selector for the STM32F7 (the code of the first sector of the flash) +# cmake -S selector/fw -B build-selector-fw && cmake --build build-selector-fw +# Result: dmsel.elf and dmsel.bin (at most 32 kB: the linker script checks) +cmake_minimum_required(VERSION 3.16) +set(CMAKE_SYSTEM_NAME Generic) +set(CMAKE_SYSTEM_PROCESSOR arm) +set(CMAKE_C_COMPILER arm-none-eabi-gcc) +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) +project(dmsel_fw C) + +set(DMSEL_WATCHDOG_MS 0 CACHE STRING "Watchdog (ms) armed for an image on trial; the firmware has to feed it, 0: none") +set(DMBOOT_DIR "${CMAKE_CURRENT_LIST_DIR}/../..") + +# dmlog.c wants the configuration header of dmod: a bare-metal one, as the firmware has it +set(DMOD_VERSION_STRING "1.0") +foreach(opt STDLIB GETENV ENVIRON STDIO DIRENT ASSERT PTHREAD MMAN ALIGNED_ALLOC ALIGNED_MALLOC_MOCK REALLOC TERMIOS TIME DMCOMP_ENCODER) + set(DMOD_USE_${opt} OFF) +endforeach() +set(DMOD_USE_FASTLZ OFF) +set(DMOD_USE_DMCOMP OFF) +set(DMOD_IMPLEMENT_PRINTF OFF) +set(DMOD_IMPLEMENT_SCANF OFF) +set(DMOD_MAX_MODULES 1) +set(DMOD_MAX_REQUIRED_MODULES 1) +set(DMOD_VFPRINTF_STACK_BUFFER_SIZE 100) +set(DMOD_MODE "DMOD_SYSTEM") +set(DMOD_SYSTEM ON) +set(DMOD_MODULE OFF) +set(DMOD_SYSTEM_VERSION_MAJOR 0) +set(DMOD_SYSTEM_VERSION_MINOR 1) +set(DMOD_REPO_DIR "/") +set(DMOD_REPO_PATHS "/") +set(DMOD_CPU_NAME "stm32f746xg") +set(DMOD_ARRAY_SEP ":") +set(DMOD_PATH_SEP "/") +set(DMOD_BUILTIN_COMPRESSION_API OFF) +set(DMOD_BUILD_DIR "${CMAKE_BINARY_DIR}") +set(dmod_VERSION_MAJOR 1) +set(dmod_VERSION_MINOR 0) +configure_file("${DMBOOT_DIR}/lib/dmod/dmod-config.h.in" "${CMAKE_BINARY_DIR}/gen/dmod-config.h") + +add_executable(dmsel.elf + startup.c main.c dmsel_port_f7.c dmod_stubs.c libc_min.c + ../src/dmsel.c + ${DMBOOT_DIR}/lib/dmlog/src/dmlog.c) +set_source_files_properties(${DMBOOT_DIR}/lib/dmlog/src/dmlog.c PROPERTIES COMPILE_OPTIONS "-w") +target_include_directories(dmsel.elf PRIVATE . ../include ${DMBOOT_DIR}/lib/dmlog/include ${DMBOOT_DIR}/lib/dmod/inc ${CMAKE_BINARY_DIR}/gen) +target_compile_definitions(dmsel.elf PRIVATE DMOD_EXTERNAL_REGISTRATION DMOD_CURRENT_ALLOCATOR="dmsel" DMSEL_WATCHDOG_MS=${DMSEL_WATCHDOG_MS}) +target_compile_options(dmsel.elf PRIVATE -mcpu=cortex-m7 -mthumb -Os -ffunction-sections -fdata-sections -ffreestanding + -Wall -Wextra -std=gnu11) +target_link_options(dmsel.elf PRIVATE -mcpu=cortex-m7 -mthumb -nostdlib -nostartfiles -Wl,--gc-sections + -T${CMAKE_CURRENT_LIST_DIR}/dmsel_f7.ld -Wl,-Map=${CMAKE_BINARY_DIR}/dmsel.map -lgcc) +set_target_properties(dmsel.elf PROPERTIES LINK_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/dmsel_f7.ld) +add_custom_command(TARGET dmsel.elf POST_BUILD + COMMAND arm-none-eabi-objcopy -O binary dmsel.elf dmsel.bin + COMMAND arm-none-eabi-size dmsel.elf) diff --git a/selector/fw/dmod_stubs.c b/selector/fw/dmod_stubs.c new file mode 100644 index 0000000..2849283 --- /dev/null +++ b/selector/fw/dmod_stubs.c @@ -0,0 +1,28 @@ +/** + * @file dmod_stubs.c + * @brief What dmlog.c takes from dmod and the selector does not have: nothing runs beside it, there are no files and no heap + */ +#include "dmod.h" + +void Dmod_Assert( int Condition, const char* Message, const char* File, int Line, const char* Function ) +{ + (void)Message; (void)File; (void)Line; (void)Function; + while( !Condition ) {} +} +bool Dmod_CheckLogLevel( Dmod_LogLevel_t LogLevel ) { (void)LogLevel; return false; } +void Dmod_EnterCritical( void ) {} +void Dmod_ExitCritical( void ) {} +int Dmod_FPrintf( void* File, const char* Format, ... ) { (void)File; (void)Format; return 0; } +void Dmod_FileClose( void* File ) { (void)File; } +void* Dmod_FileOpen( const char* Path, const char* Mode ) { (void)Path; (void)Mode; return 0; } +size_t Dmod_FileRead( void* Buffer, size_t Size, size_t Count, void* File ) { (void)Buffer; (void)Size; (void)Count; (void)File; return 0; } +Dmod_FileSize_t Dmod_FileSize( void* File ) { (void)File; return 0; } +size_t Dmod_FileWrite( const void* Buffer, size_t Size, size_t Count, void* File ) { (void)Buffer; (void)Size; (void)Count; (void)File; return 0; } +void Dmod_FreeEx( void* Ptr, bool Concatenate ) { (void)Ptr; (void)Concatenate; } +void* Dmod_GetStreamLogFile( Dmod_LogLevel_t LogLevel ) { (void)LogLevel; return 0; } +int Dmod_Ioctl( void* File, int Command, void* Arg ) { (void)File; (void)Command; (void)Arg; return -1; } +void* Dmod_LockStdio( void* StdHandle ) { (void)StdHandle; return 0; } +void* Dmod_MallocEx( size_t Size, const char* ModuleName ) { (void)Size; (void)ModuleName; return 0; } +void Dmod_ThreadSleep( uint32_t Milliseconds ) { (void)Milliseconds; } +void Dmod_UnlockStdio( void* StdHandle ) { (void)StdHandle; } +const char* strerror( int errnum ) { (void)errnum; return ""; } diff --git a/selector/fw/dmsel_f7.h b/selector/fw/dmsel_f7.h new file mode 100644 index 0000000..0969cea --- /dev/null +++ b/selector/fw/dmsel_f7.h @@ -0,0 +1,15 @@ +#ifndef DMSEL_F7_H +#define DMSEL_F7_H + +#include + +/** The sector of the store (S1) - the only one that this port erases and programs */ +#define DMSEL_F7_STORE_OFFSET 0x08000u +#define DMSEL_F7_STORE_SIZE 0x08000u + +int dmsel_f7_erase( void* user, uint32_t offset, uint32_t size ); +int dmsel_f7_program( void* user, uint32_t offset, const void* data, uint32_t size ); +void dmsel_f7_jump( void* user, uint32_t vector_address ); +void dmsel_f7_watchdog_start( void* user, uint32_t timeout_ms ); + +#endif diff --git a/selector/fw/dmsel_f7.ld b/selector/fw/dmsel_f7.ld new file mode 100644 index 0000000..8cb3390 --- /dev/null +++ b/selector/fw/dmsel_f7.ld @@ -0,0 +1,20 @@ +/* The selector: the first sector of the flash (32 kB), the DTCM for the RAM */ +MEMORY +{ + FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K + DTCM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K +} +ENTRY(Reset_Handler) +_estack = ORIGIN(DTCM) + 0x1000; /* 4 kB of the stack at the bottom of the DTCM, the data above it */ + +SECTIONS +{ + .isr_vector : { KEEP(*(.isr_vector)) } > FLASH + .dmsel_table ORIGIN(FLASH) + 0x100 : { KEEP(*(.dmsel_table)) } > FLASH /* DMSEL_TABLE_ADDRESS */ + .text : { *(.text .text.*) *(.rodata .rodata.*) . = ALIGN(4); } > FLASH + _sidata = LOADADDR(.data); + .data ORIGIN(DTCM) + 0x1000 : { _sdata = .; *(.data .data.*) . = ALIGN(4); _edata = .; } > DTCM AT > FLASH + .bss ORIGIN(DTCM) + 0x1000 (NOLOAD) : { _sbss = .; *(.bss .bss.*) *(COMMON) . = ALIGN(4); _ebss = .; } > DTCM + /DISCARD/ : { *(.ARM.exidx*) *(.comment) *(.inputs) *(.dmod*) } +} +ASSERT(SIZEOF(.data) == 0, "the selector has no initialized RAM: keep it const") diff --git a/selector/fw/dmsel_port_f7.c b/selector/fw/dmsel_port_f7.c new file mode 100644 index 0000000..db72e19 --- /dev/null +++ b/selector/fw/dmsel_port_f7.c @@ -0,0 +1,166 @@ +/** + * @file dmsel_port_f7.c + * @brief The selector's port for the STM32F7 (RM0385): the flash, the jump, the watchdog + * + * Only the store sector is ever erased or programmed from here (a guard): a mistake of the core cannot touch the selector + * or the banks. The flash is single bank: the CPU is stopped while a sector is erased (up to a second for 256 kB). + */ +#include +#include "dmsel.h" +#include "dmsel_f7.h" + +#define REG(a) (*(volatile uint32_t*)(a)) +#define FLASH_BASE 0x40023C00u +#define FLASH_ACR REG( FLASH_BASE + 0x00 ) +#define FLASH_KEYR REG( FLASH_BASE + 0x04 ) +#define FLASH_SR REG( FLASH_BASE + 0x0C ) +#define FLASH_CR REG( FLASH_BASE + 0x10 ) +#define SR_BSY (1u << 16) +#define SR_ERRORS 0xF2u // OPERR, WRPERR, PGAERR, PGPERR, ERSERR +#define SR_CLEAR 0xF3u +#define CR_PG (1u << 0) +#define CR_SER (1u << 1) +#define CR_SNB_MASK (0xFu << 3) +#define CR_PSIZE_MASK (3u << 8) +#define CR_PSIZE_X32 (2u << 8) +#define CR_STRT (1u << 16) +#define CR_LOCK (1u << 31) +#define ACR_ICEN (1u << 9) +#define ACR_DCEN (1u << 10) +#define ACR_ICRST (1u << 11) +#define ACR_DCRST (1u << 12) + +static const struct { uint32_t offset, size; } g_sectors[8] = +{ + { 0x00000, 0x08000 }, { 0x08000, 0x08000 }, { 0x10000, 0x08000 }, { 0x18000, 0x08000 }, + { 0x20000, 0x20000 }, { 0x40000, 0x40000 }, { 0x80000, 0x40000 }, { 0xC0000, 0x40000 }, +}; + +static int flash_wait( void ) +{ + while( FLASH_SR & SR_BSY ) {} + return (FLASH_SR & SR_ERRORS) ? -1 : 0; +} + +static void flash_unlock( void ) +{ + if( FLASH_CR & CR_LOCK ) + { + FLASH_KEYR = 0x45670123u; + FLASH_KEYR = 0xCDEF89ABu; + } + FLASH_SR = SR_CLEAR; +} + +/** The accelerator (ART) and the data cache hold what was there before: drop it, so that the next read sees the flash */ +static void flash_refresh( uint32_t offset, uint32_t size ) +{ + uint32_t acr = FLASH_ACR, a; + + FLASH_ACR = acr & ~(ACR_ICEN | ACR_DCEN); + FLASH_ACR = (acr & ~(ACR_ICEN | ACR_DCEN)) | ACR_ICRST | ACR_DCRST; + FLASH_ACR = acr; + if( REG( 0xE000ED14u ) & (1u << 16) ) // CCR.DC: the data cache of the core is on (the firmware turned it on) + { + for( a = (0x08000000u + offset) & ~31u; a < 0x08000000u + offset + size; a += 32 ) + { + REG( 0xE000EF5Cu ) = a; // DCIMVAC + } + __asm volatile ( "dsb" ::: "memory" ); + } + __asm volatile ( "isb" ::: "memory" ); +} + +static int allowed( uint32_t offset, uint32_t size ) +{ + return offset >= DMSEL_F7_STORE_OFFSET && offset + size <= DMSEL_F7_STORE_OFFSET + DMSEL_F7_STORE_SIZE && offset + size >= offset; +} + +int dmsel_f7_erase( void* user, uint32_t offset, uint32_t size ) +{ + unsigned n; + int result = 0; + + (void)user; + if( !allowed( offset, size ) ) + { + return -1; + } + flash_unlock(); + for( n = 0; n < 8 && result == 0; n++ ) + { + if( g_sectors[n].offset >= offset && g_sectors[n].offset + g_sectors[n].size <= offset + size ) + { + result = flash_wait(); + FLASH_CR = (FLASH_CR & ~(CR_SNB_MASK | CR_PSIZE_MASK)) | CR_PSIZE_X32 | CR_SER | (n << 3); + FLASH_CR |= CR_STRT; + result |= flash_wait(); + FLASH_CR &= ~(CR_SER | CR_SNB_MASK); + } + } + FLASH_CR |= CR_LOCK; + flash_refresh( offset, size ); + return result; +} + +int dmsel_f7_program( void* user, uint32_t offset, const void* data, uint32_t size ) +{ + const uint8_t* d = (const uint8_t*)data; + uint32_t i; + int result = 0; + + (void)user; + if( (offset & 3u) != 0 || (size & 3u) != 0 || !allowed( offset, size ) ) + { + return -1; + } + flash_unlock(); + FLASH_CR = (FLASH_CR & ~CR_PSIZE_MASK) | CR_PSIZE_X32; + for( i = 0; i < size && result == 0; i += 4 ) + { + uint32_t w = (uint32_t)d[i] | ((uint32_t)d[i + 1] << 8) | ((uint32_t)d[i + 2] << 16) | ((uint32_t)d[i + 3] << 24); + result = flash_wait(); + FLASH_CR |= CR_PG; + *(volatile uint32_t*)(0x08000000u + offset + i) = w; + __asm volatile ( "dsb" ::: "memory" ); + result |= flash_wait(); + FLASH_CR &= ~CR_PG; + } + FLASH_CR |= CR_LOCK; + flash_refresh( offset, size ); + return result; +} + +void dmsel_f7_jump( void* user, uint32_t vector_address ) +{ + const uint32_t* vectors = (const uint32_t*)vector_address; + uint32_t sp = vectors[0], reset = vectors[1], i; + + (void)user; + __asm volatile ( "cpsid i" ::: "memory" ); + REG( 0xE000E010u ) = 0; // SysTick off + for( i = 0; i < 8; i++ ) + { + REG( 0xE000E180u + 4 * i ) = 0xFFFFFFFFu; // ICER + REG( 0xE000E280u + 4 * i ) = 0xFFFFFFFFu; // ICPR + } + REG( 0xE000ED08u ) = vector_address; // VTOR + __asm volatile ( "dsb\n isb\n msr msp, %0\n cpsie i\n bx %1" :: "r"(sp), "r"(reset) : "memory" ); + for( ;; ) {} +} + +void dmsel_f7_watchdog_start( void* user, uint32_t timeout_ms ) +{ + uint32_t reload = timeout_ms / 8u; // LSI 32 kHz / 256 = 125 Hz: 8 ms per tick + + (void)user; + if( reload > 0xFFFu ) reload = 0xFFFu; + if( reload == 0 ) reload = 1; + REG( 0xE0042008u ) |= (1u << 12); // DBGMCU: the watchdog stops while the core is halted by the debugger + REG( 0x40003000u ) = 0xCCCCu; // IWDG: start (cannot be stopped) + REG( 0x40003000u ) = 0x5555u; + REG( 0x40003004u ) = 6; // /256 + REG( 0x40003008u ) = reload; + while( REG( 0x4000300Cu ) != 0 ) {} + REG( 0x40003000u ) = 0xAAAAu; +} diff --git a/selector/fw/libc_min.c b/selector/fw/libc_min.c new file mode 100644 index 0000000..623a14b --- /dev/null +++ b/selector/fw/libc_min.c @@ -0,0 +1,52 @@ +/** + * @file libc_min.c + * @brief The few functions of the C library that the selector uses (it is linked without one) + */ +#include +#include + +#define NO_PATTERNS __attribute__(( optimize( "no-tree-loop-distribute-patterns" ) )) + +NO_PATTERNS void* memcpy( void* dst, const void* src, size_t n ) +{ + uint8_t* d = dst; const uint8_t* s = src; + while( n-- ) *d++ = *s++; + return dst; +} + +NO_PATTERNS void* memmove( void* dst, const void* src, size_t n ) +{ + uint8_t* d = dst; const uint8_t* s = src; + if( d < s ) { while( n-- ) *d++ = *s++; } + else { d += n; s += n; while( n-- ) *--d = *--s; } + return dst; +} + +NO_PATTERNS void* memset( void* dst, int c, size_t n ) +{ + uint8_t* d = dst; + while( n-- ) *d++ = (uint8_t)c; + return dst; +} + +NO_PATTERNS int memcmp( const void* a, const void* b, size_t n ) +{ + const uint8_t *x = a, *y = b; + while( n-- ) { if( *x != *y ) return *x - *y; x++; y++; } + return 0; +} + +NO_PATTERNS size_t strlen( const char* s ) +{ + size_t n = 0; + while( s[n] ) n++; + return n; +} + +NO_PATTERNS char* strncpy( char* dst, const char* src, size_t n ) +{ + size_t i = 0; + for( ; i < n && src[i]; i++ ) dst[i] = src[i]; + for( ; i < n; i++ ) dst[i] = 0; + return dst; +} diff --git a/selector/fw/main.c b/selector/fw/main.c new file mode 100644 index 0000000..faa6e7f --- /dev/null +++ b/selector/fw/main.c @@ -0,0 +1,87 @@ +/** + * @file main.c + * @brief The selector for the STM32F746G-DISCO: the layout of the flash, the log, the table for the firmware + * + * Flash: S0 selector | S1 store (state of the banks, persistent items) | S2, S3 file systems of the banks | + * S4+S5 bank A (384 kB) | S6+S7 bank B (384 kB of the 512 kB) + */ +#include +#include "dmlog.h" +#include "dmsel_table.h" +#include "dmsel_f7.h" + +#ifndef DMSEL_WATCHDOG_MS +#define DMSEL_WATCHDOG_MS 0 // The firmware has to feed the watchdog from the moment it starts: 0 until it does +#endif +#define LOG_SIZE 4096u +#define SCRATCH_SIZE 0x8000u +#define SELECTOR_VERSION 1 + +static const dmsel_layout_t g_layout = +{ + .flash_address = 0x08000000u, + .store_offset = DMSEL_F7_STORE_OFFSET, + .store_size = DMSEL_F7_STORE_SIZE, + .bank_count = 2, + .banks = { { 0x20000u, 0x60000u }, { 0x80000u, 0x60000u } }, + .max_tries = 3, + .watchdog_ms = DMSEL_WATCHDOG_MS, +}; + +static uint8_t g_scratch[SCRATCH_SIZE] __attribute__(( aligned( 8 ) )); +static uint8_t g_log_storage[LOG_SIZE] __attribute__(( aligned( 8 ) )); +static dmlog_ctx_t g_log; +static dmsel_t g_state; + +static void port_log( void* user, const char* text ) +{ + (void)user; + if( g_log != NULL ) + { + dmlog_puts( g_log, text ); + } +} + +/** The selector's own port (it logs), and the one behind the table (the firmware has the log of its own) */ +static const dmsel_port_t g_port = +{ + .flash = (const uint8_t*)0x08000000u, .erase = dmsel_f7_erase, .program = dmsel_f7_program, + .jump = dmsel_f7_jump, .log = port_log, .watchdog_start = dmsel_f7_watchdog_start, +}; +static const dmsel_port_t g_port_for_firmware = +{ + .flash = (const uint8_t*)0x08000000u, .erase = dmsel_f7_erase, .program = dmsel_f7_program, + .jump = dmsel_f7_jump, .log = 0, .watchdog_start = dmsel_f7_watchdog_start, +}; + +static int table_init( dmsel_t* s, int running, uint8_t* scratch ) +{ + int r = dmsel_init( s, &g_layout, &g_port_for_firmware, scratch ); + + s->running = (running >= 0 && (uint32_t)running < g_layout.bank_count) ? (uint8_t)running : DMSEL_MAX_BANKS; + return r; +} + +__attribute__(( section( ".dmsel_table" ), used )) +const dmsel_table_t g_table = +{ + .magic = DMSEL_TABLE_MAGIC, .abi = DMSEL_TABLE_ABI, .table_size = sizeof(dmsel_table_t), + .state_size = sizeof(dmsel_t), .scratch_size = DMSEL_F7_STORE_SIZE, .version = SELECTOR_VERSION, + .log_address = (uint32_t)(uintptr_t)g_log_storage, .log_size = LOG_SIZE, + .init = table_init, .bank_count = dmsel_bank_count, .running_bank = dmsel_running_bank, + .get_info = dmsel_get_info, .verify = dmsel_verify, .select = dmsel_select, .ready = dmsel_ready, + .persist_write = dmsel_persist_write, .persist_read = dmsel_persist_read, +}; + +int main( void ) +{ + g_log = dmlog_create( g_log_storage, LOG_SIZE ); + dmlog_puts( g_log, "dmsel: selector started\n" ); + dmsel_init( &g_state, &g_layout, &g_port, g_scratch ); + dmsel_boot( &g_state ); + // Only when nothing can be started: the log stays in the RAM for the debugger + for( ;; ) + { + __asm volatile ( "wfi" ); + } +} diff --git a/selector/fw/startup.c b/selector/fw/startup.c new file mode 100644 index 0000000..85ac786 --- /dev/null +++ b/selector/fw/startup.c @@ -0,0 +1,34 @@ +/** + * @file startup.c + * @brief Vectors and the reset handler of the selector: the RAM is the DTCM, nothing else runs here + */ +#include + +extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss; +int main( void ); + +void Default_Handler( void ) +{ + for( ;; ) {} +} + +void Reset_Handler( void ) +{ + uint32_t *src = &_sidata, *dst; + + for( dst = &_sdata; dst < &_edata; ) *dst++ = *src++; + for( dst = &_sbss; dst < &_ebss; ) *dst++ = 0; + main(); + for( ;; ) { __asm volatile ( "wfi" ); } +} + +void NMI_Handler( void ) __attribute__(( weak, alias( "Default_Handler" ) )); +void HardFault_Handler( void ) __attribute__(( weak, alias( "Default_Handler" ) )); + +__attribute__(( section( ".isr_vector" ), used )) +const void* const g_vectors[16] = +{ + &_estack, Reset_Handler, NMI_Handler, HardFault_Handler, + Default_Handler, Default_Handler, Default_Handler, 0, 0, 0, 0, + Default_Handler, Default_Handler, 0, Default_Handler, Default_Handler, +}; diff --git a/selector/fw/testbank/bank.ld b/selector/fw/testbank/bank.ld new file mode 100644 index 0000000..962fac5 --- /dev/null +++ b/selector/fw/testbank/bank.ld @@ -0,0 +1,17 @@ +/* A test image of a bank; BANK_ORIGIN (the vectors: the start of the bank + 0x200) comes from --defsym */ +MEMORY +{ + FLASH (rx) : ORIGIN = BANK_ORIGIN, LENGTH = 0x5FE00 + RAM (rwx) : ORIGIN = 0x20010000, LENGTH = 0x3C000 +} +_estack = 0x2004C000; +ENTRY(Reset_Handler) +SECTIONS +{ + .isr_vector : { KEEP(*(.isr_vector)) } > FLASH + .text : { *(.text .text.*) *(.rodata .rodata.*) . = ALIGN(4); } > FLASH + .result 0x20010000 (NOLOAD) : { KEEP(*(.result)) } > RAM + .cmd 0x20010100 (NOLOAD) : { KEEP(*(.cmd)) } > RAM + .bss 0x20010200 (NOLOAD) : { _sbss = .; *(.bss .bss.*) . = ALIGN(4); _ebss = .; } > RAM + /DISCARD/ : { *(.ARM.exidx*) *(.comment) } +} diff --git a/selector/fw/testbank/build.sh b/selector/fw/testbank/build.sh new file mode 100755 index 0000000..67c4565 --- /dev/null +++ b/selector/fw/testbank/build.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# Builds the test images of the banks A and B: build.sh +set -e +HERE=$(cd "$(dirname "$0")" && pwd) +OUT=${1:?output directory}; mkdir -p "$OUT" +build() { # + arm-none-eabi-gcc -mcpu=cortex-m7 -mthumb -Os -ffreestanding -nostdlib -nostartfiles -Wall -Wextra -std=gnu11 \ + -DBANK_INDEX=$2 -I"$HERE/../../include" -Wl,--defsym=BANK_ORIGIN=$3 -T"$HERE/bank.ld" -Wl,--gc-sections \ + -o "$OUT/$1.elf" "$HERE/start.c" "$HERE/main.c" "$HERE/libc.c" -lgcc + arm-none-eabi-objcopy -O binary "$OUT/$1.elf" "$OUT/$1.code.bin" + python3 -I "$HERE/../../tools/dmsel_mkimage.py" "$OUT/$1.code.bin" "$OUT/$1.bin" --version "$4" --capacity 0x60000 +} +build bank_a 0 0x08020200 "${2:-1}" +build bank_b 1 0x08080200 "${3:-2}" diff --git a/selector/fw/testbank/libc.c b/selector/fw/testbank/libc.c new file mode 100644 index 0000000..3295c6d --- /dev/null +++ b/selector/fw/testbank/libc.c @@ -0,0 +1,6 @@ +#include +#include +#define NP __attribute__(( optimize( "no-tree-loop-distribute-patterns" ) )) +NP void* memcpy( void* d, const void* s, size_t n ) { uint8_t* a = d; const uint8_t* b = s; while( n-- ) *a++ = *b++; return d; } +NP void* memset( void* d, int c, size_t n ) { uint8_t* a = d; while( n-- ) *a++ = (uint8_t)c; return d; } +NP int memcmp( const void* x, const void* y, size_t n ) { const uint8_t *a = x, *b = y; while( n-- ) { if( *a != *b ) return *a - *b; a++; b++; } return 0; } diff --git a/selector/fw/testbank/main.c b/selector/fw/testbank/main.c new file mode 100644 index 0000000..e3ab67c --- /dev/null +++ b/selector/fw/testbank/main.c @@ -0,0 +1,89 @@ +/** + * @file main.c + * @brief A test image of a bank: what a firmware would do with the table of the selector, reported into the RAM for + * openocd (g_result at 0x20010000), and driven by a command written to g_cmd (0x20010100) + * + * Built twice, for the bank A and the bank B (-DBANK_INDEX=...); it blinks the LD1 LED (PI1). + */ +#include +#include "dmsel_table.h" + +#define REG(a) (*(volatile uint32_t*)(a)) + +volatile uint32_t g_result[32] __attribute__(( section( ".result" ) )); +volatile uint32_t g_cmd __attribute__(( section( ".cmd" ) )); +static uint8_t g_scratch[0x8000] __attribute__(( aligned( 8 ) )); +static dmsel_t g_state __attribute__(( aligned( 8 ) )); +static const dmsel_table_t* g_table; + +static void report( void ) +{ + dmsel_bank_info_t info; + int b; + + g_result[2] = (uint32_t)g_table->bank_count( &g_state ); + g_result[3] = (uint32_t)g_table->running_bank( &g_state ); + for( b = 0; b < 2; b++ ) + { + info.struct_size = sizeof(info); + g_table->get_info( &g_state, b, &info ); + g_result[4 + b] = info.state; + g_result[6 + b] = info.version; + g_result[14 + b] = info.tries; + } +} + +int main( void ) +{ + uint32_t n = 0; + + REG( 0x40023830u ) |= (1u << 8); // GPIOI clock + REG( 0x40022000u ) = (REG( 0x40022000u ) & ~(3u << 2)) | (1u << 2); // PI1 output + + g_table = (const dmsel_table_t*)DMSEL_TABLE_ADDRESS; + g_result[1] = g_table->magic == DMSEL_TABLE_MAGIC ? 0x600D : 0xBAD; + if( g_result[1] != 0x600D ) + { + for( ;; ) {} + } + g_result[9] = (uint32_t)g_table->init( &g_state, BANK_INDEX, g_scratch ); + g_result[16] = g_table->version; + g_result[17] = g_table->log_address; + report(); + + for( ;; ) + { + uint32_t cmd = g_cmd; + + g_result[0] = ++n; + if( (n & 0x3FFFF) == 0 ) REG( 0x40022014u ) ^= (1u << 1); // ODR: blink + if( cmd != 0 ) + { + g_cmd = 0; + if( cmd == 1 ) // the other bank on trial, and a reset + { + int other = 1 - BANK_INDEX; + g_result[10] = (uint32_t)g_table->verify( &g_state, other ); + g_result[11] = (uint32_t)g_table->select( &g_state, other, 1 ); + REG( 0xE000ED0Cu ) = 0x05FA0004u; // SYSRESETREQ + } + else if( cmd == 2 ) // it runs properly + { + g_result[12] = (uint32_t)g_table->ready( &g_state ); + } + else if( cmd == 3 ) // the persistent data + { + uint8_t buf[16] = { 0 }; + g_result[18] = (uint32_t)g_table->persist_write( &g_state, 0x1234, "persist", 8 ); + g_result[19] = (uint32_t)g_table->persist_read( &g_state, 0x1234, buf, sizeof(buf) ); + g_result[20] = (uint32_t)buf[0] | ((uint32_t)buf[1] << 8) | ((uint32_t)buf[2] << 16) | ((uint32_t)buf[3] << 24); + } + else if( cmd == 4 ) // back to the other bank for good + { + g_result[13] = (uint32_t)g_table->select( &g_state, 1 - BANK_INDEX, 0 ); + REG( 0xE000ED0Cu ) = 0x05FA0004u; + } + report(); + } + } +} diff --git a/selector/fw/testbank/start.c b/selector/fw/testbank/start.c new file mode 100644 index 0000000..9f0366a --- /dev/null +++ b/selector/fw/testbank/start.c @@ -0,0 +1,13 @@ +#include +extern uint32_t _estack, _sbss, _ebss; +int main( void ); +void Default_Handler( void ) { for( ;; ) {} } +void Reset_Handler( void ) +{ + for( uint32_t* p = &_sbss; p < &_ebss; ) *p++ = 0; + main(); + for( ;; ) {} +} +__attribute__(( section( ".isr_vector" ), used )) +const void* const g_vectors[16] = { &_estack, Reset_Handler, Default_Handler, Default_Handler, Default_Handler, Default_Handler, + Default_Handler, 0, 0, 0, 0, Default_Handler, Default_Handler, 0, Default_Handler, Default_Handler }; diff --git a/selector/include/dmsel.h b/selector/include/dmsel.h index 2d727ba..ba9ef17 100644 --- a/selector/include/dmsel.h +++ b/selector/include/dmsel.h @@ -88,7 +88,7 @@ typedef struct uint32_t bank_count; dmsel_bank_t banks[DMSEL_MAX_BANKS]; uint32_t max_tries; //!< Starts of an image on trial that does not confirm, before it is dropped - uint8_t* scratch; //!< store_size bytes of RAM for the compaction of the store + uint32_t watchdog_ms; //!< Watchdog armed for an image on trial (0: none); the firmware has to feed it } dmsel_layout_t; /** The chip */ @@ -99,6 +99,7 @@ typedef struct int (*program)( void* user, uint32_t offset, const void* data, uint32_t size ); //!< Words, only on erased flash void (*jump)( void* user, uint32_t vector_address ); //!< Does not return void (*log)( void* user, const char* text ); //!< May be NULL + void (*watchdog_start)( void* user, uint32_t timeout_ms ); //!< May be NULL; cannot be stopped void* user; } dmsel_port_t; @@ -113,12 +114,16 @@ typedef struct uint8_t state[DMSEL_MAX_BANKS]; uint8_t tries[DMSEL_MAX_BANKS]; uint32_t store_end; //!< Offset in the store of the first free byte + uint8_t* scratch; //!< layout->store_size bytes of RAM (the compaction of the store, the records) } dmsel_t; uint32_t dmsel_crc32( uint32_t crc, const void* data, size_t size ); -/** Reads the state from the store (a lost state is rebuilt from the banks at the next dmsel_choose) */ -int dmsel_init( dmsel_t* s, const dmsel_layout_t* layout, const dmsel_port_t* port ); +/** + * Reads the state from the store (a lost state is rebuilt from the banks at the next dmsel_choose). + * @param scratch layout->store_size bytes that are used while a call writes to the store, and only then + */ +int dmsel_init( dmsel_t* s, const dmsel_layout_t* layout, const dmsel_port_t* port, uint8_t* scratch ); /** * Decides which bank to start, stores what has to be remembered (a try of an image on trial) and returns the bank. diff --git a/selector/include/dmsel_table.h b/selector/include/dmsel_table.h new file mode 100644 index 0000000..a33aabc --- /dev/null +++ b/selector/include/dmsel_table.h @@ -0,0 +1,50 @@ +/** + * @file dmsel_table.h + * @brief The table of functions that the selector leaves in its sector for the firmware + * + * The firmware cannot link to the selector, it finds the table at a fixed address (DMSEL_TABLE_ADDRESS) and calls the + * functions through it. The state (dmsel_t) is the firmware's own memory: the RAM of the selector is gone when the + * firmware starts. The functions that write the store need dmsel_table_t.scratch_size bytes of RAM, given at init. + * + * What the firmware reads at its start, before it uses the RAM of the selector (the first 64 kB of the DTCM): log_address. + */ +#ifndef DMSEL_TABLE_H +#define DMSEL_TABLE_H + +#include "dmsel.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define DMSEL_TABLE_ADDRESS 0x08000100u +#define DMSEL_TABLE_MAGIC 0x4C455344u //!< "DSEL" +#define DMSEL_TABLE_ABI 1 + +typedef struct +{ + uint32_t magic; //!< DMSEL_TABLE_MAGIC + uint32_t abi; //!< DMSEL_TABLE_ABI; a firmware built for another does not use the table + uint32_t table_size; //!< sizeof of this structure in the selector + uint32_t state_size; //!< sizeof(dmsel_t): the memory that the firmware gives to init + uint32_t scratch_size; //!< Of the RAM that init takes + uint32_t version; //!< Of the selector (a number) + uint32_t log_address; //!< The log of the selector (a dmlog buffer in the DTCM), valid until the firmware uses that RAM + uint32_t log_size; + /** @param running the bank that the firmware was linked for, i.e. runs from */ + int (*init)( dmsel_t* s, int running, uint8_t* scratch ); + int (*bank_count)( const dmsel_t* s ); + int (*running_bank)( const dmsel_t* s ); + int (*get_info)( dmsel_t* s, int bank, dmsel_bank_info_t* info ); + int (*verify)( dmsel_t* s, int bank ); + int (*select)( dmsel_t* s, int bank, int trial ); + int (*ready)( dmsel_t* s ); + int (*persist_write)( dmsel_t* s, uint32_t id, const void* data, size_t size ); + int (*persist_read)( dmsel_t* s, uint32_t id, void* buffer, size_t buffer_size ); +} dmsel_table_t; + +#ifdef __cplusplus +} +#endif + +#endif /* DMSEL_TABLE_H */ diff --git a/selector/port/host/dmsel_port_host.c b/selector/port/host/dmsel_port_host.c index 2bf0b01..78dc982 100644 --- a/selector/port/host/dmsel_port_host.c +++ b/selector/port/host/dmsel_port_host.c @@ -55,6 +55,11 @@ static void h_jump( void* user, uint32_t vector_address ) ((host_flash_t*)user)->jumped_to = vector_address; } +static void h_watchdog( void* user, uint32_t ms ) +{ + ((host_flash_t*)user)->watchdog_ms = ms; +} + static void h_log( void* user, const char* text ) { host_flash_t* f = (host_flash_t*)user; @@ -75,5 +80,6 @@ void host_flash_init( host_flash_t* f ) f->port.program = h_program; f->port.jump = h_jump; f->port.log = h_log; + f->port.watchdog_start = h_watchdog; f->port.user = f; } diff --git a/selector/port/host/dmsel_port_host.h b/selector/port/host/dmsel_port_host.h index 721b5b5..6d52c28 100644 --- a/selector/port/host/dmsel_port_host.h +++ b/selector/port/host/dmsel_port_host.h @@ -20,6 +20,7 @@ typedef struct int erases; int program_errors; //!< Programs refused because the flash was not erased uint32_t jumped_to; //!< Address of the last jump (0: none) + uint32_t watchdog_ms; //!< Timeout the watchdog was started with (0: not started) char log[4096]; dmsel_port_t port; } host_flash_t; diff --git a/selector/src/dmsel.c b/selector/src/dmsel.c index 975393f..bbad497 100644 --- a/selector/src/dmsel.c +++ b/selector/src/dmsel.c @@ -194,7 +194,7 @@ static void load_state( dmsel_t* s ) static int write_record( dmsel_t* s, uint32_t off, uint16_t type, const void* a, uint32_t na, const void* b, uint32_t nb ) { - uint8_t* buf = s->layout->scratch; + uint8_t* buf = s->scratch; uint32_t len = na + nb, padded = pad4( len ); rec_t r; @@ -230,7 +230,7 @@ static void state_payload( const dmsel_t* s, uint8_t* out ) /** Erases the store and writes the state and the latest value of every item again */ static int compact( dmsel_t* s ) { - uint8_t* out = s->layout->scratch; + uint8_t* out = s->scratch; uint8_t payload[STATE_SIZE]; rec_t r, hdr; const uint8_t* p; @@ -379,17 +379,18 @@ static void rebuild( dmsel_t* s ) // Public // --------------------------------------------------------------------------------------------------------------------- -int dmsel_init( dmsel_t* s, const dmsel_layout_t* layout, const dmsel_port_t* port ) +int dmsel_init( dmsel_t* s, const dmsel_layout_t* layout, const dmsel_port_t* port, uint8_t* scratch ) { unsigned i; - if( s == NULL || layout == NULL || port == NULL || layout->bank_count > DMSEL_MAX_BANKS || layout->scratch == NULL ) + if( s == NULL || layout == NULL || port == NULL || layout->bank_count > DMSEL_MAX_BANKS || scratch == NULL ) { return DMSEL_ERR_PARAM; } memset( s, 0, sizeof(*s) ); s->layout = layout; s->port = port; + s->scratch = scratch; s->selected = s->previous = s->running = NO_BANK; for( i = 0; i < DMSEL_MAX_BANKS; i++ ) { @@ -476,6 +477,11 @@ void dmsel_boot( dmsel_t* s ) if( bank >= 0 && check_image( s, (unsigned)bank, &h ) == DMSEL_OK ) { + if( s->state[bank] == DMSEL_BANK_TRIAL && s->layout->watchdog_ms != 0 && s->port->watchdog_start != NULL ) + { + // An image that hangs before it confirms is reset by the watchdog, and the next start counts a try + s->port->watchdog_start( s->port->user, s->layout->watchdog_ms ); + } s->port->jump( s->port->user, s->layout->flash_address + s->layout->banks[bank].offset + h.entry_offset ); } diff --git a/selector/tests/mkimage.cmake b/selector/tests/mkimage.cmake new file mode 100644 index 0000000..81ef1be --- /dev/null +++ b/selector/tests/mkimage.cmake @@ -0,0 +1,9 @@ +# ctest: make an image with the tool and let the test of the core verify it +execute_process(COMMAND ${PY} ${TOOL} ${IN} ${OUT} --version 7 --capacity 0x60000 RESULT_VARIABLE r) +if(NOT r EQUAL 0) + message(FATAL_ERROR "dmsel_mkimage.py failed") +endif() +execute_process(COMMAND ${CHECK} ${OUT} RESULT_VARIABLE r) +if(NOT r EQUAL 0) + message(FATAL_ERROR "the core does not accept the image of the tool") +endif() diff --git a/selector/tests/test_selector.c b/selector/tests/test_selector.c index 1115db9..2e3d3bb 100644 --- a/selector/tests/test_selector.c +++ b/selector/tests/test_selector.c @@ -40,15 +40,16 @@ static void setup( void ) g_layout.banks[1].offset = BANK_B; g_layout.banks[1].capacity = BANK_SIZE; g_layout.max_tries = 3; - g_layout.scratch = g_scratch; + g_layout.watchdog_ms = 0; } /** The selector starts again after a reset: the flash stays, the RAM does not */ static void reset( void ) { g_f.jumped_to = 0; + g_f.watchdog_ms = 0; g_f.fail_after = -1; - CHECK_EQ( dmsel_init( &g_s, &g_layout, &g_f.port ), DMSEL_OK ); + CHECK_EQ( dmsel_init( &g_s, &g_layout, &g_f.port, g_scratch ), DMSEL_OK ); } /** An image: header, vectors at 512, a body; written straight into the flash (as the update tool would) */ @@ -163,6 +164,19 @@ static void test_trial_not_confirmed_goes_back( void ) CHECK_EQ( g_s.state[1], DMSEL_BANK_INVALID ); } +static void test_watchdog_only_for_a_trial( void ) +{ + setup(); put_image( 0, 1, 0x4000, 1 ); put_image( 1, 2, 0x4000, 2 ); + g_layout.watchdog_ms = 5000; reset(); + dmsel_boot( &g_s ); // B, newest, not on trial + CHECK_EQ( g_f.watchdog_ms, 0 ); + CHECK_EQ( dmsel_select( &g_s, 0, 1 ), DMSEL_OK ); // A on trial + reset(); + dmsel_boot( &g_s ); + CHECK_EQ( g_f.jumped_to, vector_of( 0 ) ); + CHECK_EQ( g_f.watchdog_ms, 5000 ); +} + static void test_permanent_select( void ) { setup(); put_image( 0, 1, 0x4000, 1 ); put_image( 1, 2, 0x4000, 2 ); reset(); @@ -377,14 +391,41 @@ static void test_get_info_extensible( void ) CHECK_EQ( info.capacity, BANK_SIZE ); } -int main( void ) +/** An image made by tools/dmsel_mkimage.py (the file is the argument) is a good image for the core */ +static int check_tool_image( const char* path ) +{ + FILE* f = fopen( path, "rb" ); + size_t n; + + setup(); reset(); + if( f == NULL ) + { + printf( "cannot open %s\n", path ); + return 1; + } + n = fread( g_f.flash + BANK_A, 1, BANK_SIZE, f ); + fclose( f ); + CHECK( n > 512 ); + CHECK_EQ( dmsel_verify( &g_s, 0 ), DMSEL_OK ); + g_f.flash[BANK_A + n - 1] ^= 1; + CHECK_EQ( dmsel_verify( &g_s, 0 ), DMSEL_ERR_BAD ); + printf( "image of the tool: %d checks, %d failures\n", g_checks, g_failures ); + return g_failures ? 1 : 0; +} + +int main( int argc, char** argv ) { + if( argc > 1 ) + { + return check_tool_image( argv[1] ); + } RUN( test_crc ); RUN( test_nothing_to_start ); RUN( test_single_image_starts ); RUN( test_newest_image_without_a_state ); RUN( test_trial_confirmed ); RUN( test_trial_not_confirmed_goes_back ); + RUN( test_watchdog_only_for_a_trial ); RUN( test_permanent_select ); RUN( test_damaged_image_falls_back ); RUN( test_header_checks ); diff --git a/selector/tools/dmsel_mkimage.py b/selector/tools/dmsel_mkimage.py new file mode 100644 index 0000000..829e16b --- /dev/null +++ b/selector/tools/dmsel_mkimage.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Makes a bank image: the header of dmsel.h in front of the code. + +The code (objcopy -O binary of an ELF linked at + 0x200) starts with the vector table. The image is +512 bytes of the header area (the header, then 0xFF) and the code, padded to a whole word. + + dmsel_mkimage.py code.bin image.bin --version 3 [--capacity 0x60000] +""" +import argparse, struct, sys, zlib + +MAGIC = 0x4B424D44 +HEADER = struct.Struct(' capacity: + raise SystemExit(f'the image ({size} bytes) does not fit the bank ({capacity})') + body = bytearray(b'\xff' * AREA) + code + image_crc = zlib.crc32(bytes(body[HEADER.size:size])) & 0xFFFFFFFF + head = struct.pack(' Date: Thu, 29 Oct 2026 06:06:18 +0000 Subject: [PATCH 03/14] CI: the selector job takes the submodules (the firmware of the selector uses dmlog and the config header of dmod) Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1d4776f..87b4c5f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -153,6 +153,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + submodules: recursive - name: Build and run the tests of the selector run: | From b2463ada6223f5fee918192e44d9f6d5df49eb83 Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Sat, 31 Oct 2026 06:04:13 +0000 Subject: [PATCH 04/14] The image linked for a bank, started by the selector: DMBOOT_BANK=A|B -DDMBOOT_BANK=A links dmod-boot for the bank A of the board (DMBOOT_BANK_A_ORIGIN/SIZE in board.cmake): the ROM of the linker script is the bank without the first 512 bytes (--defsym DMBOOT_ROM_ORIGIN/LENGTH, used by the stm32f746xg script), the vector table is aligned to 512 bytes, and the startup code sets VTOR to it - so the image also runs when the selector (or nothing) starts it from an address other than 0. After the link the image gets the header of the selector (selector/tools/dmsel_mkimage.py; the version is the time of the build by default). install-firmware programs the selector (built by the target 'selector' from selector/fw), erases its store (S1: the state is rebuilt from the banks) and writes the image; install-bank writes the bank only. The file systems of the banks (S2, S3) are not touched. Checked on the STM32F746G-DISCO: the image of the bank A (348 kB) is started by the selector, VTOR is 0x08020200, the services and the SD card run, the state is in S1 (and holds across a reset). Co-Authored-By: Claude Sonnet 5.5 --- CMakeLists.txt | 16 ++++++++ configs/board/stm32f746g-disco/board.cmake | 11 ++++++ configs/mcu/stm32f7/stm32f746xg/mcu.ld | 3 +- linker/common.ld | 1 + scripts/targets.cmake | 44 ++++++++++++++++++++++ selector/tools/dmsel_mkimage.py | 8 ++-- src/arch/armv7/cortex-m4/startup.s | 12 ++++++ src/arch/armv7/cortex-m7/startup.s | 12 ++++++ 8 files changed, 103 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1fb5cde..4a90e32 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -276,8 +276,24 @@ target_compile_definitions(${MODULE_NAME}.elf DMBOOT_MAIN_HEAP_TAGS_STRING="${DMBOOT_MAIN_HEAP_TAGS}" ) +# The image can be linked for a bank of the flash, to be started by the selector (selector/): the ROM is the bank without the +# 512 bytes of the header; the whole flash otherwise +set(DMBOOT_BANK "" CACHE STRING "Bank (A or B) that the image is linked for, started by the selector; empty: the whole flash, no selector") +set(DMBOOT_ROM_ORIGIN 0x08000000) +set(DMBOOT_ROM_LENGTH 0x100000) +if(DMBOOT_BANK) + if(NOT DEFINED DMBOOT_BANK_${DMBOOT_BANK}_ORIGIN) + message(FATAL_ERROR "DMBOOT_BANK=${DMBOOT_BANK}: the board does not define the bank (DMBOOT_BANK_${DMBOOT_BANK}_ORIGIN)") + endif() + math(EXPR DMBOOT_ROM_ORIGIN "${DMBOOT_BANK_${DMBOOT_BANK}_ORIGIN} + 512" OUTPUT_FORMAT HEXADECIMAL) + math(EXPR DMBOOT_ROM_LENGTH "${DMBOOT_BANK_${DMBOOT_BANK}_SIZE} - 512" OUTPUT_FORMAT HEXADECIMAL) + message(STATUS "Linked for the bank ${DMBOOT_BANK}: vectors at ${DMBOOT_ROM_ORIGIN}, ${DMBOOT_ROM_LENGTH} bytes") +endif() + target_link_options(${MODULE_NAME}.elf PRIVATE + -Wl,--defsym=DMBOOT_ROM_ORIGIN=${DMBOOT_ROM_ORIGIN} + -Wl,--defsym=DMBOOT_ROM_LENGTH=${DMBOOT_ROM_LENGTH} -L${DMOD_DIR}/scripts -T${CMAKE_CURRENT_SOURCE_DIR}/configs/arch/${DMBOOT_ARCH}/arch.ld -T${CMAKE_CURRENT_SOURCE_DIR}/configs/mcu/${DMBOOT_MCU_SERIES}/${DMBOOT_MCU_NAME}/mcu.ld diff --git a/configs/board/stm32f746g-disco/board.cmake b/configs/board/stm32f746g-disco/board.cmake index 38adc62..91447ca 100644 --- a/configs/board/stm32f746g-disco/board.cmake +++ b/configs/board/stm32f746g-disco/board.cmake @@ -59,5 +59,16 @@ set(DMBOOT_DMSECRETS_PATH "/ext/secrets.vault" CACHE STRING "Path of the default # The display driver is on /ext: the splash logo (60 kB of raw pixels) is too, and shows as soon as /ext is mounted. set(DMBOOT_SPLASH_LOGO_PATH "/ext/views/splash_logo.dmvir" CACHE STRING "Path of the splash logo (.dmvir) on the target") +# Dual bank layout of the internal flash (selector/): S0 the selector, S1 its store (the state of the banks, the persistent +# items), S2 and S3 the file systems of the banks, A = S4+S5, B = S6+S7 (384 kB each; the header of the image takes the first +# 512 bytes of a bank, the vectors follow). Linking for a bank: -DDMBOOT_BANK=A (or B). +set(DMBOOT_BANK_A_ORIGIN 0x08020000) +set(DMBOOT_BANK_A_SIZE 0x00060000) +set(DMBOOT_BANK_B_ORIGIN 0x08080000) +set(DMBOOT_BANK_B_SIZE 0x00060000) +set(DMBOOT_SELECTOR_ORIGIN 0x08000000) +set(DMBOOT_SELECTOR_STORE 0x08008000) +set(DMBOOT_SELECTOR_STORE_SECTOR 1) + # QUADSPI bank 1 + N25Q128A, DMA2 stream 7/channel 3, IRQ 92 (RM0385). set(DMBOOT_RENODE_QSPI_MODEL "${CMAKE_SOURCE_DIR}/configs/renode/STM32F7_QUADSPI.cs") diff --git a/configs/mcu/stm32f7/stm32f746xg/mcu.ld b/configs/mcu/stm32f7/stm32f746xg/mcu.ld index afda0a6..d170083 100644 --- a/configs/mcu/stm32f7/stm32f746xg/mcu.ld +++ b/configs/mcu/stm32f7/stm32f746xg/mcu.ld @@ -1,6 +1,7 @@ MEMORY { - rom (rx) : ORIGIN = 0x08000000, LENGTH = 1024K + /* The whole flash, or a bank of it (DMBOOT_BANK, see CMakeLists.txt): the origin and the length come from --defsym */ + rom (rx) : ORIGIN = DMBOOT_ROM_ORIGIN, LENGTH = DMBOOT_ROM_LENGTH ram (rwx) : ORIGIN = 0x20010000, LENGTH = 256K ext (rwx) : ORIGIN = 0x60000000, LENGTH = 2047M dma (rwx) : ORIGIN = 0x20000000, LENGTH = 64k diff --git a/linker/common.ld b/linker/common.ld index d308c3e..7f0c6a3 100644 --- a/linker/common.ld +++ b/linker/common.ld @@ -31,6 +31,7 @@ SECTIONS PROVIDE(__text_start__ = __text_start__); . = ALIGN(4); + . = ALIGN(512); /* VTOR wants the table aligned to its size rounded up to a power of 2 (114 entries: 512) */ KEEP(*(.vectors)) /* Startup code */ . = ALIGN(4); diff --git a/scripts/targets.cmake b/scripts/targets.cmake index 4fa685e..e5df914 100644 --- a/scripts/targets.cmake +++ b/scripts/targets.cmake @@ -146,12 +146,56 @@ else() set(CONNECT_COMMENT "Connecting to ${TARGET} with OpenOCD...") endif() +# Linked for a bank: the image gets the header of the selector, and the board is programmed with the selector as well +if(DMBOOT_BANK AND NOT DMBOOT_EMULATION) + set(DMBOOT_BANK_IMAGE ${CMAKE_BINARY_DIR}/${MODULE_NAME}_bank_${DMBOOT_BANK}.bin) + set(DMBOOT_SELECTOR_BUILD_DIR ${CMAKE_BINARY_DIR}/selector) + set(DMBOOT_SELECTOR_BIN ${DMBOOT_SELECTOR_BUILD_DIR}/dmsel.bin) + set(DMBOOT_BANK_VERSION "auto" CACHE STRING "Version in the header of the image: a number, or auto - the time of the build (the bigger the newer)") + + add_custom_command(TARGET ${MODULE_NAME}.elf POST_BUILD + COMMAND ${CMAKE_OBJCOPY} -O binary ${MODULE_NAME}.elf ${MODULE_NAME}.code.bin + COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/selector/tools/dmsel_mkimage.py + ${MODULE_NAME}.code.bin ${DMBOOT_BANK_IMAGE} --version ${DMBOOT_BANK_VERSION} + --capacity ${DMBOOT_BANK_${DMBOOT_BANK}_SIZE} + COMMENT "Image of the bank ${DMBOOT_BANK}: ${DMBOOT_BANK_IMAGE}" + VERBATIM) + + add_custom_target(selector + COMMAND ${CMAKE_COMMAND} -S ${CMAKE_CURRENT_SOURCE_DIR}/selector/fw -B ${DMBOOT_SELECTOR_BUILD_DIR} + COMMAND ${CMAKE_COMMAND} --build ${DMBOOT_SELECTOR_BUILD_DIR} + BYPRODUCTS ${DMBOOT_SELECTOR_BIN} + COMMENT "Building the selector...") + + # The whole flash: the selector, the store erased (the state is rebuilt from the banks: the newest good image + # starts), the image of the bank. The file systems of the banks (S2, S3) are not touched. + set(INSTALL_FIRMWARE_COMMAND ${OPENOCD} -f ${OPENOCD_INTERFACE} -f ${OPENOCD_TARGET} + -c "init" -c "reset halt" + -c "flash write_image erase ${DMBOOT_SELECTOR_BIN} ${DMBOOT_SELECTOR_ORIGIN} bin" + -c "flash erase_sector 0 ${DMBOOT_SELECTOR_STORE_SECTOR} ${DMBOOT_SELECTOR_STORE_SECTOR}" + -c "flash write_image erase ${DMBOOT_BANK_IMAGE} ${DMBOOT_BANK_${DMBOOT_BANK}_ORIGIN} bin" + -c "verify_image ${DMBOOT_BANK_IMAGE} ${DMBOOT_BANK_${DMBOOT_BANK}_ORIGIN} bin" + -c "reset run" -c "shutdown") + set(INSTALL_FIRMWARE_COMMENT "Installing the selector and the bank ${DMBOOT_BANK} on ${TARGET}...") + add_custom_target(install-bank + COMMAND ${OPENOCD} -f ${OPENOCD_INTERFACE} -f ${OPENOCD_TARGET} + -c "init" -c "reset halt" + -c "flash write_image erase ${DMBOOT_BANK_IMAGE} ${DMBOOT_BANK_${DMBOOT_BANK}_ORIGIN} bin" + -c "verify_image ${DMBOOT_BANK_IMAGE} ${DMBOOT_BANK_${DMBOOT_BANK}_ORIGIN} bin" + -c "reset run" -c "shutdown" + DEPENDS ${MODULE_NAME}.elf + COMMENT "Writing the bank ${DMBOOT_BANK} only (the selector and its store stay)...") +endif() + # Define install-firmware target add_custom_target(install-firmware COMMAND ${INSTALL_FIRMWARE_COMMAND} DEPENDS ${MODULE_NAME}.elf COMMENT ${INSTALL_FIRMWARE_COMMENT} ) +if(DMBOOT_BANK AND NOT DMBOOT_EMULATION) + add_dependencies(install-firmware selector) +endif() # Define connect target add_custom_target(connect diff --git a/selector/tools/dmsel_mkimage.py b/selector/tools/dmsel_mkimage.py index 829e16b..55129f0 100644 --- a/selector/tools/dmsel_mkimage.py +++ b/selector/tools/dmsel_mkimage.py @@ -29,9 +29,11 @@ def make(code, version, capacity=None): if __name__ == '__main__': ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) ap.add_argument('code'); ap.add_argument('image') - ap.add_argument('--version', type=lambda x: int(x, 0), required=True) + ap.add_argument('--version', required=True, help='a number, or auto: the time of the build (the bigger the newer)') ap.add_argument('--capacity', type=lambda x: int(x, 0)) a = ap.parse_args() - out = make(open(a.code, 'rb').read(), a.version, a.capacity) + import time + version = int(time.time()) if a.version == 'auto' else int(a.version, 0) + out = make(open(a.code, 'rb').read(), version, a.capacity) open(a.image, 'wb').write(out) - print(f'{a.image}: {len(out)} bytes, version {a.version}') + print(f'{a.image}: {len(out)} bytes, version {version}') diff --git a/src/arch/armv7/cortex-m4/startup.s b/src/arch/armv7/cortex-m4/startup.s index 944743f..2c15df1 100644 --- a/src/arch/armv7/cortex-m4/startup.s +++ b/src/arch/armv7/cortex-m4/startup.s @@ -69,6 +69,18 @@ Initialize the process stack pointer ldr r0, =__stack_end__ msr PSP, r0 +/*============================================================================== +The vector table is where this image is linked, which is not address 0 when a +boot loader (the selector) starts it from a bank of the flash: it sets VTOR +itself, and so does the image, so that it also runs when it is started the +other way (the vectors are aligned to 512 bytes in the linker script) +==============================================================================*/ + ldr r0, =0xE000ED08 // VTOR + ldr r1, =vectors + str r1, [r0] + dsb + isb + /*============================================================================== Thread mode uses process stack (PSP) and is privileged ==============================================================================*/ diff --git a/src/arch/armv7/cortex-m7/startup.s b/src/arch/armv7/cortex-m7/startup.s index afcee0c..12e0aa8 100644 --- a/src/arch/armv7/cortex-m7/startup.s +++ b/src/arch/armv7/cortex-m7/startup.s @@ -69,6 +69,18 @@ Initialize the process stack pointer ldr r0, =__stack_end__ msr PSP, r0 +/*============================================================================== +The vector table is where this image is linked, which is not address 0 when a +boot loader (the selector) starts it from a bank of the flash: it sets VTOR +itself, and so does the image, so that it also runs when it is started the +other way (the vectors are aligned to 512 bytes in the linker script) +==============================================================================*/ + ldr r0, =0xE000ED08 // VTOR + ldr r1, =vectors + str r1, [r0] + dsb + isb + /*============================================================================== Thread mode uses process stack (PSP) and is privileged ==============================================================================*/ From d02f9f78494f88e02518b56fcf48add052d4c7da Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Sat, 31 Oct 2026 06:36:36 +0000 Subject: [PATCH 05/14] stm32f746g-disco: link for the bank A by default Co-Authored-By: Claude Sonnet 5.5 --- configs/board/stm32f746g-disco/board.cmake | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/configs/board/stm32f746g-disco/board.cmake b/configs/board/stm32f746g-disco/board.cmake index 91447ca..5a287d4 100644 --- a/configs/board/stm32f746g-disco/board.cmake +++ b/configs/board/stm32f746g-disco/board.cmake @@ -61,7 +61,12 @@ set(DMBOOT_SPLASH_LOGO_PATH "/ext/views/splash_logo.dmvir" CACHE STRING "Path of # Dual bank layout of the internal flash (selector/): S0 the selector, S1 its store (the state of the banks, the persistent # items), S2 and S3 the file systems of the banks, A = S4+S5, B = S6+S7 (384 kB each; the header of the image takes the first -# 512 bytes of a bank, the vectors follow). Linking for a bank: -DDMBOOT_BANK=A (or B). +# 512 bytes of a bank, the vectors follow). The firmware is linked for the bank A by default (and installed with the +# selector); -DDMBOOT_BANK=B links it for the other one, -DDMBOOT_BANK= for the whole flash without the selector. The +# emulation (Renode) loads the plain image. +if(NOT DMBOOT_EMULATION) + set(DMBOOT_BANK "A" CACHE STRING "Bank (A or B) that the image is linked for, started by the selector; empty: the whole flash, no selector") +endif() set(DMBOOT_BANK_A_ORIGIN 0x08020000) set(DMBOOT_BANK_A_SIZE 0x00060000) set(DMBOOT_BANK_B_ORIGIN 0x08080000) From ef9f8f8c6ad9b34ab6bfd9534e25f74de29b57f9 Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Sat, 31 Oct 2026 08:47:55 +0000 Subject: [PATCH 06/14] Firmware SAL (Dmod_Firmware*, Dmod_*Persistency) over the table of the selector lib/dmod is bumped to the develop with the bank SAL (dmod#343). Co-Authored-By: Claude Sonnet 5.5 --- CMakeLists.txt | 10 ++ lib/dmod | 2 +- src/firmware/dmboot_firmware.c | 172 +++++++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 src/firmware/dmboot_firmware.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a90e32..dd47376 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -288,6 +288,16 @@ if(DMBOOT_BANK) math(EXPR DMBOOT_ROM_ORIGIN "${DMBOOT_BANK_${DMBOOT_BANK}_ORIGIN} + 512" OUTPUT_FORMAT HEXADECIMAL) math(EXPR DMBOOT_ROM_LENGTH "${DMBOOT_BANK_${DMBOOT_BANK}_SIZE} - 512" OUTPUT_FORMAT HEXADECIMAL) message(STATUS "Linked for the bank ${DMBOOT_BANK}: vectors at ${DMBOOT_ROM_ORIGIN}, ${DMBOOT_ROM_LENGTH} bytes") + + # Dmod_Firmware*, Dmod_*Persistency over the table of the selector (an object library: the weak defaults of the SAL + # would otherwise be enough for the linker and the archive member would not be taken) + string(FIND "ABCD" "${DMBOOT_BANK}" DMBOOT_BANK_INDEX) + add_library(dmboot_firmware OBJECT src/firmware/dmboot_firmware.c) + dmod_set_current_allocator(dmboot_firmware) + target_compile_definitions(dmboot_firmware PRIVATE DMBOOT_BANK_INDEX=${DMBOOT_BANK_INDEX}) + target_include_directories(dmboot_firmware PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/selector/include) + target_link_libraries(dmboot_firmware PRIVATE dmod) + target_link_libraries(${MODULE_NAME}.elf PRIVATE dmboot_firmware) endif() target_link_options(${MODULE_NAME}.elf diff --git a/lib/dmod b/lib/dmod index 8264cbb..1d2bb69 160000 --- a/lib/dmod +++ b/lib/dmod @@ -1 +1 @@ -Subproject commit 8264cbbc7e7c3bd0cf17bda5491f9efc3beefa5d +Subproject commit 1d2bb69c1ef88376f1760ae566eee768354f25ad diff --git a/src/firmware/dmboot_firmware.c b/src/firmware/dmboot_firmware.c new file mode 100644 index 0000000..8b6c4aa --- /dev/null +++ b/src/firmware/dmboot_firmware.c @@ -0,0 +1,172 @@ +/** + * @brief The firmware interface of the SAL (Dmod_Firmware*, Dmod_*Persistency) over the table of the selector. + * + * The image that is linked for a bank (DMBOOT_BANK) is started by the selector, which leaves a table of functions in + * its sector (selector/include/dmsel_table.h). The state of the banks is read from the store of the selector at the + * first call; the memory for it is ours (the RAM of the selector is gone), and so is the scratch that the calls + * writing to the store need - taken from the heap for the time of the call only. + */ +#include "dmod.h" +#include "dmsel_table.h" +#include +#include + +#ifndef DMBOOT_BANK_INDEX +# error "DMBOOT_BANK_INDEX (the number of the bank that the image is linked for) is required" +#endif + +static dmsel_t g_state; +static uint8_t g_dummy_scratch; +static bool g_initialized; +static const dmsel_table_t* g_table; + +static int to_errno(int result) +{ + switch(result) + { + case DMSEL_ERR_PARAM: return -EINVAL; + case DMSEL_ERR_NOENT: return -ENOENT; + case DMSEL_ERR_NOSPACE: return -ENOSPC; + case DMSEL_ERR_IO: return -EIO; + case DMSEL_ERR_EMPTY: return -ENODATA; + case DMSEL_ERR_BAD: return -EBADMSG; + default: return result < 0 ? -EIO : result; + } +} + +/* The table of the selector, with the state read; NULL when there is no selector (the image was not started by it) */ +static const dmsel_table_t* get_table(void) +{ + if(g_initialized) + { + return g_table; + } + g_initialized = true; + + const dmsel_table_t* table = (const dmsel_table_t*)DMSEL_TABLE_ADDRESS; + if(table->magic != DMSEL_TABLE_MAGIC || table->abi != DMSEL_TABLE_ABI || table->state_size != sizeof(dmsel_t)) + { + return NULL; + } + /* The scratch is given for the calls that write; the state only reads at init */ + if(table->init(&g_state, DMBOOT_BANK_INDEX, &g_dummy_scratch) != 0) + { + return NULL; + } + g_table = table; + return g_table; +} + +/* Runs a call that writes to the store: it needs the scratch (the size of the store) for its time */ +#define WITH_SCRATCH(call) \ + do { \ + uint8_t* scratch = Dmod_Malloc(table->scratch_size); \ + if(scratch == NULL) \ + { \ + return -ENOMEM; \ + } \ + g_state.scratch = scratch; \ + result = (call); \ + g_state.scratch = &g_dummy_scratch; \ + Dmod_Free(scratch); \ + } while(0) + +int Dmod_FirmwareReady(void) +{ + const dmsel_table_t* table = get_table(); + int result; + if(table == NULL) + { + return -ENOSYS; + } + WITH_SCRATCH(table->ready(&g_state)); + return to_errno(result); +} + +int Dmod_FirmwareGetBankCount(void) +{ + const dmsel_table_t* table = get_table(); + return table != NULL ? table->bank_count(&g_state) : -ENOSYS; +} + +int Dmod_FirmwareGetRunningBank(void) +{ + const dmsel_table_t* table = get_table(); + if(table == NULL) + { + return -ENOSYS; + } + int bank = table->running_bank(&g_state); + return bank >= 0 ? bank : DMOD_FIRMWARE_BANK_NONE; +} + +int Dmod_FirmwareGetBankInfo(int Bank, Dmod_FirmwareBankInfo_t* Info) +{ + const dmsel_table_t* table = get_table(); + if(table == NULL) + { + return -ENOSYS; + } + if(Info == NULL) + { + return -EINVAL; + } + dmsel_bank_info_t info; + int result = table->get_info(&g_state, Bank, &info); + if(result != 0) + { + return to_errno(result); + } + /* The layouts are the same, the caller's StructSize is what it is */ + uint32_t size = Info->StructSize < sizeof(info) ? Info->StructSize : (uint32_t)sizeof(info); + uint32_t struct_size = Info->StructSize; + memcpy(Info, &info, size); + Info->StructSize = struct_size; + return 0; +} + +int Dmod_FirmwareVerifyBank(int Bank) +{ + const dmsel_table_t* table = get_table(); + int result; + if(table == NULL) + { + return -ENOSYS; + } + WITH_SCRATCH(table->verify(&g_state, Bank)); + return to_errno(result); +} + +int Dmod_FirmwareSelectBank(int Bank, bool Trial) +{ + const dmsel_table_t* table = get_table(); + int result; + if(table == NULL) + { + return -ENOSYS; + } + WITH_SCRATCH(table->select(&g_state, Bank, Trial ? 1 : 0)); + return to_errno(result); +} + +int Dmod_WritePersistency(uint32_t Id, const void* Data, size_t Size) +{ + const dmsel_table_t* table = get_table(); + int result; + if(table == NULL) + { + return -ENOSYS; + } + WITH_SCRATCH(table->persist_write(&g_state, Id, Data, Size)); + return to_errno(result); +} + +int Dmod_ReadPersistency(uint32_t Id, void* Buffer, size_t BufferSize) +{ + const dmsel_table_t* table = get_table(); + if(table == NULL) + { + return -ENOSYS; + } + return to_errno(table->persist_read(&g_state, Id, Buffer, BufferSize)); +} From 2acbcc026b745319ce09977baf09108f21627898 Mon Sep 17 00:00:00 2001 From: Patryk Kubiak Date: Sun, 1 Nov 2026 08:30:45 +0100 Subject: [PATCH 07/14] install-all: /ext first, then a restart, then the files of the SD card The card is mounted by modules on /ext, so uploading to it before /ext was installed failed with "no card mounted". Co-Authored-By: Claude Opus 5.5 --- docs/external-flash.md | 2 +- scripts/targets.cmake | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/external-flash.md b/docs/external-flash.md index b8fdbf9..a5bb3e5 100644 --- a/docs/external-flash.md +++ b/docs/external-flash.md @@ -57,7 +57,7 @@ cmake --build build --target upload-ext ``` sends `build/ext` over the console (YMODEM) to `/ext`, so the modules land in **`/ext/bin`**, on the module search path. -`install-all` also downloads and sends these files, after installing the firmware and uploading the SD card files. +`install-all` also downloads and sends these files, after installing the firmware and before the SD card files (the SD card driver is on `/ext`), then restarts the device to start them. It uses `DMBOOT_UPLOAD_WAIT` while waiting for the console and requires the external flash to be formatted and mounted; it never runs `format-ext` automatically. diff --git a/scripts/targets.cmake b/scripts/targets.cmake index e5df914..76d2916 100644 --- a/scripts/targets.cmake +++ b/scripts/targets.cmake @@ -253,11 +253,12 @@ if(Python3_Interpreter_FOUND) USES_TERMINAL COMMENT "Uploading files to ${DMBOOT_UPLOAD_WHERE} over ${DMBOOT_UPLOAD_PORT} (YMODEM)..." ) + # The SD card driver and its file system are on /ext when the board has one (docs/external-flash.md): there the files + # of /ext go first, the device restarts to start them, and only then the card is looked for (POST_BUILD steps, in order). add_custom_target(install-all - COMMAND ${DMBOOT_UPLOAD_COMMAND} --wait ${DMBOOT_UPLOAD_WAIT} ${DMBOOT_UPLOAD_SOURCES} + COMMAND ${CMAKE_COMMAND} -E echo "Firmware installed, uploading the files..." DEPENDS install-firmware download_sdcard_modules USES_TERMINAL - COMMENT "Uploading files to ${DMBOOT_UPLOAD_WHERE} once ${TARGET} has started..." ) # The external flash (DMBOOT_EXT_DEVICE, set by the board): the console runs the commands, YMODEM sends build/ext. @@ -288,9 +289,15 @@ if(Python3_Interpreter_FOUND) add_dependencies(install-all download_ext_modules) add_custom_command(TARGET install-all POST_BUILD COMMAND ${DMBOOT_EXT_UPLOAD_COMMAND} --wait ${DMBOOT_UPLOAD_WAIT} - COMMENT "Uploading build/ext to ${DMBOOT_EXT_MOUNT} over ${DMBOOT_UPLOAD_PORT} (YMODEM)..." + COMMAND ${DMBOOT_EXT_CONSOLE_COMMAND} --wait 5 "--run=-reboot" + COMMENT "Uploading build/ext to ${DMBOOT_EXT_MOUNT} over ${DMBOOT_UPLOAD_PORT} (YMODEM), then restarting to start its modules..." ) endif() + + add_custom_command(TARGET install-all POST_BUILD + COMMAND ${DMBOOT_UPLOAD_COMMAND} --wait ${DMBOOT_UPLOAD_WAIT} ${DMBOOT_UPLOAD_SOURCES} + COMMENT "Uploading files to ${DMBOOT_UPLOAD_WHERE} once ${TARGET} has started..." + ) else() message(STATUS "Python 3 not found - the upload-files, install-all, format-ext and upload-ext targets are not available") endif() From 830c38c474128a34908d766d8bc54c2d7729e39a Mon Sep 17 00:00:00 2001 From: Patryk Kubiak Date: Sun, 1 Nov 2026 09:44:32 +0100 Subject: [PATCH 08/14] Release at -O2, format-ext on a new chip - CMake's Release is -O3: the firmware of stm32f746g-disco was 392 587 bytes of a 392 704-byte bank - 117 bytes left, any module change overflowed. At -O2 it is 369 kB, 23 kB less. - format-ext passed "-umount /ext" as a separate argument: argparse took it for an option. --run=-umount ..., like --run=-reboot. Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 6 ++++++ scripts/targets.cmake | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index dd47376..19d812d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -103,6 +103,12 @@ project(dmboot DESCRIPTION "DMOD Bootloader" LANGUAGES C CXX ASM) +# Release is -O2, not CMake's -O3: 23 kB less of the firmware (stm32f746g-disco: 369 kB instead of +# 392 kB) - at -O3 it no longer fits into a bank of the selector (384 kB). The modules are built +# by their own projects and do not change. +string(REPLACE "-O3" "-O2" CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}") +string(REPLACE "-O3" "-O2" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}") + # ====================================================================== # For VS Code # ====================================================================== diff --git a/scripts/targets.cmake b/scripts/targets.cmake index 76d2916..1fe331b 100644 --- a/scripts/targets.cmake +++ b/scripts/targets.cmake @@ -270,7 +270,7 @@ if(Python3_Interpreter_FOUND) --port ${DMBOOT_UPLOAD_PORT} --baud ${DMBOOT_UPLOAD_BAUD} --block ${DMBOOT_UPLOAD_BLOCK}) add_custom_target(format-ext COMMAND ${DMBOOT_EXT_CONSOLE_COMMAND} --wait 5 - --run "-umount ${DMBOOT_EXT_MOUNT}" + "--run=-umount ${DMBOOT_EXT_MOUNT}" --run "${DMBOOT_EXT_MKFS} ${DMBOOT_EXT_DEVICE}" --run "mount -t ${DMBOOT_EXT_FS} -o device=${DMBOOT_EXT_DEVICE} ${DMBOOT_EXT_MOUNT}" --ensure-dir ${DMBOOT_EXT_MOUNT} From f9034d8d06619b06613bfba1bb0ce9961e9fe376 Mon Sep 17 00:00:00 2001 From: Patryk Kubiak Date: Sun, 1 Nov 2026 09:44:32 +0100 Subject: [PATCH 09/14] stm32f746g-disco: the audio through the speaker, audioctl on /ext dmaudio 0.3's speaker/dmaudio@.ini: the board's speaker (JP3) plays, dmwm8994 0.5's speaker=mono mixes both channels into it. audioctl output headphone|speaker switches it while it plays. Co-Authored-By: Claude Opus 5.5 --- configs/board/stm32f746g-disco/ext.dmd | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/configs/board/stm32f746g-disco/ext.dmd b/configs/board/stm32f746g-disco/ext.dmd index d45636b..dd07fcb 100644 --- a/configs/board/stm32f746g-disco/ext.dmd +++ b/configs/board/stm32f746g-disco/ext.dmd @@ -123,10 +123,14 @@ dmwm8994 driver=board/stm32f746g-disco/codec-with-touch.ini # # The audio service: dmaudio.rules starts a dmaudio@ for every audio # device dmdevfs reports - here /dev/wm8994 -, mixing what the applications -# play (libdmaudio) into it. +# play (libdmaudio) into it. Through the speaker (JP3; speaker=mono in the +# codec's configuration mixes both channels into it) - service=dmaudio@.ini +# for the headphone jack. audioctl output headphone|speaker switches it while +# it plays. # -dmaudio service=dmaudio@.ini +dmaudio service=speaker/dmaudio@.ini dmaudio rules=dmaudio.rules +audioctl # # The display service: dmview.rules starts a dmview@ for the LCD # (/dev/dmlcdtft0), which shows what the applications claim for it - with From 620b7b6e94a7450971f59f0296046cfbd7c2c443 Mon Sep 17 00:00:00 2001 From: Patryk Kubiak Date: Sun, 1 Nov 2026 15:00:58 +0100 Subject: [PATCH 10/14] stm32f746g-disco: an FTP server on /ext (ftpd, port 21, anonymous) ftpd of dmftp (0.3.1: the current dmod API, transfers past 4 KB) on TCP port 21, the whole file system, anonymous like telnet - remote access to the board. ftpd@public.ini is the instance of the ftpd@.ini template that libsystemd starts at boot. Co-Authored-By: Claude Opus 5.5 --- configs/board/stm32f746g-disco/ext.dmd | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/configs/board/stm32f746g-disco/ext.dmd b/configs/board/stm32f746g-disco/ext.dmd index dd07fcb..1f371c4 100644 --- a/configs/board/stm32f746g-disco/ext.dmd +++ b/configs/board/stm32f746g-disco/ext.dmd @@ -85,6 +85,14 @@ dmntp service=ntp.ini # telnetd driver=telnetd.ini # +# FTP: ftpd (dmftp: libftp + dmtcp) on TCP port 21, the whole file system (/), +# anonymous - like telnet, which has no login either. ftpd@.ini is the +# template, ftpd@public.ini its instance; libsystemd starts it at boot. Another +# instance (a port, a root, --user / --pass): an ftpd@.ini of your own. +# +ftpd service=ftpd@.ini +ftpd service=ftpd@public.ini +# # SD card: on-board microSD slot (CN3) on SDMMC1. dmdevfs creates the host # node /dev/dmsdio0, the dmdevmon service (common flash.dmd) monitors the # card detect pin and announces /dev/dmsdio0/0 when a card is inserted. From 899d4cbf77dc42b94960b17cec01ea480bc37847 Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Sun, 1 Nov 2026 14:46:15 +0000 Subject: [PATCH 11/14] ymodem_send: the last lines of the console when a command does not finish Co-Authored-By: Claude Sonnet 5.5 --- scripts/ymodem_send.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/ymodem_send.py b/scripts/ymodem_send.py index dc639ad..41829e2 100755 --- a/scripts/ymodem_send.py +++ b/scripts/ymodem_send.py @@ -269,7 +269,9 @@ def run_command(link, command, timeout): if PROMPT.search(clean(seen)): seen += link.drain(0.3) # (the prompt is back: the rest of the output; a log that never stops is no reason to wait) return clean(seen) - raise Failure(f'"{command}" did not finish in {timeout:.0f} s') + # (what the device said is all there is to go by when it hangs: the last lines of it) + tail = '\n'.join(clean(seen).splitlines()[-30:]) + raise Failure(f'"{command}" did not finish in {timeout:.0f} s' + (f'; the console said:\n{tail}' if tail else '; the console said nothing')) def wait_shell(link, wait): From 13876be61583ffa00fe475a1e1fcbfc037329c7f Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Sun, 1 Nov 2026 15:01:19 +0000 Subject: [PATCH 12/14] run_renode_tests: the log of the firmware when /ext cannot be filled Co-Authored-By: Claude Sonnet 5.5 --- scripts/run_renode_tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/run_renode_tests.sh b/scripts/run_renode_tests.sh index 1462118..697a10d 100755 --- a/scripts/run_renode_tests.sh +++ b/scripts/run_renode_tests.sh @@ -209,7 +209,7 @@ if [ -n "$EXT_DEVICE" ]; then --run "$EXT_MOUNT_CMD" --ensure-dir "$EXT_MOUNT" \ && "${YMODEM[@]}" --timeout 120 --dest "$EXT_MOUNT" --card none "$BUILD_DIR/ext" \ && "${YMODEM[@]}" --timeout 120 --run "umount $EXT_MOUNT" --run "$EXT_MOUNT_CMD" \ - || { echo "✗ could not fill the external flash"; exit 1; } + || { echo "✗ could not fill the external flash"; echo "--- monitor.log (tail: the log of the firmware) ---"; tail -n 120 "$MONITOR_LOG"; exit 1; } # (the mount loads /ext/cfg: units, rules, drivers - they start now) echo "✓ build/ext is on $EXT_MOUNT" else From 9833fed94fb97fc1682cf37edb029b7fdd6fc097 Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Sun, 1 Nov 2026 15:43:12 +0000 Subject: [PATCH 13/14] selector: the code specific to the platform in selector/configs, like configs/ of dmboot arch/armv7 (startup, jump), mcu/stm32f7/stm32f746xg (flash controller, watchdog, memory map), board/stm32f746g-disco (the layout of the flash). The selector's main, the core and the common linker script are the same for every platform; the board is chosen with DMSEL_BOARD (dmboot passes its BOARD). Co-Authored-By: Claude Sonnet 5.5 --- scripts/targets.cmake | 2 +- selector/configs/arch/armv7/arch.ld | 3 + selector/configs/arch/armv7/cfg.cmake | 3 + .../configs/arch/armv7/cortex-m7/cfg.cmake | 2 + selector/configs/arch/armv7/jump.c | 26 ++++++++ selector/{fw => configs/arch/armv7}/startup.c | 2 +- .../configs/board/stm32f746g-disco/board.c | 26 ++++++++ .../board/stm32f746g-disco/board.cmake | 7 ++ .../board/stm32f746g-disco/dmsel_board.h | 8 +++ .../mcu/stm32f7/stm32f746xg/dmsel_mcu.h | 7 ++ .../mcu/stm32f7/stm32f746xg/flash.c} | 65 +++++-------------- .../configs/mcu/stm32f7/stm32f746xg/mcu.cmake | 4 ++ .../configs/mcu/stm32f7/stm32f746xg/mcu.ld | 6 ++ .../mcu/stm32f7/stm32f746xg/watchdog.c | 24 +++++++ selector/fw/CMakeLists.txt | 43 +++++++++--- selector/fw/dmsel_f7.h | 15 ----- selector/fw/dmsel_f7.ld | 20 ------ selector/fw/main.c | 45 +++++-------- selector/include/dmsel_platform.h | 38 +++++++++++ selector/linker/common.ld | 15 +++++ 20 files changed, 237 insertions(+), 124 deletions(-) create mode 100644 selector/configs/arch/armv7/arch.ld create mode 100644 selector/configs/arch/armv7/cfg.cmake create mode 100644 selector/configs/arch/armv7/cortex-m7/cfg.cmake create mode 100644 selector/configs/arch/armv7/jump.c rename selector/{fw => configs/arch/armv7}/startup.c (88%) create mode 100644 selector/configs/board/stm32f746g-disco/board.c create mode 100644 selector/configs/board/stm32f746g-disco/board.cmake create mode 100644 selector/configs/board/stm32f746g-disco/dmsel_board.h create mode 100644 selector/configs/mcu/stm32f7/stm32f746xg/dmsel_mcu.h rename selector/{fw/dmsel_port_f7.c => configs/mcu/stm32f7/stm32f746xg/flash.c} (58%) create mode 100644 selector/configs/mcu/stm32f7/stm32f746xg/mcu.cmake create mode 100644 selector/configs/mcu/stm32f7/stm32f746xg/mcu.ld create mode 100644 selector/configs/mcu/stm32f7/stm32f746xg/watchdog.c delete mode 100644 selector/fw/dmsel_f7.h delete mode 100644 selector/fw/dmsel_f7.ld create mode 100644 selector/include/dmsel_platform.h create mode 100644 selector/linker/common.ld diff --git a/scripts/targets.cmake b/scripts/targets.cmake index 1fe331b..be9f51e 100644 --- a/scripts/targets.cmake +++ b/scripts/targets.cmake @@ -162,7 +162,7 @@ if(DMBOOT_BANK AND NOT DMBOOT_EMULATION) VERBATIM) add_custom_target(selector - COMMAND ${CMAKE_COMMAND} -S ${CMAKE_CURRENT_SOURCE_DIR}/selector/fw -B ${DMBOOT_SELECTOR_BUILD_DIR} + COMMAND ${CMAKE_COMMAND} -S ${CMAKE_CURRENT_SOURCE_DIR}/selector/fw -B ${DMBOOT_SELECTOR_BUILD_DIR} -DDMSEL_BOARD=${BOARD} COMMAND ${CMAKE_COMMAND} --build ${DMBOOT_SELECTOR_BUILD_DIR} BYPRODUCTS ${DMBOOT_SELECTOR_BIN} COMMENT "Building the selector...") diff --git a/selector/configs/arch/armv7/arch.ld b/selector/configs/arch/armv7/arch.ld new file mode 100644 index 0000000..5089027 --- /dev/null +++ b/selector/configs/arch/armv7/arch.ld @@ -0,0 +1,3 @@ +/* ARMv7-M: the reset handler is the entry, the stack grows down from the top of its place at the bottom of the RAM */ +ENTRY(Reset_Handler) +_estack = ORIGIN(RAM) + DMSEL_STACK_SIZE; diff --git a/selector/configs/arch/armv7/cfg.cmake b/selector/configs/arch/armv7/cfg.cmake new file mode 100644 index 0000000..ff3224f --- /dev/null +++ b/selector/configs/arch/armv7/cfg.cmake @@ -0,0 +1,3 @@ +# ARMv7-M common to the cores: the reset handler and the jump (the core's flags: /cfg.cmake) +set(DMSEL_ARCH_SOURCES ${DMSEL_CONFIGS_DIR}/arch/armv7/startup.c ${DMSEL_CONFIGS_DIR}/arch/armv7/jump.c) +set(DMSEL_ARCH_LINKER_SCRIPT ${DMSEL_CONFIGS_DIR}/arch/armv7/arch.ld) diff --git a/selector/configs/arch/armv7/cortex-m7/cfg.cmake b/selector/configs/arch/armv7/cortex-m7/cfg.cmake new file mode 100644 index 0000000..623aaed --- /dev/null +++ b/selector/configs/arch/armv7/cortex-m7/cfg.cmake @@ -0,0 +1,2 @@ +# Cortex-M7 +set(DMSEL_CPU_FLAGS -mcpu=cortex-m7 -mthumb) diff --git a/selector/configs/arch/armv7/jump.c b/selector/configs/arch/armv7/jump.c new file mode 100644 index 0000000..2af5785 --- /dev/null +++ b/selector/configs/arch/armv7/jump.c @@ -0,0 +1,26 @@ +/** + * @file jump.c + * @brief ARMv7-M: the jump into an image - the core quiet, the vector table of the image, its stack and its reset handler + */ +#include +#include "dmsel_platform.h" + +#define REG(a) (*(volatile uint32_t*)(a)) + +void dmsel_arch_jump( void* user, uint32_t vector_address ) +{ + const uint32_t* vectors = (const uint32_t*)vector_address; + uint32_t sp = vectors[0], reset = vectors[1], i; + + (void)user; + __asm volatile ( "cpsid i" ::: "memory" ); + REG( 0xE000E010u ) = 0; // SysTick off + for( i = 0; i < 8; i++ ) + { + REG( 0xE000E180u + 4 * i ) = 0xFFFFFFFFu; // ICER + REG( 0xE000E280u + 4 * i ) = 0xFFFFFFFFu; // ICPR + } + REG( 0xE000ED08u ) = vector_address; // VTOR + __asm volatile ( "dsb\n isb\n msr msp, %0\n cpsie i\n bx %1" :: "r"(sp), "r"(reset) : "memory" ); + for( ;; ) {} +} diff --git a/selector/fw/startup.c b/selector/configs/arch/armv7/startup.c similarity index 88% rename from selector/fw/startup.c rename to selector/configs/arch/armv7/startup.c index 85ac786..c6e41ea 100644 --- a/selector/fw/startup.c +++ b/selector/configs/arch/armv7/startup.c @@ -1,6 +1,6 @@ /** * @file startup.c - * @brief Vectors and the reset handler of the selector: the RAM is the DTCM, nothing else runs here + * @brief ARMv7-M: the vectors and the reset handler of the selector (the RAM and its stack: mcu.ld, arch.ld) */ #include diff --git a/selector/configs/board/stm32f746g-disco/board.c b/selector/configs/board/stm32f746g-disco/board.c new file mode 100644 index 0000000..9551814 --- /dev/null +++ b/selector/configs/board/stm32f746g-disco/board.c @@ -0,0 +1,26 @@ +/** + * @file board.c + * @brief STM32F746G-DISCO: the layout of the flash + * + * Flash: S0 selector | S1 store (state of the banks, persistent items) | S2, S3 file systems of the banks | + * S4+S5 bank A (384 kB) | S6+S7 bank B (384 kB of the 512 kB) + * (the same banks as configs/board/stm32f746g-disco/board.cmake of dmboot: DMBOOT_BANK_*) + */ +#include "dmsel_platform.h" +#include "dmsel_mcu.h" +#include "dmsel_board.h" + +#ifndef DMSEL_WATCHDOG_MS +#define DMSEL_WATCHDOG_MS 0 // The firmware has to feed the watchdog from the moment it starts: 0 until it does +#endif + +const dmsel_layout_t dmsel_board_layout = +{ + .flash_address = DMSEL_MCU_FLASH_ADDRESS, + .store_offset = DMSEL_BOARD_STORE_OFFSET, + .store_size = DMSEL_BOARD_STORE_SIZE, + .bank_count = 2, + .banks = { { 0x20000u, 0x60000u }, { 0x80000u, 0x60000u } }, + .max_tries = 3, + .watchdog_ms = DMSEL_WATCHDOG_MS, +}; diff --git a/selector/configs/board/stm32f746g-disco/board.cmake b/selector/configs/board/stm32f746g-disco/board.cmake new file mode 100644 index 0000000..ea05600 --- /dev/null +++ b/selector/configs/board/stm32f746g-disco/board.cmake @@ -0,0 +1,7 @@ +# STM32F746G-DISCO: a Cortex-M7 of the STM32F7 family +set(DMSEL_ARCH_FAMILY armv7) +set(DMSEL_ARCH_CORE cortex-m7) +set(DMSEL_MCU_FAMILY stm32f7) +set(DMSEL_MCU stm32f746xg) +set(DMSEL_BOARD_SOURCES ${DMSEL_BOARD_DIR}/board.c) +set(DMSEL_BOARD_INCLUDE_DIR ${DMSEL_BOARD_DIR}) diff --git a/selector/configs/board/stm32f746g-disco/dmsel_board.h b/selector/configs/board/stm32f746g-disco/dmsel_board.h new file mode 100644 index 0000000..fd88c70 --- /dev/null +++ b/selector/configs/board/stm32f746g-disco/dmsel_board.h @@ -0,0 +1,8 @@ +#ifndef DMSEL_BOARD_H +#define DMSEL_BOARD_H + +/** The sector of the store (S1) - the only one that the selector erases and programs */ +#define DMSEL_BOARD_STORE_OFFSET 0x08000u +#define DMSEL_BOARD_STORE_SIZE 0x08000u + +#endif diff --git a/selector/configs/mcu/stm32f7/stm32f746xg/dmsel_mcu.h b/selector/configs/mcu/stm32f7/stm32f746xg/dmsel_mcu.h new file mode 100644 index 0000000..840aa0c --- /dev/null +++ b/selector/configs/mcu/stm32f7/stm32f746xg/dmsel_mcu.h @@ -0,0 +1,7 @@ +#ifndef DMSEL_MCU_H +#define DMSEL_MCU_H + +/** Where the flash is in the memory of the chip (offset 0 of the layout) */ +#define DMSEL_MCU_FLASH_ADDRESS 0x08000000u + +#endif diff --git a/selector/fw/dmsel_port_f7.c b/selector/configs/mcu/stm32f7/stm32f746xg/flash.c similarity index 58% rename from selector/fw/dmsel_port_f7.c rename to selector/configs/mcu/stm32f7/stm32f746xg/flash.c index db72e19..2c8f9a3 100644 --- a/selector/fw/dmsel_port_f7.c +++ b/selector/configs/mcu/stm32f7/stm32f746xg/flash.c @@ -1,13 +1,14 @@ /** - * @file dmsel_port_f7.c - * @brief The selector's port for the STM32F7 (RM0385): the flash, the jump, the watchdog + * @file flash.c + * @brief The selector's port for the STM32F746 (RM0385): the flash controller * - * Only the store sector is ever erased or programmed from here (a guard): a mistake of the core cannot touch the selector - * or the banks. The flash is single bank: the CPU is stopped while a sector is erased (up to a second for 256 kB). + * Only the store of the layout (user) is ever erased or programmed from here (a guard): a mistake of the core cannot touch + * the selector or the banks. The flash is single bank: the CPU is stopped while a sector is erased (up to a second for 256 kB). */ #include #include "dmsel.h" -#include "dmsel_f7.h" +#include "dmsel_platform.h" +#include "dmsel_mcu.h" #define REG(a) (*(volatile uint32_t*)(a)) #define FLASH_BASE 0x40023C00u @@ -62,7 +63,7 @@ static void flash_refresh( uint32_t offset, uint32_t size ) FLASH_ACR = acr; if( REG( 0xE000ED14u ) & (1u << 16) ) // CCR.DC: the data cache of the core is on (the firmware turned it on) { - for( a = (0x08000000u + offset) & ~31u; a < 0x08000000u + offset + size; a += 32 ) + for( a = (DMSEL_MCU_FLASH_ADDRESS + offset) & ~31u; a < DMSEL_MCU_FLASH_ADDRESS + offset + size; a += 32 ) { REG( 0xE000EF5Cu ) = a; // DCIMVAC } @@ -71,18 +72,19 @@ static void flash_refresh( uint32_t offset, uint32_t size ) __asm volatile ( "isb" ::: "memory" ); } -static int allowed( uint32_t offset, uint32_t size ) +static int allowed( void* user, uint32_t offset, uint32_t size ) { - return offset >= DMSEL_F7_STORE_OFFSET && offset + size <= DMSEL_F7_STORE_OFFSET + DMSEL_F7_STORE_SIZE && offset + size >= offset; + const dmsel_layout_t* l = (const dmsel_layout_t*)user; + + return offset >= l->store_offset && offset + size <= l->store_offset + l->store_size && offset + size >= offset; } -int dmsel_f7_erase( void* user, uint32_t offset, uint32_t size ) +int dmsel_mcu_erase( void* user, uint32_t offset, uint32_t size ) { unsigned n; int result = 0; - (void)user; - if( !allowed( offset, size ) ) + if( !allowed( user, offset, size ) ) { return -1; } @@ -103,14 +105,13 @@ int dmsel_f7_erase( void* user, uint32_t offset, uint32_t size ) return result; } -int dmsel_f7_program( void* user, uint32_t offset, const void* data, uint32_t size ) +int dmsel_mcu_program( void* user, uint32_t offset, const void* data, uint32_t size ) { const uint8_t* d = (const uint8_t*)data; uint32_t i; int result = 0; - (void)user; - if( (offset & 3u) != 0 || (size & 3u) != 0 || !allowed( offset, size ) ) + if( (offset & 3u) != 0 || (size & 3u) != 0 || !allowed( user, offset, size ) ) { return -1; } @@ -121,7 +122,7 @@ int dmsel_f7_program( void* user, uint32_t offset, const void* data, uint32_t si uint32_t w = (uint32_t)d[i] | ((uint32_t)d[i + 1] << 8) | ((uint32_t)d[i + 2] << 16) | ((uint32_t)d[i + 3] << 24); result = flash_wait(); FLASH_CR |= CR_PG; - *(volatile uint32_t*)(0x08000000u + offset + i) = w; + *(volatile uint32_t*)(DMSEL_MCU_FLASH_ADDRESS + offset + i) = w; __asm volatile ( "dsb" ::: "memory" ); result |= flash_wait(); FLASH_CR &= ~CR_PG; @@ -130,37 +131,3 @@ int dmsel_f7_program( void* user, uint32_t offset, const void* data, uint32_t si flash_refresh( offset, size ); return result; } - -void dmsel_f7_jump( void* user, uint32_t vector_address ) -{ - const uint32_t* vectors = (const uint32_t*)vector_address; - uint32_t sp = vectors[0], reset = vectors[1], i; - - (void)user; - __asm volatile ( "cpsid i" ::: "memory" ); - REG( 0xE000E010u ) = 0; // SysTick off - for( i = 0; i < 8; i++ ) - { - REG( 0xE000E180u + 4 * i ) = 0xFFFFFFFFu; // ICER - REG( 0xE000E280u + 4 * i ) = 0xFFFFFFFFu; // ICPR - } - REG( 0xE000ED08u ) = vector_address; // VTOR - __asm volatile ( "dsb\n isb\n msr msp, %0\n cpsie i\n bx %1" :: "r"(sp), "r"(reset) : "memory" ); - for( ;; ) {} -} - -void dmsel_f7_watchdog_start( void* user, uint32_t timeout_ms ) -{ - uint32_t reload = timeout_ms / 8u; // LSI 32 kHz / 256 = 125 Hz: 8 ms per tick - - (void)user; - if( reload > 0xFFFu ) reload = 0xFFFu; - if( reload == 0 ) reload = 1; - REG( 0xE0042008u ) |= (1u << 12); // DBGMCU: the watchdog stops while the core is halted by the debugger - REG( 0x40003000u ) = 0xCCCCu; // IWDG: start (cannot be stopped) - REG( 0x40003000u ) = 0x5555u; - REG( 0x40003004u ) = 6; // /256 - REG( 0x40003008u ) = reload; - while( REG( 0x4000300Cu ) != 0 ) {} - REG( 0x40003000u ) = 0xAAAAu; -} diff --git a/selector/configs/mcu/stm32f7/stm32f746xg/mcu.cmake b/selector/configs/mcu/stm32f7/stm32f746xg/mcu.cmake new file mode 100644 index 0000000..4bc9bc6 --- /dev/null +++ b/selector/configs/mcu/stm32f7/stm32f746xg/mcu.cmake @@ -0,0 +1,4 @@ +# STM32F746xG: the flash controller, the watchdog, the memory map +set(DMSEL_MCU_SOURCES ${DMSEL_MCU_DIR}/flash.c ${DMSEL_MCU_DIR}/watchdog.c) +set(DMSEL_MCU_LINKER_SCRIPT ${DMSEL_MCU_DIR}/mcu.ld) +set(DMSEL_MCU_INCLUDE_DIR ${DMSEL_MCU_DIR}) diff --git a/selector/configs/mcu/stm32f7/stm32f746xg/mcu.ld b/selector/configs/mcu/stm32f7/stm32f746xg/mcu.ld new file mode 100644 index 0000000..48d49d6 --- /dev/null +++ b/selector/configs/mcu/stm32f7/stm32f746xg/mcu.ld @@ -0,0 +1,6 @@ +/* STM32F746xG: the first sector of the flash (32 kB) is the selector, the DTCM is its RAM */ +MEMORY +{ + FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K + RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K +} diff --git a/selector/configs/mcu/stm32f7/stm32f746xg/watchdog.c b/selector/configs/mcu/stm32f7/stm32f746xg/watchdog.c new file mode 100644 index 0000000..7d9f97c --- /dev/null +++ b/selector/configs/mcu/stm32f7/stm32f746xg/watchdog.c @@ -0,0 +1,24 @@ +/** + * @file watchdog.c + * @brief STM32F7: the independent watchdog (IWDG) for an image on trial + */ +#include +#include "dmsel_platform.h" + +#define REG(a) (*(volatile uint32_t*)(a)) + +void dmsel_mcu_watchdog_start( void* user, uint32_t timeout_ms ) +{ + uint32_t reload = timeout_ms / 8u; // LSI 32 kHz / 256 = 125 Hz: 8 ms per tick + + (void)user; + if( reload > 0xFFFu ) reload = 0xFFFu; + if( reload == 0 ) reload = 1; + REG( 0xE0042008u ) |= (1u << 12); // DBGMCU: the watchdog stops while the core is halted by the debugger + REG( 0x40003000u ) = 0xCCCCu; // IWDG: start (cannot be stopped) + REG( 0x40003000u ) = 0x5555u; + REG( 0x40003004u ) = 6; // /256 + REG( 0x40003008u ) = reload; + while( REG( 0x4000300Cu ) != 0 ) {} + REG( 0x40003000u ) = 0xAAAAu; +} diff --git a/selector/fw/CMakeLists.txt b/selector/fw/CMakeLists.txt index 09febd7..c800440 100644 --- a/selector/fw/CMakeLists.txt +++ b/selector/fw/CMakeLists.txt @@ -1,6 +1,9 @@ -# The selector for the STM32F7 (the code of the first sector of the flash) -# cmake -S selector/fw -B build-selector-fw && cmake --build build-selector-fw -# Result: dmsel.elf and dmsel.bin (at most 32 kB: the linker script checks) +# The selector (the code of the first sector of the flash) +# cmake -S selector/fw -B build-selector-fw [-DDMSEL_BOARD=stm32f746g-disco] && cmake --build build-selector-fw +# Result: dmsel.elf and dmsel.bin (at most the first sector: the linker script checks) +# +# What depends on the platform is in selector/configs, the same tree as configs/ of dmboot: the board +# (board//board.cmake) names its core (arch//) and its chip (mcu//). cmake_minimum_required(VERSION 3.16) set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_PROCESSOR arm) @@ -11,6 +14,18 @@ project(dmsel_fw C) set(DMSEL_WATCHDOG_MS 0 CACHE STRING "Watchdog (ms) armed for an image on trial; the firmware has to feed it, 0: none") set(DMBOOT_DIR "${CMAKE_CURRENT_LIST_DIR}/../..") +set(DMSEL_BOARD "stm32f746g-disco" CACHE STRING "Board the selector is built for (selector/configs/board)") +set(DMSEL_CONFIGS_DIR "${CMAKE_CURRENT_LIST_DIR}/../configs") +set(DMSEL_BOARD_DIR "${DMSEL_CONFIGS_DIR}/board/${DMSEL_BOARD}") +if(NOT EXISTS "${DMSEL_BOARD_DIR}/board.cmake") + message(FATAL_ERROR "The selector has no board '${DMSEL_BOARD}' (selector/configs/board)") +endif() +include("${DMSEL_BOARD_DIR}/board.cmake") +set(DMSEL_MCU_DIR "${DMSEL_CONFIGS_DIR}/mcu/${DMSEL_MCU_FAMILY}/${DMSEL_MCU}") +include("${DMSEL_CONFIGS_DIR}/arch/${DMSEL_ARCH_FAMILY}/cfg.cmake") +include("${DMSEL_CONFIGS_DIR}/arch/${DMSEL_ARCH_FAMILY}/${DMSEL_ARCH_CORE}/cfg.cmake") +include("${DMSEL_MCU_DIR}/mcu.cmake") + # dmlog.c wants the configuration header of dmod: a bare-metal one, as the firmware has it set(DMOD_VERSION_STRING "1.0") foreach(opt STDLIB GETENV ENVIRON STDIO DIRENT ASSERT PTHREAD MMAN ALIGNED_ALLOC ALIGNED_MALLOC_MOCK REALLOC TERMIOS TIME DMCOMP_ENCODER) @@ -30,7 +45,7 @@ set(DMOD_SYSTEM_VERSION_MAJOR 0) set(DMOD_SYSTEM_VERSION_MINOR 1) set(DMOD_REPO_DIR "/") set(DMOD_REPO_PATHS "/") -set(DMOD_CPU_NAME "stm32f746xg") +set(DMOD_CPU_NAME "${DMSEL_MCU}") set(DMOD_ARRAY_SEP ":") set(DMOD_PATH_SEP "/") set(DMOD_BUILTIN_COMPRESSION_API OFF) @@ -40,17 +55,25 @@ set(dmod_VERSION_MINOR 0) configure_file("${DMBOOT_DIR}/lib/dmod/dmod-config.h.in" "${CMAKE_BINARY_DIR}/gen/dmod-config.h") add_executable(dmsel.elf - startup.c main.c dmsel_port_f7.c dmod_stubs.c libc_min.c + main.c dmod_stubs.c libc_min.c + ${DMSEL_ARCH_SOURCES} ${DMSEL_MCU_SOURCES} ${DMSEL_BOARD_SOURCES} ../src/dmsel.c ${DMBOOT_DIR}/lib/dmlog/src/dmlog.c) set_source_files_properties(${DMBOOT_DIR}/lib/dmlog/src/dmlog.c PROPERTIES COMPILE_OPTIONS "-w") -target_include_directories(dmsel.elf PRIVATE . ../include ${DMBOOT_DIR}/lib/dmlog/include ${DMBOOT_DIR}/lib/dmod/inc ${CMAKE_BINARY_DIR}/gen) +target_include_directories(dmsel.elf PRIVATE ../include ${DMSEL_MCU_INCLUDE_DIR} ${DMSEL_BOARD_INCLUDE_DIR} + ${DMBOOT_DIR}/lib/dmlog/include ${DMBOOT_DIR}/lib/dmod/inc ${CMAKE_BINARY_DIR}/gen) target_compile_definitions(dmsel.elf PRIVATE DMOD_EXTERNAL_REGISTRATION DMOD_CURRENT_ALLOCATOR="dmsel" DMSEL_WATCHDOG_MS=${DMSEL_WATCHDOG_MS}) -target_compile_options(dmsel.elf PRIVATE -mcpu=cortex-m7 -mthumb -Os -ffunction-sections -fdata-sections -ffreestanding +target_compile_options(dmsel.elf PRIVATE ${DMSEL_CPU_FLAGS} -Os -ffunction-sections -fdata-sections -ffreestanding -Wall -Wextra -std=gnu11) -target_link_options(dmsel.elf PRIVATE -mcpu=cortex-m7 -mthumb -nostdlib -nostartfiles -Wl,--gc-sections - -T${CMAKE_CURRENT_LIST_DIR}/dmsel_f7.ld -Wl,-Map=${CMAKE_BINARY_DIR}/dmsel.map -lgcc) -set_target_properties(dmsel.elf PROPERTIES LINK_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/dmsel_f7.ld) +# (the order matters: the memory map first, the entry and the stack of the core use it, the sections last) +set(DMSEL_LINKER_SCRIPTS ${DMSEL_MCU_LINKER_SCRIPT} ${DMSEL_ARCH_LINKER_SCRIPT} ${CMAKE_CURRENT_LIST_DIR}/../linker/common.ld) +set(DMSEL_LINK_SCRIPT_OPTIONS) +foreach(script IN LISTS DMSEL_LINKER_SCRIPTS) + list(APPEND DMSEL_LINK_SCRIPT_OPTIONS -T${script}) +endforeach() +target_link_options(dmsel.elf PRIVATE ${DMSEL_CPU_FLAGS} -nostdlib -nostartfiles -Wl,--gc-sections + ${DMSEL_LINK_SCRIPT_OPTIONS} -Wl,-Map=${CMAKE_BINARY_DIR}/dmsel.map -lgcc) +set_target_properties(dmsel.elf PROPERTIES LINK_DEPENDS "${DMSEL_LINKER_SCRIPTS}") add_custom_command(TARGET dmsel.elf POST_BUILD COMMAND arm-none-eabi-objcopy -O binary dmsel.elf dmsel.bin COMMAND arm-none-eabi-size dmsel.elf) diff --git a/selector/fw/dmsel_f7.h b/selector/fw/dmsel_f7.h deleted file mode 100644 index 0969cea..0000000 --- a/selector/fw/dmsel_f7.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef DMSEL_F7_H -#define DMSEL_F7_H - -#include - -/** The sector of the store (S1) - the only one that this port erases and programs */ -#define DMSEL_F7_STORE_OFFSET 0x08000u -#define DMSEL_F7_STORE_SIZE 0x08000u - -int dmsel_f7_erase( void* user, uint32_t offset, uint32_t size ); -int dmsel_f7_program( void* user, uint32_t offset, const void* data, uint32_t size ); -void dmsel_f7_jump( void* user, uint32_t vector_address ); -void dmsel_f7_watchdog_start( void* user, uint32_t timeout_ms ); - -#endif diff --git a/selector/fw/dmsel_f7.ld b/selector/fw/dmsel_f7.ld deleted file mode 100644 index 8cb3390..0000000 --- a/selector/fw/dmsel_f7.ld +++ /dev/null @@ -1,20 +0,0 @@ -/* The selector: the first sector of the flash (32 kB), the DTCM for the RAM */ -MEMORY -{ - FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K - DTCM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K -} -ENTRY(Reset_Handler) -_estack = ORIGIN(DTCM) + 0x1000; /* 4 kB of the stack at the bottom of the DTCM, the data above it */ - -SECTIONS -{ - .isr_vector : { KEEP(*(.isr_vector)) } > FLASH - .dmsel_table ORIGIN(FLASH) + 0x100 : { KEEP(*(.dmsel_table)) } > FLASH /* DMSEL_TABLE_ADDRESS */ - .text : { *(.text .text.*) *(.rodata .rodata.*) . = ALIGN(4); } > FLASH - _sidata = LOADADDR(.data); - .data ORIGIN(DTCM) + 0x1000 : { _sdata = .; *(.data .data.*) . = ALIGN(4); _edata = .; } > DTCM AT > FLASH - .bss ORIGIN(DTCM) + 0x1000 (NOLOAD) : { _sbss = .; *(.bss .bss.*) *(COMMON) . = ALIGN(4); _ebss = .; } > DTCM - /DISCARD/ : { *(.ARM.exidx*) *(.comment) *(.inputs) *(.dmod*) } -} -ASSERT(SIZEOF(.data) == 0, "the selector has no initialized RAM: keep it const") diff --git a/selector/fw/main.c b/selector/fw/main.c index faa6e7f..bae3497 100644 --- a/selector/fw/main.c +++ b/selector/fw/main.c @@ -1,34 +1,21 @@ /** * @file main.c - * @brief The selector for the STM32F746G-DISCO: the layout of the flash, the log, the table for the firmware + * @brief The selector: the log, the table for the firmware and the start of the chosen bank * - * Flash: S0 selector | S1 store (state of the banks, persistent items) | S2, S3 file systems of the banks | - * S4+S5 bank A (384 kB) | S6+S7 bank B (384 kB of the 512 kB) + * The same for every platform; what is specific is in configs/ (see include/dmsel_platform.h): the core (arch), the chip + * (mcu) and the layout of the flash (board). */ #include #include "dmlog.h" #include "dmsel_table.h" -#include "dmsel_f7.h" +#include "dmsel_platform.h" +#include "dmsel_mcu.h" +#include "dmsel_board.h" -#ifndef DMSEL_WATCHDOG_MS -#define DMSEL_WATCHDOG_MS 0 // The firmware has to feed the watchdog from the moment it starts: 0 until it does -#endif #define LOG_SIZE 4096u -#define SCRATCH_SIZE 0x8000u #define SELECTOR_VERSION 1 -static const dmsel_layout_t g_layout = -{ - .flash_address = 0x08000000u, - .store_offset = DMSEL_F7_STORE_OFFSET, - .store_size = DMSEL_F7_STORE_SIZE, - .bank_count = 2, - .banks = { { 0x20000u, 0x60000u }, { 0x80000u, 0x60000u } }, - .max_tries = 3, - .watchdog_ms = DMSEL_WATCHDOG_MS, -}; - -static uint8_t g_scratch[SCRATCH_SIZE] __attribute__(( aligned( 8 ) )); +static uint8_t g_scratch[DMSEL_BOARD_STORE_SIZE] __attribute__(( aligned( 8 ) )); static uint8_t g_log_storage[LOG_SIZE] __attribute__(( aligned( 8 ) )); static dmlog_ctx_t g_log; static dmsel_t g_state; @@ -45,20 +32,22 @@ static void port_log( void* user, const char* text ) /** The selector's own port (it logs), and the one behind the table (the firmware has the log of its own) */ static const dmsel_port_t g_port = { - .flash = (const uint8_t*)0x08000000u, .erase = dmsel_f7_erase, .program = dmsel_f7_program, - .jump = dmsel_f7_jump, .log = port_log, .watchdog_start = dmsel_f7_watchdog_start, + .flash = (const uint8_t*)DMSEL_MCU_FLASH_ADDRESS, .erase = dmsel_mcu_erase, .program = dmsel_mcu_program, + .jump = dmsel_arch_jump, .log = port_log, .watchdog_start = dmsel_mcu_watchdog_start, + .user = (void*)&dmsel_board_layout, }; static const dmsel_port_t g_port_for_firmware = { - .flash = (const uint8_t*)0x08000000u, .erase = dmsel_f7_erase, .program = dmsel_f7_program, - .jump = dmsel_f7_jump, .log = 0, .watchdog_start = dmsel_f7_watchdog_start, + .flash = (const uint8_t*)DMSEL_MCU_FLASH_ADDRESS, .erase = dmsel_mcu_erase, .program = dmsel_mcu_program, + .jump = dmsel_arch_jump, .log = 0, .watchdog_start = dmsel_mcu_watchdog_start, + .user = (void*)&dmsel_board_layout, }; static int table_init( dmsel_t* s, int running, uint8_t* scratch ) { - int r = dmsel_init( s, &g_layout, &g_port_for_firmware, scratch ); + int r = dmsel_init( s, &dmsel_board_layout, &g_port_for_firmware, scratch ); - s->running = (running >= 0 && (uint32_t)running < g_layout.bank_count) ? (uint8_t)running : DMSEL_MAX_BANKS; + s->running = (running >= 0 && (uint32_t)running < dmsel_board_layout.bank_count) ? (uint8_t)running : DMSEL_MAX_BANKS; return r; } @@ -66,7 +55,7 @@ __attribute__(( section( ".dmsel_table" ), used )) const dmsel_table_t g_table = { .magic = DMSEL_TABLE_MAGIC, .abi = DMSEL_TABLE_ABI, .table_size = sizeof(dmsel_table_t), - .state_size = sizeof(dmsel_t), .scratch_size = DMSEL_F7_STORE_SIZE, .version = SELECTOR_VERSION, + .state_size = sizeof(dmsel_t), .scratch_size = DMSEL_BOARD_STORE_SIZE, .version = SELECTOR_VERSION, .log_address = (uint32_t)(uintptr_t)g_log_storage, .log_size = LOG_SIZE, .init = table_init, .bank_count = dmsel_bank_count, .running_bank = dmsel_running_bank, .get_info = dmsel_get_info, .verify = dmsel_verify, .select = dmsel_select, .ready = dmsel_ready, @@ -77,7 +66,7 @@ int main( void ) { g_log = dmlog_create( g_log_storage, LOG_SIZE ); dmlog_puts( g_log, "dmsel: selector started\n" ); - dmsel_init( &g_state, &g_layout, &g_port, g_scratch ); + dmsel_init( &g_state, &dmsel_board_layout, &g_port, g_scratch ); dmsel_boot( &g_state ); // Only when nothing can be started: the log stays in the RAM for the debugger for( ;; ) diff --git a/selector/include/dmsel_platform.h b/selector/include/dmsel_platform.h new file mode 100644 index 0000000..41777db --- /dev/null +++ b/selector/include/dmsel_platform.h @@ -0,0 +1,38 @@ +/** + * @file dmsel_platform.h + * @brief What the selector firmware takes from the platform: configs/arch, configs/mcu and configs/board of the selector + * + * The directories follow the configs/ of dmboot (arch//, mcu//, board/): + * - arch: the core - the reset handler and the vectors, the jump into an image (dmsel_arch_jump) + * - mcu: the chip - the flash controller (dmsel_mcu_erase, dmsel_mcu_program), the watchdog, the memory map (mcu.ld); + * its dmsel_mcu.h defines DMSEL_MCU_FLASH_ADDRESS + * - board: the layout of the flash - the banks and the store (dmsel_board_layout); its dmsel_board.h defines + * DMSEL_BOARD_STORE_SIZE + * The `user` of the port is the layout (the mcu uses it to erase and program the store only). + */ +#ifndef DMSEL_PLATFORM_H +#define DMSEL_PLATFORM_H + +#include +#include "dmsel.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// arch +void dmsel_arch_jump( void* user, uint32_t vector_address ); + +// mcu +int dmsel_mcu_erase( void* user, uint32_t offset, uint32_t size ); +int dmsel_mcu_program( void* user, uint32_t offset, const void* data, uint32_t size ); +void dmsel_mcu_watchdog_start( void* user, uint32_t timeout_ms ); + +// board +extern const dmsel_layout_t dmsel_board_layout; + +#ifdef __cplusplus +} +#endif + +#endif /* DMSEL_PLATFORM_H */ diff --git a/selector/linker/common.ld b/selector/linker/common.ld new file mode 100644 index 0000000..91b684f --- /dev/null +++ b/selector/linker/common.ld @@ -0,0 +1,15 @@ +/* The selector: the code and the table in the FLASH, the stack and the (uninitialized) data in the RAM. + The memory map is the mcu's (mcu.ld), the entry and the top of the stack the arch's (arch.ld). */ +DMSEL_STACK_SIZE = 0x1000; /* 4 kB of the stack at the bottom of the RAM, the data above it */ + +SECTIONS +{ + .isr_vector : { KEEP(*(.isr_vector)) } > FLASH + .dmsel_table ORIGIN(FLASH) + 0x100 : { KEEP(*(.dmsel_table)) } > FLASH /* DMSEL_TABLE_ADDRESS */ + .text : { *(.text .text.*) *(.rodata .rodata.*) . = ALIGN(4); } > FLASH + _sidata = LOADADDR(.data); + .data ORIGIN(RAM) + DMSEL_STACK_SIZE : { _sdata = .; *(.data .data.*) . = ALIGN(4); _edata = .; } > RAM AT > FLASH + .bss ORIGIN(RAM) + DMSEL_STACK_SIZE (NOLOAD) : { _sbss = .; *(.bss .bss.*) *(COMMON) . = ALIGN(4); _ebss = .; } > RAM + /DISCARD/ : { *(.ARM.exidx*) *(.comment) *(.inputs) *(.dmod*) } +} +ASSERT(SIZEOF(.data) == 0, "the selector has no initialized RAM: keep it const") From fda8a7d63a88da352e608a5cf9838101f8e8c48e Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Sun, 1 Nov 2026 15:58:15 +0000 Subject: [PATCH 14/14] selector: the sources in selector/arch and selector/mcu, the rest in configs/ of dmboot No configs/ in the path of the sources: the code of the core and of the chip is in selector/arch/ and selector/mcu//. The linker scripts of the selector are selector.ld next to the mcu.ld and arch.ld of dmboot, and the layout of the flash is the board's own (DMBOOT_BANK_*, DMBOOT_SELECTOR_* of board.cmake, plus DMBOOT_SELECTOR_STORE_SIZE) - generated into the selector, no copy of it in C. Co-Authored-By: Claude Sonnet 5.5 --- configs/arch/armv7/selector.ld | 4 ++ configs/board/stm32f746g-disco/board.cmake | 1 + configs/mcu/stm32f7/stm32f746xg/selector.ld | 7 +++ scripts/targets.cmake | 2 +- selector/arch/armv7/arch.cmake | 3 + selector/{configs => }/arch/armv7/jump.c | 0 selector/{configs => }/arch/armv7/startup.c | 0 selector/configs/arch/armv7/arch.ld | 3 - selector/configs/arch/armv7/cfg.cmake | 3 - .../configs/arch/armv7/cortex-m7/cfg.cmake | 2 - .../configs/board/stm32f746g-disco/board.c | 26 --------- .../board/stm32f746g-disco/board.cmake | 7 --- .../board/stm32f746g-disco/dmsel_board.h | 8 --- .../configs/mcu/stm32f7/stm32f746xg/mcu.cmake | 4 -- .../configs/mcu/stm32f7/stm32f746xg/mcu.ld | 6 -- selector/fw/CMakeLists.txt | 58 +++++++++++++------ selector/fw/dmsel_layout.h.in | 22 +++++++ selector/fw/main.c | 20 +++---- selector/include/dmsel_platform.h | 14 ++--- .../mcu/stm32f7/stm32f746xg/dmsel_mcu.h | 0 .../mcu/stm32f7/stm32f746xg/flash.c | 0 selector/mcu/stm32f7/stm32f746xg/mcu.cmake | 3 + .../mcu/stm32f7/stm32f746xg/watchdog.c | 0 23 files changed, 97 insertions(+), 96 deletions(-) create mode 100644 configs/arch/armv7/selector.ld create mode 100644 configs/mcu/stm32f7/stm32f746xg/selector.ld create mode 100644 selector/arch/armv7/arch.cmake rename selector/{configs => }/arch/armv7/jump.c (100%) rename selector/{configs => }/arch/armv7/startup.c (100%) delete mode 100644 selector/configs/arch/armv7/arch.ld delete mode 100644 selector/configs/arch/armv7/cfg.cmake delete mode 100644 selector/configs/arch/armv7/cortex-m7/cfg.cmake delete mode 100644 selector/configs/board/stm32f746g-disco/board.c delete mode 100644 selector/configs/board/stm32f746g-disco/board.cmake delete mode 100644 selector/configs/board/stm32f746g-disco/dmsel_board.h delete mode 100644 selector/configs/mcu/stm32f7/stm32f746xg/mcu.cmake delete mode 100644 selector/configs/mcu/stm32f7/stm32f746xg/mcu.ld create mode 100644 selector/fw/dmsel_layout.h.in rename selector/{configs => }/mcu/stm32f7/stm32f746xg/dmsel_mcu.h (100%) rename selector/{configs => }/mcu/stm32f7/stm32f746xg/flash.c (100%) create mode 100644 selector/mcu/stm32f7/stm32f746xg/mcu.cmake rename selector/{configs => }/mcu/stm32f7/stm32f746xg/watchdog.c (100%) diff --git a/configs/arch/armv7/selector.ld b/configs/arch/armv7/selector.ld new file mode 100644 index 0000000..4281b07 --- /dev/null +++ b/configs/arch/armv7/selector.ld @@ -0,0 +1,4 @@ +/* The selector (selector/): ARMv7-M - the reset handler is the entry, the stack grows down from the top of its place at the + bottom of the RAM (DMSEL_STACK_SIZE: selector/linker/common.ld) */ +ENTRY(Reset_Handler) +_estack = ORIGIN(RAM) + DMSEL_STACK_SIZE; diff --git a/configs/board/stm32f746g-disco/board.cmake b/configs/board/stm32f746g-disco/board.cmake index 5a287d4..084484d 100644 --- a/configs/board/stm32f746g-disco/board.cmake +++ b/configs/board/stm32f746g-disco/board.cmake @@ -73,6 +73,7 @@ set(DMBOOT_BANK_B_ORIGIN 0x08080000) set(DMBOOT_BANK_B_SIZE 0x00060000) set(DMBOOT_SELECTOR_ORIGIN 0x08000000) set(DMBOOT_SELECTOR_STORE 0x08008000) +set(DMBOOT_SELECTOR_STORE_SIZE 0x00008000) set(DMBOOT_SELECTOR_STORE_SECTOR 1) # QUADSPI bank 1 + N25Q128A, DMA2 stream 7/channel 3, IRQ 92 (RM0385). diff --git a/configs/mcu/stm32f7/stm32f746xg/selector.ld b/configs/mcu/stm32f7/stm32f746xg/selector.ld new file mode 100644 index 0000000..264f154 --- /dev/null +++ b/configs/mcu/stm32f7/stm32f746xg/selector.ld @@ -0,0 +1,7 @@ +/* The selector (selector/): the first sector of the flash (DMSEL_ROM_ORIGIN, DMSEL_ROM_LENGTH come from --defsym: the + layout of the board), the DTCM is its RAM */ +MEMORY +{ + FLASH (rx) : ORIGIN = DMSEL_ROM_ORIGIN, LENGTH = DMSEL_ROM_LENGTH + RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K +} diff --git a/scripts/targets.cmake b/scripts/targets.cmake index be9f51e..cada11f 100644 --- a/scripts/targets.cmake +++ b/scripts/targets.cmake @@ -162,7 +162,7 @@ if(DMBOOT_BANK AND NOT DMBOOT_EMULATION) VERBATIM) add_custom_target(selector - COMMAND ${CMAKE_COMMAND} -S ${CMAKE_CURRENT_SOURCE_DIR}/selector/fw -B ${DMBOOT_SELECTOR_BUILD_DIR} -DDMSEL_BOARD=${BOARD} + COMMAND ${CMAKE_COMMAND} -S ${CMAKE_CURRENT_SOURCE_DIR}/selector/fw -B ${DMBOOT_SELECTOR_BUILD_DIR} -DDMSEL_BOARD=${BOARD} -DDMSEL_ARCH=${DMBOOT_ARCH} -DDMSEL_ARCH_CORE=${DMBOOT_ARCH_FAMILY} -DDMSEL_MCU_SERIES=${DMBOOT_MCU_SERIES} -DDMSEL_MCU=${DMBOOT_MCU_NAME} COMMAND ${CMAKE_COMMAND} --build ${DMBOOT_SELECTOR_BUILD_DIR} BYPRODUCTS ${DMBOOT_SELECTOR_BIN} COMMENT "Building the selector...") diff --git a/selector/arch/armv7/arch.cmake b/selector/arch/armv7/arch.cmake new file mode 100644 index 0000000..ba490f8 --- /dev/null +++ b/selector/arch/armv7/arch.cmake @@ -0,0 +1,3 @@ +# ARMv7-M: the reset handler and the jump; the flags of the core (DMSEL_ARCH_CORE is the name of the core: cortex-m7) +set(DMSEL_ARCH_SOURCES ${CMAKE_CURRENT_LIST_DIR}/startup.c ${CMAKE_CURRENT_LIST_DIR}/jump.c) +set(DMSEL_CPU_FLAGS -mcpu=${DMSEL_ARCH_CORE} -mthumb) diff --git a/selector/configs/arch/armv7/jump.c b/selector/arch/armv7/jump.c similarity index 100% rename from selector/configs/arch/armv7/jump.c rename to selector/arch/armv7/jump.c diff --git a/selector/configs/arch/armv7/startup.c b/selector/arch/armv7/startup.c similarity index 100% rename from selector/configs/arch/armv7/startup.c rename to selector/arch/armv7/startup.c diff --git a/selector/configs/arch/armv7/arch.ld b/selector/configs/arch/armv7/arch.ld deleted file mode 100644 index 5089027..0000000 --- a/selector/configs/arch/armv7/arch.ld +++ /dev/null @@ -1,3 +0,0 @@ -/* ARMv7-M: the reset handler is the entry, the stack grows down from the top of its place at the bottom of the RAM */ -ENTRY(Reset_Handler) -_estack = ORIGIN(RAM) + DMSEL_STACK_SIZE; diff --git a/selector/configs/arch/armv7/cfg.cmake b/selector/configs/arch/armv7/cfg.cmake deleted file mode 100644 index ff3224f..0000000 --- a/selector/configs/arch/armv7/cfg.cmake +++ /dev/null @@ -1,3 +0,0 @@ -# ARMv7-M common to the cores: the reset handler and the jump (the core's flags: /cfg.cmake) -set(DMSEL_ARCH_SOURCES ${DMSEL_CONFIGS_DIR}/arch/armv7/startup.c ${DMSEL_CONFIGS_DIR}/arch/armv7/jump.c) -set(DMSEL_ARCH_LINKER_SCRIPT ${DMSEL_CONFIGS_DIR}/arch/armv7/arch.ld) diff --git a/selector/configs/arch/armv7/cortex-m7/cfg.cmake b/selector/configs/arch/armv7/cortex-m7/cfg.cmake deleted file mode 100644 index 623aaed..0000000 --- a/selector/configs/arch/armv7/cortex-m7/cfg.cmake +++ /dev/null @@ -1,2 +0,0 @@ -# Cortex-M7 -set(DMSEL_CPU_FLAGS -mcpu=cortex-m7 -mthumb) diff --git a/selector/configs/board/stm32f746g-disco/board.c b/selector/configs/board/stm32f746g-disco/board.c deleted file mode 100644 index 9551814..0000000 --- a/selector/configs/board/stm32f746g-disco/board.c +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @file board.c - * @brief STM32F746G-DISCO: the layout of the flash - * - * Flash: S0 selector | S1 store (state of the banks, persistent items) | S2, S3 file systems of the banks | - * S4+S5 bank A (384 kB) | S6+S7 bank B (384 kB of the 512 kB) - * (the same banks as configs/board/stm32f746g-disco/board.cmake of dmboot: DMBOOT_BANK_*) - */ -#include "dmsel_platform.h" -#include "dmsel_mcu.h" -#include "dmsel_board.h" - -#ifndef DMSEL_WATCHDOG_MS -#define DMSEL_WATCHDOG_MS 0 // The firmware has to feed the watchdog from the moment it starts: 0 until it does -#endif - -const dmsel_layout_t dmsel_board_layout = -{ - .flash_address = DMSEL_MCU_FLASH_ADDRESS, - .store_offset = DMSEL_BOARD_STORE_OFFSET, - .store_size = DMSEL_BOARD_STORE_SIZE, - .bank_count = 2, - .banks = { { 0x20000u, 0x60000u }, { 0x80000u, 0x60000u } }, - .max_tries = 3, - .watchdog_ms = DMSEL_WATCHDOG_MS, -}; diff --git a/selector/configs/board/stm32f746g-disco/board.cmake b/selector/configs/board/stm32f746g-disco/board.cmake deleted file mode 100644 index ea05600..0000000 --- a/selector/configs/board/stm32f746g-disco/board.cmake +++ /dev/null @@ -1,7 +0,0 @@ -# STM32F746G-DISCO: a Cortex-M7 of the STM32F7 family -set(DMSEL_ARCH_FAMILY armv7) -set(DMSEL_ARCH_CORE cortex-m7) -set(DMSEL_MCU_FAMILY stm32f7) -set(DMSEL_MCU stm32f746xg) -set(DMSEL_BOARD_SOURCES ${DMSEL_BOARD_DIR}/board.c) -set(DMSEL_BOARD_INCLUDE_DIR ${DMSEL_BOARD_DIR}) diff --git a/selector/configs/board/stm32f746g-disco/dmsel_board.h b/selector/configs/board/stm32f746g-disco/dmsel_board.h deleted file mode 100644 index fd88c70..0000000 --- a/selector/configs/board/stm32f746g-disco/dmsel_board.h +++ /dev/null @@ -1,8 +0,0 @@ -#ifndef DMSEL_BOARD_H -#define DMSEL_BOARD_H - -/** The sector of the store (S1) - the only one that the selector erases and programs */ -#define DMSEL_BOARD_STORE_OFFSET 0x08000u -#define DMSEL_BOARD_STORE_SIZE 0x08000u - -#endif diff --git a/selector/configs/mcu/stm32f7/stm32f746xg/mcu.cmake b/selector/configs/mcu/stm32f7/stm32f746xg/mcu.cmake deleted file mode 100644 index 4bc9bc6..0000000 --- a/selector/configs/mcu/stm32f7/stm32f746xg/mcu.cmake +++ /dev/null @@ -1,4 +0,0 @@ -# STM32F746xG: the flash controller, the watchdog, the memory map -set(DMSEL_MCU_SOURCES ${DMSEL_MCU_DIR}/flash.c ${DMSEL_MCU_DIR}/watchdog.c) -set(DMSEL_MCU_LINKER_SCRIPT ${DMSEL_MCU_DIR}/mcu.ld) -set(DMSEL_MCU_INCLUDE_DIR ${DMSEL_MCU_DIR}) diff --git a/selector/configs/mcu/stm32f7/stm32f746xg/mcu.ld b/selector/configs/mcu/stm32f7/stm32f746xg/mcu.ld deleted file mode 100644 index 48d49d6..0000000 --- a/selector/configs/mcu/stm32f7/stm32f746xg/mcu.ld +++ /dev/null @@ -1,6 +0,0 @@ -/* STM32F746xG: the first sector of the flash (32 kB) is the selector, the DTCM is its RAM */ -MEMORY -{ - FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K - RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K -} diff --git a/selector/fw/CMakeLists.txt b/selector/fw/CMakeLists.txt index c800440..dc087ac 100644 --- a/selector/fw/CMakeLists.txt +++ b/selector/fw/CMakeLists.txt @@ -1,9 +1,11 @@ # The selector (the code of the first sector of the flash) # cmake -S selector/fw -B build-selector-fw [-DDMSEL_BOARD=stm32f746g-disco] && cmake --build build-selector-fw -# Result: dmsel.elf and dmsel.bin (at most the first sector: the linker script checks) +# Result: dmsel.elf and dmsel.bin (at most the sectors before the store: the linker script checks) # -# What depends on the platform is in selector/configs, the same tree as configs/ of dmboot: the board -# (board//board.cmake) names its core (arch//) and its chip (mcu//). +# The layout of the flash is the board's (configs/board//board.cmake of dmboot: DMBOOT_BANK_*, DMBOOT_SELECTOR_*), +# the memory map and the entry are in configs/mcu/.../selector.ld and configs/arch/.../selector.ld. The code that depends +# on the platform is in selector/arch/, selector/mcu//; dmboot passes the names of its target +# (DMSEL_ARCH ...), the defaults are those of the default board. cmake_minimum_required(VERSION 3.16) set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_PROCESSOR arm) @@ -12,19 +14,37 @@ set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) project(dmsel_fw C) set(DMSEL_WATCHDOG_MS 0 CACHE STRING "Watchdog (ms) armed for an image on trial; the firmware has to feed it, 0: none") +set(DMSEL_MAX_TRIES 3 CACHE STRING "Starts of an image on trial that does not confirm, before it is dropped") set(DMBOOT_DIR "${CMAKE_CURRENT_LIST_DIR}/../..") -set(DMSEL_BOARD "stm32f746g-disco" CACHE STRING "Board the selector is built for (selector/configs/board)") -set(DMSEL_CONFIGS_DIR "${CMAKE_CURRENT_LIST_DIR}/../configs") -set(DMSEL_BOARD_DIR "${DMSEL_CONFIGS_DIR}/board/${DMSEL_BOARD}") -if(NOT EXISTS "${DMSEL_BOARD_DIR}/board.cmake") - message(FATAL_ERROR "The selector has no board '${DMSEL_BOARD}' (selector/configs/board)") +set(DMSEL_BOARD "stm32f746g-disco" CACHE STRING "Board the selector is built for (configs/board of dmboot)") +set(DMSEL_ARCH "armv7" CACHE STRING "Architecture (selector/arch, configs/arch)") +set(DMSEL_ARCH_CORE "cortex-m7" CACHE STRING "Core") +set(DMSEL_MCU_SERIES "stm32f7" CACHE STRING "Series of the microcontroller (selector/mcu)") +set(DMSEL_MCU "stm32f746xg" CACHE STRING "Microcontroller") + +set(DMSEL_BOARD_FILE "${DMBOOT_DIR}/configs/board/${DMSEL_BOARD}/board.cmake") +if(NOT EXISTS "${DMSEL_BOARD_FILE}") + message(FATAL_ERROR "dmboot has no board '${DMSEL_BOARD}' (configs/board)") endif() -include("${DMSEL_BOARD_DIR}/board.cmake") -set(DMSEL_MCU_DIR "${DMSEL_CONFIGS_DIR}/mcu/${DMSEL_MCU_FAMILY}/${DMSEL_MCU}") -include("${DMSEL_CONFIGS_DIR}/arch/${DMSEL_ARCH_FAMILY}/cfg.cmake") -include("${DMSEL_CONFIGS_DIR}/arch/${DMSEL_ARCH_FAMILY}/${DMSEL_ARCH_CORE}/cfg.cmake") -include("${DMSEL_MCU_DIR}/mcu.cmake") +include("${DMSEL_BOARD_FILE}") +include("${CMAKE_CURRENT_LIST_DIR}/../arch/${DMSEL_ARCH}/arch.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/../mcu/${DMSEL_MCU_SERIES}/${DMSEL_MCU}/mcu.cmake") + +# The layout: offsets from the start of the flash (the selector's origin) +math(EXPR DMSEL_STORE_OFFSET "${DMBOOT_SELECTOR_STORE} - ${DMBOOT_SELECTOR_ORIGIN}" OUTPUT_FORMAT HEXADECIMAL) +math(EXPR DMSEL_STORE_SIZE "${DMBOOT_SELECTOR_STORE_SIZE}" OUTPUT_FORMAT HEXADECIMAL) +math(EXPR DMSEL_ROM_LENGTH "${DMBOOT_SELECTOR_STORE} - ${DMBOOT_SELECTOR_ORIGIN}" OUTPUT_FORMAT HEXADECIMAL) +set(DMSEL_BANK_COUNT 0) +set(DMSEL_BANKS_INIT "") +foreach(letter IN ITEMS A B C D) + if(DEFINED DMBOOT_BANK_${letter}_ORIGIN) + math(EXPR offset "${DMBOOT_BANK_${letter}_ORIGIN} - ${DMBOOT_SELECTOR_ORIGIN}" OUTPUT_FORMAT HEXADECIMAL) + string(APPEND DMSEL_BANKS_INIT "{ ${offset}u, ${DMBOOT_BANK_${letter}_SIZE}u }, ") + math(EXPR DMSEL_BANK_COUNT "${DMSEL_BANK_COUNT} + 1") + endif() +endforeach() +configure_file("${CMAKE_CURRENT_LIST_DIR}/dmsel_layout.h.in" "${CMAKE_BINARY_DIR}/gen/dmsel_layout.h" @ONLY) # dmlog.c wants the configuration header of dmod: a bare-metal one, as the firmware has it set(DMOD_VERSION_STRING "1.0") @@ -56,22 +76,26 @@ configure_file("${DMBOOT_DIR}/lib/dmod/dmod-config.h.in" "${CMAKE_BINARY_DIR}/ge add_executable(dmsel.elf main.c dmod_stubs.c libc_min.c - ${DMSEL_ARCH_SOURCES} ${DMSEL_MCU_SOURCES} ${DMSEL_BOARD_SOURCES} + ${DMSEL_ARCH_SOURCES} ${DMSEL_MCU_SOURCES} ../src/dmsel.c ${DMBOOT_DIR}/lib/dmlog/src/dmlog.c) set_source_files_properties(${DMBOOT_DIR}/lib/dmlog/src/dmlog.c PROPERTIES COMPILE_OPTIONS "-w") -target_include_directories(dmsel.elf PRIVATE ../include ${DMSEL_MCU_INCLUDE_DIR} ${DMSEL_BOARD_INCLUDE_DIR} +target_include_directories(dmsel.elf PRIVATE ../include ${DMSEL_MCU_INCLUDE_DIR} ${DMBOOT_DIR}/lib/dmlog/include ${DMBOOT_DIR}/lib/dmod/inc ${CMAKE_BINARY_DIR}/gen) -target_compile_definitions(dmsel.elf PRIVATE DMOD_EXTERNAL_REGISTRATION DMOD_CURRENT_ALLOCATOR="dmsel" DMSEL_WATCHDOG_MS=${DMSEL_WATCHDOG_MS}) +target_compile_definitions(dmsel.elf PRIVATE DMOD_EXTERNAL_REGISTRATION DMOD_CURRENT_ALLOCATOR="dmsel") target_compile_options(dmsel.elf PRIVATE ${DMSEL_CPU_FLAGS} -Os -ffunction-sections -fdata-sections -ffreestanding -Wall -Wextra -std=gnu11) # (the order matters: the memory map first, the entry and the stack of the core use it, the sections last) -set(DMSEL_LINKER_SCRIPTS ${DMSEL_MCU_LINKER_SCRIPT} ${DMSEL_ARCH_LINKER_SCRIPT} ${CMAKE_CURRENT_LIST_DIR}/../linker/common.ld) +set(DMSEL_LINKER_SCRIPTS + ${DMBOOT_DIR}/configs/mcu/${DMSEL_MCU_SERIES}/${DMSEL_MCU}/selector.ld + ${DMBOOT_DIR}/configs/arch/${DMSEL_ARCH}/selector.ld + ${CMAKE_CURRENT_LIST_DIR}/../linker/common.ld) set(DMSEL_LINK_SCRIPT_OPTIONS) foreach(script IN LISTS DMSEL_LINKER_SCRIPTS) list(APPEND DMSEL_LINK_SCRIPT_OPTIONS -T${script}) endforeach() target_link_options(dmsel.elf PRIVATE ${DMSEL_CPU_FLAGS} -nostdlib -nostartfiles -Wl,--gc-sections + -Wl,--defsym=DMSEL_ROM_ORIGIN=${DMBOOT_SELECTOR_ORIGIN} -Wl,--defsym=DMSEL_ROM_LENGTH=${DMSEL_ROM_LENGTH} ${DMSEL_LINK_SCRIPT_OPTIONS} -Wl,-Map=${CMAKE_BINARY_DIR}/dmsel.map -lgcc) set_target_properties(dmsel.elf PROPERTIES LINK_DEPENDS "${DMSEL_LINKER_SCRIPTS}") add_custom_command(TARGET dmsel.elf POST_BUILD diff --git a/selector/fw/dmsel_layout.h.in b/selector/fw/dmsel_layout.h.in new file mode 100644 index 0000000..fa44ec5 --- /dev/null +++ b/selector/fw/dmsel_layout.h.in @@ -0,0 +1,22 @@ +/* Generated from the board's layout of the flash (DMBOOT_BANK_*, DMBOOT_SELECTOR_* of configs/board/@DMSEL_BOARD@/board.cmake) */ +#ifndef DMSEL_LAYOUT_H +#define DMSEL_LAYOUT_H + +#include "dmsel.h" +#include "dmsel_mcu.h" + +#define DMSEL_STORE_SIZE @DMSEL_STORE_SIZE@u + +/** The user of the port: the mcu erases and programs the store only */ +static const dmsel_layout_t dmsel_layout = +{ + .flash_address = DMSEL_MCU_FLASH_ADDRESS, + .store_offset = @DMSEL_STORE_OFFSET@u, + .store_size = DMSEL_STORE_SIZE, + .bank_count = @DMSEL_BANK_COUNT@, + .banks = { @DMSEL_BANKS_INIT@ }, + .max_tries = @DMSEL_MAX_TRIES@, + .watchdog_ms = @DMSEL_WATCHDOG_MS@, +}; + +#endif diff --git a/selector/fw/main.c b/selector/fw/main.c index bae3497..de952bf 100644 --- a/selector/fw/main.c +++ b/selector/fw/main.c @@ -2,20 +2,20 @@ * @file main.c * @brief The selector: the log, the table for the firmware and the start of the chosen bank * - * The same for every platform; what is specific is in configs/ (see include/dmsel_platform.h): the core (arch), the chip - * (mcu) and the layout of the flash (board). + * The same for every platform; what is specific is in arch/ and mcu/ (see include/dmsel_platform.h), and in the configs/ + * of dmboot: the layout of the flash (board), the memory map and the entry (mcu, arch). */ #include #include "dmlog.h" #include "dmsel_table.h" #include "dmsel_platform.h" #include "dmsel_mcu.h" -#include "dmsel_board.h" +#include "dmsel_layout.h" #define LOG_SIZE 4096u #define SELECTOR_VERSION 1 -static uint8_t g_scratch[DMSEL_BOARD_STORE_SIZE] __attribute__(( aligned( 8 ) )); +static uint8_t g_scratch[DMSEL_STORE_SIZE] __attribute__(( aligned( 8 ) )); static uint8_t g_log_storage[LOG_SIZE] __attribute__(( aligned( 8 ) )); static dmlog_ctx_t g_log; static dmsel_t g_state; @@ -34,20 +34,20 @@ static const dmsel_port_t g_port = { .flash = (const uint8_t*)DMSEL_MCU_FLASH_ADDRESS, .erase = dmsel_mcu_erase, .program = dmsel_mcu_program, .jump = dmsel_arch_jump, .log = port_log, .watchdog_start = dmsel_mcu_watchdog_start, - .user = (void*)&dmsel_board_layout, + .user = (void*)&dmsel_layout, }; static const dmsel_port_t g_port_for_firmware = { .flash = (const uint8_t*)DMSEL_MCU_FLASH_ADDRESS, .erase = dmsel_mcu_erase, .program = dmsel_mcu_program, .jump = dmsel_arch_jump, .log = 0, .watchdog_start = dmsel_mcu_watchdog_start, - .user = (void*)&dmsel_board_layout, + .user = (void*)&dmsel_layout, }; static int table_init( dmsel_t* s, int running, uint8_t* scratch ) { - int r = dmsel_init( s, &dmsel_board_layout, &g_port_for_firmware, scratch ); + int r = dmsel_init( s, &dmsel_layout, &g_port_for_firmware, scratch ); - s->running = (running >= 0 && (uint32_t)running < dmsel_board_layout.bank_count) ? (uint8_t)running : DMSEL_MAX_BANKS; + s->running = (running >= 0 && (uint32_t)running < dmsel_layout.bank_count) ? (uint8_t)running : DMSEL_MAX_BANKS; return r; } @@ -55,7 +55,7 @@ __attribute__(( section( ".dmsel_table" ), used )) const dmsel_table_t g_table = { .magic = DMSEL_TABLE_MAGIC, .abi = DMSEL_TABLE_ABI, .table_size = sizeof(dmsel_table_t), - .state_size = sizeof(dmsel_t), .scratch_size = DMSEL_BOARD_STORE_SIZE, .version = SELECTOR_VERSION, + .state_size = sizeof(dmsel_t), .scratch_size = DMSEL_STORE_SIZE, .version = SELECTOR_VERSION, .log_address = (uint32_t)(uintptr_t)g_log_storage, .log_size = LOG_SIZE, .init = table_init, .bank_count = dmsel_bank_count, .running_bank = dmsel_running_bank, .get_info = dmsel_get_info, .verify = dmsel_verify, .select = dmsel_select, .ready = dmsel_ready, @@ -66,7 +66,7 @@ int main( void ) { g_log = dmlog_create( g_log_storage, LOG_SIZE ); dmlog_puts( g_log, "dmsel: selector started\n" ); - dmsel_init( &g_state, &dmsel_board_layout, &g_port, g_scratch ); + dmsel_init( &g_state, &dmsel_layout, &g_port, g_scratch ); dmsel_boot( &g_state ); // Only when nothing can be started: the log stays in the RAM for the debugger for( ;; ) diff --git a/selector/include/dmsel_platform.h b/selector/include/dmsel_platform.h index 41777db..73a8894 100644 --- a/selector/include/dmsel_platform.h +++ b/selector/include/dmsel_platform.h @@ -1,13 +1,12 @@ /** * @file dmsel_platform.h - * @brief What the selector firmware takes from the platform: configs/arch, configs/mcu and configs/board of the selector + * @brief What the selector firmware takes from the platform * - * The directories follow the configs/ of dmboot (arch//, mcu//, board/): - * - arch: the core - the reset handler and the vectors, the jump into an image (dmsel_arch_jump) - * - mcu: the chip - the flash controller (dmsel_mcu_erase, dmsel_mcu_program), the watchdog, the memory map (mcu.ld); + * - selector/arch/: the core - the reset handler and the vectors, the jump into an image (dmsel_arch_jump) + * - selector/mcu//: the chip - the flash controller (dmsel_mcu_erase, dmsel_mcu_program), the watchdog; * its dmsel_mcu.h defines DMSEL_MCU_FLASH_ADDRESS - * - board: the layout of the flash - the banks and the store (dmsel_board_layout); its dmsel_board.h defines - * DMSEL_BOARD_STORE_SIZE + * - the board: the layout of the flash, from configs/board//board.cmake of dmboot (fw/dmsel_layout.h.in); the memory + * map and the entry are in configs/mcu/.../selector.ld and configs/arch/.../selector.ld * The `user` of the port is the layout (the mcu uses it to erase and program the store only). */ #ifndef DMSEL_PLATFORM_H @@ -28,9 +27,6 @@ int dmsel_mcu_erase( void* user, uint32_t offset, uint32_t size ); int dmsel_mcu_program( void* user, uint32_t offset, const void* data, uint32_t size ); void dmsel_mcu_watchdog_start( void* user, uint32_t timeout_ms ); -// board -extern const dmsel_layout_t dmsel_board_layout; - #ifdef __cplusplus } #endif diff --git a/selector/configs/mcu/stm32f7/stm32f746xg/dmsel_mcu.h b/selector/mcu/stm32f7/stm32f746xg/dmsel_mcu.h similarity index 100% rename from selector/configs/mcu/stm32f7/stm32f746xg/dmsel_mcu.h rename to selector/mcu/stm32f7/stm32f746xg/dmsel_mcu.h diff --git a/selector/configs/mcu/stm32f7/stm32f746xg/flash.c b/selector/mcu/stm32f7/stm32f746xg/flash.c similarity index 100% rename from selector/configs/mcu/stm32f7/stm32f746xg/flash.c rename to selector/mcu/stm32f7/stm32f746xg/flash.c diff --git a/selector/mcu/stm32f7/stm32f746xg/mcu.cmake b/selector/mcu/stm32f7/stm32f746xg/mcu.cmake new file mode 100644 index 0000000..513fbe4 --- /dev/null +++ b/selector/mcu/stm32f7/stm32f746xg/mcu.cmake @@ -0,0 +1,3 @@ +# STM32F746xG: the flash controller and the watchdog +set(DMSEL_MCU_SOURCES ${CMAKE_CURRENT_LIST_DIR}/flash.c ${CMAKE_CURRENT_LIST_DIR}/watchdog.c) +set(DMSEL_MCU_INCLUDE_DIR ${CMAKE_CURRENT_LIST_DIR}) diff --git a/selector/configs/mcu/stm32f7/stm32f746xg/watchdog.c b/selector/mcu/stm32f7/stm32f746xg/watchdog.c similarity index 100% rename from selector/configs/mcu/stm32f7/stm32f746xg/watchdog.c rename to selector/mcu/stm32f7/stm32f746xg/watchdog.c