From 7affb1b209b5201bf3061bdf783116876af7ba56 Mon Sep 17 00:00:00 2001 From: R Date: Sun, 21 Apr 2024 05:51:35 +0100 Subject: [PATCH] first pass at comments --- README.md | 13 ++++++++ bootloader.c | 88 ++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 85 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index dea0720..2c58a82 100644 --- a/README.md +++ b/README.md @@ -1 +1,14 @@ # WCH RISC-V UF2 Bootloader + +- Fits in $\leq$ 4096 bytes +- Tested with MounRiver GCC V1.91 + - Code size is improved through use of "XW" instructions +- Supports USBD peripheral *only* (i.e. not USBFS) + - USBD and USBFS are completely different, and the QFN28 package (which is available in largest quantities on LCSC) only bonds out USBD +- Runs off of internal 8 MHz oscillator (despite stability concerns) + - The HSI oscillator's tolerance (-1.0% to +1.6%) is well outside of the range specified for USB full speed ($\pm$ 0.25%), but it seems to work in practice n=1 ¯\\\_(ツ)\_/¯ +- Should work across most of the CH32V2xx and CH32V3xx family with appropriate changes to hardcoded constants. +- Allows both download to flash and to SRAM with the "not main flash" flag (this is how the RP2040 bootrom works) + - The entire size of the SRAM can be used, as USBD contains its own buffer memory independent of the main SRAM +- Auto-reboot on complete download, _but_ **only** works if the download is sufficiently small. Larger downloads will of course still flash, but they will not trigger auto-reboot and a manual reboot will be required. +- Lots of nasty code golfing tricks -- see comments in [bootloader.c](bootloader.c) \ No newline at end of file diff --git a/bootloader.c b/bootloader.c index 15187a0..68e07df 100644 --- a/bootloader.c +++ b/bootloader.c @@ -1,3 +1,5 @@ +// CH32V UF2 bootloader, size-optimized (target: <= 4096 bytes) + #include typedef struct USBD_descriptor { @@ -25,7 +27,13 @@ typedef struct USBD_descriptor { // ram for stashing variables // +0x200 // <256-byte flash page buffer> +// In addition to the expected USB buffers, this RAM is used to store program state. +// This allows the *entire* SRAM to be used when downloading to SRAM. +// Note that every buffer that needs to be sent over USB must be 16-bit aligned. +// This is assumed by copying code (which will copy in 16-bit chunks). + +// Device descriptor const uint8_t USB_DEV_DESC[18] __attribute__((aligned(2))) = { 18, // bLength 1, // bDescriptorType @@ -44,6 +52,7 @@ const uint8_t USB_DEV_DESC[18] __attribute__((aligned(2))) = { }; const uint8_t USB_CONF_DESC[0x20] __attribute__((aligned(2))) = { + // Configuration descriptor 9, // bLength 2, // bDescriptorType 0x20, 0x00, // wTotalLength @@ -53,6 +62,7 @@ const uint8_t USB_CONF_DESC[0x20] __attribute__((aligned(2))) = { 0x80, // bmAttributes 250, // bMaxPower + // Interface descriptor 9, // bLength 4, // bDescriptorType 0, // bInterfaceNumber @@ -63,6 +73,7 @@ const uint8_t USB_CONF_DESC[0x20] __attribute__((aligned(2))) = { 0x50, // bInterfaceProtocol 0, // iInterface + // EP1 IN endpoint descriptor 7, // bLength 5, // bDescriptorType 0x81, // bEndpointAddress @@ -70,6 +81,7 @@ const uint8_t USB_CONF_DESC[0x20] __attribute__((aligned(2))) = { 0x40, 0x00, // wMaxPacketSize 0, // bInterval + // EP1 OUT endpoint descriptor 7, // bLength 5, // bDescriptorType 0x01, // bEndpointAddress @@ -78,24 +90,34 @@ const uint8_t USB_CONF_DESC[0x20] __attribute__((aligned(2))) = { 0, // bInterval }; +// Must be UTF-16, and the first character is a *manually-calculated* total length +// (combined with a 0x03 string descriptor type) const uint16_t USB_MANUF[13] = u"\u031aArcaneNibble"; const uint16_t USB_PRODUCT[15] = u"\u031eCH32V UF2 Boot"; +// Used for outputting serial number from electronic signature bytes const uint8_t HEXLUT[16] = "0123456789ABCDEF"; +// SCSI INQUIRY standard response const uint8_t INQUIRY_RESPONSE[36] __attribute__((aligned(2))) = { - 0x00, - 0x80, - 0x04, - 0x02, - 0x1F, - 0x00, + 0x00, // direct access block device + 0x80, // RMB removable media + 0x04, // SPC-2 + 0x02, // fixed, response data format + 0x1F, // additional length + 0x00, // xxx mostly obsolete junk 0x00, 0x00, + // Vendor 'A', 'r', 'c', 'a', 'n', 'e', 'N', 'b', + // Product 'C', 'H', '3', '2', 'V', ' ', 'U', 'F', '2', ' ', 'B', 'o', 'o', 't', ' ', ' ', + // Revision ' ', ' ', ' ', ' ', }; +// FAT16 boot sector +// Most of these parameters *cannot* be changed, +// as synthesize_block assumes a particular layout const uint8_t BOOT_SECTOR[0x3e] __attribute__((aligned(2))) = { 0xeb, 0x3c, 0x90, // jump 'A', 'r', 'c', 'a', 'n', 'e', 'N', 'b', // oem name @@ -116,13 +138,19 @@ const uint8_t BOOT_SECTOR[0x3e] __attribute__((aligned(2))) = { 'F', 'A', 'T', '1', '6', ' ', ' ', ' ', // fs type }; +// Change here to change UF2 data files const uint8_t INFO_UF2[70] __attribute__((aligned(2))) = "UF2 Bootloader v0.0.0\nModel: CH32V Generic\nBoard-ID: CH32Vxxx-Generic\n"; const uint8_t INDEX_HTM[119] __attribute__((aligned(2))) = "\n\n"; +// Change here for different chips #define THIS_CHIP_FLASH_MAX_SZ_BYTES (224 * 1024) -#define BOOTLOADER_RESERVED_SZ_BYTES (4 * 1024) #define THIS_CHIP_RAM_MAX_SZ_BYTES (20 * 1024) -#define FAMILY_ID 0xdeadbeef +#define BOOTLOADER_RESERVED_SZ_BYTES (4 * 1024) +#define FAMILY_ID 0x699b62ec + +// FAT16 root directory entries +// Most of these parameters *cannot* be changed, +// as synthesize_block assumes a particular layout const uint8_t ROOT_DIR[32 * 3] __attribute__((aligned(2))) = { 'C', 'H', '3', '2', 'V', ' ', 'U', 'F', '2', ' ', ' ', // name 0x08, // attributes (volume label) @@ -131,7 +159,7 @@ const uint8_t ROOT_DIR[32 * 3] __attribute__((aligned(2))) = { 0x00, 0x00, // reserved 0x00, 0x00, 0x00, 0x00, // timestamps 0x00, 0x00, // start cluster - 0x00, 0x00, 0x00, 0x00, // file size] + 0x00, 0x00, 0x00, 0x00, // file size 'I', 'N', 'F', 'O', '_', 'U', 'F', '2', 'T', 'X', 'T', // name 0x01, // attributes (RO) @@ -155,6 +183,12 @@ const uint8_t ROOT_DIR[32 * 3] __attribute__((aligned(2))) = { #define ESIG_UNIID(x) (*(volatile uint8_t*)(0x1FFFF7E8 + (x))) // XXX manual claims 96 bits but only 64 bits seem to actually be programmed? +// Note that all of this stuff is declared *extern* +// A significant amount of code size is being saved because the gp register +// is pointed at 0x40006000, right at the beginning of USBD RAM +// (and within reach of the USBD registers). +// The linker is then being instructed to perform gp-relative linker relaxation. +// Actual addresses for these variables are in the linker.lds file. extern volatile USBD_descriptor USB_DESCS[2]; extern volatile uint32_t USB_EP0_OUT[4]; extern volatile uint32_t USB_EP0_IN[4]; @@ -175,9 +209,12 @@ extern uint32_t CTRL_XFER_STATE_X; extern uint32_t CTRL_XFER_DESC_SZ; extern uint32_t USB_SECTOR_STASH[128]; +// Files larger than this won't cause an auto-reboot +// (there is not enough USBD RAM to mark which blocks have been received) #define MAX_AUTO_BOOT_BLOCKS (36 * 16 - 1) extern uint32_t UF2_GOT_BLOCKS[36]; +// Constants for USBD registers #define USB_EPTYPE_BULK 0b00 #define USB_EPTYPE_CONTROL 0b01 @@ -186,6 +223,7 @@ extern uint32_t UF2_GOT_BLOCKS[36]; #define USB_STAT_NAK 0b10 #define USB_STAT_ACK 0b11 +// Other registers we need #define R16_BKP_DATAR10 (*(volatile uint32_t*)0x40006C28) #define R32_PWR_CTLR (*(volatile uint32_t*)0x40007000) @@ -200,6 +238,7 @@ extern uint32_t UF2_GOT_BLOCKS[36]; #define R32_GPIOA_CFGHR (*(volatile uint32_t*)0x40010804) #define R32_GPIOA_BSHR (*(volatile uint32_t*)0x40010810) +// USBD registers are within range to perform gp relaxation // XXX wrong access size extern volatile uint32_t R16_USBD_EPR[16]; extern volatile uint16_t R16_USBD_CNTR; @@ -220,6 +259,9 @@ extern volatile uint16_t R16_USBD_DADDR; #define PFIC_CFGR (*(volatile uint32_t*)0xE000E048) +// Everything in this code is controlled by various state machines + +// USB device stack control transfer state handling // state_x[7:0] = addr #define STATE_SET_ADDR 0x00 // state_x[15:8] = byte pos @@ -228,17 +270,34 @@ extern volatile uint16_t R16_USBD_DADDR; #define STATE_GET_STR_SERIAL 0x02 #define STATE_CTRL_SIMPLE_IN 0x03 +// USB MSC state handling +// msc_state contains both the state *and* request sense: +// state[31:24] = additional sense code +// state[23:20] = sense key +// state[7:0] = state #define STATE_WANT_CBW 0x00 #define STATE_SENT_CSW 0x01 #define STATE_SENT_CSW_REBOOT 0x02 #define STATE_SENT_DATA_IN 0x03 -// state[10:8] = sector fragment +// state[10:8] = sector fragment #define STATE_SEND_MORE_READ 0x04 -// state[12] = uf2 good so far -// state[10:8] = sector fragment +// state[12] = uf2 good so far +// state[10:8] = sector fragment #define STATE_WAITING_FOR_WRITE 0x05 -// WARNING: only one level of non-inline subroutine calls are possible +// Because this code is running completely RAM-less +// (other than explicit usage of USBD RAM), +// there is no stack, and thus no space to spill registers. +// WARNING: This means that only one level of +// non-inline subroutine calls are possible. +// This is checked by manually searching the assembly listing +// for access to the sp register. + +// Just to be entirely sure, we've marked some functions as +// __attribute__((always_inline)). Other functions are +// explicitly *outlined* in order to save code size. +// This is not entirely stable and relies on hand-checking +// the generated assembly. __attribute__((always_inline)) static inline void set_ep_mode(uint32_t epidx, uint32_t epaddr, uint32_t eptype, uint32_t stat_rx, uint32_t stat_tx, uint32_t xtra, int clear_dtog) { uint32_t val = R16_USBD_EPR[epidx]; @@ -389,9 +448,6 @@ __attribute__((naked)) int main(void) { CTRL_XFER_DESC_SZ = 0; ADDRESS_HI = 0; - // bits [31:24] = additional sense code - // bits [23:20] = sense key - // bits [7:0] = state uint32_t msc_state = STATE_WANT_CBW; UF2_GOT_BLOCKS[35] = 0;