From b62ac586501c43c61f21c0fca67dca437ff3a9dc Mon Sep 17 00:00:00 2001 From: UMRnInside <30196401+UMRnInside@users.noreply.github.com> Date: Sun, 26 Apr 2026 20:35:54 +0800 Subject: Fix CH32V103 hardfault on startup --- hw/bsp/ch32v10x/family.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/hw/bsp/ch32v10x/family.c b/hw/bsp/ch32v10x/family.c index 72dae7086..9943bf856 100644 --- a/hw/bsp/ch32v10x/family.c +++ b/hw/bsp/ch32v10x/family.c @@ -67,7 +67,10 @@ uint32_t tusb_time_millis_api(void) { #endif void board_init(void) { - __disable_irq(); + /* __disable_irq() in CH32V103 EVT attempts to call + * `csrc mstatus, 0x88` in U-mode, which is allowed ONLY in M-mode. + * Disable this to avoid hard-fault. */ + //__disable_irq(); #if CFG_TUSB_OS == OPT_OS_NONE SysTick_Config(SystemCoreClock / 1000); @@ -123,7 +126,7 @@ void board_init(void) { USART_Init(USART1, &usart); USART_Cmd(USART1, ENABLE); - __enable_irq(); + //__enable_irq(); board_led_write(true); } -- cgit v1.3.1 From 2eb1599b4d5e773b71135d82d71e9b422935a410 Mon Sep 17 00:00:00 2001 From: UMRnInside <30196401+UMRnInside@users.noreply.github.com> Date: Sun, 26 Apr 2026 20:36:55 +0800 Subject: Add ch32v103c_bluepill board --- .../ch32v10x/boards/ch32v103c_bluepill/board.cmake | 8 +++++++ hw/bsp/ch32v10x/boards/ch32v103c_bluepill/board.h | 27 ++++++++++++++++++++++ hw/bsp/ch32v10x/boards/ch32v103c_bluepill/board.mk | 5 ++++ 3 files changed, 40 insertions(+) create mode 100644 hw/bsp/ch32v10x/boards/ch32v103c_bluepill/board.cmake create mode 100644 hw/bsp/ch32v10x/boards/ch32v103c_bluepill/board.h create mode 100644 hw/bsp/ch32v10x/boards/ch32v103c_bluepill/board.mk diff --git a/hw/bsp/ch32v10x/boards/ch32v103c_bluepill/board.cmake b/hw/bsp/ch32v10x/boards/ch32v103c_bluepill/board.cmake new file mode 100644 index 000000000..f6e47ba30 --- /dev/null +++ b/hw/bsp/ch32v10x/boards/ch32v103c_bluepill/board.cmake @@ -0,0 +1,8 @@ +set(LD_FLASH_SIZE 64K) +set(LD_RAM_SIZE 20K) + +function(update_board TARGET) + target_compile_definitions(${TARGET} PUBLIC + CFG_EXAMPLE_MSC_DUAL_READONLY + ) +endfunction() diff --git a/hw/bsp/ch32v10x/boards/ch32v103c_bluepill/board.h b/hw/bsp/ch32v10x/boards/ch32v103c_bluepill/board.h new file mode 100644 index 000000000..02558b953 --- /dev/null +++ b/hw/bsp/ch32v10x/boards/ch32v103c_bluepill/board.h @@ -0,0 +1,27 @@ +/* Some chinese manufactors use CH32V103C8T6 to make Bluepill boards + */ +/* metadata: + name: CH32V103C8T6-Bluepill + url: https://stm32-base.org/boards/STM32F103C8T6-Blue-Pill +*/ + +#ifndef BOARD_H_ +#define BOARD_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#define LED_PORT GPIOC +#define LED_PIN GPIO_Pin_13 +#define LED_STATE_ON 0 + +#define BUTTON_PORT GPIOA +#define BUTTON_PIN GPIO_Pin_1 +#define BUTTON_STATE_ACTIVE 1 + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/hw/bsp/ch32v10x/boards/ch32v103c_bluepill/board.mk b/hw/bsp/ch32v10x/boards/ch32v103c_bluepill/board.mk new file mode 100644 index 000000000..e594f42a7 --- /dev/null +++ b/hw/bsp/ch32v10x/boards/ch32v103c_bluepill/board.mk @@ -0,0 +1,5 @@ +CFLAGS += -DCFG_EXAMPLE_MSC_DUAL_READONLY + +LDFLAGS += \ + -Wl,--defsym=__FLASH_SIZE=64K \ + -Wl,--defsym=__RAM_SIZE=20K \ -- cgit v1.3.1 From cfdab2564fa5735f0a7d5653aa6eb5388456028e Mon Sep 17 00:00:00 2001 From: Alex-Schaefer <81265029+Alex-Schaefer@users.noreply.github.com> Date: Sat, 9 May 2026 09:10:47 +0200 Subject: dwc2: preserve EP0 status completion before SETUP On STM32 DWC2, SETUP phase done and EP0 OUT transfer complete can be reported together. Processing SETUP first can overwrite control state before the previous zero-length OUT status stage is acknowledged, which causes DFU DNLOAD/GETSTATUS traffic to lose the status ACK and stall. Queue the EP0 OUT zero-length transfer completion before queuing the SETUP event when the endpoint has no pending OUT data and total_len is zero. This keeps TinyUSB control-transfer ordering intact for the combined interrupt case. --- src/portable/synopsys/dwc2/dcd_dwc2.c | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/portable/synopsys/dwc2/dcd_dwc2.c b/src/portable/synopsys/dwc2/dcd_dwc2.c index 30e24a9ad..9b8f44df8 100644 --- a/src/portable/synopsys/dwc2/dcd_dwc2.c +++ b/src/portable/synopsys/dwc2/dcd_dwc2.c @@ -946,7 +946,17 @@ static void handle_rxflvl_irq(uint8_t rhport) { } static void handle_epout_slave(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doepint_bm) { + xfer_ctl_t* xfer = XFER_CTL_BASE(epnum, TUSB_DIR_OUT); + const bool ep0_status_complete_before_setup = (epnum == 0) && doepint_bm.setup_phase_done && + doepint_bm.xfer_complete && + (_dcd_data.ep0_pending[TUSB_DIR_OUT] == 0) && + (xfer->total_len == 0); + if (doepint_bm.setup_phase_done) { + if (ep0_status_complete_before_setup) { + dcd_event_xfer_complete(rhport, epnum, 0, XFER_RESULT_SUCCESS, true); + } + // Cleanup previous pending EP0 IN transfer if any dwc2_dep_t* epin0 = &DWC2_REG(rhport)->epin[0]; if (edpt_is_enabled(epin0)) { @@ -962,7 +972,6 @@ static void handle_epout_slave(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doe // Note: even though (xfer_complete + status_phase_rx) is for buffered DMA only, for STM32L47x (dwc2 v3.00a) they // can is set when GRXSTS_PKTSTS_SETUP_RX is popped therefore they can bet set before/together with setup_phase_done if (!doepint_bm.status_phase_rx && !doepint_bm.setup_packet_rx) { - xfer_ctl_t* xfer = XFER_CTL_BASE(epnum, TUSB_DIR_OUT); if ((epnum == 0) && _dcd_data.ep0_pending[TUSB_DIR_OUT]) { // EP0 can only handle one packet, Schedule another packet to be received. edpt_schedule_packets(rhport, epnum, TUSB_DIR_OUT); @@ -1005,8 +1014,17 @@ static void handle_epin_slave(uint8_t rhport, uint8_t epnum, dwc2_diepint_t diep #if CFG_TUD_DWC2_DMA_ENABLE static void handle_epout_dma(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doepint_bm) { dwc2_regs_t* dwc2 = DWC2_REG(rhport); + xfer_ctl_t* xfer = XFER_CTL_BASE(epnum, TUSB_DIR_OUT); + const bool ep0_status_complete_before_setup = (epnum == 0) && doepint_bm.setup_phase_done && + doepint_bm.xfer_complete && + (_dcd_data.ep0_pending[TUSB_DIR_OUT] == 0) && + (xfer->total_len == 0); if (doepint_bm.setup_phase_done) { + if (ep0_status_complete_before_setup) { + dcd_event_xfer_complete(rhport, epnum, 0, XFER_RESULT_SUCCESS, true); + } + // Cleanup previous pending EP0 IN transfer if any dwc2_dep_t* epin0 = &DWC2_REG(rhport)->epin[0]; if (edpt_is_enabled(epin0)) { @@ -1028,7 +1046,6 @@ static void handle_epout_dma(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doepi edpt_schedule_packets(rhport, epnum, TUSB_DIR_OUT); } else { dwc2_dep_t* epout = &dwc2->epout[epnum]; - xfer_ctl_t* xfer = XFER_CTL_BASE(epnum, TUSB_DIR_OUT); // determine actual received bytes const dwc2_ep_tsize_t tsiz = {.value = epout->tsiz}; -- cgit v1.3.1 From 16bc0548dc0793404a5885aa90d78100b604c227 Mon Sep 17 00:00:00 2001 From: Alex-Schaefer <81265029+Alex-Schaefer@users.noreply.github.com> Date: Sat, 9 May 2026 11:14:00 +0200 Subject: dwc2: guard EP0 status completion with armed ZLP state Track when an EP0 OUT zero-length transfer is actually armed and require that state before synthesizing a status-stage completion ahead of a co-reported SETUP event. This preserves the validated status-before-SETUP ordering fix while avoiding stale zero-length state from producing spurious EP0 OUT completions. --- src/portable/synopsys/dwc2/dcd_dwc2.c | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/portable/synopsys/dwc2/dcd_dwc2.c b/src/portable/synopsys/dwc2/dcd_dwc2.c index 9b8f44df8..72beb1b80 100644 --- a/src/portable/synopsys/dwc2/dcd_dwc2.c +++ b/src/portable/synopsys/dwc2/dcd_dwc2.c @@ -62,6 +62,7 @@ static xfer_ctl_t xfer_status[DWC2_EP_MAX][2]; typedef struct { // EP0 transfers are limited to 1 packet - larger sizes has to be split uint16_t ep0_pending[2]; // Index determines direction as tusb_dir_t type + bool ep0_out_zlp_armed; // EP0 OUT ZLP transfer is armed and waiting for completion uint16_t dfifo_top; // top free location in DFIFO in words // Number of IN endpoints active @@ -665,6 +666,9 @@ bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t* buffer, uint16_t to // EP0 can only handle one packet if (epnum == 0) { _dcd_data.ep0_pending[dir] = total_bytes; + if (dir == TUSB_DIR_OUT) { + _dcd_data.ep0_out_zlp_armed = (total_bytes == 0); + } } // Schedule packets to be sent within interrupt @@ -744,6 +748,9 @@ static void handle_bus_reset(uint8_t rhport) { tu_memclr(xfer_status, sizeof(xfer_status)); + _dcd_data.ep0_pending[TUSB_DIR_OUT] = 0; + _dcd_data.ep0_pending[TUSB_DIR_IN] = 0; + _dcd_data.ep0_out_zlp_armed = false; _dcd_data.sof_en = false; _dcd_data.allocated_epin_count = 0; @@ -949,12 +956,16 @@ static void handle_epout_slave(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doe xfer_ctl_t* xfer = XFER_CTL_BASE(epnum, TUSB_DIR_OUT); const bool ep0_status_complete_before_setup = (epnum == 0) && doepint_bm.setup_phase_done && doepint_bm.xfer_complete && + _dcd_data.ep0_out_zlp_armed && (_dcd_data.ep0_pending[TUSB_DIR_OUT] == 0) && (xfer->total_len == 0); if (doepint_bm.setup_phase_done) { if (ep0_status_complete_before_setup) { + _dcd_data.ep0_out_zlp_armed = false; dcd_event_xfer_complete(rhport, epnum, 0, XFER_RESULT_SUCCESS, true); + } else if (epnum == 0) { + _dcd_data.ep0_out_zlp_armed = false; } // Cleanup previous pending EP0 IN transfer if any @@ -976,6 +987,9 @@ static void handle_epout_slave(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doe // EP0 can only handle one packet, Schedule another packet to be received. edpt_schedule_packets(rhport, epnum, TUSB_DIR_OUT); } else { + if (epnum == 0) { + _dcd_data.ep0_out_zlp_armed = false; + } dcd_event_xfer_complete(rhport, epnum, xfer->total_len, XFER_RESULT_SUCCESS, true); } } @@ -1017,12 +1031,16 @@ static void handle_epout_dma(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doepi xfer_ctl_t* xfer = XFER_CTL_BASE(epnum, TUSB_DIR_OUT); const bool ep0_status_complete_before_setup = (epnum == 0) && doepint_bm.setup_phase_done && doepint_bm.xfer_complete && + _dcd_data.ep0_out_zlp_armed && (_dcd_data.ep0_pending[TUSB_DIR_OUT] == 0) && (xfer->total_len == 0); if (doepint_bm.setup_phase_done) { if (ep0_status_complete_before_setup) { + _dcd_data.ep0_out_zlp_armed = false; dcd_event_xfer_complete(rhport, epnum, 0, XFER_RESULT_SUCCESS, true); + } else if (epnum == 0) { + _dcd_data.ep0_out_zlp_armed = false; } // Cleanup previous pending EP0 IN transfer if any @@ -1058,6 +1076,9 @@ static void handle_epout_dma(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doepi dma_setup_prepare(rhport); } + if (epnum == 0) { + _dcd_data.ep0_out_zlp_armed = false; + } dcd_dcache_invalidate(xfer->buffer, xfer->total_len); dcd_event_xfer_complete(rhport, epnum, xfer->total_len, XFER_RESULT_SUCCESS, true); } -- cgit v1.3.1 From f4d0d09c8ee06a3531c39032cbe102976aeca349 Mon Sep 17 00:00:00 2001 From: hathach Date: Thu, 21 May 2026 18:18:25 +0700 Subject: hil added hub + msc + cdc for host capable board --- test/hil/hil_ci.sh | 10 +- test/hil/hil_test.py | 5 + test/hil/requirements.txt | 1 + test/hil/tinyusb.json | 236 +++++++++++++++++++++++++++++++++++++--------- 4 files changed, 208 insertions(+), 44 deletions(-) diff --git a/test/hil/hil_ci.sh b/test/hil/hil_ci.sh index 96872e2e1..35e71f1ba 100644 --- a/test/hil/hil_ci.sh +++ b/test/hil/hil_ci.sh @@ -54,11 +54,14 @@ scp -q "$ROOT_DIR/test/hil/hil_test.py" \ "$CONFIG" \ "$REMOTE:$REMOTE_DIR/test/hil/" -# Copy only firmware binaries (elf/bin/hex), preserving directory structure +# Copy only firmware binaries (elf/bin/hex) plus esptool metadata +# (config.env + flash_args needed by the esptool flasher), preserving structure copy_board_binaries() { local src="$1" rsync -a --prune-empty-dirs \ - --include='*/' --include='*.elf' --include='*.bin' --include='*.hex' --exclude='*' \ + --include='*/' --include='*.elf' --include='*.bin' --include='*.hex' \ + --include='config.env' --include='flash_args' \ + --exclude='*' \ "$src" "$REMOTE:$REMOTE_DIR/examples/" } @@ -85,5 +88,8 @@ echo "==> Running HIL test on $REMOTE" ssh "$REMOTE" bash -s -- "$REMOTE_DIR" "${ARGS[@]}" "test/hil/$CONFIG_BASENAME" <<'REMOTE' cd -- "$1" shift +# esptool/idf tools live in ~/.local/bin on ci.lan; the non-interactive shell +# subprocess used for flashing doesn't pick that up otherwise. +export PATH="$HOME/.local/bin:$PATH" exec python3 -u test/hil/hil_test.py -B examples "$@" REMOTE diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index ed9ebbf1a..d921a910a 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -796,6 +796,10 @@ def test_host_msc_file_explorer(board): ser.close() +def test_host_msc_file_explorer_freertos(board): + return test_host_msc_file_explorer(board) + + # ------------------------------------------------------------- # Tests: device # ------------------------------------------------------------- @@ -1465,6 +1469,7 @@ dual_tests = [ host_test = [ 'host/cdc_msc_hid', 'host/msc_file_explorer', + 'host/msc_file_explorer_freertos', 'host/device_info', ] diff --git a/test/hil/requirements.txt b/test/hil/requirements.txt index ef2fecebe..127f6a8ec 100644 --- a/test/hil/requirements.txt +++ b/test/hil/requirements.txt @@ -2,3 +2,4 @@ fs hid pyfatfs pyserial +esptool diff --git a/test/hil/tinyusb.json b/test/hil/tinyusb.json index cba7677cf..dc28df7b9 100644 --- a/test/hil/tinyusb.json +++ b/test/hil/tinyusb.json @@ -3,12 +3,35 @@ { "name": "espressif_p4_function_ev", "uid": "6055F9F98715", - "build" : { - "flags_on": ["", "CFG_TUD_DWC2_DMA_ENABLE CFG_TUH_DWC2_DMA_ENABLE"] + "build": { + "flags_on": [ + "", + "CFG_TUD_DWC2_DMA_ENABLE CFG_TUH_DWC2_DMA_ENABLE" + ] }, "tests": { - "only": ["device/cdc_msc_freertos", "device/hid_composite_freertos", "device/audio_test_freertos", "host/device_info"], - "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2002427", "is_cdc": true}] + "only": [ + "device/cdc_msc_freertos", + "device/hid_composite_freertos", + "device/audio_test_freertos", + "host/device_info", + "host/msc_file_explorer_freertos" + ], + "dev_attached": [ + { + "vid_pid": "1a86_55d4", + "serial": "52D2002427", + "is_cdc": true + }, + { + "vid_pid": "21c4_0cc7", + "serial": "900058944CB80A53", + "is_msc": true, + "block_size": 512, + "block_count": 60620800, + "msc_inquiry": "Lexar USB Flash Drive PMAP" + } + ] }, "flasher": { "name": "esptool", @@ -21,12 +44,36 @@ { "name": "espressif_s3_devkitm", "uid": "84F703C084E4", - "build" : { - "flags_on": ["", "CFG_TUD_DWC2_DMA_ENABLE CFG_TUH_DWC2_DMA_ENABLE"] + "build": { + "flags_on": [ + "", + "CFG_TUD_DWC2_DMA_ENABLE CFG_TUH_DWC2_DMA_ENABLE" + ] }, "tests": { - "only": ["device/cdc_msc_freertos", "device/hid_composite_freertos", "device/audio_test_freertos", "host/device_info"], - "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2005402", "is_cdc": true}] + "only": [ + "device/cdc_msc_freertos", + "device/hid_composite_freertos", + "device/audio_test_freertos", + "host/device_info", + "host/msc_file_explorer_freertos" + ], + "dev_attached": [ + { + "vid_pid": "1a86_55d4", + "serial": "52D2005402", + "is_cdc": true + }, + { + "vid_pid": "048d_04d2", + "serial": "\u0409", + "is_msc": true, + "block_size": 512, + "block_count": 30720000, + "msc_inquiry": "General UDisk 5.00", + "comment": "General UDisk reports iSerialNumber=U+0409" + } + ] }, "flasher": { "name": "esptool", @@ -39,7 +86,9 @@ "name": "feather_nrf52840_express", "uid": "1F0479CD0F764471", "tests": { - "device": true, "host": false, "dual": false + "device": true, + "host": false, + "dual": false }, "flasher": { "name": "jlink", @@ -51,7 +100,9 @@ "name": "max32666fthr", "uid": "0C81464124010B20FF0A08CC2C", "tests": { - "device": true, "host": false, "dual": false + "device": true, + "host": false, + "dual": false }, "flasher": { "name": "openocd_adi", @@ -71,7 +122,13 @@ "device": true, "host": false, "dual": true, - "dev_attached": [{"vid_pid": "067b_2303", "serial": "0", "is_cdc": true}], + "dev_attached": [ + { + "vid_pid": "067b_2303", + "serial": "0", + "is_cdc": true + } + ], "comment": "pl23x" }, "flasher": { @@ -84,7 +141,9 @@ "name": "mimxrt1015_evk", "uid": "DC28F865D2111D228D00B0543A70463C", "tests": { - "device": true, "host": false, "dual": false + "device": true, + "host": false, + "dual": false }, "flasher": { "name": "jlink", @@ -96,9 +155,25 @@ "name": "mimxrt1064_evk", "uid": "BAE96FB95AFA6DBB8F00005002001200", "tests": { - "device": true, "host": true, "dual": true, - "dev_attached": [{"vid_pid": "10c4_ea60", "serial": "0001", "is_cdc": true}], - "comment": "cp2102" + "device": true, + "host": true, + "dual": true, + "dev_attached": [ + { + "vid_pid": "10c4_ea60", + "serial": "0001", + "is_cdc": true, + "comment": "cp2102" + }, + { + "vid_pid": "21c4_0cc7", + "serial": "900058874D871F66", + "is_msc": true, + "block_size": 512, + "block_count": 60620800, + "msc_inquiry": "Lexar USB Flash Drive PMAP" + } + ] }, "flasher": { "name": "jlink", @@ -110,7 +185,9 @@ "name": "lpcxpresso11u37", "uid": "17121919", "tests": { - "device": true, "host": false, "dual": false + "device": true, + "host": false, + "dual": false }, "flasher": { "name": "jlink", @@ -135,13 +212,32 @@ { "name": "raspberry_pi_pico", "uid": "E6614C311B764A37", - "build" : { - "flags_on": ["CFG_TUH_RPI_PIO_USB"] + "build": { + "flags_on": [ + "CFG_TUH_RPI_PIO_USB" + ] }, "tests": { - "device": true, "host": true, "dual": true, - "dev_attached": [{"vid_pid": "1a86_7523", "serial": "0", "is_cdc": true}], - "comment": "ch34x" + "device": true, + "host": true, + "dual": true, + "dev_attached": [ + { + "vid_pid": "1a86_7523", + "serial": "0", + "is_cdc": true, + "comment": "ch34x" + }, + { + "vid_pid": "048d_04d2", + "serial": "\u0409", + "is_msc": true, + "block_size": 512, + "block_count": 30720000, + "msc_inquiry": "General UDisk 5.00", + "comment": "General UDisk reports iSerialNumber=U+0409" + } + ] }, "flasher": { "name": "openocd", @@ -153,8 +249,15 @@ "name": "raspberry_pi_pico_w", "uid": "E6614864D35DAE36", "tests": { - "device": false, "host": true, "dual": false, + "device": false, + "host": true, + "dual": false, "dev_attached": [ + { + "vid_pid": "1a86_55d4", + "serial": "52D2002694", + "is_cdc": true + }, { "vid_pid": "2008_2018", "serial": "O20070925A002746", @@ -176,7 +279,9 @@ "name": "raspberry_pi_pico2", "uid": "560AE75E1C7152C9", "tests": { - "device": false, "host": true, "dual": false, + "device": false, + "host": true, + "dual": false, "dev_attached": [ { "vid_pid": "0951_1603", @@ -184,7 +289,7 @@ "is_msc": true, "block_size": 512, "block_count": 3987456, - "msc_inquiry": "Kingston DataTraveler 2.0 1.0" + "msc_inquiry": "Kingston DataTraveler 2.0 1.00" } ] }, @@ -202,14 +307,24 @@ "host": true, "dual": true, "dev_attached": [ - {"vid_pid": "0403_6001", "serial": "0", "is_cdc": true}, - {"vid_pid": "058f_6387", "serial": "A8BEE062633D", "is_msc": true, - "block_size": 512, "block_count": 7639040, "msc_inquiry": "Generic Flash Disk 8.07"} + { + "vid_pid": "0403_6001", + "serial": "0", + "is_cdc": true + }, + { + "vid_pid": "058f_6387", + "serial": "A8BEE062633D", + "is_msc": true, + "block_size": 512, + "block_count": 7639040, + "msc_inquiry": "Generic Flash Disk 8.07" + } ] }, "flasher": { "name": "openocd", - "uid": "E6614103E78E8324", + "uid": "E663AC91D3359B38", "args": "-f interface/cmsis-dap.cfg -f target/rp2350.cfg -c \"adapter speed 5000\"" } }, @@ -217,7 +332,9 @@ "name": "stm32f072disco", "uid": "3A001A001357364230353532", "tests": { - "device": true, "host": false, "dual": false + "device": true, + "host": false, + "dual": false }, "flasher": { "name": "jlink", @@ -229,12 +346,31 @@ { "name": "stm32f723disco", "uid": "460029001951373031313335", - "build" : { - "flags_on": ["", "CFG_TUH_DWC2_DMA_ENABLE"] + "build": { + "flags_on": [ + "", + "CFG_TUH_DWC2_DMA_ENABLE" + ] }, "tests": { - "device": true, "host": true, "dual": false, - "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2003414", "is_cdc": true}] + "device": true, + "host": true, + "dual": false, + "dev_attached": [ + { + "vid_pid": "1a86_55d4", + "serial": "52D2003414", + "is_cdc": true + }, + { + "vid_pid": "21c4_0cc7", + "serial": "90005893730A1A63", + "is_msc": true, + "block_size": 512, + "block_count": 60620800, + "msc_inquiry": "Lexar USB Flash Drive PMAP" + } + ] }, "flasher": { "name": "jlink", @@ -246,11 +382,16 @@ { "name": "stm32h743nucleo", "uid": "110018000951383432343236", - "build" : { - "flags_on": ["", "CFG_TUD_DWC2_DMA_ENABLE"] + "build": { + "flags_on": [ + "", + "CFG_TUD_DWC2_DMA_ENABLE" + ] }, "tests": { - "device": true, "host": false, "dual": false + "device": true, + "host": false, + "dual": false }, "flasher": { "name": "openocd", @@ -262,7 +403,9 @@ "name": "stm32g0b1nucleo", "uid": "4D0038000450434E37343120", "tests": { - "device": true, "host": false, "dual": false + "device": true, + "host": false, + "dual": false }, "flasher": { "name": "openocd", @@ -276,11 +419,16 @@ { "name": "stm32f769disco", "uid": "21002F000F51363531383437", - "build" : { - "flags_on": ["", "CFG_TUD_DWC2_DMA_ENABLE"] + "build": { + "flags_on": [ + "", + "CFG_TUD_DWC2_DMA_ENABLE" + ] }, "tests": { - "device": true, "host": false, "dual": false + "device": true, + "host": false, + "dual": false }, "flasher": { "name": "jlink", @@ -292,7 +440,9 @@ "name": "nanoch32v203", "uid": "CDAB277B0FBC03E339E339E3", "tests": { - "device": true, "host": false, "dual": false + "device": true, + "host": false, + "dual": false }, "flasher": { "name": "openocd_wch", @@ -304,7 +454,9 @@ "name": "stm32f407disco", "uid": "30001A000647313332353735", "tests": { - "device": true, "host": false, "dual": false + "device": true, + "host": false, + "dual": false }, "flasher": { "name": "jlink", -- cgit v1.3.1 From 650e8a194fcfaa4b91203e9aedfecd614ea70105 Mon Sep 17 00:00:00 2001 From: HiFiPhile Date: Wed, 20 May 2026 23:22:02 +0200 Subject: dwc2: handle EP0 status OUT in RXFLVL interrupt Signed-off-by: HiFiPhile --- src/portable/synopsys/dwc2/dcd_dwc2.c | 65 +++++++++++++---------------------- 1 file changed, 24 insertions(+), 41 deletions(-) diff --git a/src/portable/synopsys/dwc2/dcd_dwc2.c b/src/portable/synopsys/dwc2/dcd_dwc2.c index 72beb1b80..bdab49f1b 100644 --- a/src/portable/synopsys/dwc2/dcd_dwc2.c +++ b/src/portable/synopsys/dwc2/dcd_dwc2.c @@ -62,7 +62,6 @@ static xfer_ctl_t xfer_status[DWC2_EP_MAX][2]; typedef struct { // EP0 transfers are limited to 1 packet - larger sizes has to be split uint16_t ep0_pending[2]; // Index determines direction as tusb_dir_t type - bool ep0_out_zlp_armed; // EP0 OUT ZLP transfer is armed and waiting for completion uint16_t dfifo_top; // top free location in DFIFO in words // Number of IN endpoints active @@ -70,6 +69,9 @@ typedef struct { // SOF enabling flag - required for SOF to not get disabled in ISR when SOF was enabled by bool sof_en; + + // EP0 status OUT flag + bool ep0_status_out; } dcd_data_t; static dcd_data_t _dcd_data; @@ -667,7 +669,7 @@ bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t* buffer, uint16_t to if (epnum == 0) { _dcd_data.ep0_pending[dir] = total_bytes; if (dir == TUSB_DIR_OUT) { - _dcd_data.ep0_out_zlp_armed = (total_bytes == 0); + _dcd_data.ep0_status_out = (total_bytes == 0); } } @@ -748,11 +750,9 @@ static void handle_bus_reset(uint8_t rhport) { tu_memclr(xfer_status, sizeof(xfer_status)); - _dcd_data.ep0_pending[TUSB_DIR_OUT] = 0; - _dcd_data.ep0_pending[TUSB_DIR_IN] = 0; - _dcd_data.ep0_out_zlp_armed = false; _dcd_data.sof_en = false; _dcd_data.allocated_epin_count = 0; + _dcd_data.ep0_status_out = false; // 1. NAK for all OUT endpoints for (uint8_t n = 0; n < ep_count; n++) { @@ -907,6 +907,9 @@ static void handle_rxflvl_irq(uint8_t rhport) { // We can receive up to three setup packets in succession, but only the last one is valid. setup[0] = (*rx_fifo); setup[1] = (*rx_fifo); + + // Clear previous pending EP0 OUT if any + _dcd_data.ep0_status_out = false; break; } @@ -946,6 +949,12 @@ static void handle_rxflvl_irq(uint8_t rhport) { // Out packet done // After this entry is popped from the receive FIFO, dwc2 asserts a Transfer Completed interrupt on // the specified OUT endpoint which will be handled by handle_epout_irq() + + // EP0 status OUT is complete + if (epnum == 0 && _dcd_data.ep0_status_out) { + _dcd_data.ep0_status_out = false; + dcd_event_xfer_complete(rhport, epnum, 0, XFER_RESULT_SUCCESS, true); + } break; default: break; // nothing to do @@ -953,21 +962,7 @@ static void handle_rxflvl_irq(uint8_t rhport) { } static void handle_epout_slave(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doepint_bm) { - xfer_ctl_t* xfer = XFER_CTL_BASE(epnum, TUSB_DIR_OUT); - const bool ep0_status_complete_before_setup = (epnum == 0) && doepint_bm.setup_phase_done && - doepint_bm.xfer_complete && - _dcd_data.ep0_out_zlp_armed && - (_dcd_data.ep0_pending[TUSB_DIR_OUT] == 0) && - (xfer->total_len == 0); - if (doepint_bm.setup_phase_done) { - if (ep0_status_complete_before_setup) { - _dcd_data.ep0_out_zlp_armed = false; - dcd_event_xfer_complete(rhport, epnum, 0, XFER_RESULT_SUCCESS, true); - } else if (epnum == 0) { - _dcd_data.ep0_out_zlp_armed = false; - } - // Cleanup previous pending EP0 IN transfer if any dwc2_dep_t* epin0 = &DWC2_REG(rhport)->epin[0]; if (edpt_is_enabled(epin0)) { @@ -983,13 +978,16 @@ static void handle_epout_slave(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doe // Note: even though (xfer_complete + status_phase_rx) is for buffered DMA only, for STM32L47x (dwc2 v3.00a) they // can is set when GRXSTS_PKTSTS_SETUP_RX is popped therefore they can bet set before/together with setup_phase_done if (!doepint_bm.status_phase_rx && !doepint_bm.setup_packet_rx) { - if ((epnum == 0) && _dcd_data.ep0_pending[TUSB_DIR_OUT]) { - // EP0 can only handle one packet, Schedule another packet to be received. - edpt_schedule_packets(rhport, epnum, TUSB_DIR_OUT); - } else { - if (epnum == 0) { - _dcd_data.ep0_out_zlp_armed = false; + xfer_ctl_t* xfer = XFER_CTL_BASE(epnum, TUSB_DIR_OUT); + if (epnum == 0) { + if (_dcd_data.ep0_pending[TUSB_DIR_OUT]) { + // EP0 can only handle one packet, Schedule another packet to be received. + edpt_schedule_packets(rhport, epnum, TUSB_DIR_OUT); + } else if (xfer->total_len > 0) { + // EP0 status out is handled in handle_rxflvl_irq + dcd_event_xfer_complete(rhport, epnum, xfer->total_len, XFER_RESULT_SUCCESS, true); } + } else { dcd_event_xfer_complete(rhport, epnum, xfer->total_len, XFER_RESULT_SUCCESS, true); } } @@ -1028,21 +1026,8 @@ static void handle_epin_slave(uint8_t rhport, uint8_t epnum, dwc2_diepint_t diep #if CFG_TUD_DWC2_DMA_ENABLE static void handle_epout_dma(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doepint_bm) { dwc2_regs_t* dwc2 = DWC2_REG(rhport); - xfer_ctl_t* xfer = XFER_CTL_BASE(epnum, TUSB_DIR_OUT); - const bool ep0_status_complete_before_setup = (epnum == 0) && doepint_bm.setup_phase_done && - doepint_bm.xfer_complete && - _dcd_data.ep0_out_zlp_armed && - (_dcd_data.ep0_pending[TUSB_DIR_OUT] == 0) && - (xfer->total_len == 0); if (doepint_bm.setup_phase_done) { - if (ep0_status_complete_before_setup) { - _dcd_data.ep0_out_zlp_armed = false; - dcd_event_xfer_complete(rhport, epnum, 0, XFER_RESULT_SUCCESS, true); - } else if (epnum == 0) { - _dcd_data.ep0_out_zlp_armed = false; - } - // Cleanup previous pending EP0 IN transfer if any dwc2_dep_t* epin0 = &DWC2_REG(rhport)->epin[0]; if (edpt_is_enabled(epin0)) { @@ -1064,6 +1049,7 @@ static void handle_epout_dma(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doepi edpt_schedule_packets(rhport, epnum, TUSB_DIR_OUT); } else { dwc2_dep_t* epout = &dwc2->epout[epnum]; + xfer_ctl_t* xfer = XFER_CTL_BASE(epnum, TUSB_DIR_OUT); // determine actual received bytes const dwc2_ep_tsize_t tsiz = {.value = epout->tsiz}; @@ -1076,9 +1062,6 @@ static void handle_epout_dma(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doepi dma_setup_prepare(rhport); } - if (epnum == 0) { - _dcd_data.ep0_out_zlp_armed = false; - } dcd_dcache_invalidate(xfer->buffer, xfer->total_len); dcd_event_xfer_complete(rhport, epnum, xfer->total_len, XFER_RESULT_SUCCESS, true); } -- cgit v1.3.1 From 3adb63e62a6c7dbc854a72049aa23537a51fabc5 Mon Sep 17 00:00:00 2001 From: UMRnInside <30196401+UMRnInside@users.noreply.github.com> Date: Sun, 24 May 2026 09:27:32 +0800 Subject: Fix typo found by Copilot chinese -> Chinese manufactors -> manufacturers Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- hw/bsp/ch32v10x/boards/ch32v103c_bluepill/board.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hw/bsp/ch32v10x/boards/ch32v103c_bluepill/board.h b/hw/bsp/ch32v10x/boards/ch32v103c_bluepill/board.h index 02558b953..3a97b44d6 100644 --- a/hw/bsp/ch32v10x/boards/ch32v103c_bluepill/board.h +++ b/hw/bsp/ch32v10x/boards/ch32v103c_bluepill/board.h @@ -1,4 +1,4 @@ -/* Some chinese manufactors use CH32V103C8T6 to make Bluepill boards +/* Some Chinese manufacturers use CH32V103C8T6 to make Bluepill boards */ /* metadata: name: CH32V103C8T6-Bluepill -- cgit v1.3.1 From 972670aed49d3efd31b6211aa40c33d550cfa3ad Mon Sep 17 00:00:00 2001 From: UMRnInside <30196401+UMRnInside@users.noreply.github.com> Date: Sun, 24 May 2026 10:00:06 +0800 Subject: use vendor-specific CSR 0x800 to toggle IRQ --- hw/bsp/ch32v10x/family.c | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/hw/bsp/ch32v10x/family.c b/hw/bsp/ch32v10x/family.c index 9943bf856..6d52227d5 100644 --- a/hw/bsp/ch32v10x/family.c +++ b/hw/bsp/ch32v10x/family.c @@ -66,11 +66,26 @@ uint32_t tusb_time_millis_api(void) { } #endif +// 0x800 CSR register is writable in U-mode +// according to manual: https://www.wch-ic.com/downloads/QingKeV3_Processor_Manual_PDF.html +__attribute__((always_inline)) RV_STATIC_INLINE +void __wch_vendor_enable_irq() +{ + __asm volatile ("csrs 0x800, %0" : : "r" (0x88) ); +} + +__attribute__((always_inline)) RV_STATIC_INLINE +void __wch_vendor_disable_irq() +{ + __asm volatile ("csrc 0x800, %0" : : "r" (0x88) ); + __asm volatile ("fence.i"); +} + void board_init(void) { /* __disable_irq() in CH32V103 EVT attempts to call * `csrc mstatus, 0x88` in U-mode, which is allowed ONLY in M-mode. * Disable this to avoid hard-fault. */ - //__disable_irq(); + __wch_vendor_disable_irq(); #if CFG_TUSB_OS == OPT_OS_NONE SysTick_Config(SystemCoreClock / 1000); @@ -126,7 +141,7 @@ void board_init(void) { USART_Init(USART1, &usart); USART_Cmd(USART1, ENABLE); - //__enable_irq(); + __wch_vendor_enable_irq(); board_led_write(true); } -- cgit v1.3.1 From e7d59fba3506d2ae0d60f4c019153e1d9d349262 Mon Sep 17 00:00:00 2001 From: UMRnInside <30196401+UMRnInside@users.noreply.github.com> Date: Sun, 24 May 2026 10:21:41 +0800 Subject: Fix missing prototype in ch32v10x bsp --- hw/bsp/ch32v10x/family.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hw/bsp/ch32v10x/family.c b/hw/bsp/ch32v10x/family.c index 6d52227d5..8eec9f4a1 100644 --- a/hw/bsp/ch32v10x/family.c +++ b/hw/bsp/ch32v10x/family.c @@ -69,13 +69,13 @@ uint32_t tusb_time_millis_api(void) { // 0x800 CSR register is writable in U-mode // according to manual: https://www.wch-ic.com/downloads/QingKeV3_Processor_Manual_PDF.html __attribute__((always_inline)) RV_STATIC_INLINE -void __wch_vendor_enable_irq() +void __wch_vendor_enable_irq(void) { __asm volatile ("csrs 0x800, %0" : : "r" (0x88) ); } __attribute__((always_inline)) RV_STATIC_INLINE -void __wch_vendor_disable_irq() +void __wch_vendor_disable_irq(void) { __asm volatile ("csrc 0x800, %0" : : "r" (0x88) ); __asm volatile ("fence.i"); @@ -84,7 +84,7 @@ void __wch_vendor_disable_irq() void board_init(void) { /* __disable_irq() in CH32V103 EVT attempts to call * `csrc mstatus, 0x88` in U-mode, which is allowed ONLY in M-mode. - * Disable this to avoid hard-fault. */ + * Replace this with CSR 0x800 to avoid hard-fault. */ __wch_vendor_disable_irq(); #if CFG_TUSB_OS == OPT_OS_NONE -- cgit v1.3.1 From 64915e6b40e16ec486dd9c65889cc0571245461d Mon Sep 17 00:00:00 2001 From: Ha Thach Date: Mon, 25 May 2026 12:59:30 +0700 Subject: Set GitHub Sponsors username in FUNDING.yml Updated GitHub Sponsors username for funding. --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..e9930a25a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: hathach -- cgit v1.3.1 From d63a45509fd283f5ffccc29cd8569e6efd075c59 Mon Sep 17 00:00:00 2001 From: Jie Feng Date: Thu, 21 May 2026 14:52:38 +0800 Subject: ch32_usbhs: fix endpoint stall length index and clear-stall response Fix issue in the stall handling: - dcd_edpt_stall() for an IN endpoint cleared EP_TX_LEN(0) instead of EP_TX_LEN(ep_num), clobbering endpoint 0's transmit length register when stalling any other IN endpoint. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/portable/wch/dcd_ch32_usbhs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/portable/wch/dcd_ch32_usbhs.c b/src/portable/wch/dcd_ch32_usbhs.c index a6dd5bb79..ea3b052ad 100644 --- a/src/portable/wch/dcd_ch32_usbhs.c +++ b/src/portable/wch/dcd_ch32_usbhs.c @@ -354,7 +354,7 @@ void dcd_edpt_stall(uint8_t rhport, uint8_t ep_addr) { if (dir == TUSB_DIR_OUT) { EP_RX_CTRL(ep_num) = USBHS_EP_R_RES_STALL; } else { - EP_TX_LEN(0) = 0; + EP_TX_LEN(ep_num) = 0; EP_TX_CTRL(ep_num) = USBHS_EP_T_RES_STALL; } } -- cgit v1.3.1 From 4ba1c39a225ee4ee96e8449f70bf0ba06be62753 Mon Sep 17 00:00:00 2001 From: Phozer <55053232+Phozer@users.noreply.github.com> Date: Tue, 26 May 2026 16:47:12 +0200 Subject: Fix typo in CDC stack size constant --- examples/device/cdc_msc_freertos/src/main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/device/cdc_msc_freertos/src/main.c b/examples/device/cdc_msc_freertos/src/main.c index f2f71d089..4a920c90b 100644 --- a/examples/device/cdc_msc_freertos/src/main.c +++ b/examples/device/cdc_msc_freertos/src/main.c @@ -88,7 +88,7 @@ int main(void) { #else xTaskCreate(led_blinking_task, "blinky", BLINKY_STACK_SIZE, NULL, 1, NULL); xTaskCreate(usb_device_task, "usbd", USBD_STACK_SIZE, NULL, configMAX_PRIORITIES - 1, NULL); - xTaskCreate(cdc_task, "cdc", CDC_STACK_SZIE, NULL, configMAX_PRIORITIES - 2, NULL); + xTaskCreate(cdc_task, "cdc", CDC_STACK_SIZE, NULL, configMAX_PRIORITIES - 2, NULL); #endif #ifndef ESP_PLATFORM -- cgit v1.3.1 From 97d84e0fd597b2d9362e47897c98c9b5ed9c6fdc Mon Sep 17 00:00:00 2001 From: UMRnInside <30196401+UMRnInside@users.noreply.github.com> Date: Tue, 26 May 2026 22:52:30 +0800 Subject: Enable GPIOC for CH32V103 bluepill boards --- hw/bsp/ch32v10x/family.c | 1 + 1 file changed, 1 insertion(+) diff --git a/hw/bsp/ch32v10x/family.c b/hw/bsp/ch32v10x/family.c index 8eec9f4a1..9f5dc5572 100644 --- a/hw/bsp/ch32v10x/family.c +++ b/hw/bsp/ch32v10x/family.c @@ -92,6 +92,7 @@ void board_init(void) { #endif RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); + RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); EXTEN->EXTEN_CTR |= EXTEN_USBFS_IO_EN; uint8_t usb_div; -- cgit v1.3.1 From 44a897bff086d42a0e6312e88ae335cca71d5a6f Mon Sep 17 00:00:00 2001 From: Phozer <55053232+Phozer@users.noreply.github.com> Date: Tue, 26 May 2026 17:25:10 +0200 Subject: Fix typo in CDC stack size constant here too --- examples/host/cdc_msc_hid_freertos/src/cdc_app.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/host/cdc_msc_hid_freertos/src/cdc_app.c b/examples/host/cdc_msc_hid_freertos/src/cdc_app.c index 30baacaac..9fad775e9 100644 --- a/examples/host/cdc_msc_hid_freertos/src/cdc_app.c +++ b/examples/host/cdc_msc_hid_freertos/src/cdc_app.c @@ -29,16 +29,16 @@ #include "app.h" #ifdef ESP_PLATFORM - #define CDC_STACK_SZIE 2048 + #define CDC_STACK_SIZE 2048 #else - #define CDC_STACK_SZIE (3*configMINIMAL_STACK_SIZE/2) + #define CDC_STACK_SIZE (3*configMINIMAL_STACK_SIZE/2) #endif //--------------------------------------------------------------------+ // MACRO TYPEDEF CONSTANT ENUM DECLARATION //--------------------------------------------------------------------+ #if configSUPPORT_STATIC_ALLOCATION -StackType_t cdc_stack[CDC_STACK_SZIE]; +StackType_t cdc_stack[CDC_STACK_SIZE]; StaticTask_t cdc_taskdef; #endif @@ -46,9 +46,9 @@ static void cdc_app_task(void* param); void cdc_app_init(void) { #if configSUPPORT_STATIC_ALLOCATION - (void) xTaskCreateStatic(cdc_app_task, "cdc", CDC_STACK_SZIE, NULL, configMAX_PRIORITIES-2, cdc_stack, &cdc_taskdef); + (void) xTaskCreateStatic(cdc_app_task, "cdc", CDC_STACK_SIZE, NULL, configMAX_PRIORITIES-2, cdc_stack, &cdc_taskdef); #else - (void) xTaskCreate(cdc_app_task, "cdc", CDC_STACK_SZIE, NULL, configMAX_PRIORITIES-2, NULL); + (void) xTaskCreate(cdc_app_task, "cdc", CDC_STACK_SIZE, NULL, configMAX_PRIORITIES-2, NULL); #endif } -- cgit v1.3.1 From e520cff090e5da57e38ef3c7af250aab6d57a186 Mon Sep 17 00:00:00 2001 From: HiFiPhile Date: Wed, 27 May 2026 00:19:42 +0200 Subject: dwc2: simplify EP0 ZLP handling Signed-off-by: HiFiPhile --- src/portable/synopsys/dwc2/dcd_dwc2.c | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/src/portable/synopsys/dwc2/dcd_dwc2.c b/src/portable/synopsys/dwc2/dcd_dwc2.c index 8997394dd..23fe671d4 100644 --- a/src/portable/synopsys/dwc2/dcd_dwc2.c +++ b/src/portable/synopsys/dwc2/dcd_dwc2.c @@ -69,9 +69,6 @@ typedef struct { // SOF enabling flag - required for SOF to not get disabled in ISR when SOF was enabled by bool sof_en; - - // EP0 status OUT flag - bool ep0_status_out; } dcd_data_t; static dcd_data_t _dcd_data; @@ -668,9 +665,6 @@ bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t* buffer, uint16_t to // EP0 can only handle one packet if (epnum == 0) { _dcd_data.ep0_pending[dir] = total_bytes; - if (dir == TUSB_DIR_OUT) { - _dcd_data.ep0_status_out = (total_bytes == 0); - } } // Schedule packets to be sent within interrupt @@ -752,7 +746,6 @@ static void handle_bus_reset(uint8_t rhport) { _dcd_data.sof_en = false; _dcd_data.allocated_epin_count = 0; - _dcd_data.ep0_status_out = false; // 1. NAK for all OUT endpoints for (uint8_t n = 0; n < ep_count; n++) { @@ -907,9 +900,6 @@ static void handle_rxflvl_irq(uint8_t rhport) { // We can receive up to three setup packets in succession, but only the last one is valid. setup[0] = (*rx_fifo); setup[1] = (*rx_fifo); - - // Clear previous pending EP0 OUT if any - _dcd_data.ep0_status_out = false; break; } @@ -940,6 +930,10 @@ static void handle_rxflvl_irq(uint8_t rhport) { xfer->total_len -= tsiz.xfer_size; if (epnum == 0) { _dcd_data.ep0_pending[TUSB_DIR_OUT] = 0; + // Handle EP0 STATUS OUT (or ZLP) here to avoid mix with next SETUP packet received IRQ + if (xfer->total_len == 0) { + dcd_event_xfer_complete(rhport, 0, 0, XFER_RESULT_SUCCESS, true); + } } } break; @@ -949,12 +943,6 @@ static void handle_rxflvl_irq(uint8_t rhport) { // Out packet done // After this entry is popped from the receive FIFO, dwc2 asserts a Transfer Completed interrupt on // the specified OUT endpoint which will be handled by handle_epout_irq() - - // EP0 status OUT is complete - if (epnum == 0 && _dcd_data.ep0_status_out) { - _dcd_data.ep0_status_out = false; - dcd_event_xfer_complete(rhport, epnum, 0, XFER_RESULT_SUCCESS, true); - } break; default: break; // nothing to do @@ -984,7 +972,7 @@ static void handle_epout_slave(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doe // EP0 can only handle one packet, Schedule another packet to be received. edpt_schedule_packets(rhport, epnum, TUSB_DIR_OUT); } else if (xfer->total_len > 0) { - // EP0 status out is handled in handle_rxflvl_irq + // EP0 STATUS OUT (or ZLP) is handled in handle_rxflvl_irq() dcd_event_xfer_complete(rhport, epnum, xfer->total_len, XFER_RESULT_SUCCESS, true); } } else { -- cgit v1.3.1 From a560281051022473c844bdb258be452da52ce5c3 Mon Sep 17 00:00:00 2001 From: HiFiPhile Date: Fri, 22 May 2026 13:42:08 +0200 Subject: dwc2: fix EP0 DMA setup race condition Signed-off-by: HiFiPhile --- src/portable/synopsys/dwc2/dcd_dwc2.c | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/portable/synopsys/dwc2/dcd_dwc2.c b/src/portable/synopsys/dwc2/dcd_dwc2.c index 23fe671d4..1c119eef4 100644 --- a/src/portable/synopsys/dwc2/dcd_dwc2.c +++ b/src/portable/synopsys/dwc2/dcd_dwc2.c @@ -73,8 +73,9 @@ typedef struct { static dcd_data_t _dcd_data; -CFG_TUD_MEM_SECTION static struct { - TUD_EPBUF_DEF(setup_packet, 8); +CFG_TUD_MEM_SECTION static union { + TUD_EPBUF_DEF(setup_buffer, 8); + tusb_control_request_t setup_packet; } _dcd_usbbuf; static tud_configure_dwc2_t _tud_cfg = CFG_TUD_CONFIGURE_DWC2_DEFAULT; @@ -138,7 +139,7 @@ static void dma_setup_prepare(uint8_t rhport) { // Receive only 1 packet dwc2->epout[0].doeptsiz = (1 << DOEPTSIZ_STUPCNT_Pos) | (1 << DOEPTSIZ_PKTCNT_Pos) | (8 << DOEPTSIZ_XFRSIZ_Pos); - dwc2->epout[0].doepdma = (uintptr_t) _dcd_usbbuf.setup_packet; + dwc2->epout[0].doepdma = (uintptr_t) _dcd_usbbuf.setup_buffer; dwc2->epout[0].doepctl |= DOEPCTL_EPENA | DOEPCTL_USBAEP; } @@ -896,7 +897,7 @@ static void handle_rxflvl_irq(uint8_t rhport) { case GRXSTS_PKTSTS_SETUP_RX: { // Setup packet received - uint32_t* setup = (uint32_t*)(uintptr_t) _dcd_usbbuf.setup_packet; + uint32_t* setup = (uint32_t*)(uintptr_t) _dcd_usbbuf.setup_buffer; // We can receive up to three setup packets in succession, but only the last one is valid. setup[0] = (*rx_fifo); setup[1] = (*rx_fifo); @@ -956,7 +957,7 @@ static void handle_epout_slave(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doe if (edpt_is_enabled(epin0)) { edpt_disable(rhport, 0x80, false); } - dcd_event_setup_received(rhport, _dcd_usbbuf.setup_packet, true); + dcd_event_setup_received(rhport, _dcd_usbbuf.setup_buffer, true); return; } @@ -1021,9 +1022,13 @@ static void handle_epout_dma(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doepi if (edpt_is_enabled(epin0)) { edpt_disable(rhport, 0x80, false); } - dma_setup_prepare(rhport); - dcd_dcache_invalidate(_dcd_usbbuf.setup_packet, 8); - dcd_event_setup_received(rhport, _dcd_usbbuf.setup_packet, true); + dcd_dcache_invalidate(_dcd_usbbuf.setup_buffer, 8); + dcd_event_setup_received(rhport, _dcd_usbbuf.setup_buffer, true); + + // Prepare EP0 for next setup if this setup has no data stage + if (_dcd_usbbuf.setup_packet.wLength == 0) { + dma_setup_prepare(rhport); + } return; } @@ -1044,9 +1049,8 @@ static void handle_epout_dma(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doepi const uint16_t remain = tsiz.xfer_size; xfer->total_len -= remain; - // this is ZLP, so prepare EP0 for next setup - // TODO use status phase rx - if(epnum == 0 && xfer->total_len == 0) { + // prepare EP0 for next setup + if(epnum == 0) { dma_setup_prepare(rhport); } @@ -1065,9 +1069,6 @@ static void handle_epin_dma(uint8_t rhport, uint8_t epnum, dwc2_diepint_t diepin // EP0 can only handle one packet. Schedule another packet to be transmitted. edpt_schedule_packets(rhport, epnum, TUSB_DIR_IN); } else { - if(epnum == 0) { - dma_setup_prepare(rhport); - } dcd_event_xfer_complete(rhport, epnum | TUSB_DIR_IN_MASK, xfer->total_len, XFER_RESULT_SUCCESS, true); } } -- cgit v1.3.1 From 4a131e1562d8e388bb86592278324ab6bc9f215d Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 25 May 2026 15:13:30 +0700 Subject: hil: replace pyfatfs with mtools, update host setup instructions - Removed `pyfatfs` dependency in favor of `mtools` for reading FAT volumes, simplifying the block device read logic. - Updated `requirements.txt` and added detailed host setup instructions for system packages. - Switched to `cython-hidapi` for HID tests, replacing deprecated APIs with updated usage. - Removed unnecessary warnings suppression and `fs` module. --- test/hil/hil_ci.sh | 5 ++- test/hil/hil_test.py | 82 +++++++++++++++++++++++++++-------------------- test/hil/requirements.txt | 10 ++++-- 3 files changed, 59 insertions(+), 38 deletions(-) diff --git a/test/hil/hil_ci.sh b/test/hil/hil_ci.sh index 35e71f1ba..4c7ba2936 100644 --- a/test/hil/hil_ci.sh +++ b/test/hil/hil_ci.sh @@ -76,8 +76,11 @@ if [ -n "$BOARD" ]; then copy_board_binaries "$BUILD_DIR" else echo "==> Copying all built binaries" + # Use `%/` parameter expansion to strip the trailing slash from the glob — + # rsync needs the bare dir name so the per-board cmake-build-/ subdir + # is preserved on the remote (hil_test.py looks up binaries by that path). for dir in "$ROOT_DIR"/examples/cmake-build-*/; do - [ -d "$dir" ] && copy_board_binaries "$dir" + [ -d "$dir" ] && copy_board_binaries "${dir%/}" done fi diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index d921a910a..b0b3fc17e 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -22,6 +22,14 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +# Host setup: +# - System packages: sudo apt install mtools libmtp9 alsa-utils iperf +# mtools - read_disk_file (device/cdc_msc, device/msc_dual_lun) +# libmtp9 - pymtp ctypes load (device/mtp); Debian 13 uses libmtp9t64 +# alsa-utils - arecord (device/audio_test_freertos) +# iperf - throughput tests (device/net_lwip_*) +# - Python packages: pip install -r requirements.txt +# # udev rules : # ACTION=="add", SUBSYSTEM=="tty", SUBSYSTEMS=="usb", MODE="0666", PROGRAM="/bin/sh -c 'echo $$ID_SERIAL_SHORT | rev | cut -c -8 | rev'", SYMLINK+="ttyUSB_%c.%s{bInterfaceNumber}" # ACTION=="add", SUBSYSTEM=="block", SUBSYSTEMS=="usb", ENV{ID_FS_USAGE}=="filesystem", MODE="0666", PROGRAM="/bin/sh -c 'echo $$ID_SERIAL_SHORT | rev | cut -c -8 | rev'", RUN{program}+="/usr/bin/systemd-mount --no-block --automount=yes --collect $devnode /media/blkUSB_%c.%s{bInterfaceNumber}" @@ -34,17 +42,11 @@ import re import select import sys import time -import warnings import signal from contextlib import redirect_stdout from pathlib import Path from typing import Any, TypedDict, NotRequired, cast -# Suppress pkg_resources deprecation warning from fs module -warnings.filterwarnings("ignore", message="pkg_resources is deprecated") -# Suppress pyfatfs unclean unmount warning -warnings.filterwarnings("ignore", message="Filesystem was not cleanly unmounted") - import serial import subprocess import json @@ -52,7 +54,6 @@ import glob import shutil from multiprocessing import Pool, Lock from multiprocessing import TimeoutError as MpTimeoutError -import fs import hashlib import ctypes from pymtp import MTP @@ -232,23 +233,24 @@ def open_serial_dev(port: str): def read_disk_file(uid: str, lun: int, fname: str) -> bytes: - # open_fs("fat://{dev}) require 'pip install pyfatfs' + # Reads a file from a FAT volume on a block device without mounting it. + # Requires mtools: `apt install mtools` (no pip dependency). dev = get_disk_dev(uid, 'TinyUSB', lun) timeout = ENUM_TIMEOUT + last_err = None while timeout > 0: if os.path.exists(dev): - fat = fs.open_fs(f'fat://{dev}?read_only=true') try: - with fat.open(fname, 'rb') as f: - data = f.read() - finally: - fat.close() - assert data, f'Cannot read file {fname} from {dev}' - return data + data = subprocess.check_output( + ['mtype', '-i', dev, f'::/{fname}'], stderr=subprocess.PIPE) + assert data, f'Cannot read file {fname} from {dev}' + return data + except subprocess.CalledProcessError as e: + last_err = e.stderr.decode(errors='replace').strip() time.sleep(1) timeout -= 1 - raise AssertionError(f'Storage {dev} not existed') + raise AssertionError(f'mtype failed on {dev}: {last_err}' if last_err else f'Storage {dev} not existed') def open_mtp_dev(uid): @@ -1406,7 +1408,7 @@ def test_device_audio_test_freertos(board): def test_device_hid_generic_inout(board): uid = board['uid'] - import hid + import hid # cython-hidapi (pip: hidapi, apt: python3-hid) # Find HID device by UID (VID=0xCafe) timeout = ENUM_TIMEOUT @@ -1422,22 +1424,23 @@ def test_device_hid_generic_inout(board): timeout -= 1 assert dev is not None, f'HID device not found for {uid}' - h = hid.Device(vid=dev['vendor_id'], pid=dev['product_id'], serial=uid) - - # Echo test: send random data and verify echo - for size in [8, 32, 63]: - # Report ID (0) + payload, padded to 64 bytes - payload = bytes([random.randint(1, 255) for _ in range(size)]) - report = bytes([0]) + payload + bytes(64 - size) - h.write(report) - echo = h.read(64, timeout=2000) - assert echo is not None and len(echo) >= size, ( - f'HID echo timeout or short read ({size} bytes)') - assert bytes(echo[:size]) == payload, ( - f'HID echo wrong data ({size} bytes):\n' - f' expected: {payload.hex()}\n received: {bytes(echo[:size]).hex()}') - - h.close() + h = hid.device() + h.open(dev['vendor_id'], dev['product_id'], uid) + try: + # Echo test: send random data and verify echo + for size in [8, 32, 63]: + # Report ID (0) + payload, padded to 64 bytes + payload = bytes([random.randint(1, 255) for _ in range(size)]) + report = bytes([0]) + payload + bytes(64 - size) + h.write(report) + echo = h.read(64, 2000) + assert echo and len(echo) >= size, ( + f'HID echo timeout or short read ({size} bytes)') + assert bytes(echo[:size]) == payload, ( + f'HID echo wrong data ({size} bytes):\n' + f' expected: {payload.hex()}\n received: {bytes(echo[:size]).hex()}') + finally: + h.close() # ------------------------------------------------------------- @@ -1601,7 +1604,18 @@ def test_board(board: Board) -> tuple[str, int, list[str]]: if name in board_test: test_list = board_test[name] elif len(test_only) > 0: - test_list = test_only + # Explicit -t: filter against the board's capabilities so a device-only + # board doesn't try to run host/dual tests (the test functions need a + # `dev_attached` entry in the board config that won't exist). + board_tests = board.get('tests', {}) + if 'only' in board_tests: + allowed = set(board_tests['only']) + test_list = [t for t in test_only if t in allowed] + else: + for t in test_only: + category = t.split('/', 1)[0] + if board_tests.get(category) is True: + test_list.append(t) else: if 'tests' in board: board_tests = board['tests'] diff --git a/test/hil/requirements.txt b/test/hil/requirements.txt index 127f6a8ec..ef1cf575b 100644 --- a/test/hil/requirements.txt +++ b/test/hil/requirements.txt @@ -1,5 +1,9 @@ -fs -hid -pyfatfs +# System packages (install separately): +# sudo apt install mtools libmtp9 alsa-utils iperf +# mtools - read_disk_file (device/cdc_msc, device/msc_dual_lun) +# libmtp9 - pymtp ctypes load (device/mtp); Debian 13 uses libmtp9t64 +# alsa-utils - arecord (device/audio_test_freertos) +# iperf - throughput tests (device/net_lwip_*) +hidapi pyserial esptool -- cgit v1.3.1 From 616acfa7328b1a05ea0370a3628fc6a07f4caf57 Mon Sep 17 00:00:00 2001 From: hathach Date: Wed, 27 May 2026 19:20:58 +0700 Subject: osal add osal_task_get_current_handle() --- src/osal/osal.h | 47 +++++++++++++++++++++++++---------------------- src/osal/osal_freertos.h | 7 +++++++ src/osal/osal_mynewt.h | 6 ++++++ src/osal/osal_none.h | 16 ++++++++++++++++ src/osal/osal_pico.h | 7 +++++++ src/osal/osal_rtthread.h | 6 ++++++ src/osal/osal_rtx4.h | 6 ++++++ src/osal/osal_threadx.h | 5 +++++ src/osal/osal_zephyr.h | 6 ++++++ 9 files changed, 84 insertions(+), 22 deletions(-) diff --git a/src/osal/osal.h b/src/osal/osal.h index 4840463f3..69cb356d4 100644 --- a/src/osal/osal.h +++ b/src/osal/osal.h @@ -76,28 +76,31 @@ typedef void (*osal_task_func_t)(void* param); /*-------------------------------------------------------------------- OSAL Porting API Should be implemented as static inline function in osal_port.h header - uint32_t osal_time_millis(void); - - void osal_spin_init(osal_spinlock_t *ctx); - void osal_spin_lock(osal_spinlock_t *ctx, bool in_isr) - void osal_spin_unlock(osal_spinlock_t *ctx, bool in_isr); - - osal_semaphore_t osal_semaphore_create(osal_semaphore_def_t* semdef); - bool osal_semaphore_delete(osal_semaphore_t semd_hdl); - bool osal_semaphore_post(osal_semaphore_t sem_hdl, bool in_isr); - bool osal_semaphore_wait(osal_semaphore_t sem_hdl, uint32_t msec); - void osal_semaphore_reset(osal_semaphore_t sem_hdl); - - osal_mutex_t osal_mutex_create(osal_mutex_def_t* mdef); - bool osal_mutex_delete(osal_mutex_t mutex_hdl) - bool osal_mutex_lock (osal_mutex_t sem_hdl, uint32_t msec); - bool osal_mutex_unlock(osal_mutex_t mutex_hdl); - - osal_queue_t osal_queue_create(osal_queue_def_t* qdef); - bool osal_queue_delete(osal_queue_t qhdl); - bool osal_queue_receive(osal_queue_t qhdl, void* data, uint32_t msec); - bool osal_queue_send(osal_queue_t qhdl, void const * data, bool in_isr); - bool osal_queue_empty(osal_queue_t qhdl); + uint32_t osal_time_millis(void); + + void osal_task_delay(uint32_t msec); + osal_task_handle_t osal_task_get_current_handle(void); + + void osal_spin_init(osal_spinlock_t *ctx); + void osal_spin_lock(osal_spinlock_t *ctx, bool in_isr) + void osal_spin_unlock(osal_spinlock_t *ctx, bool in_isr); + + osal_semaphore_t osal_semaphore_create(osal_semaphore_def_t* semdef); + bool osal_semaphore_delete(osal_semaphore_t semd_hdl); + bool osal_semaphore_post(osal_semaphore_t sem_hdl, bool in_isr); + bool osal_semaphore_wait(osal_semaphore_t sem_hdl, uint32_t msec); + void osal_semaphore_reset(osal_semaphore_t sem_hdl); + + osal_mutex_t osal_mutex_create(osal_mutex_def_t* mdef); + bool osal_mutex_delete(osal_mutex_t mutex_hdl) + bool osal_mutex_lock (osal_mutex_t sem_hdl, uint32_t msec); + bool osal_mutex_unlock(osal_mutex_t mutex_hdl); + + osal_queue_t osal_queue_create(osal_queue_def_t* qdef); + bool osal_queue_delete(osal_queue_t qhdl); + bool osal_queue_receive(osal_queue_t qhdl, void* data, uint32_t msec); + bool osal_queue_send(osal_queue_t qhdl, void const * data, bool in_isr); + bool osal_queue_empty(osal_queue_t qhdl); --------------------------------------------------------------------------*/ diff --git a/src/osal/osal_freertos.h b/src/osal/osal_freertos.h index 898edd4ed..9b12b5c0e 100644 --- a/src/osal/osal_freertos.h +++ b/src/osal/osal_freertos.h @@ -83,6 +83,13 @@ typedef struct { //--------------------------------------------------------------------+ // TASK API //--------------------------------------------------------------------+ +typedef TaskHandle_t osal_task_handle_t; + +// Requires INCLUDE_xTaskGetCurrentTaskHandle == 1 in FreeRTOSConfig.h. +TU_ATTR_ALWAYS_INLINE static inline osal_task_handle_t osal_task_get_current_handle(void) { + return xTaskGetCurrentTaskHandle(); +} + TU_ATTR_ALWAYS_INLINE static inline uint32_t _osal_ms2tick(uint32_t msec) { if (msec == OSAL_TIMEOUT_WAIT_FOREVER) { return portMAX_DELAY; } if (msec == 0) { return 0; } diff --git a/src/osal/osal_mynewt.h b/src/osal/osal_mynewt.h index 335d53491..d1fa77ecb 100644 --- a/src/osal/osal_mynewt.h +++ b/src/osal/osal_mynewt.h @@ -36,6 +36,12 @@ //--------------------------------------------------------------------+ // TASK API //--------------------------------------------------------------------+ +typedef struct os_task* osal_task_handle_t; + +TU_ATTR_ALWAYS_INLINE static inline osal_task_handle_t osal_task_get_current_handle(void) { + return os_sched_get_current_task(); +} + TU_ATTR_ALWAYS_INLINE static inline void osal_task_delay(uint32_t msec) { os_time_delay( os_time_ms_to_ticks32(msec) ); } diff --git a/src/osal/osal_none.h b/src/osal/osal_none.h index 7bf6029d6..e174d3518 100644 --- a/src/osal/osal_none.h +++ b/src/osal/osal_none.h @@ -33,6 +33,22 @@ extern "C" { // osal_time_millis() is not provided, tusb_time_millis_api() must be implemented by user application +//--------------------------------------------------------------------+ +// TASK API +//--------------------------------------------------------------------+ +// Bare-metal single context: return a non-NULL sentinel so equality compares true. +typedef void* osal_task_handle_t; + +TU_ATTR_ALWAYS_INLINE static inline osal_task_handle_t osal_task_get_current_handle(void) { + return (osal_task_handle_t) 1; +} + +// Bare-metal has no scheduler to yield to; this is dead code in practice because +// callers gate it on running outside the host task, which can't happen here. +TU_ATTR_ALWAYS_INLINE static inline void osal_task_delay(uint32_t msec) { + (void) msec; +} + //--------------------------------------------------------------------+ // Spinlock API //--------------------------------------------------------------------+ diff --git a/src/osal/osal_pico.h b/src/osal/osal_pico.h index 6a0a21bb3..364c38b01 100644 --- a/src/osal/osal_pico.h +++ b/src/osal/osal_pico.h @@ -39,6 +39,13 @@ extern "C" { //--------------------------------------------------------------------+ // TASK API //--------------------------------------------------------------------+ +// Bare-metal single context: return a non-NULL sentinel so equality compares true. +typedef void* osal_task_handle_t; + +TU_ATTR_ALWAYS_INLINE static inline osal_task_handle_t osal_task_get_current_handle(void) { + return (osal_task_handle_t) 1; +} + TU_ATTR_ALWAYS_INLINE static inline void osal_task_delay(uint32_t msec) { sleep_ms(msec); } diff --git a/src/osal/osal_rtthread.h b/src/osal/osal_rtthread.h index f560281c5..a151a7d70 100644 --- a/src/osal/osal_rtthread.h +++ b/src/osal/osal_rtthread.h @@ -38,6 +38,12 @@ extern "C" { //--------------------------------------------------------------------+ // TASK API //--------------------------------------------------------------------+ +typedef rt_thread_t osal_task_handle_t; + +TU_ATTR_ALWAYS_INLINE static inline osal_task_handle_t osal_task_get_current_handle(void) { + return rt_thread_self(); +} + TU_ATTR_ALWAYS_INLINE static inline void osal_task_delay(uint32_t msec) { rt_thread_mdelay(msec); } diff --git a/src/osal/osal_rtx4.h b/src/osal/osal_rtx4.h index e1930c96c..e5b708a2c 100644 --- a/src/osal/osal_rtx4.h +++ b/src/osal/osal_rtx4.h @@ -37,6 +37,12 @@ extern "C" { //--------------------------------------------------------------------+ // TASK API //--------------------------------------------------------------------+ +typedef OS_TID osal_task_handle_t; + +TU_ATTR_ALWAYS_INLINE static inline osal_task_handle_t osal_task_get_current_handle(void) { + return os_tsk_self(); +} + TU_ATTR_ALWAYS_INLINE static inline void osal_task_delay(uint32_t msec) { uint16_t hi = msec >> 16; uint16_t lo = msec; diff --git a/src/osal/osal_threadx.h b/src/osal/osal_threadx.h index 6bcf9c5ab..cca4eb487 100644 --- a/src/osal/osal_threadx.h +++ b/src/osal/osal_threadx.h @@ -37,6 +37,11 @@ extern "C" { //--------------------------------------------------------------------+ // TASK API //--------------------------------------------------------------------+ +typedef TX_THREAD* osal_task_handle_t; + +TU_ATTR_ALWAYS_INLINE static inline osal_task_handle_t osal_task_get_current_handle(void) { + return tx_thread_identify(); +} TU_ATTR_ALWAYS_INLINE static inline uint32_t _osal_ms2tick(uint32_t msec) { if ( msec == TX_WAIT_FOREVER ) { diff --git a/src/osal/osal_zephyr.h b/src/osal/osal_zephyr.h index 900ac786c..6ea45131e 100644 --- a/src/osal/osal_zephyr.h +++ b/src/osal/osal_zephyr.h @@ -31,6 +31,12 @@ //--------------------------------------------------------------------+ // TASK API //--------------------------------------------------------------------+ +typedef k_tid_t osal_task_handle_t; + +TU_ATTR_ALWAYS_INLINE static inline osal_task_handle_t osal_task_get_current_handle(void) { + return k_current_get(); +} + TU_ATTR_ALWAYS_INLINE static inline void osal_task_delay(uint32_t msec) { k_msleep(msec); } -- cgit v1.3.1 From da038673936cefdff0e4aacfd2602c665b9f114d Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Wed, 27 May 2026 14:38:53 +0200 Subject: Add data loss warning to CH32 USBFS/USBHS According to WCH FAE transfer complete interrupt is not queued, later completed EP will overwrite INT_ST and RX_LEN. Which means to guarantee data integrity when multiples EPs are enabled: - USB IRQ must have highest priority - IRQ processing time must be less than ZLP duration (~600ns), which is absurd for a 144MHz CPU (85 cycles) Signed-off-by: Zixun LI --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 04998abaa..e1e73c3ad 100644 --- a/README.rst +++ b/README.rst @@ -262,11 +262,11 @@ Supported CPUs +--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ | ValentyUSB | eptri | ✔ | ✖ | ✖ | eptri | | +--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ -| WCH | CH32F20x | ✔ | | ✔ | ch32_usbhs | | +| WCH | CH32F20x | ⚠ | | ✔ | ch32_usbhs | Data loss possible | | +-----------------------------+--------+------+-----------+------------------------+--------------------+ -| | CH32V20x | ✔ | | ✖ | stm32_fsdev/ch32_usbfs | | +| | CH32V20x | ⚠ | | ✖ | stm32_fsdev/ch32_usbfs | Data loss possible | | +-----------------------------+--------+------+-----------+------------------------+--------------------+ -| | CH32V305, CH32V307 | ✔ | | ✔ | ch32_usbfs/hs | | +| | CH32V305, CH32V307 | ⚠ | | ✔ | ch32_usbfs/hs | Data loss possible | +--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ Table Legend -- cgit v1.3.1 From c954c8c4c70df616e429d8e7ae1c382d3043acea Mon Sep 17 00:00:00 2001 From: hathach Date: Thu, 28 May 2026 18:20:22 +0700 Subject: remove sync() in tinyusb callback since it cause issue with RTOS when usbh task is blocking --- examples/dual/dynamic_switch/src/main.c | 166 ++++++++++++++++---------------- examples/host/bare_api/src/main.c | 43 ++++++--- examples/host/cdc_msc_hid/src/cdc_app.c | 6 -- examples/host/device_info/src/main.c | 71 +++++++++++--- 4 files changed, 173 insertions(+), 113 deletions(-) diff --git a/examples/dual/dynamic_switch/src/main.c b/examples/dual/dynamic_switch/src/main.c index c9ad2a835..e8e0deb53 100644 --- a/examples/dual/dynamic_switch/src/main.c +++ b/examples/dual/dynamic_switch/src/main.c @@ -79,6 +79,9 @@ StaticTask_t usb_taskdef; StackType_t cdc_stack[CDC_STACK_SIZE]; StaticTask_t cdc_taskdef; + +StackType_t devinfo_stack[USBH_STACK_SIZE]; +StaticTask_t devinfo_taskdef; #endif #endif @@ -87,14 +90,13 @@ static tusb_role_t current_role = TUSB_ROLE_DEVICE; #if CFG_TUSB_OS == OPT_OS_FREERTOS static void usb_task(void *param); -void led_blinking_task(void *param); -void cdc_task(void *params); -#else -void led_blinking_task(void); -void cdc_task(void); #endif +void led_blinking_task(void *param); +void cdc_task(void *param); +void print_devinfo_task(void *param); + void usb_mode_switch(void); -static void print_device_info(uint8_t daddr); +static void print_one_device(uint8_t daddr); static void print_utf16(uint16_t* temp_buf, size_t buf_len); // Declare buffer for USB transfer @@ -124,11 +126,13 @@ int main(void) { xTaskCreateStatic(usb_task, "usb", USBD_STACK_SIZE > USBH_STACK_SIZE ? USBD_STACK_SIZE : USBH_STACK_SIZE, NULL, configMAX_PRIORITIES-1, usb_stack, &usb_taskdef); xTaskCreateStatic(cdc_task, "cdc", CDC_STACK_SIZE, NULL, configMAX_PRIORITIES - 2, cdc_stack, &cdc_taskdef); + xTaskCreateStatic(print_devinfo_task, "devinfo", USBH_STACK_SIZE, NULL, configMAX_PRIORITIES - 2, devinfo_stack, &devinfo_taskdef); #else xTaskCreate(led_blinking_task, "blinky", BLINKY_STACK_SIZE, NULL, 1, NULL); xTaskCreate(usb_task, "usb", USBD_STACK_SIZE > USBH_STACK_SIZE ? USBD_STACK_SIZE : USBH_STACK_SIZE, NULL, configMAX_PRIORITIES - 1, NULL); xTaskCreate(cdc_task, "cdc", CDC_STACK_SIZE, NULL, configMAX_PRIORITIES - 2, NULL); + xTaskCreate(print_devinfo_task, "devinfo", USBH_STACK_SIZE, NULL, configMAX_PRIORITIES - 2, NULL); #endif #ifndef ESP_PLATFORM @@ -162,12 +166,13 @@ int main(void) { // Process USB tasks based on current mode if (current_role == TUSB_ROLE_DEVICE) { tud_task(); - cdc_task(); + cdc_task(NULL); } else { tuh_task(); + print_devinfo_task(NULL); } - led_blinking_task(); + led_blinking_task(NULL); } #endif } @@ -227,21 +232,28 @@ static void usb_task(void *param) { void usb_mode_switch(void) { printf("\r\n--- Switching USB mode ---\r\n"); - // Deinitialize current mode - if (current_role == TUSB_ROLE_DEVICE) { + // Snapshot then clear current_role BEFORE tusb_deinit() so concurrent + // tasks (cdc_task / print_devinfo_task on RTOS) see the role-change + // boundary and exit cleanly instead of calling host/device APIs against + // a deinitialised stack. + const tusb_role_t prev_role = current_role; + current_role = TUSB_ROLE_INVALID; + + if (prev_role == TUSB_ROLE_DEVICE) { printf("Stopping DEVICE mode...\r\n"); - tusb_deinit(BOARD_RHPORT); } else { printf("Stopping HOST mode...\r\n"); - tusb_deinit(BOARD_RHPORT); } + tusb_deinit(BOARD_RHPORT); #if CFG_TUSB_OS == OPT_OS_FREERTOS vTaskDelay(pdMS_TO_TICKS(100)); // Small delay for clean transition #else - tusb_time_delay_ms_api(100); // Small delay for clean transition -#endif // Switch to the other mode - if (current_role == TUSB_ROLE_DEVICE) { + tusb_time_delay_ms_api(100); +#endif + + // Switch to the other mode + if (prev_role == TUSB_ROLE_DEVICE) { printf("Starting HOST mode...\r\n"); tusb_rhport_init_t host_init = { .role = TUSB_ROLE_HOST, @@ -267,61 +279,30 @@ void usb_mode_switch(void) { // Device Mode: CDC Task //--------------------------------------------------------------------+ -#if CFG_TUSB_OS == OPT_OS_FREERTOS -void cdc_task(void *params) { - (void) params; - - // RTOS forever loop +void cdc_task(void *param) { + (void) param; while (1) { - // Only process CDC when in device mode + // Only touch device-CDC APIs while we're in device mode. After + // usb_mode_switch() sets current_role to INVALID and tusb_deinit() runs, + // calling tud_cdc_write_flush() here would hit a deinit'd device stack. if (current_role == TUSB_ROLE_DEVICE) { - // Connected and there are data available - while (tud_cdc_available()) { + if (tud_cdc_available()) { uint8_t buf[64]; - - // Read data - uint32_t count = tud_cdc_read(buf, sizeof(buf)); - - // Echo back - tud_cdc_write(buf, count); - - // Add newline for carriage return - for (uint32_t i = 0; i < count; i++) { - if (buf[i] == '\r') { - tud_cdc_write_char('\n'); - break; - } + const uint32_t count = tud_cdc_read(buf, sizeof(buf)); + if (count) { + tud_cdc_write(buf, count); } } - tud_cdc_write_flush(); } +#if CFG_TUSB_OS == OPT_OS_FREERTOS vTaskDelay(pdMS_TO_TICKS(10)); - } -} #else -void cdc_task(void) { - // Connected and there are data available - if (tud_cdc_available()) { - uint8_t buf[64]; - - // Read data - uint32_t count = tud_cdc_read(buf, sizeof(buf)); - - // Echo back - for (uint32_t i = 0; i < count; i++) { - tud_cdc_write_char(buf[i]); - - if (buf[i] == '\r') { - tud_cdc_write_char('\n'); - } - } - - tud_cdc_write_flush(); + return; // main loop will call us again +#endif } } -#endif //--------------------------------------------------------------------+ // Device Callbacks @@ -356,24 +337,57 @@ void tud_resume_cb(void) { // Host Callbacks //--------------------------------------------------------------------+ -// Invoked when device is mounted (configured) +// One flag per possible device address — set by tuh_mount_cb (host task) and +// cleared by print_devinfo_task once the device's descriptors are printed. +static volatile bool need_devinfo[CFG_TUH_DEVICE_MAX + 1]; + +// Invoked when device is mounted (configured). Runs in the host task — keep +// minimal; descriptor fetching happens in print_devinfo_task (different +// context so sync helpers are safe). void tuh_mount_cb(uint8_t daddr) { printf("[HOST] Device attached, address = %d\r\n", daddr); blink_interval_ms = BLINK_MOUNTED; - print_device_info(daddr); + if (daddr < TU_ARRAY_SIZE(need_devinfo)) { + need_devinfo[daddr] = true; + } } // Invoked when device is unmounted (unplugged) void tuh_umount_cb(uint8_t daddr) { printf("[HOST] Device removed, address = %d\r\n", daddr); blink_interval_ms = BLINK_NOT_MOUNTED; + if (daddr < TU_ARRAY_SIZE(need_devinfo)) { + need_devinfo[daddr] = false; + } } //--------------------------------------------------------------------+ -// Host Device Info +// Host Device Info — serialises descriptor fetching across all mounted +// devices using sync helpers. Safe to call from main loop (OS_NONE) or a +// dedicated task (FreeRTOS) — but NOT from a host-stack callback. //--------------------------------------------------------------------+ -static void print_device_info(uint8_t daddr) { +void print_devinfo_task(void *param) { + (void) param; + while (1) { + if (current_role == TUSB_ROLE_HOST) { + for (uint8_t daddr = 1; daddr < TU_ARRAY_SIZE(need_devinfo); daddr++) { + if (need_devinfo[daddr]) { + need_devinfo[daddr] = false; + print_one_device(daddr); + } + } + } + +#if CFG_TUSB_OS == OPT_OS_FREERTOS + vTaskDelay(pdMS_TO_TICKS(10)); +#else + return; +#endif + } +} + +static void print_one_device(uint8_t daddr) { // Get Device Descriptor uint8_t xfer_result = tuh_descriptor_get_device_sync(daddr, &desc.device, 18); if (XFER_RESULT_SUCCESS != xfer_result) { @@ -467,30 +481,20 @@ static void print_utf16(uint16_t* temp_buf, size_t buf_len) { // Blinking Task //--------------------------------------------------------------------+ -#if CFG_TUSB_OS == OPT_OS_FREERTOS void led_blinking_task(void *param) { (void) param; + static uint32_t start_ms = 0; static bool led_state = false; - - // RTOS forever loop while (1) { - board_led_write(led_state); - led_state = 1 - led_state; // toggle +#if CFG_TUSB_OS == OPT_OS_FREERTOS vTaskDelay(pdMS_TO_TICKS(blink_interval_ms)); - } -} #else -void led_blinking_task(void) { - static uint32_t start_ms = 0; - static bool led_state = false; - - // Blink every interval ms - if (tusb_time_millis_api() - start_ms < blink_interval_ms) { - return; // not enough time + if (tusb_time_millis_api() - start_ms < blink_interval_ms) { + return; // not enough time + } +#endif + start_ms += blink_interval_ms; + board_led_write(led_state); + led_state = 1 - led_state; // toggle } - start_ms += blink_interval_ms; - - board_led_write(led_state); - led_state = 1 - led_state; // toggle } -#endif diff --git a/examples/host/bare_api/src/main.c b/examples/host/bare_api/src/main.c index 544f38102..679ce6f43 100644 --- a/examples/host/bare_api/src/main.c +++ b/examples/host/bare_api/src/main.c @@ -48,14 +48,19 @@ CFG_TUH_MEM_SECTION uint16_t temp_buf[128]; // temp buffer for string descriptor // MACRO CONSTANT TYPEDEF PROTYPES //--------------------------------------------------------------------+ void led_blinking_task(void); +void print_devinfo_task(void); static void print_utf16(uint16_t *temp_buf, size_t buf_len); -void print_device_descriptor(tuh_xfer_t* xfer); +static void print_one_device(uint8_t daddr); void parse_config_descriptor(uint8_t dev_addr, tusb_desc_configuration_t const* desc_cfg); uint8_t* get_hid_buf(uint8_t daddr); void free_hid_buf(uint8_t daddr); +// One flag per possible device address — set in tuh_mount_cb (host task) and +// cleared by print_devinfo_task (main loop) once the descriptors are printed. +static volatile bool need_devinfo[CFG_TUH_DEVICE_MAX + 1]; + /*------------- MAIN -------------*/ int main(void) { board_init(); @@ -74,38 +79,53 @@ int main(void) { while (1) { // tinyusb host task tuh_task(); + print_devinfo_task(); led_blinking_task(); } } /*------------- TinyUSB Callbacks -------------*/ -// Invoked when device is mounted (configured) +// Invoked when device is mounted (configured). Runs in the host task — keep +// it minimal. The descriptor fetching/printing happens in print_devinfo_task() +// below where the sync helpers are safe (different context). void tuh_mount_cb(uint8_t daddr) { printf("Device attached, address = %d\r\n", daddr); - - // Get Device Descriptor - // TODO: invoking control transfer now has issue with mounting hub with multiple devices attached, fix later - tuh_descriptor_get_device(daddr, &desc_device, 18, print_device_descriptor, 0); + if (daddr < TU_ARRAY_SIZE(need_devinfo)) { + need_devinfo[daddr] = true; + } } /// Invoked when device is unmounted (bus reset/unplugged) void tuh_umount_cb(uint8_t daddr) { printf("Device removed, address = %d\r\n", daddr); + if (daddr < TU_ARRAY_SIZE(need_devinfo)) { + need_devinfo[daddr] = false; + } free_hid_buf(daddr); } //--------------------------------------------------------------------+ -// Device Descriptor +// Print device info task — serialises descriptor fetching across all +// mounted devices via sync helpers. Sync calls are safe here because this +// runs in the main loop (outside any host-stack callback context). //--------------------------------------------------------------------+ -void print_device_descriptor(tuh_xfer_t *xfer) { - if (XFER_RESULT_SUCCESS != xfer->result) { +void print_devinfo_task(void) { + for (uint8_t daddr = 1; daddr < TU_ARRAY_SIZE(need_devinfo); daddr++) { + if (need_devinfo[daddr]) { + need_devinfo[daddr] = false; + print_one_device(daddr); + } + } +} + +static void print_one_device(uint8_t daddr) { + // Get Device Descriptor + if (XFER_RESULT_SUCCESS != tuh_descriptor_get_device_sync(daddr, &desc_device, 18)) { printf("Failed to get device descriptor\r\n"); return; } - uint8_t const daddr = xfer->daddr; - printf("Device %u: ID %04x:%04x\r\n", daddr, desc_device.idVendor, desc_device.idProduct); printf("Device Descriptor:\r\n"); printf(" bLength %u\r\n" , desc_device.bLength); @@ -119,7 +139,6 @@ void print_device_descriptor(tuh_xfer_t *xfer) { printf(" idProduct 0x%04x\r\n" , desc_device.idProduct); printf(" bcdDevice %04x\r\n" , desc_device.bcdDevice); - // Get String descriptor using Sync API printf(" iManufacturer %u ", desc_device.iManufacturer); if (XFER_RESULT_SUCCESS == tuh_descriptor_get_manufacturer_string_sync(daddr, LANGUAGE_ID, temp_buf, sizeof(temp_buf))) { print_utf16(temp_buf, TU_ARRAY_SIZE(temp_buf)); diff --git a/examples/host/cdc_msc_hid/src/cdc_app.c b/examples/host/cdc_msc_hid/src/cdc_app.c index 20033981e..e6c190715 100644 --- a/examples/host/cdc_msc_hid/src/cdc_app.c +++ b/examples/host/cdc_msc_hid/src/cdc_app.c @@ -95,7 +95,6 @@ void tuh_cdc_mount_cb(uint8_t idx) { printf("CDC Interface is mounted: address = %u, itf_num = %u\r\n", itf_info.daddr, itf_info.desc.bInterfaceNumber); -#ifdef CFG_TUH_CDC_LINE_CODING_ON_ENUM // If CFG_TUH_CDC_LINE_CODING_ON_ENUM is defined, line coding will be set by tinyusb stack // while eneumerating new cdc device cdc_line_coding_t line_coding = {0}; @@ -103,11 +102,6 @@ void tuh_cdc_mount_cb(uint8_t idx) { printf(" Baudrate: %" PRIu32 ", Stop Bits : %u\r\n", line_coding.bit_rate, line_coding.stop_bits); printf(" Parity : %u, Data Width: %u\r\n", line_coding.parity, line_coding.data_bits); } -#else - // Set Line Coding upon mounted - cdc_line_coding_t new_line_coding = { 115200, CDC_LINE_CODING_STOP_BITS_1, CDC_LINE_CODING_PARITY_NONE, 8 }; - tuh_cdc_set_line_coding(idx, &new_line_coding, NULL, 0); -#endif } // Invoked when a device with CDC interface is unmounted diff --git a/examples/host/device_info/src/main.c b/examples/host/device_info/src/main.c index b0e38dd6b..f32ed1a3e 100644 --- a/examples/host/device_info/src/main.c +++ b/examples/host/device_info/src/main.c @@ -72,8 +72,14 @@ CFG_TUH_MEM_SECTION struct { } desc; void led_blinking_task(void* param); +void print_devinfo_task(void* param); static void print_utf16(uint16_t* temp_buf, size_t buf_len); +// One flag per possible device address — set by tuh_mount_cb (host task) and +// cleared by print_devinfo_task (separate task / main loop) once the device's +// descriptor info has been printed. +static volatile bool need_devinfo[CFG_TUH_DEVICE_MAX + 1]; + #if CFG_TUSB_OS == OPT_OS_FREERTOS void init_freertos_task(void); #endif @@ -103,6 +109,7 @@ int main(void) { init_tinyusb(); while (1) { tuh_task(); // tinyusb host task + print_devinfo_task(NULL); led_blinking_task(NULL); } #endif @@ -110,10 +117,34 @@ int main(void) { /*------------- TinyUSB Callbacks -------------*/ -// Invoked when device is mounted (configured) +// Invoked when device is mounted (configured). Runs in the host task — keep +// it minimal. The actual descriptor fetching/printing happens in +// print_devinfo_task() below, which runs in a different context (main loop +// on OS_NONE / dedicated task on RTOS) where the sync helpers are safe. void tuh_mount_cb(uint8_t daddr) { blink_interval_ms = BLINK_MOUNTED; + if (daddr < TU_ARRAY_SIZE(need_devinfo)) { + need_devinfo[daddr] = true; + } +} +// Invoked when device is unmounted (bus reset/unplugged) +void tuh_umount_cb(uint8_t daddr) { + blink_interval_ms = BLINK_NOT_MOUNTED; + if (daddr < TU_ARRAY_SIZE(need_devinfo)) { + need_devinfo[daddr] = false; + } + printf("Device removed, address = %d\r\n", daddr); +} + +//--------------------------------------------------------------------+ +// Print device info task — serialises descriptor fetching across all +// mounted devices using the sync helpers. Sync calls are safe here because +// this task runs outside the host-task callback context (main loop on +// OS_NONE / dedicated FreeRTOS task on RTOS). +//--------------------------------------------------------------------+ + +static void print_one_device(uint8_t daddr) { // Get Device Descriptor uint8_t xfer_result = tuh_descriptor_get_device_sync(daddr, &desc.device, 18); if (XFER_RESULT_SUCCESS != xfer_result) { @@ -129,9 +160,8 @@ void tuh_mount_cb(uint8_t daddr) { } if (XFER_RESULT_SUCCESS != xfer_result) { uint16_t* serial = (uint16_t*)(uintptr_t) desc.serial; - serial[0] = (uint16_t)((TUSB_DESC_STRING << 8) | (2 * 1 + 2)); - serial[1] = '0'; // simply 0 + serial[1] = '0'; serial[2] = 0; } print_utf16((uint16_t*)(uintptr_t) desc.serial, sizeof(desc.serial)/2); @@ -149,12 +179,9 @@ void tuh_mount_cb(uint8_t daddr) { printf(" idProduct 0x%04x\r\n", desc.device.idProduct); printf(" bcdDevice %04x\r\n", desc.device.bcdDevice); - // Get String descriptor using Sync API - printf(" iManufacturer %u ", desc.device.iManufacturer); if (desc.device.iManufacturer != 0) { - xfer_result = tuh_descriptor_get_manufacturer_string_sync(daddr, LANGUAGE_ID, desc.buf, sizeof(desc.buf)); - if (XFER_RESULT_SUCCESS == xfer_result) { + if (XFER_RESULT_SUCCESS == tuh_descriptor_get_manufacturer_string_sync(daddr, LANGUAGE_ID, desc.buf, sizeof(desc.buf))) { print_utf16((uint16_t*)(uintptr_t) desc.buf, sizeof(desc.buf)/2); } } @@ -162,22 +189,33 @@ void tuh_mount_cb(uint8_t daddr) { printf(" iProduct %u ", desc.device.iProduct); if (desc.device.iProduct != 0) { - xfer_result = tuh_descriptor_get_product_string_sync(daddr, LANGUAGE_ID, desc.buf, sizeof(desc.buf)); - if (XFER_RESULT_SUCCESS == xfer_result) { + if (XFER_RESULT_SUCCESS == tuh_descriptor_get_product_string_sync(daddr, LANGUAGE_ID, desc.buf, sizeof(desc.buf))) { print_utf16((uint16_t*)(uintptr_t) desc.buf, sizeof(desc.buf)/2); } } printf("\r\n"); printf(" iSerialNumber %u ", desc.device.iSerialNumber); - printf("%s\r\n", (char*)desc.serial); // serial is already to UTF-8 + printf("%s\r\n", (char*)desc.serial); // serial is already UTF-8 printf(" bNumConfigurations %u\r\n", desc.device.bNumConfigurations); } -// Invoked when device is unmounted (bus reset/unplugged) -void tuh_umount_cb(uint8_t daddr) { - blink_interval_ms = BLINK_NOT_MOUNTED; - printf("Device removed, address = %d\r\n", daddr); +void print_devinfo_task(void* param) { + (void) param; + +#if CFG_TUSB_OS == OPT_OS_FREERTOS + while (1) { +#endif + for (uint8_t daddr = 1; daddr < TU_ARRAY_SIZE(need_devinfo); daddr++) { + if (need_devinfo[daddr]) { + need_devinfo[daddr] = false; + print_one_device(daddr); + } + } +#if CFG_TUSB_OS == OPT_OS_FREERTOS + vTaskDelay(pdMS_TO_TICKS(10)); + } +#endif } //--------------------------------------------------------------------+ @@ -278,6 +316,9 @@ StaticTask_t blinky_taskdef; StackType_t usb_stack[USB_STACK_SIZE]; StaticTask_t usb_taskdef; + +StackType_t devinfo_stack[USB_STACK_SIZE]; +StaticTask_t devinfo_taskdef; #endif #ifdef ESP_PLATFORM @@ -299,9 +340,11 @@ void init_freertos_task(void) { #if configSUPPORT_STATIC_ALLOCATION xTaskCreateStatic(led_blinking_task, "blinky", BLINKY_STACK_SIZE, NULL, 1, blinky_stack, &blinky_taskdef); xTaskCreateStatic(usb_host_task, "usbh", USB_STACK_SIZE, NULL, configMAX_PRIORITIES-1, usb_stack, &usb_taskdef); + xTaskCreateStatic(print_devinfo_task, "devinfo", USB_STACK_SIZE, NULL, configMAX_PRIORITIES-2, devinfo_stack, &devinfo_taskdef); #else xTaskCreate(led_blinking_task, "blinky", BLINKY_STACK_SIZE, NULL, 1, NULL); xTaskCreate(usb_host_task, "usbh", USB_STACK_SIZE, NULL, configMAX_PRIORITIES - 1, NULL); + xTaskCreate(print_devinfo_task, "devinfo", USB_STACK_SIZE, NULL, configMAX_PRIORITIES - 2, NULL); #endif // only start scheduler for non-espressif mcu -- cgit v1.3.1 From c5676382c7d5cad7b6cfe5dc185d9891b89241f2 Mon Sep 17 00:00:00 2001 From: hathach Date: Thu, 28 May 2026 18:23:46 +0700 Subject: abstract OS logic with `CFG_TUSB_OS_HAS_SCHEDULER` to simplify conditional checks --- examples/device/msc_dual_lun/src/main.c | 2 +- examples/dual/host_info_to_device_cdc/src/main.c | 6 +++--- src/tusb_option.h | 12 ++++++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/examples/device/msc_dual_lun/src/main.c b/examples/device/msc_dual_lun/src/main.c index a4ade6f9b..1d764f12c 100644 --- a/examples/device/msc_dual_lun/src/main.c +++ b/examples/device/msc_dual_lun/src/main.c @@ -71,7 +71,7 @@ static void usb_device_init(void) { board_init_after_tusb(); } -#if CFG_TUSB_OS != OPT_OS_NONE && CFG_TUSB_OS != OPT_OS_PICO +#if CFG_TUSB_OS_HAS_SCHEDULER static void usb_device_task(RTOS_PARAM param) { (void) param; usb_device_init(); diff --git a/examples/dual/host_info_to_device_cdc/src/main.c b/examples/dual/host_info_to_device_cdc/src/main.c index cf3430464..5186f91dc 100644 --- a/examples/dual/host_info_to_device_cdc/src/main.c +++ b/examples/dual/host_info_to_device_cdc/src/main.c @@ -130,7 +130,7 @@ static void main_task(void* param) { led_blinking_task(); // preempted RTOS run device/host stack in its own task -#if CFG_TUSB_OS == OPT_OS_NONE || CFG_TUSB_OS == OPT_OS_PICO +#if CFG_TUSB_OS_HAS_SCHEDULER == 0 tud_task(); // tinyusb device task tuh_task(); // tinyusb host task #endif @@ -140,7 +140,7 @@ static void main_task(void* param) { int main(void) { board_init(); -#if CFG_TUSB_OS == OPT_OS_NONE || CFG_TUSB_OS == OPT_OS_PICO +#if CFG_TUSB_OS_HAS_SCHEDULER == 0 printf("TinyUSB Host Information -> Device CDC Example\r\n"); usb_device_init(); @@ -156,7 +156,7 @@ int main(void) { return 0; } -#if CFG_TUSB_OS != OPT_OS_NONE && CFG_TUSB_OS != OPT_OS_PICO +#if CFG_TUSB_OS_HAS_SCHEDULER // USB Device Driver task for RTOS static void usb_device_task(void *param) { (void) param; diff --git a/src/tusb_option.h b/src/tusb_option.h index 74eb8cc06..dcf0646cf 100644 --- a/src/tusb_option.h +++ b/src/tusb_option.h @@ -535,6 +535,18 @@ #define CFG_TUSB_OS OPT_OS_NONE #endif +// 1 when CFG_TUSB_OS provides a preemptive scheduler with distinct tasks +// (FreeRTOS, Zephyr, ThreadX, etc.); 0 when the application is single-context +// (bare-metal OS_NONE or Pico SDK). Sync host control xfers from the host +// task are forbidden when this is 1. +#ifndef CFG_TUSB_OS_HAS_SCHEDULER + #if CFG_TUSB_OS == OPT_OS_NONE || CFG_TUSB_OS == OPT_OS_PICO + #define CFG_TUSB_OS_HAS_SCHEDULER 0 + #else + #define CFG_TUSB_OS_HAS_SCHEDULER 1 + #endif +#endif + #ifndef CFG_TUSB_OS_INC_PATH #ifndef CFG_TUSB_OS_INC_PATH_DEFAULT #define CFG_TUSB_OS_INC_PATH_DEFAULT -- cgit v1.3.1 From dafdc5c54f34f88c963f7efc0062d0e992074b8f Mon Sep 17 00:00:00 2001 From: HiFiPhile Date: Fri, 29 May 2026 00:05:44 +0200 Subject: dwc2: move OUT transfer management into RXFLVL IRQ - GRXSTSP register has internal FIFO, receiving events won't mix up (STATUS OUT & next SETUP) - Improve efficiency, remove 2nd IRQ overhead Signed-off-by: HiFiPhile --- src/portable/synopsys/dwc2/dcd_dwc2.c | 71 ++++++++++++++--------------------- 1 file changed, 28 insertions(+), 43 deletions(-) diff --git a/src/portable/synopsys/dwc2/dcd_dwc2.c b/src/portable/synopsys/dwc2/dcd_dwc2.c index 1c119eef4..233840e8b 100644 --- a/src/portable/synopsys/dwc2/dcd_dwc2.c +++ b/src/portable/synopsys/dwc2/dcd_dwc2.c @@ -794,13 +794,15 @@ static void handle_bus_reset(uint8_t rhport) { xfer_status[0][TUSB_DIR_OUT].max_size = CFG_TUD_ENDPOINT0_SIZE; xfer_status[0][TUSB_DIR_IN].max_size = CFG_TUD_ENDPOINT0_SIZE; + uint32_t oepmsk = 0; if(dma_device_enabled(dwc2)) { + oepmsk = GINTMSK_OEPINT; dma_setup_prepare(rhport); } else { dwc2->epout[0].doeptsiz |= (3 << DOEPTSIZ_STUPCNT_Pos); } - dwc2->gintmsk |= GINTMSK_OTGINT | GINTMSK_OEPINT | GINTMSK_IEPINT | GINTMSK_IISOIXFRM; + dwc2->gintmsk |= GINTMSK_OTGINT | oepmsk | GINTMSK_IEPINT | GINTMSK_IISOIXFRM; } static void handle_enum_done(uint8_t rhport) { @@ -901,13 +903,27 @@ static void handle_rxflvl_irq(uint8_t rhport) { // We can receive up to three setup packets in succession, but only the last one is valid. setup[0] = (*rx_fifo); setup[1] = (*rx_fifo); + + dwc2_dep_t* epin0 = &dwc2->epin[0]; + if (edpt_is_enabled(epin0)) { + edpt_disable(rhport, 0x80, false); + } + + // (GenID < 3.00a) Must wait SETUP_DONE before next OUT transfer, otherwise OUT data may be corrupted. + // (GenID >= 3.00a) On the other hand STUPCNT is auto reloaded and SETUP_DONE is only triggered once after bus reset. + if (dwc2->gsnpsid >= DWC2_CORE_REV_3_00a) { + dcd_event_setup_received(rhport, _dcd_usbbuf.setup_buffer, true); + } break; } case GRXSTS_PKTSTS_SETUP_DONE: // Setup packet done: - // After popping this out, dwc2 asserts a DOEPINT_SETUP interrupt which is handled by handle_epout_irq() epout->doeptsiz |= (3 << DOEPTSIZ_STUPCNT_Pos); + + if (dwc2->gsnpsid < DWC2_CORE_REV_3_00a) { + dcd_event_setup_received(rhport, _dcd_usbbuf.setup_buffer, true); + } break; case GRXSTS_PKTSTS_RX_DATA: { @@ -931,55 +947,24 @@ static void handle_rxflvl_irq(uint8_t rhport) { xfer->total_len -= tsiz.xfer_size; if (epnum == 0) { _dcd_data.ep0_pending[TUSB_DIR_OUT] = 0; - // Handle EP0 STATUS OUT (or ZLP) here to avoid mix with next SETUP packet received IRQ - if (xfer->total_len == 0) { - dcd_event_xfer_complete(rhport, 0, 0, XFER_RESULT_SUCCESS, true); - } } } break; } - case GRXSTS_PKTSTS_RX_COMPLETE: + case GRXSTS_PKTSTS_RX_COMPLETE: { // Out packet done - // After this entry is popped from the receive FIFO, dwc2 asserts a Transfer Completed interrupt on - // the specified OUT endpoint which will be handled by handle_epout_irq() - break; - - default: break; // nothing to do - } -} - -static void handle_epout_slave(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doepint_bm) { - if (doepint_bm.setup_phase_done) { - // Cleanup previous pending EP0 IN transfer if any - dwc2_dep_t* epin0 = &DWC2_REG(rhport)->epin[0]; - if (edpt_is_enabled(epin0)) { - edpt_disable(rhport, 0x80, false); - } - dcd_event_setup_received(rhport, _dcd_usbbuf.setup_buffer, true); - return; - } - - // Normal OUT transfer complete - if (doepint_bm.xfer_complete) { - // only handle data skip if it is setup or status related - // Note: even though (xfer_complete + status_phase_rx) is for buffered DMA only, for STM32L47x (dwc2 v3.00a) they - // can is set when GRXSTS_PKTSTS_SETUP_RX is popped therefore they can bet set before/together with setup_phase_done - if (!doepint_bm.status_phase_rx && !doepint_bm.setup_packet_rx) { xfer_ctl_t* xfer = XFER_CTL_BASE(epnum, TUSB_DIR_OUT); - if (epnum == 0) { - if (_dcd_data.ep0_pending[TUSB_DIR_OUT]) { - // EP0 can only handle one packet, Schedule another packet to be received. - edpt_schedule_packets(rhport, epnum, TUSB_DIR_OUT); - } else if (xfer->total_len > 0) { - // EP0 STATUS OUT (or ZLP) is handled in handle_rxflvl_irq() - dcd_event_xfer_complete(rhport, epnum, xfer->total_len, XFER_RESULT_SUCCESS, true); - } + if (epnum == 0 && _dcd_data.ep0_pending[TUSB_DIR_OUT] > 0) { + // EP0 can only handle one packet, schedule another packet to be received. + edpt_schedule_packets(rhport, 0, TUSB_DIR_OUT); } else { dcd_event_xfer_complete(rhport, epnum, xfer->total_len, XFER_RESULT_SUCCESS, true); } + break; } + + default: break; // nothing to do } } @@ -1108,8 +1093,6 @@ static void handle_ep_irq(uint8_t rhport, uint8_t dir) { #if CFG_TUD_DWC2_SLAVE_ENABLE if (dir == TUSB_DIR_IN) { handle_epin_slave(rhport, epnum, intr.diepint_bm); - } else { - handle_epout_slave(rhport, epnum, intr.doepint_bm); } #endif } @@ -1217,7 +1200,7 @@ void dcd_int_handler(uint8_t rhport) { dwc2->gotgint = otg_int; } - if(gintsts & GINTSTS_SOF) { + if(gintsts & GINTSTS_SOF && dwc2->gintmsk & GINTMSK_SOFM) { dwc2->gintsts = GINTSTS_SOF; dwc2->gintmsk |= GINTMSK_USBSUSPM; const uint32_t frame = (dwc2->dsts & DSTS_FNSOF) >> DSTS_FNSOF_Pos; @@ -1244,11 +1227,13 @@ void dcd_int_handler(uint8_t rhport) { } #endif +#if CFG_TUD_DWC2_DMA_ENABLE // OUT endpoint interrupt handling. if (gintsts & GINTSTS_OEPINT) { // OEPINT is read-only, clear using DOEPINTn handle_ep_irq(rhport, TUSB_DIR_OUT); } +#endif // IN endpoint interrupt handling. if (gintsts & GINTSTS_IEPINT) { -- cgit v1.3.1 From d85ddd2f8a93f2bfcedfb0365fa6826f1ab86fca Mon Sep 17 00:00:00 2001 From: HiFiPhile Date: Fri, 29 May 2026 00:39:19 +0200 Subject: bsp/stm32f7: fix f746disco jlink device Signed-off-by: HiFiPhile --- hw/bsp/stm32f7/boards/stm32f746disco/board.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hw/bsp/stm32f7/boards/stm32f746disco/board.cmake b/hw/bsp/stm32f7/boards/stm32f746disco/board.cmake index bc26c6ef4..5f46365f4 100644 --- a/hw/bsp/stm32f7/boards/stm32f746disco/board.cmake +++ b/hw/bsp/stm32f7/boards/stm32f746disco/board.cmake @@ -1,5 +1,5 @@ set(MCU_VARIANT stm32f746xx) -set(JLINK_DEVICE stm32f746xx) +set(JLINK_DEVICE stm32f746ng) set(LD_FILE_GNU ${CMAKE_CURRENT_LIST_DIR}/STM32F746ZGTx_FLASH.ld) -- cgit v1.3.1 From 33f151a43b39de4207bcdc49ea1e9e8dd8a5d248 Mon Sep 17 00:00:00 2001 From: HiFiPhile Date: Wed, 27 May 2026 20:44:04 +0200 Subject: bsp/stm32f4: enable flash cache Signed-off-by: HiFiPhile --- hw/bsp/stm32f4/family.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hw/bsp/stm32f4/family.c b/hw/bsp/stm32f4/family.c index 4eea5c7a8..3a323e673 100644 --- a/hw/bsp/stm32f4/family.c +++ b/hw/bsp/stm32f4/family.c @@ -109,6 +109,11 @@ void USARTn_IRQHandler(void) { void board_init(void) { board_clock_init(); + + __HAL_FLASH_INSTRUCTION_CACHE_ENABLE(); + __HAL_FLASH_DATA_CACHE_ENABLE(); + __HAL_FLASH_PREFETCH_BUFFER_ENABLE(); + //SystemCoreClockUpdate(); // Enable All GPIOs clocks -- cgit v1.3.1 From d754c0697cbd29874c56254bd97561349fb9a229 Mon Sep 17 00:00:00 2001 From: hathach Date: Fri, 29 May 2026 16:16:21 +0700 Subject: Implement asynchronous control transfer queuing for USB host stack - Added a pending FIFO queue for asynchronous control transfers when the active slot is busy. - Introduced `control_xfer_dispatch_pending` to handle queued transfers on slot availability. - Improved synchronization for blocking and non-blocking transfer modes, preventing deadlocks in RTOS. - Refactored and renamed related functions for clarity and consistency. - Enhanced error handling and callback invocation for failed or stale transfers. --- src/common/tusb_types.h | 1 + src/host/usbh.c | 332 ++++++++++++++++++++++++++++++++++++----------- src/osal/osal_freertos.h | 8 +- 3 files changed, 263 insertions(+), 78 deletions(-) diff --git a/src/common/tusb_types.h b/src/common/tusb_types.h index 959fc129a..cb06b89bb 100644 --- a/src/common/tusb_types.h +++ b/src/common/tusb_types.h @@ -282,6 +282,7 @@ typedef enum { XFER_RESULT_FAILED, XFER_RESULT_STALLED, XFER_RESULT_TIMEOUT, + XFER_RESULT_ABORTED, XFER_RESULT_INVALID } xfer_result_t; diff --git a/src/host/usbh.c b/src/host/usbh.c index 2e3c93c5e..05e03245f 100644 --- a/src/host/usbh.c +++ b/src/host/usbh.c @@ -40,6 +40,10 @@ #define CFG_TUH_TASK_QUEUE_SZ 16 #endif +#ifndef CFG_TUH_CONTROL_PENDING_QUEUE_SZ + #define CFG_TUH_CONTROL_PENDING_QUEUE_SZ 4 +#endif + #ifndef CFG_TUH_INTERFACE_MAX #define CFG_TUH_INTERFACE_MAX 8 #endif @@ -175,11 +179,11 @@ static OSAL_SPINLOCK_DEF(_usbh_spin, usbh_int_set); OSAL_QUEUE_DEF(usbh_int_set, _usbh_qdef, CFG_TUH_TASK_QUEUE_SZ, hcd_event_t); static osal_queue_t _usbh_q; - #if CFG_TUH_HUB +#if CFG_TUH_HUB // Deferred attachment queue, only needed when using hub OSAL_QUEUE_DEF(usbh_int_set, _usbh_daqdef, CFG_TUH_HUB, hcd_event_t); static osal_queue_t _usbh_daq; - #endif +#endif // Control transfers: since most controllers do not support multiple control transfers // on multiple devices concurrently and control transfers are not used much except for @@ -189,9 +193,9 @@ typedef struct { tuh_xfer_cb_t complete_cb; uintptr_t user_data; + volatile uint16_t actual_len; volatile uint8_t stage; uint8_t daddr; - volatile uint16_t actual_len; uint8_t failed_count; } usbh_ctrl_xfer_info_t; @@ -202,17 +206,32 @@ typedef struct { } usbh_call_after_t; typedef struct { - uint8_t controller_id; // controller ID + tusb_control_request_t setup; + uint8_t* buffer; + tuh_xfer_cb_t complete_cb; + uintptr_t user_data; + uint8_t daddr; + uint8_t daddr_gen; +} usbh_pending_ctrl_t; + +// FIFO for pending async control transfers since we only execute 1 control transfer at a time +TU_FIFO_DEF(_usbh_pending_ctrl_q, CFG_TUH_CONTROL_PENDING_QUEUE_SZ * sizeof(usbh_pending_ctrl_t), false); + +typedef struct { uint8_t enumerating_daddr; // device address of the device being enumerated uint8_t attach_debouncing_bm; // bitmask for roothub port attach debouncing tuh_bus_info_t dev0_bus; // bus info for dev0 in enumeration usbh_ctrl_xfer_info_t ctrl_xfer_info; // control transfer usbh_call_after_t call_after; + // Per-daddr generation counter — bumped on usbh_device_close() to identify stale pending control transfer + uint8_t daddr_gen[TOTAL_DEVICES + 1]; +#if CFG_TUSB_OS_HAS_SCHEDULER + osal_task_handle_t task_hdl; // host task handle, lazy-captured on first tuh_task_ext() +#endif } usbh_data_t; -static usbh_data_t _usbh_data = { - .controller_id = TUSB_INDEX_INVALID_8, -}; +static uint8_t _usbh_controller_id = TUSB_INDEX_INVALID_8; +static usbh_data_t _usbh_data; typedef struct { TUH_EPBUF_TYPE_DEF(tusb_control_request_t, request); @@ -346,8 +365,11 @@ static void enum_new_device(hcd_event_t* event); static void enum_delay_async(uintptr_t state); static void process_remove_event(hcd_event_t *event); static void remove_device_tree(uint8_t rhport, uint8_t hub_addr, uint8_t hub_port); + static bool usbh_edpt_control_open(uint8_t dev_addr, uint8_t max_packet_size); static bool usbh_control_xfer_cb (uint8_t daddr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes); +static void control_xfer_dispatch_pending(void); +static void control_xfer_complete(uint8_t daddr, xfer_result_t result); TU_ATTR_ALWAYS_INLINE static inline usbh_device_t* get_device(uint8_t dev_addr) { TU_VERIFY(dev_addr > 0 && dev_addr <= TOTAL_DEVICES, NULL); @@ -364,7 +386,7 @@ TU_ATTR_ALWAYS_INLINE static inline bool queue_event(hcd_event_t const * event, return true; } -TU_ATTR_ALWAYS_INLINE static inline void _control_set_xfer_stage(uint8_t stage) { +TU_ATTR_ALWAYS_INLINE static inline void control_xfer_set_stage(uint8_t stage) { if (_usbh_data.ctrl_xfer_info.stage != stage) { (void) osal_mutex_lock(_usbh_mutex, OSAL_TIMEOUT_WAIT_FOREVER); _usbh_data.ctrl_xfer_info.stage = stage; @@ -372,15 +394,6 @@ TU_ATTR_ALWAYS_INLINE static inline void _control_set_xfer_stage(uint8_t stage) } } -TU_ATTR_ALWAYS_INLINE static inline bool usbh_setup_send(uint8_t daddr, const uint8_t setup_packet[8]) { - const uint8_t rhport = usbh_get_rhport(daddr); - const bool ret = hcd_setup_send(rhport, daddr, setup_packet); - if (!ret) { - _control_set_xfer_stage(CONTROL_STAGE_IDLE); - } - return ret; -} - bool usbh_defer_func_ms_async(uint32_t ms, tusb_defer_func_t func, uintptr_t param) { TU_ASSERT(_usbh_data.call_after.func == NULL); TU_LOG_USBH("USBH schedule function after %u ms\r\n", (unsigned int)ms); @@ -394,9 +407,16 @@ bool usbh_defer_func_ms_async(uint32_t ms, tusb_defer_func_t func, uintptr_t par TU_ATTR_ALWAYS_INLINE static inline void usbh_device_close(uint8_t rhport, uint8_t daddr) { hcd_device_close(rhport, daddr); - // abort any ongoing control transfer - if (daddr == _usbh_data.ctrl_xfer_info.daddr) { - _control_set_xfer_stage(CONTROL_STAGE_IDLE); + // Bump the generation under the mutex so a concurrent producer in + // tuh_control_xfer stamps a value that is strictly monotonic w.r.t. close. + (void) osal_mutex_lock(_usbh_mutex, OSAL_TIMEOUT_WAIT_FOREVER); + _usbh_data.daddr_gen[daddr]++; + (void) osal_mutex_unlock(_usbh_mutex); + + // If this device has in-flight control xfer, complete as FAILED + usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info; + if (daddr == ctrl_info->daddr && ctrl_info->stage != CONTROL_STAGE_IDLE) { + control_xfer_complete(daddr, XFER_RESULT_FAILED); } // invalidate if enumerating @@ -458,7 +478,7 @@ tusb_speed_t tuh_speed_get(uint8_t daddr) { } bool tuh_rhport_is_active(uint8_t rhport) { - return _usbh_data.controller_id == rhport; + return _usbh_controller_id == rhport; } bool tuh_rhport_reset_bus(uint8_t rhport, bool active) { @@ -485,7 +505,7 @@ static void clear_device(usbh_device_t* dev) { } bool tuh_inited(void) { - return _usbh_data.controller_id != TUSB_INDEX_INVALID_8; + return _usbh_controller_id != TUSB_INDEX_INVALID_8; } bool tuh_rhport_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) { @@ -547,7 +567,7 @@ bool tuh_rhport_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) { tu_memclr(_usbh_devices, sizeof(_usbh_devices)); tu_memclr(&_usbh_data, sizeof(_usbh_data)); - _usbh_data.controller_id = TUSB_INDEX_INVALID_8; + _usbh_controller_id = TUSB_INDEX_INVALID_8; _usbh_data.enumerating_daddr = TUSB_INDEX_INVALID_8; for (uint8_t i = 0; i < TOTAL_DEVICES; i++) { @@ -565,7 +585,7 @@ bool tuh_rhport_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) { } // Init host controller - _usbh_data.controller_id = rhport; + _usbh_controller_id = rhport; TU_ASSERT(hcd_init(rhport, rh_init)); hcd_int_enable(rhport); @@ -580,7 +600,7 @@ bool tuh_deinit(uint8_t rhport) { // deinit host controller hcd_int_disable(rhport); TU_ASSERT(hcd_deinit(rhport)); - _usbh_data.controller_id = TUSB_INDEX_INVALID_8; + _usbh_controller_id = TUSB_INDEX_INVALID_8; // remove all devices on this rhport (hub_addr = 0, hub_port = 0) remove_device_tree(rhport, 0, 0); @@ -604,6 +624,25 @@ bool tuh_deinit(uint8_t rhport) { _usbh_daq = NULL; #endif + // Fire FAILED cb for any queued async control xfer so callers aren't stranded. + usbh_pending_ctrl_t pending; + while (tu_fifo_read_n(&_usbh_pending_ctrl_q, &pending, sizeof(pending)) == sizeof(pending)) { + if (pending.complete_cb) { + tuh_xfer_t x = { + .daddr = pending.daddr, + .ep_addr = 0, + .result = XFER_RESULT_FAILED, + .actual_len = 0, + .setup = &pending.setup, + .buffer = pending.buffer, + .complete_cb = pending.complete_cb, + .user_data = pending.user_data, + }; + pending.complete_cb(&x); + } + } + tu_fifo_clear(&_usbh_pending_ctrl_q); + #if OSAL_MUTEX_REQUIRED // TODO make sure there is no task waiting on this mutex osal_mutex_delete(_usbh_mutex); @@ -629,6 +668,12 @@ bool tuh_task_event_ready(void) { } #endif + // Pending control xfer waiting for an idle slot + if (_usbh_data.ctrl_xfer_info.stage == CONTROL_STAGE_IDLE && + !tu_fifo_empty(&_usbh_pending_ctrl_q)) { + return true; + } + if (_usbh_data.call_after.func) { int32_t remain_ms = (int32_t)(_usbh_data.call_after.at_ms - tusb_time_millis_api()); if (remain_ms <= 0) { @@ -663,6 +708,13 @@ void tuh_task_ext(uint32_t timeout_ms, bool in_isr) { (void) in_isr; // not implemented yet +#if CFG_TUSB_OS_HAS_SCHEDULER + // Save task handle on 1st run + if (_usbh_data.task_hdl == NULL) { + _usbh_data.task_hdl = osal_task_get_current_handle(); + } +#endif + // Loop until there are no more events in the queue or CFG_TUH_TASK_EVENTS_PER_RUN is reached for (unsigned epr = 0;; epr++) { #if CFG_TUH_TASK_EVENTS_PER_RUN > 0 @@ -695,6 +747,16 @@ void tuh_task_ext(uint32_t timeout_ms, bool in_isr) { } } + // Drain pending async control xfers. Slot transitions and dispatch are + // decoupled: completion / abort / device_close set stage = IDLE via + // control_xfer_set_stage() and the actual FIFO drain happens here in the + // event loop. The check is a fast non-mutex sanity gate; the dispatcher + // itself re-checks under the mutex. + if (_usbh_data.ctrl_xfer_info.stage == CONTROL_STAGE_IDLE && + !tu_fifo_empty(&_usbh_pending_ctrl_q)) { + control_xfer_dispatch_pending(); + } + hcd_event_t event; #if CFG_TUH_HUB @@ -818,73 +880,179 @@ void tuh_task_ext(uint32_t timeout_ms, bool in_isr) { // Control transfer //--------------------------------------------------------------------+ -static void _control_blocking_complete_cb(tuh_xfer_t* xfer) { - // update result - *((xfer_result_t*) xfer->user_data) = xfer->result; +// Carries both fields the sync waiter cares about — capturing from xfer_temp +// (snapshot taken before release_slot resets ctrl_info for the next pending +// entry) so the waiter sees this xfer's data, not the next dispatched one's. +typedef struct { + volatile xfer_result_t result; + volatile uint32_t actual_len; +} control_xfer_sync_param_t; + +static void control_xfer_sync_complete(tuh_xfer_t* xfer) { + control_xfer_sync_param_t* s = (control_xfer_sync_param_t*) xfer->user_data; + s->actual_len = xfer->actual_len; + s->result = xfer->result; } // TODO timeout_ms is not supported yet bool tuh_control_xfer (tuh_xfer_t* xfer) { - TU_VERIFY(xfer->ep_addr == 0 && xfer->setup); // EP0 with setup packet const uint8_t daddr = xfer->daddr; - TU_VERIFY(tuh_connected(daddr)); - + TU_VERIFY(daddr <= TOTAL_DEVICES && xfer->ep_addr == 0 && xfer->setup); // EP0 with setup packet usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info; - TU_VERIFY(ctrl_info->stage == CONTROL_STAGE_IDLE); // pre-check to help reducing mutex lock - (void) osal_mutex_lock(_usbh_mutex, OSAL_TIMEOUT_WAIT_FOREVER); - bool const is_idle = (ctrl_info->stage == CONTROL_STAGE_IDLE); - if (is_idle) { - ctrl_info->stage = CONTROL_STAGE_SETUP; - ctrl_info->daddr = daddr; - ctrl_info->actual_len = 0; - ctrl_info->failed_count = 0; - - ctrl_info->buffer = xfer->buffer; - ctrl_info->complete_cb = xfer->complete_cb; - ctrl_info->user_data = xfer->user_data; - _usbh_epbuf.request = (*xfer->setup); - } - (void) osal_mutex_unlock(_usbh_mutex); +#if CFG_TUSB_OS_HAS_SCHEDULER + // Sync (complete_cb == NULL) from a host-stack callback is forbidden on + // RTOS targets — the event-loop driver can't block on its own pending xfer + // (deadlock if other control xfers are queued behind). Use async with a + // chained cb instead. OS_NONE / OS_PICO are exempt: they have a single + // execution context and the recursive-drive path is the only way to wait. + TU_ASSERT(!(xfer->complete_cb == NULL && + osal_task_get_current_handle() == _usbh_data.task_hdl)); +#endif - TU_VERIFY(is_idle); + // Slot is single-threaded — when busy, sync callers block until it frees + // (blocking semantics require the result); async callers get queued in the + // pending FIFO and submitted by control_xfer_complete() when the slot + // drains. The test-and-{claim|enqueue} is one critical section so a slot + // that becomes IDLE between the check and the enqueue can't strand an async + // request in a queue nothing else drains. + const bool is_nonblocking = (xfer->complete_cb != NULL); + while (true) { + TU_VERIFY(tuh_connected(daddr)); + bool claimed = false; + bool is_queued = false; + (void) osal_mutex_lock(_usbh_mutex, OSAL_TIMEOUT_WAIT_FOREVER); + if (ctrl_info->stage == CONTROL_STAGE_IDLE) { + ctrl_info->stage = CONTROL_STAGE_SETUP; + ctrl_info->daddr = daddr; + ctrl_info->actual_len = 0; + ctrl_info->failed_count = 0; + + ctrl_info->buffer = xfer->buffer; + ctrl_info->complete_cb = xfer->complete_cb; + ctrl_info->user_data = xfer->user_data; + _usbh_epbuf.request = (*xfer->setup); + claimed = true; + } else if (is_nonblocking) { + // Async + busy: queue the transfer. + const usbh_pending_ctrl_t entry = { + .setup = *xfer->setup, + .buffer = xfer->buffer, + .complete_cb = xfer->complete_cb, + .user_data = xfer->user_data, + .daddr = daddr, + .daddr_gen = _usbh_data.daddr_gen[daddr] + }; + is_queued = tu_fifo_write_n(&_usbh_pending_ctrl_q, &entry, sizeof(entry)) == sizeof(entry); + } + + (void) osal_mutex_unlock(_usbh_mutex); + + if (claimed) { + break; + } + + if (is_nonblocking) { + return is_queued; + } + + // - OS_HAS_SCHEDULER: delay 1 ms + // - Otherwise: single execution context; drive the loop ourselves to progress the in-flight transfer. +#if CFG_TUSB_OS_HAS_SCHEDULER + osal_task_delay(1); +#else + tuh_task_ext(0, false); +#endif + } TU_LOG_USBH("[%u:%u] %s: ", usbh_get_rhport(daddr), daddr, (xfer->setup->bmRequestType_bit.type == TUSB_REQ_TYPE_STANDARD && xfer->setup->bRequest <= TUSB_REQ_SYNCH_FRAME) ? tu_str_std_request[xfer->setup->bRequest] : "Class Request"); TU_LOG_BUF_USBH(xfer->setup, 8); - if (xfer->complete_cb != NULL) { - TU_ASSERT(usbh_setup_send(daddr, (uint8_t const *) &_usbh_epbuf.request)); - }else { - // blocking if complete callback is not provided - // change callback to internal blocking, and result as user argument - volatile xfer_result_t result = XFER_RESULT_INVALID; - - // use user_data to point to xfer_result_t - ctrl_info->user_data = (uintptr_t) &result; - ctrl_info->complete_cb = _control_blocking_complete_cb; + // Sync: wire control_xfer_sync_complete BEFORE submit so a fast completion + // event has the cb in place. control_xfer_complete() captures both result + // and actual_len through this cb before release_slot overwrites ctrl_info. + volatile control_xfer_sync_param_t sync_state; + if (!is_nonblocking) { + sync_state.result = XFER_RESULT_INVALID; + sync_state.actual_len = 0; + ctrl_info->user_data = (uintptr_t) &sync_state; + ctrl_info->complete_cb = control_xfer_sync_complete; + } - TU_ASSERT(usbh_setup_send(daddr, (uint8_t const *) &_usbh_epbuf.request)); + if (!hcd_setup_send(usbh_get_rhport(daddr), daddr, (uint8_t const *) &_usbh_epbuf.request)) { + control_xfer_set_stage(CONTROL_STAGE_IDLE); + return false; + } - while (result == XFER_RESULT_INVALID) { - // Note: this can be called within an callback ie. part of tuh_task() - // therefore even with RTOS tuh_task_ext() still need to be invoked + if (!is_nonblocking) { + // No tuh_connected() escape needed: usbh_device_close() routes through + // control_xfer_complete(daddr, FAILED) on disconnect, which fires + // sync_complete and unblocks this poll. + while (sync_state.result == XFER_RESULT_INVALID) { +#if CFG_TUSB_OS_HAS_SCHEDULER + osal_task_delay(1); +#else tuh_task_ext(0, false); - // TODO probably some timeout to prevent hanged +#endif } - // update transfer result, user_data is expected to point to xfer_result_t + // Forward to caller (xfer->user_data, if set, is a xfer_result_t pointer). if (xfer->user_data != 0) { - *((xfer_result_t*) xfer->user_data) = result; + *((xfer_result_t*) xfer->user_data) = sync_state.result; } - xfer->result = result; - xfer->actual_len = ctrl_info->actual_len; + xfer->result = sync_state.result; + xfer->actual_len = sync_state.actual_len; } return true; } -static void _control_xfer_complete(uint8_t daddr, xfer_result_t result) { +// Start control transfer from pending fifo +static void control_xfer_dispatch_pending(void) { + usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info; + + while (true) { + usbh_pending_ctrl_t xfer; + bool has_xfer = false; + + (void) osal_mutex_lock(_usbh_mutex, OSAL_TIMEOUT_WAIT_FOREVER); + if (ctrl_info->stage == CONTROL_STAGE_IDLE && + tu_fifo_read_n(&_usbh_pending_ctrl_q, &xfer, sizeof(xfer)) == sizeof(xfer)) { + ctrl_info->stage = CONTROL_STAGE_SETUP; + ctrl_info->daddr = xfer.daddr; + ctrl_info->actual_len = 0; + ctrl_info->failed_count = 0; + ctrl_info->buffer = xfer.buffer; + ctrl_info->complete_cb = xfer.complete_cb; + ctrl_info->user_data = xfer.user_data; + _usbh_epbuf.request = xfer.setup; + has_xfer = true; + } + (void) osal_mutex_unlock(_usbh_mutex); + + if (!has_xfer) { + return; // nothing to do + } + + // mismatched daddr_gen means pending transfer is stale due to the device got disconnected while in the FIFO + // Note: the address can be re-allocated to another device at this point. + if (xfer.daddr_gen == _usbh_data.daddr_gen[xfer.daddr]) { + TU_LOG_USBH("[%u:%u] %s: ", usbh_get_rhport(xfer.daddr), xfer.daddr, + (xfer.setup.bmRequestType_bit.type == TUSB_REQ_TYPE_STANDARD && xfer.setup.bRequest <= TUSB_REQ_SYNCH_FRAME) ? + tu_str_std_request[xfer.setup.bRequest] : "Class Request"); + TU_LOG_BUF_USBH(&xfer.setup, 8); + if (hcd_setup_send(usbh_get_rhport(xfer.daddr), xfer.daddr, (uint8_t const *) &_usbh_epbuf.request)) { + return; // transfer kicked-off, we are done + } + } + + // complete callback as FAILED and continue with next pending xfer + control_xfer_complete(xfer.daddr, XFER_RESULT_FAILED); + } +} + +static void control_xfer_complete(uint8_t daddr, xfer_result_t result) { TU_LOG_USBH("\r\n"); usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info; @@ -901,7 +1069,8 @@ static void _control_xfer_complete(uint8_t daddr, xfer_result_t result) { .user_data = ctrl_info->user_data }; - _control_set_xfer_stage(CONTROL_STAGE_IDLE); + // set to IDLE before callback since cb can invoke another transfer + control_xfer_set_stage(CONTROL_STAGE_IDLE); if (xfer_temp.complete_cb != NULL) { xfer_temp.complete_cb(&xfer_temp); @@ -915,11 +1084,17 @@ static bool usbh_control_xfer_cb (uint8_t daddr, uint8_t ep_addr, xfer_result_t tusb_control_request_t const * request = &_usbh_epbuf.request; usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info; + // Drop stale completions: slot already released (abort/close fired its cb) + // or now owns a different device's xfer (a pending entry was dispatched). + if (ctrl_info->stage == CONTROL_STAGE_IDLE || ctrl_info->daddr != daddr) { + return true; + } + switch (result) { case XFER_RESULT_STALLED: TU_LOG_USBH("[%u:%u] Control STALLED, xferred_bytes = %" PRIu32 "\r\n", rhport, daddr, xferred_bytes); TU_LOG_BUF_USBH(request, 8); - _control_xfer_complete(daddr, result); + control_xfer_complete(daddr, result); break; case XFER_RESULT_FAILED: @@ -931,11 +1106,14 @@ static bool usbh_control_xfer_cb (uint8_t daddr, uint8_t ep_addr, xfer_result_t ctrl_info->actual_len = 0; // reset actual_len (void) osal_mutex_unlock(_usbh_mutex); - TU_ASSERT(usbh_setup_send(daddr, (uint8_t const *) request)); + if (!hcd_setup_send(rhport, daddr, (uint8_t const *) request)) { + control_xfer_complete(daddr, XFER_RESULT_FAILED); + return false; + } } else { TU_LOG_USBH("[%u:%u] Control FAILED, xferred_bytes = %" PRIu32 "\r\n", rhport, daddr, xferred_bytes); TU_LOG_BUF_USBH(request, 8); - _control_xfer_complete(daddr, result); + control_xfer_complete(daddr, result); } break; @@ -944,7 +1122,7 @@ static bool usbh_control_xfer_cb (uint8_t daddr, uint8_t ep_addr, xfer_result_t case CONTROL_STAGE_SETUP: if (request->wLength > 0) { // DATA stage: initial data toggle is always 1 - _control_set_xfer_stage(CONTROL_STAGE_DATA); + control_xfer_set_stage(CONTROL_STAGE_DATA); const uint8_t ep_data = tu_edpt_addr(0, request->bmRequestType_bit.direction); TU_ASSERT(hcd_edpt_xfer(rhport, daddr, ep_data, ctrl_info->buffer, request->wLength)); return true; @@ -959,7 +1137,7 @@ static bool usbh_control_xfer_cb (uint8_t daddr, uint8_t ep_addr, xfer_result_t ctrl_info->actual_len = (uint16_t) xferred_bytes; // ACK stage: toggle is always 1 - _control_set_xfer_stage(CONTROL_STAGE_ACK); + control_xfer_set_stage(CONTROL_STAGE_ACK); const uint8_t ep_status = tu_edpt_addr(0, 1 - request->bmRequestType_bit.direction); TU_ASSERT(hcd_edpt_xfer(rhport, daddr, ep_status, NULL, 0)); break; @@ -976,7 +1154,7 @@ static bool usbh_control_xfer_cb (uint8_t daddr, uint8_t ep_addr, xfer_result_t } } - _control_xfer_complete(daddr, result); + control_xfer_complete(daddr, result); break; } @@ -1023,7 +1201,7 @@ bool tuh_edpt_abort_xfer(uint8_t daddr, uint8_t ep_addr) { const usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info; TU_VERIFY(daddr == ctrl_info->daddr && ctrl_info->stage != CONTROL_STAGE_IDLE); hcd_edpt_abort_xfer(rhport, daddr, ep_addr); - _control_set_xfer_stage(CONTROL_STAGE_IDLE); // reset control transfer state to idle + control_xfer_complete(daddr, XFER_RESULT_ABORTED); } else { usbh_device_t* dev = get_device(daddr); TU_VERIFY(dev); @@ -1055,9 +1233,9 @@ uint8_t *usbh_get_enum_buf(void) { void usbh_int_set(bool enabled) { // TODO all host controller if multiple are used since they shared the same event queue if (enabled) { - hcd_int_enable(_usbh_data.controller_id); + hcd_int_enable(_usbh_controller_id); } else { - hcd_int_disable(_usbh_data.controller_id); + hcd_int_disable(_usbh_controller_id); } } diff --git a/src/osal/osal_freertos.h b/src/osal/osal_freertos.h index 9b12b5c0e..2f36aa9e8 100644 --- a/src/osal/osal_freertos.h +++ b/src/osal/osal_freertos.h @@ -85,7 +85,13 @@ typedef struct { //--------------------------------------------------------------------+ typedef TaskHandle_t osal_task_handle_t; -// Requires INCLUDE_xTaskGetCurrentTaskHandle == 1 in FreeRTOSConfig.h. +// Requires INCLUDE_xTaskGetCurrentTaskHandle == 1 in FreeRTOSConfig.h. FreeRTOS +// also exposes the symbol when configUSE_MUTEXES == 1, so accept either. +#if !defined(INCLUDE_xTaskGetCurrentTaskHandle) || (INCLUDE_xTaskGetCurrentTaskHandle == 0) + #if !defined(configUSE_MUTEXES) || (configUSE_MUTEXES == 0) + #error "TinyUSB host stack requires INCLUDE_xTaskGetCurrentTaskHandle or configUSE_MUTEXES to be enabled in FreeRTOSConfig.h" + #endif +#endif TU_ATTR_ALWAYS_INLINE static inline osal_task_handle_t osal_task_get_current_handle(void) { return xTaskGetCurrentTaskHandle(); } -- cgit v1.3.1 From 84e3347badc7f9f5146b5f5eb129900f2fc59389 Mon Sep 17 00:00:00 2001 From: Wojciech Klimek Date: Thu, 28 May 2026 21:27:11 +0200 Subject: Handle OUT transfer completion in MTP Handle OUT transfer differently from IN to not prematurely change MTP phase when host sends short packet that is not end of MTP data phase. Only reaching container length or ZLP should change phase. --- src/class/mtp/mtp_device.c | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/class/mtp/mtp_device.c b/src/class/mtp/mtp_device.c index 0da984f4a..fd06b4601 100644 --- a/src/class/mtp/mtp_device.c +++ b/src/class/mtp/mtp_device.c @@ -441,9 +441,19 @@ bool mtpd_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t event, uint32_t threshold = CFG_TUD_MTP_EP_BUFSIZE; } - // Check completion: ZLP, short packet, or total length reached - const bool is_complete = - (xferred_bytes == 0 || xferred_bytes < threshold || p_mtp->xferred_len >= p_mtp->total_len); + // Check completion for IN and OUT separately + bool is_complete; + + if (is_data_in) + { + // IN completion: short packet, ZLP, or reaching total_len + is_complete = (xferred_bytes == 0 || xferred_bytes < threshold || p_mtp->xferred_len >= p_mtp->total_len); + } + else + { + // OUT completion: reaching total_len or ZLP + is_complete = (p_mtp->xferred_len >= p_mtp->total_len) || ((xferred_bytes == 0 && p_mtp->xferred_len > 0)); + } TU_LOG_DRV(" MTP Data %s CB: xferred_bytes=%lu, xferred_len/total_len=%lu/%lu, is_complete=%d\r\n", is_data_in ? "IN" : "OUT", xferred_bytes, p_mtp->xferred_len, p_mtp->total_len, is_complete ? 1 : 0); -- cgit v1.3.1 From b4e7c25c1b57a69e4c04e41d0f2203abf0d8d358 Mon Sep 17 00:00:00 2001 From: HiFiPhile Date: Fri, 29 May 2026 11:42:47 +0200 Subject: dwc2: process IN EP before OUT To avoid STATUS IN completion of previous control transfer treated as next DATA IN when IRQ latency is high. Signed-off-by: HiFiPhile --- src/portable/synopsys/dwc2/dcd_dwc2.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/portable/synopsys/dwc2/dcd_dwc2.c b/src/portable/synopsys/dwc2/dcd_dwc2.c index 233840e8b..c90429a15 100644 --- a/src/portable/synopsys/dwc2/dcd_dwc2.c +++ b/src/portable/synopsys/dwc2/dcd_dwc2.c @@ -1213,6 +1213,12 @@ void dcd_int_handler(uint8_t rhport) { dcd_event_sof(rhport, frame, true); } + // IN endpoint interrupt handling. + if (gintsts & GINTSTS_IEPINT) { + // IEPINT bit read-only, clear using DIEPINTn + handle_ep_irq(rhport, TUSB_DIR_IN); + } + #if CFG_TUD_DWC2_SLAVE_ENABLE // RxFIFO non-empty interrupt handling. if (gintsts & GINTSTS_RXFLVL) { @@ -1235,12 +1241,6 @@ void dcd_int_handler(uint8_t rhport) { } #endif - // IN endpoint interrupt handling. - if (gintsts & GINTSTS_IEPINT) { - // IEPINT bit read-only, clear using DIEPINTn - handle_ep_irq(rhport, TUSB_DIR_IN); - } - // Incomplete isochronous IN transfer interrupt handling. if (gintsts & GINTSTS_IISOIXFR) { dwc2->gintsts = GINTSTS_IISOIXFR; -- cgit v1.3.1 From 24700ea8ef09d1a990fd3aff3345064c2a54088b Mon Sep 17 00:00:00 2001 From: hathach Date: Fri, 29 May 2026 23:59:15 +0700 Subject: midi2_device: gate -Wno-type-limits to GCC/Clang for IAR build iccarm rejects -Wno-type-limits, breaking the hil-hfp-iar CI matrix (stm32l412nucleo, stm32f746disco, lpcxpresso43s67). Apply the same CMAKE_C_COMPILER_ID guard used in hw/bsp/family_support.cmake so IAR builds skip the flag without losing the GCC warning suppression. Co-Authored-By: Claude Opus 4.7 --- examples/device/midi2_device/CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/device/midi2_device/CMakeLists.txt b/examples/device/midi2_device/CMakeLists.txt index 295af6550..f1fe09db2 100644 --- a/examples/device/midi2_device/CMakeLists.txt +++ b/examples/device/midi2_device/CMakeLists.txt @@ -30,4 +30,6 @@ target_include_directories(${PROJECT_NAME} PUBLIC family_configure_device_example(${PROJECT_NAME} noos) # Suppress pre-existing warning in usbd.c (uint8_t comparison always true/false) -target_compile_options(${PROJECT_NAME} PRIVATE -Wno-type-limits) +if (CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID STREQUAL "Clang") + target_compile_options(${PROJECT_NAME} PRIVATE -Wno-type-limits) +endif() -- cgit v1.3.1 From 28beb0fe4db608c1a9001452d099cb5ea095377e Mon Sep 17 00:00:00 2001 From: hathach Date: Sat, 30 May 2026 00:11:19 +0700 Subject: ci: fix IAR Pe111 and stm32h7s3 flash overflow - examples/device/midi2_device/src/main.c: drop the unreachable `return 0;` after the `while(1)` superloop. IAR with --warnings_are_errors rejects Pe111 (statement is unreachable); C99 lets `int main` fall off the end, matching midi_test. - examples/host/msc_file_explorer_freertos/skip.txt: skip stm32h7s3nucleo. The board has only 64 KB on-chip FLASH and the FreeRTOS + FatFS host MSC explorer now overflows by ~248 bytes after the async control queue refactor. Co-Authored-By: Claude Opus 4.7 --- examples/device/midi2_device/src/main.c | 2 -- examples/host/msc_file_explorer_freertos/skip.txt | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/device/midi2_device/src/main.c b/examples/device/midi2_device/src/main.c index 62741ac41..ce052a20b 100644 --- a/examples/device/midi2_device/src/main.c +++ b/examples/device/midi2_device/src/main.c @@ -715,6 +715,4 @@ int main(void) { } } } - - return 0; } diff --git a/examples/host/msc_file_explorer_freertos/skip.txt b/examples/host/msc_file_explorer_freertos/skip.txt index f0be07d25..a8c9bea2a 100644 --- a/examples/host/msc_file_explorer_freertos/skip.txt +++ b/examples/host/msc_file_explorer_freertos/skip.txt @@ -1,3 +1,4 @@ mcu:CH32F20X board:lpcxpresso54114 mcu:FT90X +board:stm32h7s3nucleo -- cgit v1.3.1 From 0761df7420df3e62041f17e0c3e0f49d98b0667c Mon Sep 17 00:00:00 2001 From: hathach Date: Sat, 30 May 2026 00:42:14 +0700 Subject: midi2_host: drop unreachable return 0 for IAR Pe111 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same fix as midi2_device — IAR rejects the unreachable statement after the while(1) superloop. Let int main fall off the end. Co-Authored-By: Claude Opus 4.7 --- examples/host/midi2_host/src/main.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/host/midi2_host/src/main.c b/examples/host/midi2_host/src/main.c index 63b08318c..3d5938a2d 100644 --- a/examples/host/midi2_host/src/main.c +++ b/examples/host/midi2_host/src/main.c @@ -160,6 +160,4 @@ int main(void) { while (1) { tuh_task(); } - - return 0; } -- cgit v1.3.1 From fc933e341df29e2ab6fa62508d45a92c65a621fb Mon Sep 17 00:00:00 2001 From: hathach Date: Sat, 30 May 2026 00:46:16 +0700 Subject: midi2_host: silence IAR Pe550 for midi2_idx The variable is set in mount/umount callbacks but not read elsewhere in the example (rx_cb already receives idx as a parameter). IAR treats Pe550 as an error under --warnings_are_errors. Tag it TU_ATTR_UNUSED so the example still shows the pattern of tracking the device index without erroring on unused-set. Co-Authored-By: Claude Opus 4.7 --- examples/host/midi2_host/src/main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/host/midi2_host/src/main.c b/examples/host/midi2_host/src/main.c index 3d5938a2d..d81e77a33 100644 --- a/examples/host/midi2_host/src/main.c +++ b/examples/host/midi2_host/src/main.c @@ -35,7 +35,7 @@ // State //--------------------------------------------------------------------+ -static uint8_t midi2_idx = 0xFF; +TU_ATTR_UNUSED static uint8_t midi2_idx = 0xFF; //--------------------------------------------------------------------+ // UMP printer - shows MT and word(s) in hex; decodes Channel Voice -- cgit v1.3.1 From a64eb336c4a8d3f9c160aa30b81033e3ad7227d8 Mon Sep 17 00:00:00 2001 From: hathach Date: Sat, 30 May 2026 00:52:22 +0700 Subject: dynamic_switch: hoist while(1) out for OS_NONE Sonar flagged the loop body as executing only once on OS_NONE because the OS_NONE branch returns inside the first iteration (main() drives the task again). Make the while(1) conditional on RTOS so the OS_NONE build is a straight-line function with no misleading loop. Co-Authored-By: Claude Opus 4.7 --- examples/dual/dynamic_switch/src/main.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/examples/dual/dynamic_switch/src/main.c b/examples/dual/dynamic_switch/src/main.c index e8e0deb53..ac5126e56 100644 --- a/examples/dual/dynamic_switch/src/main.c +++ b/examples/dual/dynamic_switch/src/main.c @@ -281,7 +281,9 @@ void usb_mode_switch(void) { void cdc_task(void *param) { (void) param; +#if CFG_TUSB_OS == OPT_OS_FREERTOS while (1) { +#endif // Only touch device-CDC APIs while we're in device mode. After // usb_mode_switch() sets current_role to INVALID and tusb_deinit() runs, // calling tud_cdc_write_flush() here would hit a deinit'd device stack. @@ -295,13 +297,10 @@ void cdc_task(void *param) { } tud_cdc_write_flush(); } - #if CFG_TUSB_OS == OPT_OS_FREERTOS vTaskDelay(pdMS_TO_TICKS(10)); -#else - return; // main loop will call us again -#endif } +#endif } //--------------------------------------------------------------------+ @@ -369,7 +368,9 @@ void tuh_umount_cb(uint8_t daddr) { void print_devinfo_task(void *param) { (void) param; +#if CFG_TUSB_OS == OPT_OS_FREERTOS while (1) { +#endif if (current_role == TUSB_ROLE_HOST) { for (uint8_t daddr = 1; daddr < TU_ARRAY_SIZE(need_devinfo); daddr++) { if (need_devinfo[daddr]) { @@ -378,13 +379,10 @@ void print_devinfo_task(void *param) { } } } - #if CFG_TUSB_OS == OPT_OS_FREERTOS vTaskDelay(pdMS_TO_TICKS(10)); -#else - return; -#endif } +#endif } static void print_one_device(uint8_t daddr) { -- cgit v1.3.1 From 08381d44214135cc9ff9423de837cb466b6ad701 Mon Sep 17 00:00:00 2001 From: HiFiPhile Date: Sat, 30 May 2026 11:46:18 +0200 Subject: esp32 build fixes Signed-off-by: HiFiPhile --- examples/device/audio_4_channel_mic_freertos/CMakeLists.txt | 5 +++++ examples/device/audio_test_freertos/CMakeLists.txt | 5 +++++ examples/device/board_test/CMakeLists.txt | 5 +++++ examples/device/cdc_msc_freertos/CMakeLists.txt | 5 +++++ examples/device/hid_composite_freertos/CMakeLists.txt | 5 +++++ 5 files changed, 25 insertions(+) diff --git a/examples/device/audio_4_channel_mic_freertos/CMakeLists.txt b/examples/device/audio_4_channel_mic_freertos/CMakeLists.txt index d43a72e58..66ef19fbc 100644 --- a/examples/device/audio_4_channel_mic_freertos/CMakeLists.txt +++ b/examples/device/audio_4_channel_mic_freertos/CMakeLists.txt @@ -2,6 +2,11 @@ cmake_minimum_required(VERSION 3.20) include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake) +# Need to set Espressif defaults before project() is called +if(FAMILY STREQUAL "espressif") + list(APPEND SDKCONFIG_DEFAULTS "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults") +endif() + project(audio_4_channel_mic_freertos C CXX ASM) # Checks this example is valid for the family and initializes the project diff --git a/examples/device/audio_test_freertos/CMakeLists.txt b/examples/device/audio_test_freertos/CMakeLists.txt index 71d65eccc..a39e56822 100644 --- a/examples/device/audio_test_freertos/CMakeLists.txt +++ b/examples/device/audio_test_freertos/CMakeLists.txt @@ -2,6 +2,11 @@ cmake_minimum_required(VERSION 3.20) include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake) +# Need to set Espressif defaults before project() is called +if(FAMILY STREQUAL "espressif") + list(APPEND SDKCONFIG_DEFAULTS "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults") +endif() + project(audio_test_freertos C CXX ASM) # Checks this example is valid for the family and initializes the project diff --git a/examples/device/board_test/CMakeLists.txt b/examples/device/board_test/CMakeLists.txt index bd7b8e0ca..f14d72c08 100644 --- a/examples/device/board_test/CMakeLists.txt +++ b/examples/device/board_test/CMakeLists.txt @@ -2,6 +2,11 @@ cmake_minimum_required(VERSION 3.20) include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake) +# Need to set Espressif defaults before project() is called +if(FAMILY STREQUAL "espressif") + list(APPEND SDKCONFIG_DEFAULTS "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults") +endif() + project(board_test C CXX ASM) # Checks this example is valid for the family and initializes the project diff --git a/examples/device/cdc_msc_freertos/CMakeLists.txt b/examples/device/cdc_msc_freertos/CMakeLists.txt index 429000427..1eafd529a 100644 --- a/examples/device/cdc_msc_freertos/CMakeLists.txt +++ b/examples/device/cdc_msc_freertos/CMakeLists.txt @@ -2,6 +2,11 @@ cmake_minimum_required(VERSION 3.20) include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake) +# Need to set Espressif defaults before project() is called +if(FAMILY STREQUAL "espressif") + list(APPEND SDKCONFIG_DEFAULTS "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults") +endif() + project(cdc_msc_freertos C CXX ASM) # Checks this example is valid for the family and initializes the project diff --git a/examples/device/hid_composite_freertos/CMakeLists.txt b/examples/device/hid_composite_freertos/CMakeLists.txt index b52373011..2081a7782 100644 --- a/examples/device/hid_composite_freertos/CMakeLists.txt +++ b/examples/device/hid_composite_freertos/CMakeLists.txt @@ -2,6 +2,11 @@ cmake_minimum_required(VERSION 3.20) include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake) +# Need to set Espressif defaults before project() is called +if(FAMILY STREQUAL "espressif") + list(APPEND SDKCONFIG_DEFAULTS "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults") +endif() + project(hid_composite_freertos C CXX ASM) # Checks this example is valid for the family and initializes the project -- cgit v1.3.1 From 7e0fcaa41ee9330274808d88e5211bdbe37511a4 Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 1 Jun 2026 10:05:17 +0700 Subject: ultrareview nits: keep xfer_result table in sync, hoist blinky loop - src/tusb.c: extend tu_str_xfer_result[] with "ABORTED" and "INVALID" to match the new enum size. Not reachable today (no HCD posts those values through hcd_event_xfer_complete), but keeps the enum/table invariant intact so future HCDs that surface ABORTED don't index OOB. - examples/dual/dynamic_switch/src/main.c: apply the same while(1) hoist already done for cdc_task / print_devinfo_task to led_blinking_task. On OS_NONE the loop returned mid-iteration, which on first call could fire multiple back-to-back toggles while start_ms (initially 0) caught up to uptime. Co-Authored-By: Claude Opus 4.7 --- examples/dual/dynamic_switch/src/main.c | 17 ++++++++++------- src/tusb.c | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/examples/dual/dynamic_switch/src/main.c b/examples/dual/dynamic_switch/src/main.c index ac5126e56..f67cd885c 100644 --- a/examples/dual/dynamic_switch/src/main.c +++ b/examples/dual/dynamic_switch/src/main.c @@ -483,16 +483,19 @@ void led_blinking_task(void *param) { (void) param; static uint32_t start_ms = 0; static bool led_state = false; - while (1) { #if CFG_TUSB_OS == OPT_OS_FREERTOS + while (1) { vTaskDelay(pdMS_TO_TICKS(blink_interval_ms)); -#else - if (tusb_time_millis_api() - start_ms < blink_interval_ms) { - return; // not enough time - } -#endif start_ms += blink_interval_ms; board_led_write(led_state); - led_state = 1 - led_state; // toggle + led_state = 1 - led_state; + } +#else + if (tusb_time_millis_api() - start_ms < blink_interval_ms) { + return; // not enough time } + start_ms += blink_interval_ms; + board_led_write(led_state); + led_state = 1 - led_state; +#endif } diff --git a/src/tusb.c b/src/tusb.c index 5d656fb8c..634cbc10b 100644 --- a/src/tusb.c +++ b/src/tusb.c @@ -497,7 +497,7 @@ char const* const tu_str_std_request[] = { }; char const* const tu_str_xfer_result[] = { - "OK", "FAILED", "STALLED", "TIMEOUT" + "OK", "FAILED", "STALLED", "TIMEOUT", "ABORTED", "INVALID" }; #endif -- cgit v1.3.1 From 17185428df755d7229407e6ac87c124e522877dc Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 1 Jun 2026 10:58:36 +0700 Subject: CFG_TUH_CONTROL_PENDING_QUEUE_SZ defefault to 4 if hub is eanbled, 2 if not --- src/host/usbh.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/host/usbh.c b/src/host/usbh.c index 05e03245f..9d159985e 100644 --- a/src/host/usbh.c +++ b/src/host/usbh.c @@ -41,7 +41,11 @@ #endif #ifndef CFG_TUH_CONTROL_PENDING_QUEUE_SZ - #define CFG_TUH_CONTROL_PENDING_QUEUE_SZ 4 + #if CFG_TUH_HUB + #define CFG_TUH_CONTROL_PENDING_QUEUE_SZ 4 + #else + #define CFG_TUH_CONTROL_PENDING_QUEUE_SZ 2 + #endif #endif #ifndef CFG_TUH_INTERFACE_MAX -- cgit v1.3.1 From 95d11a8a7a779e51643a578fe30c70466eb85cef Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 1 Jun 2026 23:51:24 +0700 Subject: docs(hil): support running HIL locally on ci.lan Update the hil skill so config selection is per-host: run `hostname` first, then htpc uses local.json and ci uses tinyusb.json. ci can now run HIL on its own large board pool locally instead of only via SSH from htpc. Remote (SSH) mode is htpc-only since ci cannot reach htpc. Also compact the skill for brevity. --- .claude/skills/hil/SKILL.md | 58 +++++++++++++++++---------------------------- 1 file changed, 22 insertions(+), 36 deletions(-) diff --git a/.claude/skills/hil/SKILL.md b/.claude/skills/hil/SKILL.md index 1f3d7d072..c705c149c 100644 --- a/.claude/skills/hil/SKILL.md +++ b/.claude/skills/hil/SKILL.md @@ -1,66 +1,52 @@ --- name: hil -description: Use when running TinyUSB Hardware-in-the-Loop (HIL) tests on physical boards, debugging HIL failures, or copying firmware to the ci.lan test rig. Covers local execution and remote execution over SSH, config selection, and debugging tips. +description: Use when running TinyUSB Hardware-in-the-Loop (HIL) tests on physical boards, debugging HIL failures, or copying firmware to the ci.lan test rig. Covers per-host config selection (htpc uses local.json, ci uses tinyusb.json), local execution on either htpc or ci, remote execution over SSH from htpc, and debugging tips. --- # Hardware-in-the-Loop (HIL) Testing -Run TinyUSB HIL tests against real boards. Two execution modes — **local** (boards attached to this machine) and **remote** (boards attached to `ci.lan`, reached over SSH). Default to **local** unless the user specifies `remote`. Do not auto-detect. +Run TinyUSB HIL tests on real boards. **Run `hostname` first** — it sets the default config and whether remote mode is possible. -## Prerequisites +| Host | Local boards | Remote (SSH → ci.lan)? | +|------|--------------|------------------------| +| `htpc` (dev PC) | `local.json` | yes (large pool, `tinyusb.json`) | +| `ci` (the rig) | `tinyusb.json` (large pool) | no — can't SSH to htpc, and boards are already local | -- Examples must already be built for the target board(s). See AGENTS.md "Build" → "All examples for a board", which produces `examples/cmake-build-/`. -- `-B examples` tells `hil_test.py` that `examples/` is the parent folder containing the per-board build outputs. +Default to **local**. Use **remote** only when on `htpc` and the user says `remote`/`ci.lan`. Never attempt remote on `ci`. -## Choosing arguments +## Prerequisites -Infer from the user's request: +Examples must be built for the target board(s) — see AGENTS.md "Build" → "All examples for a board" (produces `examples/cmake-build-/`). `-B examples` points `hil_test.py` at that parent folder. -- **Mode:** `local` (default) or `remote`. Only switch to `remote` if the user explicitly says so or names `ci.lan`. -- **Board:** if the user names a specific board, pass `-b BOARD_NAME`. Otherwise omit `-b` to run all boards in the config. -- **Pass-through flags:** `-v` (verbose), `-r N` (retry count), etc. — pass through unchanged. +## Arguments -Config file follows from mode: -- **Local** → `test/hil/local.json` (user-supplied; not tracked in repo — describes boards attached locally) -- **Remote** → `test/hil/tinyusb.json` (tracked; describes the `ci.lan` test rig) +- **Board:** `-b BOARD_NAME` for one board; omit to run all boards in the config. +- **Pass-through:** `-v`, `-r N`, etc. forwarded unchanged. -If `local.json` is missing, fall back to `tinyusb.json` only when explicitly told to; otherwise stop and ask the user to supply one. +If `local.json` is missing on `htpc`, ask the user to supply one (only fall back to `tinyusb.json` if told to). ## Local execution -Boards attached to this machine: +Pick `$CONFIG` from `hostname`: `local.json` on `htpc`, `tinyusb.json` on `ci`. ```bash -# Specific board: -python3 test/hil/hil_test.py -b BOARD_NAME -B examples test/hil/local.json $EXTRA_ARGS -# All boards in the config (no -b): -python3 test/hil/hil_test.py -B examples test/hil/local.json $EXTRA_ARGS +python3 test/hil/hil_test.py [-b BOARD_NAME] -B examples $CONFIG $EXTRA_ARGS ``` -## Remote execution (ci.lan) +## Remote execution (htpc → ci.lan only) -Use `test/hil/hil_ci.sh` — it handles dir setup, scp of test scripts, rsync of firmware artifacts (`.elf` / `.bin` / `.hex` only), and running `hil_test.py` on `ci.lan`: +`test/hil/hil_ci.sh` handles dir setup, scp of test scripts, rsync of firmware (`.elf`/`.bin`/`.hex`), and runs `hil_test.py` on `ci.lan` with `tinyusb.json`: ```bash -# Specific board: -bash test/hil/hil_ci.sh -b raspberry_pi_pico2 -# All boards in tinyusb.json: -bash test/hil/hil_ci.sh -# Pass-through extra args (any non -b flag is forwarded to hil_test.py): -bash test/hil/hil_ci.sh -b raspberry_pi_pico2 -t host/cdc_msc_hid -r 1 +bash test/hil/hil_ci.sh [-b BOARD_NAME] [extra hil_test.py args...] ``` -Overrides via env vars: `REMOTE=ci.lan`, `REMOTE_DIR=/tmp/tinyusb-hil`, `CONFIG=test/hil/tinyusb.json`. - -The script fails fast if the build dir or repo layout is missing. +Env overrides: `REMOTE`, `REMOTE_DIR`, `CONFIG`. Fails fast if the build dir/repo layout is missing. ## Timing -HIL runs take 2-5 minutes. Use a timeout of at least 20 minutes (600000 ms). NEVER cancel early. +Runs take 2-5 min. Use a timeout ≥ 20 min (600000 ms). NEVER cancel early. -## Reporting results +## Reporting -After the test completes: -- Show the test output to the user. -- Summarize pass/fail per board. -- On failure, suggest re-running with `-v` for verbose output. If `-v` isn't enough, temporarily add debug prints to `test/hil/hil_test.py` to pinpoint the issue. +Show the output, summarize pass/fail per board. On failure, retry with `-v`; if that's not enough, add temporary debug prints to `hil_test.py`. -- cgit v1.3.1 From 87f9cc01cfd3134594d196210d28ee590c7a5fc3 Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 1 Jun 2026 23:51:41 +0700 Subject: ci: re-enable Claude PR review and harden auth/permissions - claude-code-review.yml: re-enable (drop `if: false`); switch from pull_request_target to pull_request so fork PRs never receive the OAuth token (avoids prompt-injection token leak). Auto-review on open/synchronize/reopen/ready_for_review, skip drafts, sticky comment. - claude.yml: grant contents/pull-requests/issues write so @claude can reply and push fixes; @claude is the on-demand path for fork PRs. --- .github/workflows/claude-code-review.yml | 19 +++++++++++++++---- .github/workflows/claude.yml | 6 +++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 43144bb5e..2f055287c 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -1,18 +1,27 @@ name: Claude Code Review on: - pull_request_target: - types: [opened, synchronize, ready_for_review, reopened] + pull_request: + # opened/reopened/ready_for_review -> first auto review + # synchronize -> auto re-review on new pushes + # + # NOTE: pull_request (not _target) means fork PRs from non-write-access + # contributors get NO token, so they are not auto-reviewed -> use @claude + # on those. Same-repo branches (yours or write-access contributors) get + # full auto-review safely. + types: [opened, synchronize, reopened, ready_for_review] jobs: claude-review: - if: false + # Skip drafts; review real PRs only + if: github.event.pull_request.draft == false runs-on: ubuntu-latest permissions: contents: read pull-requests: write issues: read id-token: write + actions: read # Required for Claude to read CI results on PRs steps: - name: Checkout repository @@ -28,5 +37,7 @@ jobs: plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' plugins: 'code-review@claude-code-plugins' prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' + # Reuse one comment instead of posting a new one each push + use_sticky_comment: true + claude_args: '--max-turns 20' # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://code.claude.com/docs/en/cli-reference for available options diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 50f449949..660edfb7b 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -19,9 +19,9 @@ jobs: (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) runs-on: ubuntu-latest permissions: - contents: read - pull-requests: read - issues: read + contents: write # allow Claude to push commits/branches when asked + pull-requests: write # allow Claude to comment on / update PRs + issues: write # allow Claude to comment on / update issues id-token: write actions: read # Required for Claude to read CI results on PRs steps: -- cgit v1.3.1 From 1ea04f7fe67bac927848e9dfd3ce2f607e7b93d7 Mon Sep 17 00:00:00 2001 From: hathach Date: Tue, 2 Jun 2026 00:16:54 +0700 Subject: ci: address codex/copilot review on claude workflows - claude.yml: gate @claude on author_association (OWNER/MEMBER/COLLABORATOR) so the write-scoped token and OAuth secret are never issued for an untrusted commenter on this public repo (defense-in-depth). - claude-code-review.yml: skip fork PRs in the job condition (head.repo.full_name == github.repository) since forks get no secrets and would only fail noisily; fix the misleading token comment; pass additional_permissions: actions: read so actions: read is effective. - hil SKILL.md: reword hostname guidance, use full test/hil/* paths, and show an explicit CONFIG= assignment so the local command is runnable. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/hil/SKILL.md | 14 ++++++++------ .github/workflows/claude-code-review.yml | 17 +++++++++++------ .github/workflows/claude.yml | 15 +++++++++++---- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/.claude/skills/hil/SKILL.md b/.claude/skills/hil/SKILL.md index c705c149c..22588eba3 100644 --- a/.claude/skills/hil/SKILL.md +++ b/.claude/skills/hil/SKILL.md @@ -5,12 +5,12 @@ description: Use when running TinyUSB Hardware-in-the-Loop (HIL) tests on physic # Hardware-in-the-Loop (HIL) Testing -Run TinyUSB HIL tests on real boards. **Run `hostname` first** — it sets the default config and whether remote mode is possible. +Run TinyUSB HIL tests on real boards. **Run `hostname` first** — it tells you which host you are on, which determines the default config and whether remote mode is possible. -| Host | Local boards | Remote (SSH → ci.lan)? | +| Host | Local config | Remote (SSH → ci.lan)? | |------|--------------|------------------------| -| `htpc` (dev PC) | `local.json` | yes (large pool, `tinyusb.json`) | -| `ci` (the rig) | `tinyusb.json` (large pool) | no — can't SSH to htpc, and boards are already local | +| `htpc` (dev PC) | `test/hil/local.json` | yes (large pool, `test/hil/tinyusb.json`) | +| `ci` (the rig) | `test/hil/tinyusb.json` (large pool) | no — can't SSH to htpc, and boards are already local | Default to **local**. Use **remote** only when on `htpc` and the user says `remote`/`ci.lan`. Never attempt remote on `ci`. @@ -27,10 +27,12 @@ If `local.json` is missing on `htpc`, ask the user to supply one (only fall back ## Local execution -Pick `$CONFIG` from `hostname`: `local.json` on `htpc`, `tinyusb.json` on `ci`. +Set `CONFIG` from `hostname` first, then run: ```bash -python3 test/hil/hil_test.py [-b BOARD_NAME] -B examples $CONFIG $EXTRA_ARGS +CONFIG=test/hil/local.json # on htpc +# CONFIG=test/hil/tinyusb.json # on ci +python3 test/hil/hil_test.py [-b BOARD_NAME] -B examples "$CONFIG" $EXTRA_ARGS ``` ## Remote execution (htpc → ci.lan only) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 2f055287c..6c8bbb03d 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -5,16 +5,18 @@ on: # opened/reopened/ready_for_review -> first auto review # synchronize -> auto re-review on new pushes # - # NOTE: pull_request (not _target) means fork PRs from non-write-access - # contributors get NO token, so they are not auto-reviewed -> use @claude - # on those. Same-repo branches (yours or write-access contributors) get - # full auto-review safely. + # NOTE: pull_request (not _target) means fork PRs get a read-only GITHUB_TOKEN + # and NO repository secrets (CLAUDE_CODE_OAUTH_TOKEN), so they cannot be + # auto-reviewed. The job condition below skips them cleanly -> use @claude on + # those. Same-repo branches (yours or write-access contributors) auto-review. types: [opened, synchronize, reopened, ready_for_review] jobs: claude-review: - # Skip drafts; review real PRs only - if: github.event.pull_request.draft == false + # Skip drafts, and skip fork PRs (no secrets -> would only fail noisily) + if: > + github.event.pull_request.draft == false && + github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest permissions: contents: read @@ -34,6 +36,9 @@ jobs: uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + # Pairs with the actions: read permission so Claude can read CI results + additional_permissions: | + actions: read plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' plugins: 'code-review@claude-code-plugins' prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 660edfb7b..dedecd349 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -12,11 +12,18 @@ on: jobs: claude: + # Only trusted actors (repo owner/member/collaborator) may summon @claude, so the + # write-scoped token and OAuth secret are never issued for an outside contributor's + # comment on this public repo. Defense-in-depth on top of the action's own check. if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association)) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.issue.author_association)) runs-on: ubuntu-latest permissions: contents: write # allow Claude to push commits/branches when asked -- cgit v1.3.1 From 044cd06f87117afa541d8cc743fcc9ef2bdc8089 Mon Sep 17 00:00:00 2001 From: hathach Date: Tue, 2 Jun 2026 00:24:05 +0700 Subject: ci: address second codex/copilot review round - claude.yml: drop the issues "assigned" trigger; its author_association gate keys on the issue author, not the assigner, so a maintainer assigning an outside contributor's issue would be wrongly skipped. - claude-code-review.yml: issues: read -> write so use_sticky_comment can create/update its PR comment via the issues API. - hil SKILL.md: make local/remote command blocks copy-pasteable (drop [-b BOARD_NAME] notation for concrete examples) and fix timeout (600000 ms is 10 min; use 1200000 ms for the stated 20 min). Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/hil/SKILL.md | 22 ++++++++++++++++------ .github/workflows/claude-code-review.yml | 2 +- .github/workflows/claude.yml | 5 ++++- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/.claude/skills/hil/SKILL.md b/.claude/skills/hil/SKILL.md index 22588eba3..a7a916907 100644 --- a/.claude/skills/hil/SKILL.md +++ b/.claude/skills/hil/SKILL.md @@ -27,27 +27,37 @@ If `local.json` is missing on `htpc`, ask the user to supply one (only fall back ## Local execution -Set `CONFIG` from `hostname` first, then run: +Set `CONFIG` from `hostname` first (`test/hil/local.json` on htpc, `test/hil/tinyusb.json` on ci): ```bash -CONFIG=test/hil/local.json # on htpc -# CONFIG=test/hil/tinyusb.json # on ci -python3 test/hil/hil_test.py [-b BOARD_NAME] -B examples "$CONFIG" $EXTRA_ARGS +CONFIG=test/hil/local.json # on ci use: CONFIG=test/hil/tinyusb.json + +# All boards in the config: +python3 test/hil/hil_test.py -B examples "$CONFIG" + +# A single board (replace stm32f723disco): +python3 test/hil/hil_test.py -b stm32f723disco -B examples "$CONFIG" ``` +Append pass-through flags (`-v`, `-r 1`, …) to either command as needed. + ## Remote execution (htpc → ci.lan only) `test/hil/hil_ci.sh` handles dir setup, scp of test scripts, rsync of firmware (`.elf`/`.bin`/`.hex`), and runs `hil_test.py` on `ci.lan` with `tinyusb.json`: ```bash -bash test/hil/hil_ci.sh [-b BOARD_NAME] [extra hil_test.py args...] +# All boards: +bash test/hil/hil_ci.sh + +# A single board, with pass-through flags: +bash test/hil/hil_ci.sh -b raspberry_pi_pico2 -t host/cdc_msc_hid -r 1 ``` Env overrides: `REMOTE`, `REMOTE_DIR`, `CONFIG`. Fails fast if the build dir/repo layout is missing. ## Timing -Runs take 2-5 min. Use a timeout ≥ 20 min (600000 ms). NEVER cancel early. +Runs take 2-5 min. Use a timeout ≥ 20 min (1200000 ms). NEVER cancel early. ## Reporting diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 6c8bbb03d..4a0e4639b 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -21,7 +21,7 @@ jobs: permissions: contents: read pull-requests: write - issues: read + issues: write # use_sticky_comment posts/updates a PR comment via the issues API id-token: write actions: read # Required for Claude to read CI results on PRs diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index dedecd349..bf7a401e4 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -6,7 +6,10 @@ on: pull_request_review_comment: types: [created] issues: - types: [opened, assigned] + # only "opened" — an issue's author_association gates the summon below; + # "assigned" would gate on the issue author, not the assigner, so a + # maintainer assigning an outsider's issue would be wrongly skipped. + types: [opened] pull_request_review: types: [submitted] -- cgit v1.3.1 From 9c316a458cdfcc00f1d349e24f3504337a59daa1 Mon Sep 17 00:00:00 2001 From: HiFiPhile Date: Mon, 1 Jun 2026 22:37:14 +0200 Subject: add device errata document Signed-off-by: HiFiPhile --- README.rst | 275 +++++++++++++++++++++++------------------------ docs/index.rst | 1 + docs/troubleshooting.rst | 2 + 3 files changed, 140 insertions(+), 138 deletions(-) diff --git a/README.rst b/README.rst index e1e73c3ad..c376599ec 100644 --- a/README.rst +++ b/README.rst @@ -130,144 +130,143 @@ TinyUSB is completely thread-safe by pushing all Interrupt Service Request (ISR) Supported CPUs -------------- - -+--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ -| Manufacturer | Family | Device | Host | Highspeed | Driver | Note | -+==============+=============================+========+======+===========+========================+====================+ -| Allwinner | F1C100s/F1C200s | ✔ | | ✔ | sunxi | musb variant | -+--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ -| Analog | MAX3421E | | ✔ | ✖ | max3421 | via SPI | -| +-----------------------------+--------+------+-----------+------------------------+--------------------+ -| | MAX32 650, 666, 690, | ✔ | | ✔ | musb | 1-dir ep | -| | MAX78002 | | | | | | -+--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ -| Artery AT32 | F403a_407, F413 | ✔ | | | fsdev | 512 USB RAM | -| +-----------------------------+--------+------+-----------+------------------------+--------------------+ -| | F415, F435_437, F423, | ✔ | ✔ | | dwc2 | | -| | F425, F45x | | | | | | -| +-----------------------------+--------+------+-----------+------------------------+--------------------+ -| | F402_F405 | ✔ | ✔ | ✔ | dwc2 | F405 is HS | -+--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ -| Bridgetek | FT90x | ✔ | | ✔ | ft9xx | 1-dir ep | -+--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ -| Broadcom | BCM2711, BCM2837 | ✔ | | ✔ | dwc2 | | -+--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ -| Dialog | DA1469x | ✔ | ✖ | ✖ | da146xx | | -+--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ -| Espressif | S2, S3, H4 | ✔ | ✔ | ✖ | dwc2 | | -| ESP32 +-----------------------------+--------+------+-----------+------------------------+--------------------+ -| | P4 | ✔ | ✔ | ✔ | dwc2 | | -| +-----------------------------+--------+------+-----------+------------------------+--------------------+ -| | S31 | ✔ | ✔ | ✔ | dwc2 | | -+--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ -| GigaDevice | GD32VF103 | ✔ | | ✖ | dwc2 | | -+--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ -| HPMicro | HPM6750 | ✔ | ✔ | ✔ | ci_hs, ehci | | -+--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ -| Infineon | XMC4500 | ✔ | ✔ | ✖ | dwc2 | | -+--------------+-----+-----------------------+--------+------+-----------+------------------------+--------------------+ -| MicroChip | SAM | D11, D21, L21, L22 | ✔ | | ✖ | samd | | -| | +-----------------------+--------+------+-----------+------------------------+--------------------+ -| | | D51, E5x | ✔ | | ✖ | samd | | -| | +-----------------------+--------+------+-----------+------------------------+--------------------+ -| | | G55 | ✔ | | ✖ | samg | 1-dir ep | -| | +-----------------------+--------+------+-----------+------------------------+--------------------+ -| | | E70,S70,V70,V71 | ✔ | | ✔ | samx7x | 1-dir ep | -| +-----+-----------------------+--------+------+-----------+------------------------+--------------------+ -| | PIC | 24 | ✔ | | | pic | ci_fs variant | -| | +-----------------------+--------+------+-----------+------------------------+--------------------+ -| | | 32 mm, mk, mx | ✔ | | | pic | ci_fs variant | -| | +-----------------------+--------+------+-----------+------------------------+--------------------+ -| | | dsPIC33 | ✔ | | | pic | ci_fs variant | -| | +-----------------------+--------+------+-----------+------------------------+--------------------+ -| | | 32mz | ✔ | | | pic32mz | musb variant | -+--------------+-----+-----------------------+--------+------+-----------+------------------------+--------------------+ -| MindMotion | mm32 | ✔ | | ✖ | mm32f327x_otg | ci_fs variant | -+--------------+-----+-----------------------+--------+------+-----------+------------------------+--------------------+ -| NordicSemi | nRF52, nRF53 | ✔ | ✖ | ✖ | nrf5x | only ep8 is ISO | -| +-----------------------------+--------+------+-----------+------------------------+--------------------+ -| | nRF54 | ✔ | ✖ | ✔ | dwc2 | | -+--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ -| Nuvoton | NUC120 | ✔ | ✖ | ✖ | nuc120 | | -| +-----------------------------+--------+------+-----------+------------------------+--------------------+ -| | NUC121/NUC125, NUC126 | ✔ | ✖ | ✖ | nuc121 | | -| +-----------------------------+--------+------+-----------+------------------------+--------------------+ -| | NUC505 | ✔ | | ✔ | nuc505 | | -+--------------+---------+-------------------+--------+------+-----------+------------------------+--------------------+ -| NXP | iMXRT | RT 10xx, 11xx | ✔ | ✔ | ✔ | ci_hs, ehci | | -| +---------+-------------------+--------+------+-----------+------------------------+--------------------+ -| | Kinetis | KL | ✔ | ⚠ | ✖ | ci_fs, khci | | -| | +-------------------+--------+------+-----------+------------------------+--------------------+ -| | | K32L2 | ✔ | | ✖ | khci | ci_fs variant | -| +---------+-------------------+--------+------+-----------+------------------------+--------------------+ -| | LPC | 11u, 13, 15 | ✔ | ✖ | ✖ | lpc_ip3511 | | -| | +-------------------+--------+------+-----------+------------------------+--------------------+ -| | | 17, 40 | ✔ | ⚠ | ✖ | lpc17_40, ohci | | -| | +-------------------+--------+------+-----------+------------------------+--------------------+ -| | | 18, 43 | ✔ | ✔ | ✔ | ci_hs, ehci | | -| | +-------------------+--------+------+-----------+------------------------+--------------------+ -| | | 51u | ✔ | ✖ | ✖ | lpc_ip3511 | | -| | +-------------------+--------+------+-----------+------------------------+--------------------+ -| | | 54 | ⚠ | ⚠ | ✔ | lpc_ip3511, lpc_ip3516 | NRND, read errata | -| | +-------------------+--------+------+-----------+------------------------+--------------------+ -| | | 55 | ✔ | ✔ | ✔ | lpc_ip3511, lpc_ip3516 | | -| +---------+-------------------+--------+------+-----------+------------------------+--------------------+ -| | MCX | N9 | ✔ | | ✔ | ci_fs, ci_hs, ehci | | -| | +-------------------+--------+------+-----------+------------------------+--------------------+ -| | | A15 | ✔ | | | ci_fs | | -| +---------+-------------------+--------+------+-----------+------------------------+--------------------+ -| | RW61x | ✔ | ✔ | ✔ | ci_hs, ehci | | -+--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ -| Raspberry Pi | RP2040, RP2350 | ✔ | ✔ | ✖ | rp2040, pio_usb | | -+--------------+-----+-----------------------+--------+------+-----------+------------------------+--------------------+ -| Renesas | RX | 63N, 65N, 72N | ✔ | ✔ | ✖ | rusb2 | | -| +-----+-----------------------+--------+------+-----------+------------------------+--------------------+ -| | RA | 4M1, 4M3, 6M1 | ✔ | ✔ | ✖ | rusb2 | | -| | +-----------------------+--------+------+-----------+------------------------+--------------------+ -| | | 6M5 | ✔ | ✔ | ✔ | rusb2 | | -+--------------+-----+-----------------------+--------+------+-----------+------------------------+--------------------+ -| Silabs | EFM32GG12 | ✔ | | ✖ | dwc2 | | -+--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ -| Sony | CXD56 | ✔ | ✖ | ✔ | cxd56 | | -+--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ -| ST STM32 | F0, F3, L0, L1, L5, WBx5 | ✔ | ✖ | ✖ | stm32_fsdev | | -| +----+------------------------+--------+------+-----------+------------------------+--------------------+ -| | F1 | 102, 103 | ✔ | ✖ | ✖ | stm32_fsdev | 512 USB RAM | -| | +------------------------+--------+------+-----------+------------------------+--------------------+ -| | | 105, 107 | ✔ | ✔ | ✖ | dwc2 | | -| +----+------------------------+--------+------+-----------+------------------------+--------------------+ -| | F2, F4, F7, H7, H7RS | ✔ | ✔ | ✔ | dwc2 | | -| +-----------------------------+--------+------+-----------+------------------------+--------------------+ -| | C0, G0, H5, U3 | ✔ | ✔ | ✖ | stm32_fsdev | 2KB USB RAM | -| +-----------------------------+--------+------+-----------+------------------------+--------------------+ -| | G4 | ✔ | ✖ | ✖ | stm32_fsdev | 1KB USB RAM | -| +----+------------------------+--------+------+-----------+------------------------+--------------------+ -| | L4 | 4x2, 4x3 | ✔ | ✖ | ✖ | stm32_fsdev | 1KB USB RAM | -| | +------------------------+--------+------+-----------+------------------------+--------------------+ -| | | 4x5, 4x6, 4+ | ✔ | ✔ | ✖ | dwc2 | | -| +----+------------------------+--------+------+-----------+------------------------+--------------------+ -| | N6 | ✔ | ✔ | ✔ | dwc2 | | -| +-----------------------------+--------+------+-----------+------------------------+--------------------+ -| | U0 | ✔ | ✖ | ✖ | stm32_fsdev | 1KB USB RAM | -| +----+------------------------+--------+------+-----------+------------------------+--------------------+ -| | U5 | 535, 545 | ✔ | ✔ | ✖ | stm32_fsdev | 2KB USB RAM | -| | +------------------------+--------+------+-----------+------------------------+--------------------+ -| | | 575, 585 | ✔ | ✔ | ✖ | dwc2 | | -| | +------------------------+--------+------+-----------+------------------------+--------------------+ -| | | 59x,5Ax,5Fx,5Gx | ✔ | ✔ | ✔ | dwc2 | | -+--------------+----+------------------------+--------+------+-----------+------------------------+--------------------+ -| TI | MSP430 | ✔ | ✖ | ✖ | msp430x5xx | | -| +-----------------------------+--------+------+-----------+------------------------+--------------------+ -| | MSP432E4, TM4C123 | ✔ | | ✖ | musb | | -+--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ -| ValentyUSB | eptri | ✔ | ✖ | ✖ | eptri | | -+--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ -| WCH | CH32F20x | ⚠ | | ✔ | ch32_usbhs | Data loss possible | -| +-----------------------------+--------+------+-----------+------------------------+--------------------+ -| | CH32V20x | ⚠ | | ✖ | stm32_fsdev/ch32_usbfs | Data loss possible | -| +-----------------------------+--------+------+-----------+------------------------+--------------------+ -| | CH32V305, CH32V307 | ⚠ | | ✔ | ch32_usbfs/hs | Data loss possible | -+--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ ++--------------+-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| Manufacturer | Family | Device | Host | Highspeed | Driver | Note | ++==============+=============================+========+======+===========+========================+=============================================+ +| Allwinner | F1C100s/F1C200s | ✔ | | ✔ | sunxi | musb variant | ++--------------+-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| Analog | MAX3421E | | ✔ | ✖ | max3421 | via SPI | +| +-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | MAX32 650, 666, 690, | ✔ | | ✔ | musb | 1-dir ep | +| | MAX78002 | | | | | | ++--------------+-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| Artery AT32 | F403a_407, F413 | ✔ | | | fsdev | 512 USB RAM | +| +-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | F415, F435_437, F423, | ✔ | ✔ | | dwc2 | | +| | F425, F45x | | | | | | +| +-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | F402_F405 | ✔ | ✔ | ✔ | dwc2 | F405 is HS | ++--------------+-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| Bridgetek | FT90x | ✔ | | ✔ | ft9xx | 1-dir ep | ++--------------+-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| Broadcom | BCM2711, BCM2837 | ✔ | | ✔ | dwc2 | | ++--------------+-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| Dialog | DA1469x | ✔ | ✖ | ✖ | da146xx | | ++--------------+-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| Espressif | S2, S3, H4 | ✔ | ✔ | ✖ | dwc2 | | +| ESP32 +-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | P4 | ✔ | ✔ | ✔ | dwc2 | | +| +-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | S31 | ✔ | ✔ | ✔ | dwc2 | | ++--------------+-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| GigaDevice | GD32VF103 | ✔ | | ✖ | dwc2 | | ++--------------+-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| HPMicro | HPM6750 | ✔ | ✔ | ✔ | ci_hs, ehci | | ++--------------+-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| Infineon | XMC4500 | ✔ | ✔ | ✖ | dwc2 | | ++--------------+-----+-----------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| MicroChip | SAM | D11, D21, L21, L22 | ✔ | | ✖ | samd | | +| | +-----------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | | D51, E5x | ✔ | | ✖ | samd | | +| | +-----------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | | G55 | ✔ | | ✖ | samg | 1-dir ep | +| | +-----------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | | E70,S70,V70,V71 | ✔ | | ✔ | samx7x | 1-dir ep | +| +-----+-----------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | PIC | 24 | ✔ | | | pic | ci_fs variant | +| | +-----------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | | 32 mm, mk, mx | ✔ | | | pic | ci_fs variant | +| | +-----------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | | dsPIC33 | ✔ | | | pic | ci_fs variant | +| | +-----------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | | 32mz | ✔ | | | pic32mz | musb variant | ++--------------+-----+-----------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| MindMotion | mm32 | ✔ | | ✖ | mm32f327x_otg | ci_fs variant | ++--------------+-----+-----------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| NordicSemi | nRF52, nRF53 | ✔ | ✖ | ✖ | nrf5x | only ep8 is ISO | +| +-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | nRF54 | ✔ | ✖ | ✔ | dwc2 | | ++--------------+-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| Nuvoton | NUC120 | ✔ | ✖ | ✖ | nuc120 | | +| +-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | NUC121/NUC125, NUC126 | ✔ | ✖ | ✖ | nuc121 | | +| +-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | NUC505 | ✔ | | ✔ | nuc505 | | ++--------------+---------+-------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| NXP | iMXRT | RT 10xx, 11xx | ✔ | ✔ | ✔ | ci_hs, ehci | | +| +---------+-------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | Kinetis | KL | ✔ | ⚠ | ✖ | ci_fs, khci | | +| | +-------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | | K32L2 | ✔ | | ✖ | khci | ci_fs variant | +| +---------+-------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | LPC | 11u, 13, 15 | ✔ | ✖ | ✖ | lpc_ip3511 | | +| | +-------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | | 17, 40 | ✔ | ⚠ | ✖ | lpc17_40, ohci | | +| | +-------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | | 18, 43 | ✔ | ✔ | ✔ | ci_hs, ehci | | +| | +-------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | | 51u | ✔ | ✖ | ✖ | lpc_ip3511 | | +| | +-------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | | 54 | ⚠ | ⚠ | ✔ | lpc_ip3511, lpc_ip3516 | :ref:`NRND, read errata ` | +| | +-------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | | 55 | ✔ | ✔ | ✔ | lpc_ip3511, lpc_ip3516 | | +| +---------+-------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | MCX | N9 | ✔ | | ✔ | ci_fs, ci_hs, ehci | | +| | +-------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | | A15 | ✔ | | | ci_fs | | +| +---------+-------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | RW61x | ✔ | ✔ | ✔ | ci_hs, ehci | | ++--------------+-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| Raspberry Pi | RP2040, RP2350 | ✔ | ✔ | ✖ | rp2040, pio_usb | | ++--------------+-----+-----------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| Renesas | RX | 63N, 65N, 72N | ✔ | ✔ | ✖ | rusb2 | | +| +-----+-----------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | RA | 4M1, 4M3, 6M1 | ✔ | ✔ | ✖ | rusb2 | | +| | +-----------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | | 6M5 | ✔ | ✔ | ✔ | rusb2 | | ++--------------+-----+-----------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| Silabs | EFM32GG12 | ✔ | | ✖ | dwc2 | | ++--------------+-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| Sony | CXD56 | ✔ | ✖ | ✔ | cxd56 | | ++--------------+-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| ST STM32 | F0, F3, L0, L1, L5, WBx5 | ✔ | ✖ | ✖ | stm32_fsdev | | +| +----+------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | F1 | 102, 103 | ✔ | ✖ | ✖ | stm32_fsdev | 512 USB RAM | +| | +------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | | 105, 107 | ✔ | ✔ | ✖ | dwc2 | | +| +----+------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | F2, F4, F7, H7, H7RS | ✔ | ✔ | ✔ | dwc2 | | +| +-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | C0, G0, H5, U3 | ✔ | ✔ | ✖ | stm32_fsdev | 2KB USB RAM | +| +-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | G4 | ✔ | ✖ | ✖ | stm32_fsdev | 1KB USB RAM | +| +----+------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | L4 | 4x2, 4x3 | ✔ | ✖ | ✖ | stm32_fsdev | 1KB USB RAM | +| | +------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | | 4x5, 4x6, 4+ | ✔ | ✔ | ✖ | dwc2 | | +| +----+------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | N6 | ✔ | ✔ | ✔ | dwc2 | | +| +-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | U0 | ✔ | ✖ | ✖ | stm32_fsdev | 1KB USB RAM | +| +----+------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | U5 | 535, 545 | ✔ | ✔ | ✖ | stm32_fsdev | 2KB USB RAM | +| | +------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | | 575, 585 | ✔ | ✔ | ✖ | dwc2 | | +| | +------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | | 59x,5Ax,5Fx,5Gx | ✔ | ✔ | ✔ | dwc2 | | ++--------------+----+------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| TI | MSP430 | ✔ | ✖ | ✖ | msp430x5xx | | +| +-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | MSP432E4, TM4C123 | ✔ | | ✖ | musb | | ++--------------+-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| ValentyUSB | eptri | ✔ | ✖ | ✖ | eptri | | ++--------------+-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| WCH | CH32F20x | ⚠ | | ✔ | ch32_usbhs | :ref:`ISO data loss ` | +| +-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | CH32V20x | ⚠ | | ✖ | stm32_fsdev/ch32_usbfs | :ref:`ISO data loss ` | +| +-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ +| | CH32V305, CH32V307 | ⚠ | | ✔ | ch32_usbfs/hs | :ref:`ISO data loss ` | ++--------------+-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ Table Legend ^^^^^^^^^^^^ diff --git a/docs/index.rst b/docs/index.rst index 39d30a038..a8762c2c2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,6 +10,7 @@ reference/index faq troubleshooting + reference/errata .. toctree:: :maxdepth: 1 diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index bb9f15166..531d471ed 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -76,6 +76,8 @@ Invalid board name in build command. Runtime Issues ============== +Check :doc:`reference/errata` for known hardware-specific issues that may affect USB functionality on your device. + Device Mode Problems -------------------- -- cgit v1.3.1 From b009ddb01232192538762f21371d65a4e6d04f14 Mon Sep 17 00:00:00 2001 From: hathach Date: Tue, 2 Jun 2026 10:38:09 +0700 Subject: ci(claude): enable @claude to fix bugs and commit from comments Configure the @claude summon workflow so it can actually produce a verified fix when asked in an issue/PR comment: - use_commit_signing: bot commits show as Verified - --allowedTools Bash: lets Claude build/test to verify the fix before committing (default allowlist blocks Bash). Safe because the job `if` gate restricts this to OWNER/MEMBER/COLLABORATOR. - --max-turns 30: enough turns to investigate -> fix -> verify Auto-commit/PR is already built into claude-code-action and the required write permissions were already present, so no permission changes are needed. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/claude.yml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index bf7a401e4..9e7c8335f 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -50,10 +50,15 @@ jobs: additional_permissions: | actions: read - # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. - # prompt: 'Update the pull request description to include a summary of changes.' + # Sign the bot's commits so they show as "Verified". The action commits + # automatically — on a PR comment it pushes to that PR's branch; on an + # issue comment it opens a new claude/* branch + PR with the fix. + use_commit_signing: true - # Optional: Add claude_args to customize behavior and configuration - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://code.claude.com/docs/en/cli-reference for available options - # claude_args: '--allowed-tools Bash(gh pr:*)' + # No custom prompt: Claude performs the instructions in the @claude comment. + + # Let summoned runs actually fix bugs: allow Bash so Claude can build/test + # and verify the change before it commits, plus enough turns to investigate. + # File edits (Edit/Write) and git push are handled by the action itself. + # Safe because the job `if` gate restricts this to OWNER/MEMBER/COLLABORATOR. + claude_args: '--allowedTools Bash --max-turns 30' -- cgit v1.3.1 From 6936cc630dfc0d125337e3f4f6e9322b503df3b2 Mon Sep 17 00:00:00 2001 From: hathach Date: Tue, 2 Jun 2026 10:42:39 +0700 Subject: ci(claude): scope Bash allowlist instead of wide-open (Codex P1) Codex flagged that @claude can be summoned on a fork PR (the review workflow even directs fork PRs here), so the checked-out PR content is potentially attacker-controlled. Unrestricted Bash in this write-token + OAuth-secret job let prompt injection steer Claude into arbitrary shell/network commands. Scope Bash to the repo's actual verification commands (cmake, ninja, make, ctest, python/python3, pre-commit, clang-format, codespell, git). This blocks the injection-to-arbitrary-command path while still letting Claude build/test before committing. Building fork code itself is already done by the existing CircleCI, so that surface is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/claude.yml | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 9e7c8335f..66a1098ab 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -57,8 +57,18 @@ jobs: # No custom prompt: Claude performs the instructions in the @claude comment. - # Let summoned runs actually fix bugs: allow Bash so Claude can build/test - # and verify the change before it commits, plus enough turns to investigate. - # File edits (Edit/Write) and git push are handled by the action itself. - # Safe because the job `if` gate restricts this to OWNER/MEMBER/COLLABORATOR. - claude_args: '--allowedTools Bash --max-turns 30' + # Let summoned runs actually fix bugs: allow the repo's build/test/lint + # commands so Claude can verify the change before it commits, plus enough + # turns to investigate. File edits (Edit/Write) and git push are handled + # by the action itself. + # + # Bash is scoped to a curated allowlist rather than wide-open: the job `if` + # gate trusts the *commenter*, but @claude can be summoned on a fork PR + # (claude-code-review.yml even directs fork PRs here), so the checked-out + # PR content is potentially attacker-controlled. Scoping blocks prompt + # injection from steering Claude into arbitrary shell/network commands + # while this job holds the OAuth secret + write token. Keep `bash`/`sh`/ + # `curl`/`wget`/`eval` OUT of this list. + claude_args: >- + --allowedTools "Bash(git:*),Bash(cmake:*),Bash(ninja:*),Bash(make:*),Bash(ctest:*),Bash(python3:*),Bash(python:*),Bash(pre-commit:*),Bash(clang-format:*),Bash(codespell:*)" + --max-turns 30 -- cgit v1.3.1 From 2fc46b690996f776e3b36bcaa9865e7d2a8f8c89 Mon Sep 17 00:00:00 2001 From: hathach Date: Tue, 2 Jun 2026 11:18:14 +0700 Subject: ci(claude): drop Bash allowlist entirely (Codex P1, round 2) Codex correctly noted that allowing python3/python (and really cmake/make too) is arbitrary code + network execution: a command allowlist cannot contain a prompt-injected or malicious fork PR when this job holds the OAuth secret + write token, and the review workflow directs fork PRs to @claude. The Bash allowlist was beyond the original scope (auto-commit/PR) anyway. Remove it: Claude edits files and the action commits/opens the PR, and the resulting commit is verified by the existing CircleCI matrix. Keep use_commit_signing and --max-turns 30. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/claude.yml | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 66a1098ab..66e36897c 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -57,18 +57,12 @@ jobs: # No custom prompt: Claude performs the instructions in the @claude comment. - # Let summoned runs actually fix bugs: allow the repo's build/test/lint - # commands so Claude can verify the change before it commits, plus enough - # turns to investigate. File edits (Edit/Write) and git push are handled - # by the action itself. - # - # Bash is scoped to a curated allowlist rather than wide-open: the job `if` - # gate trusts the *commenter*, but @claude can be summoned on a fork PR - # (claude-code-review.yml even directs fork PRs here), so the checked-out - # PR content is potentially attacker-controlled. Scoping blocks prompt - # injection from steering Claude into arbitrary shell/network commands - # while this job holds the OAuth secret + write token. Keep `bash`/`sh`/ - # `curl`/`wget`/`eval` OUT of this list. - claude_args: >- - --allowedTools "Bash(git:*),Bash(cmake:*),Bash(ninja:*),Bash(make:*),Bash(ctest:*),Bash(python3:*),Bash(python:*),Bash(pre-commit:*),Bash(clang-format:*),Bash(codespell:*)" - --max-turns 30 + # Deliberately NO Bash in the tool allowlist. @claude can be summoned on a + # fork PR (claude-code-review.yml even directs fork PRs here), and this job + # holds the OAuth secret + a write token. Any build/interpreter command + # (python -c, cmake/make custom targets, etc.) run against attacker- + # controlled PR content is arbitrary code + network execution, so no + # command allowlist can safely contain it. Claude still edits files and + # the action commits/opens the PR; the resulting commit is verified by the + # repo's CircleCI matrix. --max-turns gives room to investigate + fix. + claude_args: '--max-turns 30' -- cgit v1.3.1 From 72006885c7a2fd20d123536d25db9fe1f154d9b3 Mon Sep 17 00:00:00 2001 From: hathach Date: Tue, 2 Jun 2026 15:24:31 +0700 Subject: add sponsor section --- README.rst | 70 +++++++++++++++++++++++++++++++++++-------- docs/assets/adafruit_logo.svg | 6 ++-- docs/assets/stack.svg | 2 +- 3 files changed, 62 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index 04998abaa..ea33a2a53 100644 --- a/README.rst +++ b/README.rst @@ -1,12 +1,32 @@ TinyUSB ======= -|Build Status| |CircleCI Status| |Documentation Status| |Static Analysis| |Fuzzing Status| |Membrowse| |License| +|Sponsor| |Build Status| |CircleCI Status| |Documentation Status| |Static Analysis| |Fuzzing Status| |Membrowse| |License| + +Overview +-------- + +.. figure:: docs/assets/logo.svg + :alt: TinyUSB + :align: left + +.. raw:: html + +
+ +TinyUSB is an open-source cross-platform USB Host/Device stack for embedded systems. It’s designed for memory safety +(no dynamic allocation) and thread safety (all interrupts deferred to non-ISR task functions). The stack emphasizes portability, +small footprint, and real-time performance across 50+ MCU families. Sponsors -------- -TinyUSB is funded by: Adafruit. Purchasing products from them helps to support this project. +TinyUSB's continued development is made possible by its sponsors. If TinyUSB helps your products or projects, consider funding its development on `GitHub Sponsors`_. + +Platinum +^^^^^^^^ + +`Adafruit`_ has backed TinyUSB since day one as its founding and sole Platinum sponsor. Buying their products directly funds the project's development. .. figure:: docs/assets/adafruit_logo.svg :alt: Adafruit Logo @@ -17,20 +37,44 @@ TinyUSB is funded by: Adafruit. Purchasing products from them helps to support t
-Overview --------- +Sponsors (QWORD) +^^^^^^^^^^^^^^^^ -.. figure:: docs/assets/logo.svg - :alt: TinyUSB - :align: left +.. QWORD-SPONSORS-START -.. raw:: html +*No QWORD sponsors yet — be the first!* -
+.. QWORD-SPONSORS-END -TinyUSB is an open-source cross-platform USB Host/Device stack for embedded systems. It’s designed for memory safety -(no dynamic allocation) and thread safety (all interrupts deferred to non-ISR task functions). The stack emphasizes portability, -small footprint, and real-time performance across 50+ MCU families. +Backers (DWORD) +^^^^^^^^^^^^^^^ + +.. DWORD-BACKERS-START + +*No backers yet — be the first!* + +.. DWORD-BACKERS-END + +Supporters (Word) +^^^^^^^^^^^^^^^^^ + +.. WORD-SUPPORTERS-START + +*No supporters yet — be the first!* + +.. WORD-SUPPORTERS-END + +Thanks (Byte) +^^^^^^^^^^^^^ + +.. BYTE-THANKS-START + +*No names listed yet — be the first!* + +.. BYTE-THANKS-END + +.. _GitHub Sponsors: https://github.com/sponsors/hathach +.. _Adafruit: https://www.adafruit.com Key Features ------------ @@ -303,6 +347,8 @@ The following tools are provided freely to support the development of the TinyUS :target: https://membrowse.com/public/hathach/tinyusb .. |License| image:: https://img.shields.io/badge/license-MIT-brightgreen.svg :target: https://opensource.org/licenses/MIT +.. |Sponsor| image:: https://img.shields.io/badge/sponsor-%E2%9D%A4-ec6cb9.svg + :target: https://github.com/sponsors/hathach .. _Changelog: docs/info/changelog.rst diff --git a/docs/assets/adafruit_logo.svg b/docs/assets/adafruit_logo.svg index cafd5a10e..e978f2c6e 100644 --- a/docs/assets/adafruit_logo.svg +++ b/docs/assets/adafruit_logo.svg @@ -1,8 +1,8 @@ - + - + diff --git a/docs/assets/stack.svg b/docs/assets/stack.svg index ed46c8649..113c92b60 100644 --- a/docs/assets/stack.svg +++ b/docs/assets/stack.svg @@ -1 +1 @@ - + -- cgit v1.3.1 From e35b070dae92fd2750b8116a6ca0f1de393c7a6a Mon Sep 17 00:00:00 2001 From: HiFiPhile Date: Tue, 2 Jun 2026 10:41:27 +0200 Subject: rename to device issues Signed-off-by: HiFiPhile --- README.rst | 8 ++++---- docs/index.rst | 2 +- docs/reference/device_issues.rst | 39 +++++++++++++++++++++++++++++++++++++++ docs/reference/index.rst | 1 + docs/troubleshooting.rst | 2 +- 5 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 docs/reference/device_issues.rst diff --git a/README.rst b/README.rst index c376599ec..e039c2b61 100644 --- a/README.rst +++ b/README.rst @@ -207,7 +207,7 @@ Supported CPUs | | +-------------------+--------+------+-----------+------------------------+---------------------------------------------+ | | | 51u | ✔ | ✖ | ✖ | lpc_ip3511 | | | | +-------------------+--------+------+-----------+------------------------+---------------------------------------------+ -| | | 54 | ⚠ | ⚠ | ✔ | lpc_ip3511, lpc_ip3516 | :ref:`NRND, read errata ` | +| | | 54 | ⚠ | ⚠ | ✔ | lpc_ip3511, lpc_ip3516 | :ref:`NRND, read errata ` | | | +-------------------+--------+------+-----------+------------------------+---------------------------------------------+ | | | 55 | ✔ | ✔ | ✔ | lpc_ip3511, lpc_ip3516 | | | +---------+-------------------+--------+------+-----------+------------------------+---------------------------------------------+ @@ -261,11 +261,11 @@ Supported CPUs +--------------+-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ | ValentyUSB | eptri | ✔ | ✖ | ✖ | eptri | | +--------------+-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ -| WCH | CH32F20x | ⚠ | | ✔ | ch32_usbhs | :ref:`ISO data loss ` | +| WCH | CH32F20x | ⚠ | | ✔ | ch32_usbhs | :ref:`ISO data loss ` | | +-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ -| | CH32V20x | ⚠ | | ✖ | stm32_fsdev/ch32_usbfs | :ref:`ISO data loss ` | +| | CH32V20x | ⚠ | | ✖ | stm32_fsdev/ch32_usbfs | :ref:`ISO data loss ` | | +-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ -| | CH32V305, CH32V307 | ⚠ | | ✔ | ch32_usbfs/hs | :ref:`ISO data loss ` | +| | CH32V305, CH32V307 | ⚠ | | ✔ | ch32_usbfs/hs | :ref:`ISO data loss ` | +--------------+-----------------------------+--------+------+-----------+------------------------+---------------------------------------------+ Table Legend diff --git a/docs/index.rst b/docs/index.rst index a8762c2c2..b80804ad4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,7 +10,7 @@ reference/index faq troubleshooting - reference/errata + reference/device_issues .. toctree:: :maxdepth: 1 diff --git a/docs/reference/device_issues.rst b/docs/reference/device_issues.rst new file mode 100644 index 000000000..22677f967 --- /dev/null +++ b/docs/reference/device_issues.rst @@ -0,0 +1,39 @@ +Device specific known issues and workarounds +=============================================== +This page lists known issues and workarounds for specific devices. + +.. _LPC54600 Issues: + +NXP LPC54600 +---------------- +**Severity: High** + +**Not recommended for USB device applications (except high-speed host controller)** + +Reference: `LPC54600 Errata Sheet`_ + +.. _LPC54600 Errata Sheet: https://www.nxp.com/docs/en/errata/ES_LPC546XX.pdf + +The LPC54600 series have a very buggy USB controller, with totally 17 issues listed in the errata which is more than half of the total issues. + +Most severe issues are: + +- USB.2: In USB high-speed device mode, the NBytes field is not correct after BULK IN transfer +- USB.5: In USB full-speed host mode, linked list on done queue is broken. +- USB.15: USB high-speed device in endpoint TX data corruption + +.. _WCH CH32X Issues: + +WCH CH32V10X/CH32V20X/CH32V30X +--------------------------------- +**Severity: Medium** + +**Not recommended for USB audio applications** + +Reference: `CH32V30X Reference Manual`_ USBFS/USBHS controller chapter + +.. _CH32V30X Reference Manual: https://www.wch-ic.com/downloads/CH32FV2x_V3xRM_PDF.html + +Data corruption may occur on isochronous endpoints. Due to the lacking of FIFO for interrupt status registers, later completed transfer will overwrite `INT_ST` and `RX_LEN` register if previous transfer processing is not completed. + +Other types of transfers are not affected. diff --git a/docs/reference/index.rst b/docs/reference/index.rst index d3c96eeee..fe504627d 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -12,4 +12,5 @@ Complete reference documentation for TinyUSB APIs, configuration, and supported boards dependencies concurrency + device_issues glossary diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 531d471ed..613c7fcef 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -76,7 +76,7 @@ Invalid board name in build command. Runtime Issues ============== -Check :doc:`reference/errata` for known hardware-specific issues that may affect USB functionality on your device. +Check :doc:`reference/device_issues` for known hardware-specific issues that may affect USB functionality on your device. Device Mode Problems -------------------- -- cgit v1.3.1 From 22c2ece872262c1331482f8e80881ab1f5f36c0b Mon Sep 17 00:00:00 2001 From: HiFiPhile Date: Tue, 2 Jun 2026 11:10:15 +0200 Subject: try to get render right Signed-off-by: HiFiPhile --- README.rst | 8 ++++---- docs/conf.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index fac098c1c..ecfbeb7cc 100644 --- a/README.rst +++ b/README.rst @@ -251,7 +251,7 @@ Supported CPUs | | +-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ | | | 51u | ✔ | ✖ | ✖ | lpc_ip3511 | | | | +-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | | 54 | ⚠ | ⚠ | ✔ | lpc_ip3511, lpc_ip3516 | `NRND, read errata `_ | +| | | 54 | ⚠ | ⚠ | ✔ | lpc_ip3511, lpc_ip3516 | `NRND, read errata `_ | | | +-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ | | | 55 | ✔ | ✔ | ✔ | lpc_ip3511, lpc_ip3516 | | | +---------+-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ @@ -305,11 +305,11 @@ Supported CPUs +--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ | ValentyUSB | eptri | ✔ | ✖ | ✖ | eptri | | +--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| WCH | CH32F20x | ⚠ | | ✔ | ch32_usbhs | `ISO data loss `_ | +| WCH | CH32F20x | ⚠ | | ✔ | ch32_usbhs | `ISO data loss `_ | | +-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | CH32V20x | ⚠ | | ✖ | stm32_fsdev/ch32_usbfs | `ISO data loss `_ | +| | CH32V20x | ⚠ | | ✖ | stm32_fsdev/ch32_usbfs | `ISO data loss `_ | | +-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | CH32V305, CH32V307 | ⚠ | | ✔ | ch32_usbfs/hs | `ISO data loss `_ | +| | CH32V305, CH32V307 | ⚠ | | ✔ | ch32_usbfs/hs | `ISO data loss `_ | +--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ Table Legend diff --git a/docs/conf.py b/docs/conf.py index 9e9784fb7..86ddcf672 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,6 +53,25 @@ def preprocess_readme(): tgt = Path(__file__).parent.parent / "README_processed.rst" if src.exists(): content = src.read_text(encoding='utf-8') + # if the matching is inside a table, keep the table cell width by adding the same number of spaces in the end of the line + # match pattern: | ... `... `_ ... | + # change into: | ... `... <...>`_ ... | + def _rewrite_table_line(line): + if not (line.startswith('|') and line.rstrip().endswith('|')): + return line + + rewritten = re.sub(r"]+)>", r"<\1>", line) + delta = len(line) - len(rewritten) - 1 # -1 for rst->html + + if delta > 0: + last_pipe = rewritten.rfind('|') + if last_pipe >= 0: + rewritten = rewritten[:last_pipe] + (' ' * delta) + rewritten[last_pipe:] + + return rewritten + + content = ''.join(_rewrite_table_line(line) for line in content.splitlines(keepends=True)) + content = re.sub(r"docs/", r"", content) content = re.sub(r"\.rst\b", r".html", content) if not content.endswith("\n"): -- cgit v1.3.1 From ff3fef931dd2be604fd525814654d1cfcd7c725c Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:01:23 +0000 Subject: docs: fix review issues in device_issues and README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove duplicate toctree entry for reference/device_issues from docs/index.rst (already included via docs/reference/index.rst) - Fix WCH section title: CH32V10X/CH32V20X/CH32V30X → CH32F20x/CH32V20x/CH32V30x to match the three actual README table entries (CH32F20x, CH32V20x, CH32V305/307) - Update README anchor links to match the renamed section - Qualify USBFS non-ISO transfer safety claim: USBHS is protected by USBHS_INT_BUSY_EN but USBFS behavior is not yet confirmed - Fix LPC54600 note: "read errata" → "see device issues" - Remove "totally" from LPC54600 description Co-authored-by: Ha Thach --- README.rst | 8 ++++---- docs/index.rst | 1 - docs/reference/device_issues.rst | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index ecfbeb7cc..67149a4f2 100644 --- a/README.rst +++ b/README.rst @@ -251,7 +251,7 @@ Supported CPUs | | +-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ | | | 51u | ✔ | ✖ | ✖ | lpc_ip3511 | | | | +-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | | 54 | ⚠ | ⚠ | ✔ | lpc_ip3511, lpc_ip3516 | `NRND, read errata `_ | +| | | 54 | ⚠ | ⚠ | ✔ | lpc_ip3511, lpc_ip3516 | `NRND, see device issues `_ | | | +-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ | | | 55 | ✔ | ✔ | ✔ | lpc_ip3511, lpc_ip3516 | | | +---------+-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ @@ -305,11 +305,11 @@ Supported CPUs +--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ | ValentyUSB | eptri | ✔ | ✖ | ✖ | eptri | | +--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| WCH | CH32F20x | ⚠ | | ✔ | ch32_usbhs | `ISO data loss `_ | +| WCH | CH32F20x | ⚠ | | ✔ | ch32_usbhs | `ISO data loss `_ | | +-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | CH32V20x | ⚠ | | ✖ | stm32_fsdev/ch32_usbfs | `ISO data loss `_ | +| | CH32V20x | ⚠ | | ✖ | stm32_fsdev/ch32_usbfs | `ISO data loss `_ | | +-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | CH32V305, CH32V307 | ⚠ | | ✔ | ch32_usbfs/hs | `ISO data loss `_ | +| | CH32V305, CH32V307 | ⚠ | | ✔ | ch32_usbfs/hs | `ISO data loss `_ | +--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ Table Legend diff --git a/docs/index.rst b/docs/index.rst index b80804ad4..39d30a038 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,7 +10,6 @@ reference/index faq troubleshooting - reference/device_issues .. toctree:: :maxdepth: 1 diff --git a/docs/reference/device_issues.rst b/docs/reference/device_issues.rst index a69f6ba80..6dafe4de2 100644 --- a/docs/reference/device_issues.rst +++ b/docs/reference/device_issues.rst @@ -12,7 +12,7 @@ Reference: `LPC54600 Errata Sheet`_ .. _LPC54600 Errata Sheet: https://www.nxp.com/docs/en/errata/ES_LPC546XX.pdf -The LPC54600 series have a very buggy USB controller, with totally 17 issues listed in the errata which is more than half of the total issues. +The LPC54600 series have a very buggy USB controller, with 17 issues listed in the errata which is more than half of the total issues. Most severe issues are: @@ -20,7 +20,7 @@ Most severe issues are: - USB.5: In USB full-speed host mode, linked list on done queue is broken. - USB.15: USB high-speed device in endpoint TX data corruption -WCH CH32V10X/CH32V20X/CH32V30X +WCH CH32F20x/CH32V20x/CH32V30x --------------------------------- **Severity: Medium** @@ -32,4 +32,4 @@ Reference: `CH32V30X Reference Manual`_ USBFS/USBHS controller chapter Data corruption may occur on isochronous endpoints. Due to the lacking of FIFO for interrupt status registers, later completed transfer will overwrite `INT_ST` and `RX_LEN` register if previous transfer processing is not completed. -Other types of transfers are not affected. +For USBHS devices (CH32F20x, CH32V305/CH32V307), other transfer types are protected by the ``USBHS_INT_BUSY_EN`` mechanism which holds off new transfers while an interrupt is being processed. USBFS device behavior is not fully confirmed. -- cgit v1.3.1 From f19c01216fc5fc0f07789932df1db6fde82f525b Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:59:14 +0000 Subject: docs: revert USBFS non-ISO transfer qualification in device_issues Restore original "Other types of transfers are not affected" statement, removing the conditional USBHS_INT_BUSY_EN / USBFS-unconfirmed wording. Co-authored-by: Zixun LI --- docs/reference/device_issues.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/device_issues.rst b/docs/reference/device_issues.rst index 6dafe4de2..ae9cd55f1 100644 --- a/docs/reference/device_issues.rst +++ b/docs/reference/device_issues.rst @@ -32,4 +32,4 @@ Reference: `CH32V30X Reference Manual`_ USBFS/USBHS controller chapter Data corruption may occur on isochronous endpoints. Due to the lacking of FIFO for interrupt status registers, later completed transfer will overwrite `INT_ST` and `RX_LEN` register if previous transfer processing is not completed. -For USBHS devices (CH32F20x, CH32V305/CH32V307), other transfer types are protected by the ``USBHS_INT_BUSY_EN`` mechanism which holds off new transfers while an interrupt is being processed. USBFS device behavior is not fully confirmed. +Other types of transfers are not affected. -- cgit v1.3.1 From cc979da5163d0d407e27d90411a6d4b1a0092779 Mon Sep 17 00:00:00 2001 From: HiFiPhile Date: Tue, 2 Jun 2026 22:38:57 +0200 Subject: dcd/dwc2: fix back-to-back SETUP reception in DMA mode Signed-off-by: HiFiPhile --- src/portable/synopsys/dwc2/dcd_dwc2.c | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/portable/synopsys/dwc2/dcd_dwc2.c b/src/portable/synopsys/dwc2/dcd_dwc2.c index c90429a15..e6d7dc08e 100644 --- a/src/portable/synopsys/dwc2/dcd_dwc2.c +++ b/src/portable/synopsys/dwc2/dcd_dwc2.c @@ -73,9 +73,8 @@ typedef struct { static dcd_data_t _dcd_data; -CFG_TUD_MEM_SECTION static union { - TUD_EPBUF_DEF(setup_buffer, 8); - tusb_control_request_t setup_packet; +CFG_TUD_MEM_SECTION static struct { + TUD_EPBUF_DEF(setup_buffer, 24); } _dcd_usbbuf; static tud_configure_dwc2_t _tud_cfg = CFG_TUD_CONFIGURE_DWC2_DEFAULT; @@ -137,8 +136,8 @@ static void dma_setup_prepare(uint8_t rhport) { } } - // Receive only 1 packet - dwc2->epout[0].doeptsiz = (1 << DOEPTSIZ_STUPCNT_Pos) | (1 << DOEPTSIZ_PKTCNT_Pos) | (8 << DOEPTSIZ_XFRSIZ_Pos); + // Receive back-to-back setup packets + dwc2->epout[0].doeptsiz = (3 << DOEPTSIZ_STUPCNT_Pos); dwc2->epout[0].doepdma = (uintptr_t) _dcd_usbbuf.setup_buffer; dwc2->epout[0].doepctl |= DOEPCTL_EPENA | DOEPCTL_USBAEP; } @@ -1003,15 +1002,19 @@ static void handle_epout_dma(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doepi if (doepint_bm.setup_phase_done) { // Cleanup previous pending EP0 IN transfer if any - dwc2_dep_t* epin0 = &DWC2_REG(rhport)->epin[0]; + dwc2_dep_t* epin0 = &dwc2->epin[0]; + dwc2_dep_t* epout0 = &dwc2->epout[0]; if (edpt_is_enabled(epin0)) { edpt_disable(rhport, 0x80, false); } - dcd_dcache_invalidate(_dcd_usbbuf.setup_buffer, 8); - dcd_event_setup_received(rhport, _dcd_usbbuf.setup_buffer, true); + + dcd_dcache_invalidate(_dcd_usbbuf.setup_buffer, sizeof(_dcd_usbbuf.setup_buffer)); + + tusb_control_request_t *setup_packet = (tusb_control_request_t *) (epout0->doepdma - 8); + dcd_event_setup_received(rhport, (uint8_t*)setup_packet, true); // Prepare EP0 for next setup if this setup has no data stage - if (_dcd_usbbuf.setup_packet.wLength == 0) { + if (setup_packet->wLength == 0) { dma_setup_prepare(rhport); } return; -- cgit v1.3.1 From d585977d9275d70a75003529136dec2f3de9a875 Mon Sep 17 00:00:00 2001 From: hathach Date: Wed, 3 Jun 2026 09:05:46 +0700 Subject: ci: allow claude[bot] pushes in code review workflow Add allowed_bots: 'claude' so that when claude[bot] pushes commits the workflow skips gracefully instead of erroring. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/claude-code-review.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 4a0e4639b..97950e888 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -21,7 +21,7 @@ jobs: permissions: contents: read pull-requests: write - issues: write # use_sticky_comment posts/updates a PR comment via the issues API + issues: write # Claude posts the review comment via the issues API id-token: write actions: read # Required for Claude to read CI results on PRs @@ -36,13 +36,17 @@ jobs: uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + # Allow claude[bot]'s own pushes to be handled gracefully (skip) instead + # of erroring out the workflow + allowed_bots: 'claude' # Pairs with the actions: read permission so Claude can read CI results additional_permissions: | actions: read plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' plugins: 'code-review@claude-code-plugins' prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' - # Reuse one comment instead of posting a new one each push - use_sticky_comment: true + # TEMPORARY: expose the full Claude transcript in the Actions log for + # debugging. Revert to remove once done. + show_full_output: true claude_args: '--max-turns 20' # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md -- cgit v1.3.1 From 9a3e32bf549c5fdf6ef7931a47b8b8e2e483d856 Mon Sep 17 00:00:00 2001 From: hathach Date: Wed, 3 Jun 2026 15:15:07 +0700 Subject: ci(claude): post sticky summary comment on code review The review workflow posted nothing when a review found no issues: with use_sticky_comment unset, the only output path was inline comments, so a clean review surfaced no comment at all on the PR. Enable use_sticky_comment so a single summary comment is posted/ updated every run, making "no issues found" results visible. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/claude-code-review.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 97950e888..91859af9d 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -44,6 +44,9 @@ jobs: actions: read plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' plugins: 'code-review@claude-code-plugins' + # Post/update a single summary comment every run, so a clean review + # ("no issues found") is still visible instead of posting nothing. + use_sticky_comment: true prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' # TEMPORARY: expose the full Claude transcript in the Actions log for # debugging. Revert to remove once done. -- cgit v1.3.1 From 73af6494cd5defc296b91da80e2f59b8af4efa70 Mon Sep 17 00:00:00 2001 From: hathach Date: Wed, 3 Jun 2026 21:44:19 +0700 Subject: dwc2: submit setup packet on SETUP_DONE and drop spurious EP0 RX_COMPLETE on core v3.10a (STM32L476) DWC2 core rev 3.10a pushes an extra EP0 RX_COMPLETE (RXFLVL PKTSTS 0x3) that is not a real OUT data completion, in two cases flagged on DOEPINT: - STPKTRX (Setup Packet Received): between SETUP_RX and SETUP_DONE - STSPHSRX (Status Phase Received, control write): after the OUT data stage when the host starts the IN status phase --- src/portable/synopsys/dwc2/dcd_dwc2.c | 64 +++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/src/portable/synopsys/dwc2/dcd_dwc2.c b/src/portable/synopsys/dwc2/dcd_dwc2.c index c90429a15..447e64479 100644 --- a/src/portable/synopsys/dwc2/dcd_dwc2.c +++ b/src/portable/synopsys/dwc2/dcd_dwc2.c @@ -794,15 +794,15 @@ static void handle_bus_reset(uint8_t rhport) { xfer_status[0][TUSB_DIR_OUT].max_size = CFG_TUD_ENDPOINT0_SIZE; xfer_status[0][TUSB_DIR_IN].max_size = CFG_TUD_ENDPOINT0_SIZE; - uint32_t oepmsk = 0; + uint32_t gintmsk = GINTMSK_OTGINT | GINTMSK_IEPINT | GINTMSK_IISOIXFRM; if(dma_device_enabled(dwc2)) { - oepmsk = GINTMSK_OEPINT; + gintmsk |= GINTMSK_OEPINT; dma_setup_prepare(rhport); } else { dwc2->epout[0].doeptsiz |= (3 << DOEPTSIZ_STUPCNT_Pos); } - dwc2->gintmsk |= GINTMSK_OTGINT | oepmsk | GINTMSK_IEPINT | GINTMSK_IISOIXFRM; + dwc2->gintmsk |= gintmsk; } static void handle_enum_done(uint8_t rhport) { @@ -886,45 +886,50 @@ static void handle_rxflvl_irq(uint8_t rhport) { dwc2_regs_t* dwc2 = DWC2_REG(rhport); const volatile uint32_t* rx_fifo = dwc2->fifo[0]; + // DWC2 v3.10a (e.g. STM32L476) emits an extra EP0 RX_COMPLETE that is NOT a real OUT data transfer completion, in two + // situations - each flagged by a DOEPINT bit set on that word: + // - DOEPINT.STPKTRX (Setup Packet Received): pushed between SETUP_RX and SETUP_DONE of every control transfer. + // - DOEPINT.STSPHSRX (Status Phase Received for control write): pushed after the OUT data stage when the host + // starts the IN status phase. + // Both are dropped in the RX_COMPLETE case below, clearing the flag (W1C) so a latched STSPHSRX + // does not block the core from emitting the next SETUP_DONE. usbd still processes the real OUT data + // and queues the IN status ZLP itself - the core does not auto-complete the control-write status. + const bool quirk_v310a = (dwc2->gsnpsid == DWC2_CORE_REV_3_10a); + // Pop control word off FIFO const dwc2_grxstsp_t grxstsp = {.value = dwc2->grxstsp}; + const uint8_t packet_status = grxstsp.packet_status; const uint8_t epnum = grxstsp.ep_ch_num; dwc2_dep_t* epout = &dwc2->epout[epnum]; - switch (grxstsp.packet_status) { + TU_LOG1("packet_status = %u, ep %u, doepint = 0x%04lX\r\n", packet_status, epnum, epout->doepint); + + switch (packet_status) { case GRXSTS_PKTSTS_GLOBAL_OUT_NAK: // Global OUT NAK: do nothing break; case GRXSTS_PKTSTS_SETUP_RX: { // Setup packet received - uint32_t* setup = (uint32_t*)(uintptr_t) _dcd_usbbuf.setup_buffer; + uint32_t * setup = (uint32_t*)(uintptr_t) _dcd_usbbuf.setup_buffer; // We can receive up to three setup packets in succession, but only the last one is valid. setup[0] = (*rx_fifo); setup[1] = (*rx_fifo); - - dwc2_dep_t* epin0 = &dwc2->epin[0]; - if (edpt_is_enabled(epin0)) { - edpt_disable(rhport, 0x80, false); - } - - // (GenID < 3.00a) Must wait SETUP_DONE before next OUT transfer, otherwise OUT data may be corrupted. - // (GenID >= 3.00a) On the other hand STUPCNT is auto reloaded and SETUP_DONE is only triggered once after bus reset. - if (dwc2->gsnpsid >= DWC2_CORE_REV_3_00a) { - dcd_event_setup_received(rhport, _dcd_usbbuf.setup_buffer, true); - } break; } - case GRXSTS_PKTSTS_SETUP_DONE: - // Setup packet done: + case GRXSTS_PKTSTS_SETUP_DONE: { + // Pop this word cause Setup interrupt + // TU_LOG1("\r\n"); epout->doeptsiz |= (3 << DOEPTSIZ_STUPCNT_Pos); - - if (dwc2->gsnpsid < DWC2_CORE_REV_3_00a) { - dcd_event_setup_received(rhport, _dcd_usbbuf.setup_buffer, true); + epout->doepint = DOEPINT_SETUP | DOEPINT_STPKTRX; // Clear SETUP interrupt, required for core to re-write this control word + if (edpt_is_enabled(&dwc2->epin[0])) { + edpt_disable(rhport, 0x80, false); } + dcd_event_setup_received(rhport, _dcd_usbbuf.setup_buffer, true); break; + } case GRXSTS_PKTSTS_RX_DATA: { // Out packet received @@ -953,7 +958,20 @@ static void handle_rxflvl_irq(uint8_t rhport) { } case GRXSTS_PKTSTS_RX_COMPLETE: { - // Out packet done + // Pop this word cause xfer complete interrupt + const uint32_t doepint = epout->doepint; + epout->doepint = DOEPINT_XFRC; + + // v3.10a quirk (see top of function): the extra RX_COMPLETE flagged with Setup Packet Received (STPKTRX) or + // Status Phase Received for control write (STSPHSRX) is not a real OUT completion. Drop it + if (quirk_v310a) { + if (doepint & (DOEPINT_STPKTRX | DOEPINT_STSPHSRX)) { + epout->doepint = DOEPINT_STPKTRX | DOEPINT_STSPHSRX; + break; + } + } + // TU_LOG1("\r\n"); + xfer_ctl_t* xfer = XFER_CTL_BASE(epnum, TUSB_DIR_OUT); if (epnum == 0 && _dcd_data.ep0_pending[TUSB_DIR_OUT] > 0) { // EP0 can only handle one packet, schedule another packet to be received. @@ -1093,6 +1111,8 @@ static void handle_ep_irq(uint8_t rhport, uint8_t dir) { #if CFG_TUD_DWC2_SLAVE_ENABLE if (dir == TUSB_DIR_IN) { handle_epin_slave(rhport, epnum, intr.diepint_bm); + } else { + // epout is handled in rxflv } #endif } -- cgit v1.3.1 From a6098c38ac3390716a6cf3046a87b13dd85fecca Mon Sep 17 00:00:00 2001 From: hathach Date: Wed, 3 Jun 2026 21:46:11 +0700 Subject: refactor(cmake): comment out unused target folder properties --- hw/bsp/family_support.cmake | 36 ++++++++++++------------ hw/bsp/stm32f7/boards/stm32f769disco/board.cmake | 1 + src/class/mtp/mtp_device.c | 11 +++----- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/hw/bsp/family_support.cmake b/hw/bsp/family_support.cmake index 2468ac43c..07d693d77 100644 --- a/hw/bsp/family_support.cmake +++ b/hw/bsp/family_support.cmake @@ -244,7 +244,7 @@ function(family_add_bloaty TARGET) COMMAND ${BLOATY_EXE} ${OPTION_LIST} $ VERBATIM) - set_property(TARGET ${TARGET}-bloaty PROPERTY FOLDER ${TARGET}-group) + #set_property(TARGET ${TARGET}-bloaty PROPERTY FOLDER ${TARGET}-group) # post build # add_custom_command(TARGET ${TARGET} POST_BUILD # COMMAND ${BLOATY_EXE} --csv ${OPTION_LIST} $ > ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_bloaty.csv @@ -265,7 +265,7 @@ function(family_add_linkermap TARGET) VERBATIM ) - set_property(TARGET ${TARGET}-linkermap PROPERTY FOLDER ${TARGET}-group) + #set_property(TARGET ${TARGET}-linkermap PROPERTY FOLDER ${TARGET}-group) # post build add_custom_command(TARGET ${TARGET} POST_BUILD @@ -347,7 +347,7 @@ echo \"$MEMBROWSE_CMD\"") COMMAND ${CMAKE_COMMAND} -E env MEMBROWSE_UPLOAD=0 bash -lc "${MEMBROWSE_PREPARE_CMD}; eval \"$MEMBROWSE_CMD\"" VERBATIM ) - set_property(TARGET ${TARGET}-membrowse PROPERTY FOLDER ${TARGET}-group) + #set_property(TARGET ${TARGET}-membrowse PROPERTY FOLDER ${TARGET}-group) add_custom_target(${TARGET}-membrowse-upload COMMAND ${CMAKE_COMMAND} -E env MEMBROWSE_UPLOAD=1 bash -lc "${MEMBROWSE_PREPARE_CMD}; eval \"$MEMBROWSE_CMD\"" @@ -359,7 +359,7 @@ echo \"$MEMBROWSE_CMD\"") endif () add_dependencies(examples-membrowse-upload ${TARGET}-membrowse-upload) - set_property(TARGET ${TARGET}-membrowse-upload PROPERTY FOLDER ${TARGET}-group) + #set_property(TARGET ${TARGET}-membrowse-upload PROPERTY FOLDER ${TARGET}-group) endif () endfunction() @@ -648,7 +648,7 @@ exit" VERBATIM ) - set_property(TARGET ${NAME_TARGET}-jlink PROPERTY FOLDER ${TARGET}-group) +# set_property(TARGET ${NAME_TARGET}-jlink PROPERTY FOLDER ${NAME_TARGET}-group) endfunction() @@ -663,7 +663,7 @@ function(family_flash_stlink TARGET) COMMAND ${STM32_PROGRAMMER_CLI} --connect port=swd --write $ --go ) - set_property(TARGET ${TARGET}-stlink PROPERTY FOLDER ${TARGET}-group) + #set_property(TARGET ${TARGET}-stlink PROPERTY FOLDER ${TARGET}-group) endfunction() @@ -678,7 +678,7 @@ function(family_flash_stflash TARGET) COMMAND ${ST_FLASH} write $/${TARGET}.bin 0x8000000 ) - set_property(TARGET ${TARGET}-stflash PROPERTY FOLDER ${TARGET}-group) + #set_property(TARGET ${TARGET}-stflash PROPERTY FOLDER ${TARGET}-group) endfunction() @@ -706,7 +706,7 @@ function(family_flash_openocd TARGET) VERBATIM ) - set_property(TARGET ${TARGET}-openocd PROPERTY FOLDER ${TARGET}-group) + #set_property(TARGET ${TARGET}-openocd PROPERTY FOLDER ${TARGET}-group) endfunction() @@ -769,7 +769,7 @@ function(family_flash_wlink_rs TARGET) COMMAND ${WLINK_RS} flash $ ) - set_property(TARGET ${TARGET}-wlink-rs PROPERTY FOLDER ${TARGET}-group) + #set_property(TARGET ${TARGET}-wlink-rs PROPERTY FOLDER ${TARGET}-group) endfunction() @@ -784,7 +784,7 @@ function(family_flash_pyocd TARGET) COMMAND ${PYOCD} flash -t ${PYOCD_TARGET} $ ) - set_property(TARGET ${TARGET}-pyocd PROPERTY FOLDER ${TARGET}-group) + #set_property(TARGET ${TARGET}-pyocd PROPERTY FOLDER ${TARGET}-group) endfunction() @@ -794,7 +794,7 @@ function(family_flash_uf2 TARGET FAMILY_ID) DEPENDS ${TARGET} COMMAND python ${UF2CONV_PY} -f ${FAMILY_ID} --deploy $/${TARGET}.uf2 ) - set_property(TARGET ${TARGET}-uf2 PROPERTY FOLDER ${TARGET}-group) + #set_property(TARGET ${TARGET}-uf2 PROPERTY FOLDER ${TARGET}-group) endfunction() @@ -810,7 +810,7 @@ function(family_flash_teensy TARGET) COMMAND ${TEENSY_CLI} --mcu=${TEENSY_MCU} -w -s $/${TARGET}.hex ) - set_property(TARGET ${TARGET}-teensy PROPERTY FOLDER ${TARGET}-group) + #set_property(TARGET ${TARGET}-teensy PROPERTY FOLDER ${TARGET}-group) endfunction() @@ -830,7 +830,7 @@ function(family_flash_nxplink TARGET) COMMAND ${LINKSERVER_PATH} flash ${NXPLINK_DEVICE} load $ ) - set_property(TARGET ${TARGET}-nxplink PROPERTY FOLDER ${TARGET}-group) + #set_property(TARGET ${TARGET}-nxplink PROPERTY FOLDER ${TARGET}-group) endfunction() @@ -845,7 +845,7 @@ function(family_flash_dfu_util TARGET OPTION) VERBATIM ) - set_property(TARGET ${TARGET}-dfu-util PROPERTY FOLDER ${TARGET}-group) + #set_property(TARGET ${TARGET}-dfu-util PROPERTY FOLDER ${TARGET}-group) endfunction() function(family_flash_msp430flasher TARGET) @@ -862,7 +862,7 @@ function(family_flash_msp430flasher TARGET) ${MSP430FLASHER} -w $/${TARGET}.hex -z [VCC] ) - set_property(TARGET ${TARGET}-msp430flasher PROPERTY FOLDER ${TARGET}-group) + #set_property(TARGET ${TARGET}-msp430flasher PROPERTY FOLDER ${TARGET}-group) endfunction() function(family_flash_rfp TARGET) @@ -880,7 +880,7 @@ function(family_flash_rfp TARGET) VERBATIM ) - set_property(TARGET ${TARGET}-rfp PROPERTY FOLDER ${TARGET}-group) + #set_property(TARGET ${TARGET}-rfp PROPERTY FOLDER ${TARGET}-group) endfunction() @@ -897,7 +897,7 @@ function(family_flash_uniflash TARGET) VERBATIM ) - set_property(TARGET ${TARGET}-uniflash PROPERTY FOLDER ${TARGET}-group) + #set_property(TARGET ${TARGET}-uniflash PROPERTY FOLDER ${TARGET}-group) endfunction() # Add flash ft9xx target need to remove kernal's ftdi_sio and bind D2XX drivers @@ -912,7 +912,7 @@ function(family_flash_ft9xx TARGET) COMMAND ${FT9XXPROG} -f $/${TARGET}.bin ) - set_property(TARGET ${TARGET}-ft9xx PROPERTY FOLDER ${TARGET}-group) + #set_property(TARGET ${TARGET}-ft9xx PROPERTY FOLDER ${TARGET}-group) endfunction() #---------------------------------- diff --git a/hw/bsp/stm32f7/boards/stm32f769disco/board.cmake b/hw/bsp/stm32f7/boards/stm32f769disco/board.cmake index 2335b869e..dbdd07e4d 100644 --- a/hw/bsp/stm32f7/boards/stm32f769disco/board.cmake +++ b/hw/bsp/stm32f7/boards/stm32f769disco/board.cmake @@ -1,5 +1,6 @@ set(MCU_VARIANT stm32f769xx) set(JLINK_DEVICE stm32f769ni) +#set(JLINK_OPTION "-USB 000778170924") set(LD_FILE_GNU ${CMAKE_CURRENT_LIST_DIR}/STM32F769ZITx_FLASH.ld) diff --git a/src/class/mtp/mtp_device.c b/src/class/mtp/mtp_device.c index fd06b4601..1f76dfcc7 100644 --- a/src/class/mtp/mtp_device.c +++ b/src/class/mtp/mtp_device.c @@ -443,15 +443,12 @@ bool mtpd_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t event, uint32_t // Check completion for IN and OUT separately bool is_complete; - - if (is_data_in) - { + if (is_data_in) { // IN completion: short packet, ZLP, or reaching total_len is_complete = (xferred_bytes == 0 || xferred_bytes < threshold || p_mtp->xferred_len >= p_mtp->total_len); - } - else - { - // OUT completion: reaching total_len or ZLP + } else { + // OUT completion: reaching total_len or ZLP only. A short packet does NOT end the phase + // (an early short packet before total_len is the cancel case, not normal completion). is_complete = (p_mtp->xferred_len >= p_mtp->total_len) || ((xferred_bytes == 0 && p_mtp->xferred_len > 0)); } -- cgit v1.3.1 From e45d5ad528e82d6d4323c5c23c6f489fe0e02dc6 Mon Sep 17 00:00:00 2001 From: hathach Date: Wed, 3 Jun 2026 23:13:17 +0700 Subject: Add STM32F407 and STM32L476 disco board configurations to tinyusb.json --- test/hil/tinyusb.json | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/hil/tinyusb.json b/test/hil/tinyusb.json index dc28df7b9..c0a35ddc2 100644 --- a/test/hil/tinyusb.json +++ b/test/hil/tinyusb.json @@ -343,6 +343,20 @@ }, "comment": "2x16 access scheme with 1KB USB SRAM" }, + { + "name": "stm32f407disco", + "uid": "30001A000647313332353735", + "tests": { + "device": true, + "host": false, + "dual": false + }, + "flasher": { + "name": "jlink", + "uid": "000773661813", + "args": "-device stm32f407vg" + } + }, { "name": "stm32f723disco", "uid": "460029001951373031313335", @@ -413,6 +427,20 @@ "args": "-f interface/stlink.cfg -f target/stm32g0x.cfg" }, "comment": "32-bit scheme, 2KB USB SRAM" + }, + { + "name": "stm32l476disco", + "uid": "3C0050001150334258343920", + "tests": { + "device": true, + "host": false, + "dual": false + }, + "flasher": { + "name": "jlink", + "uid": "777632258", + "args": "-device STM32L476VG" + } } ], "boards-skip": [ -- cgit v1.3.1 From 6b89aea9de07f537611ddffca1183a3a958a7ee4 Mon Sep 17 00:00:00 2001 From: hathach Date: Wed, 3 Jun 2026 23:18:28 +0700 Subject: dwc2: remove investigation debug logging Co-Authored-By: Claude Opus 4.8 (1M context) --- src/portable/synopsys/dwc2/dcd_dwc2.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/portable/synopsys/dwc2/dcd_dwc2.c b/src/portable/synopsys/dwc2/dcd_dwc2.c index 447e64479..ac35eb951 100644 --- a/src/portable/synopsys/dwc2/dcd_dwc2.c +++ b/src/portable/synopsys/dwc2/dcd_dwc2.c @@ -903,8 +903,6 @@ static void handle_rxflvl_irq(uint8_t rhport) { dwc2_dep_t* epout = &dwc2->epout[epnum]; - TU_LOG1("packet_status = %u, ep %u, doepint = 0x%04lX\r\n", packet_status, epnum, epout->doepint); - switch (packet_status) { case GRXSTS_PKTSTS_GLOBAL_OUT_NAK: // Global OUT NAK: do nothing @@ -921,7 +919,6 @@ static void handle_rxflvl_irq(uint8_t rhport) { case GRXSTS_PKTSTS_SETUP_DONE: { // Pop this word cause Setup interrupt - // TU_LOG1("\r\n"); epout->doeptsiz |= (3 << DOEPTSIZ_STUPCNT_Pos); epout->doepint = DOEPINT_SETUP | DOEPINT_STPKTRX; // Clear SETUP interrupt, required for core to re-write this control word if (edpt_is_enabled(&dwc2->epin[0])) { @@ -970,7 +967,6 @@ static void handle_rxflvl_irq(uint8_t rhport) { break; } } - // TU_LOG1("\r\n"); xfer_ctl_t* xfer = XFER_CTL_BASE(epnum, TUSB_DIR_OUT); if (epnum == 0 && _dcd_data.ep0_pending[TUSB_DIR_OUT] > 0) { -- cgit v1.3.1 From 1f6236ae0788e37bb833e4b018faf10fb691bbdd Mon Sep 17 00:00:00 2001 From: hathach Date: Wed, 3 Jun 2026 23:41:19 +0700 Subject: dwc2: address Copilot review (comment grammar/typo, tinyusb.json f407 dedup) Co-Authored-By: Claude Opus 4.8 (1M context) --- src/portable/synopsys/dwc2/dcd_dwc2.c | 6 +++--- test/hil/tinyusb.json | 14 -------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/src/portable/synopsys/dwc2/dcd_dwc2.c b/src/portable/synopsys/dwc2/dcd_dwc2.c index ac35eb951..bab7118dd 100644 --- a/src/portable/synopsys/dwc2/dcd_dwc2.c +++ b/src/portable/synopsys/dwc2/dcd_dwc2.c @@ -918,7 +918,7 @@ static void handle_rxflvl_irq(uint8_t rhport) { } case GRXSTS_PKTSTS_SETUP_DONE: { - // Pop this word cause Setup interrupt + // Pop this word causes the Setup interrupt epout->doeptsiz |= (3 << DOEPTSIZ_STUPCNT_Pos); epout->doepint = DOEPINT_SETUP | DOEPINT_STPKTRX; // Clear SETUP interrupt, required for core to re-write this control word if (edpt_is_enabled(&dwc2->epin[0])) { @@ -955,7 +955,7 @@ static void handle_rxflvl_irq(uint8_t rhport) { } case GRXSTS_PKTSTS_RX_COMPLETE: { - // Pop this word cause xfer complete interrupt + // Pop this word causes the xfer complete interrupt const uint32_t doepint = epout->doepint; epout->doepint = DOEPINT_XFRC; @@ -1108,7 +1108,7 @@ static void handle_ep_irq(uint8_t rhport, uint8_t dir) { if (dir == TUSB_DIR_IN) { handle_epin_slave(rhport, epnum, intr.diepint_bm); } else { - // epout is handled in rxflv + // epout is handled in handle_rxflvl_irq } #endif } diff --git a/test/hil/tinyusb.json b/test/hil/tinyusb.json index c0a35ddc2..467b7378a 100644 --- a/test/hil/tinyusb.json +++ b/test/hil/tinyusb.json @@ -477,20 +477,6 @@ "uid": "EBCA8F0670AF", "args": "" } - }, - { - "name": "stm32f407disco", - "uid": "30001A000647313332353735", - "tests": { - "device": true, - "host": false, - "dual": false - }, - "flasher": { - "name": "jlink", - "uid": "000773661813", - "args": "-device stm32f407vg" - } } ] } -- cgit v1.3.1 From 824a2d6d8567deb163ecae79bf7cccfa59159c6a Mon Sep 17 00:00:00 2001 From: hathach Date: Thu, 4 Jun 2026 12:16:13 +0700 Subject: ci(labeler): add sponsor/Adafruit tiers, owner skip, and discussion support - rename priority labels usage to Prio / Prio Top - label Adafruit members (Adafruit + Sponsor + Prio Top) and public GitHub sponsors by tier; contributors get Prio - skip sponsor/Adafruit perks for the maintainer's own issues/PRs - support discussions via the GraphQL addLabelsToLabelable mutation Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/labeler.yml | 135 +++++++++++++++++++++++++++++++++++------- 1 file changed, 112 insertions(+), 23 deletions(-) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index c3cc59d0d..1fdd24bf8 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -5,6 +5,8 @@ on: types: [opened] pull_request_target: types: [opened] + discussion: + types: [created] jobs: label-priority: @@ -12,15 +14,17 @@ jobs: permissions: issues: write pull-requests: write + discussions: write steps: - - name: Label New Issue or PR + - name: Label New Issue, PR or Discussion uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - let label = ''; + let labels = []; let username = ''; let issueOrPrNumber = 0; + let discussionNodeId = ''; if (context.eventName === 'issues') { username = context.payload.issue.user.login; @@ -28,25 +32,85 @@ jobs: } else if (context.eventName === 'pull_request_target') { username = context.payload.pull_request.user.login; issueOrPrNumber = context.payload.pull_request.number; + } else if (context.eventName === 'discussion') { + username = context.payload.discussion.user.login; + discussionNodeId = context.payload.discussion.node_id; } - // Check if an Adafruit member - try { - const adafruitResponse = await github.rest.orgs.checkMembershipForUser({ - org: 'adafruit', - username: username - }); + // Maintainer is an Adafruit member; skip the Adafruit perks for their own + // issues/PRs and treat them as a plain contributor (Prio only). + const isOwner = username.toLowerCase() === 'hathach'; - if (adafruitResponse.status === 204) { - console.log('Adafruit Member'); - label = 'Prio Urgent'; + // Check if an Adafruit member: Adafruit + Sponsor + top priority + if (!isOwner) { + try { + const adafruitResponse = await github.rest.orgs.checkMembershipForUser({ + org: 'adafruit', + username: username + }); + + if (adafruitResponse.status === 204) { + console.log('Adafruit Member'); + labels = ['Adafruit', 'Sponsor', 'Prio Top']; + } + } catch (error) { + console.log('Not an Adafruit member'); + } + } + + // Check if a public GitHub Sponsor of the repo owner. + // Word ($32) tier and up get triage priority; DWORD/QWORD ($128+) go to the top. + // Private sponsorships are not visible to GITHUB_TOKEN, so only public sponsors are detected. + if (labels.length === 0) { + try { + const result = await github.graphql(` + query($sponsorable: String!, $sponsor: String!) { + user(login: $sponsorable) { + isSponsoredBy(accountLogin: $sponsor) + sponsorshipsAsMaintainer(includePrivate: false, first: 100) { + nodes { + sponsorEntity { + ... on User { login } + ... on Organization { login } + } + tier { monthlyPriceInDollars } + } + } + } + }`, { sponsorable: context.repo.owner, sponsor: username }); + + const owner = result.user; + if (owner && owner.isSponsoredBy) { + let monthly = 0; + const nodes = (owner.sponsorshipsAsMaintainer && owner.sponsorshipsAsMaintainer.nodes) || []; + for (const node of nodes) { + const login = node.sponsorEntity && node.sponsorEntity.login; + if (login && login.toLowerCase() === username.toLowerCase()) { + monthly = (node.tier && node.tier.monthlyPriceInDollars) || 0; + break; + } + } + + if (monthly >= 128) { + console.log('Sponsor (DWORD/QWORD tier)'); + labels = ['Sponsor', 'Prio Top']; + } else if (monthly >= 32) { + console.log('Sponsor (Word tier)'); + labels = ['Sponsor', 'Prio']; + } else { + console.log('Sponsor (below Word tier or tier not visible)'); + labels = ['Sponsor']; + } + } else { + console.log('Not a public sponsor'); + } + } catch (error) { + console.log('Sponsor lookup failed: ' + error.message); } - } catch (error) { - console.log('Not an Adafruit member'); } - // Check if a contributor - if (label == '') { + // Check if a contributor: prioritized in triage queue + if (labels.length === 0) { try { const collaboratorResponse = await github.rest.repos.checkCollaborator({ owner: context.repo.owner, @@ -56,18 +120,43 @@ jobs: if (collaboratorResponse.status === 204) { console.log('Contributor'); - label = 'Prio Higher'; + labels = ['Prio']; } } catch (error) { console.log('Not a contributor'); } } - if (label !== '') { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueOrPrNumber, - labels: [label] - }); + if (labels.length !== 0) { + if (context.eventName === 'discussion') { + // Discussions are not covered by the REST issues API; resolve the label + // names to node IDs and attach them with the GraphQL labelable mutation. + const labelIds = []; + for (const name of labels) { + const res = await github.graphql(` + query($owner: String!, $repo: String!, $name: String!) { + repository(owner: $owner, name: $repo) { + label(name: $name) { id } + } + }`, { owner: context.repo.owner, repo: context.repo.repo, name: name }); + if (res.repository.label) { + labelIds.push(res.repository.label.id); + } + } + if (labelIds.length !== 0) { + await github.graphql(` + mutation($labelableId: ID!, $labelIds: [ID!]!) { + addLabelsToLabelable(input: { labelableId: $labelableId, labelIds: $labelIds }) { + clientMutationId + } + }`, { labelableId: discussionNodeId, labelIds: labelIds }); + } + } else { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueOrPrNumber, + labels: labels + }); + } } -- cgit v1.3.1 From 5e3a56a38731e21a46c9df121530644745d55685 Mon Sep 17 00:00:00 2001 From: hathach Date: Thu, 4 Jun 2026 12:16:14 +0700 Subject: ci: add Sponsor Triage board sync workflow Cron (6h) + manual workflow that adds open issues opened by GitHub sponsors (public and private) and Adafruit org members across the adafruit org and the maintainer's repos to the private Sponsor Triage project board, setting Tier and Visibility. Logs counts only to avoid leaking private sponsor logins. Needs the SPONSOR_TOKEN PAT secret. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/sponsor-triage.yml | 164 +++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 .github/workflows/sponsor-triage.yml diff --git a/.github/workflows/sponsor-triage.yml b/.github/workflows/sponsor-triage.yml new file mode 100644 index 000000000..19c1a6105 --- /dev/null +++ b/.github/workflows/sponsor-triage.yml @@ -0,0 +1,164 @@ +name: Sponsor Triage + +# Periodically add open issues opened by sponsors (public and private) to the private "Sponsor Triage" project board +# Requires a PAT in secret SPONSOR_TOKEN with scopes: +# - project (write project items / fields) +# - read:org (search org issues, check Adafruit membership) +# - read:user / sponsors (read own sponsorships incl. private) + +on: + schedule: + - cron: '0 */6 * * *' # every 6 hours + workflow_dispatch: + +concurrency: + group: sponsor-triage + cancel-in-progress: false + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Sync sponsor issues to project board + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.SPONSOR_TOKEN }} + script: | + const PROJECT_OWNER = 'hathach'; + const PROJECT_NUMBER = 3; + // Where to look for sponsor-authored open issues. + const SEARCH_SCOPES = ['org:adafruit', 'user:hathach']; + + const tierFromMonthly = (m) => { + if (m >= 512) return 'QWORD'; + if (m >= 128) return 'DWORD'; + if (m >= 32) return 'Word'; + if (m >= 8) return 'Byte'; + if (m >= 2) return 'Bit'; + return null; + }; + + // --- 1. Resolve project id + field/option ids (by name, never hardcoded) --- + const proj = await github.graphql(` + query($owner: String!, $number: Int!) { + user(login: $owner) { + projectV2(number: $number) { + id + fields(first: 50) { + nodes { + ... on ProjectV2SingleSelectField { + id name options { id name } + } + } + } + } + } + }`, { owner: PROJECT_OWNER, number: PROJECT_NUMBER }); + + const project = proj.user.projectV2; + const fieldByName = {}; + for (const f of project.fields.nodes) { + if (f && f.name) { + fieldByName[f.name] = { id: f.id, options: {} }; + for (const o of (f.options || [])) fieldByName[f.name].options[o.name] = o.id; + } + } + const tierField = fieldByName['Tier']; + const visField = fieldByName['Visibility']; + + // --- 2. Build sponsor map: loginLower -> { tier, visibility } --- + const sponsors = new Map(); + + // 2a. GitHub Sponsors of the owner, including private ones. + let after = null; + for (let page = 0; page < 20; page++) { + const res = await github.graphql(` + query($owner: String!, $after: String) { + user(login: $owner) { + sponsorshipsAsMaintainer(includePrivate: true, first: 100, after: $after) { + pageInfo { hasNextPage endCursor } + nodes { + privacyLevel + tier { monthlyPriceInDollars } + sponsorEntity { + ... on User { login } + ... on Organization { login } + } + } + } + } + }`, { owner: PROJECT_OWNER, after }); + const conn = res.user.sponsorshipsAsMaintainer; + for (const n of conn.nodes) { + const login = n.sponsorEntity && n.sponsorEntity.login; + if (!login) continue; + const tier = tierFromMonthly((n.tier && n.tier.monthlyPriceInDollars) || 0); + const visibility = n.privacyLevel === 'PRIVATE' ? 'Private' : 'Public'; + sponsors.set(login.toLowerCase(), { login, tier, visibility }); + } + if (!conn.pageInfo.hasNextPage) break; + after = conn.pageInfo.endCursor; + } + + core.info(`Resolved ${sponsors.size} sponsor account(s).`); + if (sponsors.size === 0) return; + + // 2b. Adafruit org members get the Adafruit tier regardless of $ amount. + for (const s of sponsors.values()) { + try { + const m = await github.rest.orgs.checkMembershipForUser({ org: 'adafruit', username: s.login }); + if (m.status === 204) s.tier = 'Adafruit'; + } catch (e) { /* not an Adafruit member */ } + } + + // --- 3. Find each sponsor's open issues in the search scopes --- + const found = new Map(); // contentId -> { tier, visibility } + for (const s of sponsors.values()) { + for (const scope of SEARCH_SCOPES) { + const q = `${scope} is:open is:issue author:${s.login}`; + try { + const res = await github.graphql(` + query($q: String!) { + search(query: $q, type: ISSUE, first: 100) { + nodes { ... on Issue { id } } + } + }`, { q }); + for (const node of res.search.nodes) { + if (node && node.id && !found.has(node.id)) { + found.set(node.id, { tier: s.tier, visibility: s.visibility }); + } + } + } catch (e) { + core.warning(`Search failed for one scope: ${e.message}`); + } + } + } + core.info(`Found ${found.size} open sponsor issue(s) across scopes.`); + + // --- 4. Add to board + set Tier / Visibility (idempotent) --- + const setField = async (itemId, field, optionName) => { + if (!field || !optionName) return; + const optId = field.options[optionName]; + if (!optId) return; + await github.graphql(` + mutation($p: ID!, $i: ID!, $f: ID!, $o: String!) { + updateProjectV2ItemFieldValue(input: { + projectId: $p, itemId: $i, fieldId: $f, value: { singleSelectOptionId: $o } + }) { projectV2Item { id } } + }`, { p: project.id, i: itemId, f: field.id, o: optId }); + }; + + let added = 0; + for (const [contentId, meta] of found) { + const res = await github.graphql(` + mutation($p: ID!, $c: ID!) { + addProjectV2ItemById(input: { projectId: $p, contentId: $c }) { + item { id } + } + }`, { p: project.id, c: contentId }); + const itemId = res.addProjectV2ItemById.item.id; + await setField(itemId, tierField, meta.tier); + await setField(itemId, visField, meta.visibility); + added++; + } + core.info(`Synced ${added} item(s) to the board.`); -- cgit v1.3.1 From c86fea62e72e0b75a42245b2200763922e2baaa7 Mon Sep 17 00:00:00 2001 From: hathach Date: Thu, 4 Jun 2026 12:55:32 +0700 Subject: ci(sponsor-triage): include open PRs, not just issues Drop the is:issue qualifier so sponsor pull requests are synced to the board too (search type ISSUE already returns both). A sponsor's open PR is exactly the kind of work to prioritize reviewing. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/sponsor-triage.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sponsor-triage.yml b/.github/workflows/sponsor-triage.yml index 19c1a6105..1e7d39028 100644 --- a/.github/workflows/sponsor-triage.yml +++ b/.github/workflows/sponsor-triage.yml @@ -1,6 +1,6 @@ name: Sponsor Triage -# Periodically add open issues opened by sponsors (public and private) to the private "Sponsor Triage" project board +# Periodically add open issues and PRs opened by sponsors (public and private) to the private "Sponsor Triage" project board # Requires a PAT in secret SPONSOR_TOKEN with scopes: # - project (write project items / fields) # - read:org (search org issues, check Adafruit membership) @@ -111,11 +111,12 @@ jobs: } catch (e) { /* not an Adafruit member */ } } - // --- 3. Find each sponsor's open issues in the search scopes --- + // --- 3. Find each sponsor's open issues and PRs in the search scopes --- + // (search type ISSUE returns both issues and pull requests) const found = new Map(); // contentId -> { tier, visibility } for (const s of sponsors.values()) { for (const scope of SEARCH_SCOPES) { - const q = `${scope} is:open is:issue author:${s.login}`; + const q = `${scope} is:open author:${s.login}`; try { const res = await github.graphql(` query($q: String!) { -- cgit v1.3.1 From 10e57012078f68752699e896478bcf3d99dc391c Mon Sep 17 00:00:00 2001 From: hathach Date: Thu, 4 Jun 2026 13:06:13 +0700 Subject: ci(sponsor-triage): select PullRequest id in search results The search drops is:issue to include PRs, but the GraphQL selection only had '... on Issue { id }', so PR nodes returned no id and were skipped. Add '... on PullRequest { id }'. (Codex/Copilot review finding.) Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/sponsor-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sponsor-triage.yml b/.github/workflows/sponsor-triage.yml index 1e7d39028..728503532 100644 --- a/.github/workflows/sponsor-triage.yml +++ b/.github/workflows/sponsor-triage.yml @@ -121,7 +121,7 @@ jobs: const res = await github.graphql(` query($q: String!) { search(query: $q, type: ISSUE, first: 100) { - nodes { ... on Issue { id } } + nodes { ... on Issue { id } ... on PullRequest { id } } } }`, { q }); for (const node of res.search.nodes) { -- cgit v1.3.1 From 709b33d848e953eaacf41399a65ded1724917eb7 Mon Sep 17 00:00:00 2001 From: Ha Thach Date: Thu, 4 Jun 2026 13:55:17 +0700 Subject: ci: bump actions/github-script v7 -> v8 (Node.js 24) (#3671) Node.js 20 actions are deprecated; v8 runs on Node.js 24. Co-authored-by: Claude Opus 4.8 (1M context) --- .github/workflows/labeler.yml | 2 +- .github/workflows/sponsor-triage.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 1fdd24bf8..e860c36a3 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -17,7 +17,7 @@ jobs: discussions: write steps: - name: Label New Issue, PR or Discussion - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/sponsor-triage.yml b/.github/workflows/sponsor-triage.yml index 728503532..48b2ac7cc 100644 --- a/.github/workflows/sponsor-triage.yml +++ b/.github/workflows/sponsor-triage.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Sync sponsor issues to project board - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: github-token: ${{ secrets.SPONSOR_TOKEN }} script: | -- cgit v1.3.1 From 3d0516f439cbe2c8d69a9d8dd62f7effa935d0b2 Mon Sep 17 00:00:00 2001 From: Ha Thach Date: Thu, 4 Jun 2026 14:27:54 +0700 Subject: ci(labeler): match emoji-renamed labels (#3672) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Labels were renamed to add emojis (Adafruit 🌸, Sponsor 💖, Prio 🚩, Prio Top 🚨); update the hardcoded label names in the labeler script to match so they attach to the existing labels instead of recreating plain ones. Co-authored-by: Claude Opus 4.8 (1M context) --- .github/workflows/labeler.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index e860c36a3..87d416f58 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -51,7 +51,7 @@ jobs: if (adafruitResponse.status === 204) { console.log('Adafruit Member'); - labels = ['Adafruit', 'Sponsor', 'Prio Top']; + labels = ['Adafruit 🌸', 'Sponsor 💖', 'Prio Top 🚨']; } } catch (error) { console.log('Not an Adafruit member'); @@ -93,13 +93,13 @@ jobs: if (monthly >= 128) { console.log('Sponsor (DWORD/QWORD tier)'); - labels = ['Sponsor', 'Prio Top']; + labels = ['Sponsor 💖', 'Prio Top 🚨']; } else if (monthly >= 32) { console.log('Sponsor (Word tier)'); - labels = ['Sponsor', 'Prio']; + labels = ['Sponsor 💖', 'Prio 📌']; } else { console.log('Sponsor (below Word tier or tier not visible)'); - labels = ['Sponsor']; + labels = ['Sponsor 💖']; } } else { console.log('Not a public sponsor'); @@ -120,7 +120,7 @@ jobs: if (collaboratorResponse.status === 204) { console.log('Contributor'); - labels = ['Prio']; + labels = ['Prio 📌']; } } catch (error) { console.log('Not a contributor'); -- cgit v1.3.1 From ac32feafeb2fb0f6cc5c3010d32db319ff15ca64 Mon Sep 17 00:00:00 2001 From: Ha Thach Date: Thu, 4 Jun 2026 15:18:40 +0700 Subject: ci(labeler): auto-apply Port labels from changed driver files (#3673) * ci(labeler): auto-apply Port labels from changed driver files Add path-based labeling so a PR touching a dcd/hcd driver under src/portable/ gets the matching "Port " label automatically. --- .github/labeler.yml | 77 +++++++++++++++++++++++++++++++++++++++++++ .github/workflows/labeler.yml | 22 ++++++++++++- 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 .github/labeler.yml diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000..6c7aa7e7d --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,77 @@ +# Path-based auto-labeling for USB IP / port drivers. +# Maps changed dcd/hcd files under src/portable/ to their "Port " label. +# Consumed by actions/labeler (see .github/workflows/labeler.yml -> label-port job). + +"Port DWC2": + - changed-files: + - any-glob-to-any-file: 'src/portable/synopsys/dwc2/**' + +"Port EHCI": + - changed-files: + - any-glob-to-any-file: 'src/portable/ehci/**' + +"Port OHCI": + - changed-files: + - any-glob-to-any-file: 'src/portable/ohci/**' + +"Port FSDev": + - changed-files: + - any-glob-to-any-file: 'src/portable/st/stm32_fsdev/**' + +"Port ChipIdea": + - changed-files: + - any-glob-to-any-file: 'src/portable/chipidea/**' + +"Port NXP IP3511": + - changed-files: + - any-glob-to-any-file: 'src/portable/nxp/lpc_ip3511/**' + +"Port NXP IP3516": + - changed-files: + - any-glob-to-any-file: 'src/portable/nxp/lpc_ip3516/**' + +"Port MUSB": + - changed-files: + - any-glob-to-any-file: + - 'src/portable/mentor/musb/**' + - 'src/portable/sunxi/**' + +"Port RUSB2": + - changed-files: + - any-glob-to-any-file: 'src/portable/renesas/rusb2/**' + +"Port WCH USBFS": + - changed-files: + - any-glob-to-any-file: 'src/portable/wch/*usbfs*' + +"Port WCH USBHS": + - changed-files: + - any-glob-to-any-file: 'src/portable/wch/*usbhs*' + +"Port MAX3421": + - changed-files: + - any-glob-to-any-file: 'src/portable/analog/max3421/**' + +"Port SAMD": + - changed-files: + - any-glob-to-any-file: 'src/portable/microchip/samd/**' + +"Port SAMG": + - changed-files: + - any-glob-to-any-file: 'src/portable/microchip/samg/**' + +"Port nRF": + - changed-files: + - any-glob-to-any-file: 'src/portable/nordic/nrf5x/**' + +"Port Nuvoton": + - changed-files: + - any-glob-to-any-file: 'src/portable/nuvoton/**' + +"Port RP2": + - changed-files: + - any-glob-to-any-file: 'src/portable/raspberrypi/**' + +"Port MSP430": + - changed-files: + - any-glob-to-any-file: 'src/portable/ti/msp430x5xx/**' diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 87d416f58..fe09413f1 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -4,12 +4,14 @@ on: issues: types: [opened] pull_request_target: - types: [opened] + types: [opened, synchronize, reopened] discussion: types: [created] jobs: label-priority: + # Author-based priority labels: only on issue/PR/discussion creation, not on PR updates. + if: github.event_name != 'pull_request_target' || github.event.action == 'opened' runs-on: ubuntu-latest permissions: issues: write @@ -160,3 +162,21 @@ jobs: }); } } + + # Path-based Port labels: attach "Port " when a PR touches the matching + # dcd/hcd driver under src/portable/. Mapping lives in .github/labeler.yml. + label-port: + if: github.event_name == 'pull_request_target' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write # allow auto-creating a Port label that doesn't exist yet + steps: + - uses: actions/labeler@v5 + with: + configuration-path: .github/labeler.yml + # sync-labels so a Port label is removed once a PR no longer touches + # that driver (job reruns on synchronize). Only labels listed in + # labeler.yml are managed, so author-based Prio/Sponsor labels are untouched. + sync-labels: true -- cgit v1.3.1 From b5e080732e67a52afefa6966ca33474c91a53d0c Mon Sep 17 00:00:00 2001 From: hathach Date: Thu, 4 Jun 2026 17:55:44 +0700 Subject: Update setup buffer size definition based on DMA configuration --- src/portable/synopsys/dwc2/dcd_dwc2.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/portable/synopsys/dwc2/dcd_dwc2.c b/src/portable/synopsys/dwc2/dcd_dwc2.c index c7899a354..ee52ef1e7 100644 --- a/src/portable/synopsys/dwc2/dcd_dwc2.c +++ b/src/portable/synopsys/dwc2/dcd_dwc2.c @@ -73,8 +73,15 @@ typedef struct { static dcd_data_t _dcd_data; +// DMA receives up to 3 back-to-back SETUP packets (3 x 8 bytes), Slave mode only needs 1 packet (8 bytes) +#if CFG_TUD_DWC2_DMA_ENABLE + #define DWC2_SETUP_BUFFER_SIZE 24 +#else + #define DWC2_SETUP_BUFFER_SIZE 8 +#endif + CFG_TUD_MEM_SECTION static struct { - TUD_EPBUF_DEF(setup_buffer, 24); + TUD_EPBUF_DEF(setup_buffer, DWC2_SETUP_BUFFER_SIZE); } _dcd_usbbuf; static tud_configure_dwc2_t _tud_cfg = CFG_TUD_CONFIGURE_DWC2_DEFAULT; -- cgit v1.3.1 From a900ea93db686cacde5e595dc09fdfeaa334d556 Mon Sep 17 00:00:00 2001 From: hathach Date: Thu, 4 Jun 2026 20:46:49 +0700 Subject: dwc2: cleanup setup_packet pointer cast (review feedback) Cast DOEPDMA0 through uintptr_t and use sizeof(tusb_control_request_t) instead of the magic constant 8, matching project convention. Add a reference to Programming Guide v4.20a 9.1.2.1 for the DOEPDMAn-8 rule. Addresses Copilot review comment; no functional change. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/portable/synopsys/dwc2/dcd_dwc2.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/portable/synopsys/dwc2/dcd_dwc2.c b/src/portable/synopsys/dwc2/dcd_dwc2.c index ee52ef1e7..e1a2f6cf2 100644 --- a/src/portable/synopsys/dwc2/dcd_dwc2.c +++ b/src/portable/synopsys/dwc2/dcd_dwc2.c @@ -1031,7 +1031,9 @@ static void handle_epout_dma(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doepi dcd_dcache_invalidate(_dcd_usbbuf.setup_buffer, sizeof(_dcd_usbbuf.setup_buffer)); - tusb_control_request_t *setup_packet = (tusb_control_request_t *) (epout0->doepdma - 8); + // DOEPDMA0 has advanced past the last received SETUP packet; back up one packet to the latest valid one + // (Programming Guide v4.20a section 9.1.2.1: "DOEPDMAn-8 provides the pointer to the last valid SETUP data") + tusb_control_request_t *setup_packet = (tusb_control_request_t *) (uintptr_t) (epout0->doepdma - sizeof(tusb_control_request_t)); dcd_event_setup_received(rhport, (uint8_t*)setup_packet, true); // Prepare EP0 for next setup if this setup has no data stage -- cgit v1.3.1 From ddc065dc9fd929fcc94b0993c3af4b56877b1cd6 Mon Sep 17 00:00:00 2001 From: Ha Thach Date: Thu, 4 Jun 2026 21:59:59 +0700 Subject: Remove Sponsor Triage workflow (migrated to hathach/hathach) (#3675) This personal automation now lives in the hathach/hathach repo alongside the other personal project-sync workflows; it has no place in the tinyusb library. Co-authored-by: Claude Opus 4.8 (1M context) --- .github/workflows/sponsor-triage.yml | 165 ----------------------------------- 1 file changed, 165 deletions(-) delete mode 100644 .github/workflows/sponsor-triage.yml diff --git a/.github/workflows/sponsor-triage.yml b/.github/workflows/sponsor-triage.yml deleted file mode 100644 index 48b2ac7cc..000000000 --- a/.github/workflows/sponsor-triage.yml +++ /dev/null @@ -1,165 +0,0 @@ -name: Sponsor Triage - -# Periodically add open issues and PRs opened by sponsors (public and private) to the private "Sponsor Triage" project board -# Requires a PAT in secret SPONSOR_TOKEN with scopes: -# - project (write project items / fields) -# - read:org (search org issues, check Adafruit membership) -# - read:user / sponsors (read own sponsorships incl. private) - -on: - schedule: - - cron: '0 */6 * * *' # every 6 hours - workflow_dispatch: - -concurrency: - group: sponsor-triage - cancel-in-progress: false - -jobs: - sync: - runs-on: ubuntu-latest - steps: - - name: Sync sponsor issues to project board - uses: actions/github-script@v8 - with: - github-token: ${{ secrets.SPONSOR_TOKEN }} - script: | - const PROJECT_OWNER = 'hathach'; - const PROJECT_NUMBER = 3; - // Where to look for sponsor-authored open issues. - const SEARCH_SCOPES = ['org:adafruit', 'user:hathach']; - - const tierFromMonthly = (m) => { - if (m >= 512) return 'QWORD'; - if (m >= 128) return 'DWORD'; - if (m >= 32) return 'Word'; - if (m >= 8) return 'Byte'; - if (m >= 2) return 'Bit'; - return null; - }; - - // --- 1. Resolve project id + field/option ids (by name, never hardcoded) --- - const proj = await github.graphql(` - query($owner: String!, $number: Int!) { - user(login: $owner) { - projectV2(number: $number) { - id - fields(first: 50) { - nodes { - ... on ProjectV2SingleSelectField { - id name options { id name } - } - } - } - } - } - }`, { owner: PROJECT_OWNER, number: PROJECT_NUMBER }); - - const project = proj.user.projectV2; - const fieldByName = {}; - for (const f of project.fields.nodes) { - if (f && f.name) { - fieldByName[f.name] = { id: f.id, options: {} }; - for (const o of (f.options || [])) fieldByName[f.name].options[o.name] = o.id; - } - } - const tierField = fieldByName['Tier']; - const visField = fieldByName['Visibility']; - - // --- 2. Build sponsor map: loginLower -> { tier, visibility } --- - const sponsors = new Map(); - - // 2a. GitHub Sponsors of the owner, including private ones. - let after = null; - for (let page = 0; page < 20; page++) { - const res = await github.graphql(` - query($owner: String!, $after: String) { - user(login: $owner) { - sponsorshipsAsMaintainer(includePrivate: true, first: 100, after: $after) { - pageInfo { hasNextPage endCursor } - nodes { - privacyLevel - tier { monthlyPriceInDollars } - sponsorEntity { - ... on User { login } - ... on Organization { login } - } - } - } - } - }`, { owner: PROJECT_OWNER, after }); - const conn = res.user.sponsorshipsAsMaintainer; - for (const n of conn.nodes) { - const login = n.sponsorEntity && n.sponsorEntity.login; - if (!login) continue; - const tier = tierFromMonthly((n.tier && n.tier.monthlyPriceInDollars) || 0); - const visibility = n.privacyLevel === 'PRIVATE' ? 'Private' : 'Public'; - sponsors.set(login.toLowerCase(), { login, tier, visibility }); - } - if (!conn.pageInfo.hasNextPage) break; - after = conn.pageInfo.endCursor; - } - - core.info(`Resolved ${sponsors.size} sponsor account(s).`); - if (sponsors.size === 0) return; - - // 2b. Adafruit org members get the Adafruit tier regardless of $ amount. - for (const s of sponsors.values()) { - try { - const m = await github.rest.orgs.checkMembershipForUser({ org: 'adafruit', username: s.login }); - if (m.status === 204) s.tier = 'Adafruit'; - } catch (e) { /* not an Adafruit member */ } - } - - // --- 3. Find each sponsor's open issues and PRs in the search scopes --- - // (search type ISSUE returns both issues and pull requests) - const found = new Map(); // contentId -> { tier, visibility } - for (const s of sponsors.values()) { - for (const scope of SEARCH_SCOPES) { - const q = `${scope} is:open author:${s.login}`; - try { - const res = await github.graphql(` - query($q: String!) { - search(query: $q, type: ISSUE, first: 100) { - nodes { ... on Issue { id } ... on PullRequest { id } } - } - }`, { q }); - for (const node of res.search.nodes) { - if (node && node.id && !found.has(node.id)) { - found.set(node.id, { tier: s.tier, visibility: s.visibility }); - } - } - } catch (e) { - core.warning(`Search failed for one scope: ${e.message}`); - } - } - } - core.info(`Found ${found.size} open sponsor issue(s) across scopes.`); - - // --- 4. Add to board + set Tier / Visibility (idempotent) --- - const setField = async (itemId, field, optionName) => { - if (!field || !optionName) return; - const optId = field.options[optionName]; - if (!optId) return; - await github.graphql(` - mutation($p: ID!, $i: ID!, $f: ID!, $o: String!) { - updateProjectV2ItemFieldValue(input: { - projectId: $p, itemId: $i, fieldId: $f, value: { singleSelectOptionId: $o } - }) { projectV2Item { id } } - }`, { p: project.id, i: itemId, f: field.id, o: optId }); - }; - - let added = 0; - for (const [contentId, meta] of found) { - const res = await github.graphql(` - mutation($p: ID!, $c: ID!) { - addProjectV2ItemById(input: { projectId: $p, contentId: $c }) { - item { id } - } - }`, { p: project.id, c: contentId }); - const itemId = res.addProjectV2ItemById.item.id; - await setField(itemId, tierField, meta.tier); - await setField(itemId, visField, meta.visibility); - added++; - } - core.info(`Synced ${added} item(s) to the board.`); -- cgit v1.3.1 From 9dea2c8f397c004d3f3288bac3c0df9c842fe713 Mon Sep 17 00:00:00 2001 From: Ha Thach Date: Fri, 5 Jun 2026 14:34:27 +0700 Subject: ci: carry metrics baseline forward on no-code-change pushes (#3678) * ci: carry metrics baseline forward on no-code-change pushes The code-metrics job is gated on code_changed and only uploads the metrics-tinyusb artifact on push, so a workflow/docs-only push to master (e.g. removing an unrelated workflow) leaves the latest master Build run without a baseline. PRs download the baseline from the latest master run, so the size comparison then finds nothing and silently falls back to absolute sizes. Add a small metrics-carry-forward job that, on a non-code-change push, downloads the previous metrics-tinyusb artifact and re-publishes it, so the latest run always carries a usable baseline. Carry-forward runs re-upload too, so the baseline chains across consecutive no-code pushes (bounded by artifact retention). --- .github/workflows/build.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index af0191149..e5075f46f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -192,6 +192,36 @@ jobs: header: code-metrics path: metrics_compare.md + # --------------------------------------- + # Keep the metrics baseline available on no-code-change pushes + # The code-metrics job only runs (and uploads metrics-tinyusb) when code changed, so a + # workflow/docs-only push to master would leave the latest run without a baseline for PRs + # to compare against. Carry the previous artifact forward so the baseline is never missing. + # --------------------------------------- + metrics-carry-forward: + needs: [ check-paths ] + if: github.event_name == 'push' && needs.check-paths.outputs.code_changed != 'true' + runs-on: ubuntu-latest + steps: + - name: Download previous metrics baseline from this branch + uses: dawidd6/action-download-artifact@v11 + with: + workflow: build.yml + workflow_conclusion: '' # any conclusion, matching the PR-side baseline download + search_artifacts: true # scan back past runs that lack the artifact (e.g. earlier no-code pushes) + branch: ${{ github.ref_name }} + name: metrics-tinyusb + path: . + if_no_artifact_found: warn + continue-on-error: true # best-effort: never make a no-code push red + + - name: Re-publish baseline so the latest run keeps it + if: hashFiles('metrics.json') != '' + uses: actions/upload-artifact@v7 + with: + name: metrics-tinyusb + path: metrics.json + # --------------------------------------- # Build Make/CMake on Windows/MacOS # --------------------------------------- -- cgit v1.3.1 From 1009b14b02232adc70a0a5a5ed4a3a06118fef0d Mon Sep 17 00:00:00 2001 From: Ha Thach Date: Fri, 5 Jun 2026 21:23:40 +0700 Subject: add tm4c123x evk to hill pool (#3676) * add ek_tm4c123gxl to the hil pool, flashing with lm4flash --- hw/bsp/family_support.cmake | 20 ++++++++++++++++++++ hw/bsp/tm4c/family.cmake | 4 ++-- test/hil/hil_test.py | 13 +++++++++++++ test/hil/tinyusb.json | 14 ++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/hw/bsp/family_support.cmake b/hw/bsp/family_support.cmake index 07d693d77..af2716b28 100644 --- a/hw/bsp/family_support.cmake +++ b/hw/bsp/family_support.cmake @@ -900,6 +900,26 @@ function(family_flash_uniflash TARGET) #set_property(TARGET ${TARGET}-uniflash PROPERTY FOLDER ${TARGET}-group) endfunction() +# Add flash lm4flash target (lightweight flasher for TI Tiva-C/Stellaris ICDI boards) +function(family_flash_lm4flash TARGET) + if (NOT DEFINED LM4FLASH) + set(LM4FLASH lm4flash) + endif () + + if (NOT DEFINED LM4FLASH_OPTION) + set(LM4FLASH_OPTION "") + endif () + separate_arguments(OPTION_LIST UNIX_COMMAND ${LM4FLASH_OPTION}) + + add_custom_target(${TARGET}-lm4flash + DEPENDS ${TARGET} + COMMAND ${LM4FLASH} ${OPTION_LIST} $/${TARGET}.bin + VERBATIM + ) + + #set_property(TARGET ${TARGET}-lm4flash PROPERTY FOLDER ${TARGET}-group) +endfunction() + # Add flash ft9xx target need to remove kernal's ftdi_sio and bind D2XX drivers # sudo rmmod ftdi_sio && for i in 0 1 2 3; do sudo sh -c "echo 3-3.4:1.$i > /sys/bus/usb/drivers/ftdi_sio/unbind" 2>/dev/null; done function(family_flash_ft9xx TARGET) diff --git a/hw/bsp/tm4c/family.cmake b/hw/bsp/tm4c/family.cmake index 41b8a597a..ce7c62409 100644 --- a/hw/bsp/tm4c/family.cmake +++ b/hw/bsp/tm4c/family.cmake @@ -79,6 +79,6 @@ function(family_configure_example TARGET RTOS) # Flashing family_add_bin_hex(${TARGET}) family_flash_jlink(${TARGET}) - family_flash_openocd(${TARGET}) - family_flash_uniflash(${TARGET}) + family_flash_lm4flash(${TARGET}) + # family_flash_uniflash(${TARGET}) endfunction() diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index b0b3fc17e..609199d1b 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -496,6 +496,19 @@ def reset_uniflash(board): return subprocess.CompletedProcess(args=['dummy'], returncode=0) +def flash_lm4flash(board, firmware): + # TI Tiva-C / Stellaris ICDI: lightweight lm4flash, resets and runs after write + flasher = board['flasher'] + ret = run_cmd(f'lm4flash -s {flasher["uid"]} {flasher["args"]} {firmware}.bin') + return ret + + +def reset_lm4flash(board): + # lm4flash has no reset-only mode; it resets+runs on flash, so reset is a no-op + flasher = board['flasher'] + return subprocess.CompletedProcess(args=['dummy'], returncode=0) + + # ------------------------------------------------------------- # Tests: dual # ------------------------------------------------------------- diff --git a/test/hil/tinyusb.json b/test/hil/tinyusb.json index 467b7378a..79b2645c7 100644 --- a/test/hil/tinyusb.json +++ b/test/hil/tinyusb.json @@ -1,5 +1,19 @@ { "boards": [ + { + "name": "ek_tm4c123gxl", + "uid": "010105186C60A110", + "tests": { + "device": true, + "host": false, + "dual": false + }, + "flasher": { + "name": "lm4flash", + "uid": "0E205D19", + "args": "-v" + } + }, { "name": "espressif_p4_function_ev", "uid": "6055F9F98715", -- cgit v1.3.1 From 8efcc6fbc4903c1e4b41523510a296a92d4b6a05 Mon Sep 17 00:00:00 2001 From: HiFiPhile Date: Sat, 6 Jun 2026 15:27:56 +0200 Subject: host/cdc: use local control buffer --- src/class/cdc/cdc_host.c | 92 +++++++++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 48 deletions(-) diff --git a/src/class/cdc/cdc_host.c b/src/class/cdc/cdc_host.c index 62c313b83..4441222c8 100644 --- a/src/class/cdc/cdc_host.c +++ b/src/class/cdc/cdc_host.c @@ -99,6 +99,7 @@ typedef struct { typedef struct { TUH_EPBUF_DEF(tx, CFG_TUH_CDC_TX_EPSIZE); TUH_EPBUF_DEF(rx, CFG_TUH_CDC_RX_EPSIZE); + TUH_EPBUF_DEF(ctrl, 8); } cdch_epbuf_t; static cdch_interface_t cdch_data[CFG_TUH_CDC]; @@ -1003,15 +1004,16 @@ static bool acm_set_line_coding(cdch_interface_t *p_cdc, tuh_xfer_cb_t complete_ .wLength = tu_htole16((uint16_t) sizeof(cdc_line_coding_t)) }; - // use usbh enum buf to hold line coding since user line_coding variable does not live long enough - uint8_t *enum_buf = usbh_get_enum_buf(); - memcpy(enum_buf, &p_cdc->requested_line.coding, sizeof(cdc_line_coding_t)); + // use local ctrl buf to hold line coding since user line_coding variable does not live long enough + uint8_t const idx = get_idx_by_ptr(p_cdc); + uint8_t *ctrl_buf = cdch_epbuf[idx].ctrl; + memcpy(ctrl_buf, &p_cdc->requested_line.coding, sizeof(cdc_line_coding_t)); tuh_xfer_t xfer = { .daddr = p_cdc->daddr, .ep_addr = 0, .setup = &request, - .buffer = enum_buf, + .buffer = ctrl_buf, .complete_cb = complete_cb, .user_data = user_data }; @@ -1491,7 +1493,7 @@ static inline uint32_t ftdi_get_divisor(cdch_interface_t *p_cdc) { //------------- Control Request -------------// static bool cp210x_set_request(cdch_interface_t * p_cdc, uint8_t command, uint16_t value, - uint8_t * buffer, uint16_t length, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { + uint8_t const * buffer, uint16_t length, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { tusb_control_request_t const request = { .bmRequestType_bit = { .recipient = TUSB_REQ_RCPT_INTERFACE, @@ -1504,19 +1506,20 @@ static bool cp210x_set_request(cdch_interface_t * p_cdc, uint8_t command, uint16 .wLength = tu_htole16(length) }; - // use usbh enum buf since application variable does not live long enough - uint8_t * enum_buf = NULL; + // use local ctrl buf since application variable does not live long enough + uint8_t * ctrl_buf = NULL; if (buffer && length > 0) { - enum_buf = usbh_get_enum_buf(); - tu_memcpy_s(enum_buf, CFG_TUH_ENUMERATION_BUFSIZE, buffer, length); + uint8_t const idx = get_idx_by_ptr(p_cdc); + ctrl_buf = cdch_epbuf[idx].ctrl; + tu_memcpy_s(ctrl_buf, sizeof(cdch_epbuf[idx].ctrl), buffer, length); } tuh_xfer_t xfer = { .daddr = p_cdc->daddr, .ep_addr = 0, .setup = &request, - .buffer = enum_buf, + .buffer = ctrl_buf, .complete_cb = complete_cb, .user_data = user_data }; @@ -1563,7 +1566,7 @@ static void cp210x_internal_control_complete(cdch_interface_t *p_cdc, tuh_xfer_t static bool cp210x_set_baudrate(cdch_interface_t *p_cdc, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { // Not every baud rate is supported. See datasheets and AN205 "CP210x Baud Rate Support" uint32_t baud_le = tu_htole32(p_cdc->requested_line.coding.bit_rate); - return cp210x_set_request(p_cdc, CP210X_SET_BAUDRATE, 0, (uint8_t *) &baud_le, 4, complete_cb, user_data); + return cp210x_set_request(p_cdc, CP210X_SET_BAUDRATE, 0, (uint8_t const *) &baud_le, 4, complete_cb, user_data); } static bool cp210x_set_data_format(cdch_interface_t *p_cdc, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { @@ -1640,7 +1643,7 @@ static uint16_t ch34x_get_divisor_prescaler(cdch_interface_t *p_cdc); //------------- Control Request -------------// static bool ch34x_set_request(cdch_interface_t *p_cdc, uint8_t direction, uint8_t request, - uint16_t value, uint16_t index, uint8_t *buffer, uint16_t length, + uint16_t value, uint16_t index, uint8_t const *buffer, uint16_t length, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { tusb_control_request_t const request_setup = { .bmRequestType_bit = { @@ -1654,13 +1657,14 @@ static bool ch34x_set_request(cdch_interface_t *p_cdc, uint8_t direction, uint8_ .wLength = tu_htole16(length) }; - // use usbh enum buf since application variable does not live long enough - uint8_t *enum_buf = NULL; + // use local ctrl buf since application variable does not live long enough + uint8_t *ctrl_buf = NULL; - if (buffer && length > 0) { - enum_buf = usbh_get_enum_buf(); - if (direction == TUSB_DIR_OUT) { - tu_memcpy_s(enum_buf, CFG_TUH_ENUMERATION_BUFSIZE, buffer, length); + if (length > 0) { + uint8_t const idx = get_idx_by_ptr(p_cdc); + ctrl_buf = cdch_epbuf[idx].ctrl; + if (buffer && direction == TUSB_DIR_OUT) { + tu_memcpy_s(ctrl_buf, sizeof(cdch_epbuf[idx].ctrl), buffer, length); } } @@ -1668,7 +1672,7 @@ static bool ch34x_set_request(cdch_interface_t *p_cdc, uint8_t direction, uint8_ .daddr = p_cdc->daddr, .ep_addr = 0, .setup = &request_setup, - .buffer = enum_buf, + .buffer = ctrl_buf, .complete_cb = complete_cb, .user_data = user_data }; @@ -1682,8 +1686,8 @@ TU_ATTR_ALWAYS_INLINE static inline bool ch34x_control_out(cdch_interface_t *p_c } TU_ATTR_ALWAYS_INLINE static inline bool ch34x_control_in(cdch_interface_t *p_cdc, uint8_t request, uint16_t value, uint16_t index, - uint8_t *buffer, uint16_t buffersize, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { - return ch34x_set_request(p_cdc, TUSB_DIR_IN, request, value, index, buffer, buffersize, + uint16_t buffersize, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { + return ch34x_set_request(p_cdc, TUSB_DIR_IN, request, value, index, NULL, buffersize, complete_cb, user_data); } @@ -1692,12 +1696,6 @@ TU_ATTR_ALWAYS_INLINE static inline bool ch34x_write_reg(cdch_interface_t *p_cdc return ch34x_control_out(p_cdc, CH34X_REQ_WRITE_REG, reg, reg_value, complete_cb, user_data); } -//static bool ch34x_read_reg_request ( cdch_interface_t * p_cdc, uint16_t reg, -// uint8_t *buffer, uint16_t buffersize, tuh_xfer_cb_t complete_cb, uintptr_t user_data ) -//{ -// return ch34x_control_in ( p_cdc, CH34X_REQ_READ_REG, reg, 0, buffer, buffersize, complete_cb, user_data ); -//} - //------------- Driver API -------------// // internal control complete to update state such as line state, encoding @@ -1794,8 +1792,7 @@ static bool ch34x_process_set_config(cdch_interface_t *p_cdc, tuh_xfer_t *xfer) switch (state) { case CONFIG_CH34X_READ_VERSION: { - uint8_t* enum_buf = usbh_get_enum_buf(); - TU_ASSERT(ch34x_control_in(p_cdc, CH34X_REQ_READ_VERSION, 0, 0, enum_buf, 2, + TU_ASSERT(ch34x_control_in(p_cdc, CH34X_REQ_READ_VERSION, 0, 0, 2, cdch_process_set_config, CONFIG_CH34X_SERIAL_INIT)); break; } @@ -1950,7 +1947,7 @@ static bool pl2303_encode_baud_rate(cdch_interface_t *p_cdc, uint8_t buf[PL2303_ //------------- Control Request -------------// static bool pl2303_set_request(cdch_interface_t *p_cdc, uint8_t request, uint8_t requesttype, - uint16_t value, uint16_t index, uint8_t *buffer, uint16_t length, + uint16_t value, uint16_t index, uint8_t const *buffer, uint16_t length, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { tusb_control_request_t const request_setup = { .bmRequestType = requesttype, @@ -1960,13 +1957,14 @@ static bool pl2303_set_request(cdch_interface_t *p_cdc, uint8_t request, uint8_t .wLength = tu_htole16(length) }; - // use usbh enum buf since application variable does not live long enough - uint8_t *enum_buf = NULL; + // use local ctrl buf since application variable does not live long enough + uint8_t *ctrl_buf = NULL; - if (buffer && length > 0) { - enum_buf = usbh_get_enum_buf(); - if (request_setup.bmRequestType_bit.direction == TUSB_DIR_OUT) { - tu_memcpy_s(enum_buf, CFG_TUH_ENUMERATION_BUFSIZE, buffer, length); + if (length > 0) { + uint8_t const idx = get_idx_by_ptr(p_cdc); + ctrl_buf = cdch_epbuf[idx].ctrl; + if (buffer && request_setup.bmRequestType_bit.direction == TUSB_DIR_OUT) { + tu_memcpy_s(ctrl_buf, sizeof(cdch_epbuf[idx].ctrl), buffer, length); } } @@ -1974,7 +1972,7 @@ static bool pl2303_set_request(cdch_interface_t *p_cdc, uint8_t request, uint8_t .daddr = p_cdc->daddr, .ep_addr = 0, .setup = &request_setup, - .buffer = enum_buf, + .buffer = ctrl_buf, .complete_cb = complete_cb, .user_data = user_data }; @@ -1982,10 +1980,10 @@ static bool pl2303_set_request(cdch_interface_t *p_cdc, uint8_t request, uint8_t return tuh_control_xfer(&xfer); } -static bool pl2303_vendor_read(cdch_interface_t *p_cdc, uint16_t value, uint8_t *buf, +static bool pl2303_vendor_read(cdch_interface_t *p_cdc, uint16_t value, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { uint8_t request = p_cdc->pl2303.type == PL2303_TYPE_HXN ? PL2303_VENDOR_READ_NREQUEST : PL2303_VENDOR_READ_REQUEST; - return pl2303_set_request(p_cdc, request, PL2303_VENDOR_READ_REQUEST_TYPE, value, 0, buf, 1, complete_cb, user_data); + return pl2303_set_request(p_cdc, request, PL2303_VENDOR_READ_REQUEST_TYPE, value, 0, NULL, 1, complete_cb, user_data); } static bool pl2303_vendor_write(cdch_interface_t *p_cdc, uint16_t value, uint16_t index, @@ -1995,9 +1993,8 @@ static bool pl2303_vendor_write(cdch_interface_t *p_cdc, uint16_t value, uint16_ } static inline bool pl2303_supports_hx_status(cdch_interface_t *p_cdc, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { - uint8_t buf = 0; return pl2303_set_request(p_cdc, PL2303_VENDOR_READ_REQUEST, PL2303_VENDOR_READ_REQUEST_TYPE, PL2303_READ_TYPE_HX_STATUS, 0, - &buf, 1, complete_cb, user_data); + NULL, 1, complete_cb, user_data); } //static bool pl2303_get_line_request(cdch_interface_t * p_cdc, uint8_t buf[PL2303_LINE_CODING_BUFSIZE]) { @@ -2131,7 +2128,6 @@ static bool pl2303_process_set_config(cdch_interface_t *p_cdc, tuh_xfer_t *xfer) // state CONFIG_PL2303_READ1 may have no success due to expected stall by pl2303_supports_hx_status() const uintptr_t state = xfer->user_data; TU_ASSERT(xfer->result == XFER_RESULT_SUCCESS || state == CONFIG_PL2303_READ1); - uint8_t* enum_buf = usbh_get_enum_buf(); pl2303_type_t type; switch (state) { @@ -2162,7 +2158,7 @@ static bool pl2303_process_set_config(cdch_interface_t *p_cdc, tuh_xfer_t *xfer) // purpose unknown, overtaken from Linux Kernel driver if (p_cdc->pl2303.type != PL2303_TYPE_HXN) { - TU_ASSERT(pl2303_vendor_read(p_cdc, 0x8484, enum_buf, cdch_process_set_config, CONFIG_PL2303_WRITE1)); + TU_ASSERT(pl2303_vendor_read(p_cdc, 0x8484, cdch_process_set_config, CONFIG_PL2303_WRITE1)); break; }// else: continue with next step TU_ATTR_FALLTHROUGH; @@ -2178,7 +2174,7 @@ static bool pl2303_process_set_config(cdch_interface_t *p_cdc, tuh_xfer_t *xfer) case CONFIG_PL2303_READ2: // purpose unknown, overtaken from Linux Kernel driver if (p_cdc->pl2303.type != PL2303_TYPE_HXN) { - TU_ASSERT(pl2303_vendor_read(p_cdc, 0x8484, enum_buf, cdch_process_set_config, CONFIG_PL2303_READ3)); + TU_ASSERT(pl2303_vendor_read(p_cdc, 0x8484, cdch_process_set_config, CONFIG_PL2303_READ3)); break; }// else: continue with next step TU_ATTR_FALLTHROUGH; @@ -2186,7 +2182,7 @@ static bool pl2303_process_set_config(cdch_interface_t *p_cdc, tuh_xfer_t *xfer) case CONFIG_PL2303_READ3: // purpose unknown, overtaken from Linux Kernel driver if (p_cdc->pl2303.type != PL2303_TYPE_HXN) { - TU_ASSERT(pl2303_vendor_read(p_cdc, 0x8383, enum_buf, cdch_process_set_config, CONFIG_PL2303_READ4)); + TU_ASSERT(pl2303_vendor_read(p_cdc, 0x8383, cdch_process_set_config, CONFIG_PL2303_READ4)); break; }// else: continue with next step TU_ATTR_FALLTHROUGH; @@ -2194,7 +2190,7 @@ static bool pl2303_process_set_config(cdch_interface_t *p_cdc, tuh_xfer_t *xfer) case CONFIG_PL2303_READ4: // purpose unknown, overtaken from Linux Kernel driver if (p_cdc->pl2303.type != PL2303_TYPE_HXN) { - TU_ASSERT(pl2303_vendor_read(p_cdc, 0x8484, enum_buf, cdch_process_set_config, CONFIG_PL2303_WRITE2)); + TU_ASSERT(pl2303_vendor_read(p_cdc, 0x8484, cdch_process_set_config, CONFIG_PL2303_WRITE2)); break; }// else: continue with next step TU_ATTR_FALLTHROUGH; @@ -2210,7 +2206,7 @@ static bool pl2303_process_set_config(cdch_interface_t *p_cdc, tuh_xfer_t *xfer) case CONFIG_PL2303_READ5: // purpose unknown, overtaken from Linux Kernel driver if (p_cdc->pl2303.type != PL2303_TYPE_HXN) { - TU_ASSERT(pl2303_vendor_read(p_cdc, 0x8484, enum_buf, cdch_process_set_config, CONFIG_PL2303_READ6)); + TU_ASSERT(pl2303_vendor_read(p_cdc, 0x8484, cdch_process_set_config, CONFIG_PL2303_READ6)); break; }// else: continue with next step TU_ATTR_FALLTHROUGH; @@ -2218,7 +2214,7 @@ static bool pl2303_process_set_config(cdch_interface_t *p_cdc, tuh_xfer_t *xfer) case CONFIG_PL2303_READ6: // purpose unknown, overtaken from Linux Kernel driver if (p_cdc->pl2303.type != PL2303_TYPE_HXN) { - TU_ASSERT(pl2303_vendor_read(p_cdc, 0x8383, enum_buf, cdch_process_set_config, CONFIG_PL2303_WRITE3)); + TU_ASSERT(pl2303_vendor_read(p_cdc, 0x8383, cdch_process_set_config, CONFIG_PL2303_WRITE3)); break; }// else: continue with next step TU_ATTR_FALLTHROUGH; -- cgit v1.3.1 From 7c68545ab6bd0a2b7d4b59228d84d4c2085abae7 Mon Sep 17 00:00:00 2001 From: Ha Thach Date: Mon, 8 Jun 2026 09:42:08 +0700 Subject: ci: post auto-review findings to the PR (#3684) Add --comment so the auto-review is actually posted on the PR. --- .github/workflows/claude-code-review.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 91859af9d..71c1cb8ab 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -47,7 +47,9 @@ jobs: # Post/update a single summary comment every run, so a clean review # ("no issues found") is still visible instead of posting nothing. use_sticky_comment: true - prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' + # --comment makes the code-review command post its findings to the PR. + # Without it the command only prints the review to the Actions log. + prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }} --comment' # TEMPORARY: expose the full Claude transcript in the Actions log for # debugging. Revert to remove once done. show_full_output: true -- cgit v1.3.1 From 97480bad2ce5ddbb38be6bfe00491c49b6ea2afe Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 8 Jun 2026 14:34:47 +0700 Subject: stm32u0: implement board_get_unique_id from UID_BASE Previously stm32u0 had no board_get_unique_id(), so it fell back to the weak default in hw/bsp/board.c and every board reported the placeholder USB serial 0123456789ABCDEF. HIL identifies boards by USB serial, so a non-unique serial collides on a multi-board rig. Read the 96-bit unique ID from UID_BASE, mirroring stm32u5. Verified on stm32u083nucleo: now enumerates as 300044000D5036394E373620. Co-Authored-By: Claude Opus 4.8 (1M context) --- hw/bsp/stm32u0/family.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/hw/bsp/stm32u0/family.c b/hw/bsp/stm32u0/family.c index 7bd99fba6..0d20ba43f 100644 --- a/hw/bsp/stm32u0/family.c +++ b/hw/bsp/stm32u0/family.c @@ -166,6 +166,19 @@ uint32_t board_button_read(void) { return BUTTON_STATE_ACTIVE == HAL_GPIO_ReadPin(BUTTON_PORT, BUTTON_PIN); } +size_t board_get_unique_id(uint8_t id[], size_t max_len) { + (void) max_len; + volatile uint32_t *stm32_uuid = (volatile uint32_t *) UID_BASE; + uint32_t *id32 = (uint32_t *) (uintptr_t) id; + uint8_t const len = 12; + + id32[0] = stm32_uuid[0]; + id32[1] = stm32_uuid[1]; + id32[2] = stm32_uuid[2]; + + return len; +} + int board_uart_read(uint8_t* buf, int len) { #ifdef UART_ID int count = 0; -- cgit v1.3.1 From bfaa3b6c4f9a0d64641155dc4e4f4e258a1f5eb5 Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 8 Jun 2026 14:34:57 +0700 Subject: tools/gen_presets: build into cmake-build- with single-config Ninja Change the default configure preset binaryDir from build/ to cmake-build- (the dir name HIL expects) and switch the generator from Ninja Multi-Config to single-config Ninja. Multi-Config nests binaries under a RelWithDebInfo/ subdir, which hil_test.py does not look in; single-config emits device//.elf so preset-built firmware is directly consumable by `hil_test.py -B examples`. Regenerated BoardPresets.json (also picks up the tracked ch32v103c_bluepill board that was missing from presets). Co-Authored-By: Claude Opus 4.8 (1M context) --- hw/bsp/BoardPresets.json | 30 ++++++++++++++++++++++++++---- tools/gen_presets.py | 8 ++++---- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/hw/bsp/BoardPresets.json b/hw/bsp/BoardPresets.json index 86609d075..a700e7309 100644 --- a/hw/bsp/BoardPresets.json +++ b/hw/bsp/BoardPresets.json @@ -5,10 +5,10 @@ "name": "default", "hidden": true, "description": "Configure preset for the ${presetName} board", - "generator": "Ninja Multi-Config", - "binaryDir": "${sourceDir}/build/${presetName}", + "generator": "Ninja", + "binaryDir": "${sourceDir}/cmake-build-${presetName}", "cacheVariables": { - "CMAKE_DEFAULT_BUILD_TYPE": "RelWithDebInfo", + "CMAKE_BUILD_TYPE": "RelWithDebInfo", "BOARD": "${presetName}" } }, @@ -17,7 +17,7 @@ "hidden": true, "description": "Configure preset for the ${presetName} board", "generator": "Ninja", - "binaryDir": "${sourceDir}/build/${presetName}", + "binaryDir": "${sourceDir}/cmake-build-${presetName}", "cacheVariables": { "BOARD": "${presetName}" } @@ -122,6 +122,10 @@ "name": "ch32f205r-r0", "inherits": "default" }, + { + "name": "ch32v103c_bluepill", + "inherits": "default" + }, { "name": "ch32v103r_r1_1v0", "inherits": "default" @@ -1087,6 +1091,11 @@ "description": "Build preset for the ch32f205r-r0 board", "configurePreset": "ch32f205r-r0" }, + { + "name": "ch32v103c_bluepill", + "description": "Build preset for the ch32v103c_bluepill board", + "configurePreset": "ch32v103c_bluepill" + }, { "name": "ch32v103r_r1_1v0", "description": "Build preset for the ch32v103r_r1_1v0 board", @@ -2472,6 +2481,19 @@ } ] }, + { + "name": "ch32v103c_bluepill", + "steps": [ + { + "type": "configure", + "name": "ch32v103c_bluepill" + }, + { + "type": "build", + "name": "ch32v103c_bluepill" + } + ] + }, { "name": "ch32v103r_r1_1v0", "steps": [ diff --git a/tools/gen_presets.py b/tools/gen_presets.py index 94a9361db..60404a5a7 100755 --- a/tools/gen_presets.py +++ b/tools/gen_presets.py @@ -31,17 +31,17 @@ def main(): {"name": "default", "hidden": True, "description": r"Configure preset for the ${presetName} board", - "generator": "Ninja Multi-Config", - "binaryDir": r"${sourceDir}/build/${presetName}", + "generator": "Ninja", + "binaryDir": r"${sourceDir}/cmake-build-${presetName}", "cacheVariables": { - "CMAKE_DEFAULT_BUILD_TYPE": "RelWithDebInfo", + "CMAKE_BUILD_TYPE": "RelWithDebInfo", "BOARD": r"${presetName}" }}, {"name": "default single config", "hidden": True, "description": r"Configure preset for the ${presetName} board", "generator": "Ninja", - "binaryDir": r"${sourceDir}/build/${presetName}", + "binaryDir": r"${sourceDir}/cmake-build-${presetName}", "cacheVariables": { "BOARD": r"${presetName}" }}] -- cgit v1.3.1 From d2e7bbb0855a3ff3d6393c7d7edd755cfd3362df Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 8 Jun 2026 14:35:07 +0700 Subject: test/hil: add stm32u083nucleo to pool and expose ~/bin on remote PATH Add stm32u083nucleo to the active boards in tinyusb.json, flashed via the stlink flasher (onboard ST-Link + STM32CubeProgrammer); ci's openocd build has no STM32U0 flash driver. STM32_Programmer_CLI lives in ~/bin on ci, which the remote `bash -s` shell in hil_ci.sh did not have on PATH, so add $HOME/bin to its PATH export (matching the GHA runner .path). Verified remote: 13/13 device tests pass on ci.lan. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/hil/hil_ci.sh | 7 ++++--- test/hil/tinyusb.json | 13 +++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/test/hil/hil_ci.sh b/test/hil/hil_ci.sh index 4c7ba2936..4bb459af3 100644 --- a/test/hil/hil_ci.sh +++ b/test/hil/hil_ci.sh @@ -91,8 +91,9 @@ echo "==> Running HIL test on $REMOTE" ssh "$REMOTE" bash -s -- "$REMOTE_DIR" "${ARGS[@]}" "test/hil/$CONFIG_BASENAME" <<'REMOTE' cd -- "$1" shift -# esptool/idf tools live in ~/.local/bin on ci.lan; the non-interactive shell -# subprocess used for flashing doesn't pick that up otherwise. -export PATH="$HOME/.local/bin:$PATH" +# Flasher CLIs live in the user bin dirs on ci.lan (esptool/idf in ~/.local/bin, +# STM32CubeProgrammer's STM32_Programmer_CLI in ~/bin); the non-interactive shell +# subprocess used for flashing doesn't source profile/rc, so add them explicitly. +export PATH="$HOME/.local/bin:$HOME/bin:$PATH" exec python3 -u test/hil/hil_test.py -B examples "$@" REMOTE diff --git a/test/hil/tinyusb.json b/test/hil/tinyusb.json index 79b2645c7..319ee9a79 100644 --- a/test/hil/tinyusb.json +++ b/test/hil/tinyusb.json @@ -455,6 +455,19 @@ "uid": "777632258", "args": "-device STM32L476VG" } + }, + { + "name": "stm32u083nucleo", + "uid": "300044000D5036394E373620", + "tests": { + "device": true, + "host": false, + "dual": false + }, + "flasher": { + "name": "stlink", + "uid": "0668FF575457657187061314" + } } ], "boards-skip": [ -- cgit v1.3.1 From 752d0d7884bec86d3f6735ce0ac17bc98c9c8622 Mon Sep 17 00:00:00 2001 From: Ha Thach Date: Mon, 8 Jun 2026 14:39:14 +0700 Subject: Fix formatting in README.rst table --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 67149a4f2..73e1e413e 100644 --- a/README.rst +++ b/README.rst @@ -251,7 +251,7 @@ Supported CPUs | | +-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ | | | 51u | ✔ | ✖ | ✖ | lpc_ip3511 | | | | +-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | | 54 | ⚠ | ⚠ | ✔ | lpc_ip3511, lpc_ip3516 | `NRND, see device issues `_ | +| | | 54 | ⚠ | ⚠ | ✔ | lpc_ip3511, lpc_ip3516 | `NRND, see device issues `_ | | | +-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ | | | 55 | ✔ | ✔ | ✔ | lpc_ip3511, lpc_ip3516 | | | +---------+-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -- cgit v1.3.1 From 6586d94af07c96b6842d31fd6b06288d78e26864 Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 8 Jun 2026 15:10:35 +0700 Subject: gitattributes: keep shell scripts LF under core.autocrlf With core.autocrlf=true, *.sh files were checked out / restored with CRLF line endings, which breaks bash ($'\r': command not found; set: pipefail: invalid option). Pin *.sh to eol=lf so shell scripts stay LF in the working tree regardless of autocrlf. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitattributes b/.gitattributes index 140ae8929..723f48277 100644 --- a/.gitattributes +++ b/.gitattributes @@ -19,6 +19,9 @@ Makefile text +# Shell scripts must stay LF even when core.autocrlf=true (CRLF breaks bash) +*.sh text eol=lf + # Windows-only Visual Studio things *.sln text eol=crlf -- cgit v1.3.1 From d38ba79ad4a9cd90589a422b6eebd90614af4008 Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 8 Jun 2026 15:10:47 +0700 Subject: test/hil: report board x test results as a markdown matrix hil_test.py now writes hil_report.md and prints it to stdout: rows are boards, columns are tests (bare example names), cells are pass/fail/skip. test_example returns a per-test status, test_board collects a board x test grid (one row per flags-on variant), and main() renders an aligned table. A missing binary counts as skipped. hil_ci.sh copies the report back from the remote after a run; hil_report.md is gitignored. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 1 + test/hil/hil_ci.sh | 13 ++++++-- test/hil/hil_test.py | 92 ++++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 91 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index c11e51bb9..ba1574558 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ html latex +hil_report.md *.a *.d *.o diff --git a/test/hil/hil_ci.sh b/test/hil/hil_ci.sh index 4bb459af3..4f68ed067 100644 --- a/test/hil/hil_ci.sh +++ b/test/hil/hil_ci.sh @@ -88,12 +88,21 @@ fi # parameters; quoting and metacharacters in args are preserved. CONFIG_BASENAME="$(basename "$CONFIG")" echo "==> Running HIL test on $REMOTE" -ssh "$REMOTE" bash -s -- "$REMOTE_DIR" "${ARGS[@]}" "test/hil/$CONFIG_BASENAME" <<'REMOTE' +rc=0 +ssh "$REMOTE" bash -s -- "$REMOTE_DIR" "${ARGS[@]}" "test/hil/$CONFIG_BASENAME" <<'REMOTE' || rc=$? cd -- "$1" shift # Flasher CLIs live in the user bin dirs on ci.lan (esptool/idf in ~/.local/bin, # STM32CubeProgrammer's STM32_Programmer_CLI in ~/bin); the non-interactive shell # subprocess used for flashing doesn't source profile/rc, so add them explicitly. export PATH="$HOME/.local/bin:$HOME/bin:$PATH" -exec python3 -u test/hil/hil_test.py -B examples "$@" +python3 -u test/hil/hil_test.py -B examples "$@" REMOTE + +# Copy the generated report back to the local checkout (best-effort; the run's +# exit code is preserved regardless of whether a report was produced). +scp -q "$REMOTE:$REMOTE_DIR/hil_report.md" "$ROOT_DIR/hil_report.md" \ + && echo "==> Report copied to $ROOT_DIR/hil_report.md" \ + || echo "==> warning: no hil_report.md copied back" >&2 + +exit $rc diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index 609199d1b..2fb5f6b3f 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -65,6 +65,10 @@ STATUS_OK = "\033[32mOK\033[0m" STATUS_FAILED = "\033[31mFailed\033[0m" STATUS_SKIPPED = "\033[33mSkipped\033[0m" +# Plain (non-ANSI) cell symbols for the markdown matrix report (hil_report.md). +# A missing binary is reported as skipped too. +REPORT_CELL = {'pass': '✔', 'fail': '✖', 'skip': '➖'} + verbose = False test_only = [] board_test = {} @@ -1490,20 +1494,26 @@ host_test = [ ] -def test_example(board: Board, f1: str, example: str) -> int: +def f1_suffix(f1: str) -> str: + """Build dir / row-label suffix for a flags-on variant ('' for the default).""" + return '-f1_' + f1.replace(' ', '_') if f1 else '' + + +def test_example(board: Board, f1: str, example: str) -> tuple[int, str]: """ Test example firmware :param board: board dict :param f1: flags on :param example: example name - :return: 0 if success/skip, 1 if failed + :return: (err_count, status) where err_count is 0 on success/skip or 1 on + failure, and status is one of 'pass'/'fail'/'skip' (a missing + binary counts as 'skip') """ name = board['name'] err_count = 0 + result_status = 'fail' - f1_str = "" - if f1 != "": - f1_str = '-f1_' + f1.replace(' ', '_') + f1_str = f1_suffix(f1) fw_dir = TINYUSB_ROOT / build_dir / f'cmake-build-{name}{f1_str}' / example fw_name = fw_dir / Path(example).name @@ -1511,7 +1521,7 @@ def test_example(board: Board, f1: str, example: str) -> int: if not fw_dir.exists() or not ((fw_name.with_suffix('.elf')).exists() or (fw_name.with_suffix('.bin')).exists()): log_line(f'{test_name} Skip (no binary)') - return 0 + return 0, 'skip' if verbose: log_line(f'Flashing {fw_name}.elf') @@ -1534,8 +1544,10 @@ def test_example(board: Board, f1: str, example: str) -> int: last_detail = compact_output(attempt_out.getvalue()) if tret == 'skipped': status = STATUS_SKIPPED + result_status = 'skip' else: status = STATUS_OK + result_status = 'pass' msg = f'{test_name} {status}' if last_detail: msg += f' {last_detail}' @@ -1578,7 +1590,7 @@ def test_example(board: Board, f1: str, example: str) -> int: msg += f' in {time.time() - start_s:.1f}s' log_line(msg) - return err_count + return err_count, result_status def build_board(board: Board) -> tuple[str, int]: @@ -1607,7 +1619,7 @@ def build_board(board: Board) -> tuple[str, int]: return name, failed -def test_board(board: Board) -> tuple[str, int, list[str]]: +def test_board(board: Board) -> tuple[str, int, list[str], list]: name = board['name'] flasher = board['flasher'] @@ -1648,22 +1660,68 @@ def test_board(board: Board) -> tuple[str, int, list[str]]: err_count = 0 failed_tests = [] + rows = [] # list of (row_label, {example: status}) — one row per board[-f1] variant flags_on_list = [""] if 'build' in board and 'flags_on' in board['build']: flags_on_list = board['build']['flags_on'] for f1 in flags_on_list: + cells = {} for test in test_list: - ec = test_example(board, f1, test) + ec, status = test_example(board, f1, test) err_count += ec + cells[test] = status if ec > 0: failed_tests.append(test) + rows.append((name + f1_suffix(f1), cells)) # flash board_test last to disable board's usb (skipped when --skip-flash is set) if not skip_flash: - test_example(board, flags_on_list[0], 'device/board_test') + _ec, status = test_example(board, flags_on_list[0], 'device/board_test') + if rows: + rows[0][1]['device/board_test'] = status + + return name, err_count, sorted(set(failed_tests)), rows + + +def generate_report(mret: list) -> str: + """Build a markdown matrix (rows = boards, columns = tests) from test_board + results. Each mret entry is (name, err, failed_tests, rows) where rows is a + list of (row_label, {example: status}). Columns are padded so the raw table + is aligned in plain text (boards left-aligned, test cells centered).""" + canonical = device_tests + dual_tests + host_test + ['device/board_test'] + rows_all = [] # flattened (row_label, cells), preserving board/f1 order + seen = set() + for _, _, _, rows in mret: + for row_label, cells in rows: + rows_all.append((row_label, cells)) + seen.update(cells) + if not seen: + return 'No tests were run.' + + # columns: canonical order first, then any extras (e.g. from -t) alphabetically + columns = [t for t in canonical if t in seen] + columns += [t for t in sorted(seen) if t not in canonical] + headers = [c.rsplit('/', 1)[-1] for c in columns] # bare example name - return name, err_count, sorted(set(failed_tests)) + def cell(cells, col): + return REPORT_CELL.get(cells.get(col), '') + + board_hdr = 'Board' + board_w = max([len(board_hdr)] + [len(lbl) for lbl, _ in rows_all]) + col_w = [max([len(h)] + [len(cell(cells, c)) for _, cells in rows_all]) + for h, c in zip(headers, columns)] + + def line(label, values): + padded = [label.ljust(board_w)] + [v.center(w) for v, w in zip(values, col_w)] + return '| ' + ' | '.join(padded) + ' |' + + header = line(board_hdr, headers) + sep = '| ' + '-' * board_w + ' | ' + ' | '.join(':' + '-' * (w - 2) + ':' for w in col_w) + ' |' + body = [line(lbl, [cell(cells, c) for c in columns]) for lbl, cells in rows_all] + + legend = 'Legend: ✔ pass · ✖ fail · ➖ skipped · blank not run' + return '\n'.join([header, sep] + body) + '\n\n' + legend def main() -> None: @@ -1747,14 +1805,22 @@ def main() -> None: # and emit -bt BOARD:t1,t2 so each failed board only re-runs its own failed tests. skip_fname = config_file.with_suffix(config_file.suffix + '.skip') if err_count > 0: - skip_boards += [name for name, err, _ in mret if err == 0] + skip_boards += [name for name, err, _, _ in mret if err == 0] parts = [f'--skip-board {i}' for i in skip_boards] - parts += [f'-bt {name}:{",".join(fts)}' for name, err, fts in mret if err > 0 and fts] + parts += [f'-bt {name}:{",".join(fts)}' for name, err, fts, _ in mret if err > 0 and fts] with skip_fname.open('w') as f: f.write(' '.join(parts)) elif skip_fname.exists(): skip_fname.unlink() + # board x test result matrix -> hil_report.md and stdout + report = generate_report(mret) + report_path = Path('hil_report.md') + report_path.write_text(report + '\n', encoding='utf-8') + print() + print(report) + print(f'\nReport written to {report_path.resolve()}') + duration = time.time() - duration print() print("-" * 30) -- cgit v1.3.1 From 46aded44af947e1be32426edcd6ff2c0596f1765 Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 8 Jun 2026 15:42:00 +0700 Subject: presets,hil: keep Ninja Multi-Config; make HIL find its output Per review (HiFiPhile): Ninja Multi-Config is needed for IAR, otherwise the optimization level can't be lowered to none for debug. Revert gen_presets.py back to Ninja Multi-Config (keeping only the cmake-build- binaryDir change), and instead teach hil_test.py to locate .elf whether it sits directly in the example dir (single-config) or under a per-config subdir like RelWithDebInfo/ (multi-config). Verified: stm32u083nucleo passes 13/13 remote HIL with a multi-config preset build (rsync preserves the RelWithDebInfo/ subdir; the resolver finds it). Co-Authored-By: Claude Opus 4.8 (1M context) --- hw/bsp/BoardPresets.json | 4 ++-- test/hil/hil_test.py | 14 ++++++++++++-- tools/gen_presets.py | 4 ++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/hw/bsp/BoardPresets.json b/hw/bsp/BoardPresets.json index a700e7309..09a9ef18f 100644 --- a/hw/bsp/BoardPresets.json +++ b/hw/bsp/BoardPresets.json @@ -5,10 +5,10 @@ "name": "default", "hidden": true, "description": "Configure preset for the ${presetName} board", - "generator": "Ninja", + "generator": "Ninja Multi-Config", "binaryDir": "${sourceDir}/cmake-build-${presetName}", "cacheVariables": { - "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "CMAKE_DEFAULT_BUILD_TYPE": "RelWithDebInfo", "BOARD": "${presetName}" } }, diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index 2fb5f6b3f..2758d093c 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -1516,10 +1516,20 @@ def test_example(board: Board, f1: str, example: str) -> tuple[int, str]: f1_str = f1_suffix(f1) fw_dir = TINYUSB_ROOT / build_dir / f'cmake-build-{name}{f1_str}' / example - fw_name = fw_dir / Path(example).name + base = Path(example).name test_name = f'{name+f1_str:40} {example:30} ...' - if not fw_dir.exists() or not ((fw_name.with_suffix('.elf')).exists() or (fw_name.with_suffix('.bin')).exists()): + # firmware sits directly in the example dir (single-config Ninja) or under a + # per-config subdir like RelWithDebInfo/ (Ninja Multi-Config); accept either. + fw_name = None + if fw_dir.is_dir(): + for cand in [fw_dir / base, fw_dir / 'RelWithDebInfo' / base, + *(p.with_suffix('') for p in sorted(fw_dir.glob(f'*/{base}.elf')))]: + if cand.with_suffix('.elf').exists() or cand.with_suffix('.bin').exists(): + fw_name = cand + break + + if fw_name is None: log_line(f'{test_name} Skip (no binary)') return 0, 'skip' diff --git a/tools/gen_presets.py b/tools/gen_presets.py index 60404a5a7..6f32976a7 100755 --- a/tools/gen_presets.py +++ b/tools/gen_presets.py @@ -31,10 +31,10 @@ def main(): {"name": "default", "hidden": True, "description": r"Configure preset for the ${presetName} board", - "generator": "Ninja", + "generator": "Ninja Multi-Config", "binaryDir": r"${sourceDir}/cmake-build-${presetName}", "cacheVariables": { - "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "CMAKE_DEFAULT_BUILD_TYPE": "RelWithDebInfo", "BOARD": r"${presetName}" }}, {"name": "default single config", -- cgit v1.3.1 From fba8d257846ab9149f9db5443827d741492f837d Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 8 Jun 2026 16:11:13 +0700 Subject: test/hil: accumulate HIL report across re-runs; post as sticky PR comment hil_test.py persists results in a hil_report.json sidecar and regenerates hil_report.md from it. A full run starts fresh; a re-run (--skip-board / -bt, i.e. the .skip file) merges into the existing report so already-passed boards/tests are preserved while only re-run cells update. The report dir is configurable via HIL_REPORT_DIR. build.yml: each HIL rig writes the report to a workspace-sibling dir that survives the per-attempt workspace clean, and uploads it as an artifact. A new hil-report job merges the rigs' reports into one sticky PR comment (marocchino) with one table per rig. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build.yml | 54 +++++++++++++++++++++++++++++++++++++++++ .gitignore | 1 + test/hil/hil_test.py | 59 +++++++++++++++++++++++++++++++++------------ 3 files changed, 99 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e5075f46f..0ff29cdda 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -306,6 +306,9 @@ jobs: env: HIL_JSON: ${{ matrix.hil_json }} steps: + - name: Set HIL report dir (sibling of workspace; persists across run attempts) + run: echo "HIL_REPORT_DIR=$(dirname "$GITHUB_WORKSPACE")/hil-report" >> "$GITHUB_ENV" + - name: Get Skip Boards from previous run if: github.run_attempt != '1' run: | @@ -344,6 +347,15 @@ jobs: exit 1 fi) + - name: Upload HIL report + if: always() && github.event_name == 'pull_request' + uses: actions/upload-artifact@v7 + with: + name: hil-report-${{ matrix.display }} + path: ${{ env.HIL_REPORT_DIR }}/hil_report.md + if-no-files-found: ignore + overwrite: true + # --------------------------------------- # Hardware in the loop (HIL) # self-hosted by HFP, build with IAR toolchain, for attached hardware checkout test/hil/hfp.json @@ -390,3 +402,45 @@ jobs: - name: Test on actual hardware (hardware in the loop) run: | python3 test/hil/hil_test.py hfp.json + + # --------------------------------------- + # Combine HIL results from the rigs into a single sticky PR comment (one table per rig) + # --------------------------------------- + hil-report: + needs: hil-tinyusb + if: | + always() && + needs.hil-tinyusb.result != 'skipped' && + github.event_name == 'pull_request' && + github.repository_owner == 'hathach' && + github.event.pull_request.head.repo.fork == false + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Download HIL reports + uses: actions/download-artifact@v5 + with: + pattern: hil-report-* + path: hil-reports + + - name: Combine rig reports (one table per rig) + run: | + { + echo "## HIL test results" + echo + for d in hil-reports/hil-report-*; do + [ -d "$d" ] || continue + echo "### ${d#hil-reports/hil-report-}" + echo + cat "$d/hil_report.md" 2>/dev/null || echo "_no report produced_" + echo + done + } > hil_combined.md + cat hil_combined.md + + - name: Post HIL report as sticky PR comment + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: hil-report + path: hil_combined.md diff --git a/.gitignore b/.gitignore index ba1574558..0f3c9ce49 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ html latex hil_report.md +hil_report.json *.a *.d *.o diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index 2758d093c..515c20e75 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -1694,18 +1694,17 @@ def test_board(board: Board) -> tuple[str, int, list[str], list]: return name, err_count, sorted(set(failed_tests)), rows -def generate_report(mret: list) -> str: - """Build a markdown matrix (rows = boards, columns = tests) from test_board - results. Each mret entry is (name, err, failed_tests, rows) where rows is a - list of (row_label, {example: status}). Columns are padded so the raw table - is aligned in plain text (boards left-aligned, test cells centered).""" +REPORT_MD = 'hil_report.md' +REPORT_JSON = 'hil_report.json' + + +def render_matrix(rows_all: list) -> str: + """Render rows (list of (row_label, {example: status})) as an aligned markdown + matrix: columns = tests (bare names) centered, boards left-aligned.""" canonical = device_tests + dual_tests + host_test + ['device/board_test'] - rows_all = [] # flattened (row_label, cells), preserving board/f1 order seen = set() - for _, _, _, rows in mret: - for row_label, cells in rows: - rows_all.append((row_label, cells)) - seen.update(cells) + for _, cells in rows_all: + seen.update(cells) if not seen: return 'No tests were run.' @@ -1734,6 +1733,34 @@ def generate_report(mret: list) -> str: return '\n'.join([header, sep] + body) + '\n\n' + legend +def accumulate_report(mret: list, report_dir: Path, fresh: bool) -> str: + """Merge this run's results into hil_report.json in report_dir, then (re)write + the markdown matrix to hil_report.md. `fresh` (a full run, no --skip-board/-bt) + starts a new report; otherwise a re-run accumulates so boards/tests that + already passed are preserved while re-run cells are updated. Returns the md.""" + acc = {} # ordered {row_label: {example: status}} + jpath = report_dir / REPORT_JSON + if not fresh and jpath.is_file(): + try: + for entry in json.loads(jpath.read_text()).get('rows', []): + acc[entry['board']] = dict(entry['cells']) + except (ValueError, KeyError, TypeError): + pass # corrupt/old sidecar: start fresh + + # merge this run: current cells override prior for boards/tests that ran + for _, _, _, rows in mret: + for row_label, cells in rows: + acc.setdefault(row_label, {}).update(cells) + + report_dir.mkdir(parents=True, exist_ok=True) + jpath.write_text(json.dumps({'rows': [{'board': k, 'cells': v} for k, v in acc.items()]}, + indent=2) + '\n') + + md = render_matrix(list(acc.items())) + (report_dir / REPORT_MD).write_text(md + '\n', encoding='utf-8') + return md + + def main() -> None: """ Hardware test on specified boards @@ -1823,13 +1850,15 @@ def main() -> None: elif skip_fname.exists(): skip_fname.unlink() - # board x test result matrix -> hil_report.md and stdout - report = generate_report(mret) - report_path = Path('hil_report.md') - report_path.write_text(report + '\n', encoding='utf-8') + # board x test result matrix -> hil_report.md (accumulates across re-runs) + stdout. + # A full run starts fresh; a re-run (--skip-board / -bt, i.e. the .skip file) merges + # into the existing report so already-passed boards/tests are preserved. + report_dir = Path(os.environ.get('HIL_REPORT_DIR', '.')) + fresh = not (args.skip_board or args.board_test) + report = accumulate_report(mret, report_dir, fresh) print() print(report) - print(f'\nReport written to {report_path.resolve()}') + print(f'\nReport written to {(report_dir / REPORT_MD).resolve()}') duration = time.time() - duration print() -- cgit v1.3.1 From 7b60e3951e800bcdc5e1e015ca078d4f3cd6080e Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 8 Jun 2026 16:18:04 +0700 Subject: test/hil: clear HIL report up front on a fresh run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The report sidecar lives in a persistent dir (it survives the CI workspace clean so accumulation works across run attempts). A full run is "fresh" and must not merge prior state, but previously fresh only avoided *loading* the json at merge time — if a fresh run crashed before writing the report, the stale json/md from an earlier run lingered and a retry (fresh=False) could merge it, or the always() upload could post it. Delete hil_report.json/.md at the start of a fresh run so prior results can never leak. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/hil/hil_test.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index 515c20e75..542aa4a11 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -1828,6 +1828,17 @@ def main() -> None: print(f'Build phase done: {build_err} failed') print('-' * 30) + # HIL report sidecar (hil_report.json/.md). A full run starts fresh; a re-run + # (--skip-board / -bt, i.e. the .skip file) accumulates so already-passed + # boards/tests are preserved. Clear any prior report up front on a fresh run so + # a crash mid-run can't leave stale results to be merged by a retry or posted. + report_dir = Path(os.environ.get('HIL_REPORT_DIR', '.')) + fresh = not (args.skip_board or args.board_test) + if fresh: + report_dir.mkdir(parents=True, exist_ok=True) + for f in (REPORT_JSON, REPORT_MD): + (report_dir / f).unlink(missing_ok=True) + with Pool(processes=os.cpu_count() or 1, initializer=init_worker, initargs=(Lock(),)) as pool: async_ret = pool.map_async(test_board, config_boards) try: @@ -1850,11 +1861,7 @@ def main() -> None: elif skip_fname.exists(): skip_fname.unlink() - # board x test result matrix -> hil_report.md (accumulates across re-runs) + stdout. - # A full run starts fresh; a re-run (--skip-board / -bt, i.e. the .skip file) merges - # into the existing report so already-passed boards/tests are preserved. - report_dir = Path(os.environ.get('HIL_REPORT_DIR', '.')) - fresh = not (args.skip_board or args.board_test) + # board x test result matrix -> hil_report.md (accumulates across re-runs) + stdout report = accumulate_report(mret, report_dir, fresh) print() print(report) -- cgit v1.3.1 From a70c5a626b161eae1211fa47e9929de35b18bcd7 Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 8 Jun 2026 16:21:34 +0700 Subject: ci: include hil-hfp-iar (IAR) results in the HIL PR comment hil-hfp-iar runs hil_test.py on hfp.json built with IAR on its own rig. Upload its report as the hil-report-hfp-iar artifact and add the job to the hil-report combine job's needs, so the sticky comment shows a third table for the IAR rig alongside tinyusb.json and hfp.json (gcc). The combine gate now runs if either HIL job produced results. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0ff29cdda..ce94a9829 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -403,14 +403,23 @@ jobs: run: | python3 test/hil/hil_test.py hfp.json + - name: Upload HIL report + if: always() && github.event_name == 'pull_request' + uses: actions/upload-artifact@v7 + with: + name: hil-report-hfp-iar + path: hil_report.md + if-no-files-found: ignore + overwrite: true + # --------------------------------------- # Combine HIL results from the rigs into a single sticky PR comment (one table per rig) # --------------------------------------- hil-report: - needs: hil-tinyusb + needs: [ hil-tinyusb, hil-hfp-iar ] if: | always() && - needs.hil-tinyusb.result != 'skipped' && + (needs.hil-tinyusb.result != 'skipped' || needs.hil-hfp-iar.result != 'skipped') && github.event_name == 'pull_request' && github.repository_owner == 'hathach' && github.event.pull_request.head.repo.fork == false -- cgit v1.3.1 From 71f7ba0415764ef3dad0fe0dec49cde4d490fdc5 Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 8 Jun 2026 16:28:53 +0700 Subject: ci: demote sticky-comment report headings to h2; rename HIL report The Size Difference Report and HIL comments rendered their titles at h1, which is oversized inside a PR comment. Use h2 for both titles (with subsections demoted to h3 to keep the hierarchy), and rename the HIL comment from "HIL test results" to "Hardware-in-the-loop (HIL) Test Report" for consistency. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build.yml | 2 +- tools/metrics.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ce94a9829..e22ba909c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -436,7 +436,7 @@ jobs: - name: Combine rig reports (one table per rig) run: | { - echo "## HIL test results" + echo "## Hardware-in-the-loop (HIL) Test Report" echo for d in hil-reports/hil-report-*; do [ -d "$d" ] || continue diff --git a/tools/metrics.py b/tools/metrics.py index f624f382f..05978b6ef 100644 --- a/tools/metrics.py +++ b/tools/metrics.py @@ -400,7 +400,7 @@ def write_combine_markdown(json_data, path, sort_order='name+', title="TinyUSB A def write_compare_markdown(comparison, path, sort_order='size'): """Write comparison data to markdown file.""" md_lines = [ - "# Size Difference Report", + "## Size Difference Report", "", "Because TinyUSB code size varies by port and configuration, the metrics below represent the averaged totals across all example builds.", "", @@ -415,7 +415,7 @@ def write_compare_markdown(comparison, path, sort_order='size'): md_lines.append(f"
{title}") md_lines.append("") else: - md_lines.append(f"## {title}") + md_lines.append(f"### {title}") md_lines.extend(render_compare_table(_build_rows(rows, sort_order), include_sum=True)) md_lines.append("") -- cgit v1.3.1 From fe273aa114c6ee20c23b506949a80490feff69b4 Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 8 Jun 2026 16:30:19 +0700 Subject: ci: demote Average Code Size Metrics title to h2 metrics.md (write_combine_markdown) is also used as the PR size comment when there is no base-metrics baseline; use h2 for its title too so the sticky comment heading is consistent (and not oversized) in that fallback case. Co-Authored-By: Claude Opus 4.8 (1M context) --- tools/metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/metrics.py b/tools/metrics.py index 05978b6ef..b7aa056e4 100644 --- a/tools/metrics.py +++ b/tools/metrics.py @@ -384,7 +384,7 @@ def render_combine_table(json_data, sort_order='name+'): def write_combine_markdown(json_data, path, sort_order='name+', title="TinyUSB Average Code Size Metrics"): """Write averaged size data to a markdown file.""" - md_lines = [f"# {title}", ""] + md_lines = [f"## {title}", ""] md_lines.extend(render_combine_table(json_data, sort_order)) md_lines.append("") -- cgit v1.3.1 From 3875e9cde897392255e2bd1a70de3231ec69d482 Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 8 Jun 2026 17:17:26 +0700 Subject: test/hil: use ✅/❌ emoji for HIL report pass/fail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Colored emoji render green/red in the GitHub PR comment, far more visible than the monochrome ✔/✖ dingbats. Skip stays ➖. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/hil/hil_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index 542aa4a11..1d3f50a19 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -67,7 +67,7 @@ STATUS_SKIPPED = "\033[33mSkipped\033[0m" # Plain (non-ANSI) cell symbols for the markdown matrix report (hil_report.md). # A missing binary is reported as skipped too. -REPORT_CELL = {'pass': '✔', 'fail': '✖', 'skip': '➖'} +REPORT_CELL = {'pass': '✅', 'fail': '❌', 'skip': '➖'} verbose = False test_only = [] @@ -1729,7 +1729,7 @@ def render_matrix(rows_all: list) -> str: sep = '| ' + '-' * board_w + ' | ' + ' | '.join(':' + '-' * (w - 2) + ':' for w in col_w) + ' |' body = [line(lbl, [cell(cells, c) for c in columns]) for lbl, cells in rows_all] - legend = 'Legend: ✔ pass · ✖ fail · ➖ skipped · blank not run' + legend = 'Legend: ✅ pass · ❌ fail · ➖ skipped · blank not run' return '\n'.join([header, sep] + body) + '\n\n' + legend -- cgit v1.3.1 From 1b3627d2b6a22e0f8f1a5dcd161121a63b238092 Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 8 Jun 2026 17:42:11 +0700 Subject: test/hil: use ⚪ for skipped in HIL report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neutral white circle for skipped, giving a ✅/❌/⚪ pass/fail/skip set. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/hil/hil_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index 1d3f50a19..a1c4b7bdd 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -67,7 +67,7 @@ STATUS_SKIPPED = "\033[33mSkipped\033[0m" # Plain (non-ANSI) cell symbols for the markdown matrix report (hil_report.md). # A missing binary is reported as skipped too. -REPORT_CELL = {'pass': '✅', 'fail': '❌', 'skip': '➖'} +REPORT_CELL = {'pass': '✅', 'fail': '❌', 'skip': '⚪'} verbose = False test_only = [] @@ -1729,7 +1729,7 @@ def render_matrix(rows_all: list) -> str: sep = '| ' + '-' * board_w + ' | ' + ' | '.join(':' + '-' * (w - 2) + ':' for w in col_w) + ' |' body = [line(lbl, [cell(cells, c) for c in columns]) for lbl, cells in rows_all] - legend = 'Legend: ✅ pass · ❌ fail · ➖ skipped · blank not run' + legend = 'Legend: ✅ pass · ❌ fail · ⚪ skipped · blank not run' return '\n'.join([header, sep] + body) + '\n\n' + legend -- cgit v1.3.1 From 8219efdc6c5a9dd6a8768453050c1e7c62f04363 Mon Sep 17 00:00:00 2001 From: hathach Date: Tue, 9 Jun 2026 11:44:06 +0700 Subject: test/hil: erase MCU after tests; show throughput speeds in report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Teardown: instead of flashing device/board_test (a USB-less blink loop that keeps the MCU busy-looping), erase the first flash sector (vector table) so the board faults to idle after its tests — no USB, lower power, faster. Per-flasher erase_: openocd/openocd_adi `flash erase_sector 0 0 0`; stlink `--erase 0`; jlink erases the sector at the flash origin read from the ELF (pure-Python, new elf_flash_origin); esptool `erase_region 0x0 0x4000`; lm4flash writes a 4 KB all-0xFF blank image (lm4flash erases before programming, so the first sector ends up blank). device/board_test flash remains a fallback for flashers with no erase_ function. The teardown is no longer a report column (it's cleanup). Report: cdc_msc_throughput and msc_file_explorer[_freertos] now return a compact read/write speed shown in their report cell instead of the pass tick (e.g. "C 652k/422k M 1.1M/783k", "rd 1.2MB/s"). test_example returns an optional metric; render_matrix shows it verbatim. Firmware lookup factored into find_firmware (reused by the erase teardown). Verified on the rig (stm32f723disco, jlink): erase disables the board in 0.8 s and it disappears from the bus; the throughput cell shows live speeds. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/hil/hil_test.py | 171 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 142 insertions(+), 29 deletions(-) diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index a1c4b7bdd..226e97780 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -40,6 +40,7 @@ import os import random import re import select +import struct import sys import time import signal @@ -513,6 +514,80 @@ def reset_lm4flash(board): return subprocess.CompletedProcess(args=['dummy'], returncode=0) +# ------------------------------------------------------------- +# Erase: wipe the first flash sector (vector table) after a board's tests so the +# MCU faults to an idle state — no USB, lower power, and faster than programming +# device/board_test. Same (board, firmware) signature as flash_*; `firmware` is +# only used to find the flash origin (jlink) or the esp flash metadata. +# ------------------------------------------------------------- +def elf_flash_origin(elf_path: str) -> int: + """Flash base address (first PT_LOAD segment physical address) of a + little-endian ELF32 firmware — i.e. where the vector table is programmed.""" + data = Path(elf_path).read_bytes() + if data[:4] != b'\x7fELF': + raise ValueError(f'not an ELF: {elf_path}') + e_phoff = struct.unpack_from(' subprocess.CompletedProcess: + flasher = board['flasher'] + origin = elf_flash_origin(f'{firmware}.elf') + script = ['halt', f'erase 0x{origin:x} 0x{origin + 4:x}', 'exit'] + f_jlink = Path(f'{board["name"]}_erase.jlink') + with f_jlink.open('w') as f: + f.writelines(f'{s}\n' for s in script) + ret = run_cmd(f'JLinkExe -USB {flasher["uid"]} {flasher["args"]} -if swd -JTAGConf -1,-1 -speed auto -NoGui 1 -ExitOnError 1 -CommandFile {f_jlink}') + f_jlink.unlink(missing_ok=True) + return ret + + +def erase_stlink(board: Board, firmware: str) -> subprocess.CompletedProcess: + flasher = board['flasher'] + return run_cmd(f'STM32_Programmer_CLI --connect port=swd sn={flasher["uid"]} --erase 0') + + +def erase_openocd(board: Board, firmware: str) -> subprocess.CompletedProcess: + flasher = board['flasher'] + return run_cmd(f'openocd -c "tcl_port disabled" -c "gdb_port disabled" -c "adapter serial {flasher["uid"]}" ' + f'{flasher["args"]} -c "init; reset halt; flash erase_sector 0 0 0; exit"') + + +def erase_openocd_adi(board: Board, firmware: str) -> subprocess.CompletedProcess: + flasher = board['flasher'] + openocd = OPENCOD_ADI_PATH / 'src' / 'openocd' + tcl_dir = OPENCOD_ADI_PATH / 'tcl' + return run_cmd(f'{openocd} -c "adapter serial {flasher["uid"]}" -s {tcl_dir} ' + f'{flasher["args"]} -c "init; reset halt; flash erase_sector 0 0 0; exit"') + + +def erase_esptool(board: Board, firmware: str) -> subprocess.CompletedProcess: + flasher = board['flasher'] + port = get_serial_dev(flasher["uid"], None, None, 0) + fw_dir = Path(f'{firmware}.bin').parent + with (fw_dir / 'config.env').open() as f: + idf_target = json.load(f)['IDF_TARGET'] + return run_cmd(f'esptool --chip {idf_target} -p {port} {flasher["args"]} erase_region 0x0 0x4000', + cwd=str(fw_dir)) + + +def erase_lm4flash(board: Board, firmware: str) -> subprocess.CompletedProcess: + # lm4flash has no erase command, but it erases the sectors it programs — so + # writing a blank (all-0xFF) image leaves the first sector erased. + flasher = board['flasher'] + blank = Path(f'{board["name"]}_blank.bin') + blank.write_bytes(b'\xff' * 4096) + ret = run_cmd(f'lm4flash -s {flasher["uid"]} {flasher["args"]} {blank}') + blank.unlink(missing_ok=True) + return ret + + # ------------------------------------------------------------- # Tests: dual # ------------------------------------------------------------- @@ -807,12 +882,17 @@ def test_host_msc_file_explorer(board): t -= 0.05 resp_text = resp.decode('utf-8', errors='ignore') + speed = None for line in resp_text.splitlines(): if 'KB/s' in line: print(f'{line.strip()} ', end='') + m = re.search(r'([\d.]+\s*[KMG]B/s)', line) # MSC read speed for the report cell + if m: + speed = 'rd ' + m.group(1).replace(' ', '') break ser.close() + return speed def test_host_msc_file_explorer_freertos(board): @@ -971,6 +1051,9 @@ def test_device_cdc_msc_throughput(board): pass print(f' CDC read {cdc_r} write {cdc_w}, MSC read {msc_r} write {msc_w} ', end='') + # compact read/write speed for the report cell, e.g. "C 652k/422k M 1.1M/783k" + short = lambda s: (s.split()[0].rstrip('0').rstrip('.') + s.split()[-1][0]) if ' ' in s else s + return f'C {short(cdc_r)}/{short(cdc_w)} M {short(msc_r)}/{short(msc_w)}' def test_device_dfu(board): @@ -1499,39 +1582,43 @@ def f1_suffix(f1: str) -> str: return '-f1_' + f1.replace(' ', '_') if f1 else '' +def find_firmware(name: str, f1: str, example: str): + """Locate a built example's firmware base path (no extension) under + cmake-build-[-f1_...]//. Accepts the single-config layout + (firmware directly in the example dir) or Ninja Multi-Config (a per-config + subdir like RelWithDebInfo/). Returns the base Path, or None if not built.""" + fw_dir = TINYUSB_ROOT / build_dir / f'cmake-build-{name}{f1_suffix(f1)}' / example + base = Path(example).name + if fw_dir.is_dir(): + for cand in [fw_dir / base, fw_dir / 'RelWithDebInfo' / base, + *(p.with_suffix('') for p in sorted(fw_dir.glob(f'*/{base}.elf')))]: + if cand.with_suffix('.elf').exists() or cand.with_suffix('.bin').exists(): + return cand + return None + + def test_example(board: Board, f1: str, example: str) -> tuple[int, str]: """ Test example firmware :param board: board dict :param f1: flags on :param example: example name - :return: (err_count, status) where err_count is 0 on success/skip or 1 on - failure, and status is one of 'pass'/'fail'/'skip' (a missing - binary counts as 'skip') + :return: (err_count, status, metric) where err_count is 0 on success/skip or + 1 on failure, status is one of 'pass'/'fail'/'skip' (a missing binary + counts as 'skip'), and metric is an optional string a test returns to + show in its report cell instead of the pass symbol (e.g. speed) """ name = board['name'] err_count = 0 result_status = 'fail' + metric = None - f1_str = f1_suffix(f1) - - fw_dir = TINYUSB_ROOT / build_dir / f'cmake-build-{name}{f1_str}' / example - base = Path(example).name - test_name = f'{name+f1_str:40} {example:30} ...' - - # firmware sits directly in the example dir (single-config Ninja) or under a - # per-config subdir like RelWithDebInfo/ (Ninja Multi-Config); accept either. - fw_name = None - if fw_dir.is_dir(): - for cand in [fw_dir / base, fw_dir / 'RelWithDebInfo' / base, - *(p.with_suffix('') for p in sorted(fw_dir.glob(f'*/{base}.elf')))]: - if cand.with_suffix('.elf').exists() or cand.with_suffix('.bin').exists(): - fw_name = cand - break + test_name = f'{name + f1_suffix(f1):40} {example:30} ...' + fw_name = find_firmware(name, f1, example) if fw_name is None: log_line(f'{test_name} Skip (no binary)') - return 0, 'skip' + return 0, 'skip', None if verbose: log_line(f'Flashing {fw_name}.elf') @@ -1558,6 +1645,8 @@ def test_example(board: Board, f1: str, example: str) -> tuple[int, str]: else: status = STATUS_OK result_status = 'pass' + # a test may return a string to show in its report cell (e.g. speed) + metric = tret if isinstance(tret, str) else None msg = f'{test_name} {status}' if last_detail: msg += f' {last_detail}' @@ -1600,7 +1689,7 @@ def test_example(board: Board, f1: str, example: str) -> tuple[int, str]: msg += f' in {time.time() - start_s:.1f}s' log_line(msg) - return err_count, result_status + return err_count, result_status, metric def build_board(board: Board) -> tuple[str, int]: @@ -1629,6 +1718,28 @@ def build_board(board: Board) -> tuple[str, int]: return name, failed +def disable_board(board: Board, f1: str): + """Quiesce the board after its tests so it stops drawing power / enumerating + USB: erase the first flash sector (vector table) where the flasher supports + it, otherwise flash device/board_test. Skipped when --skip-flash is set. + Returns (report_key, status) or None.""" + if skip_flash: + return None + name = board['name'] + erase_fn = globals().get(f'erase_{board["flasher"]["name"].lower()}') + fw = find_firmware(name, f1, 'device/board_test') + if erase_fn and fw is not None: + start_s = time.time() + ret = erase_fn(board, str(fw)) + status = 'pass' if ret.returncode == 0 else 'fail' + st = STATUS_OK if status == 'pass' else STATUS_FAILED + log_line(f'{name:40} {"erase (disable)":30} ... {st} in {time.time() - start_s:.1f}s') + return 'erase', status + # flasher has no erase support (or board_test not built): flash board_test + _ec, status, _ = test_example(board, f1, 'device/board_test') + return 'device/board_test', status + + def test_board(board: Board) -> tuple[str, int, list[str], list]: name = board['name'] flasher = board['flasher'] @@ -1678,18 +1789,17 @@ def test_board(board: Board) -> tuple[str, int, list[str], list]: for f1 in flags_on_list: cells = {} for test in test_list: - ec, status = test_example(board, f1, test) + ec, status, metric = test_example(board, f1, test) err_count += ec - cells[test] = status + cells[test] = metric if metric else status if ec > 0: failed_tests.append(test) rows.append((name + f1_suffix(f1), cells)) - # flash board_test last to disable board's usb (skipped when --skip-flash is set) - if not skip_flash: - _ec, status = test_example(board, flags_on_list[0], 'device/board_test') - if rows: - rows[0][1]['device/board_test'] = status + # disable the board's usb after its tests (erase first flash sector, or flash + # board_test where the flasher can't erase); skipped when --skip-flash is set. + # This is teardown, not a test — not recorded in the report. + disable_board(board, flags_on_list[0]) return name, err_count, sorted(set(failed_tests)), rows @@ -1701,7 +1811,7 @@ REPORT_JSON = 'hil_report.json' def render_matrix(rows_all: list) -> str: """Render rows (list of (row_label, {example: status})) as an aligned markdown matrix: columns = tests (bare names) centered, boards left-aligned.""" - canonical = device_tests + dual_tests + host_test + ['device/board_test'] + canonical = device_tests + dual_tests + host_test seen = set() for _, cells in rows_all: seen.update(cells) @@ -1714,7 +1824,10 @@ def render_matrix(rows_all: list) -> str: headers = [c.rsplit('/', 1)[-1] for c in columns] # bare example name def cell(cells, col): - return REPORT_CELL.get(cells.get(col), '') + v = cells.get(col) + if v is None: + return '' + return REPORT_CELL.get(v, v) # status symbol, or a metric string (e.g. speed) verbatim board_hdr = 'Board' board_w = max([len(board_hdr)] + [len(lbl) for lbl, _ in rows_all]) -- cgit v1.3.1 From 575a8fbcd0e5880791ea5f834649149a8f787d95 Mon Sep 17 00:00:00 2001 From: Ha Thach Date: Wed, 10 Jun 2026 18:04:54 +0700 Subject: Merge pull request #3690 from hathach/claude/board-test-idle-park hil: park boards with idle board_test instead of erasing flash --- .github/workflows/build_util.yml | 2 +- examples/device/board_test/src/main.c | 34 +++++++++-- hw/bsp/espressif/family.cmake | 7 +++ hw/bsp/family_support.cmake | 6 ++ test/hil/hil_test.py | 105 ++-------------------------------- 5 files changed, 48 insertions(+), 106 deletions(-) diff --git a/.github/workflows/build_util.yml b/.github/workflows/build_util.yml index 69b6f28d5..2532caebe 100644 --- a/.github/workflows/build_util.yml +++ b/.github/workflows/build_util.yml @@ -67,7 +67,7 @@ jobs: IAR_LMS_BEARER_TOKEN: ${{ secrets.IAR_LMS_BEARER_TOKEN }} run: | if [ "${{ inputs.toolchain }}" == "esp-idf" ]; then - docker run --rm -e MEMBROWSE_API_KEY="$MEMBROWSE_API_KEY" -v $PWD:/project -w /project espressif/idf:tinyusb python tools/build.py --target all ${{ matrix.arg }} + docker run --rm -e MEMBROWSE_API_KEY="$MEMBROWSE_API_KEY" -e CI="$CI" -v $PWD:/project -w /project espressif/idf:tinyusb python tools/build.py --target all ${{ matrix.arg }} else BUILD_PY_ARGS="-s ${{ inputs.build-system }} ${{ steps.setup-toolchain.outputs.build_option }} ${{ inputs.build-options }} --target all" if [ "${{ inputs.upload-metrics }}" = "true" ]; then diff --git a/examples/device/board_test/src/main.c b/examples/device/board_test/src/main.c index 71e7e1da7..3d8cf9979 100644 --- a/examples/device/board_test/src/main.c +++ b/examples/device/board_test/src/main.c @@ -54,6 +54,11 @@ void tusb_time_delay_ms_api(uint32_t ms) { // //--------------------------------------------------------------------+ +// CI_BUILD (defined for all CI builds, see hw/bsp/family_support.cmake) skips the +// blink/echo loop below: after HIL tests, this firmware is flashed to park the +// board in a quiet, low-power idle state (no USB, LED, or UART activity). +#ifndef CI_BUILD + // Task parameter type: ULONG for ThreadX, void* for FreeRTOS and noos #if CFG_TUSB_OS == OPT_OS_THREADX #define RTOS_PARAM ULONG @@ -107,19 +112,37 @@ static void board_test_loop(RTOS_PARAM param) { } } +#endif // CI_BUILD + int main(void) { +#ifdef CI_BUILD + // Park the board in a quiet, low-power idle loop. board_init() is intentionally + // skipped: no clocks, peripherals, USB, LED, or UART are brought up, so the MCU + // just idles after CI flashes this over a board's previous test firmware. + while (1) { + #if defined(ESP_PLATFORM) + vTaskDelay(portMAX_DELAY); // ESP runs FreeRTOS: yield this task indefinitely + #elif defined(__ARM_ARCH) || defined(__arm__) + __asm volatile("wfe"); // Cortex-M: sleep until an event + #else + // other architectures (e.g. RISC-V): spin + #endif + } + // no return: the loop never exits (an unreachable return trips IAR's Pe111) +#else board_init(); board_led_write(true); -#if CFG_TUSB_OS == OPT_OS_FREERTOS + #if CFG_TUSB_OS == OPT_OS_FREERTOS freertos_init(); -#elif CFG_TUSB_OS == OPT_OS_THREADX + #elif CFG_TUSB_OS == OPT_OS_THREADX tx_kernel_enter(); -#else + #else board_test_loop(NULL); -#endif + #endif return 0; +#endif } #ifdef ESP_PLATFORM @@ -128,6 +151,7 @@ void app_main(void) { } #endif +#ifndef CI_BUILD //--------------------------------------------------------------------+ // FreeRTOS //--------------------------------------------------------------------+ @@ -173,3 +197,5 @@ void tx_application_define(void *first_unused_memory) { 1, 1, TX_NO_TIME_SLICE, TX_AUTO_START); } #endif + +#endif // CI_BUILD diff --git a/hw/bsp/espressif/family.cmake b/hw/bsp/espressif/family.cmake index 30d5a6ac9..b3bda4ad8 100644 --- a/hw/bsp/espressif/family.cmake +++ b/hw/bsp/espressif/family.cmake @@ -44,3 +44,10 @@ set(EXTRA_COMPONENT_DIRS "src" "${CMAKE_CURRENT_LIST_DIR}/boards" "${CMAKE_CURRE set(SDKCONFIG ${CMAKE_BINARY_DIR}/sdkconfig) include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +# CI_BUILD marks firmware built in CI (GitHub Actions sets CI). Mirrors the +# non-espressif define added in family_configure_common(); applied build-wide +# here since espressif examples return before that function runs. +if(DEFINED ENV{CI}) + idf_build_set_property(COMPILE_DEFINITIONS "CI_BUILD=1" APPEND) +endif() diff --git a/hw/bsp/family_support.cmake b/hw/bsp/family_support.cmake index af2716b28..1f3952205 100644 --- a/hw/bsp/family_support.cmake +++ b/hw/bsp/family_support.cmake @@ -454,6 +454,12 @@ function(family_configure_common TARGET RTOS) BOARD_${BOARD_UPPER} ) + # CI_BUILD marks firmware built in CI (GitHub Actions sets CI). Examples can use + # it to alter behavior under test, e.g. board_test idles to park HIL boards. + if(DEFINED ENV{CI}) + target_compile_definitions(${TARGET} PUBLIC CI_BUILD=1) + endif() + # compile define from command line if(DEFINED CFLAGS_CLI) separate_arguments(CFLAGS_CLI) diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index 226e97780..45bad7a45 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -40,7 +40,6 @@ import os import random import re import select -import struct import sys import time import signal @@ -514,80 +513,6 @@ def reset_lm4flash(board): return subprocess.CompletedProcess(args=['dummy'], returncode=0) -# ------------------------------------------------------------- -# Erase: wipe the first flash sector (vector table) after a board's tests so the -# MCU faults to an idle state — no USB, lower power, and faster than programming -# device/board_test. Same (board, firmware) signature as flash_*; `firmware` is -# only used to find the flash origin (jlink) or the esp flash metadata. -# ------------------------------------------------------------- -def elf_flash_origin(elf_path: str) -> int: - """Flash base address (first PT_LOAD segment physical address) of a - little-endian ELF32 firmware — i.e. where the vector table is programmed.""" - data = Path(elf_path).read_bytes() - if data[:4] != b'\x7fELF': - raise ValueError(f'not an ELF: {elf_path}') - e_phoff = struct.unpack_from(' subprocess.CompletedProcess: - flasher = board['flasher'] - origin = elf_flash_origin(f'{firmware}.elf') - script = ['halt', f'erase 0x{origin:x} 0x{origin + 4:x}', 'exit'] - f_jlink = Path(f'{board["name"]}_erase.jlink') - with f_jlink.open('w') as f: - f.writelines(f'{s}\n' for s in script) - ret = run_cmd(f'JLinkExe -USB {flasher["uid"]} {flasher["args"]} -if swd -JTAGConf -1,-1 -speed auto -NoGui 1 -ExitOnError 1 -CommandFile {f_jlink}') - f_jlink.unlink(missing_ok=True) - return ret - - -def erase_stlink(board: Board, firmware: str) -> subprocess.CompletedProcess: - flasher = board['flasher'] - return run_cmd(f'STM32_Programmer_CLI --connect port=swd sn={flasher["uid"]} --erase 0') - - -def erase_openocd(board: Board, firmware: str) -> subprocess.CompletedProcess: - flasher = board['flasher'] - return run_cmd(f'openocd -c "tcl_port disabled" -c "gdb_port disabled" -c "adapter serial {flasher["uid"]}" ' - f'{flasher["args"]} -c "init; reset halt; flash erase_sector 0 0 0; exit"') - - -def erase_openocd_adi(board: Board, firmware: str) -> subprocess.CompletedProcess: - flasher = board['flasher'] - openocd = OPENCOD_ADI_PATH / 'src' / 'openocd' - tcl_dir = OPENCOD_ADI_PATH / 'tcl' - return run_cmd(f'{openocd} -c "adapter serial {flasher["uid"]}" -s {tcl_dir} ' - f'{flasher["args"]} -c "init; reset halt; flash erase_sector 0 0 0; exit"') - - -def erase_esptool(board: Board, firmware: str) -> subprocess.CompletedProcess: - flasher = board['flasher'] - port = get_serial_dev(flasher["uid"], None, None, 0) - fw_dir = Path(f'{firmware}.bin').parent - with (fw_dir / 'config.env').open() as f: - idf_target = json.load(f)['IDF_TARGET'] - return run_cmd(f'esptool --chip {idf_target} -p {port} {flasher["args"]} erase_region 0x0 0x4000', - cwd=str(fw_dir)) - - -def erase_lm4flash(board: Board, firmware: str) -> subprocess.CompletedProcess: - # lm4flash has no erase command, but it erases the sectors it programs — so - # writing a blank (all-0xFF) image leaves the first sector erased. - flasher = board['flasher'] - blank = Path(f'{board["name"]}_blank.bin') - blank.write_bytes(b'\xff' * 4096) - ret = run_cmd(f'lm4flash -s {flasher["uid"]} {flasher["args"]} {blank}') - blank.unlink(missing_ok=True) - return ret - - # ------------------------------------------------------------- # Tests: dual # ------------------------------------------------------------- @@ -1718,28 +1643,6 @@ def build_board(board: Board) -> tuple[str, int]: return name, failed -def disable_board(board: Board, f1: str): - """Quiesce the board after its tests so it stops drawing power / enumerating - USB: erase the first flash sector (vector table) where the flasher supports - it, otherwise flash device/board_test. Skipped when --skip-flash is set. - Returns (report_key, status) or None.""" - if skip_flash: - return None - name = board['name'] - erase_fn = globals().get(f'erase_{board["flasher"]["name"].lower()}') - fw = find_firmware(name, f1, 'device/board_test') - if erase_fn and fw is not None: - start_s = time.time() - ret = erase_fn(board, str(fw)) - status = 'pass' if ret.returncode == 0 else 'fail' - st = STATUS_OK if status == 'pass' else STATUS_FAILED - log_line(f'{name:40} {"erase (disable)":30} ... {st} in {time.time() - start_s:.1f}s') - return 'erase', status - # flasher has no erase support (or board_test not built): flash board_test - _ec, status, _ = test_example(board, f1, 'device/board_test') - return 'device/board_test', status - - def test_board(board: Board) -> tuple[str, int, list[str], list]: name = board['name'] flasher = board['flasher'] @@ -1796,10 +1699,10 @@ def test_board(board: Board) -> tuple[str, int, list[str], list]: failed_tests.append(test) rows.append((name + f1_suffix(f1), cells)) - # disable the board's usb after its tests (erase first flash sector, or flash - # board_test where the flasher can't erase); skipped when --skip-flash is set. - # This is teardown, not a test — not recorded in the report. - disable_board(board, flags_on_list[0]) + # flash board_test last to disable board's usb (skipped when --skip-flash is set); + # this is teardown/park, not a test — not recorded in the report + if not skip_flash: + test_example(board, flags_on_list[0], 'device/board_test') return name, err_count, sorted(set(failed_tests)), rows -- cgit v1.3.1 From 6f35e76667f4015ef429ace5730e20cc0037e042 Mon Sep 17 00:00:00 2001 From: Ha Thach Date: Thu, 11 Jun 2026 08:16:43 +0700 Subject: HIL: replace build.flags_on with named build variants (#3687) * test/hil: replace build.flags_on with named variant schema Boards declare build variants as `variant: [{name, flags}]` instead of `build.flags_on`. The variant `name` is the build dir (cmake-build-) and the HIL report row; `flags` is the raw CFLAGS string (-D...=1) injected via CFLAGS_CLI. No `variant` => a single build named after the board. - build.py: --build-name (dir) + --cflag= (raw CFLAGS, repeatable, =form survives the matrix's shell word-splitting); drop -f1/CFLAGS wrapping. - hil_ci_set_matrix.py: emit one build arg per variant. - hil_test.py: iterate variants; report row + build dir = variant name. - hil_ci.sh: copy all cmake-build-* dirs for -b runs. - get_deps.py: accept (ignore) --build-name/--cflag from matrix args. - tinyusb.json: migrate all 6 flags_on boards to variant. * board_test: park CI build with busy spin instead of wfe --- .github/workflows/build.yml | 10 ++++- examples/device/board_test/src/main.c | 50 ++++++++++--------------- test/hil/hfp.json | 4 ++ test/hil/hil_ci.sh | 39 +++++++++++++++++--- test/hil/hil_ci_set_matrix.py | 26 ++++++------- test/hil/hil_test.py | 69 ++++++++++++++++++----------------- test/hil/tinyusb.json | 58 ++++++++++++----------------- tools/build.py | 30 +++++++++------ tools/get_deps.py | 2 + 9 files changed, 157 insertions(+), 131 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e22ba909c..a7c7cf99a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -397,7 +397,15 @@ jobs: run: python3 tools/get_deps.py $BUILD_ARGS - name: Build - run: python3 tools/build.py --toolchain iar $BUILD_ARGS + run: | + # Each variant carries its own --build-name/--cflag, which are global to a + # single build.py invocation — so build one matrix entry at a time rather + # than joining them (joining would leak a variant's flags onto every board). + readarray -t ENTRIES < <(python test/hil/hil_ci_set_matrix.py test/hil/hfp.json | jq -r '.["arm-gcc"][]') + for entry in "${ENTRIES[@]}"; do + echo "+ tools/build.py --toolchain iar $entry" + python3 tools/build.py --toolchain iar $entry + done - name: Test on actual hardware (hardware in the loop) run: | diff --git a/examples/device/board_test/src/main.c b/examples/device/board_test/src/main.c index 3d8cf9979..96dc1bd30 100644 --- a/examples/device/board_test/src/main.c +++ b/examples/device/board_test/src/main.c @@ -57,8 +57,16 @@ void tusb_time_delay_ms_api(uint32_t ms) { // CI_BUILD (defined for all CI builds, see hw/bsp/family_support.cmake) skips the // blink/echo loop below: after HIL tests, this firmware is flashed to park the // board in a quiet, low-power idle state (no USB, LED, or UART activity). -#ifndef CI_BUILD +#ifdef CI_BUILD +int main(void) { + while (1) { + #if defined(ESP_PLATFORM) + vTaskDelay(portMAX_DELAY); + #endif + } +} +#else // Task parameter type: ULONG for ThreadX, void* for FreeRTOS and noos #if CFG_TUSB_OS == OPT_OS_THREADX #define RTOS_PARAM ULONG @@ -112,46 +120,21 @@ static void board_test_loop(RTOS_PARAM param) { } } -#endif // CI_BUILD - int main(void) { -#ifdef CI_BUILD - // Park the board in a quiet, low-power idle loop. board_init() is intentionally - // skipped: no clocks, peripherals, USB, LED, or UART are brought up, so the MCU - // just idles after CI flashes this over a board's previous test firmware. - while (1) { - #if defined(ESP_PLATFORM) - vTaskDelay(portMAX_DELAY); // ESP runs FreeRTOS: yield this task indefinitely - #elif defined(__ARM_ARCH) || defined(__arm__) - __asm volatile("wfe"); // Cortex-M: sleep until an event - #else - // other architectures (e.g. RISC-V): spin - #endif - } - // no return: the loop never exits (an unreachable return trips IAR's Pe111) -#else board_init(); board_led_write(true); - #if CFG_TUSB_OS == OPT_OS_FREERTOS +#if CFG_TUSB_OS == OPT_OS_FREERTOS freertos_init(); - #elif CFG_TUSB_OS == OPT_OS_THREADX +#elif CFG_TUSB_OS == OPT_OS_THREADX tx_kernel_enter(); - #else +#else board_test_loop(NULL); - #endif - - return 0; #endif -} -#ifdef ESP_PLATFORM -void app_main(void) { - main(); + return 0; } -#endif -#ifndef CI_BUILD //--------------------------------------------------------------------+ // FreeRTOS //--------------------------------------------------------------------+ @@ -197,5 +180,10 @@ void tx_application_define(void *first_unused_memory) { 1, 1, TX_NO_TIME_SLICE, TX_AUTO_START); } #endif - #endif // CI_BUILD + +#ifdef ESP_PLATFORM +void app_main(void) { + main(); +} +#endif diff --git a/test/hil/hfp.json b/test/hil/hfp.json index 8ba7a8f44..bb146d2fc 100644 --- a/test/hil/hfp.json +++ b/test/hil/hfp.json @@ -15,6 +15,10 @@ { "name": "stm32f746disco", "uid": "210041000C51343237303334", + "variant": [ + { "name": "stm32f746disco", "flags": "" }, + { "name": "stm32f746disco-DMA", "flags": "-DCFG_TUD_DWC2_DMA_ENABLE=1 -DCFG_TUH_DWC2_DMA_ENABLE=1" } + ], "tests": { "device": true, "host": false, "dual": false }, diff --git a/test/hil/hil_ci.sh b/test/hil/hil_ci.sh index 4f68ed067..3ec907979 100644 --- a/test/hil/hil_ci.sh +++ b/test/hil/hil_ci.sh @@ -66,14 +66,41 @@ copy_board_binaries() { } if [ -n "$BOARD" ]; then - BUILD_DIR="$ROOT_DIR/examples/cmake-build-$BOARD" - if [ ! -d "$BUILD_DIR" ]; then - echo "Error: build directory not found: $BUILD_DIR" - echo "Build first with: cd examples && cmake -DBOARD=$BOARD -G Ninja -B cmake-build-$BOARD . && cmake --build cmake-build-$BOARD" + # Copy the board's build dir plus its variant dirs. Variant names come from + # $CONFIG (they are not required to be prefixed with the board name); the + # cmake-build--* glob is kept as a fallback for ad-hoc local builds. + # Collect only dirs that actually exist, deduplicated. + declare -A SEEN_DIRS=() + BUILD_DIRS=() + add_build_dir() { + [[ -d "$1" && -z "${SEEN_DIRS[$1]:-}" ]] || return 0 + SEEN_DIRS[$1]=1 + BUILD_DIRS+=("$1") + } + shopt -s nullglob + for d in "$ROOT_DIR"/examples/cmake-build-"$BOARD" "$ROOT_DIR"/examples/cmake-build-"$BOARD"-*; do + add_build_dir "$d" + done + shopt -u nullglob + while IFS= read -r v; do + add_build_dir "$ROOT_DIR/examples/cmake-build-$v" + done < <(python3 -c ' +import json, sys +cfg = json.load(open(sys.argv[1])) +for b in cfg.get("boards", []): + if b["name"] == sys.argv[2]: + for v in b.get("variant") or []: + print(v["name"]) +' "$CONFIG" "$BOARD") + if [ ${#BUILD_DIRS[@]} -eq 0 ]; then + echo "Error: no build directory found for $BOARD under $ROOT_DIR/examples/" + echo "Build first with: cd examples && cmake --preset $BOARD && cmake --build --preset $BOARD" exit 1 fi - echo "==> Copying binaries for $BOARD" - copy_board_binaries "$BUILD_DIR" + echo "==> Copying binaries for $BOARD (${#BUILD_DIRS[@]} build dir(s))" + for d in "${BUILD_DIRS[@]}"; do + copy_board_binaries "$d" + done else echo "==> Copying all built binaries" # Use `%/` parameter expansion to strip the trailing slash from the glob — diff --git a/test/hil/hil_ci_set_matrix.py b/test/hil/hil_ci_set_matrix.py index 2cce35ae2..baa24afb1 100644 --- a/test/hil/hil_ci_set_matrix.py +++ b/test/hil/hil_ci_set_matrix.py @@ -44,19 +44,19 @@ def main(): toolchain = 'arm-gcc' build_board = f'-b {name}' - if 'build' in board: - if 'args' in board['build']: - build_board += ' ' + ' '.join(f'-D{a}' for a in board['build']['args']) - if 'flags_on' in board['build']: - for f in board['build']['flags_on']: - if f == '': - append_build_arg(toolchain, build_board) - else: - append_build_arg(toolchain, f'{build_board} -f1 {f.replace(" ", " -f1 ")}') - else: - append_build_arg(toolchain, build_board) - else: - append_build_arg(toolchain, build_board) + if 'build' in board and 'args' in board['build']: + build_board += ' ' + ' '.join(f'-D{a}' for a in board['build']['args']) + + # Each variant builds into cmake-build- with its raw CFLAGS. + # No 'variant' -> a single build named after the board. + variants = board.get('variant') or [{'name': name, 'flags': ''}] + for v in variants: + arg = build_board + if v['name'] != name: + arg += f' --build-name {v["name"]}' + for tok in v.get('flags', '').split(): + arg += f' --cflag={tok}' + append_build_arg(toolchain, arg) print(json.dumps(matrix)) diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index 45bad7a45..da13fcbaf 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -122,16 +122,21 @@ class TestsCfg(TypedDict, total=False): class BuildCfg(TypedDict, total=False): - flags_on: list[str] args: list[str] +class VariantCfg(TypedDict, total=False): + name: str # build dir (cmake-build-) and HIL report row + flags: str # raw CFLAGS, e.g. "-DCFG_TUD_DWC2_DMA_ENABLE=1" + + class Board(TypedDict): name: str uid: str tests: TestsCfg flasher: FlasherCfg build: NotRequired[BuildCfg] + variant: NotRequired[list[VariantCfg]] class HilConfig(TypedDict): @@ -223,7 +228,9 @@ def open_serial_dev(port: str): while timeout > 0: if os.path.exists(port): try: - ser = serial.Serial(port, baudrate=115200, timeout=5) + # write_timeout: a wedged device otherwise blocks ser.write() forever, + # hanging the worker until the pool/job timeout kills the whole run + ser = serial.Serial(port, baudrate=115200, timeout=5, write_timeout=5) break except serial.SerialException: print(f'serial {port} not reaady {timeout} sec') @@ -976,9 +983,9 @@ def test_device_cdc_msc_throughput(board): pass print(f' CDC read {cdc_r} write {cdc_w}, MSC read {msc_r} write {msc_w} ', end='') - # compact read/write speed for the report cell, e.g. "C 652k/422k M 1.1M/783k" + # compact read/write speed for the report cell, e.g. "✅ CDC 652k/422k MSC 1.1M/783k" short = lambda s: (s.split()[0].rstrip('0').rstrip('.') + s.split()[-1][0]) if ' ' in s else s - return f'C {short(cdc_r)}/{short(cdc_w)} M {short(msc_r)}/{short(msc_w)}' + return f'{REPORT_CELL["pass"]} CDC {short(cdc_r)}/{short(cdc_w)} MSC {short(msc_r)}/{short(msc_w)}' def test_device_dfu(board): @@ -1502,17 +1509,12 @@ host_test = [ ] -def f1_suffix(f1: str) -> str: - """Build dir / row-label suffix for a flags-on variant ('' for the default).""" - return '-f1_' + f1.replace(' ', '_') if f1 else '' - - -def find_firmware(name: str, f1: str, example: str): +def find_firmware(variant: str, example: str): """Locate a built example's firmware base path (no extension) under - cmake-build-[-f1_...]//. Accepts the single-config layout - (firmware directly in the example dir) or Ninja Multi-Config (a per-config - subdir like RelWithDebInfo/). Returns the base Path, or None if not built.""" - fw_dir = TINYUSB_ROOT / build_dir / f'cmake-build-{name}{f1_suffix(f1)}' / example + cmake-build-//. Accepts the single-config layout (firmware + directly in the example dir) or Ninja Multi-Config (a per-config subdir like + RelWithDebInfo/). Returns the base Path, or None if not built.""" + fw_dir = TINYUSB_ROOT / build_dir / f'cmake-build-{variant}' / example base = Path(example).name if fw_dir.is_dir(): for cand in [fw_dir / base, fw_dir / 'RelWithDebInfo' / base, @@ -1522,25 +1524,24 @@ def find_firmware(name: str, f1: str, example: str): return None -def test_example(board: Board, f1: str, example: str) -> tuple[int, str]: +def test_example(board: Board, variant: str, example: str) -> tuple[int, str]: """ Test example firmware :param board: board dict - :param f1: flags on + :param variant: build variant name = build dir (cmake-build-) and report row :param example: example name :return: (err_count, status, metric) where err_count is 0 on success/skip or 1 on failure, status is one of 'pass'/'fail'/'skip' (a missing binary counts as 'skip'), and metric is an optional string a test returns to show in its report cell instead of the pass symbol (e.g. speed) """ - name = board['name'] err_count = 0 result_status = 'fail' metric = None - test_name = f'{name + f1_suffix(f1):40} {example:30} ...' + test_name = f'{variant:40} {example:30} ...' - fw_name = find_firmware(name, f1, example) + fw_name = find_firmware(variant, example) if fw_name is None: log_line(f'{test_name} Skip (no binary)') return 0, 'skip', None @@ -1619,21 +1620,22 @@ def test_example(board: Board, f1: str, example: str) -> tuple[int, str]: def build_board(board: Board) -> tuple[str, int]: """Build firmware for this board via tools/build.py. - Honors board config's build.flags_on variants and build.args defines. - Output goes to cmake-build/cmake-build-BOARD[-f1_...]/ (tools/build.py layout).""" + Honors board config's variant list and build.args defines. + Output goes to cmake-build/cmake-build-/ (tools/build.py layout).""" name = board['name'] bcfg = cast(BuildCfg, board.get('build', {})) - flags_on_list = bcfg.get('flags_on', ['']) extra_defs = bcfg.get('args', []) + variants = board.get('variant') or [{'name': name, 'flags': ''}] failed = 0 - for f1 in flags_on_list: + for v in variants: cmd = [sys.executable, str(TINYUSB_ROOT / 'tools' / 'build.py'), '-b', name] for d in extra_defs: cmd += ['-D', d] - if f1: - for flag in f1.split(): - cmd += ['-f1', flag] + if v['name'] != name: + cmd += ['--build-name', v['name']] + for tok in v.get('flags', '').split(): + cmd += [f'--cflag={tok}'] if verbose: cmd.append('-v') print(f' + {" ".join(cmd)}') @@ -1684,25 +1686,24 @@ def test_board(board: Board) -> tuple[str, int, list[str], list]: err_count = 0 failed_tests = [] - rows = [] # list of (row_label, {example: status}) — one row per board[-f1] variant - flags_on_list = [""] - if 'build' in board and 'flags_on' in board['build']: - flags_on_list = board['build']['flags_on'] + rows = [] # list of (row_label, {example: status}) — one row per build variant + variants = board.get('variant') or [{'name': name, 'flags': ''}] - for f1 in flags_on_list: + for v in variants: + vname = v['name'] cells = {} for test in test_list: - ec, status, metric = test_example(board, f1, test) + ec, status, metric = test_example(board, vname, test) err_count += ec cells[test] = metric if metric else status if ec > 0: failed_tests.append(test) - rows.append((name + f1_suffix(f1), cells)) + rows.append((vname, cells)) # flash board_test last to disable board's usb (skipped when --skip-flash is set); # this is teardown/park, not a test — not recorded in the report if not skip_flash: - test_example(board, flags_on_list[0], 'device/board_test') + test_example(board, variants[0]['name'], 'device/board_test') return name, err_count, sorted(set(failed_tests)), rows diff --git a/test/hil/tinyusb.json b/test/hil/tinyusb.json index 319ee9a79..afe3c4d03 100644 --- a/test/hil/tinyusb.json +++ b/test/hil/tinyusb.json @@ -17,12 +17,10 @@ { "name": "espressif_p4_function_ev", "uid": "6055F9F98715", - "build": { - "flags_on": [ - "", - "CFG_TUD_DWC2_DMA_ENABLE CFG_TUH_DWC2_DMA_ENABLE" - ] - }, + "variant": [ + { "name": "espressif_p4_function_ev", "flags": "" }, + { "name": "espressif_p4_function_ev-DMA", "flags": "-DCFG_TUD_DWC2_DMA_ENABLE=1 -DCFG_TUH_DWC2_DMA_ENABLE=1" } + ], "tests": { "only": [ "device/cdc_msc_freertos", @@ -58,12 +56,10 @@ { "name": "espressif_s3_devkitm", "uid": "84F703C084E4", - "build": { - "flags_on": [ - "", - "CFG_TUD_DWC2_DMA_ENABLE CFG_TUH_DWC2_DMA_ENABLE" - ] - }, + "variant": [ + { "name": "espressif_s3_devkitm", "flags": "" }, + { "name": "espressif_s3_devkitm-DMA", "flags": "-DCFG_TUD_DWC2_DMA_ENABLE=1 -DCFG_TUH_DWC2_DMA_ENABLE=1" } + ], "tests": { "only": [ "device/cdc_msc_freertos", @@ -226,11 +222,9 @@ { "name": "raspberry_pi_pico", "uid": "E6614C311B764A37", - "build": { - "flags_on": [ - "CFG_TUH_RPI_PIO_USB" - ] - }, + "variant": [ + { "name": "raspberry_pi_pico", "flags": "-DCFG_TUH_RPI_PIO_USB=1" } + ], "tests": { "device": true, "host": true, @@ -374,12 +368,10 @@ { "name": "stm32f723disco", "uid": "460029001951373031313335", - "build": { - "flags_on": [ - "", - "CFG_TUH_DWC2_DMA_ENABLE" - ] - }, + "variant": [ + { "name": "stm32f723disco", "flags": "" }, + { "name": "stm32f723disco-DMA", "flags": "-DCFG_TUH_DWC2_DMA_ENABLE=1" } + ], "tests": { "device": true, "host": true, @@ -410,12 +402,10 @@ { "name": "stm32h743nucleo", "uid": "110018000951383432343236", - "build": { - "flags_on": [ - "", - "CFG_TUD_DWC2_DMA_ENABLE" - ] - }, + "variant": [ + { "name": "stm32h743nucleo", "flags": "" }, + { "name": "stm32h743nucleo-DMA", "flags": "-DCFG_TUD_DWC2_DMA_ENABLE=1 -DCFG_TUH_DWC2_DMA_ENABLE=1" } + ], "tests": { "device": true, "host": false, @@ -474,12 +464,10 @@ { "name": "stm32f769disco", "uid": "21002F000F51363531383437", - "build": { - "flags_on": [ - "", - "CFG_TUD_DWC2_DMA_ENABLE" - ] - }, + "variant": [ + { "name": "stm32f769disco", "flags": "" }, + { "name": "stm32f769disco-DMA", "flags": "-DCFG_TUD_DWC2_DMA_ENABLE=1 -DCFG_TUH_DWC2_DMA_ENABLE=1" } + ], "tests": { "device": true, "host": false, diff --git a/tools/build.py b/tools/build.py index 3c5c3c077..86bc30d28 100755 --- a/tools/build.py +++ b/tools/build.py @@ -105,16 +105,14 @@ def print_build_result(board, build_target, status, duration): # ----------------------------- # CMake # ----------------------------- -def cmake_board(board, build_args, build_flags_on, build_targets): +def cmake_board(board, build_args, build_name, build_cflags, build_targets): ret = [0, 0, 0] start_time = time.monotonic() - build_dir = f'cmake-build/cmake-build-{board}' + build_dir = f'cmake-build/cmake-build-{build_name or board}' build_flags = [] - if len(build_flags_on) > 0: - cli_flags = ' '.join(f'-D{flag}=1' for flag in build_flags_on) - build_flags.append(f'-DCFLAGS_CLI={cli_flags}') - build_dir += '-f1_' + '_'.join(build_flags_on) + if build_cflags: + build_flags.append('-DCFLAGS_CLI=' + ' '.join(build_cflags)) family = find_family(board) if family == 'espressif': @@ -194,13 +192,13 @@ def make_board(board, build_args, build_targets): # ----------------------------- # Build Family # ----------------------------- -def build_boards_list(boards, build_defines, build_system, build_flags_on, build_targets): +def build_boards_list(boards, build_defines, build_system, build_name, build_cflags, build_targets): ret = [0, 0, 0] for b in boards: r = [0, 0, 0] if build_system == 'cmake': build_args = [f'-D{d}' for d in build_defines] - r = cmake_board(b, build_args, build_flags_on, build_targets) + r = cmake_board(b, build_args, build_name, build_cflags, build_targets) elif build_system == 'make': build_args = ' '.join(f'{d}' for d in build_defines) r = make_board(b, build_args, build_targets) @@ -261,7 +259,10 @@ def main(): parser.add_argument('-t', '--toolchain', default='gcc', help='Toolchain to use, default is gcc') parser.add_argument('-s', '--build-system', default='cmake', help='Build system to use, default is cmake') parser.add_argument('-D', '--define-symbol', action='append', default=[], help='Define to pass to build system') - parser.add_argument('-f1', '--build-flags-on', action='append', default=[], help='Build flag to pass to build system') + parser.add_argument('--build-name', default=None, + help='Override build dir name (cmake-build-); default is the board name. Used for HIL variants.') + parser.add_argument('--cflag', action='append', default=[], + help='Raw compiler flag appended to CFLAGS_CLI, e.g. --cflag=-DCFG_TUD_DWC2_DMA_ENABLE=1 (repeatable)') parser.add_argument('--one-random', action='store_true', default=False, help='Build only one random board of each specified family') parser.add_argument('--one-first', action='store_true', default=False, @@ -277,7 +278,8 @@ def main(): toolchain = args.toolchain build_system = args.build_system build_defines = args.define_symbol - build_flags_on = args.build_flags_on + build_name = args.build_name + build_cflags = args.cflag one_random = args.one_random one_first = args.one_first build_targets = args.target if args.target else ['all'] @@ -290,6 +292,12 @@ def main(): print("Please specify families or board to build") return 1 + # --build-name renames the single shared build dir, so building more than one + # board with it would clobber/mix artifacts + if build_name and (len(families) > 0 or len(boards) != 1): + print("--build-name requires exactly one board (-b) and no families") + return 1 + print(build_separator) print(build_format.format('Board', 'Target', '\033[39mResult\033[0m', 'Time')) total_time = time.monotonic() @@ -310,7 +318,7 @@ def main(): all_boards.extend(get_family_boards(f, one_random, one_first)) # build all boards - result = build_boards_list(all_boards, build_defines, build_system, build_flags_on, build_targets) + result = build_boards_list(all_boards, build_defines, build_system, build_name, build_cflags, build_targets) total_time = time.monotonic() - total_time print(build_separator) diff --git a/tools/get_deps.py b/tools/get_deps.py index eb87abf6e..abe5750f1 100755 --- a/tools/get_deps.py +++ b/tools/get_deps.py @@ -366,6 +366,8 @@ def main(): parser.add_argument('-b', '--board', action='append', default=[], help='Boards to fetch') parser.add_argument('-D', '--define', action='append', default=[], help='Have no effect') parser.add_argument('-f1', '--build-flags-on', action='append', default=[], help='Have no effect') + parser.add_argument('--build-name', default=None, help='Have no effect') + parser.add_argument('--cflag', action='append', default=[], help='Have no effect') args = parser.parse_args() families = args.families -- cgit v1.3.1 From dffc57135846a4b00aca06b2f588daa6d13b67ef Mon Sep 17 00:00:00 2001 From: Ha Thach Date: Thu, 11 Jun 2026 10:17:28 +0700 Subject: Fix stm32f723disco host/cdc_msc_hid HIL: UART RX starvation + DWC2 DMA split-IN NAK storm (#3677) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix stm32f723disco host HIL: UART RX starvation + DWC2 split bulk NAK/XactErr handling (#3677) stm32f7 BSP — UART RX starvation - The host console USART shared interrupt priority with the USB OTG ISR, so a long OTG interrupt could starve RXNE and drop received bytes. Raise the USART RX IRQ above OTG_FS/OTG_HS in both the bare-metal and FreeRTOS init paths, guarded by #ifdef UART_ID so boards without a UART console keep the default OTG priority. dwc2 host — split NAK/XactErr handling - Slave mode: a persistently-NAKing split bulk/control IN poll re-armed the start-split immediately, storming the ISR and starving task context. Throttle by disabling the channel and re-arming on the resulting halt (no frame deferral). - Buffer-DMA mode: a pure split bulk-OUT NAK was unhandled, leaving the channel halted and stalling the transfer — the dominant cause of CDC echo truncation. Handle it by rewinding the buffer pointers and retrying the start-split (Programming Guide v4.20a 5.1.4.2). - Buffer-DMA mode: a split bulk-OUT XactErr was retried immediately, exhausting HCD_XFER_ERROR_MAX before the transient cleared. Throttle via channel_disable + re-arm to give the hub TT a recovery gap, mirroring slave mode. - All three are scoped to split transfers (hcsplt.split_en); non-split NAK/XactErr keep the core-handled / immediate-retry behavior. The OUT XactErr throttle also excludes periodic split, where channel_disable() is a no-op and would wedge the channel. The nak_disabled flag is generalized to retry_disabled and honors xfer->closing so an endpoint close during a throttled retry tears down cleanly. Verified on stm32f723disco HIL (slave + CFG_TUH_DWC2_DMA_ENABLE): host/cdc_msc_hid, msc_file_explorer, and device_info all pass on both variants; DMA CDC echo went from ~15-25% raw failure to 10/10 clean. --- AGENTS.md | 4 +-- hw/bsp/stm32f7/family.c | 21 +++++++++++---- src/portable/synopsys/dwc2/hcd_dwc2.c | 51 +++++++++++++++++++++++++++++++---- tools/codespell/ignore-words.txt | 1 + 4 files changed, 65 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5c9908d19..93faa6332 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,7 @@ Bias toward caution over speed. For trivial tasks, use judgment. - **Language/style:** C99, 2-space indent (no tabs), snake_case helpers, `UPPER_CASE` macros. Public APIs use `tud_`/`tuh_`; macros use `TU_`. Headers self-contained with `#if CFG_TUSB_MCU` guards. - **Safety:** no dynamic allocation; defer ISR work to task context; use `TU_ASSERT()` for error checks; always check return values; include order: C stdlib → tusb common → drivers → classes. - **Layout:** `src/` core, `hw/{mcu,bsp}/` MCU+BSP, `examples/{device,host,dual}/`, `test/{unit-test,fuzz,hil}/`, `docs/`, `tools/`. -- **Commits/PRs:** imperative mood, scoped changes, link issues, include test/build evidence. +- **Commits/PRs:** imperative mood, scoped changes, link issues, include test/build evidence. After opening a PR, monitor it and drive it to green: address automated review comments (Copilot/Codex/Claude) and fix any failing CI builds, pushing follow-up commits until checks pass and review threads are resolved. Useful: `gh pr checks --watch`, `gh pr view --comments`. - **Formatting/lint:** `clang-format` (`.clang-format`), `codespell` (`.codespellrc`), run `pre-commit run --all-files` before submitting. ## Bootstrap @@ -204,7 +204,7 @@ Device examples need real hardware to validate runtime behavior; must at least b ## References -- MCU reference manuals, datasheets, schematics: `$HOME/Documents/Calibre Library`. +- MCU reference manuals, datasheets, schematics: `$HOME/Documents/calibre-library`. - Supported MCUs/boards: `hw/bsp/` and `docs/reference/boards.rst`. - USB classes: `src/class/{cdc,hid,msc,audio,…}/` — each has `*_device.c` and `*_host.c`. - Key files: `src/tusb.h`, `src/tusb_config.h`, `tools/get_deps.py`, `tools/build.py`, `test/unit-test/project.yml`. diff --git a/hw/bsp/stm32f7/family.c b/hw/bsp/stm32f7/family.c index 7a322591b..9427ac4a6 100644 --- a/hw/bsp/stm32f7/family.c +++ b/hw/bsp/stm32f7/family.c @@ -82,8 +82,9 @@ static UART_HandleTypeDef UartHandle = {.Instance = USARTn, .OverSampling = UART_OVERSAMPLING_16, }}; -// RX ring buffer via RXNE interrupt — no HAL IT functions used (avoid HAL state conflicts) -static uint8_t uart_rx_ff_buf[32]; +// RX ring buffer via RXNE interrupt — no HAL IT functions used (avoid HAL state conflicts). +// Sized to absorb a full host-forwarding burst (>64B) when the main loop briefly stalls. +static uint8_t uart_rx_ff_buf[256]; static tu_fifo_t uart_rx_ff; void USARTn_IRQHandler(void) { @@ -142,13 +143,24 @@ void board_init(void) { // 1ms tick timer SysTick_Config(SystemCoreClock / 1000); + // Set UART interrupt higher priority than USB OTG since the F7 USART has no hardware RX FIFO, so a host example's + // UART RX must not be starved by the frequent USB host interrupts or incoming bytes overrun (ORE) and dropped. + NVIC_SetPriority(OTG_FS_IRQn, 1); + NVIC_SetPriority(OTG_HS_IRQn, 1); + #ifdef UART_ID + NVIC_SetPriority(USARTn_IRQn, 0); + #endif + #elif CFG_TUSB_OS == OPT_OS_FREERTOS // Explicitly disable systick to prevent its ISR from running before scheduler start SysTick->CTRL &= ~1U; // If freeRTOS is used, IRQ priority is limit by max syscall ( smaller is higher ) - NVIC_SetPriority(OTG_FS_IRQn, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY); - NVIC_SetPriority(OTG_HS_IRQn, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY); + NVIC_SetPriority(OTG_FS_IRQn, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY + 1); + NVIC_SetPriority(OTG_HS_IRQn, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY + 1); + #ifdef UART_ID + NVIC_SetPriority(USARTn_IRQn, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY); + #endif #endif #ifdef UART_ID @@ -156,7 +168,6 @@ void board_init(void) { HAL_UART_Init(&UartHandle); tu_fifo_config(&uart_rx_ff, uart_rx_ff_buf, sizeof(uart_rx_ff_buf), false); USARTn->CR1 |= USART_CR1_RXNEIE; - NVIC_SetPriority(USARTn_IRQn, (1 << __NVIC_PRIO_BITS) - 1); NVIC_EnableIRQ(USARTn_IRQn); #endif diff --git a/src/portable/synopsys/dwc2/hcd_dwc2.c b/src/portable/synopsys/dwc2/hcd_dwc2.c index 9ea5f33c5..84a0c6afd 100644 --- a/src/portable/synopsys/dwc2/hcd_dwc2.c +++ b/src/portable/synopsys/dwc2/hcd_dwc2.c @@ -104,6 +104,7 @@ typedef struct { uint16_t xferred_bytes; // bytes that accumulate transferred though USB bus for the whole hcd_edpt_xfer(), which can // be composed of multiple channel_xfer_start() (retry with NAK/NYET) uint16_t fifo_bytes; // bytes written/read from/to FIFO (may not be transferred on USB bus). + uint8_t retry_disabled; // 1: channel was disabled to throttle a split retry (NAK in / XactErr out); re-arm on its halt } hcd_xfer_t; typedef struct { @@ -1137,7 +1138,16 @@ static bool handle_channel_in_dma(dwc2_regs_t* dwc2, uint8_t ch_id, uint32_t hci // TU_LOG1("in hcint = %02lX\r\n", hcint); if (hcint & HCINT_HALTED) { - if (hcint & (HCINT_XFER_COMPLETE | HCINT_STALL | HCINT_BABBLE_ERR)) { + if (xfer->retry_disabled) { + // Halt from our split-NAK throttle disable (below): re-arm the start-split, or let teardown finish + // if the endpoint is closing. Programming Guide 3.5 "Halting a Channel" (p73). + xfer->retry_disabled = 0; + if (xfer->closing) { + is_done = true; + } else { + channel_send_in_token(dwc2, channel); + } + } else if (hcint & (HCINT_XFER_COMPLETE | HCINT_STALL | HCINT_BABBLE_ERR)) { const uint16_t remain_bytes = (uint16_t) hctsiz.xfer_size; const uint16_t remain_packets = hctsiz.packet_count; const uint16_t actual_len = edpt->buflen - remain_bytes; @@ -1203,7 +1213,15 @@ static bool handle_channel_in_dma(dwc2_regs_t* dwc2, uint8_t ch_id, uint32_t hci channel->hcintmsk &= ~(HCINT_NAK | HCINT_DATATOGGLE_ERR); hcsplt.split_compl = 0; // restart with start-split channel->hcsplt = hcsplt.value; - channel_xfer_in_retry(dwc2, ch_id, hcint); + // Persistent split bulk/control IN NAK (e.g. idle polled endpoint): re-enabling immediately storms + // the ISR and starves the task. Disable + re-arm on the resulting halt to throttle (like the slave + // path); no frame deferral. Programming Guide 3.5 (p73) Note permits disable on NAK/FrmOvrn splits. + if ((hcint & HCINT_NAK) && hcsplt.split_en && !channel_is_periodic(channel->hcchar)) { + xfer->retry_disabled = 1; + channel_disable(dwc2, channel); + } else { + channel_xfer_in_retry(dwc2, ch_id, hcint); + } } else if (hcint & HCINT_FARME_OVERRUN) { // retry start-split in next binterval channel_xfer_in_retry(dwc2, ch_id, hcint); @@ -1228,7 +1246,16 @@ static bool handle_channel_out_dma(dwc2_regs_t* dwc2, uint8_t ch_id, uint32_t hc // TU_LOG1("out hcint = %02lX\r\n", hcint); if (hcint & HCINT_HALTED) { - if (hcint & (HCINT_XFER_COMPLETE | HCINT_STALL)) { + if (xfer->retry_disabled) { + // Halt from our split-XactErr throttle disable (below): re-issue the start-split (pointers already + // rewound), giving the hub TT a recovery gap. Programming Guide 3.5 "Halting a Channel" (p73). + xfer->retry_disabled = 0; + if (xfer->closing) { + is_done = true; + } else { + channel_xfer_start(dwc2, ch_id); + } + } else if (hcint & (HCINT_XFER_COMPLETE | HCINT_STALL)) { is_done = true; xfer->err_count = 0; if (hcint & HCINT_XFER_COMPLETE) { @@ -1251,9 +1278,17 @@ static bool handle_channel_out_dma(dwc2_regs_t* dwc2, uint8_t ch_id, uint32_t hc xfer->result = XFER_RESULT_FAILED; is_done = true; } else { - // clean up transfer so far and start again + // Rewind, then retry the start-split. Non-periodic SPLIT throttles via channel_disable + re-arm on + // the halt (immediate re-fire exhausts the retry budget; the disable gives the hub TT a recovery + // gap, like slave). Periodic split is excluded: channel_disable() is a no-op for it, so the halt + // never fires and the channel would wedge. Non-split re-inits immediately (Programming Guide 5.1.2.3). channel_xfer_out_wrapup(dwc2, ch_id); - channel_xfer_start(dwc2, ch_id); + if (hcsplt.split_en && !channel_is_periodic(channel->hcchar)) { + xfer->retry_disabled = 1; + channel_disable(dwc2, channel); + } else { + channel_xfer_start(dwc2, ch_id); + } } } } else if (hcint & HCINT_NYET) { @@ -1271,6 +1306,12 @@ static bool handle_channel_out_dma(dwc2_regs_t* dwc2, uint8_t ch_id, uint32_t hc channel->hcsplt = hcsplt.value; channel->hcchar |= HCCHAR_CHENA; } + } else if ((hcint & HCINT_NAK) && hcsplt.split_en) { + // Split OUT NAK: rewind + retry the start-split, else the channel stalls (Programming Guide 5.1.4.2). + // Non-split OUT NAK is core-handled (5.1.2.2), so this is split-only. + xfer->err_count = 0; + channel_xfer_out_wrapup(dwc2, ch_id); + channel_xfer_start(dwc2, ch_id); } if (xfer->closing == 1) { diff --git a/tools/codespell/ignore-words.txt b/tools/codespell/ignore-words.txt index 7ce778fab..0b1aa284a 100644 --- a/tools/codespell/ignore-words.txt +++ b/tools/codespell/ignore-words.txt @@ -6,6 +6,7 @@ fro hsi inout mot +ore pris ptd ser -- cgit v1.3.1 From 0244a4f12e019406a4b73c75cd96f8efa096bbdc Mon Sep 17 00:00:00 2001 From: Ha Thach Date: Thu, 11 Jun 2026 17:06:49 +0700 Subject: README: use emoji for Supported CPUs status marks (#3691) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the ✔/⚠/✖ status symbols in the Supported CPUs table and its legend with ✅ (Supported), 🟡 (Partial support) and ❌ (Not supported by hardware) for clearer at-a-glance scanning. --- README.rst | 283 +++++++++++++++++++++++++++++++------------------------------ 1 file changed, 143 insertions(+), 140 deletions(-) diff --git a/README.rst b/README.rst index 73e1e413e..3474172be 100644 --- a/README.rst +++ b/README.rst @@ -174,154 +174,157 @@ TinyUSB is completely thread-safe by pushing all Interrupt Service Request (ISR) Supported CPUs -------------- -+--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| Manufacturer | Family | Device | Host | Highspeed | Driver | Note | -+==============+=============================+========+======+===========+========================+=========================================================================================+ -| Allwinner | F1C100s/F1C200s | ✔ | | ✔ | sunxi | musb variant | -+--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| Analog | MAX3421E | | ✔ | ✖ | max3421 | via SPI | -| +-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | MAX32 650, 666, 690, | ✔ | | ✔ | musb | 1-dir ep | -| | MAX78002 | | | | | | -+--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| Artery AT32 | F403a_407, F413 | ✔ | | | fsdev | 512 USB RAM | -| +-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | F415, F435_437, F423, | ✔ | ✔ | | dwc2 | | -| | F425, F45x | | | | | | -| +-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | F402_F405 | ✔ | ✔ | ✔ | dwc2 | F405 is HS | -+--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| Bridgetek | FT90x | ✔ | | ✔ | ft9xx | 1-dir ep | -+--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| Broadcom | BCM2711, BCM2837 | ✔ | | ✔ | dwc2 | | -+--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| Dialog | DA1469x | ✔ | ✖ | ✖ | da146xx | | -+--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| Espressif | S2, S3, H4 | ✔ | ✔ | ✖ | dwc2 | | -| ESP32 +-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | P4 | ✔ | ✔ | ✔ | dwc2 | | -| +-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | S31 | ✔ | ✔ | ✔ | dwc2 | | -+--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| GigaDevice | GD32VF103 | ✔ | | ✖ | dwc2 | | -+--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| HPMicro | HPM6750 | ✔ | ✔ | ✔ | ci_hs, ehci | | -+--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| Infineon | XMC4500 | ✔ | ✔ | ✖ | dwc2 | | -+--------------+-----+-----------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| MicroChip | SAM | D11, D21, L21, L22 | ✔ | | ✖ | samd | | -| | +-----------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | | D51, E5x | ✔ | | ✖ | samd | | -| | +-----------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | | G55 | ✔ | | ✖ | samg | 1-dir ep | -| | +-----------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | | E70,S70,V70,V71 | ✔ | | ✔ | samx7x | 1-dir ep | -| +-----+-----------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | PIC | 24 | ✔ | | | pic | ci_fs variant | -| | +-----------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | | 32 mm, mk, mx | ✔ | | | pic | ci_fs variant | -| | +-----------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | | dsPIC33 | ✔ | | | pic | ci_fs variant | -| | +-----------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | | 32mz | ✔ | | | pic32mz | musb variant | -+--------------+-----+-----------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| MindMotion | mm32 | ✔ | | ✖ | mm32f327x_otg | ci_fs variant | -+--------------+-----+-----------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| NordicSemi | nRF52, nRF53 | ✔ | ✖ | ✖ | nrf5x | only ep8 is ISO | -| +-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | nRF54 | ✔ | ✖ | ✔ | dwc2 | | -+--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| Nuvoton | NUC120 | ✔ | ✖ | ✖ | nuc120 | | -| +-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | NUC121/NUC125, NUC126 | ✔ | ✖ | ✖ | nuc121 | | -| +-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | NUC505 | ✔ | | ✔ | nuc505 | | -+--------------+---------+-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| NXP | iMXRT | RT 10xx, 11xx | ✔ | ✔ | ✔ | ci_hs, ehci | | -| +---------+-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | Kinetis | KL | ✔ | ⚠ | ✖ | ci_fs, khci | | -| | +-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | | K32L2 | ✔ | | ✖ | khci | ci_fs variant | -| +---------+-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | LPC | 11u, 13, 15 | ✔ | ✖ | ✖ | lpc_ip3511 | | -| | +-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | | 17, 40 | ✔ | ⚠ | ✖ | lpc17_40, ohci | | -| | +-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | | 18, 43 | ✔ | ✔ | ✔ | ci_hs, ehci | | -| | +-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | | 51u | ✔ | ✖ | ✖ | lpc_ip3511 | | -| | +-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | | 54 | ⚠ | ⚠ | ✔ | lpc_ip3511, lpc_ip3516 | `NRND, see device issues `_ | -| | +-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | | 55 | ✔ | ✔ | ✔ | lpc_ip3511, lpc_ip3516 | | -| +---------+-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | MCX | N9 | ✔ | | ✔ | ci_fs, ci_hs, ehci | | -| | +-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | | A15 | ✔ | | | ci_fs | | -| +---------+-------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | RW61x | ✔ | ✔ | ✔ | ci_hs, ehci | | -+--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| Raspberry Pi | RP2040, RP2350 | ✔ | ✔ | ✖ | rp2040, pio_usb | | -+--------------+-----+-----------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| Renesas | RX | 63N, 65N, 72N | ✔ | ✔ | ✖ | rusb2 | | -| +-----+-----------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | RA | 4M1, 4M3, 6M1 | ✔ | ✔ | ✖ | rusb2 | | -| | +-----------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | | 6M5 | ✔ | ✔ | ✔ | rusb2 | | -+--------------+-----+-----------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| Silabs | EFM32GG12 | ✔ | | ✖ | dwc2 | | -+--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| Sony | CXD56 | ✔ | ✖ | ✔ | cxd56 | | -+--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| ST STM32 | F0, F3, L0, L1, L5, WBx5 | ✔ | ✖ | ✖ | stm32_fsdev | | -| +----+------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | F1 | 102, 103 | ✔ | ✖ | ✖ | stm32_fsdev | 512 USB RAM | -| | +------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | | 105, 107 | ✔ | ✔ | ✖ | dwc2 | | -| +----+------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | F2, F4, F7, H7, H7RS | ✔ | ✔ | ✔ | dwc2 | | -| +-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | C0, G0, H5, U3 | ✔ | ✔ | ✖ | stm32_fsdev | 2KB USB RAM | -| +-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | G4 | ✔ | ✖ | ✖ | stm32_fsdev | 1KB USB RAM | -| +----+------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | L4 | 4x2, 4x3 | ✔ | ✖ | ✖ | stm32_fsdev | 1KB USB RAM | -| | +------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | | 4x5, 4x6, 4+ | ✔ | ✔ | ✖ | dwc2 | | -| +----+------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | N6 | ✔ | ✔ | ✔ | dwc2 | | -| +-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | U0 | ✔ | ✖ | ✖ | stm32_fsdev | 1KB USB RAM | -| +----+------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | U5 | 535, 545 | ✔ | ✔ | ✖ | stm32_fsdev | 2KB USB RAM | -| | +------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | | 575, 585 | ✔ | ✔ | ✖ | dwc2 | | -| | +------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | | 59x,5Ax,5Fx,5Gx | ✔ | ✔ | ✔ | dwc2 | | -+--------------+----+------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| TI | MSP430 | ✔ | ✖ | ✖ | msp430x5xx | | -| +-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | MSP432E4, TM4C123 | ✔ | | ✖ | musb | | -+--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| ValentyUSB | eptri | ✔ | ✖ | ✖ | eptri | | -+--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| WCH | CH32F20x | ⚠ | | ✔ | ch32_usbhs | `ISO data loss `_ | -| +-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | CH32V20x | ⚠ | | ✖ | stm32_fsdev/ch32_usbfs | `ISO data loss `_ | -| +-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ -| | CH32V305, CH32V307 | ⚠ | | ✔ | ch32_usbfs/hs | `ISO data loss `_ | -+--------------+-----------------------------+--------+------+-----------+------------------------+-----------------------------------------------------------------------------------------+ ++--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ +| Manufacturer | Family | Device | Host | Highspeed | Driver | Note | ++==============+=============================+========+======+===========+========================+====================+ +| Allwinner | F1C100s/F1C200s | ✅ | | ✅ | sunxi | musb variant | ++--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ +| Analog | MAX3421E | | ✅ | ❌ | max3421 | via SPI | +| +-----------------------------+--------+------+-----------+------------------------+--------------------+ +| | MAX32 650, 666, 690, | ✅ | | ✅ | musb | 1-dir ep | +| | MAX78002 | | | | | | ++--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ +| Artery AT32 | F403a_407, F413 | ✅ | | | fsdev | 512 USB RAM | +| +-----------------------------+--------+------+-----------+------------------------+--------------------+ +| | F415, F435_437, F423, | ✅ | ✅ | | dwc2 | | +| | F425, F45x | | | | | | +| +-----------------------------+--------+------+-----------+------------------------+--------------------+ +| | F402_F405 | ✅ | ✅ | ✅ | dwc2 | F405 is HS | ++--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ +| Bridgetek | FT90x | ✅ | | ✅ | ft9xx | 1-dir ep | ++--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ +| Broadcom | BCM2711, BCM2837 | ✅ | | ✅ | dwc2 | | ++--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ +| Dialog | DA1469x | ✅ | ❌ | ❌ | da146xx | | ++--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ +| Espressif | S2, S3, H4 | ✅ | ✅ | ❌ | dwc2 | | +| ESP32 +-----------------------------+--------+------+-----------+------------------------+--------------------+ +| | P4 | ✅ | ✅ | ✅ | dwc2 | | +| +-----------------------------+--------+------+-----------+------------------------+--------------------+ +| | S31 | ✅ | ✅ | ✅ | dwc2 | | ++--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ +| GigaDevice | GD32VF103 | ✅ | | ❌ | dwc2 | | ++--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ +| HPMicro | HPM6750 | ✅ | ✅ | ✅ | ci_hs, ehci | | ++--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ +| Infineon | XMC4500 | ✅ | ✅ | ❌ | dwc2 | | ++--------------+-----+-----------------------+--------+------+-----------+------------------------+--------------------+ +| MicroChip | SAM | D11, D21, L21, L22 | ✅ | | ❌ | samd | | +| | +-----------------------+--------+------+-----------+------------------------+--------------------+ +| | | D51, E5x | ✅ | | ❌ | samd | | +| | +-----------------------+--------+------+-----------+------------------------+--------------------+ +| | | G55 | ✅ | | ❌ | samg | 1-dir ep | +| | +-----------------------+--------+------+-----------+------------------------+--------------------+ +| | | E70,S70,V70,V71 | ✅ | | ✅ | samx7x | 1-dir ep | +| +-----+-----------------------+--------+------+-----------+------------------------+--------------------+ +| | PIC | 24 | ✅ | | | pic | ci_fs variant | +| | +-----------------------+--------+------+-----------+------------------------+--------------------+ +| | | 32 mm, mk, mx | ✅ | | | pic | ci_fs variant | +| | +-----------------------+--------+------+-----------+------------------------+--------------------+ +| | | dsPIC33 | ✅ | | | pic | ci_fs variant | +| | +-----------------------+--------+------+-----------+------------------------+--------------------+ +| | | 32mz | ✅ | | | pic32mz | musb variant | ++--------------+-----+-----------------------+--------+------+-----------+------------------------+--------------------+ +| MindMotion | mm32 | ✅ | | ❌ | mm32f327x_otg | ci_fs variant | ++--------------+-----+-----------------------+--------+------+-----------+------------------------+--------------------+ +| NordicSemi | nRF52, nRF53 | ✅ | ❌ | ❌ | nrf5x | only ep8 is ISO | +| +-----------------------------+--------+------+-----------+------------------------+--------------------+ +| | nRF54 | ✅ | ❌ | ✅ | dwc2 | | ++--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ +| Nuvoton | NUC120 | ✅ | ❌ | ❌ | nuc120 | | +| +-----------------------------+--------+------+-----------+------------------------+--------------------+ +| | NUC121/NUC125, NUC126 | ✅ | ❌ | ❌ | nuc121 | | +| +-----------------------------+--------+------+-----------+------------------------+--------------------+ +| | NUC505 | ✅ | | ✅ | nuc505 | | ++--------------+---------+-------------------+--------+------+-----------+------------------------+--------------------+ +| NXP | iMXRT | RT 10xx, 11xx | ✅ | ✅ | ✅ | ci_hs, ehci | | +| +---------+-------------------+--------+------+-----------+------------------------+--------------------+ +| | Kinetis | KL | ✅ | 🟡 | ❌ | ci_fs, khci | | +| | +-------------------+--------+------+-----------+------------------------+--------------------+ +| | | K32L2 | ✅ | | ❌ | khci | ci_fs variant | +| +---------+-------------------+--------+------+-----------+------------------------+--------------------+ +| | LPC | 11u, 13, 15 | ✅ | ❌ | ❌ | lpc_ip3511 | | +| | +-------------------+--------+------+-----------+------------------------+--------------------+ +| | | 17, 40 | ✅ | 🟡 | ❌ | lpc17_40, ohci | | +| | +-------------------+--------+------+-----------+------------------------+--------------------+ +| | | 18, 43 | ✅ | ✅ | ✅ | ci_hs, ehci | | +| | +-------------------+--------+------+-----------+------------------------+--------------------+ +| | | 51u | ✅ | ❌ | ❌ | lpc_ip3511 | | +| | +-------------------+--------+------+-----------+------------------------+--------------------+ +| | | 54 | 🟡 | 🟡 | ✅ | lpc_ip3511, lpc_ip3516 | NRND [1]_ | +| | +-------------------+--------+------+-----------+------------------------+--------------------+ +| | | 55 | ✅ | ✅ | ✅ | lpc_ip3511, lpc_ip3516 | | +| +---------+-------------------+--------+------+-----------+------------------------+--------------------+ +| | MCX | N9 | ✅ | | ✅ | ci_fs, ci_hs, ehci | | +| | +-------------------+--------+------+-----------+------------------------+--------------------+ +| | | A15 | ✅ | | | ci_fs | | +| +---------+-------------------+--------+------+-----------+------------------------+--------------------+ +| | RW61x | ✅ | ✅ | ✅ | ci_hs, ehci | | ++--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ +| Raspberry Pi | RP2040, RP2350 | ✅ | ✅ | ❌ | rp2040, pio_usb | | ++--------------+-----+-----------------------+--------+------+-----------+------------------------+--------------------+ +| Renesas | RX | 63N, 65N, 72N | ✅ | ✅ | ❌ | rusb2 | | +| +-----+-----------------------+--------+------+-----------+------------------------+--------------------+ +| | RA | 4M1, 4M3, 6M1 | ✅ | ✅ | ❌ | rusb2 | | +| | +-----------------------+--------+------+-----------+------------------------+--------------------+ +| | | 6M5 | ✅ | ✅ | ✅ | rusb2 | | ++--------------+-----+-----------------------+--------+------+-----------+------------------------+--------------------+ +| Silabs | EFM32GG12 | ✅ | | ❌ | dwc2 | | ++--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ +| Sony | CXD56 | ✅ | ❌ | ✅ | cxd56 | | ++--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ +| ST STM32 | F0, F3, L0, L1, L5, WBx5 | ✅ | ❌ | ❌ | stm32_fsdev | | +| +----+------------------------+--------+------+-----------+------------------------+--------------------+ +| | F1 | 102, 103 | ✅ | ❌ | ❌ | stm32_fsdev | 512 USB RAM | +| | +------------------------+--------+------+-----------+------------------------+--------------------+ +| | | 105, 107 | ✅ | ✅ | ❌ | dwc2 | | +| +----+------------------------+--------+------+-----------+------------------------+--------------------+ +| | F2, F4, F7, H7, H7RS | ✅ | ✅ | ✅ | dwc2 | | +| +-----------------------------+--------+------+-----------+------------------------+--------------------+ +| | C0, G0, H5, U3 | ✅ | ✅ | ❌ | stm32_fsdev | 2KB USB RAM | +| +-----------------------------+--------+------+-----------+------------------------+--------------------+ +| | G4 | ✅ | ❌ | ❌ | stm32_fsdev | 1KB USB RAM | +| +----+------------------------+--------+------+-----------+------------------------+--------------------+ +| | L4 | 4x2, 4x3 | ✅ | ❌ | ❌ | stm32_fsdev | 1KB USB RAM | +| | +------------------------+--------+------+-----------+------------------------+--------------------+ +| | | 4x5, 4x6, 4+ | ✅ | ✅ | ❌ | dwc2 | | +| +----+------------------------+--------+------+-----------+------------------------+--------------------+ +| | N6 | ✅ | ✅ | ✅ | dwc2 | | +| +-----------------------------+--------+------+-----------+------------------------+--------------------+ +| | U0 | ✅ | ❌ | ❌ | stm32_fsdev | 1KB USB RAM | +| +----+------------------------+--------+------+-----------+------------------------+--------------------+ +| | U5 | 535, 545 | ✅ | ✅ | ❌ | stm32_fsdev | 2KB USB RAM | +| | +------------------------+--------+------+-----------+------------------------+--------------------+ +| | | 575, 585 | ✅ | ✅ | ❌ | dwc2 | | +| | +------------------------+--------+------+-----------+------------------------+--------------------+ +| | | 59x,5Ax,5Fx,5Gx | ✅ | ✅ | ✅ | dwc2 | | ++--------------+----+------------------------+--------+------+-----------+------------------------+--------------------+ +| TI | MSP430 | ✅ | ❌ | ❌ | msp430x5xx | | +| +-----------------------------+--------+------+-----------+------------------------+--------------------+ +| | MSP432E4, TM4C123 | ✅ | | ❌ | musb | | ++--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ +| ValentyUSB | eptri | ✅ | ❌ | ❌ | eptri | | ++--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ +| WCH | CH32F20x | 🟡 | | ✅ | ch32_usbhs | ISO data loss [2]_ | +| +-----------------------------+--------+------+-----------+------------------------+--------------------+ +| | CH32V20x | 🟡 | | ❌ | stm32_fsdev/ch32_usbfs | ISO data loss [2]_ | +| +-----------------------------+--------+------+-----------+------------------------+--------------------+ +| | CH32V305, CH32V307 | 🟡 | | ✅ | ch32_usbfs/hs | ISO data loss [2]_ | ++--------------+-----------------------------+--------+------+-----------+------------------------+--------------------+ Table Legend ^^^^^^^^^^^^ ========= ========================= -✔ Supported -⚠ Partial support -✖ Not supported by hardware +✅ Supported +🟡 Partial support +❌ Not supported by hardware \[empty\] Unknown ========= ========================= +.. [1] NRND (Not Recommended for New Design), see `NXP LPC54600 issues `_ +.. [2] ISO data loss, see `WCH CH32F20x/CH32V20x/CH32V30x issues `_ + Development Tools ----------------- -- cgit v1.3.1