diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt new file mode 100644 index 0000000..345af10 --- /dev/null +++ b/app/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.13) + +include(pico_sdk_import.cmake) + +project(app C CXX ASM) + +pico_sdk_init() + +# Zwei Varianten aus derselben Quelle: schnell- und langsam-blinkend. +# Beide werden ueber LED_DELAY_MS parametrisiert. +function(make_app_variant target_name delay_ms) + add_executable(${target_name} app.c) + target_compile_definitions(${target_name} PRIVATE LED_DELAY_MS=${delay_ms}) + pico_set_linker_script(${target_name} ${CMAKE_CURRENT_LIST_DIR}/memmap_app.ld) + target_link_libraries(${target_name} + pico_stdlib + pico_cyw43_arch_none + ) + pico_add_extra_outputs(${target_name}) +endfunction() + +make_app_variant(app_fast 80) +make_app_variant(app_slow 600) diff --git a/app/app.c b/app/app.c new file mode 100644 index 0000000..f8cfbc0 --- /dev/null +++ b/app/app.c @@ -0,0 +1,18 @@ +#include "pico/stdlib.h" +#include "pico/cyw43_arch.h" + +#ifndef LED_DELAY_MS +#define LED_DELAY_MS 80 +#endif + +int main(void) { + if (cyw43_arch_init()) { + return -1; + } + while (true) { + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, true); + sleep_ms(LED_DELAY_MS); + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, false); + sleep_ms(LED_DELAY_MS); + } +} diff --git a/app/memmap_app.ld b/app/memmap_app.ld new file mode 100644 index 0000000..083b604 --- /dev/null +++ b/app/memmap_app.ld @@ -0,0 +1,303 @@ +/* Based on GCC ARM embedded samples. + Defines the following symbols for use by code: + __exidx_start + __exidx_end + __etext + __data_start__ + __preinit_array_start + __preinit_array_end + __init_array_start + __init_array_end + __fini_array_start + __fini_array_end + __data_end__ + __bss_start__ + __bss_end__ + __end__ + end + __HeapLimit + __StackLimit + __StackTop + __stack (== StackTop) +*/ + +MEMORY +{ + /* App liegt hinter dem 512 KB grossen Bootloader-Slot. */ + FLASH(rx) : ORIGIN = 0x10080000, LENGTH = 4M - 512k + RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 512k + SCRATCH_X(rwx) : ORIGIN = 0x20080000, LENGTH = 4k + SCRATCH_Y(rwx) : ORIGIN = 0x20081000, LENGTH = 4k +} + +ENTRY(_entry_point) + +SECTIONS +{ + .flash_begin : { + __flash_binary_start = .; + } > FLASH + + /* The bootrom will enter the image at the point indicated in your + IMAGE_DEF, which is usually the reset handler of your vector table. + + The debugger will use the ELF entry point, which is the _entry_point + symbol, and in our case is *different from the bootrom's entry point.* + This is used to go back through the bootrom on debugger launches only, + to perform the same initial flash setup that would be performed on a + cold boot. + */ + + .text : { + __logical_binary_start = .; + KEEP (*(.vectors)) + KEEP (*(.binary_info_header)) + __binary_info_header_end = .; + KEEP (*(.embedded_block)) + __embedded_block_end = .; + KEEP (*(.reset)) + /* TODO revisit this now memset/memcpy/float in ROM */ + /* bit of a hack right now to exclude all floating point and time critical (e.g. memset, memcpy) code from + * FLASH ... we will include any thing excluded here in .data below by default */ + *(.init) + *libgcc.a:cmse_nonsecure_call.o + *(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a:) .text*) + *(.fini) + /* Pull all c'tors into .text */ + *crtbegin.o(.ctors) + *crtbegin?.o(.ctors) + *(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors) + *(SORT(.ctors.*)) + *(.ctors) + /* Followed by destructors */ + *crtbegin.o(.dtors) + *crtbegin?.o(.dtors) + *(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors) + *(SORT(.dtors.*)) + *(.dtors) + + . = ALIGN(4); + /* preinit data */ + PROVIDE_HIDDEN (__preinit_array_start = .); + KEEP(*(SORT(.preinit_array.*))) + KEEP(*(.preinit_array)) + PROVIDE_HIDDEN (__preinit_array_end = .); + + . = ALIGN(4); + /* init data */ + PROVIDE_HIDDEN (__init_array_start = .); + KEEP(*(SORT(.init_array.*))) + KEEP(*(.init_array)) + PROVIDE_HIDDEN (__init_array_end = .); + + . = ALIGN(4); + /* finit data */ + PROVIDE_HIDDEN (__fini_array_start = .); + *(SORT(.fini_array.*)) + *(.fini_array) + PROVIDE_HIDDEN (__fini_array_end = .); + + *(.eh_frame*) + . = ALIGN(4); + } > FLASH + + /* Note the boot2 section is optional, and should be discarded if there is + no reference to it *inside* the binary, as it is not called by the + bootrom. (The bootrom performs a simple best-effort XIP setup and + leaves it to the binary to do anything more sophisticated.) However + there is still a size limit of 256 bytes, to ensure the boot2 can be + stored in boot RAM. + + Really this is a "XIP setup function" -- the name boot2 is historic and + refers to its dual-purpose on RP2040, where it also handled vectoring + from the bootrom into the user image. + */ + + .boot2 : { + __boot2_start__ = .; + *(.boot2) + __boot2_end__ = .; + } > FLASH + + ASSERT(__boot2_end__ - __boot2_start__ <= 256, + "ERROR: Pico second stage bootloader must be no more than 256 bytes in size") + + .rodata : { + *(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a:) .rodata*) + *(.srodata*) + . = ALIGN(4); + *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.flashdata*))) + . = ALIGN(4); + } > FLASH + + .ARM.extab : + { + *(.ARM.extab* .gnu.linkonce.armextab.*) + } > FLASH + + __exidx_start = .; + .ARM.exidx : + { + *(.ARM.exidx* .gnu.linkonce.armexidx.*) + } > FLASH + __exidx_end = .; + + /* Machine inspectable binary information */ + . = ALIGN(4); + __binary_info_start = .; + .binary_info : + { + KEEP(*(.binary_info.keep.*)) + *(.binary_info.*) + } > FLASH + __binary_info_end = .; + . = ALIGN(4); + + .ram_vector_table (NOLOAD): { + *(.ram_vector_table) + } > RAM + + .uninitialized_data (NOLOAD): { + . = ALIGN(4); + *(.uninitialized_data*) + } > RAM + + .data : { + __data_start__ = .; + *(vtable) + + *(.time_critical*) + + /* remaining .text and .rodata; i.e. stuff we exclude above because we want it in RAM */ + *(.text*) + . = ALIGN(4); + *(.rodata*) + . = ALIGN(4); + + *(.data*) + *(.sdata*) + + . = ALIGN(4); + *(.after_data.*) + . = ALIGN(4); + /* preinit data */ + PROVIDE_HIDDEN (__mutex_array_start = .); + KEEP(*(SORT(.mutex_array.*))) + KEEP(*(.mutex_array)) + PROVIDE_HIDDEN (__mutex_array_end = .); + + *(.jcr) + . = ALIGN(4); + } > RAM AT> FLASH + + .tdata : { + . = ALIGN(4); + *(.tdata .tdata.* .gnu.linkonce.td.*) + /* All data end */ + __tdata_end = .; + } > RAM AT> FLASH + PROVIDE(__data_end__ = .); + + /* __etext is (for backwards compatibility) the name of the .data init source pointer (...) */ + __etext = LOADADDR(.data); + + .tbss (NOLOAD) : { + . = ALIGN(4); + __bss_start__ = .; + __tls_base = .; + *(.tbss .tbss.* .gnu.linkonce.tb.*) + *(.tcommon) + + __tls_end = .; + } > RAM + + .bss (NOLOAD) : { + . = ALIGN(4); + __tbss_end = .; + + *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.bss*))) + *(COMMON) + PROVIDE(__global_pointer$ = . + 2K); + *(.sbss*) + . = ALIGN(4); + __bss_end__ = .; + } > RAM + + .heap (NOLOAD): + { + __end__ = .; + end = __end__; + KEEP(*(.heap*)) + } > RAM + /* historically on GCC sbrk was growing past __HeapLimit to __StackLimit, however + to be more compatible, we now set __HeapLimit explicitly to where the end of the heap is */ + __HeapLimit = ORIGIN(RAM) + LENGTH(RAM); + + /* Start and end symbols must be word-aligned */ + .scratch_x : { + __scratch_x_start__ = .; + *(.scratch_x.*) + . = ALIGN(4); + __scratch_x_end__ = .; + } > SCRATCH_X AT > FLASH + __scratch_x_source__ = LOADADDR(.scratch_x); + + .scratch_y : { + __scratch_y_start__ = .; + *(.scratch_y.*) + . = ALIGN(4); + __scratch_y_end__ = .; + } > SCRATCH_Y AT > FLASH + __scratch_y_source__ = LOADADDR(.scratch_y); + + /* .stack*_dummy section doesn't contains any symbols. It is only + * used for linker to calculate size of stack sections, and assign + * values to stack symbols later + * + * stack1 section may be empty/missing if platform_launch_core1 is not used */ + + /* by default we put core 0 stack at the end of scratch Y, so that if core 1 + * stack is not used then all of SCRATCH_X is free. + */ + .stack1_dummy (NOLOAD): + { + *(.stack1*) + } > SCRATCH_X + .stack_dummy (NOLOAD): + { + KEEP(*(.stack*)) + } > SCRATCH_Y + + .flash_end : { + KEEP(*(.embedded_end_block*)) + PROVIDE(__flash_binary_end = .); + } > FLASH =0xaa + + /* stack limit is poorly named, but historically is maximum heap ptr */ + __StackLimit = ORIGIN(RAM) + LENGTH(RAM); + __StackOneTop = ORIGIN(SCRATCH_X) + LENGTH(SCRATCH_X); + __StackTop = ORIGIN(SCRATCH_Y) + LENGTH(SCRATCH_Y); + __StackOneBottom = __StackOneTop - SIZEOF(.stack1_dummy); + __StackBottom = __StackTop - SIZEOF(.stack_dummy); + PROVIDE(__stack = __StackTop); + + /* picolibc and LLVM */ + PROVIDE (__heap_start = __end__); + PROVIDE (__heap_end = __HeapLimit); + PROVIDE( __tls_align = MAX(ALIGNOF(.tdata), ALIGNOF(.tbss)) ); + PROVIDE( __tls_size_align = (__tls_size + __tls_align - 1) & ~(__tls_align - 1)); + PROVIDE( __arm32_tls_tcb_offset = MAX(8, __tls_align) ); + + /* llvm-libc */ + PROVIDE (_end = __end__); + PROVIDE (__llvm_libc_heap_limit = __HeapLimit); + + /* Check if data + heap + stack exceeds RAM limit */ + ASSERT(__StackLimit >= __HeapLimit, "region RAM overflowed") + + ASSERT( __binary_info_header_end - __logical_binary_start <= 1024, "Binary info must be in first 1024 bytes of the binary") + ASSERT( __embedded_block_end - __logical_binary_start <= 4096, "Embedded block must be in first 4096 bytes of the binary") + + /* todo assert on extra code */ +} + diff --git a/app/pico_sdk_import.cmake b/app/pico_sdk_import.cmake new file mode 100644 index 0000000..65f8a6f --- /dev/null +++ b/app/pico_sdk_import.cmake @@ -0,0 +1,73 @@ +# This is a copy of /external/pico_sdk_import.cmake + +# This can be dropped into an external project to help locate this SDK +# It should be include()ed prior to project() + +if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) + set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) + message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) + set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) + message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) + set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) + message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") +endif () + +set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") +set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") +set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") + +if (NOT PICO_SDK_PATH) + if (PICO_SDK_FETCH_FROM_GIT) + include(FetchContent) + set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) + if (PICO_SDK_FETCH_FROM_GIT_PATH) + get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") + endif () + # GIT_SUBMODULES_RECURSE was added in 3.17 + if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG master + GIT_SUBMODULES_RECURSE FALSE + ) + else () + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG master + ) + endif () + + if (NOT pico_sdk) + message("Downloading Raspberry Pi Pico SDK") + FetchContent_Populate(pico_sdk) + set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) + endif () + set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) + else () + message(FATAL_ERROR + "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." + ) + endif () +endif () + +get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") +if (NOT EXISTS ${PICO_SDK_PATH}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") +endif () + +set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) +if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") +endif () + +set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) + +include(${PICO_SDK_INIT_CMAKE_FILE}) diff --git a/bootloader/CMakeLists.txt b/bootloader/CMakeLists.txt new file mode 100644 index 0000000..35d050a --- /dev/null +++ b/bootloader/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.13) + +include(pico_sdk_import.cmake) + +project(bootloader C CXX ASM) + +pico_sdk_init() + +if (NOT WIFI_SSID OR NOT WIFI_PASSWORD) + message(FATAL_ERROR "Bitte WIFI_SSID und WIFI_PASSWORD per -D setzen") +endif() +if (NOT PICO_ID) + message(FATAL_ERROR "Bitte PICO_ID per -DPICO_ID= setzen") +endif() + +add_executable(bootloader + bootloader.c +) + +target_compile_definitions(bootloader PRIVATE + WIFI_SSID=\"${WIFI_SSID}\" + WIFI_PASSWORD=\"${WIFI_PASSWORD}\" + PICO_ID=${PICO_ID} +) + +target_include_directories(bootloader PRIVATE ${CMAKE_CURRENT_LIST_DIR}) + +pico_set_linker_script(bootloader ${CMAKE_CURRENT_LIST_DIR}/memmap_bootloader.ld) + +target_link_libraries(bootloader + pico_stdlib + pico_cyw43_arch_lwip_threadsafe_background + pico_sha256 + hardware_flash + hardware_sync +) + +pico_enable_stdio_usb(bootloader 1) +pico_enable_stdio_uart(bootloader 0) + +pico_add_extra_outputs(bootloader) diff --git a/bootloader/bootloader.c b/bootloader/bootloader.c new file mode 100644 index 0000000..beb26cc --- /dev/null +++ b/bootloader/bootloader.c @@ -0,0 +1,467 @@ +#include +#include + +#include "pico/stdlib.h" +#include "pico/cyw43_arch.h" +#include "pico/sha256.h" +#include "hardware/flash.h" +#include "hardware/sync.h" +#include "hardware/structs/scb.h" + +#include "lwip/pbuf.h" +#include "lwip/tcp.h" + +// ---- OTA-Wire-Protokoll (siehe server/server.py) ---- +#define OTA_MAGIC 0xC0FFEE01u +#define OTA_STATUS_NONE 0x00u +#define OTA_STATUS_UPDATE 0x01u + +#define SERVER_IP "192.168.161.25" +#define SERVER_PORT 9000 + +// ---- Flash-Layout ---- +#define APP_BASE 0x10080000u +#define APP_FLASH_OFFSET (APP_BASE - XIP_BASE) // = 0x80000, fuer flash_range_* +#define APP_SLOT_SIZE (3584u * 1024u) // 3.5 MB, muss zum Server passen +#define SHA256_BYTES 32 + +#ifndef PICO_ID +#error "Bitte PICO_ID per -DPICO_ID= setzen" +#endif + +// ---------- Hilfen ---------- + +static void print_hash(const char *label, const uint8_t *hash) { + printf("%s ", label); + for (int i = 0; i < SHA256_BYTES; i++) { + printf("%02x", hash[i]); + } + printf("\n"); +} + +static bool app_present(uint32_t base) { + uint32_t sp = *(volatile uint32_t *)base; + return sp > 0x20000000u && sp <= 0x20082000u; +} + +__attribute__((noreturn)) +static void jump_to_app(uint32_t base) { + uint32_t sp = *(volatile uint32_t *)base; + uint32_t reset = *(volatile uint32_t *)(base + 4u); + + cyw43_arch_deinit(); + __asm volatile ("cpsid i" ::: "memory"); + + scb_hw->vtor = base; + __asm volatile ("dsb 0xf" ::: "memory"); + __asm volatile ("isb 0xf" ::: "memory"); + + __asm volatile ( + "msr msp, %0\n\t" + "bx %1\n" + :: "r" (sp), "r" (reset) + : "memory" + ); + __builtin_unreachable(); +} + +__attribute__((noreturn)) +static void halt_with_led(void) { + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, true); + while (true) { + tight_loop_contents(); + } +} + +// ---------- Flash-Operationen mit IRQ-Maskierung ---------- + +static void flash_erase_sector(uint32_t flash_offset) { + uint32_t irq = save_and_disable_interrupts(); + flash_range_erase(flash_offset, FLASH_SECTOR_SIZE); + restore_interrupts(irq); +} + +static void flash_program_page(uint32_t flash_offset, const uint8_t *data) { + uint32_t irq = save_and_disable_interrupts(); + flash_range_program(flash_offset, data, FLASH_PAGE_SIZE); + restore_interrupts(irq); +} + +// ---------- App-Slot hashen ---------- + +static bool hash_app_slot(uint8_t out[SHA256_BYTES]) { + pico_sha256_state_t st; + if (pico_sha256_start_blocking(&st, SHA256_BIG_ENDIAN, true) != PICO_OK) { + printf("pico_sha256_start fehlgeschlagen\n"); + return false; + } + pico_sha256_update_blocking(&st, (const uint8_t *)APP_BASE, APP_SLOT_SIZE); + sha256_result_t result; + pico_sha256_finish(&st, &result); + memcpy(out, result.bytes, SHA256_BYTES); + return true; +} + +// ---------- OTA-Client ---------- + +typedef enum { + OTA_PHASE_REQUEST, // Request raus, warte auf Status-Byte + OTA_PHASE_HEADER, // Status=Update gelesen, sammle size + new_hash (36 Byte) + OTA_PHASE_PAYLOAD, // sammle payload (size Byte) — und schreibe ins Flash + OTA_PHASE_DONE, + OTA_PHASE_ERROR, +} ota_phase_t; + +typedef struct { + struct tcp_pcb *pcb; + ip_addr_t remote; + ota_phase_t phase; + uint8_t request[40]; + + // Update-Header + uint32_t expected_size; + uint8_t new_hash[SHA256_BYTES]; + uint32_t header_have; + uint8_t header_buf[4 + SHA256_BYTES]; + + // Payload + Flash + uint32_t payload_have; + uint8_t page_buf[FLASH_PAGE_SIZE]; + uint32_t page_filled; + + volatile bool done; + bool update_pending; +} ota_t; + +static void ota_finish(ota_t *o) { + if (o->pcb) { + tcp_arg(o->pcb, NULL); + tcp_recv(o->pcb, NULL); + tcp_err(o->pcb, NULL); + tcp_close(o->pcb); + o->pcb = NULL; + } + o->done = true; +} + +// Schreibt eine voll gefuellte page_buf an den Slot. Erase passiert genau dann, +// wenn das die erste Page eines neuen Sektors ist. +static void flush_page_to_flash(ota_t *o, uint32_t offset_in_slot) { + uint32_t flash_off = APP_FLASH_OFFSET + offset_in_slot; + if ((offset_in_slot % FLASH_SECTOR_SIZE) == 0) { + flash_erase_sector(flash_off); + } + flash_program_page(flash_off, o->page_buf); +} + +static void ota_handle_bytes(ota_t *o, const uint8_t *data, uint16_t len) { + uint16_t i = 0; + while (i < len && o->phase != OTA_PHASE_DONE && o->phase != OTA_PHASE_ERROR) { + if (o->phase == OTA_PHASE_REQUEST) { + uint8_t status = data[i++]; + printf("<- status=0x%02x\n", status); + if (status == OTA_STATUS_NONE) { + o->phase = OTA_PHASE_DONE; + break; + } + if (status != OTA_STATUS_UPDATE) { + printf("unerwartetes Statusbyte\n"); + o->phase = OTA_PHASE_ERROR; + break; + } + o->update_pending = true; + o->phase = OTA_PHASE_HEADER; + o->header_have = 0; + } else if (o->phase == OTA_PHASE_HEADER) { + uint16_t take = (uint16_t)(sizeof(o->header_buf) - o->header_have); + if (take > (uint16_t)(len - i)) take = (uint16_t)(len - i); + memcpy(&o->header_buf[o->header_have], &data[i], take); + o->header_have += take; + i += take; + if (o->header_have == sizeof(o->header_buf)) { + memcpy(&o->expected_size, &o->header_buf[0], 4); + memcpy(o->new_hash, &o->header_buf[4], SHA256_BYTES); + printf("Update-Header: size=%u\n", (unsigned)o->expected_size); + print_hash("new_hash:", o->new_hash); + if (o->expected_size == 0 || o->expected_size > APP_SLOT_SIZE) { + printf("Size ausserhalb gueltigem Bereich\n"); + o->phase = OTA_PHASE_ERROR; + break; + } + o->phase = OTA_PHASE_PAYLOAD; + o->payload_have = 0; + o->page_filled = 0; + } + } else if (o->phase == OTA_PHASE_PAYLOAD) { + uint32_t rest_total = o->expected_size - o->payload_have; + uint32_t rest_chunk = (uint32_t)(len - i); + uint32_t free_in_page = FLASH_PAGE_SIZE - o->page_filled; + uint32_t take = rest_chunk; + if (take > rest_total) take = rest_total; + if (take > free_in_page) take = free_in_page; + + memcpy(&o->page_buf[o->page_filled], &data[i], take); + o->page_filled += take; + o->payload_have += take; + i += (uint16_t)take; + + bool page_done = (o->page_filled == FLASH_PAGE_SIZE); + bool last_byte = (o->payload_have == o->expected_size); + + if (page_done || last_byte) { + uint32_t offset_in_slot = o->payload_have - o->page_filled; + if (last_byte && o->page_filled < FLASH_PAGE_SIZE) { + // Letzte Page: mit 0xFF auffuellen, damit der Slot-Hash passt + memset(&o->page_buf[o->page_filled], 0xFF, + FLASH_PAGE_SIZE - o->page_filled); + } + flush_page_to_flash(o, offset_in_slot); + + // Fortschritt alle 64 KB als Lebenszeichen + if (((o->payload_have & 0xFFFF) == 0) || last_byte) { + printf(" geschrieben: %u/%u Byte\n", + (unsigned)o->payload_have, (unsigned)o->expected_size); + } + + o->page_filled = 0; + if (last_byte) { + o->phase = OTA_PHASE_DONE; + break; + } + } + } + } +} + +static err_t on_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err) { + ota_t *o = (ota_t *)arg; + if (!p) { + if (o->phase == OTA_PHASE_PAYLOAD && o->payload_have < o->expected_size) { + printf("Verbindung zu frueh geschlossen (%u/%u)\n", + (unsigned)o->payload_have, (unsigned)o->expected_size); + o->phase = OTA_PHASE_ERROR; + } else if (o->phase != OTA_PHASE_DONE) { + o->phase = OTA_PHASE_DONE; + } + ota_finish(o); + return ERR_OK; + } + + struct pbuf *q = p; + while (q) { + ota_handle_bytes(o, (const uint8_t *)q->payload, q->len); + q = q->next; + } + tcp_recved(pcb, p->tot_len); + pbuf_free(p); + + if (o->phase == OTA_PHASE_DONE || o->phase == OTA_PHASE_ERROR) { + ota_finish(o); + } + return ERR_OK; +} + +static void on_err(void *arg, err_t err) { + ota_t *o = (ota_t *)arg; + printf("TCP-Fehler: %d\n", err); + o->pcb = NULL; + o->phase = OTA_PHASE_ERROR; + o->done = true; +} + +static err_t on_connected(void *arg, struct tcp_pcb *pcb, err_t err) { + ota_t *o = (ota_t *)arg; + if (err != ERR_OK) { + printf("connect-callback err=%d\n", err); + o->phase = OTA_PHASE_ERROR; + ota_finish(o); + return err; + } + err_t w = tcp_write(pcb, o->request, sizeof(o->request), TCP_WRITE_FLAG_COPY); + if (w != ERR_OK) { + printf("tcp_write err=%d\n", w); + o->phase = OTA_PHASE_ERROR; + ota_finish(o); + return w; + } + tcp_output(pcb); + return ERR_OK; +} + +// Statisch, damit das halbe KB nicht jedesmal auf dem Stack steht. +static ota_t g_ota; + +static void run_ota_request(const uint8_t current_hash[SHA256_BYTES], + bool *update_was_received, + uint32_t *received_size, + uint8_t out_new_hash[SHA256_BYTES], + bool *failed) { + *update_was_received = false; + *received_size = 0; + *failed = false; + + ota_t *o = &g_ota; + memset(o, 0, sizeof(*o)); + + if (!ip4addr_aton(SERVER_IP, &o->remote)) { + printf("ungueltige Server-IP\n"); + *failed = true; + return; + } + + uint32_t magic = OTA_MAGIC; + uint32_t pid = (uint32_t)PICO_ID; + memcpy(&o->request[0], &magic, 4); + memcpy(&o->request[4], &pid, 4); + memcpy(&o->request[8], current_hash, SHA256_BYTES); + + o->pcb = tcp_new_ip_type(IP_GET_TYPE(&o->remote)); + if (!o->pcb) { + printf("tcp_new fehlgeschlagen\n"); + *failed = true; + return; + } + tcp_arg(o->pcb, o); + tcp_recv(o->pcb, on_recv); + tcp_err(o->pcb, on_err); + o->phase = OTA_PHASE_REQUEST; + + printf("Verbinde TCP %s:%d\n", SERVER_IP, SERVER_PORT); + cyw43_arch_lwip_begin(); + err_t err = tcp_connect(o->pcb, &o->remote, SERVER_PORT, on_connected); + cyw43_arch_lwip_end(); + if (err != ERR_OK) { + printf("tcp_connect err=%d\n", err); + ota_finish(o); + *failed = true; + return; + } + + // 60 s Gesamttimeout. Reicht fuer 260 KB Download inkl. ein paar Erase-Pausen. + const uint32_t TIMEOUT_MS = 60000; + uint32_t waited = 0; + while (!o->done && waited < TIMEOUT_MS) { + sleep_ms(50); + waited += 50; + } + if (!o->done) { + printf("OTA-Timeout, breche ab\n"); + ota_finish(o); + *failed = true; + return; + } + if (o->phase == OTA_PHASE_ERROR) { + *failed = true; + return; + } + *update_was_received = o->update_pending; + if (o->update_pending) { + *received_size = o->expected_size; + memcpy(out_new_hash, o->new_hash, SHA256_BYTES); + } +} + +// ---------- Nach-Download: Tail-Erase + Hash-Verify ---------- + +// Erased alle Sektoren ab `start_offset` (im Slot, relativ) bis Slot-Ende. +static void erase_tail(uint32_t start_offset) { + // Auf Sektor-Grenze hoch runden. + uint32_t aligned = (start_offset + FLASH_SECTOR_SIZE - 1) & ~(FLASH_SECTOR_SIZE - 1); + uint32_t total_sectors = APP_SLOT_SIZE / FLASH_SECTOR_SIZE; + uint32_t first_sector = aligned / FLASH_SECTOR_SIZE; + + printf("Tail-Erase: Sektoren %u..%u (= %u KB)\n", + (unsigned)first_sector, (unsigned)(total_sectors - 1), + (unsigned)((total_sectors - first_sector) * FLASH_SECTOR_SIZE / 1024)); + + for (uint32_t s = first_sector; s < total_sectors; s++) { + flash_erase_sector(APP_FLASH_OFFSET + s * FLASH_SECTOR_SIZE); + // Optisches Feedback: LED toggeln alle 32 Sektoren (~ alle 1.4 s). + if ((s & 0x1F) == 0) { + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, (s & 0x20) != 0); + } + } + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, false); + printf("Tail-Erase fertig\n"); +} + +// ---------- main ---------- + +int main(void) { + stdio_init_all(); + sleep_ms(2500); + + printf("\n=== Bootloader Etappe C (pico_id=%d) ===\n", (int)PICO_ID); + + if (cyw43_arch_init()) { + printf("cyw43_arch_init fehlgeschlagen\n"); + halt_with_led(); + } + cyw43_arch_enable_sta_mode(); + + uint8_t current_hash[SHA256_BYTES]; + printf("Hashe App-Slot (%u Byte)...\n", (unsigned)APP_SLOT_SIZE); + absolute_time_t t0 = get_absolute_time(); + if (!hash_app_slot(current_hash)) { + halt_with_led(); + } + int64_t took = absolute_time_diff_us(t0, get_absolute_time()) / 1000; + printf("SHA256 fertig in %lld ms\n", took); + print_hash("current:", current_hash); + + printf("Verbinde mit WLAN \"%s\"...\n", WIFI_SSID); + if (cyw43_arch_wifi_connect_timeout_ms( + WIFI_SSID, WIFI_PASSWORD, CYW43_AUTH_WPA2_AES_PSK, 15000) != 0) { + printf("WLAN-Verbindung fehlgeschlagen — boote alte App\n"); + goto boot; + } + printf("WLAN OK\n"); + + bool update_received = false; + bool fail = false; + uint32_t received_size = 0; + uint8_t expected_new_hash[SHA256_BYTES]; + run_ota_request(current_hash, &update_received, &received_size, + expected_new_hash, &fail); + + if (fail) { + printf("OTA-Request fehlgeschlagen — boote alte App\n"); + goto boot; + } + if (!update_received) { + printf("Server: kein Update\n"); + goto boot; + } + + // Update wurde komplett ins Flash geschrieben. Jetzt Tail erasen + // und den ganzen Slot neu hashen. + erase_tail(received_size); + + uint8_t post_hash[SHA256_BYTES]; + printf("Verifiziere durch erneutes Hashen des Slots...\n"); + t0 = get_absolute_time(); + if (!hash_app_slot(post_hash)) { + printf("Post-Hash fehlgeschlagen — bleibe stehen\n"); + halt_with_led(); + } + took = absolute_time_diff_us(t0, get_absolute_time()) / 1000; + printf("Post-Hash fertig in %lld ms\n", took); + print_hash("got: ", post_hash); + print_hash("expected:", expected_new_hash); + + if (memcmp(post_hash, expected_new_hash, SHA256_BYTES) != 0) { + printf("HASH-MISMATCH nach Flash-Write — bleibe stehen, springe nicht.\n"); + halt_with_led(); + } + printf("Update verifiziert. \\o/\n"); + +boot: + if (!app_present(APP_BASE)) { + printf("Kein gueltiges App-Image bei 0x%08x — bleibe stehen\n", + (unsigned)APP_BASE); + halt_with_led(); + } + printf("Springe in die App...\n"); + sleep_ms(100); + jump_to_app(APP_BASE); +} diff --git a/bootloader/lwipopts.h b/bootloader/lwipopts.h new file mode 100644 index 0000000..fafb9ee --- /dev/null +++ b/bootloader/lwipopts.h @@ -0,0 +1,46 @@ +#ifndef _LWIPOPTS_H +#define _LWIPOPTS_H + +// lwIP-Konfig fuer pico_cyw43_arch_lwip_threadsafe_background. +// Werte uebernommen aus pico-examples/pico_w/wifi/lwipopts_examples_common.h. + +#define NO_SYS 1 +#define LWIP_SOCKET 0 +#define MEM_LIBC_MALLOC 0 +#define MEM_ALIGNMENT 4 +#define MEM_SIZE 4000 +#define MEMP_NUM_TCP_SEG 32 +#define MEMP_NUM_ARP_QUEUE 10 +#define PBUF_POOL_SIZE 24 + +#define LWIP_ARP 1 +#define LWIP_ETHERNET 1 +#define LWIP_ICMP 1 +#define LWIP_RAW 1 +#define LWIP_DHCP 1 +#define LWIP_IPV4 1 +#define LWIP_TCP 1 +#define LWIP_UDP 1 +#define LWIP_DNS 1 +#define LWIP_TCP_KEEPALIVE 1 +#define LWIP_NETCONN 0 + +#define TCP_MSS 1460 +#define TCP_WND (8 * TCP_MSS) +#define TCP_SND_BUF (8 * TCP_MSS) +#define TCP_SND_QUEUELEN ((4 * (TCP_SND_BUF) + (TCP_MSS - 1)) / (TCP_MSS)) + +#define LWIP_NETIF_STATUS_CALLBACK 1 +#define LWIP_NETIF_LINK_CALLBACK 1 +#define LWIP_NETIF_HOSTNAME 1 +#define LWIP_NETIF_TX_SINGLE_PBUF 1 +#define DHCP_DOES_ARP_CHECK 0 +#define LWIP_DHCP_DOES_ACD_CHECK 0 +#define LWIP_CHKSUM_ALGORITHM 3 + +#define MEM_STATS 0 +#define SYS_STATS 0 +#define MEMP_STATS 0 +#define LINK_STATS 0 + +#endif diff --git a/bootloader/memmap_bootloader.ld b/bootloader/memmap_bootloader.ld new file mode 100644 index 0000000..7d70ae4 --- /dev/null +++ b/bootloader/memmap_bootloader.ld @@ -0,0 +1,304 @@ +/* Based on GCC ARM embedded samples. + Defines the following symbols for use by code: + __exidx_start + __exidx_end + __etext + __data_start__ + __preinit_array_start + __preinit_array_end + __init_array_start + __init_array_end + __fini_array_start + __fini_array_end + __data_end__ + __bss_start__ + __bss_end__ + __end__ + end + __HeapLimit + __StackLimit + __StackTop + __stack (== StackTop) +*/ + +MEMORY +{ + /* Bootloader sitzt am Flash-Anfang und bekommt fix 512 KB. + Alles ab 0x10080000 gehoert der App. */ + FLASH(rx) : ORIGIN = 0x10000000, LENGTH = 512k + RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 512k + SCRATCH_X(rwx) : ORIGIN = 0x20080000, LENGTH = 4k + SCRATCH_Y(rwx) : ORIGIN = 0x20081000, LENGTH = 4k +} + +ENTRY(_entry_point) + +SECTIONS +{ + .flash_begin : { + __flash_binary_start = .; + } > FLASH + + /* The bootrom will enter the image at the point indicated in your + IMAGE_DEF, which is usually the reset handler of your vector table. + + The debugger will use the ELF entry point, which is the _entry_point + symbol, and in our case is *different from the bootrom's entry point.* + This is used to go back through the bootrom on debugger launches only, + to perform the same initial flash setup that would be performed on a + cold boot. + */ + + .text : { + __logical_binary_start = .; + KEEP (*(.vectors)) + KEEP (*(.binary_info_header)) + __binary_info_header_end = .; + KEEP (*(.embedded_block)) + __embedded_block_end = .; + KEEP (*(.reset)) + /* TODO revisit this now memset/memcpy/float in ROM */ + /* bit of a hack right now to exclude all floating point and time critical (e.g. memset, memcpy) code from + * FLASH ... we will include any thing excluded here in .data below by default */ + *(.init) + *libgcc.a:cmse_nonsecure_call.o + *(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a:) .text*) + *(.fini) + /* Pull all c'tors into .text */ + *crtbegin.o(.ctors) + *crtbegin?.o(.ctors) + *(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors) + *(SORT(.ctors.*)) + *(.ctors) + /* Followed by destructors */ + *crtbegin.o(.dtors) + *crtbegin?.o(.dtors) + *(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors) + *(SORT(.dtors.*)) + *(.dtors) + + . = ALIGN(4); + /* preinit data */ + PROVIDE_HIDDEN (__preinit_array_start = .); + KEEP(*(SORT(.preinit_array.*))) + KEEP(*(.preinit_array)) + PROVIDE_HIDDEN (__preinit_array_end = .); + + . = ALIGN(4); + /* init data */ + PROVIDE_HIDDEN (__init_array_start = .); + KEEP(*(SORT(.init_array.*))) + KEEP(*(.init_array)) + PROVIDE_HIDDEN (__init_array_end = .); + + . = ALIGN(4); + /* finit data */ + PROVIDE_HIDDEN (__fini_array_start = .); + *(SORT(.fini_array.*)) + *(.fini_array) + PROVIDE_HIDDEN (__fini_array_end = .); + + *(.eh_frame*) + . = ALIGN(4); + } > FLASH + + /* Note the boot2 section is optional, and should be discarded if there is + no reference to it *inside* the binary, as it is not called by the + bootrom. (The bootrom performs a simple best-effort XIP setup and + leaves it to the binary to do anything more sophisticated.) However + there is still a size limit of 256 bytes, to ensure the boot2 can be + stored in boot RAM. + + Really this is a "XIP setup function" -- the name boot2 is historic and + refers to its dual-purpose on RP2040, where it also handled vectoring + from the bootrom into the user image. + */ + + .boot2 : { + __boot2_start__ = .; + *(.boot2) + __boot2_end__ = .; + } > FLASH + + ASSERT(__boot2_end__ - __boot2_start__ <= 256, + "ERROR: Pico second stage bootloader must be no more than 256 bytes in size") + + .rodata : { + *(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a:) .rodata*) + *(.srodata*) + . = ALIGN(4); + *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.flashdata*))) + . = ALIGN(4); + } > FLASH + + .ARM.extab : + { + *(.ARM.extab* .gnu.linkonce.armextab.*) + } > FLASH + + __exidx_start = .; + .ARM.exidx : + { + *(.ARM.exidx* .gnu.linkonce.armexidx.*) + } > FLASH + __exidx_end = .; + + /* Machine inspectable binary information */ + . = ALIGN(4); + __binary_info_start = .; + .binary_info : + { + KEEP(*(.binary_info.keep.*)) + *(.binary_info.*) + } > FLASH + __binary_info_end = .; + . = ALIGN(4); + + .ram_vector_table (NOLOAD): { + *(.ram_vector_table) + } > RAM + + .uninitialized_data (NOLOAD): { + . = ALIGN(4); + *(.uninitialized_data*) + } > RAM + + .data : { + __data_start__ = .; + *(vtable) + + *(.time_critical*) + + /* remaining .text and .rodata; i.e. stuff we exclude above because we want it in RAM */ + *(.text*) + . = ALIGN(4); + *(.rodata*) + . = ALIGN(4); + + *(.data*) + *(.sdata*) + + . = ALIGN(4); + *(.after_data.*) + . = ALIGN(4); + /* preinit data */ + PROVIDE_HIDDEN (__mutex_array_start = .); + KEEP(*(SORT(.mutex_array.*))) + KEEP(*(.mutex_array)) + PROVIDE_HIDDEN (__mutex_array_end = .); + + *(.jcr) + . = ALIGN(4); + } > RAM AT> FLASH + + .tdata : { + . = ALIGN(4); + *(.tdata .tdata.* .gnu.linkonce.td.*) + /* All data end */ + __tdata_end = .; + } > RAM AT> FLASH + PROVIDE(__data_end__ = .); + + /* __etext is (for backwards compatibility) the name of the .data init source pointer (...) */ + __etext = LOADADDR(.data); + + .tbss (NOLOAD) : { + . = ALIGN(4); + __bss_start__ = .; + __tls_base = .; + *(.tbss .tbss.* .gnu.linkonce.tb.*) + *(.tcommon) + + __tls_end = .; + } > RAM + + .bss (NOLOAD) : { + . = ALIGN(4); + __tbss_end = .; + + *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.bss*))) + *(COMMON) + PROVIDE(__global_pointer$ = . + 2K); + *(.sbss*) + . = ALIGN(4); + __bss_end__ = .; + } > RAM + + .heap (NOLOAD): + { + __end__ = .; + end = __end__; + KEEP(*(.heap*)) + } > RAM + /* historically on GCC sbrk was growing past __HeapLimit to __StackLimit, however + to be more compatible, we now set __HeapLimit explicitly to where the end of the heap is */ + __HeapLimit = ORIGIN(RAM) + LENGTH(RAM); + + /* Start and end symbols must be word-aligned */ + .scratch_x : { + __scratch_x_start__ = .; + *(.scratch_x.*) + . = ALIGN(4); + __scratch_x_end__ = .; + } > SCRATCH_X AT > FLASH + __scratch_x_source__ = LOADADDR(.scratch_x); + + .scratch_y : { + __scratch_y_start__ = .; + *(.scratch_y.*) + . = ALIGN(4); + __scratch_y_end__ = .; + } > SCRATCH_Y AT > FLASH + __scratch_y_source__ = LOADADDR(.scratch_y); + + /* .stack*_dummy section doesn't contains any symbols. It is only + * used for linker to calculate size of stack sections, and assign + * values to stack symbols later + * + * stack1 section may be empty/missing if platform_launch_core1 is not used */ + + /* by default we put core 0 stack at the end of scratch Y, so that if core 1 + * stack is not used then all of SCRATCH_X is free. + */ + .stack1_dummy (NOLOAD): + { + *(.stack1*) + } > SCRATCH_X + .stack_dummy (NOLOAD): + { + KEEP(*(.stack*)) + } > SCRATCH_Y + + .flash_end : { + KEEP(*(.embedded_end_block*)) + PROVIDE(__flash_binary_end = .); + } > FLASH =0xaa + + /* stack limit is poorly named, but historically is maximum heap ptr */ + __StackLimit = ORIGIN(RAM) + LENGTH(RAM); + __StackOneTop = ORIGIN(SCRATCH_X) + LENGTH(SCRATCH_X); + __StackTop = ORIGIN(SCRATCH_Y) + LENGTH(SCRATCH_Y); + __StackOneBottom = __StackOneTop - SIZEOF(.stack1_dummy); + __StackBottom = __StackTop - SIZEOF(.stack_dummy); + PROVIDE(__stack = __StackTop); + + /* picolibc and LLVM */ + PROVIDE (__heap_start = __end__); + PROVIDE (__heap_end = __HeapLimit); + PROVIDE( __tls_align = MAX(ALIGNOF(.tdata), ALIGNOF(.tbss)) ); + PROVIDE( __tls_size_align = (__tls_size + __tls_align - 1) & ~(__tls_align - 1)); + PROVIDE( __arm32_tls_tcb_offset = MAX(8, __tls_align) ); + + /* llvm-libc */ + PROVIDE (_end = __end__); + PROVIDE (__llvm_libc_heap_limit = __HeapLimit); + + /* Check if data + heap + stack exceeds RAM limit */ + ASSERT(__StackLimit >= __HeapLimit, "region RAM overflowed") + + ASSERT( __binary_info_header_end - __logical_binary_start <= 1024, "Binary info must be in first 1024 bytes of the binary") + ASSERT( __embedded_block_end - __logical_binary_start <= 4096, "Embedded block must be in first 4096 bytes of the binary") + + /* todo assert on extra code */ +} + diff --git a/bootloader/pico_sdk_import.cmake b/bootloader/pico_sdk_import.cmake new file mode 100644 index 0000000..65f8a6f --- /dev/null +++ b/bootloader/pico_sdk_import.cmake @@ -0,0 +1,73 @@ +# This is a copy of /external/pico_sdk_import.cmake + +# This can be dropped into an external project to help locate this SDK +# It should be include()ed prior to project() + +if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) + set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) + message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) + set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) + message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) + set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) + message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") +endif () + +set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") +set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") +set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") + +if (NOT PICO_SDK_PATH) + if (PICO_SDK_FETCH_FROM_GIT) + include(FetchContent) + set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) + if (PICO_SDK_FETCH_FROM_GIT_PATH) + get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") + endif () + # GIT_SUBMODULES_RECURSE was added in 3.17 + if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG master + GIT_SUBMODULES_RECURSE FALSE + ) + else () + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG master + ) + endif () + + if (NOT pico_sdk) + message("Downloading Raspberry Pi Pico SDK") + FetchContent_Populate(pico_sdk) + set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) + endif () + set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) + else () + message(FATAL_ERROR + "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." + ) + endif () +endif () + +get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") +if (NOT EXISTS ${PICO_SDK_PATH}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") +endif () + +set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) +if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") +endif () + +set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) + +include(${PICO_SDK_INIT_CMAKE_FILE}) diff --git a/flashwrite/CMakeLists.txt b/flashwrite/CMakeLists.txt new file mode 100644 index 0000000..8c02e37 --- /dev/null +++ b/flashwrite/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.13) + +include(pico_sdk_import.cmake) + +project(flashwrite C CXX ASM) + +pico_sdk_init() + +add_executable(flashwrite + flashwrite.c +) + +target_link_libraries(flashwrite + pico_stdlib + pico_cyw43_arch_none + hardware_flash + hardware_sync +) + +pico_enable_stdio_usb(flashwrite 1) +pico_enable_stdio_uart(flashwrite 0) + +pico_add_extra_outputs(flashwrite) diff --git a/flashwrite/flashwrite.c b/flashwrite/flashwrite.c new file mode 100644 index 0000000..c5d9f6f --- /dev/null +++ b/flashwrite/flashwrite.c @@ -0,0 +1,96 @@ +#include +#include + +#include "pico/stdlib.h" +#include "pico/cyw43_arch.h" +#include "hardware/flash.h" +#include "hardware/sync.h" + +// Test-Sektor: weit weg von allem anderen. Pico 2W hat 4 MB Flash. +// 0xE0000 = 896 KB Offset. Wuerde im OTA-Layout knapp hinter der App liegen, +// hier aber unbenutzt. +#define TEST_FLASH_OFFSET 0xE0000u +#define TEST_FLASH_ADDR (XIP_BASE + TEST_FLASH_OFFSET) + +static void blink_forever(uint32_t on_ms, uint32_t off_ms) { + while (true) { + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, true); + sleep_ms(on_ms); + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, false); + sleep_ms(off_ms); + } +} + +static void hexdump16(const uint8_t *p) { + for (int i = 0; i < 16; i++) { + printf("%02x ", p[i]); + } + printf("\n"); +} + +int main(void) { + stdio_init_all(); + sleep_ms(3000); // Host-Terminal Zeit zum Verbinden + + if (cyw43_arch_init()) { + while (true) { + tight_loop_contents(); + } + } + + printf("\n=== flashwrite sanity test ===\n"); + printf("Test-Sektor: XIP-Adresse 0x%08x (Flash-Offset 0x%05x)\n", + (unsigned)TEST_FLASH_ADDR, (unsigned)TEST_FLASH_OFFSET); + printf("FLASH_SECTOR_SIZE=%u FLASH_PAGE_SIZE=%u\n", + (unsigned)FLASH_SECTOR_SIZE, (unsigned)FLASH_PAGE_SIZE); + + const uint8_t *flash = (const uint8_t *)TEST_FLASH_ADDR; + + printf("Vor erase: "); + hexdump16(flash); + + printf("Erase 4 KB...\n"); + uint32_t irq = save_and_disable_interrupts(); + flash_range_erase(TEST_FLASH_OFFSET, FLASH_SECTOR_SIZE); + restore_interrupts(irq); + + printf("Nach erase: "); + hexdump16(flash); + + for (uint32_t i = 0; i < FLASH_SECTOR_SIZE; i++) { + if (flash[i] != 0xFF) { + printf("FEHLER: Erase unvollstaendig bei +%lu: 0x%02x\n", i, flash[i]); + blink_forever(50, 50); + } + } + printf("Erase verifiziert (alle 4096 Byte = 0xFF).\n"); + + uint8_t pattern[FLASH_PAGE_SIZE]; + for (uint32_t i = 0; i < FLASH_PAGE_SIZE; i++) { + pattern[i] = (uint8_t)(i ^ 0x5A); + } + + printf("Program 256 Byte...\n"); + irq = save_and_disable_interrupts(); + flash_range_program(TEST_FLASH_OFFSET, pattern, FLASH_PAGE_SIZE); + restore_interrupts(irq); + + printf("Nach program: "); + hexdump16(flash); + + if (memcmp(flash, pattern, FLASH_PAGE_SIZE) == 0) { + printf("ERFOLG: Pattern korrekt im Flash.\n"); + printf("LED: schnelles Blinky 200/200.\n"); + blink_forever(200, 200); + } else { + printf("FEHLER: Pattern-Mismatch.\n"); + for (uint32_t i = 0; i < FLASH_PAGE_SIZE; i++) { + if (flash[i] != pattern[i]) { + printf(" Diff bei +%lu: got 0x%02x, expected 0x%02x\n", + i, flash[i], pattern[i]); + break; + } + } + blink_forever(50, 50); + } +} diff --git a/flashwrite/pico_sdk_import.cmake b/flashwrite/pico_sdk_import.cmake new file mode 100644 index 0000000..65f8a6f --- /dev/null +++ b/flashwrite/pico_sdk_import.cmake @@ -0,0 +1,73 @@ +# This is a copy of /external/pico_sdk_import.cmake + +# This can be dropped into an external project to help locate this SDK +# It should be include()ed prior to project() + +if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) + set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) + message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) + set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) + message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) + set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) + message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") +endif () + +set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") +set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") +set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") + +if (NOT PICO_SDK_PATH) + if (PICO_SDK_FETCH_FROM_GIT) + include(FetchContent) + set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) + if (PICO_SDK_FETCH_FROM_GIT_PATH) + get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") + endif () + # GIT_SUBMODULES_RECURSE was added in 3.17 + if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG master + GIT_SUBMODULES_RECURSE FALSE + ) + else () + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG master + ) + endif () + + if (NOT pico_sdk) + message("Downloading Raspberry Pi Pico SDK") + FetchContent_Populate(pico_sdk) + set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) + endif () + set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) + else () + message(FATAL_ERROR + "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." + ) + endif () +endif () + +get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") +if (NOT EXISTS ${PICO_SDK_PATH}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") +endif () + +set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) +if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") +endif () + +set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) + +include(${PICO_SDK_INIT_CMAKE_FILE}) diff --git a/server/firmware/pico_1.bin b/server/firmware/pico_1.bin new file mode 100755 index 0000000..25af35d Binary files /dev/null and b/server/firmware/pico_1.bin differ diff --git a/server/firmware/pico_2.bin b/server/firmware/pico_2.bin new file mode 100755 index 0000000..a31823c Binary files /dev/null and b/server/firmware/pico_2.bin differ diff --git a/server/server.py b/server/server.py new file mode 100644 index 0000000..7114b8d --- /dev/null +++ b/server/server.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +""" +OTA-Update-Server fuer den Pico-Bootloader. + +Wire-Protokoll: + Pico -> Server (40 bytes): + [0:4] magic 0xC0FFEE01 (LE) + [4:8] pico_id (LE u32) + [8:40] SHA256 des aktuellen App-Slots (3.5 MB, 0xFF-gepaddet) + + Server -> Pico: + [0:1] status: 0x00 = no update, 0x01 = update follows + falls status == 0x01: + [1:5] size der Binary in bytes (LE u32) + [5:37] SHA256 des neuen App-Slots (auch 0xFF-gepaddet auf 3.5 MB) + [37:] Binary-Payload (size Bytes) +""" + +import hashlib +import socket +import struct +import sys +from pathlib import Path + +MAGIC = 0xC0FFEE01 +APP_SLOT_SIZE = 3584 * 1024 # 3.5 MB +SHA256_SIZE = 32 +REQUEST_SIZE = 4 + 4 + SHA256_SIZE # 40 + +STATUS_NO_UPDATE = 0x00 +STATUS_UPDATE = 0x01 + +LISTEN_HOST = "0.0.0.0" +LISTEN_PORT = 9000 +RECV_TIMEOUT_S = 5.0 + +FIRMWARE_DIR = Path(__file__).parent / "firmware" + +# pico_id -> binary path. Picos, die hier nicht gelistet sind, bekommen +# immer status=no_update (server kennt das Geraet nicht). +MANIFEST = { + 1: FIRMWARE_DIR / "pico_2.bin", + 2: FIRMWARE_DIR / "pico_1.bin", +} + + +def slot_hash(binary_path: Path) -> bytes: + """Hash der Datei, gepaddet mit 0xFF bis 3.5 MB — exakt das, + was der Pico auf seinem App-Slot berechnet.""" + data = binary_path.read_bytes() + if len(data) > APP_SLOT_SIZE: + raise ValueError( + f"{binary_path}: {len(data)} bytes > app slot ({APP_SLOT_SIZE})" + ) + padded = data + b"\xff" * (APP_SLOT_SIZE - len(data)) + return hashlib.sha256(padded).digest() + + +def recv_exact(conn: socket.socket, n: int) -> bytes: + buf = b"" + while len(buf) < n: + chunk = conn.recv(n - len(buf)) + if not chunk: + raise ConnectionError( + f"Verbindung geschlossen nach {len(buf)}/{n} Bytes" + ) + buf += chunk + return buf + + +def handle_client(conn: socket.socket, addr) -> None: + conn.settimeout(RECV_TIMEOUT_S) + + try: + request = recv_exact(conn, REQUEST_SIZE) + except (ConnectionError, socket.timeout) as exc: + print(f"[{addr}] Request-Abbruch: {exc}") + return + + magic, pico_id = struct.unpack(" no_update") + conn.sendall(bytes([STATUS_NO_UPDATE])) + return + + if not binary_path.exists(): + print(f"[{addr}] Manifest-Eintrag fehlt: {binary_path} -> no_update") + conn.sendall(bytes([STATUS_NO_UPDATE])) + return + + target_hash = slot_hash(binary_path) + + if current_hash == target_hash: + print(f"[{addr}] aktuell -> no_update") + conn.sendall(bytes([STATUS_NO_UPDATE])) + return + + binary = binary_path.read_bytes() + print( + f"[{addr}] update -> {binary_path.name} ({len(binary)} bytes, " + f"hash {target_hash.hex()[:16]}...)" + ) + conn.sendall(bytes([STATUS_UPDATE])) + conn.sendall(struct.pack(" int: + if not FIRMWARE_DIR.exists(): + FIRMWARE_DIR.mkdir(parents=True) + print(f"firmware/ angelegt: {FIRMWARE_DIR}") + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as srv: + srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + srv.bind((LISTEN_HOST, LISTEN_PORT)) + srv.listen(4) + print(f"OTA-Server hoert auf {LISTEN_HOST}:{LISTEN_PORT}") + print(f"Manifest: {[(k, str(v.name)) for k, v in MANIFEST.items()]}") + try: + while True: + conn, addr = srv.accept() + try: + handle_client(conn, addr) + except Exception as exc: + print(f"[{addr}] Exception: {exc}") + finally: + conn.close() + except KeyboardInterrupt: + print("\nbeendet") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/server/test_client.py b/server/test_client.py new file mode 100644 index 0000000..e73d847 --- /dev/null +++ b/server/test_client.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +""" +Simuliert einen Pico, der den OTA-Server anfragt. Damit testen wir das +Protokoll ohne echtes Geraet. + +Usage: + test_client.py [--current-binary PATH] [--host HOST] [--port PORT] + +Beispiele: + # so tun, als laege auf dem Pico ein leerer Slot (alles 0xFF): + test_client.py 1 + + # so tun, als laege auf dem Pico schon pico_1.bin: + test_client.py 1 --current-binary firmware/pico_1.bin + + # unbekannte Pico-ID: + test_client.py 99 +""" + +import argparse +import hashlib +import socket +import struct +import sys +from pathlib import Path + +MAGIC = 0xC0FFEE01 +APP_SLOT_SIZE = 3584 * 1024 +SHA256_SIZE = 32 + + +def slot_hash(binary_path: Path | None) -> bytes: + if binary_path is None: + # Leerer Slot: 3.5 MB 0xFF. + padded = b"\xff" * APP_SLOT_SIZE + else: + data = binary_path.read_bytes() + if len(data) > APP_SLOT_SIZE: + raise ValueError(f"{binary_path}: zu gross ({len(data)})") + padded = data + b"\xff" * (APP_SLOT_SIZE - len(data)) + return hashlib.sha256(padded).digest() + + +def recv_exact(sock: socket.socket, n: int) -> bytes: + buf = b"" + while len(buf) < n: + chunk = sock.recv(n - len(buf)) + if not chunk: + raise ConnectionError(f"Server-Disconnect nach {len(buf)}/{n}") + buf += chunk + return buf + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("pico_id", type=int) + ap.add_argument("--current-binary", type=Path, default=None, + help="Pfad einer Binary, die wir 'als bereits installiert' simulieren") + ap.add_argument("--host", default="127.0.0.1") + ap.add_argument("--port", type=int, default=9000) + ap.add_argument("--save-update", type=Path, default=None, + help="Falls Server ein Update sendet, hier speichern") + args = ap.parse_args() + + current_hash = slot_hash(args.current_binary) + print(f"-> pico_id={args.pico_id} hash={current_hash.hex()[:16]}...") + + req = struct.pack("