summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke T. Shumaker <lukeshu@lukeshu.com>2025-02-22 04:07:17 -0700
committerLuke T. Shumaker <lukeshu@lukeshu.com>2025-04-05 21:34:27 -0600
commit9758d91ea795448689ec401570bf556b8107177c (patch)
tree3a29d91c3dfb3c7c580c87184a15b5bd54e55172
parentbd50e8b0f423d50928c129bc884385b50fb1756f (diff)
flash.bin: Fix reading, implement basic writing
m---------3rd-party/pico-sdk0
-rw-r--r--README.md5
-rw-r--r--cmd/sbc_harness/CMakeLists.txt2
-rw-r--r--cmd/sbc_harness/config/config.h2
-rw-r--r--cmd/sbc_harness/fs_harness_flash_bin.c310
-rw-r--r--cmd/sbc_harness/fs_harness_flash_bin.h30
-rw-r--r--cmd/sbc_harness/main.c7
-rw-r--r--cmd/sbc_harness/static/Documentation/harness_flash_bin.txt13
8 files changed, 362 insertions, 7 deletions
diff --git a/3rd-party/pico-sdk b/3rd-party/pico-sdk
-Subproject 1c00d64a4e0fdf948494c9aaf4d257b5739796a
+Subproject c8a16c00453e4db4b771d7f1281391057c7477d
diff --git a/README.md b/README.md
index de909d3..a1eebb9 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,10 @@ There are several ways of putting this firmware file onto the harness:
mount the device and copy the `.uf2` file to the device. It will
automatically reboot into the new firmware image.
2. debug port: Using OpenOCD (see `HACKING.md`), run the OpenOCD command
- `program /path/to/sbc_harness.elf reset`.
+ `program /path/to/sbc_harness.elf reset` (TODO: I don't really
+ understand what OpenOCD is doing that it wants the `.elf` instead
+ of the `.bin`)
+ .
If OpenOCD is not already running:
```
diff --git a/cmd/sbc_harness/CMakeLists.txt b/cmd/sbc_harness/CMakeLists.txt
index 115361f..678af07 100644
--- a/cmd/sbc_harness/CMakeLists.txt
+++ b/cmd/sbc_harness/CMakeLists.txt
@@ -12,6 +12,7 @@ add_library(sbc_harness_objs OBJECT
usb_keyboard.c
tusb_log.c
+ fs_harness_flash_bin.c
fs_harness_uptime_txt.c
)
target_include_directories(sbc_harness_objs PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/config)
@@ -22,6 +23,7 @@ target_link_libraries(sbc_harness_objs
pico_stdio_uart
hardware_flash
+ hardware_watchdog
libmisc
libfmt
diff --git a/cmd/sbc_harness/config/config.h b/cmd/sbc_harness/config/config.h
index da3edad..5e7bc06 100644
--- a/cmd/sbc_harness/config/config.h
+++ b/cmd/sbc_harness/config/config.h
@@ -9,6 +9,8 @@
#include <stddef.h> /* for size_t */
+#define CONFIG_FLASH_DEBUG 1
+
/* RP2040 *********************************************************************/
#define CONFIG_RP2040_SPI_DEBUG 1 /* bool */
diff --git a/cmd/sbc_harness/fs_harness_flash_bin.c b/cmd/sbc_harness/fs_harness_flash_bin.c
new file mode 100644
index 0000000..bdb8da4
--- /dev/null
+++ b/cmd/sbc_harness/fs_harness_flash_bin.c
@@ -0,0 +1,310 @@
+/* sbc_harness/fs_harness_flash_bin.c - 9P access to flash storage
+ *
+ * Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <hardware/flash.h>
+#include <hardware/watchdog.h>
+
+#define LOG_NAME FLASH
+#include <libmisc/log.h>
+
+#include <util9p/static.h>
+
+#define IMPLEMENTATION_FOR_FS_HARNESS_FLASH_BIN YES
+#include "fs_harness_flash_bin.h"
+
+LO_IMPLEMENTATION_C(lib9p_srv_file, struct flash_file, flash_file, static);
+
+LO_IMPLEMENTATION_H(lib9p_srv_fio, struct flash_file, flash_file);
+LO_IMPLEMENTATION_C(lib9p_srv_fio, struct flash_file, flash_file, static);
+
+#define DATA_START ((const char *)(XIP_NOALLOC_BASE))
+#define DATA_SIZE PICO_FLASH_SIZE_BYTES
+#define DATA_HSIZE (DATA_SIZE/2)
+static_assert(DATA_SIZE % FLASH_SECTOR_SIZE == 0);
+static_assert(DATA_HSIZE % FLASH_SECTOR_SIZE == 0);
+
+/* There are some memcpy()s (and memcmp()s?) in here that can (and
+ * arguably should) be replaced with SSI DMA. */
+
+/* ab_flash_* (mid-level utilities for our A/B write scheme) ******************/
+
+/**
+ * Copy the upper half of flash to the lower half of flash, then reboot.
+ *
+ * @param buf : a scratch buffer that is at least FLASH_SECTOR_SIZE
+ */
+[[noreturn]] static void __no_inline_not_in_flash_func(ab_flash_finalize)(uint8_t *buf) {
+ assert(buf);
+
+ infof("copying upper flash to lower flash...");
+
+ cr_save_and_disable_interrupts();
+
+ for (size_t off = 0; off < DATA_HSIZE; off += FLASH_SECTOR_SIZE) {
+ memcpy(buf, DATA_START+DATA_HSIZE+off, FLASH_SECTOR_SIZE);
+ if (memcmp(DATA_START+off, buf, FLASH_SECTOR_SIZE) == 0)
+ continue;
+ flash_range_erase(off, FLASH_SECTOR_SIZE);
+ flash_range_program(off, buf, FLASH_SECTOR_SIZE);
+ }
+
+ infof("rebooting...");
+
+ watchdog_reboot(0, 0, 300);
+
+ for (;;)
+ asm volatile ("nop");
+}
+
+/**
+ * Set the upper half of flash to all zero bytes.
+ *
+ * @param buf : a scratch buffer that is at least FLASH_SECTOR_SIZE
+ */
+static void ab_flash_initialize_zero(uint8_t *buf) {
+ assert(buf);
+
+ memset(buf, 0, FLASH_SECTOR_SIZE);
+
+ infof("zeroing upper flash...");
+ for (size_t off = DATA_HSIZE; off < DATA_SIZE; off += FLASH_SECTOR_SIZE) {
+ if (memcmp(buf, DATA_START+off, FLASH_SECTOR_SIZE) == 0)
+ continue;
+ bool saved = cr_save_and_disable_interrupts();
+ /* No need to `flash_range_erase()`; the way the flash
+ * works is that _erase() sets all bits to 1, and
+ * _program() sets some bits to 0. If we don't need
+ * any bits to be 1, then we can skip the
+ * _erase(). */
+ flash_range_program(off, buf, FLASH_SECTOR_SIZE);
+ cr_restore_interrupts(saved);
+ }
+ debugf("... zeroed");
+}
+
+/**
+ * Copy the lower half of flash to the upper half of flash.
+ *
+ * @param buf : a scratch buffer that is at least FLASH_SECTOR_SIZE
+ */
+static void ab_flash_initialize(uint8_t *buf) {
+ assert(buf);
+
+ infof("initializing upper flash...");
+ for (size_t off = 0; off < DATA_HSIZE; off += FLASH_SECTOR_SIZE) {
+ memcpy(buf, DATA_START+off, FLASH_SECTOR_SIZE);
+ if (memcmp(buf, DATA_START+DATA_HSIZE+off, FLASH_SECTOR_SIZE) == 0)
+ continue;
+ bool saved = cr_save_and_disable_interrupts();
+ flash_range_erase(DATA_HSIZE+off, FLASH_SECTOR_SIZE);
+ flash_range_program(DATA_HSIZE+off, buf, FLASH_SECTOR_SIZE);
+ cr_restore_interrupts(saved);
+ }
+ debugf("... initialized");
+}
+
+/**
+ * Write `dat` to flash sector `pos`+(DATA_SIZE/2) (i.e. `pos` is a
+ * sector in the lower half, but this function writes to the upper
+ * half).
+ *
+ * @param pos : start-position of the sector to write to, must be in the upper half of the flash
+ * @param dat : the FLASH_SECTOR_SIZE bytes to write
+ */
+static void ab_flash_write_sector(size_t pos, uint8_t *dat) {
+ assert(pos < DATA_HSIZE);
+ assert(pos % FLASH_SECTOR_SIZE == 0);
+ assert(dat);
+
+ pos += DATA_HSIZE;
+
+ infof("write flash sector @ %zu...", pos);
+ if (memcmp(dat, DATA_START+pos, FLASH_SECTOR_SIZE) != 0) {
+ bool saved = cr_save_and_disable_interrupts();
+ flash_range_erase(pos, FLASH_SECTOR_SIZE);
+ flash_range_program(pos, dat, FLASH_SECTOR_SIZE);
+ cr_restore_interrupts(saved);
+ }
+ debugf("... written");
+}
+
+/* srv_file *******************************************************************/
+
+static void flash_file_free(struct flash_file *self) {
+ assert(self);
+}
+static struct lib9p_qid flash_file_qid(struct flash_file *self) {
+ assert(self);
+
+ return (struct lib9p_qid){
+ .type = LIB9P_QT_FILE|LIB9P_QT_EXCL,
+ .vers = 1,
+ .path = self->pathnum,
+ };
+}
+
+static struct lib9p_stat flash_file_stat(struct flash_file *self, struct lib9p_srv_ctx *ctx) {
+ assert(self);
+ assert(ctx);
+
+ return (struct lib9p_stat){
+ .kern_type = 0,
+ .kern_dev = 0,
+ .file_qid = flash_file_qid(self),
+ .file_mode = LIB9P_DM_EXCL|0666,
+ .file_atime = UTIL9P_ATIME,
+ .file_mtime = UTIL9P_MTIME,
+ .file_size = DATA_SIZE,
+ .file_name = lib9p_str(self->name),
+ .file_owner_uid = lib9p_str("root"),
+ .file_owner_gid = lib9p_str("root"),
+ .file_last_modified_uid = lib9p_str("root"),
+ .file_extension = lib9p_str(NULL),
+ .file_owner_n_uid = 0,
+ .file_owner_n_gid = 0,
+ .file_last_modified_n_uid = 0,
+ };
+}
+static void flash_file_wstat(struct flash_file *self, struct lib9p_srv_ctx *ctx,
+ struct lib9p_stat) {
+ assert(self);
+ assert(ctx);
+
+ lib9p_error(&ctx->basectx, LINUX_EROFS, "read-only part of filesystem");
+}
+static void flash_file_remove(struct flash_file *self, struct lib9p_srv_ctx *ctx) {
+ assert(self);
+ assert(ctx);
+
+ lib9p_error(&ctx->basectx, LINUX_EROFS, "read-only part of filesystem");
+}
+
+LIB9P_SRV_NOTDIR(struct flash_file, flash_file);
+
+static lo_interface lib9p_srv_fio flash_file_fopen(struct flash_file *self, struct lib9p_srv_ctx *ctx,
+ bool rd, bool wr, bool trunc) {
+ assert(self);
+ assert(ctx);
+
+ if (rd) {
+ self->rbuf.ok = false;
+ }
+
+ if (wr) {
+ if (trunc) {
+ ab_flash_initialize_zero(self->wbuf.dat);
+ self->written = true;
+ } else {
+ ab_flash_initialize(self->wbuf.dat);
+ self->written = false;
+ }
+ self->wbuf.ok = false;
+ }
+
+ return lo_box_flash_file_as_lib9p_srv_fio(self);
+}
+
+/* srv_fio ********************************************************************/
+
+static uint32_t flash_file_iounit(struct flash_file *self) {
+ assert(self);
+ return FLASH_SECTOR_SIZE;
+}
+
+static void flash_file_iofree(struct flash_file *self) {
+ assert(self);
+
+ if (self->wbuf.ok)
+ ab_flash_write_sector(self->wbuf.pos, self->wbuf.dat);
+
+ if (self->written)
+ ab_flash_finalize(self->wbuf.dat);
+}
+
+static void flash_file_pread(struct flash_file *self, struct lib9p_srv_ctx *ctx,
+ uint32_t byte_count, uint64_t byte_offset,
+ struct iovec *ret) {
+ assert(self);
+ assert(ctx);
+ assert(ret);
+
+ if (byte_offset > DATA_SIZE) {
+ lib9p_error(&ctx->basectx,
+ LINUX_EINVAL, "offset is past the chip size");
+ return;
+ }
+
+ /* Assume that somewhere down the line the iovec we return
+ * will be passed to DMA. We don't want the DMA engine to hit
+ * (slow) XIP (for instance, this can cause reads/writes to
+ * the SSP to get out of sync with eachother), so copy the
+ * data to a buffer in (fast) RAM first. It's lame that the
+ * DMA engine can only have a DREQ on one side of the channel.
+ */
+ if (byte_offset == DATA_SIZE) {
+ *ret = (struct iovec){
+ .iov_len = 0,
+ };
+ return;
+ }
+ size_t sector_base = LM_ROUND_DOWN(byte_offset, FLASH_SECTOR_SIZE);
+ if (byte_offset + byte_count > sector_base + FLASH_SECTOR_SIZE)
+ byte_count = (sector_base + FLASH_SECTOR_SIZE) - byte_offset;
+ assert(byte_offset + byte_count <= DATA_SIZE);
+
+ if (!self->rbuf.ok || self->rbuf.pos != sector_base) {
+ self->rbuf.ok = true;
+ self->rbuf.pos = sector_base;
+ memcpy(self->rbuf.dat, DATA_START+sector_base, FLASH_SECTOR_SIZE);
+ }
+
+ *ret = (struct iovec){
+ .iov_base = &self->rbuf.dat[byte_offset-sector_base],
+ .iov_len = byte_count,
+ };
+}
+
+/* TODO: Short/corrupt writes are dangerous. This should either (1)
+ * check a checksum, (2) use uf2 instead of verbatim data, or (3) use
+ * ihex instead of verbatim data. */
+static uint32_t flash_file_pwrite(struct flash_file *self, struct lib9p_srv_ctx *ctx,
+ void *buf,
+ uint32_t byte_count,
+ uint64_t byte_offset) {
+ assert(self);
+ assert(ctx);
+
+ if (byte_offset > DATA_HSIZE) {
+ lib9p_error(&ctx->basectx,
+ LINUX_EINVAL, "offset is past half the chip size");
+ return 0;
+ }
+ if (byte_count == 0)
+ return 0;
+ if (byte_offset == DATA_HSIZE) {
+ lib9p_error(&ctx->basectx,
+ LINUX_EINVAL, "offset is at half the chip size");
+ return 0;
+ }
+
+ size_t sector_base = LM_ROUND_DOWN(byte_offset, FLASH_SECTOR_SIZE);
+ if (byte_offset + byte_count > sector_base + FLASH_SECTOR_SIZE)
+ byte_count = (sector_base + FLASH_SECTOR_SIZE) - byte_offset;
+ assert(byte_offset + byte_count < DATA_HSIZE);
+
+ if (self->wbuf.ok && self->wbuf.pos != sector_base)
+ ab_flash_write_sector(self->wbuf.pos, self->wbuf.dat);
+ if (!self->wbuf.ok || self->wbuf.pos != sector_base) {
+ self->wbuf.ok = true;
+ self->wbuf.pos = sector_base;
+ if (byte_count != FLASH_SECTOR_SIZE)
+ memcpy(self->wbuf.dat, DATA_START+DATA_HSIZE+sector_base, FLASH_SECTOR_SIZE);
+ }
+ memcpy(&self->wbuf.dat[byte_offset-sector_base], buf, byte_count);
+
+ self->written = true;
+ return byte_count;
+}
diff --git a/cmd/sbc_harness/fs_harness_flash_bin.h b/cmd/sbc_harness/fs_harness_flash_bin.h
new file mode 100644
index 0000000..36382be
--- /dev/null
+++ b/cmd/sbc_harness/fs_harness_flash_bin.h
@@ -0,0 +1,30 @@
+/* sbc_harness/fs_harness_flash_bin.h - 9P access to flash storage
+ *
+ * Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#ifndef _SBC_HARNESS_FS_HARNESS_FLASH_BIN_H_
+#define _SBC_HARNESS_FS_HARNESS_FLASH_BIN_H_
+
+#include <hardware/flash.h> /* for FLASH_SECTOR_SIZE */
+
+#include <lib9p/srv.h>
+
+struct flash_file {
+ char *name;
+ uint64_t pathnum;
+
+ BEGIN_PRIVATE(FS_HARNESS_FLASH_BIN);
+ bool written;
+ struct {
+ bool ok;
+ size_t pos;
+ uint8_t dat[FLASH_SECTOR_SIZE];
+ } wbuf, rbuf;
+ END_PRIVATE(FS_HARNESS_FLASH_BIN);
+};
+LO_IMPLEMENTATION_H(lib9p_srv_file, struct flash_file, flash_file);
+#define lo_box_flash_file_as_lib9p_srv_file(obj) util9p_box(flash_file, obj)
+
+#endif /* _SBC_HARNESS_FS_HARNESS_FLASH_BIN_H_ */
diff --git a/cmd/sbc_harness/main.c b/cmd/sbc_harness/main.c
index 3334bd8..25b122c 100644
--- a/cmd/sbc_harness/main.c
+++ b/cmd/sbc_harness/main.c
@@ -32,6 +32,7 @@
/* local headers */
#include "usb_keyboard.h"
#include "static.h"
+#include "fs_harness_flash_bin.h"
#include "fs_harness_uptime_txt.h"
/* configuration **************************************************************/
@@ -90,10 +91,8 @@ struct lib9p_srv_file root =
STATIC_FILE("rom.bin",
.data_start = (void*)0x00000000,
.data_size = 16*1024),
- // TODO: Make flash.bin writable.
- STATIC_FILE("flash.bin",
- .data_start = (void*)0x10000000,
- .data_size = PICO_FLASH_SIZE_BYTES),
+ API_FILE("flash.bin",
+ flash),
API_FILE("uptime.txt",
uptime),
// TODO: system.log
diff --git a/cmd/sbc_harness/static/Documentation/harness_flash_bin.txt b/cmd/sbc_harness/static/Documentation/harness_flash_bin.txt
index 982d7e0..115f2ee 100644
--- a/cmd/sbc_harness/static/Documentation/harness_flash_bin.txt
+++ b/cmd/sbc_harness/static/Documentation/harness_flash_bin.txt
@@ -7,6 +7,10 @@ DESCRIPTION
Any number of readers may read the flash contents.
+ Only one writer can have the file open at a time; once the
+ file is closed, the harness reboots into the new firmware.
+ Writes to the top half of the chip will fail.
+
BUGS
- The size of the chip is configured at compile-time. If the
firmware is loaded onto hardware with a larger flash chip
@@ -16,8 +20,13 @@ BUGS
compiled for, then accessing the missing upper part of the
chip will crash.
- - This file is not writable; it aught to be possible to update
- the harness firmware by writing to this file.
+ - When writing to the flash using this file, only half of the
+ chip capacity is usable; the top half and bottom half are
+ mirrors of each-other. This is to avoid the firmware
+ crashing as its program text is overwritten; the firmware is
+ executing out of the bottom half, and writing to the top
+ half; once the file is closed, a minimal in-RAM function
+ copies the top half to the bottom half and reboots.
AUTHOR
Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>