diff --git a/hw/bsp/ch32h417/boards/nanoch32h417/board.cmake b/hw/bsp/ch32h417/boards/nanoch32h417/board.cmake new file mode 100644 index 000000000..d09982d0a --- /dev/null +++ b/hw/bsp/ch32h417/boards/nanoch32h417/board.cmake @@ -0,0 +1,2 @@ +function(update_board TARGET) +endfunction() diff --git a/hw/bsp/ch32h417/boards/nanoch32h417/board.h b/hw/bsp/ch32h417/boards/nanoch32h417/board.h new file mode 100644 index 000000000..add41ce85 --- /dev/null +++ b/hw/bsp/ch32h417/boards/nanoch32h417/board.h @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Ha Thach (tinyusb.org) + * SPDX-License-Identifier: MIT + * + * This file is part of the TinyUSB stack. + */ + +/* metadata: + name: nanoCH32H417 + url: https://github.com/wuxx/nanoCH32H417 +*/ + +#ifndef BOARD_H_ +#define BOARD_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +// Green LED D2: PC2, active low (anode to VDDIO through 1K). The blue LED D1 is on PC3. +#define LED_PORT GPIOC +#define LED_PIN GPIO_Pin_2 +#define LED_STATE_ON 0 +#define LED_CLOCK RCC_HB2Periph_GPIOC + +// No user button on this board (S2/S3 belong to the on-board WCH-LinkE) + +// USART1 TX = PA9, RX = PA10, wired to the on-board WCH-LinkE virtual COM port +#ifndef CFG_BOARD_UART_BAUDRATE +#define CFG_BOARD_UART_BAUDRATE 115200 +#endif + +// Single device rhport 0: USB3.0 SuperSpeed (SPEED=super) or USB2.0 HighSpeed (SPEED=high), +// both on the USB 3.0 Type-A receptacle (the USB-C port is the full-speed OTG/PD controller, +// not supported) +#ifndef BOARD_TUD_RHPORT +#define BOARD_TUD_RHPORT 0 +#endif + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/hw/bsp/ch32h417/boards/nanoch32h417/board.mk b/hw/bsp/ch32h417/boards/nanoch32h417/board.mk new file mode 100644 index 000000000..7008629dc --- /dev/null +++ b/hw/bsp/ch32h417/boards/nanoch32h417/board.mk @@ -0,0 +1 @@ +# nanoCH32H417: no board-specific build flags diff --git a/hw/bsp/ch32h417/ch32h417_conf.h b/hw/bsp/ch32h417/ch32h417_conf.h new file mode 100644 index 000000000..56955e83b --- /dev/null +++ b/hw/bsp/ch32h417/ch32h417_conf.h @@ -0,0 +1,39 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// Library configuration header pulled in by the SDK's ch32h417.h. The EVT ships this file +// per-project (and its copy drags in the project's ch32h417_it.h); this BSP-owned version +// includes only the peripheral drivers the board and the USB dcds use. + +#ifndef CH32H417_CONF_H_ +#define CH32H417_CONF_H_ + +#include "ch32h417_dbgmcu.h" +#include "ch32h417_flash.h" +#include "ch32h417_gpio.h" +#include "ch32h417_rcc.h" +#include "ch32h417_iwdg.h" +#include "ch32h417_usart.h" + +#endif diff --git a/hw/bsp/ch32h417/family.c b/hw/bsp/ch32h417/family.c new file mode 100644 index 000000000..184e6ec45 --- /dev/null +++ b/hw/bsp/ch32h417/family.c @@ -0,0 +1,442 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Ha Thach (tinyusb.org) + * SPDX-License-Identifier: MIT + * + * This file is part of the TinyUSB stack. + */ + +/* metadata: + manufacturer: WCH +*/ + +// TinyUSB runs entirely on the V3F core (core 0, the boot core, HCLK = 100 MHz at the vendor +// default clock tree): single image at flash 0x0, no core-1 (V5F) wake-up. The vendor's own +// USBSS demo supports this Run_Core_V3F configuration. + +#include "ch32h417.h" + +#include "bsp/board_api.h" +#include "board.h" + +// Reflash backdoor: the USART1 magic-sequence hook, the UART flash loader and the flash-park +// window below. ON by default, on purpose: the chip's only SWD/SDI pins are the USB2 D+/D- pair, +// so once a USB image owns them this is the only way to get new firmware in (the test rig +// reflashes exclusively through it, test/hil/wch_uart_flash.py). A product build compiles it out +// with -DCFG_BOARD_CH32H417_UART_LOADER=0 - which also stops stray console-UART bytes from +// rebooting the board, at the price of needing physical access to recover it. +#ifndef CFG_BOARD_CH32H417_UART_LOADER +#define CFG_BOARD_CH32H417_UART_LOADER 1 +#endif + +// Crash watchdog: an ~8 s IWDG armed at the end of board_init, kicked from the 1 ms SysTick and, +// secondarily, from board_led_write(). OFF by default - it resets the chip whenever the core is +// halted longer than the period (any GDB/breakpoint session) and the board_led_write() kick is a +// contract no application signed up for. The HIL rig builds with -DCFG_BOARD_CH32H417_CRASH_WDT=1 +// so a crashed board reboots itself into the flash-park window (CFG_BOARD_CH32H417_UART_LOADER) +// and stays reflashable with no hands on the hardware. +#ifndef CFG_BOARD_CH32H417_CRASH_WDT +#define CFG_BOARD_CH32H417_CRASH_WDT 0 +#endif + +//--------------------------------------------------------------------+ +// Forward USB interrupt events to TinyUSB IRQ Handler +//--------------------------------------------------------------------+ + +// Prototypes for the (weak) vector-table handlers overridden here +void USBSS_IRQHandler(void); +void USBSS_LINK_IRQHandler(void); +void USBHS_IRQHandler(void); +#if CFG_BOARD_CH32H417_UART_LOADER +void USART1_IRQHandler(void); +#endif +void SysTick0_Handler(void); + +#if CFG_BOARD_CH32H417_UART_LOADER + +// Flash-park support: the HIL flasher writes {0x55,0xAA,'F','P'} to the WCH-LinkE virtual COM +// port; the RX interrupt below stamps this flag (in a NOLOAD region that survives the soft +// reset) and reboots. board_init then parks ~10 s before any USB init so the debug interface is +// guaranteed reachable for an unattended reflash - the chip's only SWD/SDI pins double as the +// USB2 D+/D- pair, and a running USB2(-fallback) image makes them undebuggable. +__attribute__((section(".noinit"))) static volatile uint32_t flash_park_flag; +__attribute__((section(".noinit"))) static volatile uint32_t flash_park_token; +#define FLASH_PARK_MAGIC 0x50414B31UL // 'P','A','K','1' +#define FLASH_LOAD_MAGIC 0x4C445231UL // 'L','D','R','1' - UART flash loader (see uart_flash_loader) +// The .noinit words are uninitialized SRAM on a cold boot: a request is honoured only when the +// companion token matches its magic, so random content cannot park the board (10 s stall) or drop +// it into the loader (looks bricked). board_init additionally ignores requests after a power-on +// reset, where SRAM content is undefined by definition. +#define FLASH_PARK_TOKEN(_magic) ((_magic) ^ 0xA5C33C5AUL) + +static void flash_park_request(uint32_t magic) { + flash_park_flag = magic; + flash_park_token = FLASH_PARK_TOKEN(magic); +} + +// Consume a pending request, clearing both words; returns 0 when there is none or the pair is not +// a valid request +static uint32_t flash_park_take(void) { + const uint32_t magic = flash_park_flag; + const bool valid = (flash_park_token == FLASH_PARK_TOKEN(magic)) && + ((magic == FLASH_PARK_MAGIC) || (magic == FLASH_LOAD_MAGIC)); + flash_park_flag = 0; + flash_park_token = 0; + return valid ? magic : 0; +} + +__attribute__((interrupt)) void USART1_IRQHandler(void) { + // {0x55,0xAA,'F','P'} = park for SWD flashing; {0x55,0xAA,'F','L'} = enter the UART loader + static const uint8_t seq[3] = {0x55, 0xAA, 'F'}; + static uint8_t idx = 0; + if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { + const uint8_t b = (uint8_t)USART_ReceiveData(USART1); // reading DATAR clears RXNE + if (idx < 3) { + idx = (b == seq[idx]) ? (uint8_t)(idx + 1) : ((b == seq[0]) ? 1 : 0); + } else { + if (b == 'P' || b == 'L') { + flash_park_request((b == 'P') ? FLASH_PARK_MAGIC : FLASH_LOAD_MAGIC); + NVIC_SystemReset(); + } + idx = (b == seq[0]) ? 1 : 0; + } + } +} + +//--------------------------------------------------------------------+ +// UART flash loader: reflash over the WCH-LinkE VCP with no debug-pin access. The chip's only +// SWD/SDI pins (PB8/PB9) are the USB2 D+/D- pair; with an untaped cable plugged into a host, +// the host PHY's line termination corrupts SDI beyond use, so wlink cannot flash. This loader +// needs only USART1 (dedicated pins), and the whole image executes from RAM_CODE, so rewriting +// flash under it is safe. Host side: test/hil/wch_uart_flash.py. +// +// Protocol (little-endian, image padded to a 1024-byte multiple by the host): +// dev : 'L' banner (~2 Hz) while waiting for a session +// host: 'H' len32 crc32 +// dev : erase [0x08000000, len8k) -> 'A' ok / 'E' error +// host: 'C' n16 (n == 1024) +// dev : program -> 'K' ok / 'E' error +// dev : after len bytes: CRC32 readback over flash -> 'D' + reset, or 'X' (host may retry) +//--------------------------------------------------------------------+ + +#define UART_LOAD_BASE 0x08000000UL + +static uint32_t loader_crc32(uint32_t crc, const uint8_t *p, uint32_t n) { + crc = ~crc; + while (n--) { + crc ^= *p++; + for (int k = 0; k < 8; k++) { + crc = (crc >> 1) ^ (0xEDB88320UL & (0u - (crc & 1u))); + } + } + return ~crc; +} + +static int loader_getc(uint32_t spins) { // -1 on timeout; ~10M spins/s at 100 MHz HCLK + while (spins--) { + if (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) != RESET) { + return (int)(USART_ReceiveData(USART1) & 0xFF); + } + } + return -1; +} + +static void loader_putc(uint8_t c) { + while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET) {} + USART_SendData(USART1, c); +} + +static bool loader_get_u32(uint32_t *out) { + uint32_t v = 0; + for (int i = 0; i < 4; i++) { + const int c = loader_getc(20000000u); // ~2 s + if (c < 0) { + return false; + } + v |= (uint32_t)c << (8 * i); + } + *out = v; + return true; +} + +__attribute__((noreturn)) static void uart_flash_loader(void) { + USART_ITConfig(USART1, USART_IT_RXNE, DISABLE); // polled from here on + NVIC_DisableIRQ(USART1_IRQn); + static uint8_t chunk[1024] __attribute__((aligned(4))); + + for (;;) { // one iteration = one flash session attempt + // Banner until the host opens a session + int c; + uint32_t blink = 0; + do { + loader_putc('L'); + board_led_write((++blink) & 1); + c = loader_getc(5000000u); // ~0.5 s + } while (c != 'H'); + + uint32_t len, crc; + if (!loader_get_u32(&len) || !loader_get_u32(&crc)) { + loader_putc('E'); + continue; + } + if (len == 0 || len > 448u * 1024u || (len & 1023u)) { + loader_putc('E'); + continue; + } + + const uint32_t erase_len = (len + 8191u) & ~8191u; // FLASH_ROM_ERASE needs 8 KB granularity + if (FLASH_ROM_ERASE(UART_LOAD_BASE, erase_len) != FLASH_COMPLETE) { + loader_putc('E'); + continue; + } + loader_putc('A'); + + uint32_t off = 0; + bool fail = false; + while (off < len) { + c = loader_getc(20000000u); // ~2 s for the next chunk header + if (c != 'C') { + fail = true; + break; + } + uint32_t n = 0; + for (int i = 0; i < 2 && !fail; i++) { + c = loader_getc(20000000u); + if (c < 0) { + fail = true; + } else { + n |= (uint32_t)c << (8 * i); + } + } + if (fail || n != sizeof(chunk)) { + fail = true; + break; + } + for (uint32_t i = 0; i < n; i++) { + c = loader_getc(20000000u); + if (c < 0) { + fail = true; + break; + } + chunk[i] = (uint8_t)c; + } + if (fail || FLASH_ROM_WRITE(UART_LOAD_BASE + off, (uint32_t *)(uintptr_t)chunk, n) != FLASH_COMPLETE) { + fail = true; + break; + } + off += n; + loader_putc('K'); + } + if (fail) { + loader_putc('E'); + continue; + } + + if (loader_crc32(0, (const uint8_t *)UART_LOAD_BASE, len) == crc) { + loader_putc('D'); + for (volatile uint32_t i = 0; i < 2000000; i++) {} // drain the last byte + NVIC_SystemReset(); + } + loader_putc('X'); + } +} + +#endif // CFG_BOARD_CH32H417_UART_LOADER + +// Single rhport 0: SPEED=super compiles the USB3 dcd (USBSS + USBSS_LINK vectors); SPEED=high +// the USB2 dcd (USBHS vector); the active dcd's dcd_int_handler dispatches on its own flag +// registers, so all vectors share ONE forwarder. Aliases (not identical function bodies) are +// required: gcc's identical-code-folding would otherwise rewrite one interrupt handler as a +// plain call into another, whose mret then skips the caller's epilogue and leaks its stack +// frame on every interrupt. Use the standard riscv interrupt attribute - WCH's +// "WCH-Interrupt-fast" is only understood by WCH's own gcc fork. +__attribute__((interrupt)) void USBSS_IRQHandler(void) { + tusb_int_handler(0, true); +} + +void USBSS_LINK_IRQHandler(void) __attribute__((alias("USBSS_IRQHandler"))); +void USBHS_IRQHandler(void) __attribute__((alias("USBSS_IRQHandler"))); + +// the USB3 dcd uses TIM12 for the SuperSpeed retrain backoff and, with +// CFG_TUD_WCH_USB30_FALLBACK, as the SuperSpeed-training timeout for the USB2 fallback +// (unused but harmless with SPEED=high: nothing enables the TIM12 interrupt there) +void TIM12_IRQHandler(void); +void TIM12_IRQHandler(void) __attribute__((alias("USBSS_IRQHandler"))); + +//--------------------------------------------------------------------+ +// SysTick (V3F core uses SysTick0; its ISR register holds the flags of both SysTicks) +//--------------------------------------------------------------------+ + +#if CFG_TUSB_OS == OPT_OS_NONE +volatile uint32_t system_ticks = 0; + +__attribute__((interrupt)) void SysTick0_Handler(void) { + SysTick0->ISR &= ~(1u << 0); // clear count-reached flag + system_ticks++; +#if CFG_BOARD_CH32H417_CRASH_WDT + // Primary watchdog kick: a live tick means the core is running. A halted core (debug halt, + // ebreak on a silent assert) stops the tick and the IWDG reset + auto-park below recovers the + // board. Applications are NOT required to blink the LED (see board_init). + IWDG_ReloadCounter(); +#endif +} + +uint32_t tusb_time_millis_api(void) { + return system_ticks; +} +#endif + +//--------------------------------------------------------------------+ +// Board Init +//--------------------------------------------------------------------+ + +void board_init(void) { + SystemInit(); // clock tree: SYSCLK 400 MHz, V3F core (HCLK) 100 MHz from HSE + SystemAndCoreClockUpdate(); + +#if CFG_TUSB_OS == OPT_OS_NONE + // 1 ms tick on SysTick0 (counts HCLK, auto-reload) + SysTick0->ISR &= ~(1u << 0); + SysTick0->CMP = SystemCoreClock / 1000 - 1; + SysTick0->CNT = 0; + SysTick0->CTLR = 0xF; + NVIC_SetPriority(SysTick0_IRQn, 0xF0); + NVIC_EnableIRQ(SysTick0_IRQn); +#endif + + // USART1 TX on PA9, RX on PA10 (AF7) - wired to the on-board WCH-LinkE virtual COM port +#ifdef CFG_BOARD_UART_BAUDRATE + RCC_HB2PeriphClockCmd(RCC_HB2Periph_AFIO | RCC_HB2Periph_USART1 | RCC_HB2Periph_GPIOA, ENABLE); + GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF7); + GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF7); + GPIO_InitTypeDef gpio_init = { + .GPIO_Pin = GPIO_Pin_9, + .GPIO_Speed = GPIO_Speed_Very_High, + .GPIO_Mode = GPIO_Mode_AF_PP, + }; + GPIO_Init(GPIOA, &gpio_init); + gpio_init.GPIO_Pin = GPIO_Pin_10; + gpio_init.GPIO_Mode = GPIO_Mode_IN_FLOATING; + GPIO_Init(GPIOA, &gpio_init); + + USART_InitTypeDef usart_init = { + .USART_BaudRate = CFG_BOARD_UART_BAUDRATE, + .USART_WordLength = USART_WordLength_8b, + .USART_StopBits = USART_StopBits_1, + .USART_Parity = USART_Parity_No, + .USART_Mode = USART_Mode_Tx | USART_Mode_Rx, + .USART_HardwareFlowControl = USART_HardwareFlowControl_None, + }; + USART_Init(USART1, &usart_init); +#if CFG_BOARD_CH32H417_UART_LOADER + USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // flash-park magic matcher (see USART1_IRQHandler) + NVIC_EnableIRQ(USART1_IRQn); +#endif + USART_Cmd(USART1, ENABLE); +#endif + + // LED + RCC_HB2PeriphClockCmd(LED_CLOCK, ENABLE); + GPIO_InitTypeDef led_init = { + .GPIO_Pin = LED_PIN, + .GPIO_Speed = GPIO_Speed_Very_High, + .GPIO_Mode = GPIO_Mode_Out_PP, + }; + GPIO_Init(LED_PORT, &led_init); + board_led_write(false); + +#if CFG_BOARD_CH32H417_UART_LOADER + // Flash-park window: hold here ~10 s with USB never initialized - PB8/PB9 stay in SWD/SDI mode + // and no interrupt load exists, so wlink can erase/flash unattended even when the normal image + // would own the USB2 pins (HS fallback) or storm the core. Fast LED blink signals the window. + // Entered when: + // - the previous run requested a park (magic over the WCH-LinkE VCP, see USART1_IRQHandler), or + // - the independent watchdog reset the chip (the previous image crashed and stopped kicking, + // CFG_BOARD_CH32H417_CRASH_WDT builds only): a crash-looping image thus re-opens this window + // every cycle, keeping the board recoverable with no hands and no reset line. + const bool wdg_reset = (RCC_GetFlagStatus(RCC_FLAG_IWDGRST) != RESET); + const bool por_reset = (RCC_GetFlagStatus(RCC_FLAG_PORRST) != RESET); + RCC_ClearFlag(); // reset flags are sticky: clear on every boot, else a cold boot's POR flag + // would shadow every later software reset + uint32_t park_req = flash_park_take(); + if (por_reset) { + park_req = 0; // SRAM (and with it the request words) is undefined after a power-on reset + } + if (wdg_reset || park_req == FLASH_PARK_MAGIC) { + for (uint32_t t = 0; t < 80; t++) { // 80 x ~125 ms = ~10 s at 100 MHz HCLK + board_led_write(t & 1); + for (volatile uint32_t i = 0; i < 2500000; i++) {} + } + board_led_write(false); + } + + // UART flash loader (requested via {0x55,0xAA,'F','L'} over the VCP): flash over USART1, + // for when the SWD/SDI pins are unusable (untaped USB2 cable plugged into a host). Runs + // before the watchdog is armed; never returns (resets after a successful flash). + if (park_req == FLASH_LOAD_MAGIC) { + uart_flash_loader(); + } +#endif // CFG_BOARD_CH32H417_UART_LOADER + + // Crash recovery (opt-in, see CFG_BOARD_CH32H417_CRASH_WDT): independent watchdog, ~8 s at + // LSI/256, kicked from the 1 ms SysTick (and, as a secondary, from board_led_write for OS + // builds that reconfigure the tick). Armed AFTER the park window above so the window itself + // cannot be cut short. Recovers a halted core (debug halt, ebreak); a firmware that merely + // stops calling tud_task still ticks and is NOT reset. NOTE: halting the core for more than + // the period watchdog-resets it (DBGMCU_Config(DBGMCU_IWDG_STOP) hangs when no debugger + // session is active - its csrw 0x7c0 faults - so the freeze-on-halt is deliberately not + // used); request a park first for long debug sessions, the window runs with the watchdog + // unarmed. Bare-metal only: the 1 ms SysTick kick above does not exist under an RTOS. +#if CFG_BOARD_CH32H417_CRASH_WDT && (CFG_TUSB_OS == OPT_OS_NONE) + RCC_LSICmd(ENABLE); + { // bounded: a dead LSI must not hang boot (the IWDG simply stays unarmed) + uint32_t timeout = 1000000; + while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET && --timeout) {} + if (timeout == 0) { return; } + } + IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); + IWDG_SetPrescaler(IWDG_Prescaler_256); + IWDG_SetReload(1000); // ~8 s at the ~32 kHz LSI + IWDG_ReloadCounter(); + IWDG_Enable(); +#endif +} + +//--------------------------------------------------------------------+ +// Board porting API +//--------------------------------------------------------------------+ + +void board_led_write(bool state) { +#if CFG_BOARD_CH32H417_CRASH_WDT + IWDG_ReloadCounter(); // secondary watchdog kick (primary is the SysTick handler, see board_init) +#endif + GPIO_WriteBit(LED_PORT, LED_PIN, (state ^ (LED_STATE_ON == 0)) ? Bit_SET : Bit_RESET); +} + +uint32_t board_button_read(void) { + return 0; // no user button on this board +} + +size_t board_get_unique_id(uint8_t id[], size_t max_len) { + volatile uint32_t* ch32_uuid = ((volatile uint32_t*) 0x1FFFF7E8UL); // ESIG unique ID (96 bit) + uint32_t uid[3] = {ch32_uuid[0], ch32_uuid[1], ch32_uuid[2]}; + const size_t len = max_len < sizeof(uid) ? max_len : sizeof(uid); + memcpy(id, uid, len); // byte copy: id[] need not be 4-byte aligned + return len; +} + +int board_uart_read(uint8_t* buf, int len) { + (void) buf; + (void) len; + return 0; +} + +int board_uart_write(void const* buf, int len) { + int txsize = len; + const char* bufc = (const char*) buf; + while (txsize--) { + while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET) {} + USART_SendData(USART1, (uint16_t) (uint8_t) *bufc++); + } + while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET) {} + return len; +} diff --git a/hw/bsp/ch32h417/family.cmake b/hw/bsp/ch32h417/family.cmake new file mode 100644 index 000000000..0378adc01 --- /dev/null +++ b/hw/bsp/ch32h417/family.cmake @@ -0,0 +1,141 @@ +include_guard() + +set(SDK_DIR ${TOP}/hw/mcu/wch/ch32h417/EVT/EXAM/SRC) +# system_ch32h417.c (clock setup) is a per-project file in the EVT; use the plain GPIO demo's V3F +# copy (SYSCLK 400 MHz, V3F core 100 MHz from the 25 MHz HSE - the vendor default all USB demos run) +set(SDK_SYSTEM_FILE ${TOP}/hw/mcu/wch/ch32h417/EVT/EXAM/GPIO/GPIO_Toggle/V3F/User/system_ch32h417.c) + +# include board specific +include(${CMAKE_CURRENT_LIST_DIR}/boards/${BOARD}/board.cmake) + +# toolchain set up +set(CMAKE_SYSTEM_CPU rv32imac-ilp32 CACHE INTERNAL "System Processor") +set(CMAKE_TOOLCHAIN_FILE ${TOP}/examples/build_system/cmake/toolchain/riscv_${TOOLCHAIN}.cmake) + +set(FAMILY_MCUS CH32H417 CACHE INTERNAL "") +set(OPENOCD_OPTION "-f ${CMAKE_CURRENT_LIST_DIR}/wch-riscv.cfg") + +# Controller selection: super = USB3.0 SuperSpeed (USBSS), high = USB2.0 HighSpeed (USBHS) +if (NOT DEFINED SPEED) + set(SPEED super) +endif () + +#------------------------------------ +# Startup & Linker script +#------------------------------------ +if (NOT DEFINED LD_FILE_GNU) + set(LD_FILE_GNU ${CMAKE_CURRENT_LIST_DIR}/linker/ch32h417_v3f.ld) +endif () +set(LD_FILE_Clang ${LD_FILE_GNU}) +if (NOT DEFINED STARTUP_FILE_GNU) + set(STARTUP_FILE_GNU ${SDK_DIR}/Startup/startup_ch32h417_v3f.S) +endif () +set(STARTUP_FILE_Clang ${STARTUP_FILE_GNU}) + +#------------------------------------ +# Board Target +#------------------------------------ +function(family_add_board BOARD_TARGET) + add_library(${BOARD_TARGET} STATIC + ${SDK_DIR}/Core/core_riscv.c + ${SDK_DIR}/Peripheral/src/ch32h417_dbgmcu.c + ${SDK_DIR}/Peripheral/src/ch32h417_flash.c + ${SDK_DIR}/Peripheral/src/ch32h417_gpio.c + ${SDK_DIR}/Peripheral/src/ch32h417_rcc.c + ${SDK_DIR}/Peripheral/src/ch32h417_iwdg.c + ${SDK_DIR}/Peripheral/src/ch32h417_usart.c + ${SDK_SYSTEM_FILE} + ) + target_include_directories(${BOARD_TARGET} PUBLIC + ${SDK_DIR}/Core + ${SDK_DIR}/Peripheral/inc + ${CMAKE_CURRENT_FUNCTION_LIST_DIR} + ) + # TinyUSB runs entirely on the V3F core (core 0, the boot core): single image, single link, + # standard flow. The V5F core (core 1) is left parked. Core_V3F selects the per-core paths in + # the SDK headers. + target_compile_definitions(${BOARD_TARGET} PUBLIC + Core_V3F=1 + ) + + if (SPEED STREQUAL super) + # USB3 SuperSpeed, SS-only by default: the USB2-HS fallback PHY takes PB8/PB9, which ARE the + # chip's only SWD/SDI debug pins - a fallback image cannot be debugged or reflashed in-system. + # Pass -DCFG_TUD_WCH_USB30_FALLBACK=1 to enable the runtime USB2 fallback (TIM12 training timeout). + if (NOT DEFINED CFG_TUD_WCH_USB30_FALLBACK) + set(CFG_TUD_WCH_USB30_FALLBACK 0) + endif () + target_compile_definitions(${BOARD_TARGET} PUBLIC + CFG_TUD_WCH_USBIP_USB30=1 + CFG_TUD_WCH_USB30_FALLBACK=${CFG_TUD_WCH_USB30_FALLBACK} + ) + else () + target_compile_definitions(${BOARD_TARGET} PUBLIC CFG_TUD_WCH_USBIP_USBHS=1) + endif () + + # BSP knobs of family.c, both settable with cmake -D= (see family.c for details): + # CFG_BOARD_CH32H417_UART_LOADER default 1: USART1 park + UART flash loader. Keep it on unless + # you can unplug the USB cable - the USB2 pins are the chip's only SWD/SDI pins, so this is + # the only in-system reflash path once a USB image runs. + # CFG_BOARD_CH32H417_CRASH_WDT default 0: ~8 s IWDG kicked from SysTick and board_led_write. + # The HIL rig sets 1 (a crashed board then reboots into the park window); it also resets the + # chip during any core halt longer than the period, so normal builds leave it off. + foreach (knob CFG_BOARD_CH32H417_UART_LOADER CFG_BOARD_CH32H417_CRASH_WDT) + if (DEFINED ${knob}) + target_compile_definitions(${BOARD_TARGET} PUBLIC ${knob}=${${knob}}) + endif () + endforeach () + + update_board(${BOARD_TARGET}) + + if (CMAKE_C_COMPILER_ID STREQUAL "GNU") + target_compile_options(${BOARD_TARGET} PUBLIC + -flto + -msmall-data-limit=16 + -mno-save-restore + -fmessage-length=0 + -fsigned-char + -Wno-error=strict-prototypes + -Wno-comment + ) + endif () +endfunction() + +#------------------------------------ +# Functions +#------------------------------------ +function(family_configure_example TARGET RTOS) + family_configure_common(${TARGET} ${RTOS}) + family_add_tinyusb(${TARGET} OPT_MCU_CH32H417) + + target_sources(${TARGET} PUBLIC + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/family.c + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../board.c + ${TOP}/src/portable/wch/dcd_ch32h417_usb30.c + ${TOP}/src/portable/wch/dcd_ch32h417_usbhs.c + ${STARTUP_FILE_${CMAKE_C_COMPILER_ID}} + ) + target_include_directories(${TARGET} PUBLIC + ${CMAKE_CURRENT_FUNCTION_LIST_DIR} + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../../ + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/boards/${BOARD} + ) + + if (CMAKE_C_COMPILER_ID STREQUAL "GNU") + target_link_options(${TARGET} PUBLIC + -nostartfiles + --specs=nosys.specs --specs=nano.specs + "LINKER:--script=${LD_FILE_GNU}" + ) + elseif (CMAKE_C_COMPILER_ID STREQUAL "Clang") + message(FATAL_ERROR "Clang is not supported for CH32H417") + endif () + + set_source_files_properties(${STARTUP_FILE_${CMAKE_C_COMPILER_ID}} PROPERTIES + SKIP_LINTING ON + COMPILE_OPTIONS -w) + + # Flashing + family_add_bin_hex(${TARGET}) + family_flash_openocd(${TARGET}) +endfunction() diff --git a/hw/bsp/ch32h417/family.mk b/hw/bsp/ch32h417/family.mk new file mode 100644 index 000000000..2f8f9e400 --- /dev/null +++ b/hw/bsp/ch32h417/family.mk @@ -0,0 +1,69 @@ +# Toolchain from https://github.com/xpack-dev-tools/riscv-none-elf-gcc-xpack +CROSS_COMPILE ?= riscv-none-elf- + +SDK_DIR = hw/mcu/wch/ch32h417/EVT/EXAM/SRC +# system_ch32h417.c (clock setup) is a per-project file in the EVT; use the plain GPIO demo's V3F +# copy (SYSCLK 400 MHz, V3F core 100 MHz from the 25 MHz HSE - the vendor default all USB demos run) +SDK_SYSTEM_FILE = hw/mcu/wch/ch32h417/EVT/EXAM/GPIO/GPIO_Toggle/V3F/User/system_ch32h417.c + +include $(TOP)/$(BOARD_PATH)/board.mk +CPU_CORE ?= rv32imac-ilp32 + +# Controller selection: super = USB3.0 SuperSpeed (USBSS), high = USB2.0 HighSpeed (USBHS) +SPEED ?= super + +# TinyUSB runs entirely on the V3F core (core 0, the boot core): single image, single link, +# standard flow. The V5F core (core 1) is left parked. +CFLAGS += \ + -flto \ + -msmall-data-limit=16 \ + -mno-save-restore \ + -fmessage-length=0 \ + -fsigned-char \ + -DCFG_TUSB_MCU=OPT_MCU_CH32H417 \ + -DCore_V3F=1 \ + -Wno-error=strict-prototypes \ + -Wno-comment + +ifeq ($(SPEED),super) + # USB3 SuperSpeed, SS-only by default: the USB2-HS fallback PHY takes PB8/PB9, which ARE the + # chip's only SWD/SDI debug pins - a fallback image cannot be debugged or reflashed in-system. + # Set CFG_TUD_WCH_USB30_FALLBACK=1 to enable the runtime USB2 fallback (TIM12 training timeout). + CFG_TUD_WCH_USB30_FALLBACK ?= 0 + CFLAGS += -DCFG_TUD_WCH_USBIP_USB30=1 -DCFG_TUD_WCH_USB30_FALLBACK=$(CFG_TUD_WCH_USB30_FALLBACK) +else + CFLAGS += -DCFG_TUD_WCH_USBIP_USBHS=1 +endif + +LDFLAGS += \ + -nostartfiles \ + --specs=nosys.specs --specs=nano.specs + +SRC_C += \ + src/portable/wch/dcd_ch32h417_usb30.c \ + src/portable/wch/dcd_ch32h417_usbhs.c \ + $(SDK_DIR)/Core/core_riscv.c \ + $(SDK_DIR)/Peripheral/src/ch32h417_dbgmcu.c \ + $(SDK_DIR)/Peripheral/src/ch32h417_flash.c \ + $(SDK_DIR)/Peripheral/src/ch32h417_gpio.c \ + $(SDK_DIR)/Peripheral/src/ch32h417_rcc.c \ + $(SDK_DIR)/Peripheral/src/ch32h417_iwdg.c \ + $(SDK_DIR)/Peripheral/src/ch32h417_usart.c \ + $(SDK_SYSTEM_FILE) + +SRC_S += \ + $(SDK_DIR)/Startup/startup_ch32h417_v3f.S + +INC += \ + $(TOP)/$(BOARD_PATH) \ + $(TOP)/$(FAMILY_PATH) \ + $(TOP)/$(SDK_DIR)/Core \ + $(TOP)/$(SDK_DIR)/Peripheral/inc + +LD_FILE ?= $(FAMILY_PATH)/linker/ch32h417_v3f.ld + +OPENOCD_OPTION=-f $(TOP)/$(FAMILY_PATH)/wch-riscv.cfg +flash: flash-openocd-wch + +# For freeRTOS port source +FREERTOS_PORTABLE_SRC = $(FREERTOS_PORTABLE_PATH)/RISC-V diff --git a/hw/bsp/ch32h417/linker/ch32h417_v3f.ld b/hw/bsp/ch32h417/linker/ch32h417_v3f.ld new file mode 100644 index 000000000..252a14f76 --- /dev/null +++ b/hw/bsp/ch32h417/linker/ch32h417_v3f.ld @@ -0,0 +1,216 @@ +/* CH32H417 V3F (core 0) linker script, derived from the EVT's Ld/V3F/Link_v3f.ld. + * All code is copied from flash into RAM_CODE by the startup and runs from there; the 512 KB + * shared SRAM (0x20100000..0x2017FFFF, the only SRAM on the bus matrix and thus USB-DMA + * reachable) is split 192 KB code + 320 KB data. The V5F core (TCMs, flash at 0x10000) is + * unused: TinyUSB runs single-image on the boot core. + */ +ENTRY( _start ) + +__stack_size = 4096; + +PROVIDE( _stack_size = __stack_size ); + + +MEMORY +{ + FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 448K /* image storage; code runs from RAM_CODE */ + RAM_CODE (xrw) : ORIGIN = 0x20100000, LENGTH = 192K /* shared SRAM: running code */ + RAM (xrw) : ORIGIN = (0x20130000+256), LENGTH = (320K-256-16) /* shared SRAM: .data .bss heap stack */ + RAM_NOINIT (rw) : ORIGIN = (0x20130000+320K-16), LENGTH = 16 /* survives soft reset: flash-park flag */ + RAM_LOAD (xrw) : ORIGIN = 0x20130000, LENGTH = 256 /* shared SRAM: .load copy routine */ +} + +SECTIONS +{ + .init : + { + _sinit = .; + . = ALIGN(4); + KEEP(*(SORT_NONE(.init))) + . = ALIGN(4); + _einit = .; + } >FLASH AT>FLASH + + .loadcodelalign : + { + . = ALIGN(4); + PROVIDE(_loadcode_lma = .); + } >FLASH AT>FLASH + + .loadcode : + { + . = ALIGN(4); + PROVIDE(_loadcode_vma_start = .); + *(.load); + . = ALIGN(4); + PROVIDE(_loadcode_vma_end = .); + }>RAM_LOAD AT>FLASH + + .highcodelalign : + { + . = ALIGN(4); + PROVIDE(_highcode_lma = .); + } >FLASH AT>FLASH + + .highcode : + { + . = ALIGN(4); + PROVIDE(_highcode_vma_start = .); + *(.vector); + KEEP(*(SORT_NONE(.vector_handler))) + *(.text) + *(.text.*) + *(.rodata) + *(.rodata*) + *(.sdata2.*) + *(.glue_7) + *(.glue_7t) + *(.gnu.linkonce.t.*) + . = ALIGN(4); + PROVIDE(_highcode_vma_end = .); + } >RAM_CODE AT>FLASH + + .text : + { + . = ALIGN(4); + KEEP(*(SORT_NONE(.handle_reset))) + . = ALIGN(4); + } >FLASH AT>FLASH + + .highcodelalign1 : + { + . = ALIGN(4); + PROVIDE(_highcode_lma1 = .); + } >FLASH AT>FLASH + + .fini : + { + . = ALIGN(4); + PROVIDE(_highcode_vma_start1 = .); + KEEP(*(SORT_NONE(.fini))) + . = ALIGN(4); + } >RAM_CODE AT>FLASH + + PROVIDE( _etext = . ); + PROVIDE( _eitcm = . ); + + .preinit_array : + { + PROVIDE_HIDDEN (__preinit_array_start = .); + KEEP (*(.preinit_array)) + PROVIDE_HIDDEN (__preinit_array_end = .); + } >RAM_CODE AT>FLASH + + .init_array : + { + PROVIDE_HIDDEN (__init_array_start = .); + KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*))) + KEEP (*(.init_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .ctors)) + PROVIDE_HIDDEN (__init_array_end = .); + } >RAM_CODE AT>FLASH + + .fini_array : + { + PROVIDE_HIDDEN (__fini_array_start = .); + KEEP (*(SORT_BY_INIT_PRIORITY(.fini_array.*) SORT_BY_INIT_PRIORITY(.dtors.*))) + KEEP (*(.fini_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .dtors)) + PROVIDE_HIDDEN (__fini_array_end = .); + } >RAM_CODE AT>FLASH + + .ctors : + { + /* gcc uses crtbegin.o to find the start of + the constructors, so we make sure it is + first. Because this is a wildcard, it + doesn't matter if the user does not + actually link against crtbegin.o; the + linker won't look for a file to match a + wildcard. The wildcard also means that it + doesn't matter which directory crtbegin.o + is in. */ + KEEP (*crtbegin.o(.ctors)) + KEEP (*crtbegin?.o(.ctors)) + /* We don't want to include the .ctor section from + the crtend.o file until after the sorted ctors. + The .ctor section from the crtend file contains the + end of ctors marker and it must be last */ + KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .ctors)) + KEEP (*(SORT(.ctors.*))) + KEEP (*(.ctors)) + } >RAM_CODE AT>FLASH + + .dtors : + { + KEEP (*crtbegin.o(.dtors)) + KEEP (*crtbegin?.o(.dtors)) + KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .dtors)) + KEEP (*(SORT(.dtors.*))) + KEEP (*(.dtors)) + . = ALIGN(4); + PROVIDE(_highcode_vma_end1 = .); + } >RAM_CODE AT>FLASH + + .dalign : + { + . = ALIGN(4); + PROVIDE(_data_vma = .); + } >RAM AT>FLASH + + .dlalign : + { + . = ALIGN(4); + PROVIDE(_data_lma = .); + } >FLASH AT>FLASH + + .data : + { + . = ALIGN(4); + *(.gnu.linkonce.r.*) + *(.data .data.*) + *(.gnu.linkonce.d.*) + . = ALIGN(8); + PROVIDE( __global_pointer$ = . + 0x800 ); + *(.sdata .sdata.*) + *(.sdata2.*) + *(.gnu.linkonce.s.*) + . = ALIGN(8); + *(.srodata.cst16) + *(.srodata.cst8) + *(.srodata.cst4) + *(.srodata.cst2) + *(.srodata .srodata.*) + . = ALIGN(4); + PROVIDE( _edata = .); + } >RAM AT>FLASH + + .noinit (NOLOAD) : + { + *(.noinit*) + } >RAM_NOINIT + + .bss : + { + . = ALIGN(4); + PROVIDE( _sbss = .); + *(.sbss*) + *(.gnu.linkonce.sb.*) + *(.bss*) + *(.gnu.linkonce.b.*) + *(COMMON*) + . = ALIGN(4); + PROVIDE( _ebss = .); + } >RAM AT>FLASH + + PROVIDE( _end = _ebss); + PROVIDE( end = . ); + + .stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size : + { + PROVIDE( _heap_end = . ); + . = ALIGN(4); + PROVIDE(_susrstack = . ); + . = . + __stack_size; + PROVIDE( _eusrstack = .); + } >RAM + +} diff --git a/hw/bsp/ch32h417/system_ch32h417.h b/hw/bsp/ch32h417/system_ch32h417.h new file mode 100644 index 000000000..578565e27 --- /dev/null +++ b/hw/bsp/ch32h417/system_ch32h417.h @@ -0,0 +1,46 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// System header pulled in by the SDK's ch32h417.h. The EVT ships this file per-project; the +// implementation compiled by this BSP is the EVT GPIO demo's V3F system_ch32h417.c. + +#ifndef SYSTEM_CH32H417_H_ +#define SYSTEM_CH32H417_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +extern uint32_t SystemClock; // SYSCLK +extern uint32_t HCLKClock; // HB bus / V3F core clock +extern uint32_t SystemCoreClock; // clock of the core this code runs on + +extern void SystemInit(void); +extern void SystemAndCoreClockUpdate(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/hw/bsp/ch32h417/wch-riscv.cfg b/hw/bsp/ch32h417/wch-riscv.cfg new file mode 100644 index 000000000..e1bc144fa --- /dev/null +++ b/hw/bsp/ch32h417/wch-riscv.cfg @@ -0,0 +1,18 @@ +adapter driver wlinke +adapter speed 6000 +transport select sdi + +wlink_set_address 0x00000000 +set _CHIPNAME wch_riscv +sdi newtap $_CHIPNAME cpu -irlen 5 -expected-id 0x00001 + +set _TARGETNAME $_CHIPNAME.cpu + +target create $_TARGETNAME.0 wch_riscv -chain-position $_TARGETNAME +# work area in the 512 KB shared SRAM (the TCMs are core-private) +$_TARGETNAME.0 configure -work-area-phys 0x20100000 -work-area-size 0x4000 -work-area-backup 1 +set _FLASHNAME $_CHIPNAME.flash + +flash bank $_FLASHNAME wch_riscv 0x00000000 0 0 0 $_TARGETNAME.0 + +echo "Ready for Remote Connections"