From 0dd483dbce584702a20577df86681d73790082a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20S=C3=A1ez?= Date: Sat, 14 Mar 2026 00:12:05 +0100 Subject: [PATCH 01/10] Added UART3 in MSP to debug through UART (In the future it will be refactored to a new UARTDomain, but it does the work so far) --- Core/Src/Runes/Runes.cpp | 88 +++++------------------------------- Core/Src/stm32h7xx_hal_msp.c | 53 +++++++++++++++++++++- 2 files changed, 62 insertions(+), 79 deletions(-) diff --git a/Core/Src/Runes/Runes.cpp b/Core/Src/Runes/Runes.cpp index fd9812f3..a07b2e9b 100644 --- a/Core/Src/Runes/Runes.cpp +++ b/Core/Src/Runes/Runes.cpp @@ -74,12 +74,23 @@ UART::Instance UART::instance2 = { .word_length = UART_WORDLENGTH_8B, }; +UART::Instance UART::instance3 = { + .TX = PD8, + .RX = PD9, + .huart = &huart3, + .instance = USART3, + .baud_rate = 115200, + .word_length = UART_WORDLENGTH_8B, +}; + UART::Peripheral UART::uart1 = UART::Peripheral::peripheral1; UART::Peripheral UART::uart2 = UART::Peripheral::peripheral2; +UART::Peripheral UART::uart3 = UART::Peripheral::peripheral3; unordered_map UART::available_uarts = { {UART::uart1, &UART::instance1}, {UART::uart2, &UART::instance2}, + {UART::uart3, &UART::instance3}, }; uint8_t UART::printf_uart = 0; @@ -87,83 +98,6 @@ bool UART::printf_ready = false; #endif -/************************************************ - * ADC - ***********************************************/ -#if 0 // Legacy ADC (replaced by NewADC). Kept here only as reference. -#if defined(HAL_ADC_MODULE_ENABLED) && defined(HAL_LPTIM_MODULE_ENABLED) - -LowPowerTimer lptim1(*LPTIM1, hlptim1, LPTIM1_PERIOD, "LPTIM 1"); -LowPowerTimer lptim2(*LPTIM2, hlptim2, LPTIM2_PERIOD, "LPTIM 2"); -LowPowerTimer lptim3(*LPTIM3, hlptim3, LPTIM3_PERIOD, "LPTIM 3"); - -vector channels1 = {}; -vector channels2 = {}; -vector channels3 = {}; - -ST_LIB::DMA_Domain::Instance dma_adc1 = {hdma_adc1}; -ST_LIB::DMA_Domain::Instance dma_adc2 = {hdma_adc2}; -ST_LIB::DMA_Domain::Instance dma_adc3 = {hdma_adc3}; - -ADC::InitData init_data1(ADC1, ADC_RESOLUTION_16B, ADC_EXTERNALTRIG_LPTIM1_OUT, - channels1, &dma_adc1, "ADC 1"); -ADC::InitData init_data2(ADC2, ADC_RESOLUTION_16B, ADC_EXTERNALTRIG_LPTIM2_OUT, - channels2, &dma_adc2, "ADC 2"); -ADC::InitData init_data3(ADC3, ADC_RESOLUTION_12B, ADC_EXTERNALTRIG_LPTIM3_OUT, - channels3, &dma_adc3, "ADC 3"); - -ADC::Peripheral ADC::peripherals[3] = { - ADC::Peripheral(&hadc1, lptim1, init_data1), - ADC::Peripheral(&hadc2, lptim2, init_data2), - ADC::Peripheral(&hadc3, lptim3, init_data3) -}; - -map ADC::available_instances = { - {PF11, Instance(&peripherals[0], ADC_CHANNEL_2)}, - {PF12, Instance(&peripherals[0], ADC_CHANNEL_6)}, - {PF13, Instance(&peripherals[1], ADC_CHANNEL_2)}, - {PF14, Instance(&peripherals[1], ADC_CHANNEL_6)}, - {PF5, Instance(&peripherals[2], ADC_CHANNEL_4)}, - {PF6, Instance(&peripherals[2], ADC_CHANNEL_8)}, - {PF7, Instance(&peripherals[2], ADC_CHANNEL_3)}, - {PF8, Instance(&peripherals[2], ADC_CHANNEL_7)}, - {PF9, Instance(&peripherals[2], ADC_CHANNEL_2)}, - {PF10, Instance(&peripherals[2], ADC_CHANNEL_6)}, - {PC2, Instance(&peripherals[2], ADC_CHANNEL_0)}, - {PC3, Instance(&peripherals[2], ADC_CHANNEL_1)}, - {PF10, Instance(&peripherals[2], ADC_CHANNEL_6)}, - {PC0, Instance(&peripherals[0], ADC_CHANNEL_10)}, - {PA0, Instance(&peripherals[0], ADC_CHANNEL_16)}, - {PA3, Instance(&peripherals[0], ADC_CHANNEL_15)}, - {PA4, Instance(&peripherals[0], ADC_CHANNEL_18)}, - {PA5, Instance(&peripherals[0], ADC_CHANNEL_19)}, - {PA6, Instance(&peripherals[0], ADC_CHANNEL_3)}, - {PB0, Instance(&peripherals[0], ADC_CHANNEL_9)}, - {PB1, Instance(&peripherals[0], ADC_CHANNEL_5)} -}; - -uint32_t ADC::ranks[16] = { - ADC_REGULAR_RANK_1, - ADC_REGULAR_RANK_2, - ADC_REGULAR_RANK_3, - ADC_REGULAR_RANK_4, - ADC_REGULAR_RANK_5, - ADC_REGULAR_RANK_6, - ADC_REGULAR_RANK_7, - ADC_REGULAR_RANK_8, - ADC_REGULAR_RANK_9, - ADC_REGULAR_RANK_10, - ADC_REGULAR_RANK_11, - ADC_REGULAR_RANK_12, - ADC_REGULAR_RANK_13, - ADC_REGULAR_RANK_14, - ADC_REGULAR_RANK_15, - ADC_REGULAR_RANK_16 -}; - -#endif -#endif - /************************************************ * I2C ***********************************************/ diff --git a/Core/Src/stm32h7xx_hal_msp.c b/Core/Src/stm32h7xx_hal_msp.c index bf984e42..a2b33bd9 100644 --- a/Core/Src/stm32h7xx_hal_msp.c +++ b/Core/Src/stm32h7xx_hal_msp.c @@ -92,7 +92,7 @@ void HAL_MspInit(void) { */ void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc) { (void)hadc; - /* ADC MSP is handled by HALAL/Services/ADC/NewADC.hpp */ + /* ADC MSP is handled by HALAL/Services/ADC/ADC.hpp */ } /** @@ -103,7 +103,7 @@ void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc) { */ void HAL_ADC_MspDeInit(ADC_HandleTypeDef* hadc) { (void)hadc; - /* ADC MSP is handled by HALAL/Services/ADC/NewADC.hpp */ + /* ADC MSP is handled by HALAL/Services/ADC/ADC.hpp */ } /** @@ -711,6 +711,38 @@ void HAL_UART_MspInit(UART_HandleTypeDef* huart) { /* USER CODE BEGIN USART2_MspInit 1 */ /* USER CODE END USART2_MspInit 1 */ + } else if (huart->Instance == USART3) { + /* USER CODE BEGIN USART3_MspInit 0 */ + + /* USER CODE END USART3_MspInit 0 */ + + /** Initializes the peripherals clock + */ + PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_USART3; + PeriphClkInitStruct.Usart234578ClockSelection = RCC_USART234578CLKSOURCE_D2PCLK1; + if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct) != HAL_OK) { + Error_Handler(); + } + + /* Peripheral clock enable */ + __HAL_RCC_USART3_CLK_ENABLE(); + + __HAL_RCC_GPIOD_CLK_ENABLE(); + /**USART3 GPIO Configuration + PD8 ------> USART3_TX + PD9 ------> + * USART3_RX + */ + GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9; + GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; + GPIO_InitStruct.Pull = GPIO_NOPULL; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; + GPIO_InitStruct.Alternate = GPIO_AF7_USART3; + HAL_GPIO_Init(GPIOD, &GPIO_InitStruct); + + /* USER CODE BEGIN USART3_MspInit 1 */ + + /* USER CODE END USART3_MspInit 1 */ } } @@ -753,6 +785,23 @@ void HAL_UART_MspDeInit(UART_HandleTypeDef* huart) { /* USER CODE BEGIN USART2_MspDeInit 1 */ /* USER CODE END USART2_MspDeInit 1 */ + } else if (huart->Instance == USART3) { + /* USER CODE BEGIN USART3_MspDeInit 0 */ + + /* USER CODE END USART3_MspDeInit 0 */ + /* Peripheral clock disable */ + __HAL_RCC_USART3_CLK_DISABLE(); + + /**USART3 GPIO Configuration + PD8 ------> USART3_TX + PD9 ------> + * USART3_RX + */ + HAL_GPIO_DeInit(GPIOD, GPIO_PIN_8 | GPIO_PIN_9); + + /* USER CODE BEGIN USART3_MspDeInit 1 */ + + /* USER CODE END USART3_MspDeInit 1 */ } } From 27efd4475081bd6b861528558b6200ae223d14ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20S=C3=A1ez?= Date: Sat, 14 Mar 2026 00:13:37 +0100 Subject: [PATCH 02/10] Added ADC example with one and two channels on the same peripheral --- Core/Src/Examples/ExampleADC.cpp | 157 ++++++++++++++++++++++++++++++- deps/ST-LIB | 2 +- tools/build-example.sh | 43 ++++++--- 3 files changed, 183 insertions(+), 19 deletions(-) mode change 100755 => 100644 tools/build-example.sh diff --git a/Core/Src/Examples/ExampleADC.cpp b/Core/Src/Examples/ExampleADC.cpp index defafa2f..b507e4a1 100644 --- a/Core/Src/Examples/ExampleADC.cpp +++ b/Core/Src/Examples/ExampleADC.cpp @@ -1,15 +1,92 @@ #ifdef EXAMPLE_ADC +#include +#include + #include "main.h" #include "ST-LIB.hpp" using namespace ST_LIB; +namespace { + +struct ExampleInput { + GPIODomain::Pin pin; + const char* label; +}; + +UART::Peripheral& default_terminal_uart() { +#ifdef NUCLEO + return UART::uart3; +#else + return UART::uart2; +#endif +} + +const char* terminal_hint() { +#ifdef NUCLEO + return "Terminal: ST-LINK VCP over USB (USART3, 115200 8N1)"; +#else + return "Terminal: USART2 on PD5/PD6 (115200 8N1)"; +#endif +} + +[[maybe_unused]] const char* single_channel_wiring_hint() { +#ifdef NUCLEO + return "Connect PA0 to GND, 3V3 or a potentiometer. On NUCLEO-H723ZG this is CN10 pin 29 / " + "D32, not Arduino A0."; +#else + return "Connect PA0 to GND, 3V3 or a potentiometer and watch the terminal output."; +#endif +} + +[[maybe_unused]] const char* dual_channel_wiring_hint() { +#ifdef NUCLEO + return "Connect PA0 and PC0 to two analog sources. PA1 is tied to the Ethernet PHY on " + "NUCLEO-H723ZG via SB57 and is not a safe ADC test pin."; +#else + return "Connect PA0 and PA1 to two analog sources to validate multichannel DMA."; +#endif +} + +void start_terminal(UART::Peripheral& uart) { +#ifdef HAL_UART_MODULE_ENABLED + if (!UART::set_up_printf(uart)) { + ErrorHandler("Unable to set up UART printf for ADC example"); + } + UART::start(); +#else + (void)uart; +#endif +} + +void print_banner(const char* title, const char* wiring_hint, const char* columns_hint) { + printf("\n\r=== %s ===\n\r", title); + printf("%s\n\r", terminal_hint()); + printf("%s\n\r", wiring_hint); + printf("Columns: %s\n\r\n\r", columns_hint); +} + +consteval ExampleInput single_channel_input() { return {ST_LIB::PA0, "PA0"}; } + +consteval ExampleInput dual_channel_input_0() { return {ST_LIB::PA0, "PA0"}; } + +consteval ExampleInput dual_channel_input_1() { +#ifdef NUCLEO + return {ST_LIB::PC0, "PC0"}; +#else + return {ST_LIB::PA1, "PA1"}; +#endif +} + +} // namespace + #ifdef TEST_0 constinit float adc_value = 0.0f; +constexpr auto adc_input = single_channel_input(); constexpr auto adc = ADCDomain::ADC( - ST_LIB::PA0, + adc_input.pin, adc_value, ADCDomain::Resolution::BITS_12, ADCDomain::SampleTime::CYCLES_8_5 @@ -18,16 +95,90 @@ constexpr auto adc = ADCDomain::ADC( int main(void) { using ExampleADCBoard = ST_LIB::Board; ExampleADCBoard::init(); + start_terminal(default_terminal_uart()); auto& adc_instance = ExampleADCBoard::instance_of(); - // Ready to compile for Nucleo. Validate by wiring PA0 to 3.3V and then to GND, - // and watch adc_value in the debugger to confirm the change. + print_banner("ADC single-channel example", single_channel_wiring_hint(), "t_ms raw voltage[V]"); + printf("Reading input: %s\n\r\n\r", adc_input.label); + + uint32_t sample_index = 0; while (1) { adc_instance.read(); + const float raw = adc_instance.get_raw(); + const float voltage = adc_instance.get_value(); + + printf("%10lu %8.0f %10.4f\n\r", HAL_GetTick(), raw, voltage); + + ++sample_index; + if ((sample_index % 20U) == 0U) { + printf("Current mirrored output buffer value: %.4f V\n\r\n\r", adc_value); + } + HAL_Delay(100); } } #endif // TEST_0 + +#ifdef TEST_1 + +constinit float adc_input_0_value = 0.0f; +constinit float adc_input_1_value = 0.0f; + +constexpr auto adc_input_0_cfg = dual_channel_input_0(); +constexpr auto adc_input_1_cfg = dual_channel_input_1(); + +constexpr auto adc_input_0 = ADCDomain::ADC( + adc_input_0_cfg.pin, + adc_input_0_value, + ADCDomain::Resolution::BITS_12, + ADCDomain::SampleTime::CYCLES_8_5 +); + +constexpr auto adc_input_1 = ADCDomain::ADC( + adc_input_1_cfg.pin, + adc_input_1_value, + ADCDomain::Resolution::BITS_12, + ADCDomain::SampleTime::CYCLES_8_5 +); + +int main(void) { + using ExampleADCBoard = ST_LIB::Board; + ExampleADCBoard::init(); + start_terminal(default_terminal_uart()); + + auto& adc_input_0_instance = ExampleADCBoard::instance_of(); + auto& adc_input_1_instance = ExampleADCBoard::instance_of(); + + print_banner( + "ADC dual-channel example", + dual_channel_wiring_hint(), + "t_ms raw_0 raw_1 v_0[V] v_1[V]" + ); + printf("Reading inputs: %s and %s\n\r\n\r", adc_input_0_cfg.label, adc_input_1_cfg.label); + + while (1) { + adc_input_0_instance.read(); + adc_input_1_instance.read(); + + const float raw_0 = adc_input_0_instance.get_raw(); + const float raw_1 = adc_input_1_instance.get_raw(); + const float voltage_0 = adc_input_0_instance.get_value(); + const float voltage_1 = adc_input_1_instance.get_value(); + + printf( + "%10lu %8.0f %8.0f %10.4f %10.4f\n\r", + HAL_GetTick(), + raw_0, + raw_1, + voltage_0, + voltage_1 + ); + + HAL_Delay(100); + } +} + +#endif // TEST_1 #endif // EXAMPLE_ADC diff --git a/deps/ST-LIB b/deps/ST-LIB index ac84fbcd..aaf0500a 160000 --- a/deps/ST-LIB +++ b/deps/ST-LIB @@ -1 +1 @@ -Subproject commit ac84fbcd6b212351e0c22bde9fd3728032a4f406 +Subproject commit aaf0500a14129934f4e1f8732b32ee788c445336 diff --git a/tools/build-example.sh b/tools/build-example.sh old mode 100755 new mode 100644 index 9f2d771d..22af2058 --- a/tools/build-example.sh +++ b/tools/build-example.sh @@ -5,25 +5,24 @@ usage() { cat <<'EOF' Usage: tools/build-example.sh --example [options] -Compiles one example by injecting EXAMPLE_* / TEST_* defines through CMake. +Build one example by enabling BUILD_EXAMPLES and injecting EXAMPLE_* / TEST_* macros. Options: -l, --list List all available EXAMPLE_* and their TEST_* macros. - --list-tests List TEST_* macros for one example (e.g. fmac, EXAMPLE_FMAC). - -e, --example Example name (e.g. fmac, adc, EXAMPLE_FMAC). + --list-tests List TEST_* macros for one example (e.g. adc, EXAMPLE_ADC). + -e, --example Example name, e.g. adc or EXAMPLE_ADC. -t, --test Test selector (default: TEST_0). Accepts 0, 1, TEST_1... --no-test Do not define any TEST_* macro. - -p, --preset CMake configure/build preset - (default: board-debug-eth-ksz8041). - -b, --board-name Optional BOARD_NAME override (e.g. TEST). - --extra-cxx-flags Extra CXX flags appended after EXAMPLE/TEST defines. + -p, --preset CMake preset. Defaults to nucleo-debug. + -b, --board-name Optional BOARD_NAME override. + --extra-cxx-flags Extra flags appended after the injected defines. -h, --help Show this help. Examples: tools/build-example.sh --list - tools/build-example.sh --list-tests fmac - tools/build-example.sh --example fmac - tools/build-example.sh --example EXAMPLE_ADC --test 0 --preset board-debug + tools/build-example.sh --list-tests adc + tools/build-example.sh --example adc --preset nucleo-debug + tools/build-example.sh --example adc --test 1 --preset nucleo-debug tools/build-example.sh --example ethernet --test TEST_0 --board-name TEST EOF } @@ -77,11 +76,11 @@ collect_tests_for_example() { } print_examples_table() { - local file local example_macro + local file + local rel_file local tests_csv local tests_raw - local rel_file while IFS='|' read -r macro tests file_path; do printf "%-40s tests: %s\n" "$macro" "$tests" @@ -105,6 +104,10 @@ print_examples_table() { ) } +sanitize_path_fragment() { + printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -c '[:alnum:]_-' '_' +} + script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" repo_root="$(CDPATH= cd -- "${script_dir}/.." && pwd)" @@ -113,7 +116,7 @@ list_tests_target="" example_name="" test_macro="TEST_0" test_explicit=0 -preset="board-debug-eth-ksz8041" +preset="nucleo-debug" board_name="" extra_cxx_flags="" @@ -181,8 +184,8 @@ if [[ -n "$list_tests_target" ]]; then echo "Unknown example macro '${example_macro}'." >&2 exit 1 fi - tests_output="$(collect_tests_for_example "$example_macro" || true)" + tests_output="$(collect_tests_for_example "$example_macro" || true)" echo "${example_macro}" if [[ -z "$tests_output" ]]; then echo " - " @@ -241,6 +244,13 @@ if [[ -n "$extra_cxx_flags" ]]; then define_flags+=" ${extra_cxx_flags}" fi +binary_dir="${repo_root}/out/build/examples/$(sanitize_path_fragment "${preset}")/$(sanitize_path_fragment "${example_macro}")" +if [[ -n "$test_macro" ]]; then + binary_dir+="/$(sanitize_path_fragment "${test_macro}")" +else + binary_dir+="/no_test" +fi + echo "[build-example] repo: ${repo_root}" echo "[build-example] preset: ${preset}" echo "[build-example] example: ${example_macro}" @@ -249,13 +259,16 @@ if [[ -n "$test_macro" ]]; then else echo "[build-example] test: " fi +echo "[build-example] binary dir: ${binary_dir}" cd "${repo_root}" configure_cmd=( cmake --preset "${preset}" + -B "${binary_dir}" -DBUILD_EXAMPLES=ON + -DCMAKE_EXPORT_COMPILE_COMMANDS=OFF "-DCMAKE_CXX_FLAGS=${define_flags}" ) @@ -264,6 +277,6 @@ if [[ -n "$board_name" ]]; then fi "${configure_cmd[@]}" -cmake --build --preset "${preset}" +cmake --build "${binary_dir}" echo "[build-example] Build completed." From 0d04942348fa0f15df6c141848c23926ea770f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20S=C3=A1ez?= Date: Sat, 14 Mar 2026 00:21:41 +0100 Subject: [PATCH 03/10] Simplified example --- Core/Src/Examples/ExampleADC.cpp | 72 +++++++------------------------- 1 file changed, 16 insertions(+), 56 deletions(-) diff --git a/Core/Src/Examples/ExampleADC.cpp b/Core/Src/Examples/ExampleADC.cpp index b507e4a1..eedf5608 100644 --- a/Core/Src/Examples/ExampleADC.cpp +++ b/Core/Src/Examples/ExampleADC.cpp @@ -15,76 +15,36 @@ struct ExampleInput { const char* label; }; -UART::Peripheral& default_terminal_uart() { -#ifdef NUCLEO - return UART::uart3; -#else - return UART::uart2; -#endif -} - -const char* terminal_hint() { -#ifdef NUCLEO - return "Terminal: ST-LINK VCP over USB (USART3, 115200 8N1)"; -#else - return "Terminal: USART2 on PD5/PD6 (115200 8N1)"; -#endif -} +constexpr ExampleInput kSingleChannelInput{ST_LIB::PA0, "PA0"}; +constexpr ExampleInput kDualChannelInput0{ST_LIB::PA0, "PA0"}; +constexpr ExampleInput kDualChannelInput1{ST_LIB::PC0, "PC0"}; -[[maybe_unused]] const char* single_channel_wiring_hint() { -#ifdef NUCLEO - return "Connect PA0 to GND, 3V3 or a potentiometer. On NUCLEO-H723ZG this is CN10 pin 29 / " - "D32, not Arduino A0."; -#else - return "Connect PA0 to GND, 3V3 or a potentiometer and watch the terminal output."; -#endif -} +constexpr const char* kTerminalHint = "Terminal: ST-LINK VCP over USB (USART3, 115200 8N1)"; +constexpr const char* kSingleChannelWiringHint = "Connect PA0 to GND, 3V3 or a potentiometer."; +constexpr const char* kDualChannelWiringHint = "Connect PA0 and PC0 to two analog sources."; -[[maybe_unused]] const char* dual_channel_wiring_hint() { -#ifdef NUCLEO - return "Connect PA0 and PC0 to two analog sources. PA1 is tied to the Ethernet PHY on " - "NUCLEO-H723ZG via SB57 and is not a safe ADC test pin."; -#else - return "Connect PA0 and PA1 to two analog sources to validate multichannel DMA."; -#endif -} - -void start_terminal(UART::Peripheral& uart) { +void start_terminal() { #ifdef HAL_UART_MODULE_ENABLED - if (!UART::set_up_printf(uart)) { + if (!UART::set_up_printf(UART::uart3)) { ErrorHandler("Unable to set up UART printf for ADC example"); } UART::start(); -#else - (void)uart; #endif } void print_banner(const char* title, const char* wiring_hint, const char* columns_hint) { printf("\n\r=== %s ===\n\r", title); - printf("%s\n\r", terminal_hint()); + printf("%s\n\r", kTerminalHint); printf("%s\n\r", wiring_hint); printf("Columns: %s\n\r\n\r", columns_hint); } -consteval ExampleInput single_channel_input() { return {ST_LIB::PA0, "PA0"}; } - -consteval ExampleInput dual_channel_input_0() { return {ST_LIB::PA0, "PA0"}; } - -consteval ExampleInput dual_channel_input_1() { -#ifdef NUCLEO - return {ST_LIB::PC0, "PC0"}; -#else - return {ST_LIB::PA1, "PA1"}; -#endif -} - } // namespace #ifdef TEST_0 constinit float adc_value = 0.0f; -constexpr auto adc_input = single_channel_input(); +constexpr auto adc_input = kSingleChannelInput; constexpr auto adc = ADCDomain::ADC( adc_input.pin, adc_value, @@ -95,11 +55,11 @@ constexpr auto adc = ADCDomain::ADC( int main(void) { using ExampleADCBoard = ST_LIB::Board; ExampleADCBoard::init(); - start_terminal(default_terminal_uart()); + start_terminal(); auto& adc_instance = ExampleADCBoard::instance_of(); - print_banner("ADC single-channel example", single_channel_wiring_hint(), "t_ms raw voltage[V]"); + print_banner("ADC single-channel example", kSingleChannelWiringHint, "t_ms raw voltage[V]"); printf("Reading input: %s\n\r\n\r", adc_input.label); uint32_t sample_index = 0; @@ -126,8 +86,8 @@ int main(void) { constinit float adc_input_0_value = 0.0f; constinit float adc_input_1_value = 0.0f; -constexpr auto adc_input_0_cfg = dual_channel_input_0(); -constexpr auto adc_input_1_cfg = dual_channel_input_1(); +constexpr auto adc_input_0_cfg = kDualChannelInput0; +constexpr auto adc_input_1_cfg = kDualChannelInput1; constexpr auto adc_input_0 = ADCDomain::ADC( adc_input_0_cfg.pin, @@ -146,14 +106,14 @@ constexpr auto adc_input_1 = ADCDomain::ADC( int main(void) { using ExampleADCBoard = ST_LIB::Board; ExampleADCBoard::init(); - start_terminal(default_terminal_uart()); + start_terminal(); auto& adc_input_0_instance = ExampleADCBoard::instance_of(); auto& adc_input_1_instance = ExampleADCBoard::instance_of(); print_banner( "ADC dual-channel example", - dual_channel_wiring_hint(), + kDualChannelWiringHint, "t_ms raw_0 raw_1 v_0[V] v_1[V]" ); printf("Reading inputs: %s and %s\n\r\n\r", adc_input_0_cfg.label, adc_input_1_cfg.label); From 248d3ff3e14d5089671a41d34f0f5a50f0ef8582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20S=C3=A1ez?= Date: Mon, 16 Mar 2026 22:46:46 +0100 Subject: [PATCH 04/10] New hyper CLI --- Core/Src/Runes/generated_metadata.cpp | 6 +- README.md | 33 + deps/ST-LIB | 2 +- docs/examples/README.md | 6 + docs/template-project/setup.md | 1 + hyper | 1185 +++++++++++++++++++++++++ toolchains/stm32.cmake | 119 ++- tools/build-example.sh | 0 tools/init.sh | 23 +- 9 files changed, 1348 insertions(+), 27 deletions(-) create mode 100755 hyper mode change 100644 => 100755 tools/build-example.sh diff --git a/Core/Src/Runes/generated_metadata.cpp b/Core/Src/Runes/generated_metadata.cpp index ca034a4a..1133921e 100644 --- a/Core/Src/Runes/generated_metadata.cpp +++ b/Core/Src/Runes/generated_metadata.cpp @@ -5,11 +5,11 @@ extern "C" { const char DESCRIPTION[255] __attribute__((section(".metadata_pool"))) = "****************" // placeholder for beggining - "20260218T203535" // DateTime using ISO-8601 format + "20260316T224044" // DateTime using ISO-8601 format " " // alignment - "019403b6" // STLIB commit + "aaecc655" // STLIB commit "--------" // ADJ commit - "9c87b508" // Board commit + "9d2ba8c9" // Board commit // the '=' is used for unparsing ; } diff --git a/README.md b/README.md index 0ab821c0..d21a46ff 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,39 @@ cmake --build --preset simulator ctest --preset simulator-all ``` +## Hyper CLI + +The repo now includes a local helper CLI at `./hyper` for the common hardware flow: + +```sh +./hyper examples list +./hyper build adc --test 1 +./hyper run adc --test 1 --uart +./hyper uart +./hyper doctor +``` + +For UART, `hyper` now prefers `tio` and falls back to `cu` if `tio` is not installed. + +It wraps the existing repo scripts instead of replacing them, and also exposes a small ST-LIB namespace: + +```sh +./hyper stlib build --preset simulator --run-tests +./hyper stlib sim-tests +``` + +Useful defaults can be pinned with environment variables: + +- `HYPER_DEFAULT_PRESET` +- `HYPER_FLASH_METHOD` +- `HYPER_UART_PORT` +- `HYPER_UART_BAUD` +- `HYPER_UART_TOOL` + +Recommended UART setup: + +Install `tio` with your package manager, then run `./hyper uart`. + ## Documentation - Template setup: [`docs/template-project/setup.md`](docs/template-project/setup.md) diff --git a/deps/ST-LIB b/deps/ST-LIB index aaf0500a..ee54dc7a 160000 --- a/deps/ST-LIB +++ b/deps/ST-LIB @@ -1 +1 @@ -Subproject commit aaf0500a14129934f4e1f8732b32ee788c445336 +Subproject commit ee54dc7af7a6043db75aca9ffdcea8a38889a5a5 diff --git a/docs/examples/README.md b/docs/examples/README.md index bc83488f..c9bfc1ca 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -16,12 +16,14 @@ List available examples: ```sh ./tools/build-example.sh --list +./hyper examples list ``` Build one example on a Nucleo: ```sh ./tools/build-example.sh --example adc --preset nucleo-debug --test 0 +./hyper build adc --preset nucleo-debug --test 0 ``` Build one Ethernet example on a Nucleo: @@ -36,8 +38,12 @@ Flash the latest build: ```sh STM32_Programmer_CLI -c port=SWD mode=UR -w out/build/latest.elf -v -rst +./hyper flash +./hyper run adc --test 0 --uart ``` +`./hyper uart` prefers `tio` and falls back to `cu`. + ## Documents - [ExampleADC](./example-adc.md) diff --git a/docs/template-project/setup.md b/docs/template-project/setup.md index 530d2933..6a8f139d 100644 --- a/docs/template-project/setup.md +++ b/docs/template-project/setup.md @@ -11,6 +11,7 @@ For MCU build/flash/debug: - `STM32CubeCLT` - `openocd` or `ST-LINK_gdbserver` - `arm-none-eabi-gdb` +- `tio` (recommended for `./hyper uart`; `cu` remains the fallback when available) ## 2. Quick Initialization diff --git a/hyper b/hyper new file mode 100755 index 00000000..f327c0a3 --- /dev/null +++ b/hyper @@ -0,0 +1,1185 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import glob +import os +import platform +import re +import shlex +import shutil +import subprocess +import sys +import tarfile +import tempfile +import zipfile +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parent +TOOLS_DIR = REPO_ROOT / "tools" +STLIB_ROOT = REPO_ROOT / "deps" / "ST-LIB" +BUILD_EXAMPLE_SCRIPT = TOOLS_DIR / "build-example.sh" +PREFLASH_CHECK_SCRIPT = TOOLS_DIR / "preflash_check.py" +INIT_SCRIPT = TOOLS_DIR / "init.sh" +TCPIP_NUCLEO_SCRIPT = TOOLS_DIR / "run_example_tcpip_nucleo.sh" +TCPIP_STRESS_SCRIPT = TOOLS_DIR / "run_example_tcpip_stress.sh" +STLIB_BUILD_SCRIPT = STLIB_ROOT / "tools" / "build.py" +STLIB_SIM_TESTS_SCRIPT = STLIB_ROOT / "tools" / "run_sim_tests.sh" +LATEST_ELF = REPO_ROOT / "out" / "build" / "latest.elf" + +DEFAULT_PRESET = os.environ.get("HYPER_DEFAULT_PRESET", "nucleo-debug") +DEFAULT_FLASH_METHOD = os.environ.get("HYPER_FLASH_METHOD", "auto") +DEFAULT_UART_TOOL = os.environ.get("HYPER_UART_TOOL", "auto") +DEFAULT_UART_PORT = os.environ.get("HYPER_UART_PORT") +DEFAULT_UART_BAUD = int(os.environ.get("HYPER_UART_BAUD", "115200")) +DEFAULT_REQUIRED_CLT_VERSION = os.environ.get("HYPER_REQUIRED_STM32_CLT_VERSION", "1.21.0") +DEFAULT_CLT_ROOT = os.environ.get("HYPER_STM32CLT_ROOT") or os.environ.get("STM32_CLT_ROOT") +DEFAULT_CLT_INSTALLER = os.environ.get("HYPER_STM32CLT_INSTALLER") +DEFAULT_CLT_DOWNLOAD_URL = os.environ.get("HYPER_STM32CLT_DOWNLOAD_URL") +DEFAULT_UV_VERSION = os.environ.get("HYPER_UV_VERSION") +COLOR_ENABLED = sys.stdout.isatty() and os.environ.get("NO_COLOR") is None and os.environ.get("TERM") != "dumb" +CLT_PRODUCT_PAGE = "https://www.st.com/en/development-tools/stm32cubeclt.html" +CLT_RELEASE_NOTE = "https://www.st.com/resource/en/release_note/rn0132-stm32cube-commandline-toolset-release-v1210-stmicroelectronics.pdf" +UV_INSTALL_PAGE = "https://docs.astral.sh/uv/getting-started/installation/" +HELP_BANNER_ROWS = [ + "", + "██╗ ██╗██╗ ██╗██████╗ ███████╗██████╗ ██╗ ██████╗ ██████╗ ██████╗ ", + "██║ ██║╚██╗ ██╔╝██╔══██╗██╔════╝██╔══██╗██║ ██╔═══██╗██╔═══██╗██╔══██╗", + "███████║ ╚████╔╝ ██████╔╝█████╗ ██████╔╝██║ ██║ ██║██║ ██║██████╔╝", + "██╔══██║ ╚██╔╝ ██╔═══╝ ██╔══╝ ██╔══██╗██║ ██║ ██║██║ ██║██╔═══╝ ", + "██║ ██║ ██║ ██║ ███████╗██║ ██║███████╗╚██████╔╝╚██████╔╝██║ ", + "╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ", + "", + "██╗ ██╗██████╗ ██╗ ██╗", + "██║ ██║██╔══██╗██║ ██║", + "██║ ██║██████╔╝██║ ██║", + "██║ ██║██╔═══╝ ╚██╗ ██╔╝", + "╚██████╔╝██║ ╚████╔╝ ", + " ╚═════╝ ╚═╝ ╚═══╝ ", + "", + "HyperloopUPV", + "", + "Build • Flash • UART • Diagnostics • ST-LIB", + "", +] +HELP_BANNER_WIDTH = 82 +HELP_BANNER = "\n".join( + ["╔" + "═" * HELP_BANNER_WIDTH + "╗"] + + [f"│{row.center(HELP_BANNER_WIDTH)}│" for row in HELP_BANNER_ROWS] + + ["╚" + "═" * HELP_BANNER_WIDTH + "╝"] +) +HELP_EXAMPLES = """Examples: + hyper doctor + hyper build adc --preset nucleo-debug --test 1 + hyper run adc --preset nucleo-debug --test 1 --uart + hyper uart --list-ports +""" +SERIAL_PATTERNS = ( + "/dev/serial/by-id/*", + "/dev/cu.usbmodem*", + "/dev/cu.usbserial*", + "/dev/cu.wchusbserial*", + "/dev/ttyACM*", + "/dev/ttyUSB*", + "/dev/tty.usbmodem*", + "/dev/tty.usbserial*", + "/dev/tty.wchusbserial*", +) + + +class HyperError(RuntimeError): + pass + + +class ToolStatus: + def __init__(self, *, path: str | None, version_line: str | None = None, clt_version: str | None = None) -> None: + self.path = path + self.version_line = version_line + self.clt_version = clt_version + + +class HyperArgumentParser(argparse.ArgumentParser): + def format_help(self) -> str: + return f"{HELP_BANNER}\n{super().format_help()}" + + +def style(text: str, *codes: str) -> str: + if not COLOR_ENABLED or not codes: + return text + return f"\033[{';'.join(codes)}m{text}\033[0m" + + +def status_tag(status: str) -> str: + tags = { + "ok": ("[ok]", ("32", "1")), + "wrong": ("[wrong]", ("31", "1")), + "warn": ("[warn]", ("33", "1")), + "missing": ("[missing]", ("33", "1")), + "info": ("[info]", ("36", "1")), + } + text, codes = tags.get(status, (f"[{status}]", ("0",))) + return style(text, *codes) + + +def section_title(title: str) -> None: + print() + print(style(f"== {title} ==", "36", "1")) + + +def print_detail(label: str, value: str) -> None: + print(f" {style(f'{label}:', '2')} {value}") + + +def print_status_item(label: str, status: str, value: str, *, detail: str | None = None) -> None: + print(f" {status_tag(status):<12} {style(label, '1'):<20} {value}") + if detail: + print_detail("detail", detail) + + +def print_note(message: str, *, status: str = "info") -> None: + print(f" {status_tag(status)} {message}") + + +def print_action(title: str, **details: str) -> None: + print() + print(style(title, "35", "1")) + print(style("-" * len(title), "2")) + for label, value in details.items(): + print_detail(label.replace("_", " "), value) + + +def print_command_context(cmd: list[str], cwd: Path) -> None: + print(style("== Running ==", "34", "1")) + print(f" {style('$', '34', '1')} {shell_join(cmd)}") + if cwd != REPO_ROOT: + print_detail("cwd", str(cwd)) + + +def shell_join(cmd: list[object]) -> str: + return " ".join(shlex.quote(str(part)) for part in cmd) + + +def run_command( + cmd: list[object], + *, + cwd: Path = REPO_ROOT, + env: dict[str, str] | None = None, + check: bool = True, +) -> subprocess.CompletedProcess[str]: + cmd_as_text = [str(part) for part in cmd] + print_command_context(cmd_as_text, cwd) + return subprocess.run( + cmd_as_text, + cwd=cwd, + env=env, + check=check, + text=True, + ) + + +def exec_command(cmd: list[object]) -> int: + cmd_as_text = [str(part) for part in cmd] + print_command_context(cmd_as_text, REPO_ROOT) + os.execvpe(cmd_as_text[0], cmd_as_text, os.environ.copy()) + return 1 + + +def command_path(name: str) -> str | None: + return shutil.which(name) + + +def known_uv_paths() -> list[Path]: + candidates = [ + Path.home() / ".local" / "bin" / ("uv.exe" if detect_host_os() == "windows" else "uv"), + Path.home() / ".cargo" / "bin" / ("uv.exe" if detect_host_os() == "windows" else "uv"), + ] + unique: list[Path] = [] + seen: set[Path] = set() + for candidate in candidates: + resolved = candidate.expanduser().resolve() + if resolved in seen or not resolved.is_file(): + continue + seen.add(resolved) + unique.append(resolved) + return unique + + +def uv_executable() -> str | None: + path = command_path("uv") + if path: + return path + candidates = known_uv_paths() + if candidates: + return str(candidates[0]) + return None + + +def download_file(url: str, destination: Path) -> None: + if detect_host_os() == "windows": + run_command( + [ + "powershell", + "-NoProfile", + "-Command", + f"Invoke-WebRequest -Uri '{url}' -OutFile '{destination}'", + ] + ) + return + + curl = command_path("curl") + if curl: + run_command([curl, "-LsSf", url, "-o", destination]) + return + + wget = command_path("wget") + if wget: + run_command([wget, "-q", "-O", destination, url]) + return + + raise HyperError("Neither curl nor wget is available for downloading files.") + + +def strip_ansi(text: str) -> str: + return re.sub(r"\x1b\[[0-9;]*[A-Za-z]", "", text) + + +def read_command_first_line(cmd: list[str], *, preferred_pattern: str | None = None) -> str | None: + try: + result = subprocess.run( + cmd, + check=True, + text=True, + capture_output=True, + ) + except (OSError, subprocess.CalledProcessError): + return None + + regex = re.compile(preferred_pattern, re.IGNORECASE) if preferred_pattern else None + cleaned_lines: list[str] = [] + for stream in (result.stdout, result.stderr): + if not stream: + continue + for line in stream.splitlines(): + stripped = strip_ansi(line).strip() + if stripped: + cleaned_lines.append(stripped) + + if regex: + for line in cleaned_lines: + if regex.search(line): + return line + + for line in cleaned_lines: + return line + return None + + +def parse_clt_version_from_path(path: str | None) -> str | None: + if not path: + return None + match = re.search(r"STM32CubeCLT[_-](\d+\.\d+\.\d+)", path, re.IGNORECASE) + if match: + return match.group(1) + return None + + +def inspect_tool(name: str, *, version_args: list[str] | None = None, version_pattern: str | None = None) -> ToolStatus: + path = command_path(name) + version_line = None + if path and version_args: + version_line = read_command_first_line([path, *version_args], preferred_pattern=version_pattern) + return ToolStatus(path=path, version_line=version_line, clt_version=parse_clt_version_from_path(path)) + + +def detect_host_os() -> str: + system = platform.system().lower() + if system == "darwin": + return "macos" + if system == "windows": + return "windows" + return "linux" + + +def clt_root_candidates(version: str | None = None) -> list[Path]: + candidates: list[Path] = [] + if DEFAULT_CLT_ROOT: + candidates.append(Path(DEFAULT_CLT_ROOT).expanduser()) + + host_os = detect_host_os() + version_patterns = [version] if version else [None] + for ver in version_patterns: + suffixes = [f"STM32CubeCLT_{ver}", f"STM32CubeCLT-{ver}"] if ver else [] + if host_os in ("macos", "linux"): + if ver: + suffixes.extend([f"STM32CubeCLT_{ver}*", f"STM32CubeCLT-{ver}*"]) + base_dirs = [Path("/opt/ST"), Path.home() / "ST"] + for base_dir in base_dirs: + if not base_dir.is_dir(): + continue + if ver: + for suffix in suffixes: + candidates.extend(base_dir.glob(suffix)) + else: + candidates.extend(base_dir.glob("STM32CubeCLT_*")) + candidates.extend(base_dir.glob("STM32CubeCLT-*")) + else: + program_files = os.environ.get("ProgramFiles", r"C:\Program Files") + base_dirs = [ + Path(program_files) / "STMicroelectronics" / "STM32Cube", + Path(program_files) / "STMicroelectronics", + Path(r"C:\ST"), + ] + for base_dir in base_dirs: + if not base_dir.is_dir(): + continue + if ver: + candidates.extend(base_dir.glob(f"STM32CubeCLT_{ver}*")) + candidates.extend(base_dir.glob(f"STM32CubeCLT-{ver}*")) + else: + candidates.extend(base_dir.glob("STM32CubeCLT_*")) + candidates.extend(base_dir.glob("STM32CubeCLT-*")) + + unique: list[Path] = [] + seen: set[Path] = set() + for candidate in candidates: + resolved = candidate.expanduser().resolve() + if resolved in seen or not resolved.exists(): + continue + seen.add(resolved) + unique.append(resolved) + return unique + + +def infer_clt_root_from_tool(path: str | None) -> Path | None: + if not path: + return None + resolved = Path(path).expanduser().resolve() + for parent in (resolved, *resolved.parents): + if re.search(r"STM32CubeCLT[_-]\d+\.\d+\.\d+", parent.name, re.IGNORECASE): + return parent + return None + + +def clt_tool_status(tool_relative_path: str, *, version_args: list[str], version_pattern: str) -> ToolStatus: + for root in clt_root_candidates(DEFAULT_REQUIRED_CLT_VERSION): + tool_path = root / tool_relative_path + if tool_path.is_file(): + version_line = read_command_first_line([str(tool_path), *version_args], preferred_pattern=version_pattern) + return ToolStatus( + path=str(tool_path), + version_line=version_line, + clt_version=parse_clt_version_from_path(str(root)), + ) + + path_status = inspect_tool(Path(tool_relative_path).name, version_args=version_args, version_pattern=version_pattern) + inferred_root = infer_clt_root_from_tool(path_status.path) + if inferred_root is not None: + path_status.clt_version = parse_clt_version_from_path(str(inferred_root)) + return path_status + + +def inspect_clt() -> tuple[ToolStatus, ToolStatus, str | None]: + arm_gcc = clt_tool_status( + "GNU-tools-for-STM32/bin/arm-none-eabi-gcc", + version_args=["--version"], + version_pattern=r"arm-none-eabi-gcc", + ) + programmer = clt_tool_status( + "STM32CubeProgrammer/bin/STM32_Programmer_CLI", + version_args=["--version"], + version_pattern=r"version", + ) + clt_version = arm_gcc.clt_version or programmer.clt_version + if clt_version is None: + for root in clt_root_candidates(): + clt_version = parse_clt_version_from_path(str(root)) + if clt_version: + break + return arm_gcc, programmer, clt_version + + +def installer_version_tokens(version: str) -> tuple[str, str, str]: + return version, version.replace(".", "-"), version.replace(".", "_") + + +def installer_matches_version(path: Path, version: str) -> bool: + lower_name = path.name.lower() + if "stm32cubeclt" not in lower_name: + return False + return any(token in lower_name for token in installer_version_tokens(version)) + + +def installer_candidates(version: str) -> list[Path]: + dirs = [REPO_ROOT, Path.cwd(), Path.home() / "Downloads", Path.home() / "Desktop"] + candidates: list[Path] = [] + seen: set[Path] = set() + for directory in dirs: + resolved_dir = directory.expanduser().resolve() + if resolved_dir in seen or not resolved_dir.is_dir(): + continue + seen.add(resolved_dir) + try: + for entry in resolved_dir.iterdir(): + if not entry.is_file(): + continue + if installer_matches_version(entry, version): + candidates.append(entry.resolve()) + except PermissionError: + continue + candidates.sort(key=lambda path: path.stat().st_mtime, reverse=True) + return candidates + + +def maybe_download_clt_installer(version: str) -> Path | None: + if not DEFAULT_CLT_DOWNLOAD_URL: + return None + + suffix = Path(DEFAULT_CLT_DOWNLOAD_URL).suffix or ".bin" + download_dir = REPO_ROOT / "out" / "downloads" + download_dir.mkdir(parents=True, exist_ok=True) + destination = download_dir / f"stm32cubeclt-{version}{suffix}" + print_detail("download url", DEFAULT_CLT_DOWNLOAD_URL) + print_detail("download dest", str(destination)) + download_file(DEFAULT_CLT_DOWNLOAD_URL, destination) + return destination + + +def prepare_clt_installers(installer: Path) -> tuple[list[Path], Path | None]: + host_os = detect_host_os() + if host_os == "macos": + if installer.suffix.lower() == ".pkg": + return [installer], None + if not installer.name.endswith((".tar.gz", ".tgz")) and installer.suffix.lower() != ".zip": + raise HyperError(f"Unsupported macOS STM32CubeCLT installer: {installer.name}") + tmpdir = Path(tempfile.mkdtemp(prefix="hyper-clt-macos-")) + if installer.suffix.lower() == ".zip": + with zipfile.ZipFile(installer) as archive: + archive.extractall(tmpdir) + pkgs = [path for path in tmpdir.rglob("*.pkg") if path.is_file()] + if not pkgs: + nested_archives = [ + path + for path in tmpdir.rglob("*") + if path.is_file() and path.name.endswith((".tar.gz", ".tgz")) + ] + if not nested_archives: + raise HyperError(f"No .pkg or .tar.gz files found in {installer.name}") + nested_archives.sort( + key=lambda path: ("stm32cubeclt" not in path.name.lower(), path.name.lower()) + ) + for nested_archive in nested_archives: + with tarfile.open(nested_archive, "r:*") as archive: + archive.extractall(tmpdir) + pkgs = [path for path in tmpdir.rglob("*.pkg") if path.is_file()] + if not pkgs: + raise HyperError(f"No .pkg files found in {installer.name}") + pkgs.sort(key=lambda path: ("st-link" in path.name.lower(), path.name.lower())) + return pkgs, tmpdir + + with tarfile.open(installer, "r:*") as archive: + archive.extractall(tmpdir) + pkgs = [path for path in tmpdir.rglob("*.pkg") if path.is_file()] + if not pkgs: + raise HyperError(f"No .pkg files found in {installer.name}") + pkgs.sort(key=lambda path: ("st-link" in path.name.lower(), path.name.lower())) + return pkgs, tmpdir + + if host_os == "windows": + if installer.suffix.lower() == ".exe": + return [installer], None + if installer.suffix.lower() != ".zip": + raise HyperError(f"Unsupported Windows STM32CubeCLT installer: {installer.name}") + tmpdir = Path(tempfile.mkdtemp(prefix="hyper-clt-windows-")) + with zipfile.ZipFile(installer) as archive: + archive.extractall(tmpdir) + exes = [path for path in tmpdir.rglob("*.exe") if path.is_file()] + if not exes: + raise HyperError(f"No .exe files found in {installer.name}") + exes.sort(key=lambda path: ("stm32cubeclt" not in path.name.lower(), path.name.lower())) + return exes, tmpdir + + if installer.suffix != ".sh": + raise HyperError(f"Unsupported Linux STM32CubeCLT installer: {installer.name}") + return [installer], None + + +def run_clt_installer(installer: Path) -> None: + host_os = detect_host_os() + if host_os == "macos": + run_command(["sudo", "installer", "-pkg", installer, "-target", "/"]) + return + if host_os == "windows": + run_command([installer]) + return + run_command(["sudo", "sh", installer]) + + +def ensure_required_clt() -> None: + arm_gcc, programmer, clt_version = inspect_clt() + if clt_version == DEFAULT_REQUIRED_CLT_VERSION: + print_action("STM32CubeCLT", version=DEFAULT_REQUIRED_CLT_VERSION, status="ready") + if arm_gcc.path: + print_detail("arm gcc", arm_gcc.path) + if programmer.path: + print_detail("programmer", programmer.path) + print_note("required STM32CubeCLT is already installed", status="ok") + return + + print_action( + "STM32CubeCLT", + expected=DEFAULT_REQUIRED_CLT_VERSION, + detected=clt_version or "missing", + host=detect_host_os(), + ) + installer = Path(DEFAULT_CLT_INSTALLER).expanduser().resolve() if DEFAULT_CLT_INSTALLER else None + if installer is None: + candidates = installer_candidates(DEFAULT_REQUIRED_CLT_VERSION) + installer = candidates[0] if candidates else None + if installer is None: + installer = maybe_download_clt_installer(DEFAULT_REQUIRED_CLT_VERSION) + + if installer is None: + raise HyperError( + "STM32CubeCLT installer not found. Download the official installer for " + f"{DEFAULT_REQUIRED_CLT_VERSION} from {CLT_PRODUCT_PAGE} " + f"(release note: {CLT_RELEASE_NOTE}) and place it in ~/Downloads, " + "or set HYPER_STM32CLT_INSTALLER to the installer path, " + "or set HYPER_STM32CLT_DOWNLOAD_URL to a direct installer URL." + ) + + print_detail("installer", str(installer)) + installers, cleanup_dir = prepare_clt_installers(installer) + try: + for prepared_installer in installers: + print_detail("install step", prepared_installer.name) + run_clt_installer(prepared_installer) + finally: + if cleanup_dir is not None: + shutil.rmtree(cleanup_dir, ignore_errors=True) + + _, _, installed_version = inspect_clt() + if installed_version != DEFAULT_REQUIRED_CLT_VERSION: + raise HyperError( + f"STM32CubeCLT installation completed but detected version is {installed_version or 'missing'}." + ) + print_note(f"STM32CubeCLT {DEFAULT_REQUIRED_CLT_VERSION} installed", status="ok") + + +def uv_install_url() -> str: + if DEFAULT_UV_VERSION: + return f"https://astral.sh/uv/{DEFAULT_UV_VERSION}/install.sh" + return "https://astral.sh/uv/install.sh" + + +def ensure_uv() -> str: + uv_path = uv_executable() + if uv_path: + version_line = read_command_first_line([uv_path, "--version"]) + print_action("uv", status="ready") + print_detail("binary", uv_path) + if version_line: + print_detail("version", version_line) + print_note("uv is available", status="ok") + return uv_path + + print_action("uv", status="installing", host=detect_host_os()) + install_url = uv_install_url() + print_detail("source", install_url) + + tmpdir = Path(tempfile.mkdtemp(prefix="hyper-uv-install-")) + script_name = "install.ps1" if detect_host_os() == "windows" else "install.sh" + installer = tmpdir / script_name + try: + download_file(install_url, installer) + if detect_host_os() == "windows": + run_command( + [ + "powershell", + "-ExecutionPolicy", + "Bypass", + "-File", + installer, + ] + ) + else: + env = os.environ.copy() + env.setdefault("UV_NO_MODIFY_PATH", "1") + run_command(["sh", installer], env=env) + finally: + shutil.rmtree(tmpdir, ignore_errors=True) + + uv_path = uv_executable() + if not uv_path: + raise HyperError(f"uv installation completed but the binary is still not visible. See {UV_INSTALL_PAGE}") + + version_line = read_command_first_line([uv_path, "--version"]) + print_detail("binary", uv_path) + if version_line: + print_detail("version", version_line) + print_note("uv installed", status="ok") + return uv_path + + +def virtual_python_path() -> Path: + if detect_host_os() == "windows": + return REPO_ROOT / "virtual" / "Scripts" / "python.exe" + return REPO_ROOT / "virtual" / "bin" / "python" + + +def setup_python_env_with_uv(uv_path: str) -> None: + print_action("Python Env", tool="uv", venv=str(REPO_ROOT / "virtual")) + run_command([uv_path, "venv", REPO_ROOT / "virtual"]) + run_command([uv_path, "pip", "install", "--python", virtual_python_path(), "-r", REPO_ROOT / "requirements.txt"]) + print_note("python environment ready", status="ok") + + +def ensure_file(path: Path, label: str) -> None: + if not path.is_file(): + raise HyperError(f"{label} not found: {path}") + + +def resolve_flash_method(requested: str) -> str: + if requested != "auto": + if requested == "stm32prog" and not command_path("STM32_Programmer_CLI"): + raise HyperError("STM32_Programmer_CLI is not available in PATH.") + if requested == "openocd" and not command_path("openocd"): + raise HyperError("openocd is not available in PATH.") + return requested + + if command_path("STM32_Programmer_CLI"): + return "stm32prog" + if command_path("openocd"): + return "openocd" + raise HyperError("No supported flash tool found. Install STM32_Programmer_CLI or openocd.") + + +def serial_port_rank(port: str) -> tuple[int, str]: + if port.startswith("/dev/serial/by-id/"): + return (0, port) + if port.startswith("/dev/cu.usbmodem"): + return (1, port) + if port.startswith("/dev/ttyACM"): + return (2, port) + if port.startswith("/dev/cu.usbserial"): + return (3, port) + if port.startswith("/dev/ttyUSB"): + return (4, port) + if port.startswith("/dev/cu.wchusbserial"): + return (5, port) + if port.startswith("/dev/tty.usbmodem"): + return (6, port) + if port.startswith("/dev/tty.usbserial"): + return (7, port) + if port.startswith("/dev/tty.wchusbserial"): + return (8, port) + return (99, port) + + +def find_serial_ports() -> list[str]: + ports: set[str] = set() + for pattern in SERIAL_PATTERNS: + ports.update(glob.glob(pattern)) + return sorted(ports, key=serial_port_rank) + + +def choose_serial_port(requested: str | None) -> str: + explicit = requested or DEFAULT_UART_PORT + if explicit and explicit != "auto": + return explicit + + ports = find_serial_ports() + if not ports: + raise HyperError("No USB serial port detected. Connect the board or pass --port explicitly.") + if len(ports) == 1: + return ports[0] + + preferred_prefixes = ( + "/dev/serial/by-id/", + "/dev/cu.usbmodem", + "/dev/ttyACM", + "/dev/cu.usbserial", + "/dev/ttyUSB", + ) + for prefix in preferred_prefixes: + matches = [port for port in ports if port.startswith(prefix)] + if len(matches) == 1: + return matches[0] + + lines = ["Multiple UART ports detected. Pass --port explicitly:"] + lines.extend(f" - {port}" for port in ports) + raise HyperError("\n".join(lines)) + + +def resolve_uart_tool(requested: str) -> str: + if requested != "auto": + if not command_path(requested): + raise HyperError(f"UART tool '{requested}' is not available in PATH.") + return requested + + if command_path("tio"): + return "tio" + if command_path("cu"): + return "cu" + raise HyperError("No supported UART tool found. Install 'tio' or ensure 'cu' is available.") + + +def run_build_example( + *, + example: str, + test: str | None, + no_test: bool, + preset: str, + board_name: str | None, + extra_cxx_flags: str | None, + jobs: int | None, +) -> None: + ensure_file(BUILD_EXAMPLE_SCRIPT, "build-example helper") + selected_test = "none" if no_test else (test or "default") + print_action( + "Build", + example=example, + preset=preset, + test=selected_test, + board_name=board_name or "default", + ) + + cmd: list[object] = [BUILD_EXAMPLE_SCRIPT, "--example", example, "--preset", preset] + if no_test: + cmd.append("--no-test") + elif test is not None: + cmd.extend(["--test", test]) + if board_name: + cmd.extend(["--board-name", board_name]) + if extra_cxx_flags: + cmd.extend(["--extra-cxx-flags", extra_cxx_flags]) + + env = os.environ.copy() + if jobs: + env["CMAKE_BUILD_PARALLEL_LEVEL"] = str(jobs) + print_detail("jobs", str(jobs)) + if extra_cxx_flags: + print_detail("extra cxx flags", extra_cxx_flags) + + run_command(cmd, env=env) + print_note("build completed", status="ok") + + +def run_preflash_check(skip_preflight: bool) -> None: + if skip_preflight: + print_note("preflight skipped", status="warn") + return + ensure_file(PREFLASH_CHECK_SCRIPT, "preflash helper") + print_action("Preflight", target=str(REPO_ROOT / "out" / "build")) + run_command([sys.executable, PREFLASH_CHECK_SCRIPT, REPO_ROOT / "out" / "build"]) + print_note("preflight passed", status="ok") + + +def flash_elf(elf: Path, method: str, verify: bool, skip_preflight: bool) -> None: + ensure_file(elf, "ELF image") + resolved_method = resolve_flash_method(method) + print_action( + "Flash", + elf=str(elf), + method=resolved_method, + verify="yes" if verify else "no", + ) + run_preflash_check(skip_preflight) + if resolved_method == "stm32prog": + cmd = [ + "STM32_Programmer_CLI", + "-c", + "port=SWD", + "mode=UR", + "-w", + elf, + ] + if verify: + cmd.append("-v") + cmd.append("-rst") + run_command(cmd) + print_note("flash completed", status="ok") + return + + base_cmd: list[object] = [ + "openocd", + "-f", + REPO_ROOT / ".vscode" / "stlink.cfg", + "-f", + REPO_ROOT / ".vscode" / "stm32h7x.cfg", + ] + if verify: + verify_cmd = base_cmd + ["-c", f"program {elf} verify reset exit"] + result = run_command(verify_cmd, check=False) + if result.returncode == 0: + print_note("flash completed", status="ok") + return + print_note("OpenOCD verify failed, retrying without verify", status="warn") + + run_command(base_cmd + ["-c", f"program {elf} reset exit"]) + print_note("flash completed", status="ok") + + +def open_uart(port: str | None, baud: int, tool: str) -> int: + resolved_port = choose_serial_port(port) + resolved_tool = resolve_uart_tool(tool) + print_action( + "UART", + tool=resolved_tool, + port=resolved_port, + baud=str(baud), + ) + print_note("opening interactive UART session", status="info") + + if resolved_tool == "tio": + return exec_command(["tio", "--baudrate", str(baud), resolved_port]) + if resolved_tool == "cu": + return exec_command(["cu", "-l", resolved_port, "-s", str(baud)]) + raise HyperError(f"Unsupported UART tool '{resolved_tool}'.") + + +def list_serial_ports() -> int: + ports = find_serial_ports() + print_action("UART Ports") + if not ports: + print_note("no candidate serial ports found", status="warn") + return 1 + for port in ports: + print_detail("port", port) + print_note(f"{len(ports)} port(s) found", status="ok") + return 0 + + +def handle_init(_: argparse.Namespace) -> int: + ensure_required_clt() + uv_path = ensure_uv() + setup_python_env_with_uv(uv_path) + ensure_file(INIT_SCRIPT, "init helper") + print_action("Init", script=str(INIT_SCRIPT)) + env = os.environ.copy() + env["HYPER_SKIP_PYTHON_INIT"] = "1" + run_command([INIT_SCRIPT], env=env) + print_note("init completed", status="ok") + return 0 + + +def handle_examples_list(_: argparse.Namespace) -> int: + ensure_file(BUILD_EXAMPLE_SCRIPT, "build-example helper") + print_action("Examples", mode="list") + run_command([BUILD_EXAMPLE_SCRIPT, "--list"]) + return 0 + + +def handle_examples_tests(args: argparse.Namespace) -> int: + ensure_file(BUILD_EXAMPLE_SCRIPT, "build-example helper") + print_action("Examples", mode="tests", example=args.example) + run_command([BUILD_EXAMPLE_SCRIPT, "--list-tests", args.example]) + return 0 + + +def handle_build(args: argparse.Namespace) -> int: + run_build_example( + example=args.example, + test=args.test, + no_test=args.no_test, + preset=args.preset, + board_name=args.board_name, + extra_cxx_flags=args.extra_cxx_flags, + jobs=args.jobs, + ) + return 0 + + +def handle_flash(args: argparse.Namespace) -> int: + flash_elf(args.elf.resolve(), args.method, not args.no_verify, args.skip_preflight) + return 0 + + +def handle_run(args: argparse.Namespace) -> int: + print(style("Hyper Run", "1")) + print(style("=" * 16, "2")) + run_build_example( + example=args.example, + test=args.test, + no_test=args.no_test, + preset=args.preset, + board_name=args.board_name, + extra_cxx_flags=args.extra_cxx_flags, + jobs=args.jobs, + ) + flash_elf(args.elf.resolve(), args.flash_method, not args.no_verify, args.skip_preflight) + if args.uart: + return open_uart(args.port, args.baud, args.uart_tool) + print_note("run completed", status="ok") + return 0 + + +def handle_uart(args: argparse.Namespace) -> int: + if args.list_ports: + return list_serial_ports() + return open_uart(args.port, args.baud, args.tool) + + +def print_status_line(label: str, value: str) -> None: + print(f"{label:<18} {value}") + + +def handle_doctor(_: argparse.Namespace) -> int: + issues: list[str] = [] + arm_gcc, programmer, clt_version = inspect_clt() + uv_path = uv_executable() + uv_version = read_command_first_line([uv_path, "--version"]) if uv_path else None + + if not arm_gcc.path: + issues.append("arm-none-eabi-gcc missing from PATH") + if not clt_version: + issues.append("STM32CubeCLT version could not be inferred from tool paths") + elif clt_version != DEFAULT_REQUIRED_CLT_VERSION: + issues.append(f"STM32CubeCLT {clt_version} detected, expected {DEFAULT_REQUIRED_CLT_VERSION}") + if not uv_path: + issues.append("uv is not installed") + + overall_status = "ok" if not issues else "wrong" + + print(style(f"Hyper Doctor {status_tag(overall_status)}", "1")) + print(style("=" * 32, "2")) + + section_title("Environment") + print_detail("repo", str(REPO_ROOT)) + print_detail("default preset", DEFAULT_PRESET) + print_detail("default flash", DEFAULT_FLASH_METHOD) + print_detail("default baud", str(DEFAULT_UART_BAUD)) + print_detail("latest elf", "present" if LATEST_ELF.exists() else "missing") + + section_title("Toolchain") + print_status_item("arm-none-eabi-gcc", "ok" if arm_gcc.path else "missing", arm_gcc.path or "not found") + if arm_gcc.version_line: + print_detail("version", arm_gcc.version_line) + print_status_item("STM32_Programmer", "ok" if programmer.path else "missing", programmer.path or "not found") + if programmer.version_line: + print_detail("version", programmer.version_line) + print_status_item( + "STM32CubeCLT", + overall_status, + f"detected={clt_version or 'unknown'} expected={DEFAULT_REQUIRED_CLT_VERSION}", + ) + if issues: + for issue in issues: + print_note(issue, status="wrong") + + section_title("Host Tools") + for tool_name in ("cmake", "ninja", "openocd", "tio", "cu"): + path = command_path(tool_name) + print_status_item(tool_name, "ok" if path else "missing", path or "not found") + print_status_item("uv", "ok" if uv_path else "missing", uv_path or "not found") + if uv_version: + print_detail("version", uv_version) + + try: + resolved_uart_tool = resolve_uart_tool("auto") + uart_tool_status = "ok" + except HyperError: + resolved_uart_tool = "not found" + uart_tool_status = "missing" + print_status_item("uart tool", uart_tool_status, resolved_uart_tool) + + section_title("Serial") + ports = find_serial_ports() + if not ports: + print_status_item("serial ports", "warn", "none detected") + elif len(ports) == 1: + print_status_item("serial port", "ok", ports[0]) + else: + print_status_item("serial ports", "ok", f"{len(ports)} detected") + for port in ports: + print_detail("port", port) + + section_title("Repo Helpers") + print_status_item("stlib build.py", "ok" if STLIB_BUILD_SCRIPT.is_file() else "missing", str(STLIB_BUILD_SCRIPT)) + print_status_item( + "stlib sim tests", + "ok" if STLIB_SIM_TESTS_SCRIPT.is_file() else "missing", + str(STLIB_SIM_TESTS_SCRIPT), + ) + + section_title("Summary") + if issues: + print_note("doctor failed", status="wrong") + else: + print_note("doctor passed", status="ok") + return 1 if issues else 0 + + +def handle_stlib_build(args: argparse.Namespace) -> int: + ensure_file(STLIB_BUILD_SCRIPT, "ST-LIB build helper") + print_action("ST-LIB Build", preset=args.preset, tests="yes" if args.run_tests else "no") + cmd: list[object] = [sys.executable, STLIB_BUILD_SCRIPT, "--preset", args.preset] + if args.run_tests: + cmd.append("--run-tests") + run_command(cmd, cwd=STLIB_ROOT) + print_note("ST-LIB build completed", status="ok") + return 0 + + +def handle_stlib_sim_tests(_: argparse.Namespace) -> int: + ensure_file(STLIB_SIM_TESTS_SCRIPT, "ST-LIB simulator test helper") + print_action("ST-LIB Sim Tests", script=str(STLIB_SIM_TESTS_SCRIPT)) + run_command([STLIB_SIM_TESTS_SCRIPT], cwd=STLIB_ROOT) + print_note("sim tests completed", status="ok") + return 0 + + +def handle_tcpip_nucleo(args: argparse.Namespace) -> int: + ensure_file(TCPIP_NUCLEO_SCRIPT, "TCPIP Nucleo helper") + print_action("TCPIP Nucleo", script=str(TCPIP_NUCLEO_SCRIPT)) + run_command([TCPIP_NUCLEO_SCRIPT, *args.forwarded_args]) + print_note("tcpip nucleo helper completed", status="ok") + return 0 + + +def handle_tcpip_stress(args: argparse.Namespace) -> int: + ensure_file(TCPIP_STRESS_SCRIPT, "TCPIP stress helper") + print_action("TCPIP Stress", script=str(TCPIP_STRESS_SCRIPT)) + run_command([TCPIP_STRESS_SCRIPT, *args.forwarded_args]) + print_note("tcpip stress helper completed", status="ok") + return 0 + + +def add_example_build_arguments(parser: argparse.ArgumentParser) -> None: + parser.add_argument("example", help="Example name, e.g. adc or tcpip") + group = parser.add_mutually_exclusive_group() + group.add_argument("--test", help="Test selector, e.g. 0, 1 or TEST_1") + group.add_argument("--no-test", action="store_true", help="Do not inject any TEST_* macro") + parser.add_argument("--preset", default=DEFAULT_PRESET, help="CMake preset to build") + parser.add_argument("--board-name", help="Override BOARD_NAME for code generation") + parser.add_argument("--extra-cxx-flags", help="Extra flags appended after the injected defines") + parser.add_argument("-j", "--jobs", type=int, help="Set CMAKE_BUILD_PARALLEL_LEVEL") + + +def build_parser() -> argparse.ArgumentParser: + parser = HyperArgumentParser( + prog="hyper", + formatter_class=argparse.RawDescriptionHelpFormatter, + description="Repo-local helper CLI for example builds, flashing, UART and ST-LIB wrappers.", + epilog=HELP_EXAMPLES, + ) + subparsers = parser.add_subparsers(dest="command", required=True) + + init_parser = subparsers.add_parser("init", help="Run tools/init.sh") + init_parser.set_defaults(func=handle_init) + + examples_parser = subparsers.add_parser("examples", help="Inspect available examples") + examples_subparsers = examples_parser.add_subparsers(dest="examples_command", required=True) + + examples_list_parser = examples_subparsers.add_parser("list", help="List EXAMPLE_* selectors") + examples_list_parser.set_defaults(func=handle_examples_list) + + examples_tests_parser = examples_subparsers.add_parser("tests", help="List TEST_* selectors for one example") + examples_tests_parser.add_argument("example", help="Example name, e.g. adc") + examples_tests_parser.set_defaults(func=handle_examples_tests) + + build_parser_cmd = subparsers.add_parser("build", help="Build one example") + add_example_build_arguments(build_parser_cmd) + build_parser_cmd.set_defaults(func=handle_build) + + flash_parser = subparsers.add_parser("flash", help="Flash an ELF image onto the board") + flash_parser.add_argument("--elf", type=Path, default=LATEST_ELF, help="ELF image to flash") + flash_parser.add_argument( + "--method", + choices=("auto", "stm32prog", "openocd"), + default=DEFAULT_FLASH_METHOD, + help="Flash tool selection", + ) + flash_parser.add_argument("--no-verify", action="store_true", help="Skip flash verification when supported") + flash_parser.add_argument( + "--skip-preflight", + action="store_true", + help="Skip tools/preflash_check.py before flashing", + ) + flash_parser.set_defaults(func=handle_flash) + + run_parser = subparsers.add_parser("run", help="Build then flash one example") + add_example_build_arguments(run_parser) + run_parser.add_argument("--elf", type=Path, default=LATEST_ELF, help="ELF image to flash after build") + run_parser.add_argument( + "--flash-method", + choices=("auto", "stm32prog", "openocd"), + default=DEFAULT_FLASH_METHOD, + help="Flash tool selection", + ) + run_parser.add_argument("--no-verify", action="store_true", help="Skip flash verification when supported") + run_parser.add_argument( + "--skip-preflight", + action="store_true", + help="Skip tools/preflash_check.py before flashing", + ) + run_parser.add_argument("--uart", action="store_true", help="Open UART after flashing") + run_parser.add_argument("--port", help="Serial port path or 'auto'") + run_parser.add_argument("--baud", type=int, default=DEFAULT_UART_BAUD, help="UART baud rate") + run_parser.add_argument( + "--uart-tool", + choices=("auto", "tio", "cu"), + default=DEFAULT_UART_TOOL, + help="Serial terminal program", + ) + run_parser.set_defaults(func=handle_run) + + uart_parser = subparsers.add_parser("uart", help="Open the board UART") + uart_parser.add_argument("--port", help="Serial port path or 'auto'") + uart_parser.add_argument("--baud", type=int, default=DEFAULT_UART_BAUD, help="UART baud rate") + uart_parser.add_argument( + "--tool", + choices=("auto", "tio", "cu"), + default=DEFAULT_UART_TOOL, + help="Serial terminal program", + ) + uart_parser.add_argument("--list-ports", action="store_true", help="List candidate serial ports and exit") + uart_parser.set_defaults(func=handle_uart) + + doctor_parser = subparsers.add_parser("doctor", help="Show tool and serial-port availability") + doctor_parser.set_defaults(func=handle_doctor) + + stlib_parser = subparsers.add_parser("stlib", help="Run ST-LIB helper flows") + stlib_subparsers = stlib_parser.add_subparsers(dest="stlib_command", required=True) + + stlib_build_parser = stlib_subparsers.add_parser("build", help="Run deps/ST-LIB/tools/build.py") + stlib_build_parser.add_argument("--preset", default="simulator", help="ST-LIB CMake preset") + stlib_build_parser.add_argument("--run-tests", action="store_true", help="Run tests after build") + stlib_build_parser.set_defaults(func=handle_stlib_build) + + stlib_tests_parser = stlib_subparsers.add_parser("sim-tests", help="Run ST-LIB simulator tests") + stlib_tests_parser.set_defaults(func=handle_stlib_sim_tests) + + tcpip_parser = subparsers.add_parser("tcpip", help="Forward to existing TCP/IP helper scripts") + tcpip_subparsers = tcpip_parser.add_subparsers(dest="tcpip_command", required=True) + + tcpip_nucleo_parser = tcpip_subparsers.add_parser("nucleo", help="Run tools/run_example_tcpip_nucleo.sh") + tcpip_nucleo_parser.add_argument("forwarded_args", nargs=argparse.REMAINDER) + tcpip_nucleo_parser.set_defaults(func=handle_tcpip_nucleo) + + tcpip_stress_parser = tcpip_subparsers.add_parser("stress", help="Run tools/run_example_tcpip_stress.sh") + tcpip_stress_parser.add_argument("forwarded_args", nargs=argparse.REMAINDER) + tcpip_stress_parser.set_defaults(func=handle_tcpip_stress) + + return parser + + +def main() -> int: + parser = build_parser() + if len(sys.argv) == 1: + print(parser.format_help(), end="") + return 0 + args = parser.parse_args() + try: + return args.func(args) + except subprocess.CalledProcessError as exc: + print_note(f"command failed with exit code {exc.returncode}", status="wrong") + return exc.returncode + except HyperError as exc: + print(style(f"Hyper Error {status_tag('wrong')}", "1"), file=sys.stderr) + print(f" {exc}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/toolchains/stm32.cmake b/toolchains/stm32.cmake index 0c1e0047..ceb0c22b 100644 --- a/toolchains/stm32.cmake +++ b/toolchains/stm32.cmake @@ -1,31 +1,114 @@ set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_PROCESSOR ARM) +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) + +set(STM32_CLT_REQUIRED_VERSION "1.21.0" CACHE STRING "Required STM32CubeCLT version") +set(TOOLCHAIN_PREFIX arm-none-eabi-) + +set(_stm32_clt_candidate_roots "") -if(MINGW OR CYGWIN OR WIN32) - set(UTIL_SEARCH_CMD where) -elseif(UNIX OR APPLE) - set(UTIL_SEARCH_CMD which) +if(DEFINED ENV{STM32_CLT_ROOT} AND NOT "$ENV{STM32_CLT_ROOT}" STREQUAL "") + list(APPEND _stm32_clt_candidate_roots "$ENV{STM32_CLT_ROOT}") +endif() +if(DEFINED ENV{HYPER_STM32CLT_ROOT} AND NOT "$ENV{HYPER_STM32CLT_ROOT}" STREQUAL "") + list(APPEND _stm32_clt_candidate_roots "$ENV{HYPER_STM32CLT_ROOT}") endif() -set(TOOLCHAIN_PREFIX arm-none-eabi-) +if(WIN32) + if(DEFINED ENV{ProgramFiles} AND NOT "$ENV{ProgramFiles}" STREQUAL "") + list(APPEND _stm32_clt_candidate_roots + "$ENV{ProgramFiles}/STMicroelectronics/STM32Cube/STM32CubeCLT_${STM32_CLT_REQUIRED_VERSION}" + "$ENV{ProgramFiles}/STMicroelectronics/STM32CubeCLT_${STM32_CLT_REQUIRED_VERSION}" + ) + file(GLOB _stm32_clt_globbed LIST_DIRECTORIES true + "$ENV{ProgramFiles}/STMicroelectronics/STM32Cube/STM32CubeCLT_${STM32_CLT_REQUIRED_VERSION}*" + "$ENV{ProgramFiles}/STMicroelectronics/STM32Cube/STM32CubeCLT-${STM32_CLT_REQUIRED_VERSION}*" + "$ENV{ProgramFiles}/STMicroelectronics/STM32CubeCLT_${STM32_CLT_REQUIRED_VERSION}*" + "$ENV{ProgramFiles}/STMicroelectronics/STM32CubeCLT-${STM32_CLT_REQUIRED_VERSION}*" + ) + list(APPEND _stm32_clt_candidate_roots ${_stm32_clt_globbed}) + endif() + list(APPEND _stm32_clt_candidate_roots + "C:/ST/STM32CubeCLT_${STM32_CLT_REQUIRED_VERSION}" + "C:/ST/STM32CubeCLT-${STM32_CLT_REQUIRED_VERSION}" + ) + file(GLOB _stm32_clt_globbed_c LIST_DIRECTORIES true + "C:/ST/STM32CubeCLT_${STM32_CLT_REQUIRED_VERSION}*" + "C:/ST/STM32CubeCLT-${STM32_CLT_REQUIRED_VERSION}*" + ) + list(APPEND _stm32_clt_candidate_roots ${_stm32_clt_globbed_c}) +else() + list(APPEND _stm32_clt_candidate_roots + "/opt/ST/STM32CubeCLT_${STM32_CLT_REQUIRED_VERSION}" + "/opt/ST/STM32CubeCLT-${STM32_CLT_REQUIRED_VERSION}" + "$ENV{HOME}/ST/STM32CubeCLT_${STM32_CLT_REQUIRED_VERSION}" + "$ENV{HOME}/ST/STM32CubeCLT-${STM32_CLT_REQUIRED_VERSION}" + ) + file(GLOB _stm32_clt_globbed LIST_DIRECTORIES true + "/opt/ST/STM32CubeCLT_${STM32_CLT_REQUIRED_VERSION}*" + "/opt/ST/STM32CubeCLT-${STM32_CLT_REQUIRED_VERSION}*" + "$ENV{HOME}/ST/STM32CubeCLT_${STM32_CLT_REQUIRED_VERSION}*" + "$ENV{HOME}/ST/STM32CubeCLT-${STM32_CLT_REQUIRED_VERSION}*" + ) + list(APPEND _stm32_clt_candidate_roots ${_stm32_clt_globbed}) +endif() -execute_process( - COMMAND ${UTIL_SEARCH_CMD} ${TOOLCHAIN_PREFIX}gcc - OUTPUT_VARIABLE BINUTILS_PATH - OUTPUT_STRIP_TRAILING_WHITESPACE -) +list(REMOVE_DUPLICATES _stm32_clt_candidate_roots) -get_filename_component(ARM_TOOLCHAIN_DIR ${BINUTILS_PATH} DIRECTORY) -set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) +set(STM32_CLT_ROOT "") +foreach(_candidate IN LISTS _stm32_clt_candidate_roots) + if(EXISTS "${_candidate}/GNU-tools-for-STM32/bin/${TOOLCHAIN_PREFIX}gcc") + set(STM32_CLT_ROOT "${_candidate}") + break() + endif() +endforeach() + +if(NOT STM32_CLT_ROOT) + find_program(_stm32_arm_gcc NAMES ${TOOLCHAIN_PREFIX}gcc) + if(NOT _stm32_arm_gcc) + message(FATAL_ERROR + "STM32CubeCLT ${STM32_CLT_REQUIRED_VERSION} is required, but arm-none-eabi-gcc was not found. " + "Install the required CLT and/or set STM32_CLT_ROOT." + ) + endif() + + get_filename_component(_stm32_arm_gcc_realpath "${_stm32_arm_gcc}" REALPATH) + string(REGEX MATCH "STM32CubeCLT[_-]([0-9]+\\.[0-9]+\\.[0-9]+)" _stm32_path_match "${_stm32_arm_gcc_realpath}") + + if(NOT CMAKE_MATCH_1) + message(FATAL_ERROR + "STM32CubeCLT ${STM32_CLT_REQUIRED_VERSION} is required, but the compiler in PATH " + "does not point to a versioned STM32CubeCLT installation:\n ${_stm32_arm_gcc_realpath}" + ) + endif() + + if(NOT CMAKE_MATCH_1 STREQUAL STM32_CLT_REQUIRED_VERSION) + message(FATAL_ERROR + "STM32CubeCLT ${STM32_CLT_REQUIRED_VERSION} is required, but found ${CMAKE_MATCH_1}:\n ${_stm32_arm_gcc_realpath}" + ) + endif() + + get_filename_component(_stm32_bin_dir "${_stm32_arm_gcc_realpath}" DIRECTORY) + get_filename_component(_stm32_gnu_dir "${_stm32_bin_dir}" DIRECTORY) + get_filename_component(STM32_CLT_ROOT "${_stm32_gnu_dir}" DIRECTORY) +endif() + +string(REGEX MATCH "STM32CubeCLT[_-]([0-9]+\\.[0-9]+\\.[0-9]+)" _stm32_root_match "${STM32_CLT_ROOT}") +if(CMAKE_MATCH_1 AND NOT CMAKE_MATCH_1 STREQUAL STM32_CLT_REQUIRED_VERSION) + message(FATAL_ERROR + "STM32CubeCLT ${STM32_CLT_REQUIRED_VERSION} is required, but selected root is ${CMAKE_MATCH_1}:\n ${STM32_CLT_ROOT}" + ) +endif() -set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}gcc) -set(CMAKE_ASM_COMPILER ${CMAKE_C_COMPILER}) -set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}g++) +set(ARM_TOOLCHAIN_DIR "${STM32_CLT_ROOT}/GNU-tools-for-STM32/bin") +set(CMAKE_C_COMPILER "${ARM_TOOLCHAIN_DIR}/${TOOLCHAIN_PREFIX}gcc") +set(CMAKE_ASM_COMPILER "${CMAKE_C_COMPILER}") +set(CMAKE_CXX_COMPILER "${ARM_TOOLCHAIN_DIR}/${TOOLCHAIN_PREFIX}g++") -set(CMAKE_OBJCOPY ${ARM_TOOLCHAIN_DIR}/${TOOLCHAIN_PREFIX}objcopy CACHE INTERNAL "objcopy tool") -set(CMAKE_SIZE_UTIL ${ARM_TOOLCHAIN_DIR}/${TOOLCHAIN_PREFIX}size CACHE INTERNAL "size tool") +set(CMAKE_OBJCOPY "${ARM_TOOLCHAIN_DIR}/${TOOLCHAIN_PREFIX}objcopy" CACHE INTERNAL "objcopy tool") +set(CMAKE_SIZE_UTIL "${ARM_TOOLCHAIN_DIR}/${TOOLCHAIN_PREFIX}size" CACHE INTERNAL "size tool") -set(CMAKE_FIND_ROOT_PATH ${BINUTILS_PATH}) +set(CMAKE_FIND_ROOT_PATH "${STM32_CLT_ROOT}") set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) diff --git a/tools/build-example.sh b/tools/build-example.sh old mode 100644 new mode 100755 diff --git a/tools/init.sh b/tools/init.sh index 7bcdb9b0..fb6b8a63 100755 --- a/tools/init.sh +++ b/tools/init.sh @@ -10,12 +10,25 @@ echo "Script directory: $SCRIPT_DIR" echo "Repository directory: $REPO_DIR" cd "$REPO_DIR" -python3 -m venv virtual -source ./virtual/bin/activate -python -m pip install --upgrade pip -pip install -r requirements.txt +if [[ "${HYPER_SKIP_PYTHON_INIT:-0}" != "1" ]]; then + python3 -m venv virtual + source ./virtual/bin/activate + python -m pip install --upgrade pip + pip install -r requirements.txt +else + echo "Skipping Python environment setup: managed by hyper/uv" +fi + +while read -r _ submodule_path; do + if git -C "$submodule_path" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + if [[ -n "$(git -C "$submodule_path" status --porcelain)" ]]; then + echo "Skipping dirty submodule: $submodule_path" + continue + fi + fi + git submodule update --init -- "$submodule_path" +done < <(git config --file .gitmodules --get-regexp path) -git submodule update --init ./deps/ST-LIB/tools/init-submodules.sh echo "Setup complete." From aab889e19e35ebd1e91ed29a02721f90bd92496a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20S=C3=A1ez?= Date: Mon, 16 Mar 2026 23:26:40 +0100 Subject: [PATCH 05/10] Updated hyper CLI --- Core/Src/Runes/generated_metadata.cpp | 6 ++-- deps/ST-LIB | 2 +- hyper | 41 ++++------------------ tools/build-example.sh | 49 ++++++++++++++++++++++----- 4 files changed, 52 insertions(+), 46 deletions(-) diff --git a/Core/Src/Runes/generated_metadata.cpp b/Core/Src/Runes/generated_metadata.cpp index 1133921e..e10aef85 100644 --- a/Core/Src/Runes/generated_metadata.cpp +++ b/Core/Src/Runes/generated_metadata.cpp @@ -5,11 +5,11 @@ extern "C" { const char DESCRIPTION[255] __attribute__((section(".metadata_pool"))) = "****************" // placeholder for beggining - "20260316T224044" // DateTime using ISO-8601 format + "20260316T225223" // DateTime using ISO-8601 format " " // alignment - "aaecc655" // STLIB commit + "ee54dc7a" // STLIB commit "--------" // ADJ commit - "9d2ba8c9" // Board commit + "a3e59583" // Board commit // the '=' is used for unparsing ; } diff --git a/deps/ST-LIB b/deps/ST-LIB index ee54dc7a..0e20d995 160000 --- a/deps/ST-LIB +++ b/deps/ST-LIB @@ -1 +1 @@ -Subproject commit ee54dc7af7a6043db75aca9ffdcea8a38889a5a5 +Subproject commit 0e20d995dae551323c86f641f48775a64ef8dba1 diff --git a/hyper b/hyper index f327c0a3..c0952214 100755 --- a/hyper +++ b/hyper @@ -22,8 +22,6 @@ STLIB_ROOT = REPO_ROOT / "deps" / "ST-LIB" BUILD_EXAMPLE_SCRIPT = TOOLS_DIR / "build-example.sh" PREFLASH_CHECK_SCRIPT = TOOLS_DIR / "preflash_check.py" INIT_SCRIPT = TOOLS_DIR / "init.sh" -TCPIP_NUCLEO_SCRIPT = TOOLS_DIR / "run_example_tcpip_nucleo.sh" -TCPIP_STRESS_SCRIPT = TOOLS_DIR / "run_example_tcpip_stress.sh" STLIB_BUILD_SCRIPT = STLIB_ROOT / "tools" / "build.py" STLIB_SIM_TESTS_SCRIPT = STLIB_ROOT / "tools" / "run_sim_tests.sh" LATEST_ELF = REPO_ROOT / "out" / "build" / "latest.elf" @@ -72,6 +70,7 @@ HELP_BANNER = "\n".join( HELP_EXAMPLES = """Examples: hyper doctor hyper build adc --preset nucleo-debug --test 1 + hyper build main --preset nucleo-debug hyper run adc --preset nucleo-debug --test 1 --uart hyper uart --list-ports """ @@ -735,7 +734,8 @@ def run_build_example( jobs: int | None, ) -> None: ensure_file(BUILD_EXAMPLE_SCRIPT, "build-example helper") - selected_test = "none" if no_test else (test or "default") + is_main_target = example.strip().lower() in {"main", "default"} + selected_test = "none" if no_test or is_main_target else (test or "default") print_action( "Build", example=example, @@ -1029,24 +1029,8 @@ def handle_stlib_sim_tests(_: argparse.Namespace) -> int: return 0 -def handle_tcpip_nucleo(args: argparse.Namespace) -> int: - ensure_file(TCPIP_NUCLEO_SCRIPT, "TCPIP Nucleo helper") - print_action("TCPIP Nucleo", script=str(TCPIP_NUCLEO_SCRIPT)) - run_command([TCPIP_NUCLEO_SCRIPT, *args.forwarded_args]) - print_note("tcpip nucleo helper completed", status="ok") - return 0 - - -def handle_tcpip_stress(args: argparse.Namespace) -> int: - ensure_file(TCPIP_STRESS_SCRIPT, "TCPIP stress helper") - print_action("TCPIP Stress", script=str(TCPIP_STRESS_SCRIPT)) - run_command([TCPIP_STRESS_SCRIPT, *args.forwarded_args]) - print_note("tcpip stress helper completed", status="ok") - return 0 - - def add_example_build_arguments(parser: argparse.ArgumentParser) -> None: - parser.add_argument("example", help="Example name, e.g. adc or tcpip") + parser.add_argument("example", help="Target name, e.g. adc or main") group = parser.add_mutually_exclusive_group() group.add_argument("--test", help="Test selector, e.g. 0, 1 or TEST_1") group.add_argument("--no-test", action="store_true", help="Do not inject any TEST_* macro") @@ -1060,7 +1044,7 @@ def build_parser() -> argparse.ArgumentParser: parser = HyperArgumentParser( prog="hyper", formatter_class=argparse.RawDescriptionHelpFormatter, - description="Repo-local helper CLI for example builds, flashing, UART and ST-LIB wrappers.", + description="Repo-local helper CLI for firmware targets, flashing, UART and ST-LIB wrappers.", epilog=HELP_EXAMPLES, ) subparsers = parser.add_subparsers(dest="command", required=True) @@ -1078,7 +1062,7 @@ def build_parser() -> argparse.ArgumentParser: examples_tests_parser.add_argument("example", help="Example name, e.g. adc") examples_tests_parser.set_defaults(func=handle_examples_tests) - build_parser_cmd = subparsers.add_parser("build", help="Build one example") + build_parser_cmd = subparsers.add_parser("build", help="Build one firmware target") add_example_build_arguments(build_parser_cmd) build_parser_cmd.set_defaults(func=handle_build) @@ -1098,7 +1082,7 @@ def build_parser() -> argparse.ArgumentParser: ) flash_parser.set_defaults(func=handle_flash) - run_parser = subparsers.add_parser("run", help="Build then flash one example") + run_parser = subparsers.add_parser("run", help="Build then flash one firmware target") add_example_build_arguments(run_parser) run_parser.add_argument("--elf", type=Path, default=LATEST_ELF, help="ELF image to flash after build") run_parser.add_argument( @@ -1150,17 +1134,6 @@ def build_parser() -> argparse.ArgumentParser: stlib_tests_parser = stlib_subparsers.add_parser("sim-tests", help="Run ST-LIB simulator tests") stlib_tests_parser.set_defaults(func=handle_stlib_sim_tests) - tcpip_parser = subparsers.add_parser("tcpip", help="Forward to existing TCP/IP helper scripts") - tcpip_subparsers = tcpip_parser.add_subparsers(dest="tcpip_command", required=True) - - tcpip_nucleo_parser = tcpip_subparsers.add_parser("nucleo", help="Run tools/run_example_tcpip_nucleo.sh") - tcpip_nucleo_parser.add_argument("forwarded_args", nargs=argparse.REMAINDER) - tcpip_nucleo_parser.set_defaults(func=handle_tcpip_nucleo) - - tcpip_stress_parser = tcpip_subparsers.add_parser("stress", help="Run tools/run_example_tcpip_stress.sh") - tcpip_stress_parser.add_argument("forwarded_args", nargs=argparse.REMAINDER) - tcpip_stress_parser.set_defaults(func=handle_tcpip_stress) - return parser diff --git a/tools/build-example.sh b/tools/build-example.sh index 22af2058..24b8c13c 100755 --- a/tools/build-example.sh +++ b/tools/build-example.sh @@ -5,12 +5,13 @@ usage() { cat <<'EOF' Usage: tools/build-example.sh --example [options] -Build one example by enabling BUILD_EXAMPLES and injecting EXAMPLE_* / TEST_* macros. +Build one firmware target. Examples enable BUILD_EXAMPLES and inject EXAMPLE_* / TEST_* macros. +The special target 'main' builds Core/Src/main.cpp as the default firmware. Options: -l, --list List all available EXAMPLE_* and their TEST_* macros. --list-tests List TEST_* macros for one example (e.g. adc, EXAMPLE_ADC). - -e, --example Example name, e.g. adc or EXAMPLE_ADC. + -e, --example Target name, e.g. adc, EXAMPLE_ADC or main. -t, --test Test selector (default: TEST_0). Accepts 0, 1, TEST_1... --no-test Do not define any TEST_* macro. -p, --preset CMake preset. Defaults to nucleo-debug. @@ -23,12 +24,19 @@ Examples: tools/build-example.sh --list-tests adc tools/build-example.sh --example adc --preset nucleo-debug tools/build-example.sh --example adc --test 1 --preset nucleo-debug + tools/build-example.sh --example main --preset nucleo-debug tools/build-example.sh --example ethernet --test TEST_0 --board-name TEST EOF } normalize_example_macro() { local input="$1" + local normalized_lower + normalized_lower="$(printf '%s' "$input" | tr '[:upper:]-' '[:lower:]_')" + if [[ "$normalized_lower" == "main" || "$normalized_lower" == "default" ]]; then + printf 'MAIN' + return + fi input="${input#EXAMPLE_}" input="${input#example_}" input="$(printf '%s' "$input" | tr '[:lower:]-' '[:upper:]_')" @@ -48,11 +56,18 @@ normalize_test_macro() { } collect_examples() { - grep -Rho "EXAMPLE_[A-Z0-9_]\+" "${repo_root}/Core/Src/Examples"/*.cpp 2>/dev/null | sort -u + { + printf 'MAIN\n' + grep -Rho "EXAMPLE_[A-Z0-9_]\+" "${repo_root}/Core/Src/Examples"/*.cpp 2>/dev/null + } | sort -u } find_example_file() { local example_macro="$1" + if [[ "$example_macro" == "MAIN" ]]; then + printf '%s\n' "${repo_root}/Core/Src/main.cpp" + return 0 + fi local file for file in "${repo_root}/Core/Src/Examples"/*.cpp; do [[ -f "$file" ]] || continue @@ -66,6 +81,9 @@ find_example_file() { collect_tests_for_example() { local example_macro="$1" + if [[ "$example_macro" == "MAIN" ]]; then + return 0 + fi local file file="$(find_example_file "$example_macro" || true)" if [[ -z "$file" ]]; then @@ -86,6 +104,7 @@ print_examples_table() { printf "%-40s tests: %s\n" "$macro" "$tests" printf " file: %s\n" "$file_path" done < <( + printf "MAIN||Core/Src/main.cpp\n" for file in "${repo_root}/Core/Src/Examples"/*.cpp; do [[ -f "$file" ]] || continue example_macro="$(grep -Eho "EXAMPLE_[A-Z0-9_]+" "$file" | head -n1 || true)" @@ -236,12 +255,26 @@ if [[ "$test_explicit" -ne 1 ]]; then fi fi -define_flags="-D${example_macro}" -if [[ -n "$test_macro" ]]; then - define_flags+=" -D${test_macro}" +build_examples="ON" +define_flags="" +if [[ "$example_macro" != "MAIN" ]]; then + define_flags="-D${example_macro}" + if [[ -n "$test_macro" ]]; then + define_flags+=" -D${test_macro}" + fi +else + if [[ -n "$test_macro" ]]; then + echo "Target 'main' does not support TEST_* macros." >&2 + exit 1 + fi + build_examples="OFF" fi if [[ -n "$extra_cxx_flags" ]]; then - define_flags+=" ${extra_cxx_flags}" + if [[ -n "$define_flags" ]]; then + define_flags+=" ${extra_cxx_flags}" + else + define_flags="${extra_cxx_flags}" + fi fi binary_dir="${repo_root}/out/build/examples/$(sanitize_path_fragment "${preset}")/$(sanitize_path_fragment "${example_macro}")" @@ -267,7 +300,7 @@ configure_cmd=( cmake --preset "${preset}" -B "${binary_dir}" - -DBUILD_EXAMPLES=ON + "-DBUILD_EXAMPLES=${build_examples}" -DCMAKE_EXPORT_COMPILE_COMMANDS=OFF "-DCMAKE_CXX_FLAGS=${define_flags}" ) From 2bb1c13a6006af588a3462d7ab320d07411e7112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20S=C3=A1ez?= Date: Mon, 16 Mar 2026 23:56:37 +0100 Subject: [PATCH 06/10] ST-LIB updated --- deps/ST-LIB | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/ST-LIB b/deps/ST-LIB index 0e20d995..f8cf58cf 160000 --- a/deps/ST-LIB +++ b/deps/ST-LIB @@ -1 +1 @@ -Subproject commit 0e20d995dae551323c86f641f48775a64ef8dba1 +Subproject commit f8cf58cf824fa15da36ebc8b4fe5ecb0a6fd1af7 From d57cede097ccd267532b4226e604c9eee0871779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20S=C3=A1ez?= Date: Tue, 17 Mar 2026 00:02:26 +0100 Subject: [PATCH 07/10] minor change on banner --- hyper | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hyper b/hyper index c0952214..1dbae30c 100755 --- a/hyper +++ b/hyper @@ -64,7 +64,7 @@ HELP_BANNER_ROWS = [ HELP_BANNER_WIDTH = 82 HELP_BANNER = "\n".join( ["╔" + "═" * HELP_BANNER_WIDTH + "╗"] - + [f"│{row.center(HELP_BANNER_WIDTH)}│" for row in HELP_BANNER_ROWS] + + [f"║{row.center(HELP_BANNER_WIDTH)}║" for row in HELP_BANNER_ROWS] + ["╚" + "═" * HELP_BANNER_WIDTH + "╝"] ) HELP_EXAMPLES = """Examples: From 82c3af312f9c198c96e2c6509bdb60381ed39be7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20S=C3=A1ez?= Date: Sat, 21 Mar 2026 09:13:49 +0100 Subject: [PATCH 08/10] Updated docs with new hyper CLI --- README.md | 25 ++++++++----------- docs/examples/README.md | 12 +++++---- docs/examples/example-adc.md | 2 +- docs/examples/example-ethernet.md | 4 +-- docs/examples/example-exti.md | 2 +- docs/examples/example-hardfault.md | 10 ++++---- .../example-linear-sensor-characterization.md | 3 +-- docs/examples/example-mpu.md | 8 +++--- docs/examples/example-packets.md | 2 +- docs/examples/example-tcpip.md | 2 +- docs/template-project/build-debug.md | 15 +++-------- docs/template-project/example-tcpip.md | 18 ++++++------- docs/template-project/setup.md | 6 ++--- docs/template-project/testing.md | 8 +++--- 14 files changed, 52 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index d21a46ff..6ddf4786 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,15 @@ HyperloopUPV STM32 firmware template based on CMake + VSCode, using `deps/ST-LIB ## Quickstart ```sh -./tools/init.sh -cmake --preset simulator -cmake --build --preset simulator -ctest --preset simulator-all +./hyper init +./hyper doctor +./hyper build main --preset simulator +./hyper stlib build --preset simulator --run-tests ``` ## Hyper CLI -The repo now includes a local helper CLI at `./hyper` for the common hardware flow: +The repo includes a local helper CLI at `./hyper` for the common hardware flow: ```sh ./hyper examples list @@ -23,8 +23,6 @@ The repo now includes a local helper CLI at `./hyper` for the common hardware fl ./hyper doctor ``` -For UART, `hyper` now prefers `tio` and falls back to `cu` if `tio` is not installed. - It wraps the existing repo scripts instead of replacing them, and also exposes a small ST-LIB namespace: ```sh @@ -40,9 +38,8 @@ Useful defaults can be pinned with environment variables: - `HYPER_UART_BAUD` - `HYPER_UART_TOOL` -Recommended UART setup: - -Install `tio` with your package manager, then run `./hyper uart`. +> [!NOTE] +To connect through UART (`./hyper uart`), it's recommended to install `tio` with your package manager. ## Documentation @@ -58,10 +55,10 @@ Install `tio` with your package manager, then run `./hyper uart`. - `simulator`: fast local development and tests. - `nucleo-*` / `board-*`: hardware builds. -List all presets: - ```sh -cmake --list-presets +./hyper build main --preset simulator +./hyper build main --preset nucleo-debug +./hyper build main --preset board-debug ``` ## VSCode Debug @@ -83,7 +80,7 @@ Packet code generation uses `BOARD_NAME` (a key from JSON_ADE). Example: ```sh -cmake --preset board-debug -DBOARD_NAME=TEST +./hyper build main --preset board-debug --board-name TEST ``` Generated packet headers such as `Core/Inc/Communications/Packets/DataPackets.hpp` and `Core/Inc/Communications/Packets/OrderPackets.hpp` are build outputs derived from the active `JSON_ADE` schema. They are intentionally gitignored and should not be edited or committed. diff --git a/docs/examples/README.md b/docs/examples/README.md index c9bfc1ca..01d1de5a 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -15,21 +15,19 @@ Use this folder as the quick reference for: List available examples: ```sh -./tools/build-example.sh --list ./hyper examples list ``` Build one example on a Nucleo: ```sh -./tools/build-example.sh --example adc --preset nucleo-debug --test 0 ./hyper build adc --preset nucleo-debug --test 0 ``` Build one Ethernet example on a Nucleo: ```sh -./tools/build-example.sh --example tcpip --preset nucleo-debug-eth --extra-cxx-flags "-DTCPIP_TEST_HOST_IP=192.168.1.9" +./hyper build tcpip --preset nucleo-debug-eth --extra-cxx-flags "-DTCPIP_TEST_HOST_IP=192.168.1.9" ``` For Ethernet examples, configure the host-side board-link interface with a static IPv4 on the same subnet as the board (default examples expect `192.168.1.9` on the host and `192.168.1.7` on the board). @@ -37,8 +35,12 @@ For Ethernet examples, configure the host-side board-link interface with a stati Flash the latest build: ```sh -STM32_Programmer_CLI -c port=SWD mode=UR -w out/build/latest.elf -v -rst ./hyper flash +``` + +Build, flash, and open UART in one step: + +```sh ./hyper run adc --test 0 --uart ``` @@ -118,7 +120,7 @@ Before building a packet-dependent example, check its document first and confirm ## Notes -- `tools/build-example.sh` now handles named tests such as `usage_fault` as `TEST_USAGE_FAULT`. +- `./hyper build` handles named tests such as `usage_fault` as `TEST_USAGE_FAULT`. - If an example does not define any `TEST_*` selector, the script no longer injects `TEST_0` by default. - `out/build/latest.elf` always points to the most recent MCU build, so flash immediately after building the example you want. - `Core/Inc/Communications/Packets/DataPackets.hpp` and `Core/Inc/Communications/Packets/OrderPackets.hpp` are generated packet headers. They are not source-of-truth files and are intentionally gitignored. diff --git a/docs/examples/example-adc.md b/docs/examples/example-adc.md index bbeb37c8..abb64ae0 100644 --- a/docs/examples/example-adc.md +++ b/docs/examples/example-adc.md @@ -14,7 +14,7 @@ It validates: ## Build ```sh -./tools/build-example.sh --example adc --preset nucleo-debug --test 0 +./hyper build adc --preset nucleo-debug --test 0 ``` Equivalent macro selection: diff --git a/docs/examples/example-ethernet.md b/docs/examples/example-ethernet.md index efab601c..08f1f93c 100644 --- a/docs/examples/example-ethernet.md +++ b/docs/examples/example-ethernet.md @@ -17,13 +17,13 @@ It validates: Nucleo + on-board LAN8742: ```sh -./tools/build-example.sh --example ethernet --preset nucleo-debug-eth --test 0 +./hyper build ethernet --preset nucleo-debug-eth --test 0 ``` Custom board with KSZ8041: ```sh -./tools/build-example.sh --example ethernet --preset board-debug-eth-ksz8041 --test 0 +./hyper build ethernet --preset board-debug-eth-ksz8041 --test 0 ``` Equivalent macro selection: diff --git a/docs/examples/example-exti.md b/docs/examples/example-exti.md index 91920669..cd9b01c3 100644 --- a/docs/examples/example-exti.md +++ b/docs/examples/example-exti.md @@ -13,7 +13,7 @@ It validates: ## Build ```sh -./tools/build-example.sh --example exti --preset nucleo-debug --test 0 +./hyper build exti --preset nucleo-debug --test 0 ``` Equivalent macro selection: diff --git a/docs/examples/example-hardfault.md b/docs/examples/example-hardfault.md index bc98cf83..b91e1d4b 100644 --- a/docs/examples/example-hardfault.md +++ b/docs/examples/example-hardfault.md @@ -9,7 +9,7 @@ It exists to validate: - hard fault capture in flash - CFSR/MMFAR/BFAR decoding - call stack extraction -- the offline analyzer in `hard_faullt_analysis.py` +- the offline analyzer behind `./hyper hardfault-analysis` ## Available tests @@ -22,19 +22,19 @@ It exists to validate: Usage fault example: ```sh -./tools/build-example.sh --example hardfault --preset nucleo-debug --test usage_fault +./hyper build hardfault --preset nucleo-debug --test usage_fault ``` Bus fault example: ```sh -./tools/build-example.sh --example hardfault --preset nucleo-debug --test bus_fault +./hyper build hardfault --preset nucleo-debug --test bus_fault ``` Memory fault example: ```sh -./tools/build-example.sh --example hardfault --preset nucleo-debug --test memory_fault +./hyper build hardfault --preset nucleo-debug --test memory_fault ``` Equivalent macro selection: @@ -57,7 +57,7 @@ The goal is not for the application to keep running. The goal is to verify that 3. Run the analyzer: ```sh -python3 hard_faullt_analysis.py +./hyper hardfault-analysis ``` If the script says you must stop debugging first, disconnect the live debugger and run it again. diff --git a/docs/examples/example-linear-sensor-characterization.md b/docs/examples/example-linear-sensor-characterization.md index bb1693c9..7a6b330d 100644 --- a/docs/examples/example-linear-sensor-characterization.md +++ b/docs/examples/example-linear-sensor-characterization.md @@ -62,8 +62,7 @@ In practice, this means you need the legacy characterization schema checked out Typical sequence once that schema is restored: ```sh -cmake --preset nucleo-debug-eth -DBUILD_EXAMPLES=ON -DCMAKE_CXX_FLAGS='-DEXAMPLE_LINEAR_SENSOR_CHARACTERIZATION' -cmake --build --preset nucleo-debug-eth +./hyper build linear_sensor_characterization --preset nucleo-debug-eth --board-name ``` ## Intended runtime behavior diff --git a/docs/examples/example-mpu.md b/docs/examples/example-mpu.md index 05d6a28e..8bd80365 100644 --- a/docs/examples/example-mpu.md +++ b/docs/examples/example-mpu.md @@ -18,7 +18,7 @@ It validates: Baseline build: ```sh -./tools/build-example.sh --example mpu --preset nucleo-debug --test 0 +./hyper build mpu --preset nucleo-debug --test 0 ``` Pick any other selector with `--test `. @@ -26,8 +26,8 @@ Pick any other selector with `--test `. Examples: ```sh -./tools/build-example.sh --example mpu --preset nucleo-debug --test 11 -./tools/build-example.sh --example mpu --preset nucleo-debug --test 12 +./hyper build mpu --preset nucleo-debug --test 11 +./hyper build mpu --preset nucleo-debug --test 12 ``` Equivalent macro selection: @@ -90,7 +90,7 @@ For runtime fault tests: - Run: ```sh -python3 hard_faullt_analysis.py +./hyper hardfault-analysis ``` ## What a failure usually means diff --git a/docs/examples/example-packets.md b/docs/examples/example-packets.md index 1589c5a5..e27bac0c 100644 --- a/docs/examples/example-packets.md +++ b/docs/examples/example-packets.md @@ -58,7 +58,7 @@ If that **ADJ** content changes, regenerate and verify that these generated symb Nucleo Ethernet build: ```sh -./tools/build-example.sh --example packets --preset nucleo-debug-eth +./hyper build packets --preset nucleo-debug-eth --board-name TEST ``` Equivalent macro selection: diff --git a/docs/examples/example-tcpip.md b/docs/examples/example-tcpip.md index 7c4c92b9..33748f57 100644 --- a/docs/examples/example-tcpip.md +++ b/docs/examples/example-tcpip.md @@ -20,7 +20,7 @@ It validates: Nucleo Ethernet build: ```sh -./tools/build-example.sh --example tcpip --preset nucleo-debug-eth --extra-cxx-flags "-DTCPIP_TEST_HOST_IP=192.168.1.9" +./hyper build tcpip --preset nucleo-debug-eth --extra-cxx-flags "-DTCPIP_TEST_HOST_IP=192.168.1.9" ``` Equivalent macro selection: diff --git a/docs/template-project/build-debug.md b/docs/template-project/build-debug.md index 4bab67f3..a3b324a6 100644 --- a/docs/template-project/build-debug.md +++ b/docs/template-project/build-debug.md @@ -7,24 +7,16 @@ - `nucleo-*` - `board-*` -List all presets: - -```sh -cmake --list-presets -``` - ## 2. Build for Simulator ```sh -cmake --preset simulator -cmake --build --preset simulator +./hyper build main --preset simulator ``` With sanitizers: ```sh -cmake --preset simulator-asan -cmake --build --preset simulator-asan +./hyper build main --preset simulator-asan ``` ## 3. Build for MCU @@ -32,8 +24,7 @@ cmake --build --preset simulator-asan Example: ```sh -cmake --preset board-debug-eth-ksz8041 -DBOARD_NAME=TEST -cmake --build --preset board-debug-eth-ksz8041 +./hyper build main --preset board-debug-eth-ksz8041 --board-name TEST ``` The build output is copied to: diff --git a/docs/template-project/example-tcpip.md b/docs/template-project/example-tcpip.md index 6ae55bd3..c9f0e1ab 100644 --- a/docs/template-project/example-tcpip.md +++ b/docs/template-project/example-tcpip.md @@ -28,19 +28,13 @@ Build with Ethernet enabled and `EXAMPLE_TCPIP` defined. Example (board + KSZ8041): ```sh -cmake --preset board-debug-eth-ksz8041 \ - -DBUILD_EXAMPLES=ON \ - -DCMAKE_CXX_FLAGS='-DEXAMPLE_TCPIP -DTCPIP_TEST_HOST_IP=192.168.1.9' -cmake --build --preset board-debug-eth-ksz8041 +./hyper build tcpip --preset board-debug-eth-ksz8041 --extra-cxx-flags "-DTCPIP_TEST_HOST_IP=192.168.1.9" ``` Example (nucleo + LAN8742): ```sh -cmake --preset nucleo-debug-eth \ - -DBUILD_EXAMPLES=ON \ - -DCMAKE_CXX_FLAGS='-DEXAMPLE_TCPIP -DTCPIP_TEST_HOST_IP=192.168.1.9' -cmake --build --preset nucleo-debug-eth +./hyper build tcpip --preset nucleo-debug-eth --extra-cxx-flags "-DTCPIP_TEST_HOST_IP=192.168.1.9" ``` Notes: @@ -54,7 +48,13 @@ Notes: ## 2. Flash and run -Flash as usual (`out/build/latest.elf`) and power the board. +Flash the latest build with: + +```sh +./hyper flash +``` + +Then power-cycle or reset the board if needed. One-shot automation (build + flash + ping + tests): diff --git a/docs/template-project/setup.md b/docs/template-project/setup.md index 6a8f139d..ea268305 100644 --- a/docs/template-project/setup.md +++ b/docs/template-project/setup.md @@ -18,7 +18,7 @@ For MCU build/flash/debug: From the repository root: ```sh -./tools/init.sh +./hyper init ``` This command: @@ -30,7 +30,7 @@ This command: On Windows: ```bat -tools\init.bat +python hyper init ``` ## 3. `BOARD_NAME` Configuration (codegen) @@ -42,7 +42,7 @@ Code generation uses `BOARD_NAME` (CMake cache variable), and the value must exi Example: ```sh -cmake --preset board-debug -DBOARD_NAME=TEST +./hyper build main --preset board-debug --board-name TEST ``` If not set, the default value is `TEST`. diff --git a/docs/template-project/testing.md b/docs/template-project/testing.md index ace90ebe..88023361 100644 --- a/docs/template-project/testing.md +++ b/docs/template-project/testing.md @@ -3,22 +3,20 @@ ## 1. Local Simulator Tests ```sh -cmake --preset simulator -cmake --build --preset simulator -ctest --preset simulator-all +./hyper stlib build --preset simulator --run-tests ``` Run only ADC tests: ```sh +./hyper stlib build --preset simulator ctest --preset simulator-adc ``` ## 2. Tests with Sanitizers ```sh -cmake --preset simulator-asan -cmake --build --preset simulator-asan +./hyper stlib build --preset simulator-asan ctest --preset simulator-all-asan ``` From 485bd7c8d0a3e547e0cd0e54ee8ccfe53d34c440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20S=C3=A1ez?= Date: Sat, 21 Mar 2026 09:14:28 +0100 Subject: [PATCH 09/10] Moved hard fault analyzer --- tools/hard_fault_analysis.py | 243 +++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 tools/hard_fault_analysis.py diff --git a/tools/hard_fault_analysis.py b/tools/hard_fault_analysis.py new file mode 100644 index 00000000..233eb2c0 --- /dev/null +++ b/tools/hard_fault_analysis.py @@ -0,0 +1,243 @@ +import subprocess +import struct +import re +import os +HF_FLASH_ADDR = 0x080C0000 +HF_FLASH_ADDR_STRING = "0x080C000" +ELF_FILE = "out/build/latest.elf" + +CALL_TRACE_MAX_DEPTH = 16 +def read_flash(): + try: + cmd = [ + "STM32_Programmer_CLI", + "-c", "port=SWD", + "-r32", hex(HF_FLASH_ADDR), "112" + ] + out = subprocess.check_output(cmd, text=True) + return out + except subprocess.CalledProcessError as e: + print("Stop debugging to check fault analysis!!!") + print(f"Error: {e}") + return None + except FileNotFoundError: + print("STM32_Programmer_CLI not found. Make sure it is installed and in PATH.") + return None +def decode_cfsr_memory(cfsr, fault_addr): + memory_fault = cfsr & 0xFF + if memory_fault == 0: + return 0 + print("\nMemory Fault (MMFSR):") + if memory_fault & 0b10000000: + print(f" MMARVALID: Memory fault address valid -> 0x{fault_addr:08X}") + if fault_addr in (0xFFFFFFFF, 0x00000000): + print(" Fault address is invalid / unmapped memory") + else: + mem_info = addr2line(fault_addr) + print_code_context(mem_info) + if memory_fault & 0b00100000: + print(" MLSPERR : Floating Point Unit lazy state preservation error") + if memory_fault & 0b00010000: + print(" MSTKERR : Stack error on entry to exception") + if memory_fault & 0b00001000: + print(" MUNSTKERR : Stack error on return from exception") + if memory_fault & 0b00000010: + print(" DACCVIOL : Data access violation (NULL pointer or invalid access)") + if memory_fault & 0b00000001: + print(" IACCVIOL : Instruction access violation") + return 1 + +# -------------------------- +# Decode Bus Fault (BFSR) +# -------------------------- +def decode_cfsr_bus(cfsr, fault_addr): + bus_fault = (cfsr & 0x0000FF00) >> 8 + if bus_fault == 0: + return 0 + print("\nBus Fault (BFSR):") + if bus_fault & 0b10000000: + if(bus_fault & 0b00000001): + print(f" BFARVALID : Bus fault address valid -> 0x{fault_addr:08X}") + if bus_fault & 0b00000100: + print(f"\033[91m Bus fault address imprecise\033[0m (DON'T LOOK CALL STACK)") + + if bus_fault & 0b00100000: + print(" LSPERR : Floating Point Unit lazy state preservation error") + if bus_fault & 0b00010000: + print(" STKERR : Stack error on entry to exception") + if bus_fault & 0b00001000: + print(" UNSTKERR : Stack error on return from exception") + return 2 + +# -------------------------- +# Decode Usage Fault (UFSR) +# -------------------------- +def decode_cfsr_usage(cfsr): + usage_fault = (cfsr & 0xFFFF0000) >> 16 + if usage_fault == 0: + return 0 + print("\nUsage Fault (UFSR):") + if usage_fault & 0x0200: + print(" DIVBYZERO : Division by zero") + if usage_fault & 0x0100: + print(" UNALIGNED : Unaligned memory access") + if usage_fault & 0x0008: + print(" NOCP : Accessed FPU when not present") + if usage_fault & 0x0004: + print(" INVPC : Invalid Program Counter(PC) load") + if usage_fault & 0x0002: + print(" INVSTATE : Invalid processor state") + if usage_fault & 0x0001: + print(" UNDEFINSTR : Undefined instruction") + return 4 + +def decode_cfsr(cfsr, fault_addr): + error = 0 + error = decode_cfsr_memory(cfsr, fault_addr) + error + error = decode_cfsr_bus(cfsr, fault_addr) + error + error = decode_cfsr_usage(cfsr) + error + return error + + +def addr2line(addr): + cmd = ["arm-none-eabi-addr2line", "-e", ELF_FILE, "-f", "-C", hex(addr)] + try: + output = subprocess.check_output(cmd, text=True).strip() + return output + except Exception as e: + return f"addr2line failed: {e}" + +def analyze_call_stack(calltrace_depth, calltrace_pcs, context=2): + """ + Muestra el call stack, omitiendo frames sin fuente y mostrando snippet de código. + """ + print("\n==== Call Stack Trace ====") + if calltrace_depth == 0: + print("No call trace available.") + return + +def analyze_call_stack(calltrace_depth, calltrace_pcs, context=0): + """ + Muestra el call stack, mostrando snippet de código de la línea exacta + sin intentar sumar líneas arriba/abajo (context=0 por defecto). + Omite frames sin fuente. + """ + print("\n==== Call Stack Trace ====") + if calltrace_depth == 0: + print("No call trace available.") + return + + for pc in calltrace_pcs[:calltrace_depth]: + pc_base = pc & ~1 + snippet = addr2line(pc_base- 4).strip() + if not snippet or snippet.startswith("??:?"): + continue # no hay fuente, saltar + print_code_context(snippet,1) + + print("======================================================") + + + + +def print_code_context(lines, context=2): + """ + lines: exit of addr2line (función + file:line) + context: how many lines up/down show + """ + line_list = lines.splitlines() + if len(line_list) < 2: + print("Invalid addr2line output") + return + + file_line = line_list[1].strip() + split = file_line.rfind(':') + file_path = file_line[:split] + try: + line_no = int(file_line[split+1:]) - 1 # índice base 0 + except ValueError: + print("\33[91m Couldn't find exact line\33[0m") + return + if not os.path.exists(file_path): + print("Source file not found") + return + + with open(file_path, "r") as f: + file_lines = f.readlines() + + start = max(0, line_no - context) + end = min(len(file_lines), line_no + context + 1) + + print(f"\nSource snippet from {file_path}:") + for i in range(start, end): + code = file_lines[i].rstrip() + # Si es la línea del error, la ponemos en rojo + if i == line_no: + print(f"\033[91m{i+1:>4}: {code}\033[0m") # rojo + else: + print(f"{i+1:>4}: {code}") + +def hard_fault_analysis(memory_string): + raw = bytes.fromhex(memory_string) + raw = struct.unpack(">28I",raw) + hf = { + "HF_Flag": raw[0], + "r0": raw[1], + "r1": raw[2], + "r2": raw[3], + "r3": raw[4], + "r12": raw[5], + "lr": raw[6], + "pc": raw[7], + "psr": raw[8], + "cfsr": raw[9], + "fault_addr": raw[10], + "calltrace_depth": raw[11], + "calltrace_pcs": raw[12:28] + } + if(hf["HF_Flag"] != 0xFF00FF00): + print("There was no hardfault in your Microcontroller, Kudos for you, I hope...") + return + print("================HARDFAULT DETECTED ===========") + print("Registers:") + + for r in ['r0','r1','r2','r3','r12','lr','pc','psr']: + print(f" {r.upper():<4}: 0x{hf[r]:08X}") + + print(f" CFSR: 0x{hf['cfsr']:08X}") + error = decode_cfsr(hf["cfsr"], hf["fault_addr"]) + print("\nSource Location:") + pc_loc = addr2line(hf["pc"]) + lr_loc = addr2line(hf["lr"]) + print(f" Linker Register : 0x{hf['lr']:08X} -> {lr_loc}") + + print(f" Program Counter : 0x{hf['pc']:08X} -> {pc_loc}") + print_code_context(pc_loc) + + analyze_call_stack(hf["calltrace_depth"],hf["calltrace_pcs"]) + + print("======================================================") + + + print("Note: In Release builds (-O2/-O3) the PC may not point exactly to the failing instruction.") + print(" During interrupts, bus faults, or stack corruption, the PC can be imprecise.") + print("\nIn case of Imprecise error is dificult to find due to is asynchronous fault") + print("The error has to be before PC. But not possible to know exactly when.") + print("Check this link to know more : https://interrupt.memfault.com/blog/cortex-m-hardfault-debug#fn:8") + + +if __name__ == '__main__': + out = read_flash() + if(out == None): + exit() + pos_memory_flash = out.rfind(HF_FLASH_ADDR_STRING) + print(out[0:pos_memory_flash]) + flash = out[pos_memory_flash:] + print(flash) + memory_string = "" + for line in flash.splitlines(): + if(line.find(':') == -1): + break + _,mem = line.split(":") + memory_string += mem + memory_string = memory_string.replace(" ","") + hard_fault_analysis(memory_string) From b0b0a4f13955e0d741dddd5d7c11597ef1333b9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20S=C3=A1ez?= Date: Sat, 21 Mar 2026 09:46:10 +0100 Subject: [PATCH 10/10] Moved hard fault analyzer and included on hyper CLI --- deps/ST-LIB | 2 +- hard_fault_analysis.py | 359 ----------------------------------- hyper | 289 ++++++++++++++++++++++------ tools/hard_fault_analysis.py | 310 ++++++++++++++++++++---------- 4 files changed, 447 insertions(+), 513 deletions(-) delete mode 100644 hard_fault_analysis.py diff --git a/deps/ST-LIB b/deps/ST-LIB index f8cf58cf..8ba761b2 160000 --- a/deps/ST-LIB +++ b/deps/ST-LIB @@ -1 +1 @@ -Subproject commit f8cf58cf824fa15da36ebc8b4fe5ecb0a6fd1af7 +Subproject commit 8ba761b2315739303d28a70334f057031c0faadb diff --git a/hard_fault_analysis.py b/hard_fault_analysis.py deleted file mode 100644 index 02e069af..00000000 --- a/hard_fault_analysis.py +++ /dev/null @@ -1,359 +0,0 @@ -import subprocess -import struct -import os -from dataclasses import dataclass -from typing import List, Optional - -HF_FLASH_ADDR = 0x080C0000 -HF_FLASH_ADDR_STRING = "0x080C000" -ELF_FILE = "out/build/latest.elf" - -CALL_TRACE_MAX_DEPTH = 16 - - -@dataclass -class HardFaultFrame: - flag: int - r0: int - r1: int - r2: int - r3: int - r12: int - lr: int - pc: int - psr: int - cfsr: int - fault_addr: int - calltrace_depth: int - calltrace_pcs: List[int] - - -def read_flash(): - try: - cmd = [ - "STM32_Programmer_CLI", - "-c", - "port=SWD", - "-r32", - hex(HF_FLASH_ADDR), - "112", - ] - out = subprocess.check_output(cmd, text=True) - return out - except subprocess.CalledProcessError as e: - print("Stop debugging to check fault analysis!!!") - print(f"Error: {e}") - return None - except FileNotFoundError: - print("STM32_Programmer_CLI not found. Make sure it is installed and in PATH.") - return None - - -def decode_cfsr_memory(cfsr, fault_addr): - memory_fault = cfsr & 0xFF - if memory_fault == 0: - return 0 - print("\nMemory Fault (MMFSR):") - if memory_fault & 0b10000000: - print(f" MMARVALID: Memory fault address valid -> 0x{fault_addr:08X}") - if fault_addr in (0xFFFFFFFF, 0x00000000): - print(" Fault address is invalid / unmapped memory") - else: - mem_info = addr2line(fault_addr) - print_code_context(mem_info) - if memory_fault & 0b00100000: - print(" MLSPERR : Floating Point Unit lazy state preservation error") - if memory_fault & 0b00010000: - print(" MSTKERR : Stack error on entry to exception") - if memory_fault & 0b00001000: - print(" MUNSTKERR : Stack error on return from exception") - if memory_fault & 0b00000010: - print(" DACCVIOL : Data access violation (NULL pointer or invalid access)") - if memory_fault & 0b00000001: - print(" IACCVIOL : Instruction access violation") - return 1 - - -# -------------------------- -# Decode Bus Fault (BFSR) -# -------------------------- -def decode_cfsr_bus(cfsr, fault_addr): - bus_fault = (cfsr & 0x0000FF00) >> 8 - if bus_fault == 0: - return 0 - print("\nBus Fault (BFSR):") - if bus_fault & 0b10000000: - if bus_fault & 0b00000001: - print(f" BFARVALID : Bus fault address valid -> 0x{fault_addr:08X}") - if bus_fault & 0b00000100: - print("\033[91m Bus fault address imprecise\033[0m (DON'T LOOK CALL STACK)") - - if bus_fault & 0b00100000: - print(" LSPERR : Floating Point Unit lazy state preservation error") - if bus_fault & 0b00010000: - print(" STKERR : Stack error on entry to exception") - if bus_fault & 0b00001000: - print(" UNSTKERR : Stack error on return from exception") - return 2 - - -# -------------------------- -# Decode Usage Fault (UFSR) -# -------------------------- -def decode_cfsr_usage(cfsr): - usage_fault = (cfsr & 0xFFFF0000) >> 16 - if usage_fault == 0: - return 0 - print("\nUsage Fault (UFSR):") - if usage_fault & 0x0200: - print(" DIVBYZERO : Division by zero") - if usage_fault & 0x0100: - print(" UNALIGNED : Unaligned memory access") - if usage_fault & 0x0008: - print(" NOCP : Accessed FPU when not present") - if usage_fault & 0x0004: - print(" INVPC : Invalid Program Counter(PC) load") - if usage_fault & 0x0002: - print(" INVSTATE : Invalid processor state") - if usage_fault & 0x0001: - print(" UNDEFINSTR : Undefined instruction") - return 4 - - -def decode_cfsr(cfsr, fault_addr): - error = 0 - error = decode_cfsr_memory(cfsr, fault_addr) + error - error = decode_cfsr_bus(cfsr, fault_addr) + error - error = decode_cfsr_usage(cfsr) + error - return error - - -def addr2line(addr): - cmd = ["arm-none-eabi-addr2line", "-e", ELF_FILE, "-f", "-C", hex(addr)] - try: - output = subprocess.check_output(cmd, text=True).strip() - return output - except Exception as e: - return f"addr2line failed: {e}" - - -def print_code_context(lines, context=2): - # addr2line - # line 0: function name - # line 1 : file : line - line_list = lines.splitlines() - if len(line_list) < 2: - print("Invalid addr2line output") - return - - file_line = line_list[1].strip() - split = file_line.rfind(":") - - if split == -1: - print("Invalid file:line format") - return - - file_path = file_line[:split] - - try: - line_no = int(file_line[split + 1 :]) - 1 - except ValueError: - print("Couldn't parse line number") - return - - if not os.path.exists(file_path): - print(f"Source file not found: {file_path}") - return - - with open(file_path, "r") as f: - file_lines = f.readlines() - - start = max(0, line_no - context) - end = min(len(file_lines), line_no + context + 1) - - print(f"\nSource snippet from {file_path}:") - - for i in range(start, end): - code = file_lines[i].rstrip() - if i == line_no: - print(f"\033[91m{i+1:>4}: {code}\033[0m") - else: - print(f"{i+1:>4}: {code}") - - -def parse_hardfault(memory_string: str) -> Optional[HardFaultFrame]: - try: - raw_bytes = bytes.fromhex(memory_string) - - expected_size = 28 * 4 - if len(raw_bytes) != expected_size: - print(f"Invalid dump size. Expected {expected_size}, got {len(raw_bytes)}") - return None - - raw = struct.unpack(">28I", raw_bytes) - - return HardFaultFrame( - flag=raw[0], - r0=raw[1], - r1=raw[2], - r2=raw[3], - r3=raw[4], - r12=raw[5], - lr=raw[6], - pc=raw[7], - psr=raw[8], - cfsr=raw[9], - fault_addr=raw[10], - calltrace_depth=raw[11], - calltrace_pcs=list(raw[12:28]), - ) - - except Exception as e: - print(f"Error parsing hardfault frame: {e}") - return None - - -def analyze_call_stack( - calltrace_depth: int, calltrace_pcs: List[int], context: int = 1 -): - print("\n==== Call Stack Trace ====") - - try: - if not isinstance(calltrace_depth, int): - print("Invalid calltrace_depth type") - return - - if calltrace_depth <= 0: - print("No call trace available.") - return - - if not isinstance(calltrace_pcs, list): - print("Invalid calltrace_pcs structure") - return - - depth = min(calltrace_depth, len(calltrace_pcs), CALL_TRACE_MAX_DEPTH) - - for i in range(depth): - pc = calltrace_pcs[i] - - # Validación básica de PC - if not isinstance(pc, int) or pc == 0: - continue - - # Limpiar bit Thumb - pc_base = pc & ~1 - - # Protección contra underflow - if pc_base < 4: - continue - - try: - snippet = addr2line(pc_base - 4) - except Exception as e: - print(f"addr2line failed for PC 0x{pc:08X}: {e}") - continue - - if not snippet or "??" in snippet: - continue - - try: - print_code_context(snippet, context) - except Exception as e: - print(f"Failed printing context for PC 0x{pc:08X}: {e}") - - except Exception as e: - print(f"Unexpected error in analyze_call_stack: {e}") - - print("===============================================") - - -def hard_fault_analysis(hf, context): - if hf.flag != 0xFF00FF00: - print( - "There was no hardfault in your Microcontroller, Kudos for you, I hope..." - ) - return - - print("================HARDFAULT DETECTED ===========") - print("Registers:") - - for r in ["r0", "r1", "r2", "r3", "r12", "lr", "pc", "psr"]: - value = getattr(hf, r) - print(f" {r.upper():<4}: 0x{value:08X}") - print("\n") - print("Register that contains the info about the HardFault") - print(f" CFSR: 0x{hf.cfsr:08X}") - # get the cause of the error - print("------HardFault Fail------") - decode_cfsr(hf.cfsr, hf.fault_addr) - print("---------------------------") - - pc_loc = addr2line(hf.pc) - lr_loc = addr2line(hf.lr) - - print("\n=======Source Location: ===========\n") - print(f" --> Linker Register : 0x{hf.lr:08X}\n -> {lr_loc}") - print_code_context(lr_loc, context) - print("\n") - print(f" -->Program Counter : 0x{hf.pc:08X}\n -> {pc_loc}") - print_code_context(pc_loc, context) - print("=============================") - analyze_call_stack(hf.calltrace_depth, hf.calltrace_pcs, context) - - print("======================================================") - - print( - "Note: In Release builds (-O2/-O3) the PC may not point exactly to the failing instruction." - ) - print( - " During interrupts, bus faults, or stack corruption, the PC can be imprecise." - ) - print( - "\nIn case of Imprecise error is dificult to find due to is asynchronous fault" - ) - print("The error has to be before PC. But not possible to know exactly when.") - print( - "Check this link to know more : https://interrupt.memfault.com/blog/cortex-m-hardfault-debug#fn:8" - ) - - -# get the memory with the address of HardFault -def extract_memory_dump(cli_output: str) -> Optional[str]: - pos = cli_output.rfind(HF_FLASH_ADDR_STRING) - if pos == -1: - print("The address was not found in CLI output") - return None - - flash = cli_output[pos:] - - memory_string = "" - for line in flash.splitlines(): - if ":" not in line: - break - _, mem = line.split(":", 1) - memory_string += mem.strip() - - return memory_string.replace(" ", "") - - -if __name__ == "__main__": - # First get all the information from the flash Memory - out = read_flash() - if out is None: - exit(1) - - memory_string = extract_memory_dump(out) - if not memory_string: - exit(1) - hf = parse_hardfault(memory_string) - if not hf: - exit(1) - print("Lines of context to watch (max 10):", end="") - try: - context = int(input()) - except ValueError: - context = 2 - if context < 0 or context > 10: - context = 10 - print("\n") - hard_fault_analysis(hf, context) diff --git a/hyper b/hyper index 1dbae30c..d2efedfc 100755 --- a/hyper +++ b/hyper @@ -24,6 +24,7 @@ PREFLASH_CHECK_SCRIPT = TOOLS_DIR / "preflash_check.py" INIT_SCRIPT = TOOLS_DIR / "init.sh" STLIB_BUILD_SCRIPT = STLIB_ROOT / "tools" / "build.py" STLIB_SIM_TESTS_SCRIPT = STLIB_ROOT / "tools" / "run_sim_tests.sh" +HARD_FAULT_ANALYSIS_SCRIPT = TOOLS_DIR / "hard_fault_analysis.py" LATEST_ELF = REPO_ROOT / "out" / "build" / "latest.elf" DEFAULT_PRESET = os.environ.get("HYPER_DEFAULT_PRESET", "nucleo-debug") @@ -31,12 +32,20 @@ DEFAULT_FLASH_METHOD = os.environ.get("HYPER_FLASH_METHOD", "auto") DEFAULT_UART_TOOL = os.environ.get("HYPER_UART_TOOL", "auto") DEFAULT_UART_PORT = os.environ.get("HYPER_UART_PORT") DEFAULT_UART_BAUD = int(os.environ.get("HYPER_UART_BAUD", "115200")) -DEFAULT_REQUIRED_CLT_VERSION = os.environ.get("HYPER_REQUIRED_STM32_CLT_VERSION", "1.21.0") -DEFAULT_CLT_ROOT = os.environ.get("HYPER_STM32CLT_ROOT") or os.environ.get("STM32_CLT_ROOT") +DEFAULT_REQUIRED_CLT_VERSION = os.environ.get( + "HYPER_REQUIRED_STM32_CLT_VERSION", "1.21.0" +) +DEFAULT_CLT_ROOT = os.environ.get("HYPER_STM32CLT_ROOT") or os.environ.get( + "STM32_CLT_ROOT" +) DEFAULT_CLT_INSTALLER = os.environ.get("HYPER_STM32CLT_INSTALLER") DEFAULT_CLT_DOWNLOAD_URL = os.environ.get("HYPER_STM32CLT_DOWNLOAD_URL") DEFAULT_UV_VERSION = os.environ.get("HYPER_UV_VERSION") -COLOR_ENABLED = sys.stdout.isatty() and os.environ.get("NO_COLOR") is None and os.environ.get("TERM") != "dumb" +COLOR_ENABLED = ( + sys.stdout.isatty() + and os.environ.get("NO_COLOR") is None + and os.environ.get("TERM") != "dumb" +) CLT_PRODUCT_PAGE = "https://www.st.com/en/development-tools/stm32cubeclt.html" CLT_RELEASE_NOTE = "https://www.st.com/resource/en/release_note/rn0132-stm32cube-commandline-toolset-release-v1210-stmicroelectronics.pdf" UV_INSTALL_PAGE = "https://docs.astral.sh/uv/getting-started/installation/" @@ -71,6 +80,7 @@ HELP_EXAMPLES = """Examples: hyper doctor hyper build adc --preset nucleo-debug --test 1 hyper build main --preset nucleo-debug + hyper hardfault-analysis hyper run adc --preset nucleo-debug --test 1 --uart hyper uart --list-ports """ @@ -92,7 +102,13 @@ class HyperError(RuntimeError): class ToolStatus: - def __init__(self, *, path: str | None, version_line: str | None = None, clt_version: str | None = None) -> None: + def __init__( + self, + *, + path: str | None, + version_line: str | None = None, + clt_version: str | None = None, + ) -> None: self.path = path self.version_line = version_line self.clt_version = clt_version @@ -130,7 +146,9 @@ def print_detail(label: str, value: str) -> None: print(f" {style(f'{label}:', '2')} {value}") -def print_status_item(label: str, status: str, value: str, *, detail: str | None = None) -> None: +def print_status_item( + label: str, status: str, value: str, *, detail: str | None = None +) -> None: print(f" {status_tag(status):<12} {style(label, '1'):<20} {value}") if detail: print_detail("detail", detail) @@ -190,8 +208,14 @@ def command_path(name: str) -> str | None: def known_uv_paths() -> list[Path]: candidates = [ - Path.home() / ".local" / "bin" / ("uv.exe" if detect_host_os() == "windows" else "uv"), - Path.home() / ".cargo" / "bin" / ("uv.exe" if detect_host_os() == "windows" else "uv"), + Path.home() + / ".local" + / "bin" + / ("uv.exe" if detect_host_os() == "windows" else "uv"), + Path.home() + / ".cargo" + / "bin" + / ("uv.exe" if detect_host_os() == "windows" else "uv"), ] unique: list[Path] = [] seen: set[Path] = set() @@ -243,7 +267,9 @@ def strip_ansi(text: str) -> str: return re.sub(r"\x1b\[[0-9;]*[A-Za-z]", "", text) -def read_command_first_line(cmd: list[str], *, preferred_pattern: str | None = None) -> str | None: +def read_command_first_line( + cmd: list[str], *, preferred_pattern: str | None = None +) -> str | None: try: result = subprocess.run( cmd, @@ -283,12 +309,23 @@ def parse_clt_version_from_path(path: str | None) -> str | None: return None -def inspect_tool(name: str, *, version_args: list[str] | None = None, version_pattern: str | None = None) -> ToolStatus: +def inspect_tool( + name: str, + *, + version_args: list[str] | None = None, + version_pattern: str | None = None, +) -> ToolStatus: path = command_path(name) version_line = None if path and version_args: - version_line = read_command_first_line([path, *version_args], preferred_pattern=version_pattern) - return ToolStatus(path=path, version_line=version_line, clt_version=parse_clt_version_from_path(path)) + version_line = read_command_first_line( + [path, *version_args], preferred_pattern=version_pattern + ) + return ToolStatus( + path=path, + version_line=version_line, + clt_version=parse_clt_version_from_path(path), + ) def detect_host_os() -> str: @@ -360,18 +397,26 @@ def infer_clt_root_from_tool(path: str | None) -> Path | None: return None -def clt_tool_status(tool_relative_path: str, *, version_args: list[str], version_pattern: str) -> ToolStatus: +def clt_tool_status( + tool_relative_path: str, *, version_args: list[str], version_pattern: str +) -> ToolStatus: for root in clt_root_candidates(DEFAULT_REQUIRED_CLT_VERSION): tool_path = root / tool_relative_path if tool_path.is_file(): - version_line = read_command_first_line([str(tool_path), *version_args], preferred_pattern=version_pattern) + version_line = read_command_first_line( + [str(tool_path), *version_args], preferred_pattern=version_pattern + ) return ToolStatus( path=str(tool_path), version_line=version_line, clt_version=parse_clt_version_from_path(str(root)), ) - path_status = inspect_tool(Path(tool_relative_path).name, version_args=version_args, version_pattern=version_pattern) + path_status = inspect_tool( + Path(tool_relative_path).name, + version_args=version_args, + version_pattern=version_pattern, + ) inferred_root = infer_clt_root_from_tool(path_status.path) if inferred_root is not None: path_status.clt_version = parse_clt_version_from_path(str(inferred_root)) @@ -449,8 +494,13 @@ def prepare_clt_installers(installer: Path) -> tuple[list[Path], Path | None]: if host_os == "macos": if installer.suffix.lower() == ".pkg": return [installer], None - if not installer.name.endswith((".tar.gz", ".tgz")) and installer.suffix.lower() != ".zip": - raise HyperError(f"Unsupported macOS STM32CubeCLT installer: {installer.name}") + if ( + not installer.name.endswith((".tar.gz", ".tgz")) + and installer.suffix.lower() != ".zip" + ): + raise HyperError( + f"Unsupported macOS STM32CubeCLT installer: {installer.name}" + ) tmpdir = Path(tempfile.mkdtemp(prefix="hyper-clt-macos-")) if installer.suffix.lower() == ".zip": with zipfile.ZipFile(installer) as archive: @@ -463,9 +513,14 @@ def prepare_clt_installers(installer: Path) -> tuple[list[Path], Path | None]: if path.is_file() and path.name.endswith((".tar.gz", ".tgz")) ] if not nested_archives: - raise HyperError(f"No .pkg or .tar.gz files found in {installer.name}") + raise HyperError( + f"No .pkg or .tar.gz files found in {installer.name}" + ) nested_archives.sort( - key=lambda path: ("stm32cubeclt" not in path.name.lower(), path.name.lower()) + key=lambda path: ( + "stm32cubeclt" not in path.name.lower(), + path.name.lower(), + ) ) for nested_archive in nested_archives: with tarfile.open(nested_archive, "r:*") as archive: @@ -473,7 +528,9 @@ def prepare_clt_installers(installer: Path) -> tuple[list[Path], Path | None]: pkgs = [path for path in tmpdir.rglob("*.pkg") if path.is_file()] if not pkgs: raise HyperError(f"No .pkg files found in {installer.name}") - pkgs.sort(key=lambda path: ("st-link" in path.name.lower(), path.name.lower())) + pkgs.sort( + key=lambda path: ("st-link" in path.name.lower(), path.name.lower()) + ) return pkgs, tmpdir with tarfile.open(installer, "r:*") as archive: @@ -488,14 +545,21 @@ def prepare_clt_installers(installer: Path) -> tuple[list[Path], Path | None]: if installer.suffix.lower() == ".exe": return [installer], None if installer.suffix.lower() != ".zip": - raise HyperError(f"Unsupported Windows STM32CubeCLT installer: {installer.name}") + raise HyperError( + f"Unsupported Windows STM32CubeCLT installer: {installer.name}" + ) tmpdir = Path(tempfile.mkdtemp(prefix="hyper-clt-windows-")) with zipfile.ZipFile(installer) as archive: archive.extractall(tmpdir) exes = [path for path in tmpdir.rglob("*.exe") if path.is_file()] if not exes: raise HyperError(f"No .exe files found in {installer.name}") - exes.sort(key=lambda path: ("stm32cubeclt" not in path.name.lower(), path.name.lower())) + exes.sort( + key=lambda path: ( + "stm32cubeclt" not in path.name.lower(), + path.name.lower(), + ) + ) return exes, tmpdir if installer.suffix != ".sh": @@ -517,7 +581,9 @@ def run_clt_installer(installer: Path) -> None: def ensure_required_clt() -> None: arm_gcc, programmer, clt_version = inspect_clt() if clt_version == DEFAULT_REQUIRED_CLT_VERSION: - print_action("STM32CubeCLT", version=DEFAULT_REQUIRED_CLT_VERSION, status="ready") + print_action( + "STM32CubeCLT", version=DEFAULT_REQUIRED_CLT_VERSION, status="ready" + ) if arm_gcc.path: print_detail("arm gcc", arm_gcc.path) if programmer.path: @@ -531,7 +597,11 @@ def ensure_required_clt() -> None: detected=clt_version or "missing", host=detect_host_os(), ) - installer = Path(DEFAULT_CLT_INSTALLER).expanduser().resolve() if DEFAULT_CLT_INSTALLER else None + installer = ( + Path(DEFAULT_CLT_INSTALLER).expanduser().resolve() + if DEFAULT_CLT_INSTALLER + else None + ) if installer is None: candidates = installer_candidates(DEFAULT_REQUIRED_CLT_VERSION) installer = candidates[0] if candidates else None @@ -610,7 +680,9 @@ def ensure_uv() -> str: uv_path = uv_executable() if not uv_path: - raise HyperError(f"uv installation completed but the binary is still not visible. See {UV_INSTALL_PAGE}") + raise HyperError( + f"uv installation completed but the binary is still not visible. See {UV_INSTALL_PAGE}" + ) version_line = read_command_first_line([uv_path, "--version"]) print_detail("binary", uv_path) @@ -629,7 +701,17 @@ def virtual_python_path() -> Path: def setup_python_env_with_uv(uv_path: str) -> None: print_action("Python Env", tool="uv", venv=str(REPO_ROOT / "virtual")) run_command([uv_path, "venv", REPO_ROOT / "virtual"]) - run_command([uv_path, "pip", "install", "--python", virtual_python_path(), "-r", REPO_ROOT / "requirements.txt"]) + run_command( + [ + uv_path, + "pip", + "install", + "--python", + virtual_python_path(), + "-r", + REPO_ROOT / "requirements.txt", + ] + ) print_note("python environment ready", status="ok") @@ -650,7 +732,9 @@ def resolve_flash_method(requested: str) -> str: return "stm32prog" if command_path("openocd"): return "openocd" - raise HyperError("No supported flash tool found. Install STM32_Programmer_CLI or openocd.") + raise HyperError( + "No supported flash tool found. Install STM32_Programmer_CLI or openocd." + ) def serial_port_rank(port: str) -> tuple[int, str]: @@ -689,7 +773,9 @@ def choose_serial_port(requested: str | None) -> str: ports = find_serial_ports() if not ports: - raise HyperError("No USB serial port detected. Connect the board or pass --port explicitly.") + raise HyperError( + "No USB serial port detected. Connect the board or pass --port explicitly." + ) if len(ports) == 1: return ports[0] @@ -720,7 +806,9 @@ def resolve_uart_tool(requested: str) -> str: return "tio" if command_path("cu"): return "cu" - raise HyperError("No supported UART tool found. Install 'tio' or ensure 'cu' is available.") + raise HyperError( + "No supported UART tool found. Install 'tio' or ensure 'cu' is available." + ) def run_build_example( @@ -907,7 +995,9 @@ def handle_run(args: argparse.Namespace) -> int: extra_cxx_flags=args.extra_cxx_flags, jobs=args.jobs, ) - flash_elf(args.elf.resolve(), args.flash_method, not args.no_verify, args.skip_preflight) + flash_elf( + args.elf.resolve(), args.flash_method, not args.no_verify, args.skip_preflight + ) if args.uart: return open_uart(args.port, args.baud, args.uart_tool) print_note("run completed", status="ok") @@ -935,7 +1025,9 @@ def handle_doctor(_: argparse.Namespace) -> int: if not clt_version: issues.append("STM32CubeCLT version could not be inferred from tool paths") elif clt_version != DEFAULT_REQUIRED_CLT_VERSION: - issues.append(f"STM32CubeCLT {clt_version} detected, expected {DEFAULT_REQUIRED_CLT_VERSION}") + issues.append( + f"STM32CubeCLT {clt_version} detected, expected {DEFAULT_REQUIRED_CLT_VERSION}" + ) if not uv_path: issues.append("uv is not installed") @@ -952,10 +1044,18 @@ def handle_doctor(_: argparse.Namespace) -> int: print_detail("latest elf", "present" if LATEST_ELF.exists() else "missing") section_title("Toolchain") - print_status_item("arm-none-eabi-gcc", "ok" if arm_gcc.path else "missing", arm_gcc.path or "not found") + print_status_item( + "arm-none-eabi-gcc", + "ok" if arm_gcc.path else "missing", + arm_gcc.path or "not found", + ) if arm_gcc.version_line: print_detail("version", arm_gcc.version_line) - print_status_item("STM32_Programmer", "ok" if programmer.path else "missing", programmer.path or "not found") + print_status_item( + "STM32_Programmer", + "ok" if programmer.path else "missing", + programmer.path or "not found", + ) if programmer.version_line: print_detail("version", programmer.version_line) print_status_item( @@ -995,12 +1095,21 @@ def handle_doctor(_: argparse.Namespace) -> int: print_detail("port", port) section_title("Repo Helpers") - print_status_item("stlib build.py", "ok" if STLIB_BUILD_SCRIPT.is_file() else "missing", str(STLIB_BUILD_SCRIPT)) + print_status_item( + "stlib build.py", + "ok" if STLIB_BUILD_SCRIPT.is_file() else "missing", + str(STLIB_BUILD_SCRIPT), + ) print_status_item( "stlib sim tests", "ok" if STLIB_SIM_TESTS_SCRIPT.is_file() else "missing", str(STLIB_SIM_TESTS_SCRIPT), ) + print_status_item( + "hard fault tool", + "ok" if HARD_FAULT_ANALYSIS_SCRIPT.is_file() else "missing", + str(HARD_FAULT_ANALYSIS_SCRIPT), + ) section_title("Summary") if issues: @@ -1012,7 +1121,9 @@ def handle_doctor(_: argparse.Namespace) -> int: def handle_stlib_build(args: argparse.Namespace) -> int: ensure_file(STLIB_BUILD_SCRIPT, "ST-LIB build helper") - print_action("ST-LIB Build", preset=args.preset, tests="yes" if args.run_tests else "no") + print_action( + "ST-LIB Build", preset=args.preset, tests="yes" if args.run_tests else "no" + ) cmd: list[object] = [sys.executable, STLIB_BUILD_SCRIPT, "--preset", args.preset] if args.run_tests: cmd.append("--run-tests") @@ -1029,14 +1140,32 @@ def handle_stlib_sim_tests(_: argparse.Namespace) -> int: return 0 +def handle_hardfault_analysis(_: argparse.Namespace) -> int: + ensure_file(HARD_FAULT_ANALYSIS_SCRIPT, "hard fault analysis helper") + print_action( + "Hard Fault Analysis", + script=str(HARD_FAULT_ANALYSIS_SCRIPT), + elf=str(LATEST_ELF), + ) + run_command([sys.executable, HARD_FAULT_ANALYSIS_SCRIPT], cwd=REPO_ROOT) + print_note("hard fault analysis completed", status="ok") + return 0 + + def add_example_build_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument("example", help="Target name, e.g. adc or main") group = parser.add_mutually_exclusive_group() group.add_argument("--test", help="Test selector, e.g. 0, 1 or TEST_1") - group.add_argument("--no-test", action="store_true", help="Do not inject any TEST_* macro") - parser.add_argument("--preset", default=DEFAULT_PRESET, help="CMake preset to build") + group.add_argument( + "--no-test", action="store_true", help="Do not inject any TEST_* macro" + ) + parser.add_argument( + "--preset", default=DEFAULT_PRESET, help="CMake preset to build" + ) parser.add_argument("--board-name", help="Override BOARD_NAME for code generation") - parser.add_argument("--extra-cxx-flags", help="Extra flags appended after the injected defines") + parser.add_argument( + "--extra-cxx-flags", help="Extra flags appended after the injected defines" + ) parser.add_argument("-j", "--jobs", type=int, help="Set CMAKE_BUILD_PARALLEL_LEVEL") @@ -1052,13 +1181,21 @@ def build_parser() -> argparse.ArgumentParser: init_parser = subparsers.add_parser("init", help="Run tools/init.sh") init_parser.set_defaults(func=handle_init) - examples_parser = subparsers.add_parser("examples", help="Inspect available examples") - examples_subparsers = examples_parser.add_subparsers(dest="examples_command", required=True) + examples_parser = subparsers.add_parser( + "examples", help="Inspect available examples" + ) + examples_subparsers = examples_parser.add_subparsers( + dest="examples_command", required=True + ) - examples_list_parser = examples_subparsers.add_parser("list", help="List EXAMPLE_* selectors") + examples_list_parser = examples_subparsers.add_parser( + "list", help="List EXAMPLE_* selectors" + ) examples_list_parser.set_defaults(func=handle_examples_list) - examples_tests_parser = examples_subparsers.add_parser("tests", help="List TEST_* selectors for one example") + examples_tests_parser = examples_subparsers.add_parser( + "tests", help="List TEST_* selectors for one example" + ) examples_tests_parser.add_argument("example", help="Example name, e.g. adc") examples_tests_parser.set_defaults(func=handle_examples_tests) @@ -1066,15 +1203,23 @@ def build_parser() -> argparse.ArgumentParser: add_example_build_arguments(build_parser_cmd) build_parser_cmd.set_defaults(func=handle_build) - flash_parser = subparsers.add_parser("flash", help="Flash an ELF image onto the board") - flash_parser.add_argument("--elf", type=Path, default=LATEST_ELF, help="ELF image to flash") + flash_parser = subparsers.add_parser( + "flash", help="Flash an ELF image onto the board" + ) + flash_parser.add_argument( + "--elf", type=Path, default=LATEST_ELF, help="ELF image to flash" + ) flash_parser.add_argument( "--method", choices=("auto", "stm32prog", "openocd"), default=DEFAULT_FLASH_METHOD, help="Flash tool selection", ) - flash_parser.add_argument("--no-verify", action="store_true", help="Skip flash verification when supported") + flash_parser.add_argument( + "--no-verify", + action="store_true", + help="Skip flash verification when supported", + ) flash_parser.add_argument( "--skip-preflight", action="store_true", @@ -1082,24 +1227,36 @@ def build_parser() -> argparse.ArgumentParser: ) flash_parser.set_defaults(func=handle_flash) - run_parser = subparsers.add_parser("run", help="Build then flash one firmware target") + run_parser = subparsers.add_parser( + "run", help="Build then flash one firmware target" + ) add_example_build_arguments(run_parser) - run_parser.add_argument("--elf", type=Path, default=LATEST_ELF, help="ELF image to flash after build") + run_parser.add_argument( + "--elf", type=Path, default=LATEST_ELF, help="ELF image to flash after build" + ) run_parser.add_argument( "--flash-method", choices=("auto", "stm32prog", "openocd"), default=DEFAULT_FLASH_METHOD, help="Flash tool selection", ) - run_parser.add_argument("--no-verify", action="store_true", help="Skip flash verification when supported") + run_parser.add_argument( + "--no-verify", + action="store_true", + help="Skip flash verification when supported", + ) run_parser.add_argument( "--skip-preflight", action="store_true", help="Skip tools/preflash_check.py before flashing", ) - run_parser.add_argument("--uart", action="store_true", help="Open UART after flashing") + run_parser.add_argument( + "--uart", action="store_true", help="Open UART after flashing" + ) run_parser.add_argument("--port", help="Serial port path or 'auto'") - run_parser.add_argument("--baud", type=int, default=DEFAULT_UART_BAUD, help="UART baud rate") + run_parser.add_argument( + "--baud", type=int, default=DEFAULT_UART_BAUD, help="UART baud rate" + ) run_parser.add_argument( "--uart-tool", choices=("auto", "tio", "cu"), @@ -1110,28 +1267,48 @@ def build_parser() -> argparse.ArgumentParser: uart_parser = subparsers.add_parser("uart", help="Open the board UART") uart_parser.add_argument("--port", help="Serial port path or 'auto'") - uart_parser.add_argument("--baud", type=int, default=DEFAULT_UART_BAUD, help="UART baud rate") + uart_parser.add_argument( + "--baud", type=int, default=DEFAULT_UART_BAUD, help="UART baud rate" + ) uart_parser.add_argument( "--tool", choices=("auto", "tio", "cu"), default=DEFAULT_UART_TOOL, help="Serial terminal program", ) - uart_parser.add_argument("--list-ports", action="store_true", help="List candidate serial ports and exit") + uart_parser.add_argument( + "--list-ports", action="store_true", help="List candidate serial ports and exit" + ) uart_parser.set_defaults(func=handle_uart) - doctor_parser = subparsers.add_parser("doctor", help="Show tool and serial-port availability") + doctor_parser = subparsers.add_parser( + "doctor", help="Show tool and serial-port availability" + ) doctor_parser.set_defaults(func=handle_doctor) + hardfault_parser = subparsers.add_parser( + "hardfault-analysis", + help="Analyze the latest stored hard fault dump", + ) + hardfault_parser.set_defaults(func=handle_hardfault_analysis) + stlib_parser = subparsers.add_parser("stlib", help="Run ST-LIB helper flows") stlib_subparsers = stlib_parser.add_subparsers(dest="stlib_command", required=True) - stlib_build_parser = stlib_subparsers.add_parser("build", help="Run deps/ST-LIB/tools/build.py") - stlib_build_parser.add_argument("--preset", default="simulator", help="ST-LIB CMake preset") - stlib_build_parser.add_argument("--run-tests", action="store_true", help="Run tests after build") + stlib_build_parser = stlib_subparsers.add_parser( + "build", help="Run deps/ST-LIB/tools/build.py" + ) + stlib_build_parser.add_argument( + "--preset", default="simulator", help="ST-LIB CMake preset" + ) + stlib_build_parser.add_argument( + "--run-tests", action="store_true", help="Run tests after build" + ) stlib_build_parser.set_defaults(func=handle_stlib_build) - stlib_tests_parser = stlib_subparsers.add_parser("sim-tests", help="Run ST-LIB simulator tests") + stlib_tests_parser = stlib_subparsers.add_parser( + "sim-tests", help="Run ST-LIB simulator tests" + ) stlib_tests_parser.set_defaults(func=handle_stlib_sim_tests) return parser diff --git a/tools/hard_fault_analysis.py b/tools/hard_fault_analysis.py index 233eb2c0..02e069af 100644 --- a/tools/hard_fault_analysis.py +++ b/tools/hard_fault_analysis.py @@ -1,18 +1,42 @@ import subprocess import struct -import re import os +from dataclasses import dataclass +from typing import List, Optional + HF_FLASH_ADDR = 0x080C0000 HF_FLASH_ADDR_STRING = "0x080C000" ELF_FILE = "out/build/latest.elf" CALL_TRACE_MAX_DEPTH = 16 + + +@dataclass +class HardFaultFrame: + flag: int + r0: int + r1: int + r2: int + r3: int + r12: int + lr: int + pc: int + psr: int + cfsr: int + fault_addr: int + calltrace_depth: int + calltrace_pcs: List[int] + + def read_flash(): try: cmd = [ "STM32_Programmer_CLI", - "-c", "port=SWD", - "-r32", hex(HF_FLASH_ADDR), "112" + "-c", + "port=SWD", + "-r32", + hex(HF_FLASH_ADDR), + "112", ] out = subprocess.check_output(cmd, text=True) return out @@ -23,6 +47,8 @@ def read_flash(): except FileNotFoundError: print("STM32_Programmer_CLI not found. Make sure it is installed and in PATH.") return None + + def decode_cfsr_memory(cfsr, fault_addr): memory_fault = cfsr & 0xFF if memory_fault == 0: @@ -47,6 +73,7 @@ def decode_cfsr_memory(cfsr, fault_addr): print(" IACCVIOL : Instruction access violation") return 1 + # -------------------------- # Decode Bus Fault (BFSR) # -------------------------- @@ -56,10 +83,10 @@ def decode_cfsr_bus(cfsr, fault_addr): return 0 print("\nBus Fault (BFSR):") if bus_fault & 0b10000000: - if(bus_fault & 0b00000001): + if bus_fault & 0b00000001: print(f" BFARVALID : Bus fault address valid -> 0x{fault_addr:08X}") if bus_fault & 0b00000100: - print(f"\033[91m Bus fault address imprecise\033[0m (DON'T LOOK CALL STACK)") + print("\033[91m Bus fault address imprecise\033[0m (DON'T LOOK CALL STACK)") if bus_fault & 0b00100000: print(" LSPERR : Floating Point Unit lazy state preservation error") @@ -69,6 +96,7 @@ def decode_cfsr_bus(cfsr, fault_addr): print(" UNSTKERR : Stack error on return from exception") return 2 + # -------------------------- # Decode Usage Fault (UFSR) # -------------------------- @@ -91,6 +119,7 @@ def decode_cfsr_usage(cfsr): print(" UNDEFINSTR : Undefined instruction") return 4 + def decode_cfsr(cfsr, fault_addr): error = 0 error = decode_cfsr_memory(cfsr, fault_addr) + error @@ -107,58 +136,33 @@ def addr2line(addr): except Exception as e: return f"addr2line failed: {e}" -def analyze_call_stack(calltrace_depth, calltrace_pcs, context=2): - """ - Muestra el call stack, omitiendo frames sin fuente y mostrando snippet de código. - """ - print("\n==== Call Stack Trace ====") - if calltrace_depth == 0: - print("No call trace available.") - return - -def analyze_call_stack(calltrace_depth, calltrace_pcs, context=0): - """ - Muestra el call stack, mostrando snippet de código de la línea exacta - sin intentar sumar líneas arriba/abajo (context=0 por defecto). - Omite frames sin fuente. - """ - print("\n==== Call Stack Trace ====") - if calltrace_depth == 0: - print("No call trace available.") - return - - for pc in calltrace_pcs[:calltrace_depth]: - pc_base = pc & ~1 - snippet = addr2line(pc_base- 4).strip() - if not snippet or snippet.startswith("??:?"): - continue # no hay fuente, saltar - print_code_context(snippet,1) - - print("======================================================") - - - def print_code_context(lines, context=2): - """ - lines: exit of addr2line (función + file:line) - context: how many lines up/down show - """ + # addr2line + # line 0: function name + # line 1 : file : line line_list = lines.splitlines() if len(line_list) < 2: print("Invalid addr2line output") return file_line = line_list[1].strip() - split = file_line.rfind(':') + split = file_line.rfind(":") + + if split == -1: + print("Invalid file:line format") + return + file_path = file_line[:split] + try: - line_no = int(file_line[split+1:]) - 1 # índice base 0 + line_no = int(file_line[split + 1 :]) - 1 except ValueError: - print("\33[91m Couldn't find exact line\33[0m") + print("Couldn't parse line number") return - if not os.path.exists(file_path): - print("Source file not found") + + if not os.path.exists(file_path): + print(f"Source file not found: {file_path}") return with open(file_path, "r") as f: @@ -168,76 +172,188 @@ def print_code_context(lines, context=2): end = min(len(file_lines), line_no + context + 1) print(f"\nSource snippet from {file_path}:") + for i in range(start, end): code = file_lines[i].rstrip() - # Si es la línea del error, la ponemos en rojo if i == line_no: - print(f"\033[91m{i+1:>4}: {code}\033[0m") # rojo + print(f"\033[91m{i+1:>4}: {code}\033[0m") else: print(f"{i+1:>4}: {code}") -def hard_fault_analysis(memory_string): - raw = bytes.fromhex(memory_string) - raw = struct.unpack(">28I",raw) - hf = { - "HF_Flag": raw[0], - "r0": raw[1], - "r1": raw[2], - "r2": raw[3], - "r3": raw[4], - "r12": raw[5], - "lr": raw[6], - "pc": raw[7], - "psr": raw[8], - "cfsr": raw[9], - "fault_addr": raw[10], - "calltrace_depth": raw[11], - "calltrace_pcs": raw[12:28] - } - if(hf["HF_Flag"] != 0xFF00FF00): - print("There was no hardfault in your Microcontroller, Kudos for you, I hope...") + +def parse_hardfault(memory_string: str) -> Optional[HardFaultFrame]: + try: + raw_bytes = bytes.fromhex(memory_string) + + expected_size = 28 * 4 + if len(raw_bytes) != expected_size: + print(f"Invalid dump size. Expected {expected_size}, got {len(raw_bytes)}") + return None + + raw = struct.unpack(">28I", raw_bytes) + + return HardFaultFrame( + flag=raw[0], + r0=raw[1], + r1=raw[2], + r2=raw[3], + r3=raw[4], + r12=raw[5], + lr=raw[6], + pc=raw[7], + psr=raw[8], + cfsr=raw[9], + fault_addr=raw[10], + calltrace_depth=raw[11], + calltrace_pcs=list(raw[12:28]), + ) + + except Exception as e: + print(f"Error parsing hardfault frame: {e}") + return None + + +def analyze_call_stack( + calltrace_depth: int, calltrace_pcs: List[int], context: int = 1 +): + print("\n==== Call Stack Trace ====") + + try: + if not isinstance(calltrace_depth, int): + print("Invalid calltrace_depth type") + return + + if calltrace_depth <= 0: + print("No call trace available.") + return + + if not isinstance(calltrace_pcs, list): + print("Invalid calltrace_pcs structure") + return + + depth = min(calltrace_depth, len(calltrace_pcs), CALL_TRACE_MAX_DEPTH) + + for i in range(depth): + pc = calltrace_pcs[i] + + # Validación básica de PC + if not isinstance(pc, int) or pc == 0: + continue + + # Limpiar bit Thumb + pc_base = pc & ~1 + + # Protección contra underflow + if pc_base < 4: + continue + + try: + snippet = addr2line(pc_base - 4) + except Exception as e: + print(f"addr2line failed for PC 0x{pc:08X}: {e}") + continue + + if not snippet or "??" in snippet: + continue + + try: + print_code_context(snippet, context) + except Exception as e: + print(f"Failed printing context for PC 0x{pc:08X}: {e}") + + except Exception as e: + print(f"Unexpected error in analyze_call_stack: {e}") + + print("===============================================") + + +def hard_fault_analysis(hf, context): + if hf.flag != 0xFF00FF00: + print( + "There was no hardfault in your Microcontroller, Kudos for you, I hope..." + ) return + print("================HARDFAULT DETECTED ===========") print("Registers:") - for r in ['r0','r1','r2','r3','r12','lr','pc','psr']: - print(f" {r.upper():<4}: 0x{hf[r]:08X}") - - print(f" CFSR: 0x{hf['cfsr']:08X}") - error = decode_cfsr(hf["cfsr"], hf["fault_addr"]) - print("\nSource Location:") - pc_loc = addr2line(hf["pc"]) - lr_loc = addr2line(hf["lr"]) - print(f" Linker Register : 0x{hf['lr']:08X} -> {lr_loc}") + for r in ["r0", "r1", "r2", "r3", "r12", "lr", "pc", "psr"]: + value = getattr(hf, r) + print(f" {r.upper():<4}: 0x{value:08X}") + print("\n") + print("Register that contains the info about the HardFault") + print(f" CFSR: 0x{hf.cfsr:08X}") + # get the cause of the error + print("------HardFault Fail------") + decode_cfsr(hf.cfsr, hf.fault_addr) + print("---------------------------") - print(f" Program Counter : 0x{hf['pc']:08X} -> {pc_loc}") - print_code_context(pc_loc) + pc_loc = addr2line(hf.pc) + lr_loc = addr2line(hf.lr) - analyze_call_stack(hf["calltrace_depth"],hf["calltrace_pcs"]) + print("\n=======Source Location: ===========\n") + print(f" --> Linker Register : 0x{hf.lr:08X}\n -> {lr_loc}") + print_code_context(lr_loc, context) + print("\n") + print(f" -->Program Counter : 0x{hf.pc:08X}\n -> {pc_loc}") + print_code_context(pc_loc, context) + print("=============================") + analyze_call_stack(hf.calltrace_depth, hf.calltrace_pcs, context) print("======================================================") - - print("Note: In Release builds (-O2/-O3) the PC may not point exactly to the failing instruction.") - print(" During interrupts, bus faults, or stack corruption, the PC can be imprecise.") - print("\nIn case of Imprecise error is dificult to find due to is asynchronous fault") + print( + "Note: In Release builds (-O2/-O3) the PC may not point exactly to the failing instruction." + ) + print( + " During interrupts, bus faults, or stack corruption, the PC can be imprecise." + ) + print( + "\nIn case of Imprecise error is dificult to find due to is asynchronous fault" + ) print("The error has to be before PC. But not possible to know exactly when.") - print("Check this link to know more : https://interrupt.memfault.com/blog/cortex-m-hardfault-debug#fn:8") + print( + "Check this link to know more : https://interrupt.memfault.com/blog/cortex-m-hardfault-debug#fn:8" + ) -if __name__ == '__main__': - out = read_flash() - if(out == None): - exit() - pos_memory_flash = out.rfind(HF_FLASH_ADDR_STRING) - print(out[0:pos_memory_flash]) - flash = out[pos_memory_flash:] - print(flash) +# get the memory with the address of HardFault +def extract_memory_dump(cli_output: str) -> Optional[str]: + pos = cli_output.rfind(HF_FLASH_ADDR_STRING) + if pos == -1: + print("The address was not found in CLI output") + return None + + flash = cli_output[pos:] + memory_string = "" for line in flash.splitlines(): - if(line.find(':') == -1): + if ":" not in line: break - _,mem = line.split(":") - memory_string += mem - memory_string = memory_string.replace(" ","") - hard_fault_analysis(memory_string) + _, mem = line.split(":", 1) + memory_string += mem.strip() + + return memory_string.replace(" ", "") + + +if __name__ == "__main__": + # First get all the information from the flash Memory + out = read_flash() + if out is None: + exit(1) + + memory_string = extract_memory_dump(out) + if not memory_string: + exit(1) + hf = parse_hardfault(memory_string) + if not hf: + exit(1) + print("Lines of context to watch (max 10):", end="") + try: + context = int(input()) + except ValueError: + context = 2 + if context < 0 or context > 10: + context = 10 + print("\n") + hard_fault_analysis(hf, context)