diff --git a/Core/Src/Examples/ExampleADC.cpp b/Core/Src/Examples/ExampleADC.cpp index defafa2f..eedf5608 100644 --- a/Core/Src/Examples/ExampleADC.cpp +++ b/Core/Src/Examples/ExampleADC.cpp @@ -1,15 +1,52 @@ #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; +}; + +constexpr ExampleInput kSingleChannelInput{ST_LIB::PA0, "PA0"}; +constexpr ExampleInput kDualChannelInput0{ST_LIB::PA0, "PA0"}; +constexpr ExampleInput kDualChannelInput1{ST_LIB::PC0, "PC0"}; + +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."; + +void start_terminal() { +#ifdef HAL_UART_MODULE_ENABLED + if (!UART::set_up_printf(UART::uart3)) { + ErrorHandler("Unable to set up UART printf for ADC example"); + } + UART::start(); +#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", kTerminalHint); + printf("%s\n\r", wiring_hint); + printf("Columns: %s\n\r\n\r", columns_hint); +} + +} // namespace + #ifdef TEST_0 constinit float adc_value = 0.0f; +constexpr auto adc_input = kSingleChannelInput; constexpr auto adc = ADCDomain::ADC( - ST_LIB::PA0, + adc_input.pin, adc_value, ADCDomain::Resolution::BITS_12, ADCDomain::SampleTime::CYCLES_8_5 @@ -18,16 +55,90 @@ constexpr auto adc = ADCDomain::ADC( int main(void) { using ExampleADCBoard = ST_LIB::Board; ExampleADCBoard::init(); + start_terminal(); 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", kSingleChannelWiringHint, "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 = kDualChannelInput0; +constexpr auto adc_input_1_cfg = kDualChannelInput1; + +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(); + + auto& adc_input_0_instance = ExampleADCBoard::instance_of(); + auto& adc_input_1_instance = ExampleADCBoard::instance_of(); + + print_banner( + "ADC dual-channel example", + 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); + + 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/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/Runes/generated_metadata.cpp b/Core/Src/Runes/generated_metadata.cpp index ca034a4a..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 - "20260218T203535" // DateTime using ISO-8601 format + "20260316T225223" // DateTime using ISO-8601 format " " // alignment - "019403b6" // STLIB commit + "ee54dc7a" // STLIB commit "--------" // ADJ commit - "9c87b508" // Board commit + "a3e59583" // Board commit // the '=' is used for unparsing ; } 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 */ } } diff --git a/README.md b/README.md index 0ab821c0..6ddf4786 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,42 @@ 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 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 +``` + +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` + +> [!NOTE] +To connect through UART (`./hyper uart`), it's recommended to install `tio` with your package manager. + ## Documentation - Template setup: [`docs/template-project/setup.md`](docs/template-project/setup.md) @@ -25,10 +55,10 @@ ctest --preset simulator-all - `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 @@ -50,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/deps/ST-LIB b/deps/ST-LIB index ac84fbcd..8ba761b2 160000 --- a/deps/ST-LIB +++ b/deps/ST-LIB @@ -1 +1 @@ -Subproject commit ac84fbcd6b212351e0c22bde9fd3728032a4f406 +Subproject commit 8ba761b2315739303d28a70334f057031c0faadb diff --git a/docs/examples/README.md b/docs/examples/README.md index bc83488f..01d1de5a 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -15,19 +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). @@ -35,9 +35,17 @@ 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 +``` + +`./hyper uart` prefers `tio` and falls back to `cu`. + ## Documents - [ExampleADC](./example-adc.md) @@ -112,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 530d2933..ea268305 100644 --- a/docs/template-project/setup.md +++ b/docs/template-project/setup.md @@ -11,13 +11,14 @@ 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 From the repository root: ```sh -./tools/init.sh +./hyper init ``` This command: @@ -29,7 +30,7 @@ This command: On Windows: ```bat -tools\init.bat +python hyper init ``` ## 3. `BOARD_NAME` Configuration (codegen) @@ -41,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 ``` diff --git a/hyper b/hyper new file mode 100755 index 00000000..d2efedfc --- /dev/null +++ b/hyper @@ -0,0 +1,1335 @@ +#!/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" +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") +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 build main --preset nucleo-debug + hyper hardfault-analysis + 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") + 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, + 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), + ) + 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: + 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_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" + ) + 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 firmware targets, 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 firmware target") + 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 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( + "--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) + + 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.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) + + 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 index 9f2d771d..24b8c13c 100755 --- a/tools/build-example.sh +++ b/tools/build-example.sh @@ -5,31 +5,38 @@ usage() { cat <<'EOF' Usage: tools/build-example.sh --example [options] -Compiles one example by injecting EXAMPLE_* / TEST_* defines through CMake. +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. 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 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 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 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:]_')" @@ -49,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 @@ -67,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 @@ -77,16 +94,17 @@ 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" 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)" @@ -105,6 +123,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 +135,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 +203,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 " - " @@ -233,12 +255,33 @@ 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}")" +if [[ -n "$test_macro" ]]; then + binary_dir+="/$(sanitize_path_fragment "${test_macro}")" +else + binary_dir+="/no_test" fi echo "[build-example] repo: ${repo_root}" @@ -249,13 +292,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}" - -DBUILD_EXAMPLES=ON + -B "${binary_dir}" + "-DBUILD_EXAMPLES=${build_examples}" + -DCMAKE_EXPORT_COMPILE_COMMANDS=OFF "-DCMAKE_CXX_FLAGS=${define_flags}" ) @@ -264,6 +310,6 @@ if [[ -n "$board_name" ]]; then fi "${configure_cmd[@]}" -cmake --build --preset "${preset}" +cmake --build "${binary_dir}" echo "[build-example] Build completed." diff --git a/hard_fault_analysis.py b/tools/hard_fault_analysis.py similarity index 100% rename from hard_fault_analysis.py rename to tools/hard_fault_analysis.py 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."