/* sbc_harness/fs_harness_flash_bin.c - 9P access to flash storage * * Copyright (C) 2025 Luke T. Shumaker * SPDX-License-Identifier: AGPL-3.0-or-later */ #include #include #include #define LOG_NAME FLASH #include #include #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); LO_IMPLEMENTATION_STATIC(lib9p_srv_fio, struct flash_file, flash_file); #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 maybe 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); log_infoln("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); } log_infoln("rebooting..."); watchdog_reboot(0, 0, 300); for (;;) asm volatile ("nop"); } /** * Set the upper half of flash to all "1" bits. * * @param buf : a scratch buffer that is at least FLASH_SECTOR_SIZE */ static void ab_flash_initialize(void *buf) { assert(buf); memset(buf, 0xFF, FLASH_SECTOR_SIZE); log_infoln("erasing 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(); flash_range_erase(off, FLASH_SECTOR_SIZE); cr_restore_interrupts(saved); } log_debugln("... erased"); } /** * 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 * @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; log_infoln("write flash sector @ ", (base16_u32_, 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); } log_debugln("... written"); } /* io_preader_to, io_pwriter, io_closer ***************************************/ LO_IMPLEMENTATION_STATIC(io_preader_to, struct flashio, flashio); LO_IMPLEMENTATION_STATIC(io_pwriter, struct flashio, flashio); LO_IMPLEMENTATION_STATIC(io_closer, struct flashio, flashio); /** read from anywhere on the chip */ static size_t_and_error flashio_pread_to(struct flashio *self, lo_interface io_writer dst, uoff_t _src_offset, size_t count) { assert(self); if (_src_offset > DATA_SIZE) return ERROR_AND(size_t, 0, error_new(E_POSIX_EINVAL, "offset is past the chip size")); size_t src_offset = (size_t) _src_offset; if (src_offset == DATA_SIZE) return ERROR_AND(size_t, 0, error_new(E_EOF)); /* Assume that somewhere down the line the pointer we pass to * io_write() 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. */ size_t sector_base = LM_ROUND_DOWN(src_offset, FLASH_SECTOR_SIZE); if (src_offset + count > sector_base + FLASH_SECTOR_SIZE) count = (sector_base + FLASH_SECTOR_SIZE) - src_offset; assert(src_offset + 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); } return io_write(dst, &self->rbuf.dat[src_offset-sector_base], count); } /** takes offsets in the lower half, writes to the upper half */ static size_t_and_error flashio_pwritev(struct flashio *self, const struct iovec *iov, int iovcnt, uoff_t _offset) { assert(self); assert(iov); assert(iovcnt > 0); size_t total_count = 0; for (int i = 0; i < iovcnt; i++) total_count += iov[i].iov_len; assert(total_count > 0); if (_offset >= DATA_HSIZE) return ERROR_AND(size_t, 0, error_new(E_POSIX_ENOSPC, "cannot write past half the chip size")); size_t offset = (size_t) _offset; size_t total_done = 0; for (int i = 0; i < iovcnt; i++) { size_t iov_done = 0; while (iov_done < iov[i].iov_len) { if (offset >= DATA_HSIZE) return ERROR_AND(size_t, total_done, error_new(E_POSIX_ENOSPC, "cannot write past half the chip size")); size_t sector_base = LM_ROUND_DOWN(offset, FLASH_SECTOR_SIZE); size_t len = iov[i].iov_len - iov_done; if (offset + len > sector_base + FLASH_SECTOR_SIZE) len = (sector_base + FLASH_SECTOR_SIZE) - offset; assert(offset + len <= 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 (len != FLASH_SECTOR_SIZE) /* Don't bother with a read if we're just going to overwrite it. */ memcpy(self->wbuf.dat, DATA_START+DATA_HSIZE+sector_base, FLASH_SECTOR_SIZE); } memcpy(&self->wbuf.dat[offset-sector_base], iov[i].iov_base+iov_done, len); total_done += len; iov_done += len; offset += len; } } return ERROR_AND(size_t, total_done, ERROR_NULL); } static error flashio_close(struct flashio *self) { assert(self); if (self->finalize) { if (self->wbuf.ok) ab_flash_write_sector(self->wbuf.pos, self->wbuf.dat); ab_flash_finalize(self->wbuf.dat); } return ERROR_NULL; } /* srv_file *******************************************************************/ void flash_file_free(struct flash_file *self) { assert(self); } struct lib9p_qid flash_file_qid(struct flash_file *self) { assert(self); return (struct lib9p_qid){ .type = LIB9P_QT_FILE|LIB9P_QT_EXCL|LIB9P_QT_APPEND, .vers = 1, .path = self->pathnum, }; } lib9p_srv_stat_or_error flash_file_stat(struct flash_file *self, struct lib9p_srv_ctx *ctx) { assert(self); assert(ctx); return ERROR_NEW_VAL(lib9p_srv_stat, ((struct lib9p_srv_stat){ .qid = flash_file_qid(self), .mode = LIB9P_DM_EXCL|LIB9P_DM_APPEND|0666, .atime_sec = UTIL9P_ATIME, .mtime_sec = UTIL9P_MTIME, .size = DATA_SIZE, .name = lib9p_str(self->name), .owner_uid = { .name = lib9p_str("root"), .num = 0 }, .owner_gid = { .name = lib9p_str("root"), .num = 0 }, .last_modifier_uid = { .name = lib9p_str("root"), .num = 0 }, .extension = lib9p_str(NULL), })); } error flash_file_wstat(struct flash_file *self, struct lib9p_srv_ctx *ctx, struct lib9p_srv_stat) { assert(self); assert(ctx); return error_new(E_POSIX_EROFS, "read-only part of filesystem"); } error flash_file_remove(struct flash_file *self, struct lib9p_srv_ctx *ctx) { assert(self); assert(ctx); return error_new(E_POSIX_EROFS, "read-only part of filesystem"); } LIB9P_SRV_NOTDIR(, struct flash_file, flash_file); static error flash_handle_ihex_data(void *, uint32_t off, uint8_t count, uint8_t *dat); static error flash_handle_ihex_eof(void *); lib9p_srv_fio_or_error flash_file_fopen(struct flash_file *self, struct lib9p_srv_ctx *ctx, bool LM_UNUSED(rd), bool wr, bool LM_UNUSED(trunc)) { assert(self); assert(ctx); memset(&self->io, 0, sizeof(self->io)); if (wr) { ab_flash_initialize(self->io.wbuf.dat); } memset(&self->ihex, 0, sizeof(self->ihex)); self->ihex.handle_arg = self; self->ihex.handle_data = flash_handle_ihex_data; self->ihex.handle_eof = flash_handle_ihex_eof; return ERROR_NEW_VAL(lib9p_srv_fio, LO_BOX(lib9p_srv_fio, self)); } /* srv_fio ********************************************************************/ static struct lib9p_qid flash_file_ioqid(struct flash_file *self) { return flash_file_qid(self); } 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); error err = flashio_close(&self->io); assert(ERROR_IS_NULL(err)); } static error flash_file_pread(struct flash_file *self, struct lib9p_srv_ctx *ctx, lo_interface io_writer dst, uint64_t byte_offset, uint32_t byte_count) { assert(self); assert(ctx); return flashio_pread_to(&self->io, dst, byte_offset, byte_count).err; } static error flash_handle_ihex_data(void *_self, uint32_t off, uint8_t count, uint8_t *dat) { struct flash_file *self = _self; if (off < XIP_BASE || off >= XIP_BASE + DATA_HSIZE) return error_new(E_POSIX_ENOSPC, "cannot write outside of the first half of the chip: ", (base16_u32_, off), " is outside of [", (base16_u32_, XIP_BASE), ",", (base16_u32_, XIP_BASE + DATA_HSIZE), ")"); return flashio_pwritev(&self->io, &((struct iovec){.iov_base = dat, .iov_len = count}), 1, off - XIP_BASE).err; } static error flash_handle_ihex_eof(void *_self) { struct flash_file *self = _self; self->io.finalize = true; return ERROR_NULL; } /* TODO: Also support uf2, not just ihex. */ static uint32_t_or_error flash_file_pwrite(struct flash_file *self, struct lib9p_srv_ctx *ctx, void *buf, uint32_t byte_count, uint64_t LM_UNUSED(byte_offset)) { assert(self); assert(ctx); size_t_and_error r = ihex_decoder_writev(&self->ihex, &((struct iovec){.iov_base = buf, .iov_len = byte_count}), 1); if (r.size_t == 0 && !ERROR_IS_NULL(r.err)) return ERROR_NEW_ERR(uint32_t, r.err); return ERROR_NEW_VAL(uint32_t, (uint32_t) r.size_t); }