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(-) (limited to 'examples/device') 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 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(-) (limited to 'examples/device') 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 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(-) (limited to 'examples/device') 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(-) (limited to 'examples/device') 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 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(+) (limited to 'examples/device') 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 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(-) (limited to 'examples/device') 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(-) (limited to 'examples/device') 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