feat: More flexible ADC inputs (#3870)

Co-authored-by: 3djc <lesitewebdejc@hotmail.com>
Co-authored-by: Peter Feerick <peter.feerick@gmail.com>
Co-authored-by: philmoz <phil.a.mitchell@gmail.com>
This commit is contained in:
Raphael Coeffic
2023-11-02 11:07:53 +10:00
committed by GitHub
co-authored by 3djc Peter Feerick philmoz
parent f5593c44e8
commit b48e07026d
78 changed files with 1291 additions and 773 deletions
+6
View File
@@ -7,8 +7,14 @@ set(SRC ${SRC}
hal/module_port.cpp
hal/adc_driver.cpp
hal/key_driver.cpp
hal/switch_driver.cpp
)
if (FLEXSW)
# define the number of allowed flexible adc entries used as switch
add_definitions(-DMAX_FLEX_SWITCHES=${FLEXSW})
endif()
include_directories(${FATFS_DIR} ${FATFS_DIR}/option)
set(BOOTLOADER_SRC ${BOOTLOADER_SRC}
+158 -92
View File
@@ -22,10 +22,8 @@
#include "adc_driver.h"
#include "board.h"
// IS_POT_SLIDER_AVAILABLE()
#include "opentx.h"
const etx_hal_adc_driver_t* _hal_adc_driver = nullptr;
const etx_hal_adc_inputs_t* _hal_adc_inputs = nullptr;
@@ -71,10 +69,20 @@ bool adcRead()
return true;
}
void adcCalibDefaults()
{
for (int i = 0; i < adcGetMaxCalibratedInputs(); i++) {
CalibData* calib = &g_eeGeneral.calib[i];
calib->mid = 1023;
calib->spanNeg = 1024 - (1024 / STICK_TOLERANCE);
calib->spanPos = 1024 - (1024 / STICK_TOLERANCE);
}
}
void adcCalibSetMidPoint()
{
uint8_t max_inputs = adcGetMaxCalibratedInputs();
uint8_t pot_offset = adcGetInputOffset(ADC_INPUT_POT);
uint8_t pot_offset = adcGetInputOffset(ADC_INPUT_FLEX);
for (uint8_t i = 0; i < max_inputs; i++) {
@@ -124,32 +132,32 @@ void adcCalibSetMinMax()
{
// get low and high vals for sticks and pots
uint8_t max_input = adcGetMaxCalibratedInputs();
uint8_t pot_offset = adcGetInputOffset(ADC_INPUT_POT);
uint8_t pot_offset = adcGetInputOffset(ADC_INPUT_FLEX);
for (uint8_t i = 0; i < max_input; i++) {
auto& calib = reusableBuffer.calib.inputs[i];
int16_t vt = getAnalogValue(i) >> 1;
if (i < pot_offset || !IS_POT_MULTIPOS(i - pot_offset)) {
int16_t vt = anaIn(i);
calib.input.loVal = min(vt, calib.input.loVal);
calib.input.hiVal = max(vt, calib.input.hiVal);
if (i >= pot_offset && IS_POT_WITHOUT_DETENT(i - pot_offset)) {
calib.input.midVal = (calib.input.hiVal + calib.input.loVal) / 2;
if (i >= pot_offset) {
auto pot_cfg = POT_CONFIG(i - pot_offset);
if (pot_cfg == FLEX_POT || pot_cfg == FLEX_SWITCH || pot_cfg == FLEX_NONE) {
calib.input.midVal = (calib.input.hiVal + calib.input.loVal) / 2;
}
}
// in case we enough input movement, store the result
if (abs(calib.input.loVal - calib.input.hiVal) > 50) {
writeAnalogCalib(i, calib.input.loVal, calib.input.midVal, calib.input.hiVal);
writeAnalogCalib(i, calib.input.loVal, calib.input.midVal, calib.input.hiVal);
}
} else {
auto& xpot = calib.xpot;
int count = xpot.stepsCount;
if (count <= XPOTS_MULTIPOS_COUNT) {
// use raw analog value for multipos calibraton,
// anaIn() already has multipos decoded value
int16_t vt = getAnalogValue(i) >> 1;
if (xpot.lastCount == 0 || vt < xpot.lastPosition - XPOT_DELTA ||
vt > xpot.lastPosition + XPOT_DELTA) {
xpot.lastPosition = vt;
@@ -192,8 +200,8 @@ void adcCalibSetMinMax()
static void disableUncalibratedXPots()
{
uint8_t pot_offset = adcGetInputOffset(ADC_INPUT_POT);
uint8_t max_pots = adcGetMaxInputs(ADC_INPUT_POT);
uint8_t pot_offset = adcGetInputOffset(ADC_INPUT_FLEX);
uint8_t max_pots = adcGetMaxInputs(ADC_INPUT_FLEX);
for (uint8_t i = 0; i < max_pots; i++) {
if (IS_POT_MULTIPOS(i)) {
@@ -252,8 +260,8 @@ uint16_t* getAnalogValues()
// used by diaganas
uint32_t s_anaFilt[MAX_ANALOG_INPUTS];
#define ANALOG_MULTIPLIER (1<<ANALOG_SCALE)
#define ANA_FILT(chan) (s_anaFilt[chan] / (JITTER_ALPHA * ANALOG_MULTIPLIER))
#define ANALOG_MULTIPLIER (1 << ANALOG_SCALE)
#define ANA_FILT(chan) (s_anaFilt[chan] / (JITTER_ALPHA * ANALOG_MULTIPLIER))
#if (JITTER_ALPHA * ANALOG_MULTIPLIER > 32)
#error "JITTER_FILTER_STRENGTH and ANALOG_SCALE are too big, their summ should be <= 5 !!!"
#endif
@@ -308,10 +316,121 @@ uint16_t getBatteryVoltage()
#endif
}
static uint32_t apply_low_pass_filter(uint32_t v, uint32_t v_prev,
bool is_main_input)
{
// Jitter filter:
// * pass trough any big change directly
// * for small change use Modified moving average (MMA) filter
//
// Explanation:
//
// Normal MMA filter has this formula:
// <out> = ((ALPHA-1)*<out> + <in>)/ALPHA
//
// If calculation is done this way with integer arithmetics, then any small
// change in input signal is lost. One way to combat that, is to rearrange the
// formula somewhat, to store a more precise (larger) number between
// iterations. The basic idea is to store undivided value between iterations.
// Therefore an new variable <filtered> is used. The new formula becomes:
// <filtered> = <filtered> - <filtered>/ALPHA + <in>
// <out> = <filtered>/ALPHA (use only when out is needed)
//
// The above formula with a maximum allowed ALPHA value (we are limited by
// the 16 bit s_anaFilt[]) was tested on the radio. The resulting signal still
// had some jitter (a value of 1 was observed). The jitter might be bigger on
// other radios.
//
// So another idea is to use larger input values for filtering. So instead of
// using input in a range from 0 to 2047, we use twice larger number (temp[x]
// is divided less)
//
// This also means that ALPHA must be lowered (remember 16 bit limit), but
// test results have proved that this kind of filtering gives better results.
// So the recommended values for filter are:
// JITTER_FILTER_STRENGTH 4
// ANALOG_SCALE 1
//
uint32_t previous = v_prev / JITTER_ALPHA;
uint32_t diff = (v > previous) ? (v - previous) : (previous - v);
// Combine ADC jitter filter setting form radio and model.
// Model can override (on or off) or use setting from radio setup.
// Model setting is active when 1, radio setting is active when 0
// Please note: these settings only apply to main controls.
bool useJitterFilter = true;
if (is_main_input) {
if (g_model.jitterFilter == OVERRIDE_GLOBAL) {
// Use radio setting - which is inverted
useJitterFilter = !g_eeGeneral.noJitterFilter;
} else {
// Enable if value is "On", disable if "Off"
useJitterFilter = (g_model.jitterFilter == OVERRIDE_ON);
}
}
uint32_t out;
if (useJitterFilter && diff < (10 * ANALOG_MULTIPLIER)) {
// apply jitter filter
out = (v_prev - previous) + v;
} else {
// use unfiltered value
out = v * JITTER_ALPHA;
}
return out;
}
static uint32_t apply_calibration(const CalibData* calib, uint32_t v)
{
// Simu uses normed inputs
#if !defined(SIMU)
// Apply calibration relative to mid-point
int32_t s = v - 2 * calib->mid;
s = s * (int32_t)RESX /
(max((int16_t)100, (s > 0 ? calib->spanPos : calib->spanNeg)));
// Translate back in range
s += 2 * RESX;
// Limit values to supported range
if (s < 0) {
s = 0;
} else if (s > 4 * RESX) {
s = 4 * RESX;
}
v = s;
#endif
return v;
}
static uint32_t apply_multipos(const StepsCalibData* calib, uint32_t v)
{
constexpr uint32_t ALPHA_MULT = JITTER_ALPHA * ANALOG_MULTIPLIER;
constexpr uint32_t ANAFILT_MAX = 2 * RESX * ALPHA_MULT;
// TODO: consider adding another low pass filter to eliminate multipos
// switching glitches
uint8_t vShifted = (v / ALPHA_MULT) >> 4;
for (uint32_t i = 0; i < calib->count; i++) {
if (vShifted < calib->steps[i]) {
return (i * (ANAFILT_MAX + ALPHA_MULT)) / calib->count;
}
}
return ANAFILT_MAX;
}
void getADC()
{
uint8_t max_analogs = adcGetMaxInputs(ADC_INPUT_ALL);
uint8_t pot_offset = adcGetInputOffset(ADC_INPUT_POT);
auto max_analogs = adcGetMaxInputs(ADC_INPUT_ALL);
auto max_mains = adcGetMaxInputs(ADC_INPUT_MAIN);
auto max_pots = adcGetMaxInputs(ADC_INPUT_FLEX);
auto pot_offset = adcGetInputOffset(ADC_INPUT_FLEX);
auto max_calib_analogs = adcGetMaxCalibratedInputs();
#if defined(JITTER_MEASURE)
if (JITTER_MEASURE_ACTIVE() && jitterResetTime < get_tmr10ms()) {
@@ -320,78 +439,39 @@ void getADC()
rawJitter[x].reset();
avgJitter[x].reset();
}
jitterResetTime = get_tmr10ms() + 100; //every second
jitterResetTime = get_tmr10ms() + 100; // every second
}
#endif
DEBUG_TIMER_START(debugTimerAdcRead);
if (!adcRead())
TRACE("adcRead failed");
if (!adcRead()) TRACE("adcRead failed");
DEBUG_TIMER_STOP(debugTimerAdcRead);
// TODO: jitter filter should probably still be applied
// to VBAT and RTC_BAT, no matter what is configured
//
for (uint8_t x = 0; x < max_analogs; x++) {
uint32_t v = getAnalogValue(x) >> (1 - ANALOG_SCALE);
bool is_flex_input = (x >= pot_offset) && (x < pot_offset + max_pots);
bool is_multipos = is_flex_input && IS_POT_MULTIPOS(x - pot_offset);
// Jitter filter:
// * pass trough any big change directly
// * for small change use Modified moving average (MMA) filter
//
// Explanation:
//
// Normal MMA filter has this formula:
// <out> = ((ALPHA-1)*<out> + <in>)/ALPHA
//
// If calculation is done this way with integer arithmetics, then any small change in
// input signal is lost. One way to combat that, is to rearrange the formula somewhat,
// to store a more precise (larger) number between iterations. The basic idea is to
// store undivided value between iterations. Therefore an new variable <filtered> is
// used. The new formula becomes:
// <filtered> = <filtered> - <filtered>/ALPHA + <in>
// <out> = <filtered>/ALPHA (use only when out is needed)
//
// The above formula with a maximum allowed ALPHA value (we are limited by
// the 16 bit s_anaFilt[]) was tested on the radio. The resulting signal still had
// some jitter (a value of 1 was observed). The jitter might be bigger on other
// radios.
//
// So another idea is to use larger input values for filtering. So instead of using
// input in a range from 0 to 2047, we use twice larger number (temp[x] is divided less)
//
// This also means that ALPHA must be lowered (remember 16 bit limit), but test results
// have proved that this kind of filtering gives better results. So the recommended values
// for filter are:
// JITTER_FILTER_STRENGTH 4
// ANALOG_SCALE 1
//
// Variables mapping:
// * <in> = v
// * <out> = s_anaFilt[x]
uint32_t previous = s_anaFilt[x] / JITTER_ALPHA;
uint32_t diff = (v > previous) ? (v - previous) : (previous - v);
// 1st: apply calibration
uint32_t v = getAnalogValue(x);
// Combine ADC jitter filter setting form radio and model.
// Model can override (on or off) or use setting from radio setup.
// Model setting is active when 1, radio setting is active when 0
uint8_t useJitterFilter = 0;
if (g_model.jitterFilter == OVERRIDE_GLOBAL) {
// Use radio setting - which is inverted
useJitterFilter = !g_eeGeneral.noJitterFilter;
} else {
// Enable if value is "On", disable if "Off"
useJitterFilter = (g_model.jitterFilter == OVERRIDE_ON)?1:0;
if (x < max_calib_analogs && !is_multipos) {
v = apply_calibration(&g_eeGeneral.calib[x], v);
}
if (useJitterFilter && diff < (10*ANALOG_MULTIPLIER)) {
// apply jitter filter
s_anaFilt[x] = (s_anaFilt[x] - previous) + v;
// 2nd: apply inversion
if (is_flex_input && getPotInversion(x - pot_offset)) {
v = 4 * RESX - v;
}
else {
// use unfiltered value
s_anaFilt[x] = v * JITTER_ALPHA;
// 3rd: apply filtering
s_anaFilt[x] = apply_low_pass_filter(v, s_anaFilt[x], x < max_mains);
if (is_multipos) {
const auto* calib = (const StepsCalibData*)&g_eeGeneral.calib[x];
if (IS_MULTIPOS_CALIBRATED(calib)) {
s_anaFilt[x] = apply_multipos(calib, s_anaFilt[x]);
}
}
#if defined(JITTER_MEASURE)
@@ -399,20 +479,6 @@ void getADC()
avgJitter[x].measure(ANA_FILT(x));
}
#endif
#define ANAFILT_MAX (2 * RESX * JITTER_ALPHA * ANALOG_MULTIPLIER)
StepsCalibData * calib = (StepsCalibData *) &g_eeGeneral.calib[x];
if (IS_POT_MULTIPOS(x - pot_offset) && IS_MULTIPOS_CALIBRATED(calib)) {
// TODO: consider adding another low pass filter to eliminate multipos switching glitches
uint8_t vShifted = ANA_FILT(x) >> 4;
s_anaFilt[x] = ANAFILT_MAX;
for (uint32_t i = 0; i < calib->count; i++) {
if (vShifted < calib->steps[i]) {
s_anaFilt[x] = (i * (ANAFILT_MAX + JITTER_ALPHA * ANALOG_MULTIPLIER)) / calib->count;
break;
}
}
}
}
}
@@ -436,7 +502,7 @@ uint8_t adcGetInputOffset(uint8_t type)
uint8_t adcGetMaxCalibratedInputs()
{
// ADC_INPUT_MAIN + ADC_INPUT_POT + ADC_INPUT_AXIS
// ADC_INPUT_MAIN + ADC_INPUT_FLEX + ADC_INPUT_AXIS
return adcGetInputOffset(ADC_INPUT_VBAT);
}
@@ -467,7 +533,7 @@ const char* adcGetInputName(uint8_t idx)
// find the proper input type
while (_hal_adc_inputs[type].offset + _hal_adc_inputs[type].n_inputs <= idx) {
if (++type > ADC_INPUT_AXIS) return nullptr;
if (++type > ADC_INPUT_FLEX) return nullptr;
}
idx -= _hal_adc_inputs[type].offset;
+4 -2
View File
@@ -37,8 +37,7 @@
enum {
ADC_INPUT_MAIN=0, // gimbals / wheel + throttle
ADC_INPUT_POT,
ADC_INPUT_AXIS,
ADC_INPUT_FLEX,
ADC_INPUT_VBAT,
ADC_INPUT_RTC_BAT,
ADC_INPUT_ALL,
@@ -92,6 +91,9 @@ uint16_t* getAnalogValues();
// Run calibration steps:
// Set default values before loading radio settings
void adcCalibDefaults();
// Set mid-points and reset min/max in temporary buffer
void adcCalibSetMidPoint();
+255
View File
@@ -0,0 +1,255 @@
/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#include "switch_driver.h"
#include "definitions.h"
#include "hal/adc_driver.h"
#include "dataconstants.h"
#include "opentx_helpers.h"
#include "opentx.h"
// Board API
void boardInitSwitches();
SwitchHwPos boardSwitchGetPosition(uint8_t cat, uint8_t idx);
const char* boardSwitchGetName(uint8_t cat, uint8_t idx);
SwitchHwType boardSwitchGetType(uint8_t cat, uint8_t idx);
uint8_t boardGetMaxSwitches();
uint8_t boardGetMaxFctSwitches();
swconfig_t boardSwitchGetDefaultConfig();
constexpr uint8_t _INVALID_ADC_CH = 0xFF;
static uint8_t _flex_switches[MAX_FLEX_SWITCHES];
void switchInit()
{
memset(_flex_switches, _INVALID_ADC_CH, sizeof(_flex_switches));
boardInitSwitches();
}
swconfig_t switchGetDefaultConfig() { return boardSwitchGetDefaultConfig(); }
uint8_t switchGetMaxSwitches()
{
return boardGetMaxSwitches() + MAX_FLEX_SWITCHES;
}
uint8_t switchGetMaxFctSwitches() { return boardGetMaxFctSwitches(); }
uint32_t switchState(uint8_t pos_idx)
{
auto d = div(pos_idx, 3);
auto idx = (uint8_t)d.quot;
auto pos = (SwitchHwPos)d.rem;
return switchGetPosition(idx) == pos;
}
SwitchHwPos switchGetPosition(uint8_t sw_idx)
{
auto idx = (int)sw_idx;
auto max_switches = boardGetMaxSwitches();
if (idx < max_switches) {
return boardSwitchGetPosition(SWITCH_PHYSICAL, idx);
}
idx -= max_switches;
if (MAX_FLEX_SWITCHES > 0) {
if (idx < MAX_FLEX_SWITCHES) {
auto channel = _flex_switches[idx];
SwitchHwPos ret = SWITCH_HW_UP;
if (channel != _INVALID_ADC_CH) {
auto offset = adcGetInputOffset(ADC_INPUT_FLEX);
uint16_t value = anaIn(channel + offset);
if (value > 3 * 512) {
ret = SWITCH_HW_DOWN;
} else if (value >= 512) {
ret = SWITCH_HW_MID;
}
}
return ret;
}
idx -= MAX_FLEX_SWITCHES;
}
if (idx < boardGetMaxFctSwitches()) {
return boardSwitchGetPosition(SWITCH_FUNCTION, idx);
}
return SWITCH_HW_UP;
}
static const char * const _flex_sw_canon_names[] = {
"FL1", "FL2", "FL3", "FL4"
};
static_assert(DIM(_flex_sw_canon_names) >= MAX_FLEX_SWITCHES,
"Missing canonical name for flex switches");
const char* switchGetName(uint8_t sw_idx)
{
auto idx = (int)sw_idx;
auto max_switches = boardGetMaxSwitches();
if (idx < max_switches) {
return boardSwitchGetName(SWITCH_PHYSICAL, idx);
}
idx -= max_switches;
if (MAX_FLEX_SWITCHES > 0) {
if (idx < MAX_FLEX_SWITCHES) {
return _flex_sw_canon_names[idx];
}
idx -= MAX_FLEX_SWITCHES;
}
if (idx < boardGetMaxFctSwitches()) {
return boardSwitchGetName(SWITCH_FUNCTION, idx);
}
return nullptr;
}
SwitchHwType switchGetHwType(uint8_t sw_idx)
{
auto idx = (int)sw_idx;
auto max_switches = boardGetMaxSwitches();
if (idx < max_switches) {
return boardSwitchGetType(SWITCH_PHYSICAL, idx);
}
idx -= max_switches;
if (idx < MAX_FLEX_SWITCHES) {
return SWITCH_HW_3POS;
}
return SWITCH_HW_2POS;
}
bool switchIsFlex(uint8_t idx)
{
auto max_switches = boardGetMaxSwitches();
return idx >= max_switches && idx < max_switches + MAX_FLEX_SWITCHES;
}
void switchConfigFlex_raw(uint8_t idx, int8_t channel)
{
if (!MAX_FLEX_SWITCHES || idx > MAX_FLEX_SWITCHES) return;
if ((int)channel >= (int)adcGetMaxInputs(ADC_INPUT_FLEX)) return;
auto& sw_channel = _flex_switches[idx];
if (channel < 0) {
sw_channel = _INVALID_ADC_CH;
} else if (POT_CONFIG((uint8_t)channel) == FLEX_SWITCH) {
sw_channel = (uint8_t)channel;
}
}
uint8_t switchGetFlexChannel(uint8_t idx)
{
return _flex_switches[idx];
}
void switchConfigFlex(uint8_t idx, int8_t channel)
{
auto max_switches = boardGetMaxSwitches();
if (idx < max_switches) return;
idx -= max_switches;
switchConfigFlex_raw(idx, channel);
}
int8_t switchGetFlexConfig_raw(uint8_t idx)
{
if (!MAX_FLEX_SWITCHES || idx > MAX_FLEX_SWITCHES) return _INVALID_ADC_CH;
return (int8_t)_flex_switches[idx];
}
int8_t switchGetFlexConfig(uint8_t idx)
{
auto max_switches = boardGetMaxSwitches();
if (idx < max_switches) return _INVALID_ADC_CH;
idx -= max_switches;
return switchGetFlexConfig_raw(idx);
}
bool switchIsFlexValid_raw(uint8_t sw_idx)
{
auto idx = (int)sw_idx;
return MAX_FLEX_SWITCHES > 0 && idx < MAX_FLEX_SWITCHES &&
_flex_switches[idx] != _INVALID_ADC_CH;
}
bool switchIsFlexValid(uint8_t idx)
{
auto max_switches = boardGetMaxSwitches();
if (idx < max_switches) return false;
idx -= max_switches;
return switchIsFlexValid_raw(idx);
}
static bool is_flex_input_available(uint8_t flex_idx, uint8_t channel)
{
for (int i = 0; i < MAX_FLEX_SWITCHES; i++) {
if (_flex_switches[i] == channel && (int)flex_idx != i)
return false;
}
return true;
}
bool switchIsFlexInputAvailable(uint8_t idx, uint8_t channel)
{
if (MAX_FLEX_SWITCHES == 0) return false;
if (POT_CONFIG(channel) != FLEX_SWITCH) return false;
auto phy_switches = boardGetMaxSwitches();
if (idx < phy_switches) return false;
return is_flex_input_available(idx - phy_switches, channel);
}
static void invalidate_flex_config(uint8_t flex_idx)
{
auto sw_idx = flex_idx + boardGetMaxSwitches();
swconfig_t mask = (swconfig_t)SWITCH_CONFIG_MASK(sw_idx);
g_eeGeneral.switchConfig &= ~mask;
_flex_switches[flex_idx] = _INVALID_ADC_CH;
}
void switchFixFlexConfig()
{
for (int i = 0; i < MAX_FLEX_SWITCHES; i++) {
auto channel = _flex_switches[i];
if (channel != _INVALID_ADC_CH && POT_CONFIG(channel) != FLEX_SWITCH) {
invalidate_flex_config(i);
}
}
}
+21 -1
View File
@@ -36,6 +36,11 @@ enum SwitchHwPos {
SWITCH_HW_DOWN,
};
enum SwitchCategory {
SWITCH_PHYSICAL=0,
SWITCH_FUNCTION,
};
#define SWITCH_HW_INVERTED 1
// init hardware for switches
@@ -44,7 +49,7 @@ void switchInit();
// returns the factory configuration for all switches
swconfig_t switchGetDefaultConfig();
// returns the maximum number of regular switches supported in hardware
// returns the maximum number of regular switches supported in hardware (customizable switches NOT included)
uint8_t switchGetMaxSwitches();
// returns the number of configured switches
@@ -63,6 +68,21 @@ struct switch_display_pos_t {
switch_display_pos_t switchGetDisplayPosition(uint8_t idx);
// Configure Flex switch (using unified switch index)
void switchConfigFlex(uint8_t idx, int8_t channel);
int8_t switchGetFlexConfig(uint8_t idx);
bool switchIsFlex(uint8_t idx);
bool switchIsFlexValid(uint8_t idx);
bool switchIsFlexInputAvailable(uint8_t idx, uint8_t channel);
void switchFixFlexConfig();
// "Raw" variants indexing only flex switches (used for storage)
void switchConfigFlex_raw(uint8_t idx, int8_t channel);
int8_t switchGetFlexConfig_raw(uint8_t idx);
bool switchIsFlexValid_raw(uint8_t idx);
// The functions bellow support regular as well as customizable switches.
//
// Regular switches are indexed from `0` to `switchGetMaxSwitches() - 1`