From 6730402a23e1da3d55c3d18b49d79fba8b678564 Mon Sep 17 00:00:00 2001 From: PonomarevDA Date: Fri, 17 Jul 2026 09:59:48 +0300 Subject: [PATCH 1/4] feat(adc): support board-provided polled channels Boards define HAL::adc_channels() to opt out of the DMA-scanned ADC1 path, which the STM32H7 cannot use for ADC3. Default stays empty, so existing boards are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- Src/drivers/board_monitor/board_monitor.cpp | 3 +- Src/drivers/board_monitor/board_monitor.hpp | 9 +- Src/peripheral/adc/adc_stm32.cpp | 110 ++++++++++++++++++-- Src/peripheral/adc/adc_stm32.hpp | 34 ++++++ 4 files changed, 147 insertions(+), 9 deletions(-) create mode 100644 Src/peripheral/adc/adc_stm32.hpp diff --git a/Src/drivers/board_monitor/board_monitor.cpp b/Src/drivers/board_monitor/board_monitor.cpp index 9d6d0f37..4f363c57 100644 --- a/Src/drivers/board_monitor/board_monitor.cpp +++ b/Src/drivers/board_monitor/board_monitor.cpp @@ -42,7 +42,8 @@ uint16_t BoardMonitor::temperature() { auto adc_12b = HAL::Adc::get(BoardAdc::RANK_TEMPERATURE); uint16_t temperature_kelvin; -#ifdef STM32G0B1xx +#if defined(STM32G0B1xx) || defined(STM32H753xx) + // Factory-calibrated (TS_CAL1/TS_CAL2) conversion. temperature_kelvin = __HAL_ADC_CALC_TEMPERATURE(3300, adc_12b, ADC_RESOLUTION_12B) + 273; #else // STM32F103xB static const uint16_t TEMP_REF = 25; diff --git a/Src/drivers/board_monitor/board_monitor.hpp b/Src/drivers/board_monitor/board_monitor.hpp index f7818f8f..b68f424b 100644 --- a/Src/drivers/board_monitor/board_monitor.hpp +++ b/Src/drivers/board_monitor/board_monitor.hpp @@ -48,8 +48,13 @@ class BoardMonitor{ return std::numeric_limits::quiet_NaN(); } - if (auto hw_version = hardware_version(); hw_version < 2403 || hw_version > 2450) { - return std::numeric_limits::quiet_NaN(); + // The INA169 calibration below only holds for the kirpi revisions that carry a + // hardware-version divider. Boards without one (RANK_VERSION unmapped) have their + // own current sense, so the version gate would reject them forever. + if constexpr (BoardAdc::RANK_VERSION != BoardAdc::INVALID_RANK) { + if (auto hw_version = hardware_version(); hw_version < 2403 || hw_version > 2450) { + return std::numeric_limits::quiet_NaN(); + } } // Current sensor: INA169NA/3K, R = 33K ohm diff --git a/Src/peripheral/adc/adc_stm32.cpp b/Src/peripheral/adc/adc_stm32.cpp index 05f9cc74..0a9ed04b 100644 --- a/Src/peripheral/adc/adc_stm32.cpp +++ b/Src/peripheral/adc/adc_stm32.cpp @@ -6,6 +6,7 @@ */ #include "peripheral/adc/adc.hpp" +#include "peripheral/adc/adc_stm32.hpp" #include "main.h" extern ADC_HandleTypeDef hadc1; @@ -13,18 +14,104 @@ extern ADC_HandleTypeDef hadc1; static constexpr uint8_t ADC_MAX_DMA_CHANNELS = 16; static uint16_t adc_dma_buffer[ADC_MAX_DMA_CHANNELS]; +// Long enough for the internal temperature sensor, which needs a far greater +// acquisition time than a resistor divider (~9us minimum on the STM32H7). +#if defined(STM32H753xx) +static constexpr uint32_t ADC_POLL_SAMPLETIME = ADC_SAMPLETIME_387CYCLES_5; +#elif defined(STM32G0B1xx) +static constexpr uint32_t ADC_POLL_SAMPLETIME = ADC_SAMPLETIME_79CYCLES_5; +#else +static constexpr uint32_t ADC_POLL_SAMPLETIME = ADC_SAMPLETIME_55CYCLES_5; +#endif + +static constexpr uint32_t ADC_POLL_TIMEOUT_MS = 10; + namespace HAL { -int8_t Adc::init(uint8_t channel_count) { - if (channel_count == 0 || channel_count > ADC_MAX_DMA_CHANNELS) { +// Weak default: no board-provided channels -> legacy DMA-scanned ADC1. +// A board enables polled reads (and extra ADCs) by defining HAL::adc_channels(). +[[gnu::weak]] std::span adc_channels() { + return {}; +} + +namespace { + +int8_t calibrate(ADC_HandleTypeDef* hadc) { +#ifdef STM32H753xx + if (HAL_ADCEx_Calibration_Start(hadc, ADC_CALIB_OFFSET, ADC_SINGLE_ENDED) != HAL_OK) { +#else + if (HAL_ADCEx_Calibration_Start(hadc) != HAL_OK) { +#endif return -1; } + return 0; +} +// Single blocking conversion. Cheap enough for the slow rates the monitors use. +uint16_t read_channel(const AdcChannel& input) { + ADC_ChannelConfTypeDef config{}; + config.Channel = input.channel; + config.Rank = ADC_REGULAR_RANK_1; + config.SamplingTime = ADC_POLL_SAMPLETIME; #ifdef STM32H753xx - if (HAL_ADCEx_Calibration_Start(&hadc1, ADC_CALIB_OFFSET, ADC_SINGLE_ENDED) != HAL_OK) { -#else - if (HAL_ADCEx_Calibration_Start(&hadc1) != HAL_OK) { + config.SingleDiff = ADC_SINGLE_ENDED; + config.OffsetNumber = ADC_OFFSET_NONE; + config.Offset = 0; #endif + + if (HAL_ADC_ConfigChannel(input.hadc, &config) != HAL_OK) { + return 0; + } + + if (HAL_ADC_Start(input.hadc) != HAL_OK) { + return 0; + } + + uint16_t value = 0; + if (HAL_ADC_PollForConversion(input.hadc, ADC_POLL_TIMEOUT_MS) == HAL_OK) { + value = static_cast(HAL_ADC_GetValue(input.hadc)); + } + + HAL_ADC_Stop(input.hadc); + return value; +} + +} // namespace + +int8_t Adc::init(uint8_t channel_count) { + if (channel_count == 0 || channel_count > ADC_MAX_DMA_CHANNELS) { + return -1; + } + + const auto channels = adc_channels(); + + if (!channels.empty()) { + // Calibrate every distinct ADC referenced by the board's channel table. + for (size_t idx = 0; idx < channels.size(); idx++) { + auto* hadc = channels[idx].hadc; + if (hadc == nullptr) { + return -1; + } + + bool already_done = false; + for (size_t prev = 0; prev < idx; prev++) { + if (channels[prev].hadc == hadc) { + already_done = true; + break; + } + } + + if (!already_done && calibrate(hadc) != 0) { + return -1; + } + } + + _channel_count = static_cast(channels.size()); + _is_adc_already_inited = true; + return 0; + } + + if (calibrate(&hadc1) != 0) { return -1; } @@ -41,6 +128,12 @@ uint16_t Adc::get(uint8_t rank) { if (!_is_adc_already_inited || rank >= _channel_count) { return 0; } + + const auto channels = adc_channels(); + if (!channels.empty()) { + return read_channel(channels[rank]); + } + return adc_dma_buffer[rank]; } @@ -48,9 +141,14 @@ uint16_t Adc::get(uint8_t rank) { #ifdef HAL_ADC_MODULE_ENABLED /** - * @note We assume that hadc->Instance == ADC1 always! + * @note Only used by the legacy DMA-scanned ADC1 path; polled boards never start a + * DMA transfer, so this stays a no-op for them. */ void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { + if (!HAL::adc_channels().empty()) { + return; + } + auto channel_count = HAL::Adc::channel_count(); if (channel_count == 0) { return; diff --git a/Src/peripheral/adc/adc_stm32.hpp b/Src/peripheral/adc/adc_stm32.hpp new file mode 100644 index 00000000..784e1f12 --- /dev/null +++ b/Src/peripheral/adc/adc_stm32.hpp @@ -0,0 +1,34 @@ +/** + * This program is free software under the GNU General Public License v3. + * See for details. + * Author: Dmitry Ponomarev + */ + +#ifndef SRC_PERIPHERAL_ADC_ADC_STM32_HPP_ +#define SRC_PERIPHERAL_ADC_ADC_STM32_HPP_ + +#include +#include "main.h" + +namespace HAL { + +// One measured input: which ADC peripheral and which channel on it. +struct AdcChannel { + ADC_HandleTypeDef* hadc; + uint32_t channel; // ADC_CHANNEL_x +}; + +// Boards that cannot use the default single-ADC1 DMA scan override this in a +// board source file, returning one entry per BoardAdc rank (rank == index). +// Such inputs are read by polling a single conversion on demand, which keeps +// the driver free of DMA/domain constraints — on the STM32H7 in particular, +// ADC3 lives in D3 and could otherwise only be served by BDMA out of SRAM4. +// +// The default returns an empty span: the driver then keeps the legacy +// DMA-scanned ADC1 behaviour driven by Adc::init(channel_count), so existing +// boards are unaffected. +std::span adc_channels(); + +} // namespace HAL + +#endif // SRC_PERIPHERAL_ADC_ADC_STM32_HPP_ From 7dc2cbf8b0771f972763a47b1985738a73b087c3 Mon Sep 17 00:00:00 2001 From: PonomarevDA Date: Fri, 17 Jul 2026 10:00:08 +0300 Subject: [PATCH 2/4] fix(i2c): keep error codes negative, allow instance override Negating an already-negative HAL code made failures indistinguishable from valid register values. Also adds NC_I2C_INSTANCE to select the I2C peripheral per board; defaults to hi2c1. Co-Authored-By: Claude Opus 4.8 (1M context) --- Src/peripheral/i2c/i2c_stm32.cpp | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/Src/peripheral/i2c/i2c_stm32.cpp b/Src/peripheral/i2c/i2c_stm32.cpp index 1af444da..dc806717 100644 --- a/Src/peripheral/i2c/i2c_stm32.cpp +++ b/Src/peripheral/i2c/i2c_stm32.cpp @@ -12,12 +12,20 @@ namespace HAL { static const constexpr uint32_t I2C_TIMEOUT = 30; +// The I2C instance can be overridden per board via -DNC_I2C_INSTANCE= +// (e.g. am/node_h7 puts the INA228 sensors on I2C3). Defaults to hi2c1. +#if defined(NC_I2C_INSTANCE) +static I2C_HandleTypeDef* const nc_hi2c = &NC_I2C_INSTANCE; +#else +static I2C_HandleTypeDef* const nc_hi2c = &hi2c1; +#endif + int8_t I2C::init() { - if (auto res = HAL_I2C_DeInit(&hi2c1); res != 0) { + if (auto res = HAL_I2C_DeInit(nc_hi2c); res != 0) { return -res; } - if (auto res = HAL_I2C_Init(&hi2c1); res != 0) { + if (auto res = HAL_I2C_Init(nc_hi2c); res != 0) { return -res; } @@ -25,29 +33,32 @@ int8_t I2C::init() { } int8_t I2C::is_device_ready(uint16_t address, uint8_t trials) { - auto res = HAL_I2C_IsDeviceReady(&hi2c1, address, trials, I2C_TIMEOUT); + auto res = HAL_I2C_IsDeviceReady(nc_hi2c, address, trials, I2C_TIMEOUT); return (res == HAL_OK) ? 0 : -res; } int8_t I2C::transmit(uint16_t id, uint8_t tx[], uint8_t len) { - auto res = HAL_I2C_Master_Transmit(&hi2c1, id, tx, len, I2C_TIMEOUT); + auto res = HAL_I2C_Master_Transmit(nc_hi2c, id, tx, len, I2C_TIMEOUT); return (res == HAL_OK) ? 0 : -res; } int8_t I2C::receive(uint16_t id, uint8_t *rx, uint8_t len) { - auto res = HAL_I2C_Master_Receive(&hi2c1, id, rx, len, I2C_TIMEOUT); + auto res = HAL_I2C_Master_Receive(nc_hi2c, id, rx, len, I2C_TIMEOUT); return (res == HAL_OK) ? 0 : -res; } int32_t I2C::read_register_1_byte(uint16_t device_id, uint8_t reg_address) { std::array tx_buffer = {{reg_address}}; + // Propagate the negative error code as documented: negating it here would make a + // failure indistinguishable from a valid register value and defeat every caller's + // `if (reg_value < 0)` check. if (auto res = HAL::I2C::transmit(device_id, tx_buffer.data(), tx_buffer.size()); res < 0) { - return -res; + return res; } std::array reg_value = {}; if (auto res = HAL::I2C::receive(device_id, reg_value.data(), reg_value.size()); res < 0) { - return -res; + return res; } return reg_value[0]; @@ -55,13 +66,14 @@ int32_t I2C::read_register_1_byte(uint16_t device_id, uint8_t reg_address) { int32_t I2C::read_register_2_bytes(uint16_t device_id, uint8_t reg_address) { std::array tx_buffer = {{reg_address}}; + // See read_register_1_byte(): the error code must stay negative. if (auto res = HAL::I2C::transmit(device_id, tx_buffer.data(), tx_buffer.size()); res < 0) { - return -res; + return res; } std::array reg_value = {}; if (auto res = HAL::I2C::receive(device_id, reg_value.data(), reg_value.size()); res < 0) { - return -res; + return res; } return (reg_value[0] << 8) | reg_value[1]; From 24d7062f0b0585b2de7b5a19a2d4ed7e70ce174d Mon Sep 17 00:00:00 2001 From: PonomarevDA Date: Fri, 17 Jul 2026 10:00:31 +0300 Subject: [PATCH 3/4] feat(spi): allow per-board instance and NSS overrides Adds NC_SPI_INSTANCE and NC_SPI_NSS_GPIO_Port/Pin. Defaults keep the historical behaviour. Co-Authored-By: Claude Opus 4.8 (1M context) --- Src/peripheral/spi/spi_stm32.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Src/peripheral/spi/spi_stm32.cpp b/Src/peripheral/spi/spi_stm32.cpp index eee2fdbc..173c3884 100644 --- a/Src/peripheral/spi/spi_stm32.cpp +++ b/Src/peripheral/spi/spi_stm32.cpp @@ -13,7 +13,13 @@ static constexpr uint32_t TRANSMIT_DELAY = 100; // 100 is experimental value. Need to prove it static constexpr std::byte SPI_READ{0x80}; -#if defined(STM32F103xB) || defined(STM32F103xE) +// The SPI instance and chip-select GPIO can be overridden per board via +// -DNC_SPI_INSTANCE= and -DNC_SPI_NSS_GPIO_Port / -DNC_SPI_NSS_Pin +// (e.g. am/node_h7 puts the MAX14906 on SPI6 with nCS on PI10). Defaults keep +// the historical behaviour for boards that don't override. +#if defined(NC_SPI_INSTANCE) +static SPI_HandleTypeDef* hspi = &NC_SPI_INSTANCE; +#elif defined(STM32F103xB) || defined(STM32F103xE) static SPI_HandleTypeDef* hspi = &hspi1; #else static SPI_HandleTypeDef* hspi = &hspi2; @@ -22,12 +28,15 @@ static SPI_HandleTypeDef* hspi = &hspi2; namespace HAL { static void spi_set_nss(bool nss_state) { -#ifdef SPI2_NSS_GPIO_Port auto state = nss_state ? GPIO_PIN_SET : GPIO_PIN_RESET; +#if defined(NC_SPI_NSS_GPIO_Port) + HAL_GPIO_WritePin(NC_SPI_NSS_GPIO_Port, NC_SPI_NSS_Pin, state); +#elif defined(SPI2_NSS_GPIO_Port) HAL_GPIO_WritePin(SPI2_NSS_GPIO_Port, SPI2_NSS_Pin, state); #elif defined(SPI_SS_GPIO_Port) - auto state = nss_state ? GPIO_PIN_SET : GPIO_PIN_RESET; HAL_GPIO_WritePin(SPI_SS_GPIO_Port, SPI_SS_Pin, state); +#else + (void)state; #endif } From 40c865d3ffd6f2ac74446114a5c24b0c8b23b485 Mon Sep 17 00:00:00 2001 From: PonomarevDA Date: Fri, 17 Jul 2026 10:00:51 +0300 Subject: [PATCH 4/4] fix(cmake): re-configure when params.yaml changes Without CONFIGURE_DEPENDS a yaml edit was silently ignored. Also drop the two libparams defines deprecated upstream. Co-Authored-By: Claude Opus 4.8 (1M context) --- CMakeLists.txt | 2 -- cmake/params.cmake | 5 +++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1729ab7b..44dae4e3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -348,8 +348,6 @@ set(BUILD_OBJ_DIR ${BUILD_TARGET_DIR}/obj) if(USE_PLATFORM_UBUNTU) add_definitions(-DLIBPARAMS_PARAMS_DIR="${BUILD_SRC_DIR}") - add_definitions(-DLIBPARAMS_INIT_PARAMS_FILE_NAME="default_params") - add_definitions(-DLIBPARAMS_TEMP_PARAMS_FILE_NAME="temp_params") endif() include(${LIBPARAMS_PATH}/${LIBPARAMS_CMAKE}) diff --git a/cmake/params.cmake b/cmake/params.cmake index f676f253..32528fb9 100644 --- a/cmake/params.cmake +++ b/cmake/params.cmake @@ -11,6 +11,11 @@ elseif(NOT APPLICATION_DIR) message(SEND_ERROR "APPLICATION_DIR is unknown.") endif() +# The generators below run at configure time, so CMake must re-configure whenever a +# params.yaml changes. Without this a yaml edit is silently ignored: the build reuses +# the previously generated params.cpp and the firmware keeps the old defaults. +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${LIBPARAMS_PARAMS}) + execute_process( COMMAND python ${LIBPARAMS_PATH}/scripts/generate_params.py --out-dir ${BUILD_SRC_DIR} -f ${LIBPARAMS_PARAMS} RESULT_VARIABLE result