summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHa Thach <[email protected]>2026-06-10 18:04:54 +0700
committerGitHub <[email protected]>2026-06-10 18:04:54 +0700
commit575a8fbcd0e5880791ea5f834649149a8f787d95 (patch)
tree07b949eba780ba8cbcce27ab929d62d36e20dc7c
parent474ea5684d3a096abc7b61f06b791cdc3879f2b6 (diff)
Merge pull request #3690 from hathach/claude/board-test-idle-park
hil: park boards with idle board_test instead of erasing flash
-rw-r--r--.github/workflows/build_util.yml2
-rw-r--r--examples/device/board_test/src/main.c34
-rw-r--r--hw/bsp/espressif/family.cmake7
-rw-r--r--hw/bsp/family_support.cmake6
-rwxr-xr-xtest/hil/hil_test.py105
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
@@ -515,80 +514,6 @@ def reset_lm4flash(board):
# -------------------------------------------------------------
-# 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('<I', data, 0x1c)[0]
- e_phentsize = struct.unpack_from('<H', data, 0x2a)[0]
- e_phnum = struct.unpack_from('<H', data, 0x2c)[0]
- for i in range(e_phnum):
- p_type, _off, _vaddr, p_paddr = struct.unpack_from('<IIII', data, e_phoff + i * e_phentsize)
- if p_type == 1: # PT_LOAD
- return p_paddr
- raise ValueError(f'no PT_LOAD segment in {elf_path}')
-
-
-def erase_jlink(board: Board, firmware: str) -> 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
# -------------------------------------------------------------
def test_dual_host_info_to_device_cdc(board):
@@ -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