From 2c9876ad61da54d942682172f021dfe410c2f078 Mon Sep 17 00:00:00 2001 From: Peter Feerick <5500713+pfeerick@users.noreply.github.com> Date: Fri, 9 Oct 2026 00:54:45 +0000 Subject: [PATCH 1/3] fix(v15): correct IMU axis orientation The IMU is mounted upside down and rotated, so TltX followed the Y tilt and TltY the X tilt, both reversed. Swap X/Y in the board's IMU read (before the generic inversion and calibration, so the X/Y settings stay attached to the right source). Both axes are already inverted by default via imu_invert_x/y. Co-Authored-By: Claude Opus 5.5 --- radio/src/targets/hrs/v15/board.cpp | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/radio/src/targets/hrs/v15/board.cpp b/radio/src/targets/hrs/v15/board.cpp index ff12f3b122c..b8f56bf3f4e 100644 --- a/radio/src/targets/hrs/v15/board.cpp +++ b/radio/src/targets/hrs/v15/board.cpp @@ -84,9 +84,30 @@ static const etx_imu_t _imu_candidates[] = { #endif }; +// The IMU is mounted upside down and rotated, so its X/Y axes are swapped +// (and both reversed, see imu_invert_x/y in hw_defs). Swap them here, before +// the generic inversion/calibration, so that TltX/TltY and their settings +// line up +static imu_read_fn _imuRead = nullptr; + +static int imuReadSwapXY(etx_imu_data_t* data) +{ + int ret = _imuRead(data); + if (ret >= 0) { + float t = data->accel_x; + data->accel_x = data->accel_y; + data->accel_y = t; + t = data->gyro_x; + data->gyro_x = data->gyro_y; + data->gyro_y = t; + } + return ret; +} + static void gyroInit() { - gyroStart(imuDetect(_imu_candidates, DIM(_imu_candidates))); + _imuRead = imuDetect(_imu_candidates, DIM(_imu_candidates)); + gyroStart(_imuRead ? imuReadSwapXY : nullptr); } #if defined(SIXPOS_SWITCH_INDEX) From 1ad992692db090d16559bfada0f957885467aa8a Mon Sep 17 00:00:00 2001 From: Peter Feerick <5500713+pfeerick@users.noreply.github.com> Date: Fri, 9 Oct 2026 00:54:45 +0000 Subject: [PATCH 2/3] fix(v15): read function switches on press, drop phantom S3 multipos The six function switch buttons are a resistor ladder on PC5, read by sixPosUpdateFromAdc(). V15 exposed that input as P3, a FLEX pot with a MULTIPOS default, so a meaningless S3 showed up in sources and calibration. Its switch state map was also inverted: idle read as all six pressed and the held button as released, so function switches acted on release. Declare the input as RAW1 (as V12 does) and use V12's debounced six-position/function switch code. Co-Authored-By: Claude Opus 5.5 --- radio/src/boards/hw_defs/v15.json | 9 +- radio/src/targets/hrs/v15/board.cpp | 128 ++++++++++++---------------- radio/src/targets/hrs/v15/board.h | 2 - radio/src/targets/hrs/v15/hal.h | 6 +- 4 files changed, 61 insertions(+), 84 deletions(-) diff --git a/radio/src/boards/hw_defs/v15.json b/radio/src/boards/hw_defs/v15.json index 6c1c52bf317..41c26e1c96c 100644 --- a/radio/src/boards/hw_defs/v15.json +++ b/radio/src/boards/hw_defs/v15.json @@ -87,15 +87,12 @@ "default": "POT_CENTER" }, { - "name": "P3", - "type": "FLEX", + "name": "RAW1", + "type": "RAW", "adc": "MAIN", "gpio": "GPIOC", "pin": "LL_GPIO_PIN_5", - "channel": "LL_ADC_CHANNEL_8", - "label": "S3", - "short_label": "3", - "default": "MULTIPOS" + "channel": "LL_ADC_CHANNEL_8" }, { "name": "VBAT", diff --git a/radio/src/targets/hrs/v15/board.cpp b/radio/src/targets/hrs/v15/board.cpp index b8f56bf3f4e..99715fa18c5 100644 --- a/radio/src/targets/hrs/v15/board.cpp +++ b/radio/src/targets/hrs/v15/board.cpp @@ -115,14 +115,63 @@ static void gyroInit() #if defined(FUNCTION_SWITCHES) extern const stm32_switch_t* boardGetSwitchDef(uint8_t idx); +extern SwitchHwPos stm32_switch_get_position(const stm32_switch_t* sw); + extern uint8_t isSwitch3Pos(uint8_t idx); struct _adckey_switches_expander { uint8_t state; }; -const uint8_t _adckeyidx[8] = { - 0, 1,2,4,8,0x10, 0x20,0 -}; -static _adckey_switches_expander _adckey_switches; +// Bitmask of all 6 function switch bits +#define FS_ALL_BITS 0x3F + +// For each 6POS position (0=none, 1-6=SW1-SW6): bit of the active switch, +// inverted so active switch pin reads low (DOWN) like real PCA95xx hardware. +// position 0 (none): all bits set = all UP +// position N: all bits set except SW_N's bit = SW_N is DOWN +static uint8_t _pos_to_state(uint8_t pos) +{ + if (pos == 0) return FS_ALL_BITS; + uint8_t active_bit = (1 << (pos - 1)); + return FS_ALL_BITS & ~active_bit; +} +static _adckey_switches_expander _adckey_switches = { FS_ALL_BITS }; + +// Called whenever the sticky 6POS position changes. Boards that need to +// map position to a switch-expander state (e.g. FUNCTION_SWITCHES) can +// override this to react to position changes without polling adcValues[]. +void sixPosUpdateFromAdc() +{ + uint16_t* values = getAnalogValues(); + uint16_t adcValue = values[SIXPOS_SWITCH_INDEX]; + + uint8_t current = 0; + if (adcValue > 3800) current = 1; + else if (adcValue > 3100) current = 2; + else if (adcValue > 2300) current = 3; + else if (adcValue > 1500) current = 4; + else if (adcValue > 1000) current = 5; + else if (adcValue > 400) current = 6; + + static uint8_t lastSeen = 0; + static uint8_t debounce = 0; + static uint8_t sixPosState = 0; + + if (current != lastSeen) { + lastSeen = current; + debounce = 2; // require this many more stable samples before committing + } else if (debounce > 0) { + if (--debounce == 0) { + // Switch state always reflects the true debounced position + _adckey_switches.state = _pos_to_state(current); + if (current != 0) { + // Indicator value stays sticky on the last active position + sixPosState = current; + } + } + } + + values[SIXPOS_SWITCH_INDEX] = sixPosState ? (4096 / 5) * (7 - sixPosState) : 0; +} static SwitchHwPos _get_switch_pos(uint8_t idx) { @@ -131,10 +180,6 @@ static SwitchHwPos _get_switch_pos(uint8_t idx) const stm32_switch_t* def = boardGetSwitchDef(idx); uint8_t state = _adckey_switches.state; - if (def->GPIOx_high != nullptr) { // hardware-backed switches (SA-SF) - return stm32_switch_get_position(def); - } - //get adc key switch position if (def->isCustomSwitch) { if ((state & def->Pin_high) == 0) { return SWITCH_HW_DOWN; @@ -183,78 +228,15 @@ SwitchHwPos boardSwitchGetPosition(uint8_t idx) { if (boardIsCustomSwitch(idx)) { return _get_fs_switch_pos(idx); + } else if (boardGetSwitchDef(idx)->GPIOx_high != nullptr) { + return stm32_switch_get_position(boardGetSwitchDef(idx)); } else { return _get_switch_pos(idx); } } -#endif - -uint8_t lastADCState = 0; -uint8_t sixPosState = 0; - -uint8_t uploadPosState = 5; -bool dirty = true; -uint16_t getSixPosAnalogValue(uint16_t adcValue) -{ - uint8_t currentADCState = 0; - if(uploadPosState){ - uploadPosState--; - goto __retposadc__; - } - else if (adcValue > 3800) - currentADCState = 1; - else if (adcValue > 3100) - currentADCState = 2; - else if (adcValue > 2300) - currentADCState = 3; - else if (adcValue > 1500) - currentADCState = 4; - else if (adcValue > 1000) - currentADCState = 5; - else if (adcValue > 400) - currentADCState = 6; - if (lastADCState != currentADCState) { - lastADCState = currentADCState; - uploadPosState=10; - } -#if defined(FUNCTION_SWITCHES) - else if (lastADCState != sixPosState) { - sixPosState = lastADCState ; - dirty = true; - } -#else - else if (lastADCState != 0 && lastADCState - 1 != sixPosState) { - sixPosState = lastADCState - 1; - dirty = true; - } #endif - if (dirty) { - #if !defined(FUNCTION_SWITCHES) - for (uint8_t i = 0; i < 6; i++) { - if (i == sixPosState) { - rgbSetLedColor(i, SIXPOS_LED_RED, SIXPOS_LED_GREEN, SIXPOS_LED_BLUE); - } else { - rgbSetLedColor(i, 0, 160, 0); - } - } - rgbLedColorApply(); - #else - _adckey_switches.state=_adckeyidx[sixPosState]; - #endif - dirty = false; - } -__retposadc__: - - return (4096/5)*(sixPosState); -} -void sixPosUpdateFromAdc() -{ - uint16_t* values = getAnalogValues(); - values[SIXPOS_SWITCH_INDEX] = - getSixPosAnalogValue(values[SIXPOS_SWITCH_INDEX]); -} #endif static void led_strip_off() diff --git a/radio/src/targets/hrs/v15/board.h b/radio/src/targets/hrs/v15/board.h index fb57287c3a5..08281c0cefc 100644 --- a/radio/src/targets/hrs/v15/board.h +++ b/radio/src/targets/hrs/v15/board.h @@ -170,8 +170,6 @@ void ledBlue(); void ledGreen(); void ledBoot(); -uint16_t getSixPosAnalogValue(uint16_t adcValue); - // LCD driver void lcdSetInitalFrameBuffer(void* fbAddress); void lcdInit(); diff --git a/radio/src/targets/hrs/v15/hal.h b/radio/src/targets/hrs/v15/hal.h index 0d7f2fe615d..cf67e4d8c12 100644 --- a/radio/src/targets/hrs/v15/hal.h +++ b/radio/src/targets/hrs/v15/hal.h @@ -183,11 +183,11 @@ USART6: EXTMODULE_USART #define ADC_GPIO_PIN_POT1 LL_GPIO_PIN_3 // PC.03 POT2 ADC12_INP13 #define ADC_GPIO_PIN_POT2 LL_GPIO_PIN_2 // PC.02 POT1 ADC123_INP12 -#define ADC_GPIO_PIN_POT3 LL_GPIO_PIN_5 // PC.05 POT3 ADC12_INP8 +#define ADC_GPIO_PIN_RAW1 LL_GPIO_PIN_5 // PC.05 6POS ADC12_INP8 #define ADC_GPIO_PIN_BATT LL_GPIO_PIN_4 // PC.04 ADC12_INP4 #define ADC_GPIOA_PINS (ADC_GPIO_PIN_STICK_LH | ADC_GPIO_PIN_STICK_LV | ADC_GPIO_PIN_STICK_RH | ADC_GPIO_PIN_STICK_RV) -#define ADC_GPIOC_PINS (ADC_GPIO_PIN_POT1 | ADC_GPIO_PIN_POT2 | ADC_GPIO_PIN_POT3 | ADC_GPIO_PIN_BATT) +#define ADC_GPIOC_PINS (ADC_GPIO_PIN_POT1 | ADC_GPIO_PIN_POT2 | ADC_GPIO_PIN_RAW1 | ADC_GPIO_PIN_BATT) #define ADC_CHANNEL_STICK_LH LL_ADC_CHANNEL_16 #define ADC_CHANNEL_STICK_LV LL_ADC_CHANNEL_17 @@ -196,7 +196,7 @@ USART6: EXTMODULE_USART #define ADC_CHANNEL_POT1 LL_ADC_CHANNEL_12 #define ADC_CHANNEL_POT2 LL_ADC_CHANNEL_13 -#define ADC_CHANNEL_POT3 LL_ADC_CHANNEL_8 +#define ADC_CHANNEL_RAW1 LL_ADC_CHANNEL_8 #define ADC_CHANNEL_BATT LL_ADC_CHANNEL_4 #define ADC_CHANNEL_RTC_BAT LL_ADC_CHANNEL_VBAT // ADC3 IMP17 From d374b8d09295c0f9191eab198015b3cbbd07387c Mon Sep 17 00:00:00 2001 From: Peter Feerick <5500713+pfeerick@users.noreply.github.com> Date: Fri, 9 Oct 2026 01:21:21 +0000 Subject: [PATCH 3/3] chore(web): regenerate radio list for V15 RAW1 change Co-Authored-By: Claude Opus 5.5 --- web/public/radios.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/web/public/radios.json b/web/public/radios.json index 013458c9848..71c4c57b302 100644 --- a/web/public/radios.json +++ b/web/public/radios.json @@ -2664,12 +2664,6 @@ "type": "FLEX", "label": "S2", "default": "POT_CENTER" - }, - { - "name": "P3", - "type": "FLEX", - "label": "S3", - "default": "MULTIPOS" } ], "switches": [