summaryrefslogtreecommitdiff
path: root/cmd/sbc_harness
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/sbc_harness')
-rw-r--r--cmd/sbc_harness/CMakeLists.txt64
-rw-r--r--cmd/sbc_harness/config/config.h77
-rw-r--r--cmd/sbc_harness/config/tusb_config.h30
-rw-r--r--cmd/sbc_harness/fs_harness_flash_bin.c327
-rw-r--r--cmd/sbc_harness/fs_harness_flash_bin.h36
-rw-r--r--cmd/sbc_harness/fs_harness_uptime_txt.c165
-rw-r--r--cmd/sbc_harness/fs_harness_uptime_txt.h18
-rw-r--r--cmd/sbc_harness/ihex.c225
-rw-r--r--cmd/sbc_harness/ihex.h48
-rw-r--r--cmd/sbc_harness/main.c197
-rw-r--r--cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS.md29
l---------cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/agpl-3.0.txt1
l---------cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/dhcp.bsd3-mit.txt1
l---------cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/newlib.txt1
l---------cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/pico-sdk.bsd3.txt1
l---------cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/tinyusb.mit.txt1
-rw-r--r--cmd/sbc_harness/static/Documentation/harness_flash_bin.txt43
-rw-r--r--cmd/sbc_harness/static/Documentation/harness_rom_bin.txt41
-rw-r--r--cmd/sbc_harness/static/Documentation/harness_uptime_txt.txt27
-rw-r--r--cmd/sbc_harness/tests/test_ihex.c128
-rw-r--r--cmd/sbc_harness/tusb_log.c22
-rw-r--r--cmd/sbc_harness/usb_keyboard.c49
-rw-r--r--cmd/sbc_harness/usb_keyboard.h4
23 files changed, 1436 insertions, 99 deletions
diff --git a/cmd/sbc_harness/CMakeLists.txt b/cmd/sbc_harness/CMakeLists.txt
index 6199e0c..7656ab6 100644
--- a/cmd/sbc_harness/CMakeLists.txt
+++ b/cmd/sbc_harness/CMakeLists.txt
@@ -1,6 +1,6 @@
# cmd/sbc_harness/CMakeLists.txt - Build script for main sbc_harness.uf2 firmware file
#
-# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+# Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
# SPDX-License-Identifier: AGPL-3.0-or-later
if (PICO_PLATFORM STREQUAL "rp2040")
@@ -10,23 +10,41 @@ if (PICO_PLATFORM STREQUAL "rp2040")
add_library(sbc_harness_objs OBJECT
main.c
usb_keyboard.c
+ tusb_log.c
+
+ fs_harness_flash_bin.c
+ fs_harness_uptime_txt.c
+
+ ihex.c
)
target_include_directories(sbc_harness_objs PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/config)
target_include_directories(sbc_harness_objs PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
+target_include_directories(sbc_harness_objs PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(sbc_harness_objs
- pico_stdlib
+ pico_runtime
+ pico_stdio_uart
+
hardware_flash
+ hardware_watchdog
libmisc
libusb
libdhcp
- libhw
+ libhw_cr
+ lib9p_srv
+ lib9p_util
)
pico_minimize_runtime(sbc_harness_objs)
-pico_enable_stdio_usb(sbc_harness_objs 0)
-pico_enable_stdio_uart(sbc_harness_objs 1)
-pico_enable_stdio_semihosting(sbc_harness_objs 0)
-pico_enable_stdio_rtt(sbc_harness_objs 0)
+target_compile_definitions(sbc_harness_objs PRIVATE
+ #PICO_USE_FASTEST_SUPPORTED_CLOCK=1
+
+ # Calculated by `./3rd-party/pico-sdk/src/rp2_common/hardware_clocks/scripts/vcocalc.py --cmake-only 170`
+ PLL_SYS_REFDIV=2
+ PLL_SYS_VCO_FREQ_HZ=1530000000
+ PLL_SYS_POSTDIV1=3
+ PLL_SYS_POSTDIV2=3
+ SYS_CLK_HZ=170000000
+)
suppress_tinyusb_warnings()
@@ -37,10 +55,6 @@ add_stack_analysis(sbc_harness_stack.c sbc_harness_objs)
# Link #########################################################################
add_executable(sbc_harness)
-set_source_files_properties("$<TARGET_OBJECTS:sbc_harness_objs>" PROPERTIES
- EXTERNAL_OBJECT true
- GENERATED true
-)
target_sources(sbc_harness PRIVATE
sbc_harness_stack.c
"$<TARGET_OBJECTS:sbc_harness_objs>"
@@ -48,7 +62,35 @@ target_sources(sbc_harness PRIVATE
target_link_libraries(sbc_harness
pico_standard_link
)
+target_link_options(sbc_harness PRIVATE "$<TARGET_PROPERTY:sbc_harness_objs,LINK_OPTIONS>")
pico_add_extra_outputs(sbc_harness) # create .map/.bin/.hex/.uf2 files in addition to .elf
pico_set_program_url(sbc_harness "https://git.lukeshu.com/sbc-harness")
+# Embed ########################################################################
+
+target_embed_sources(sbc_harness_objs sbc_harness static.h
+ static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS.md
+ static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/agpl-3.0.txt
+ static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/dhcp.bsd3-mit.txt
+ static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/newlib.txt
+ static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/pico-sdk.bsd3.txt
+ static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/tinyusb.mit.txt
+ static/Documentation/harness_rom_bin.txt
+ static/Documentation/harness_flash_bin.txt
+ static/Documentation/harness_uptime_txt.txt
+)
+
+endif()
+
+# Tests ########################################################################
+if ((PICO_PLATFORM STREQUAL "host") AND (ENABLE_TESTS))
+ add_executable(test_ihex "tests/test_ihex.c" "ihex.c")
+ target_include_directories(test_ihex PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/tests)
+ target_link_libraries(test_ihex
+ libhw_generic
+ )
+ add_test(
+ NAME "cmd/sbc_harness/test_ihex"
+ COMMAND "${CMAKE_SOURCE_DIR}/build-aux/valgrind" "./test_ihex"
+ )
endif()
diff --git a/cmd/sbc_harness/config/config.h b/cmd/sbc_harness/config/config.h
index 85170cc..61745e5 100644
--- a/cmd/sbc_harness/config/config.h
+++ b/cmd/sbc_harness/config/config.h
@@ -1,18 +1,24 @@
/* config.h - Compile-time configuration for sbc_harness
*
- * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#ifndef _CONFIG_H_
#define _CONFIG_H_
-/* W5500 **********************************************************************/
+#include <stddef.h> /* for size_t */
-/**
- * How many W5500 chips we have.
- */
-#define CONFIG_W5500_NUM 1
+#define CONFIG_FLASH_DEBUG 1
+
+#define _CONFIG_9P_MAX_CONNS 3 /* FIXME: bump this back up to 8 */
+#define _CONFIG_9P_MAX_REQS (2*_CONFIG_9P_MAX_CONNS)
+
+/* RP2040 *********************************************************************/
+
+#define CONFIG_RP2040_SPI_DEBUG 1 /* bool */
+
+/* W5500 **********************************************************************/
/**
* When allocating an arbitrary local port, what range should it be
@@ -25,9 +31,22 @@
#define CONFIG_W5500_LOCAL_PORT_MIN 32768
#define CONFIG_W5500_LOCAL_PORT_MAX 60999
+#define CONFIG_W5500_VALIDATE_SPI 1 /* bool */
+#define CONFIG_W5500_DEBUG 0 /* bool */
+#define CONFIG_W5500_LL_DEBUG 0 /* bool */
+
/* 9P *************************************************************************/
-#define CONFIG_9P_PORT 564
+#define CONFIG_9P_ENABLE_9P2000 1 /* bool */
+#define CONFIG_9P_ENABLE_9P2000_u 1 /* bool */
+#define CONFIG_9P_ENABLE_9P2000_e 0 /* bool */
+#define CONFIG_9P_ENABLE_9P2000_L 0 /* bool */
+#define CONFIG_9P_ENABLE_9P2000_p9p 0 /* bool */
+
+/* 9P_SRV *********************************************************************/
+
+#define CONFIG_9P_SRV_DEBUG 1 /* bool */
+
/**
* This max-msg-size is sized so that a Twrite message can return
* 8KiB of data.
@@ -45,46 +64,54 @@
* negotiated. In Plan 9 1e it was (8*1024)+128, and was bumped to
* (8*1024)+160 in 2e and 3e.
*/
-#define CONFIG_9P_MAX_MSG_SIZE ((4*1024)+24)
+#define CONFIG_9P_SRV_MAX_MSG_SIZE ((4*1024)+24)
+#define CONFIG_9P_SRV_MAX_ERR_SIZE 128 /* 128 is what Plan 9 4e uses */
/**
* Maximum host-data-structure size. A message may be larger in
* unmarshaled-host-structures than marshaled-net-bytes due to (1)
- * struct padding, (2) nul-terminator byes for strings.
+ * struct padding, (2) array pointers.
*/
-#define CONFIG_9P_MAX_HOSTMSG_SIZE CONFIG_9P_MAX_MSG_SIZE+16
-#define CONFIG_9P_MAX_FIDS 16
-#define CONFIG_9P_MAX_REQS 2
-#define CONFIG_9P_MAX_ERR_SIZE 128 /* 128 is what Plan 9 4e uses */
-#define CONFIG_9P_ENABLE_9P2000 1 /* bool */
-#define CONFIG_9P_ENABLE_9P2000_u 1 /* bool */
-#define CONFIG_9P_ENABLE_9P2000_e 0 /* bool */
+#define CONFIG_9P_SRV_MAX_HOSTMSG_SIZE CONFIG_9P_SRV_MAX_MSG_SIZE+16
/* DHCP ***********************************************************************/
#define CONFIG_DHCP_CAN_RECV_UNICAST_IP_WITHOUT_IP 0 /* bool */
-#define CONFIG_DHCP_DEBUG 0 /* bool */
+#define CONFIG_DHCP_DEBUG 1 /* bool */
#define CONFIG_DHCP_OPT_SIZE 312 /* minimum of 312 */
#define CONFIG_DHCP_SELECTING_NS (5*NS_PER_S)
/* USB KEYBOARD ***************************************************************/
+/**
+ * Which USB port to use for the Root Hub.
+ *
+ * The RP2040 only has 1 port, so it's gotta be port #0.
+ */
+#define CONFIG_USB_COMMON_RHPORT 0
+
#define CONFIG_USB_COMMON_DEBUG 1 /* bool */
/* COROUTINE ******************************************************************/
-#define CONFIG_COROUTINE_DEFAULT_STACK_SIZE (2*1024)
+extern const size_t CONFIG_COROUTINE_STACK_SIZE_dhcp_cr;
+extern const size_t CONFIG_COROUTINE_STACK_SIZE_init_cr;
+extern const size_t CONFIG_COROUTINE_STACK_SIZE_read9p_cr;
+extern const size_t CONFIG_COROUTINE_STACK_SIZE_write9p_cr;
+extern const size_t CONFIG_COROUTINE_STACK_SIZE_usb_common_cr;
+extern const size_t CONFIG_COROUTINE_STACK_SIZE_usb_keyboard_cr;
+extern const size_t CONFIG_COROUTINE_STACK_SIZE_w5500_irq_cr;
#define CONFIG_COROUTINE_NAME_LEN 16
#define CONFIG_COROUTINE_MEASURE_STACK 1 /* bool */
#define CONFIG_COROUTINE_PROTECT_STACK 1 /* bool */
#define CONFIG_COROUTINE_DEBUG 0 /* bool */
#define CONFIG_COROUTINE_VALGRIND 0 /* bool */
+#define CONFIG_COROUTINE_GDB 1 /* bool */
-#define _CONFIG_9P_NUM_SOCKS 7
-#define CONFIG_COROUTINE_NUM ( \
- 1 /* usb_common */ + \
- 1 /* usb_keyboard */ + \
- CONFIG_W5500_NUM /* irq handler */ + \
- _CONFIG_9P_NUM_SOCKS /* 9P accept()+read() */ + \
- (CONFIG_9P_MAX_REQS*_CONFIG_9P_NUM_SOCKS) /* 9P work+write() */ )
+#define CONFIG_COROUTINE_NUM ( \
+ 1 /* usb_common */ + \
+ 1 /* usb_keyboard */ + \
+ 1 /* W5500 irq handler */ + \
+ _CONFIG_9P_MAX_CONNS /* 9P accept()+read() */ + \
+ _CONFIG_9P_MAX_REQS /* 9P work+write() */ )
#endif /* _CONFIG_H_ */
diff --git a/cmd/sbc_harness/config/tusb_config.h b/cmd/sbc_harness/config/tusb_config.h
index fc963ac..2c7c02a 100644
--- a/cmd/sbc_harness/config/tusb_config.h
+++ b/cmd/sbc_harness/config/tusb_config.h
@@ -1,6 +1,6 @@
/* tusb_config.h - Compile-time configuration for the TinyUSB library
*
- * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* SPDX-License-Identifier: MIT
@@ -31,21 +31,31 @@
#ifndef _TUSB_CONFIG_H_
#define _TUSB_CONFIG_H_
+#include <stdint.h> /* for uint{n}_t */
+
#ifdef __cplusplus
extern "C" {
#endif
//--------------------------------------------------------------------
-// TinyUSB Device (TUD) initialization for rp2040-based boards
+// Override the default TU_MESS_FAILED() and tu_print_*() to use our logging
//--------------------------------------------------------------------
-// Which USB port to use for the RootHub.
-// The rp2040 only has 1 port, so it's gotta be port #0.
-#define BOARD_TUD_RHPORT 0
-
-// RHPort max operational speed.
-// Use OPT_MODE_DEFAULT_SPEED for max speed supported by MCU.
-#define BOARD_TUD_MAX_SPEED OPT_MODE_DEFAULT_SPEED
+#define TU_MESS_FAILED(_cond_str) _libmisc_tu_mess_failed(_cond_str, __FILE__, __LINE__, __func__)
+#define tu_print_str _libmisc_tu_print_str
+#define tu_print_byte _libmisc_tu_print_byte
+#define tu_print_base10 _libmisc_tu_print_base10
+#define tu_print_base16 _libmisc_tu_print_base16
+#define tu_print_base16_u8 _libmisc_tu_print_base16_u8
+#define tu_print_base16_u16 _libmisc_tu_print_base16_u16
+
+void _libmisc_tu_mess_failed(const char *expr, const char *file, unsigned int line, const char *func);
+void _libmisc_tu_print_str(const char *);
+void _libmisc_tu_print_byte(uint8_t);
+void _libmisc_tu_print_base10(unsigned long);
+void _libmisc_tu_print_base16(unsigned long);
+void _libmisc_tu_print_base16_u8(uint8_t);
+void _libmisc_tu_print_base16_u16(uint16_t);
//--------------------------------------------------------------------
// Configuration: common
@@ -72,7 +82,7 @@ extern "C" {
#define CFG_TUSB_MEM_ALIGN [[gnu::aligned(4)]]
#define CFG_TUD_ENABLED 1
-#define CFG_TUD_MAX_SPEED BOARD_TUD_MAX_SPEED
+#define CFG_TUD_MAX_SPEED OPT_MODE_DEFAULT_SPEED
//--------------------------------------------------------------------
// Configuration: TinyUSB Device (TUD)
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..6ae9e77
--- /dev/null
+++ b/cmd/sbc_harness/fs_harness_flash_bin.c
@@ -0,0 +1,327 @@
+/* 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 <string.h>
+
+#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);
+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);
+}
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..84cc494
--- /dev/null
+++ b/cmd/sbc_harness/fs_harness_flash_bin.h
@@ -0,0 +1,36 @@
+/* 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>
+
+#include "ihex.h"
+
+struct flashio {
+ bool finalize;
+ struct {
+ bool ok;
+ size_t pos;
+ uint8_t dat[FLASH_SECTOR_SIZE];
+ } wbuf, rbuf;
+};
+
+struct flash_file {
+ char *name;
+ uint64_t pathnum;
+
+ BEGIN_PRIVATE(FS_HARNESS_FLASH_BIN);
+ struct flashio io;
+ struct ihex_decoder ihex;
+ END_PRIVATE(FS_HARNESS_FLASH_BIN);
+};
+LO_IMPLEMENTATION_H(lib9p_srv_file, struct flash_file, flash_file);
+
+#endif /* _SBC_HARNESS_FS_HARNESS_FLASH_BIN_H_ */
diff --git a/cmd/sbc_harness/fs_harness_uptime_txt.c b/cmd/sbc_harness/fs_harness_uptime_txt.c
new file mode 100644
index 0000000..9b03b46
--- /dev/null
+++ b/cmd/sbc_harness/fs_harness_uptime_txt.c
@@ -0,0 +1,165 @@
+/* sbc_harness/fs_harness_uptime_txt.c - 9P access to harness uptime
+ *
+ * Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libhw/generic/alarmclock.h>
+#include <libmisc/alloc.h> /* for heap_alloc(), free() */
+#include <libmisc/fmt.h> /* for fmt_snprint() */
+#include <util9p/static.h>
+
+#include "fs_harness_uptime_txt.h"
+
+LO_IMPLEMENTATION_C(lib9p_srv_file, struct uptime_file, uptime_file);
+
+struct uptime_fio {
+ struct uptime_file *parent;
+ size_t buf_len;
+ /* The maximum length (UINT64_MAX) string is 52 bytes, not
+ * including a nul-terminator:
+ *
+ * "18446744073709551615ns\n" # 22+1
+ * "584y343d 23h34m33.709551615s\n" # 28+1
+ */
+ char buf[52];
+};
+LO_IMPLEMENTATION_STATIC(lib9p_srv_fio, struct uptime_fio, uptime_fio);
+
+/* srv_file *******************************************************************/
+
+void uptime_file_free(struct uptime_file *self) {
+ assert(self);
+}
+struct lib9p_qid uptime_file_qid(struct uptime_file *self) {
+ assert(self);
+
+ return (struct lib9p_qid){
+ .type = LIB9P_QT_FILE,
+ .vers = 1,
+ .path = self->pathnum,
+ };
+}
+
+lib9p_srv_stat_or_error uptime_file_stat(struct uptime_file *self, struct lib9p_srv_ctx *ctx) {
+ assert(self);
+ assert(ctx);
+
+ uint64_t now = LO_CALL(bootclock, get_time_ns);
+ uint64_t size = 0;
+ while (now) {
+ size++;
+ now /= 10;
+ }
+ if (!size)
+ size++;
+ size += 3;
+
+ return ERROR_NEW_VAL(lib9p_srv_stat, ((struct lib9p_srv_stat){
+ .qid = uptime_file_qid(self),
+ .mode = 0444,
+ .atime_sec = UTIL9P_ATIME,
+ .mtime_sec = UTIL9P_MTIME,
+ .size = 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 uptime_file_wstat(struct uptime_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 uptime_file_remove(struct uptime_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 uptime_file, uptime_file);
+
+lib9p_srv_fio_or_error uptime_file_fopen(struct uptime_file *self, struct lib9p_srv_ctx *ctx,
+ bool LM_UNUSED(rd), bool LM_UNUSED(wr), bool LM_UNUSED(trunc)) {
+ assert(self);
+ assert(ctx);
+
+ struct uptime_fio *ret = heap_alloc(1, struct uptime_fio);
+ ret->parent = self;
+ ret->buf_len = 0;
+
+ return ERROR_NEW_VAL(lib9p_srv_fio, LO_BOX(lib9p_srv_fio, ret));
+}
+
+/* srv_fio ********************************************************************/
+
+static uint32_t uptime_fio_iounit(struct uptime_fio *self) {
+ assert(self);
+ return sizeof(self->buf)-1;
+}
+
+static void uptime_fio_iofree(struct uptime_fio *self) {
+ assert(self);
+ free(self);
+}
+
+static struct lib9p_qid uptime_fio_ioqid(struct uptime_fio *self) {
+ assert(self);
+ assert(self->parent);
+ return uptime_file_qid(self->parent);
+}
+
+#define NS_PER_M (NS_PER_S*60)
+#define NS_PER_H (NS_PER_S*60*60)
+#define NS_PER_D (NS_PER_S*60*60*24)
+#define NS_PER_Y (NS_PER_S*60*60*24*365)
+
+static error uptime_fio_pread(struct uptime_fio *self, struct lib9p_srv_ctx *ctx,
+ lo_interface io_writer dst, uint64_t byte_offset, uint32_t byte_count) {
+ assert(self);
+ assert(ctx);
+
+ if (byte_offset == 0 || self->buf_len == 0) {
+ uint64_t now = LO_CALL(bootclock, get_time_ns);
+ self->buf_len = fmt_snprint(self->buf, sizeof(self->buf), now, "ns\n");
+
+ uint64_t ns = now;
+ uint64_t y = ns/NS_PER_Y; ns -= y*NS_PER_Y;
+ uint64_t d = ns/NS_PER_D; ns -= d*NS_PER_D;
+ uint64_t h = ns/NS_PER_H; ns -= h*NS_PER_H;
+ uint64_t m = ns/NS_PER_M; ns -= m*NS_PER_M;
+ uint64_t s = ns/NS_PER_S; ns -= s*NS_PER_S;
+ if (y)
+ self->buf_len += fmt_snprint(&self->buf[self->buf_len], sizeof(self->buf)-self->buf_len, y, "y");
+ if (y || d)
+ self->buf_len += fmt_snprint(&self->buf[self->buf_len], sizeof(self->buf)-self->buf_len, d, "d ");
+ if (y || d || h)
+ self->buf_len += fmt_snprint(&self->buf[self->buf_len], sizeof(self->buf)-self->buf_len, h, "h");
+ if (y || d || h || m)
+ self->buf_len += fmt_snprint(&self->buf[self->buf_len], sizeof(self->buf)-self->buf_len, m, "m");
+ self->buf_len += fmt_snprint(&self->buf[self->buf_len], sizeof(self->buf)-self->buf_len, s, ".", (rjust, 9, '0', ns), "s\n");
+ }
+
+ if (byte_offset > (uint64_t)self->buf_len)
+ return error_new(E_POSIX_EINVAL, "offset is past end-of-file length");
+
+ size_t beg_off = (size_t)byte_offset;
+ size_t end_off = beg_off + (size_t)byte_count;
+ if (end_off > self->buf_len)
+ end_off = self->buf_len;
+ return io_write(dst, &self->buf[beg_off], end_off-beg_off).err;
+}
+
+static uint32_t_or_error uptime_fio_pwrite(struct uptime_fio *self, struct lib9p_srv_ctx *ctx,
+ void *LM_UNUSED(buf),
+ uint32_t LM_UNUSED(byte_count),
+ uint64_t LM_UNUSED(byte_offset)) {
+ assert(self);
+ assert(ctx);
+
+ return ERROR_NEW_ERR(uint32_t, error_new(E_POSIX_EROFS, "read-only part of filesystem"));
+}
diff --git a/cmd/sbc_harness/fs_harness_uptime_txt.h b/cmd/sbc_harness/fs_harness_uptime_txt.h
new file mode 100644
index 0000000..c575580
--- /dev/null
+++ b/cmd/sbc_harness/fs_harness_uptime_txt.h
@@ -0,0 +1,18 @@
+/* sbc_harness/fs_harness_uptime_txt.h - 9P access to harness uptime
+ *
+ * Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#ifndef _SBC_HARNESS_FS_HARNESS_UPTIME_TXT_H_
+#define _SBC_HARNESS_FS_HARNESS_UPTIME_TXT_H_
+
+#include <lib9p/srv.h>
+
+struct uptime_file {
+ char *name;
+ uint64_t pathnum;
+};
+LO_IMPLEMENTATION_H(lib9p_srv_file, struct uptime_file, uptime_file);
+
+#endif /* _SBC_HARNESS_FS_HARNESS_UPTIME_TXT_H_ */
diff --git a/cmd/sbc_harness/ihex.c b/cmd/sbc_harness/ihex.c
new file mode 100644
index 0000000..565ad16
--- /dev/null
+++ b/cmd/sbc_harness/ihex.c
@@ -0,0 +1,225 @@
+/* sbc_harness/ihex.c - Intel Hex decoder
+ *
+ * Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+/* https://archive.org/details/IntelHEXStandard */
+
+#include <string.h> /* for memchr() */
+
+#include <libmisc/assert.h>
+#include <libmisc/endian.h>
+
+#define IMPLEMENTATION_FOR_IHEX_H YES
+#include "ihex.h"
+
+LO_IMPLEMENTATION_C(io_writer, struct ihex_decoder, ihex_decoder);
+
+enum ihex_record_type {
+ /* [U]SBA: [Upper] Segment Base Address : SBA = USBA<< 4 */
+ /* [U]LBA: [Upper] Linear Base Address : LBA = ULBA<<16 */
+ /* _EXT records define where DATA records are written to */
+ /* _START records define where execution should start */
+ IHEX_REC_DATA = 0x00, /* .dat is .len bytes of data, which go at either (USBA<<4)+(.off%64KiB) or ((ULBA<<16)+.off)%4GiB */
+ IHEX_REC_EOF = 0x01, /* .len=0, .off=0 */
+ IHEX_REC_ADDR_SEG_EXT = 0x02, /* .len=2, .off=0, .dat is u16be USBA */
+ IHEX_REC_ADDR_SEG_START = 0x03, /* .len=4, .off=0, .dat is u16be CS register then u16be IP register */
+ IHEX_REC_ADDR_LIN_EXT = 0x04, /* .len=2, .off=0, .dat is u16be ULBA */
+ IHEX_REC_ADDR_LIN_START = 0x05, /* .len=4, .off=0, .dat is u32be EIP register */
+};
+
+struct ihex_record {
+ uint8_t len;
+ uint16_t off;
+ enum ihex_record_type typ;
+ uint8_t *dat;
+};
+
+static error ihex_handle_record(struct ihex_decoder *self, struct ihex_record *rec) {
+ switch (rec->typ) {
+ case IHEX_REC_ADDR_SEG_EXT:
+ self->addr_mode = _IHEX_MODE_SEG;
+ self->addr_base = ((uint32_t)uint16be_decode(rec->dat)) << 4;
+ return ERROR_NULL;
+ case IHEX_REC_ADDR_LIN_EXT:
+ self->addr_mode = _IHEX_MODE_LIN;
+ self->addr_base = ((uint32_t)uint16be_decode(rec->dat)) << 16;
+ return ERROR_NULL;;
+ case IHEX_REC_DATA:
+ switch (self->addr_mode) {
+ case _IHEX_MODE_NONE:
+ return error_new(E_POSIX_EINVAL, "ihex: data record before base-address record");
+ case _IHEX_MODE_SEG:
+ if (!self->handle_data)
+ return ERROR_NULL;
+ if (rec->len <= UINT16_MAX - rec->off) {
+ /* 1 write */
+ return self->handle_data(self->handle_arg, self->addr_base + rec->off, rec->len, rec->dat);
+ } else {
+ /* wraps around; split into 2 writes */
+ uint8_t first_len = (uint8_t) (UINT16_MAX - rec->off);
+ if (first_len) {
+ error err = self->handle_data(self->handle_arg, self->addr_base + rec->off, first_len, rec->dat);
+ if (!ERROR_IS_NULL(err))
+ return err;
+ }
+ return self->handle_data(self->handle_arg, self->addr_base, rec->len - first_len, &rec->dat[first_len]);
+ }
+ case _IHEX_MODE_LIN:
+ if (!self->handle_data)
+ return ERROR_NULL;
+ uint32_t off = self->addr_base + rec->off;
+ if (rec->len <= UINT32_MAX - off) {
+ /* 1 write */
+ return self->handle_data(self->handle_arg, off, rec->len, rec->dat);
+ } else {
+ /* wraps around; split into 2 writes */
+ uint8_t first_len = (uint8_t) (UINT32_MAX - off);
+ if (first_len) {
+ error err = self->handle_data(self->handle_arg, off, first_len, rec->dat);
+ if (!ERROR_IS_NULL(err))
+ return err;
+ }
+ return self->handle_data(self->handle_arg, 0, rec->len - first_len, &rec->dat[first_len]);
+ }
+ default:
+ assert_notreached("bad addr_mode");
+ }
+ case IHEX_REC_EOF:
+ self->seen_eof = true;
+ if (!self->handle_eof)
+ return ERROR_NULL;
+ return self->handle_eof(self->handle_arg);
+ case IHEX_REC_ADDR_SEG_START:
+ if (!self->handle_set_exec_start_seg)
+ return ERROR_NULL;
+ uint16_t cs = uint16be_decode(&rec->dat[0]);
+ uint16_t ip = uint16be_decode(&rec->dat[2]);
+ return self->handle_set_exec_start_seg(self->handle_arg, cs, ip);
+ case IHEX_REC_ADDR_LIN_START:
+ if (!self->handle_set_exec_start_lin)
+ return ERROR_NULL;
+ uint32_t eip = uint32be_decode(rec->dat);
+ return self->handle_set_exec_start_lin(self->handle_arg, eip);
+ default:
+ assert_notreached("bad record type");
+ }
+}
+
+/**
+ * Hex-decode the byte 0xAB, and push it onto self->buf. If this
+ * completes the record in self->buf, then handle that record.
+ *
+ * @return the number of ASCII bytes consumed (0, 1, or 2) before
+ * encountering an error.
+ */
+static size_t_and_error ihex_decode_byte(struct ihex_decoder *self, char a, char b) {
+ uint8_t byte;
+ if ('0' <= a && a <= '9')
+ byte = (a - '0') << 4;
+ else if ('A' <= a && a <= 'F')
+ byte = (a - 'A' + 10) << 4;
+ else
+ return ERROR_AND(size_t, 0, error_new(E_POSIX_EILSEQ, "ihex: invalid hexadecimal: ", (qbyte, a)));
+ if ('0' <= b && b <= '9')
+ byte |= b - '0';
+ else if ('A' <= b && b <= 'F')
+ byte |= b - 'A' + 10;
+ else
+ return ERROR_AND(size_t, 1, error_new(E_POSIX_EILSEQ, "ihex: invalid hexadecimal: ", (qbyte, b)));
+ self->buf[self->buf_len++] = byte;
+ if (self->buf_len == self->buf[0]+5) {
+ uint8_t sum = 0;
+ for (size_t i = 0; i < (size_t)self->buf[0]+5; i++)
+ sum += self->buf[i];
+ if (sum != 0) {
+ self->sticky_err = error_new(E_POSIX_EPROTO, "ihex: checksum mismatch");
+ return ERROR_AND(size_t, 2, error_dup(self->sticky_err));
+ }
+ struct ihex_record rec = {
+ .len = self->buf[0],
+ .off = uint16be_decode(&self->buf[1]),
+ .typ = self->buf[3],
+ .dat = &self->buf[4],
+ };
+ error err = ihex_handle_record(self, &rec);
+ if (!ERROR_IS_NULL(err)) {
+ self->sticky_err = err;
+ return ERROR_AND(size_t, 2, error_dup(err));
+ }
+ self->in_record = false;
+ self->buf_len = 0;
+ }
+ return ERROR_AND(size_t, 2, ERROR_NULL);
+}
+
+static size_t_and_error ihex_decoder_write(struct ihex_decoder *self, const char *dat, size_t len_in) {
+ assert(self);
+ if (!len_in)
+ return ERROR_AND(size_t, 0, ERROR_NULL);
+ assert(dat);
+
+ if (!ERROR_IS_NULL(self->sticky_err))
+ return ERROR_AND(size_t, 0, error_dup(self->sticky_err));
+
+ size_t len_consumed = 0;
+
+ if (self->buf_char) {
+ assert(self->in_record);
+ size_t_and_error r = ihex_decode_byte(self, self->buf_char, dat[0]);
+ if (r.size_t)
+ len_consumed += r.size_t - 1;
+ self->buf_char = 0;
+ if (!ERROR_IS_NULL(r.err))
+ return ERROR_AND(size_t, len_consumed, r.err);
+ }
+
+ while (len_consumed < len_in) {
+ if (!self->in_record) {
+ const char *marker = memchr(&dat[len_consumed], ':', len_in-len_consumed);
+ if (!marker) {
+ len_consumed = len_in;
+ continue;
+ }
+ len_consumed += marker - &dat[len_consumed];
+
+ assert(dat[len_consumed] == ':');
+ if (self->seen_eof)
+ return ERROR_AND(size_t, len_consumed, error_new(E_POSIX_EPROTO, "ihex: record after EOF record"));
+ len_consumed++;
+ self->in_record = true;
+ }
+ while (len_in - len_consumed >= 2 && self->in_record) {
+ size_t_and_error r = ihex_decode_byte(self, dat[len_consumed], dat[len_consumed+1]);
+ len_consumed += r.size_t;
+ if (!ERROR_IS_NULL(r.err))
+ return ERROR_AND(size_t, len_consumed, r.err);
+ }
+ if (len_in - len_consumed && self->in_record) {
+ assert(len_in - len_consumed == 1);
+ if (!(('0' <= dat[len_consumed] && dat[len_consumed] <= '9') ||
+ ('A' <= dat[len_consumed] && dat[len_consumed] <= 'F')))
+ return ERROR_AND(size_t, len_consumed, error_new(E_POSIX_EILSEQ, "ihex: invalid hexadecimal: ", (qbyte, dat[len_consumed])));
+ self->buf_char = dat[len_consumed++];
+ }
+ }
+
+ assert(len_consumed == len_in);
+ return ERROR_AND(size_t, len_in, ERROR_NULL);
+}
+
+size_t_and_error ihex_decoder_writev(struct ihex_decoder *self, const struct iovec *iov, int iovcnt) {
+ assert(self);
+ assert(iov);
+ assert(iovcnt);
+
+ size_t total = 0;
+ for (int i = 0; i < iovcnt; i++) {
+ size_t_and_error r = ihex_decoder_write(self, iov[i].iov_base, iov[i].iov_len);
+ total += r.size_t;
+ if (!ERROR_IS_NULL(r.err))
+ return ERROR_AND(size_t, total, r.err);
+ }
+ return ERROR_AND(size_t, total, ERROR_NULL);
+}
diff --git a/cmd/sbc_harness/ihex.h b/cmd/sbc_harness/ihex.h
new file mode 100644
index 0000000..d5ac70c
--- /dev/null
+++ b/cmd/sbc_harness/ihex.h
@@ -0,0 +1,48 @@
+/* sbc_harness/ihex.h - Intel Hex decoder
+ *
+ * Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+/* https://archive.org/details/IntelHEXStandard */
+
+#ifndef _SBC_HARNESS_IHEX_H_
+#define _SBC_HARNESS_IHEX_H_
+
+#include <stdbool.h> /* for bool */
+#include <stddef.h> /* for size_t */
+#include <stdint.h> /* for uint{n}_t */
+
+#include <libhw/generic/io.h>
+#include <libmisc/private.h>
+
+struct ihex_decoder {
+ void *handle_arg;
+ error (*handle_data)(void *arg, uint32_t off, uint8_t count, uint8_t *dat);
+ error (*handle_set_exec_start_seg)(void *arg, uint16_t cs, uint16_t ip);
+ error (*handle_set_exec_start_lin)(void *arg, uint32_t eip);
+ error (*handle_eof)(void *arg);
+
+ BEGIN_PRIVATE(IHEX_H);
+
+ bool seen_eof;
+ error sticky_err;
+
+ /* ihex_decoder_write: deal with ASCII soup */
+ bool in_record;
+ char buf_char; /* the first nibble of a byte (still in ASCII) */
+
+ /* ihex_decode_byte: build records from decoded bytes */
+ /* The currently-being-decoded record, after hex decoding. */
+ uint16_t buf_len;
+ uint8_t buf[0xFF+5];
+
+ /* ihex_handle_record: handle record semantics */
+ enum { _IHEX_MODE_NONE, _IHEX_MODE_SEG, _IHEX_MODE_LIN } addr_mode;
+ uint32_t addr_base;
+
+ END_PRIVATE(IHEX_H);
+};
+LO_IMPLEMENTATION_H(io_writer, struct ihex_decoder, ihex_decoder);
+
+#endif /* _SBC_HARNESS_IHEX_H_ */
diff --git a/cmd/sbc_harness/main.c b/cmd/sbc_harness/main.c
index 4683c72..6f53c0b 100644
--- a/cmd/sbc_harness/main.c
+++ b/cmd/sbc_harness/main.c
@@ -1,27 +1,127 @@
/* sbc_harness/main.c - Main entry point and event loop for sbc-harness
*
- * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-#include <string.h> /* libc: for strlen() */
+/* libc */
+#include <string.h> /* libc: for strlen() */
-#include <pico/stdlib.h> /* pico-sdk:pico_stdlib: for stdio_uart_init() */
-#include <hardware/flash.h> /* pico-sdk:hardware_flash: for flash_get_unique_id() */
+/* pico-sdk */
+#include <hardware/flash.h> /* pico-sdk:hardware_flash: for flash_get_unique_id() */
+#include <pico/stdio_uart.h> /* pico-sdk:pico_stdio_uart: for stdio_uart_init() */
+/* our OS */
#include <libcr/coroutine.h>
+#include <libhw/generic/alarmclock.h> /* so we can set `bootclock` */
#include <libhw/rp2040_hwspi.h>
+#include <libhw/rp2040_hwtimer.h>
#include <libhw/w5500.h>
-#include <libmisc/hash.h>
-#include <libusb/usb_common.h>
+
+/* our application libraries */
+#include <lib9p/srv.h>
#include <libdhcp/client.h>
+#include <libusb/usb_common.h>
+#include <util9p/static.h>
+/* our utility libraries */
+#include <libmisc/hash.h>
#define LOG_NAME MAIN
#include <libmisc/log.h>
+/* local headers */
+#include "fs_harness_flash_bin.h"
+#include "fs_harness_uptime_txt.h"
+#include "static.h"
#include "usb_keyboard.h"
-COROUTINE hello_world_cr(void *_chan) {
+/* configuration **************************************************************/
+
+#include "config.h"
+
+#ifndef _CONFIG_9P_MAX_CONNS
+ #error config.h must define _CONFIG_9P_MAX_CONNS
+#endif
+#ifndef _CONFIG_9P_MAX_REQS
+ #error config.h must define _CONFIG_9P_MAX_REQS
+#endif
+
+/* file tree ******************************************************************/
+
+enum { PATH_BASE = __COUNTER__ };
+#define PATH_COUNTER __COUNTER__ - PATH_BASE
+
+#define STATIC_FILE(STRNAME, ...) UTIL9P_STATIC_FILE(PATH_COUNTER, STRNAME, __VA_ARGS__)
+#define STATIC_DIR(STRNAME, ...) UTIL9P_STATIC_DIR(PATH_COUNTER, STRNAME, __VA_ARGS__)
+
+#define API_FILE(STRNAME, SYMNAME, ...) \
+ lo_box_##SYMNAME##_file_as_lib9p_srv_file(&((struct SYMNAME##_file){ \
+ .name = STRNAME, \
+ .pathnum = PATH_COUNTER \
+ __VA_OPT__(,) __VA_ARGS__ \
+ }))
+
+static struct lib9p_srv_file root =
+ STATIC_DIR("",
+ STATIC_DIR("Documentation",
+ STATIC_FILE("YOUR_RIGHTS_AND_OBLIGATIONS.md",
+ .data_start = _binary_static_Documentation_YOUR_RIGHTS_AND_OBLIGATIONS_md_start,
+ .data_end = _binary_static_Documentation_YOUR_RIGHTS_AND_OBLIGATIONS_md_end),
+ STATIC_DIR("YOUR_RIGHTS_AND_OBLIGATIONS",
+ STATIC_FILE("agpl-3.0.txt",
+ .data_start = _binary_static_Documentation_YOUR_RIGHTS_AND_OBLIGATIONS_agpl_3_0_txt_start,
+ .data_end = _binary_static_Documentation_YOUR_RIGHTS_AND_OBLIGATIONS_agpl_3_0_txt_end),
+ STATIC_FILE("dhcp.bsd3-mit.txt",
+ .data_start = _binary_static_Documentation_YOUR_RIGHTS_AND_OBLIGATIONS_dhcp_bsd3_mit_txt_start,
+ .data_end = _binary_static_Documentation_YOUR_RIGHTS_AND_OBLIGATIONS_dhcp_bsd3_mit_txt_end),
+ STATIC_FILE("newlib.txt",
+ .data_start = _binary_static_Documentation_YOUR_RIGHTS_AND_OBLIGATIONS_newlib_txt_start,
+ .data_end = _binary_static_Documentation_YOUR_RIGHTS_AND_OBLIGATIONS_newlib_txt_end),
+ STATIC_FILE("pico-sdk.bsd3.txt",
+ .data_start = _binary_static_Documentation_YOUR_RIGHTS_AND_OBLIGATIONS_pico_sdk_bsd3_txt_start,
+ .data_end = _binary_static_Documentation_YOUR_RIGHTS_AND_OBLIGATIONS_pico_sdk_bsd3_txt_end),
+ STATIC_FILE("tinyusb.mit.txt",
+ .data_start = _binary_static_Documentation_YOUR_RIGHTS_AND_OBLIGATIONS_tinyusb_mit_txt_start,
+ .data_end = _binary_static_Documentation_YOUR_RIGHTS_AND_OBLIGATIONS_tinyusb_mit_txt_end),
+ ),
+ STATIC_FILE("harness_rom_bin.txt",
+ .data_start = _binary_static_Documentation_harness_rom_bin_txt_start,
+ .data_end = _binary_static_Documentation_harness_rom_bin_txt_end),
+ STATIC_FILE("harness_flash_bin.txt",
+ .data_start = _binary_static_Documentation_harness_flash_bin_txt_start,
+ .data_end = _binary_static_Documentation_harness_flash_bin_txt_end),
+ STATIC_FILE("harness_uptime_txt.txt",
+ .data_start = _binary_static_Documentation_harness_uptime_txt_txt_start,
+ .data_end = _binary_static_Documentation_harness_uptime_txt_txt_end),
+ ),
+ STATIC_DIR("harness",
+ STATIC_FILE("rom.bin",
+ .data_start = (void*)0x00000000,
+ .data_size = 16*1024),
+ API_FILE("flash.bin",
+ flash),
+ API_FILE("uptime.txt",
+ uptime),
+ // TODO: system.log
+ // TODO: proc.txt
+ // TODO: cpuinfo.txt
+ // TODO: ctl
+ ),
+ STATIC_DIR("dut",
+ // TODO: hdmi.nut
+ // TODO: uart.txt
+ // TODO: usb-keyboard.txt
+ ),
+ );
+
+static lib9p_srv_file_or_error get_root(struct lib9p_srv_ctx *LM_UNUSED(ctx), struct lib9p_s LM_UNUSED(treename)) {
+ return ERROR_NEW_VAL(lib9p_srv_file, root);
+}
+
+/* Code ***********************************************************************/
+
+/*
+static COROUTINE hello_world_cr(void *_chan) {
const char *msg = "Hello world!\n";
usb_keyboard_rpc_t *chan = _chan;
cr_begin();
@@ -29,28 +129,52 @@ COROUTINE hello_world_cr(void *_chan) {
for (size_t i = 0;; i = (i+1) % strlen(msg)) {
int result = usb_keyboard_rpc_send_req(chan, (uint32_t)msg[i]);
if (result < 1) {
- errorf("error sending rune U+%d", (uint32_t)msg[i]);
+ log_errorln("error sending rune U+", msg[i]);
break;
}
}
cr_end();
}
+*/
+
+struct {
+ struct rp2040_hwspi dev_spi;
+ struct w5500 dev_w5500;
+ usb_keyboard_rpc_t keyboard_chan;
+ uint16_t usb_serial[sizeof(uint64_t)*2]; /* UTF-16 */
+ struct lib9p_srv srv;
+} globals;
-COROUTINE dhcp_cr(void *_chip) {
- struct w5500 *chip = _chip;
+static COROUTINE dhcp_cr(void *) {
cr_begin();
- dhcp_client_main(chip, "harness");
+ dhcp_client_main(LO_BOX(net_iface, &globals.dev_w5500), "harness");
cr_end();
}
-struct {
- struct rp2040_hwspi dev_spi;
- struct w5500 dev_w5500;
- usb_keyboard_rpc_t keyboard_chan;
-} globals;
+static COROUTINE read9p_cr(void *) {
+ cr_begin();
+
+ lo_interface net_iface iface = LO_BOX(net_iface, &globals.dev_w5500);
+ lo_interface net_stream_listener listener = LO_CALL(iface, tcp_listen, LIB9P_DEFAULT_PORT_9FS);
+
+ lib9p_srv_accept_and_read_loop(&globals.srv, listener);
+
+ cr_end();
+}
+
+static COROUTINE write9p_cr(void *) {
+ cr_begin();
+
+ lib9p_srv_worker_loop(&globals.srv);
+
+ cr_end();
+}
+
+static const char *const hexdig = "0123456789ABCDEF";
+static_assert(_CONFIG_9P_MAX_REQS <= 16);
COROUTINE init_cr(void *) {
cr_begin();
@@ -71,12 +195,16 @@ COROUTINE init_cr(void *) {
rp2040_hwspi_init(&globals.dev_spi, "W5500", RP2040_HWSPI_0,
SPI_MODE_0, /* the W5500 supports mode 0 or mode 3 */
- 60*1000*1000, /* as close to the W5500's max rate of 80MHz as we can without hwspi borking */
- 16, /* PIN_MISO */
- 19, /* PIN_MOSI */
- 18, /* PIN_CLK */
- 17); /* PIN_CS */
- w5500_init(&globals.dev_w5500, "W5500", &globals.dev_spi,
+ 42500000, /* min(w5500, hwspi); w5500=80MHz; hwspi=42.5MHz, see rp2040_hwspi.h for a comment about why this is so low */
+ 30, /* W5500 datasheet says min(T_CS = SCSn High Time) = 30ns */
+ 0, /* bogus write write data when doing a read */
+ 16, /* PIN_MISO */
+ 19, /* PIN_MOSI */
+ 18, /* PIN_CLK */
+ 17, /* PIN_CS */
+ 0, 1, 2, 3); /* DMA channels */
+ w5500_init(&globals.dev_w5500, "W5500",
+ LO_BOX(spi, &globals.dev_spi),
21, /* PIN_INTR */
20, /* PIN_RESET */
((struct net_eth_addr){{
@@ -86,23 +214,40 @@ COROUTINE init_cr(void *) {
flash_id24[0], flash_id24[1], flash_id24[2],
}}));
- usb_common_earlyinit();
+ static_assert(sizeof(flash_id64)*2 == LM_ARRAY_LEN(globals.usb_serial));
+ for (size_t i = 0; i < LM_ARRAY_LEN(globals.usb_serial); i++)
+ globals.usb_serial[i] = hexdig[(flash_id64 >> ((sizeof(flash_id64)*8)-((i+1)*4))) & 0xF];
+ usb_common_earlyinit(globals.usb_serial, sizeof(globals.usb_serial));
usb_keyboard_init();
usb_common_lateinit();
- globals.keyboard_chan = (usb_keyboard_rpc_t){0};
+ globals.keyboard_chan = (usb_keyboard_rpc_t){};
+
+ globals.srv.rootdir = get_root;
/* set up coroutines **************************************************/
coroutine_add("usb_common", usb_common_cr, NULL);
coroutine_add("usb_keyboard", usb_keyboard_cr, &globals.keyboard_chan);
- //coroutine_add("hello_world", hello_world_cr, &keyboard_chan);
- coroutine_add_with_stack_size(4*1024, "dhcp", dhcp_cr, &globals.dev_w5500);
+ //coroutine_add("hello_world", hello_world_cr, &globals.keyboard_chan);
+ coroutine_add("dhcp", dhcp_cr, NULL);
+ for (int i = 0; i < _CONFIG_9P_MAX_CONNS; i++) {
+ char name[] = {'r', 'e', 'a', 'd', '-', hexdig[i], '\0'};
+ coroutine_add(name, read9p_cr, NULL);
+ }
+ for (int i = 0; i < _CONFIG_9P_MAX_REQS; i++) {
+ char name[] = {'w', 'r', 'i', 't', 'e', '-', hexdig[i], '\0'};
+ coroutine_add(name, write9p_cr, NULL);
+ }
cr_exit();
}
int main() {
+ bootclock = rp2040_hwtimer(0);
stdio_uart_init();
+ /* char *hdr = "=" * (80-strlen("info : MAIN: ")); */
+ log_infoln("===================================================================");
coroutine_add("init", init_cr, NULL);
coroutine_main();
+ assert_notreached("all coroutines exited");
}
diff --git a/cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS.md b/cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS.md
new file mode 100644
index 0000000..b3fc12e
--- /dev/null
+++ b/cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS.md
@@ -0,0 +1,29 @@
+<!--
+ Documentation/YOUR_RIGHTS_AND_OBLIGATIONS.md - Overview of your
+ rights and obligations with regard to the SBC-Harness firmware
+
+ Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+The firmware running on the SBC-Harness is Free Software -- if you
+have access to this file, then you have the freedom to use, study,
+share, and improve the firmware; as long as you follow a few
+conditions that basically amount to "paying it forward".
+
+The precise terms of your rights and obligations are given in the
+files in the `YOUR_RIGHTS_AND_OBLIGATIONS/` directory. You must obey
+each of the files in that directory when distributing
+verbatim-or-modified copies of the firmware:
+
+ - `agpl-3.0.txt`: The firmware is as-a-whole licensed to you under
+ the terms of the GNU Affero General Public License (GNU AGPL) as
+ published by the Free Software Foundation, either version 3 of the
+ License, or (at your option) any later version. This is the main
+ document that protects your rights and defines your obligations.
+
+ - other files: The firmware makes use of other code that is under
+ various other licenses. When taken with the AGPL, they amount to
+ requiring that you pass along the copyright and license text in
+ those files when you distribute verbatim-or-modified copies of the
+ firmware.
diff --git a/cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/agpl-3.0.txt b/cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/agpl-3.0.txt
new file mode 120000
index 0000000..8a9b6f3
--- /dev/null
+++ b/cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/agpl-3.0.txt
@@ -0,0 +1 @@
+../../../../../COPYING.txt \ No newline at end of file
diff --git a/cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/dhcp.bsd3-mit.txt b/cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/dhcp.bsd3-mit.txt
new file mode 120000
index 0000000..0277bc8
--- /dev/null
+++ b/cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/dhcp.bsd3-mit.txt
@@ -0,0 +1 @@
+../../../../../3rd-party/COPYING.wiznet-dhcp.txt \ No newline at end of file
diff --git a/cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/newlib.txt b/cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/newlib.txt
new file mode 120000
index 0000000..5c5939c
--- /dev/null
+++ b/cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/newlib.txt
@@ -0,0 +1 @@
+../../../../../3rd-party/COPYING.newlib.txt \ No newline at end of file
diff --git a/cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/pico-sdk.bsd3.txt b/cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/pico-sdk.bsd3.txt
new file mode 120000
index 0000000..52c4374
--- /dev/null
+++ b/cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/pico-sdk.bsd3.txt
@@ -0,0 +1 @@
+../../../../../3rd-party/pico-sdk/LICENSE.TXT \ No newline at end of file
diff --git a/cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/tinyusb.mit.txt b/cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/tinyusb.mit.txt
new file mode 120000
index 0000000..22a67cf
--- /dev/null
+++ b/cmd/sbc_harness/static/Documentation/YOUR_RIGHTS_AND_OBLIGATIONS/tinyusb.mit.txt
@@ -0,0 +1 @@
+../../../../../3rd-party/pico-sdk/lib/tinyusb/LICENSE \ No newline at end of file
diff --git a/cmd/sbc_harness/static/Documentation/harness_flash_bin.txt b/cmd/sbc_harness/static/Documentation/harness_flash_bin.txt
new file mode 100644
index 0000000..1b58d6d
--- /dev/null
+++ b/cmd/sbc_harness/static/Documentation/harness_flash_bin.txt
@@ -0,0 +1,43 @@
+NAME
+ /harness/flash.bin
+
+DESCRIPTION
+ Access the flash storage chip (where the harness firmware is
+ stored).
+
+ Reading from the file reads the raw flash contents.
+
+ Writing to the file does not accept raw data; instead the data
+ must be encapsulated in the [Intel Hex] format, with the Hex
+ file writing to the region 0x1000_0000-0x1010_0000. While less
+ convenient than verbatim data, the Hex format provides in-band
+ checksums and EOF-markers that help prevent rendering the
+ harness unbootable with corrupted or truncated writes. Any
+ holes in the Intel Hex file are filled with "1" bits. Once a
+ complete Intel Hex file has been written without error and the
+ file is closed, the harness reboots into the new firmware.
+
+BUGS
+ - The size of the chip is configured at compile-time. If the
+ firmware is loaded onto hardware with a larger flash chip
+ than it was compiled for, then the upper part of the chip
+ will not be accessible with this file. If the firmware is
+ loaded onto hardware with a smaller flash chip than it was
+ compiled for, then accessing the missing upper part of the
+ chip will crash.
+
+ - When writing to the flash using this file, only half of the
+ chip capacity is usable (the size of the region specified
+ above is half the chip size); 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.
+
+SEE ALSO:
+ [Intel Hex]: https://archive.org/details/IntelHEXStandard
+
+AUTHOR
+ Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/cmd/sbc_harness/static/Documentation/harness_rom_bin.txt b/cmd/sbc_harness/static/Documentation/harness_rom_bin.txt
new file mode 100644
index 0000000..63fd0a3
--- /dev/null
+++ b/cmd/sbc_harness/static/Documentation/harness_rom_bin.txt
@@ -0,0 +1,41 @@
+NAME
+ /harness/rom.bin
+
+DESCRIPTION
+ Read access to the RP2040 CPU's ROM. This contains code that
+ initializes the chip to load the main firmware from the
+ external flash chip, provides a failsafe USB-programmable
+ mode, and provides a few functions that the main firmware can
+ call to.
+
+BUGS
+ This ROM is programmed into the chip at the factory; revising
+ it means issuing a new revison of the RP2040 CPU. So while
+ the source code to the ROM is freely available to be used,
+ studied, and shared; one cannot install modified versions onto
+ the CPU.
+
+HISTORY
+ - RP2040 B0 : chips manufactured before September 2020 or so
+ - RP2040 B1 : chips manufactured after September 2020 or so
+ - Released to the public January 2021; chance whether you get
+ a B0 or a B1 chip.
+ - RP2040 B2 : released September 2021
+
+ Printed on the physical CPU is a label that indicates which
+ revision it is. For example:
+
+ RP2-B2 21/24
+
+ indicates that it is the "B2" revision (and was manufactured
+ the 21st week (late May) of 2024).
+
+SEE ALSO
+ - /harness/cpuinfo.txt can report which CPU version you have.
+
+ - The source code to each ROM revision is published at
+ https://github.com/raspberrypi/pico-bootrom-rp2040
+
+AUTHOR
+ Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/cmd/sbc_harness/static/Documentation/harness_uptime_txt.txt b/cmd/sbc_harness/static/Documentation/harness_uptime_txt.txt
new file mode 100644
index 0000000..09e9243
--- /dev/null
+++ b/cmd/sbc_harness/static/Documentation/harness_uptime_txt.txt
@@ -0,0 +1,27 @@
+NAME
+ /harness/uptime.txt
+
+DESCRIPTION
+ Reading this file gives a text string of the format
+
+ {ns}ns
+ [[[[{y}y]{d}d ]{h}h]{m}m]{s.09}s
+
+ That is: the first line is simply the harness's uptime in an
+ integer number of nanoseconds; and the second line is this
+ same number in a more human-readable form; divided into
+ seconds, minutes, hours, days, and years.
+
+BUGS
+ - Using nanoseconds gives the illusion of more precision than
+ there actually is; the harness' clock only has microsecond
+ resolution; the last 3 digits of the returned nanosecond
+ count will always be 0.
+
+ - In the human-readable form, the days are always exactly
+ 60*60*24 seconds (leap seconds are ignored), and the years
+ are always exactly 365 days (leap years are ignored).
+
+AUTHOR
+ Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/cmd/sbc_harness/tests/test_ihex.c b/cmd/sbc_harness/tests/test_ihex.c
new file mode 100644
index 0000000..143e3b4
--- /dev/null
+++ b/cmd/sbc_harness/tests/test_ihex.c
@@ -0,0 +1,128 @@
+/* cmd/sbc_harness/tests/test_ihex.c - Tests for ihex.c
+ *
+ * Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <stdio.h> /* for putchar() */
+#include <string.h>
+
+#include <libmisc/fmt.h>
+
+#include "ihex.h"
+
+struct stdout { size_t len; };
+LO_IMPLEMENTATION_STATIC(fmt_dest, struct stdout, stdout);
+static void stdout_putb(struct stdout *self, uint8_t b) {
+ putchar(b);
+ self->len++;
+}
+static size_t stdout_tell(struct stdout *self) {
+ return self->len;
+}
+
+static lo_interface fmt_dest fmt_stdout = lo_box_stdout_as_fmt_dest(&((struct stdout){}));
+
+#define test_assert(expr) do { \
+ if (!(expr)) \
+ fmt_print( \
+ fmt_stdout, \
+ "test failure: ", __FILE__, ":", __LINE__, ":", __func__, \
+ ": " #expr "\n"); \
+ } while (0)
+
+static char *input =
+ /* ,-byte count
+ * | ,-address
+ * | | ,-record type
+ * | | | ,- checksum
+ *[][--][][..............................][]\r\n */
+ ":020000041000EA\r\n" /* base_addr = linear(0x1000) = 0x1000<<16 */
+ ":1000000000B5324B212058609868022188439860DF\r\n" /* memcpy(chip[base_addr+0x0000], "\x00\xB5\x32\x4B\x21\x20\x58\x60\x98\x68\x02\x21\x88\x43\x98\x60", 16) */
+ ":10001000D860186158612E4B002199600221596106\r\n" /* memcpy(chip[base_addr+0x0010], "\xD8\x60\x18\x61\x58\x61\x2E\x4B\x00\x21\x99\x60\x02\x21\x59\x61", 16) */
+ ":10BE9C000000000000000000000000000000000096\r\n" /* memcpy(chip[base_addr+0xBE9C], "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 16) */
+ ":04BEAC00CC4A00205C\r\n" /* memcpy(chip[base_addr+0xBEAC], "\x00\xCC\x4A\x00\x20", 5) */
+ ":04000005100001E9FD\r\n" /* start_exec_at = linear(0x100001E9) */
+ ":00000001FF\r\n"; /* EOF */
+
+static int cnt = 0;
+
+static error handle_data(void *, uint32_t off, uint8_t count, uint8_t *dat) {
+ switch (cnt) {
+ case 0:
+ test_assert(off == UINT32_C(0x10000000));
+ test_assert(count == 16);
+ test_assert(memcmp(dat, "\x00\xB5\x32\x4B\x21\x20\x58\x60\x98\x68\x02\x21\x88\x43\x98\x60", 16) == 0);
+ break;
+ case 1:
+ test_assert(off == UINT32_C(0x10000010));
+ test_assert(count == 16);
+ test_assert(memcmp(dat, "\xD8\x60\x18\x61\x58\x61\x2E\x4B\x00\x21\x99\x60\x02\x21\x59\x61", 16) == 0);
+ break;
+ case 2:
+ test_assert(off == UINT32_C(0x1000BE9C));
+ test_assert(count == 16);
+ test_assert(memcmp(dat, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 16) == 0);
+ break;
+ case 4:
+ test_assert(off == UINT32_C(0x1000BE9C));
+ test_assert(count == 5);
+ test_assert(memcmp(dat, "\x00\xCC\x4A\x00\x20", 5) == 0);
+ break;
+ default:
+ test_assert(false);
+ }
+ cnt++;
+ return ERROR_NULL;
+}
+static error handle_set_exec_start_seg(void *, uint16_t LM_UNUSED(cs), uint16_t LM_UNUSED(ip)) {
+ switch (cnt) {
+ default:
+ test_assert(false);
+ }
+ cnt++;
+ return ERROR_NULL;
+}
+static error handle_set_exec_start_lin(void *, uint32_t eip) {
+ switch (cnt) {
+ case 5:
+ test_assert(eip == UINT32_C(0x100001E9));
+ break;
+ default:
+ test_assert(false);
+ }
+ cnt++;
+ return ERROR_NULL;
+}
+static error handle_eof(void *) {
+ switch (cnt) {
+ case 6:
+ break;
+ default:
+ test_assert(false);
+ }
+ cnt++;
+ return ERROR_NULL;
+}
+
+int main() {
+ struct ihex_decoder dec = {
+ .handle_data = handle_data,
+ .handle_set_exec_start_seg = handle_set_exec_start_seg,
+ .handle_set_exec_start_lin = handle_set_exec_start_lin,
+ .handle_eof = handle_eof,
+ };
+
+ size_t_and_error ret = ihex_decoder_writev(&dec, &((struct iovec){
+ .iov_base = input,
+ .iov_len = strlen(input),
+ }), 1);
+ fmt_print(fmt_stdout,
+ "ret = (", ret.size_t, ", ", (error, ret.err), ")\n",
+ "cnt = ", cnt, "\n");
+ test_assert(ret.size_t == strlen(input));
+ test_assert(ERROR_IS_NULL(ret.err));
+ test_assert(cnt == 6);
+
+ return 0;
+}
diff --git a/cmd/sbc_harness/tusb_log.c b/cmd/sbc_harness/tusb_log.c
new file mode 100644
index 0000000..09fe755
--- /dev/null
+++ b/cmd/sbc_harness/tusb_log.c
@@ -0,0 +1,22 @@
+/* sbc_harness/tusb_log.c - Logger for tusb_config.h
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#define LOG_NAME TINY_USB
+#include <libmisc/log.h>
+
+#include "tusb_config.h"
+
+void _libmisc_tu_mess_failed(const char *expr,
+ const char *file, unsigned int line, const char *func) {
+ log_errorln(file, ":", line, ":", func, "(): assertion ", (qstr, expr), " failed");
+}
+
+void _libmisc_tu_print_str(const char *x) { fmt_print_str(_log_dest, x); }
+void _libmisc_tu_print_byte(uint8_t x) { fmt_print_byte(_log_dest, x); }
+void _libmisc_tu_print_base10(unsigned long x) { fmt_print_base10(_log_dest, x); }
+void _libmisc_tu_print_base16(unsigned long x) { fmt_print(_log_dest, "0x", (base16, x)); }
+void _libmisc_tu_print_base16_u8(uint8_t x) { fmt_print_base16_u8_(_log_dest, x); }
+void _libmisc_tu_print_base16_u16(uint16_t x) { fmt_print_base16_u16_(_log_dest, x); }
diff --git a/cmd/sbc_harness/usb_keyboard.c b/cmd/sbc_harness/usb_keyboard.c
index 637921e..dcd4465 100644
--- a/cmd/sbc_harness/usb_keyboard.c
+++ b/cmd/sbc_harness/usb_keyboard.c
@@ -1,18 +1,17 @@
/* sbc_harness/usb_keyboard.c - Implementation of a USB keyboard device
*
- * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <tusb.h>
+#include <libmisc/macro.h>
#include <libusb/tusb_helpers.h> /* for TUD_ENDPOINT_IN */
#include <libusb/usb_common.h>
#include "usb_keyboard.h"
-#define UNUSED(name)
-
/**
* A USB-HID "Report Descriptor" (see USB-HID 1.11 §6.2.2 "Report
* Descriptor") describing a keyboard.
@@ -21,10 +20,9 @@ static uint8_t const hid_report_descriptor_keyboard[] = { TUD_HID_REPORT_DESC_KE
static uint8_t kbd_ifc = 0;
-void usb_keyboard_init() {
+void usb_keyboard_init(void) {
if (kbd_ifc)
return;
- usb_common_earlyinit();
kbd_ifc = usb_add_interface(cfgnum_std, TUD_HID_DESC_LEN, (uint8_t[]){
/* USB-HID input-only descriptor for inclusion in the config descriptor; consisting of 3 parts:
@@ -51,13 +49,13 @@ COROUTINE usb_keyboard_cr(void *_chan) {
uint8_t report_id = 0;
uint8_t modifier = 0;
- uint8_t keycodes[6] = {0};
+ uint8_t keycodes[6] = {};
for (;;) {
while (!tud_hid_n_ready(kbd_ifc))
cr_yield();
- if (usb_keyboard_rpc_can_recv_req(chan)) {
- usb_keyboard_rpc_req_t req = usb_keyboard_rpc_recv_req(chan);
+ if (cr_rpc_can_recv_req(chan)) {
+ usb_keyboard_rpc_req_t req = cr_rpc_recv_req(chan);
uint32_t rune = req.req;
modifier = ascii2keycode[rune][0] ? KEYBOARD_MODIFIER_LEFTSHIFT : 0;
@@ -71,7 +69,7 @@ COROUTINE usb_keyboard_cr(void *_chan) {
keycodes[0] = 0;
tud_hid_n_keyboard_report(kbd_ifc, report_id, modifier, keycodes);
- usb_keyboard_rpc_send_resp(req, 1);
+ cr_rpc_send_resp(req, 1);
} else {
modifier = 0;
keycodes[0] = 0;
@@ -87,29 +85,30 @@ COROUTINE usb_keyboard_cr(void *_chan) {
* §6.2.2 "Report Descriptor") for the given index.
*/
uint8_t const *tud_hid_descriptor_report_cb(uint8_t index) {
- static uint8_t const *reports[] = {
- hid_report_descriptor_keyboard,
- };
- if (index >= TU_ARRAY_SIZE(reports))
- return NULL;
- return reports[index];
+ static uint8_t const *reports[] = {
+ hid_report_descriptor_keyboard,
+ };
+ if (index >= TU_ARRAY_SIZE(reports))
+ return NULL;
+ return reports[index];
}
-uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen)
-{
- // TODO not Implemented
- (void) instance;
- (void) report_id;
- (void) report_type;
- (void) buffer;
- (void) reqlen;
-
+uint16_t tud_hid_get_report_cb(uint8_t LM_UNUSED(instance),
+ uint8_t LM_UNUSED(report_id),
+ hid_report_type_t LM_UNUSED(report_type),
+ uint8_t* LM_UNUSED(buffer),
+ uint16_t LM_UNUSED(reqlen)) {
+ // TODO not implemented
return 0;
}
// Invoked when received SET_REPORT control request or
// received data on OUT endpoint ( Report ID = 0, Type = 0 )
-void tud_hid_set_report_cb(uint8_t UNUSED(instance), uint8_t UNUSED(report_id), hid_report_type_t UNUSED(report_type), uint8_t const *UNUSED(buffer), uint16_t UNUSED(bufsize))
+void tud_hid_set_report_cb(uint8_t LM_UNUSED(instance),
+ uint8_t LM_UNUSED(report_id),
+ hid_report_type_t LM_UNUSED(report_type),
+ uint8_t const *LM_UNUSED(buffer),
+ uint16_t LM_UNUSED(bufsize))
{
// TODO not implemented
}
diff --git a/cmd/sbc_harness/usb_keyboard.h b/cmd/sbc_harness/usb_keyboard.h
index 210014d..cf8483b 100644
--- a/cmd/sbc_harness/usb_keyboard.h
+++ b/cmd/sbc_harness/usb_keyboard.h
@@ -1,6 +1,6 @@
/* sbc_harness/usb_keyboard.h - Implementation of a USB keyboard device
*
- * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
@@ -12,7 +12,7 @@
#include <libcr/coroutine.h> /* for COROUTINE */
#include <libcr_ipc/rpc.h> /* for CR_RPC_DECLARE */
-CR_RPC_DECLARE(usb_keyboard_rpc, uint32_t, int)
+CR_RPC_DECLARE(usb_keyboard_rpc, uint32_t, int);
void usb_keyboard_init(void);
COROUTINE usb_keyboard_cr(void *arg);