@adcockm, I’ll probably fork the work of @adwuard (because I’ve seen a display error when the file name is too large, so just set a limit of display length). In the meantime, the two files main.c and memmap_sdcard_app.ld that I modified yet below:
main.c (only changed made at line 166)
/**
* PicoCalc SD Firmware Loader
*
* Author: Hsuan Han Lai
* Email: hsuan.han.lai@gmail.com
* Website: https://hsuanhanlai.com
* Year: 2025
*
*
* This project is a bootloader for the PicoCalc device, designed to load and execute
* firmware applications from an SD card.
*
*/
#include <stdio.h>
#include <string.h>
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "hardware/clocks.h"
#include "debug.h"
#include "i2ckbd.h"
#include "lcdspi.h"
#include <hardware/flash.h>
#include <errno.h>
#include <hardware/watchdog.h>
#include "config.h"
#include "blockdevice/sd.h"
#include "filesystem/fat.h"
#include "filesystem/vfs.h"
#include "text_directory_ui.h"
#include "key_event.h"
const uint LEDPIN = 25;
bool sd_card_inserted(void)
{
// Active low detection - returns true when pin is low
return !gpio_get(SD_DET_PIN);
}
bool fs_init(void)
{
DEBUG_PRINT("fs init SD\n");
blockdevice_t *sd = blockdevice_sd_create(spi0,
SD_MOSI_PIN,
SD_MISO_PIN,
SD_SCLK_PIN,
SD_CS_PIN,
125000000 / 2 / 4, // 15.6MHz
true);
filesystem_t *fat = filesystem_fat_create();
int err = fs_mount("/sd", fat, sd);
if (err == -1)
{
DEBUG_PRINT("format /sd\n");
err = fs_format(fat, sd);
if (err == -1)
{
DEBUG_PRINT("format err: %s\n", strerror(errno));
return false;
}
err = fs_mount("/sd", fat, sd);
if (err == -1)
{
DEBUG_PRINT("mount err: %s\n", strerror(errno));
return false;
}
}
return true;
}
static bool __not_in_flash_func(is_same_existing_program)(FILE *fp)
{
uint8_t buffer[FLASH_SECTOR_SIZE] = {0};
size_t program_size = 0;
size_t len = 0;
while ((len = fread(buffer, 1, sizeof(buffer), fp)) > 0)
{
uint8_t *flash = (uint8_t *)(XIP_BASE + SD_BOOT_FLASH_OFFSET + program_size);
if (memcmp(buffer, flash, len) != 0)
return false;
program_size += len;
}
return true;
}
// This function must run from RAM since it erases and programs flash memory
static bool __not_in_flash_func(load_program)(const char *filename)
{
FILE *fp = fopen(filename, "r");
if (fp == NULL)
{
DEBUG_PRINT("open %s fail: %s\n", filename, strerror(errno));
return false;
}
if (is_same_existing_program(fp))
{
// Program is up to date
}
// Check file size to ensure it doesn't exceed the available flash space
if (fseek(fp, 0, SEEK_END) == -1)
{
DEBUG_PRINT("seek err: %s\n", strerror(errno));
fclose(fp);
return false;
}
long file_size = ftell(fp);
if (file_size <= 0)
{
DEBUG_PRINT("invalid size: %ld\n", file_size);
fclose(fp);
return false;
}
if (file_size > MAX_APP_SIZE)
{
DEBUG_PRINT("file too large: %ld > %d\n", file_size, MAX_APP_SIZE);
fclose(fp);
return false;
}
DEBUG_PRINT("updating: %ld bytes\n", file_size);
if (fseek(fp, 0, SEEK_SET) == -1)
{
DEBUG_PRINT("seek err: %s\n", strerror(errno));
fclose(fp);
return false;
}
size_t program_size = 0;
uint8_t buffer[FLASH_SECTOR_SIZE] = {0};
size_t len = 0;
// Erase and program flash in FLASH_SECTOR_SIZE chunks
while ((len = fread(buffer, 1, sizeof(buffer), fp)) > 0)
{
// Ensure we don't write beyond the application area
if ((program_size + len) > MAX_APP_SIZE)
{
DEBUG_PRINT("err: write beyond app area\n");
fclose(fp);
return false;
}
uint32_t ints = save_and_disable_interrupts();
flash_range_erase(SD_BOOT_FLASH_OFFSET + program_size, FLASH_SECTOR_SIZE);
flash_range_program(SD_BOOT_FLASH_OFFSET + program_size, buffer, len);
restore_interrupts(ints);
program_size += len;
}
DEBUG_PRINT("program loaded\n");
fclose(fp);
return true;
}
// This function jumps to the application entry point
// It must update the vector table and stack pointer before jumping
void __not_in_flash_func(launch_application_from)(uint32_t *app_location)
{
// https://vanhunteradams.com/Pico/Bootloader/Bootloader.html
uint32_t *new_vector_table = app_location;
volatile uint32_t *vtor = (uint32_t *)(PPB_BASE + M33_VTOR_OFFSET);
*vtor = (uint32_t)new_vector_table;
asm volatile(
"msr msp, %0\n"
"bx %1\n"
:
: "r"(new_vector_table[0]), "r"(new_vector_table[1])
:);
}
// Check if a valid application exists in flash by examining the vector table
static bool is_valid_application(uint32_t *app_location)
{
// Check that the initial stack pointer is within a plausible RAM region (assumed range for Pico: 0x20000000 to 0x20040000)
uint32_t stack_pointer = app_location[0];
if (stack_pointer < 0x20000000 || stack_pointer > 0x20040000)
{
return false;
}
// Check that the reset vector is within the valid flash application area
uint32_t reset_vector = app_location[1];
if (reset_vector < (0x10000000 + SD_BOOT_FLASH_OFFSET) || reset_vector > (0x10000000 + PICO_FLASH_SIZE_BYTES))
{
return false;
}
return true;
}
int load_firmware_by_path(const char *path)
{
text_directory_ui_set_status("STAT: loading app...");
// Attempt to load the application from the SD card
// bool load_success = load_program(FIRMWARE_PATH);
bool load_success = load_program(path);
// Get the pointer to the application flash area
uint32_t *app_location = (uint32_t *)(XIP_BASE + SD_BOOT_FLASH_OFFSET);
// Check if there is an already valid application in flash
bool has_valid_app = is_valid_application(app_location);
if (load_success || has_valid_app)
{
text_directory_ui_set_status("STAT: launching app...");
DEBUG_PRINT("launching app\n");
// Small delay to allow printf to complete
sleep_ms(100);
launch_application_from(app_location);
}
else
{
text_directory_ui_set_status("ERR: No valid app");
DEBUG_PRINT("no valid app, halting\n");
sleep_ms(2000);
// Trigger a watchdog reboot
watchdog_reboot(0, 0, 0);
}
// We should never reach here
while (1)
{
tight_loop_contents();
}
}
void final_selection_callback(const char *path)
{
// Trigger firmware loading with the selected path
DEBUG_PRINT("selected: %s\n", path);
char status_message[128];
const char *extension = ".bin";
size_t path_len = strlen(path);
size_t ext_len = strlen(extension);
if (path_len < ext_len || strcmp(path + path_len - ext_len, extension) != 0)
{
DEBUG_PRINT("not a bin: %s\n", path);
snprintf(status_message, sizeof(status_message), "Err: FILE is not a .bin file");
text_directory_ui_set_status(status_message);
return;
}
snprintf(status_message, sizeof(status_message), "SEL: %s", path);
text_directory_ui_set_status(status_message);
sleep_ms(200);
load_firmware_by_path(path);
}
int main()
{
char buf[64];
stdio_init_all();
uart_init(uart0, 115200);
uart_set_format(uart0, 8, 1, UART_PARITY_NONE); // 8-N-1
uart_set_fifo_enabled(uart0, false);
// Initialize SD card detection pin
gpio_init(SD_DET_PIN);
gpio_set_dir(SD_DET_PIN, GPIO_IN);
gpio_pull_up(SD_DET_PIN); // Enable pull-up resistor
keypad_init();
lcd_init();
lcd_clear();
text_directory_ui_init();
// Check for SD card presence
DEBUG_PRINT("Checking for SD card...\n");
if (!sd_card_inserted())
{
DEBUG_PRINT("SD card not detected\n");
text_directory_ui_set_status("SD card not detected. \nPlease insert SD card.");
// Poll until SD card is inserted
while (!sd_card_inserted())
{
sleep_ms(100);
}
// Card detected, wait for it to stabilize
DEBUG_PRINT("SD card detected\n");
text_directory_ui_set_status("SD card detected. Mounting...");
sleep_ms(1500); // Wait for card to stabilize
}
else
{
// If SD card is detected at boot, wait for stabilization
DEBUG_PRINT("SD card stabilization delay on boot\n");
text_directory_ui_set_status("Stabilizing SD card...");
sleep_ms(1500); // Delay to allow the SD card to fully power up and stabilize
}
// Initialize filesystem
if (!fs_init())
{
text_directory_ui_set_status("Failed to mount SD card!");
DEBUG_PRINT("Failed to mount SD card\n");
sleep_ms(2000);
watchdog_reboot(0, 0, 0);
}
sleep_ms(500);
lcd_clear();
text_directory_ui_init();
text_directory_ui_set_final_callback(final_selection_callback);
text_directory_ui_run();
}
memmap_sdcard_app.ld:
MEMORY
{
FLASH(rx) : ORIGIN = 0x10000000 + 512k, LENGTH = 4096k - 512k
RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 512k
SCRATCH_X(rwx) : ORIGIN = 0x20040000, LENGTH = 4k
SCRATCH_Y(rwx) : ORIGIN = 0x20041000, LENGTH = 4k
}
ENTRY(_entry_point)
SECTIONS
{
/* Second stage bootloader is prepended to the image. It must be 256 bytes big
and checksummed. It is usually built by the boot_stage2 target
in the Raspberry Pi Pico SDK
*/
.flash_begin : {
__flash_binary_start = .;
} > FLASH
/* The second stage will always enter the image at the start of .text.
The debugger will use the ELF entry point, which is the _entry_point
symbol if present, otherwise defaults to start of .text.
This can be used to transfer control back to the bootrom on debugger
launches only, to perform proper flash setup.
*/
.text : {
__logical_binary_start = .;
KEEP (*(.vectors))
KEEP (*(.binary_info_header))
__binary_info_header_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)
*(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)
*(.eh_frame*)
. = ALIGN(4);
} > FLASH
.rodata : {
*(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a:) .rodata*)
. = 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
.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*)
. = 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 = .);
. = 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 = .);
*(.jcr)
. = ALIGN(4);
/* All data end */
__data_end__ = .;
} > RAM AT> FLASH
/* __etext is (for backwards compatibility) the name of the .data init source pointer (...) */
__etext = LOADADDR(.data);
.uninitialized_data (NOLOAD): {
. = ALIGN(4);
*(.uninitialized_data*)
} > 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);
.bss : {
. = ALIGN(4);
__bss_start__ = .;
*(SORT_BY_ALIGNMENT(SORT_BY_NAME(.bss*)))
*(COMMON)
. = ALIGN(4);
__bss_end__ = .;
} > RAM
.heap (NOLOAD):
{
__end__ = .;
end = __end__;
KEEP(*(.heap*))
__HeapLimit = .;
} > RAM
/* .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 : {
PROVIDE(__flash_binary_end = .);
} > FLASH
/* 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);
/* Check if data + heap + stack exceeds RAM limit */
ASSERT(__StackLimit >= __HeapLimit, "region RAM overflowed")
ASSERT( __binary_info_header_end - __logical_binary_start <= 512, "Binary info must be in first 512 bytes of the binary")
/* todo assert on extra code */
}
My CMakeLists.txt (for Pico 2 w)
cmake_minimum_required(VERSION 3.13...3.27)
set(PICO_PLATFORM rp2350)
set(PICO_BOARD pico2_w)
include(pico_sdk_import.cmake)
project(picocalc_sd_boot C CXX ASM)
set(CMAKE_INSTALL_PREFIX ${CMAKE_SOURCE_DIR} CACHE PATH "Install prefix set to project root" FORCE)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
pico_sdk_init()
include_directories(
${CMAKE_CURRENT_LIST_DIR}
)
add_subdirectory(i2ckbd)
add_subdirectory(lcdspi)
add_subdirectory(pico-vfs)
function(build_for_board board_name)
add_executable(picocalc_sd_boot_${board_name}
main.c
key_event.c
text_directory_ui.c
)
target_link_libraries(picocalc_sd_boot_${board_name}
pico_stdlib
hardware_sync
hardware_flash
hardware_irq
hardware_adc
hardware_pwm
hardware_i2c
hardware_spi
hardware_dma
hardware_exception
hardware_pio
pico_multicore
i2ckbd
lcdspi
blockdevice_sd
filesystem_fat
filesystem_vfs
)
pico_enable_stdio_usb(picocalc_sd_boot_${board_name} 0)
pico_enable_stdio_uart(picocalc_sd_boot_${board_name} 1)
pico_add_extra_outputs(picocalc_sd_boot_${board_name})
target_link_options(picocalc_sd_boot_${board_name} PRIVATE -Wl,--print-memory-usage)
# Define the output directory relative to the project root directory
set(output_dir prebuild_output/${board_name})
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/picocalc_sd_boot_${board_name}.elf
DESTINATION ${output_dir}
)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/picocalc_sd_boot_${board_name}.uf2
DESTINATION ${output_dir}
)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/picocalc_sd_boot_${board_name}.bin
DESTINATION ${output_dir}
)
endfunction()
# Build for Pico
#build_for_board(pico)
# Build for Pico 2
build_for_board(pico2_w)
# This function sets the custom linker script for SD card applications
# The memmap_sdcard_app.ld script configures the application to start at
# FLASH origin = 0x10000000 + 256k, which aligns with the SD_BOOT_FLASH_OFFSET
# defined in main.c (256 * 1024). This ensures the bootloader (first 256KB)
# is preserved when flashing the application.
function(enable_sdcard_app target)
pico_set_linker_script(${target} ${CMAKE_SOURCE_DIR}/memmap_sdcard_app.ld)
endfunction()
I hope to have some time tommorrow to try to compile @adcockm Webmite version.