Skip to content

Commit 8ac7525

Browse files
refactor(radio): flatten per-FM logical switch state to single global context
Replace lswFm[MAX_FLIGHT_MODES] (one LogicalSwitchContext per FM per LS) with a flat lswCtx[MAX_LOGICAL_SWITCHES] array. The previous commit's test suite proved that per-FM state is fully redundant: timer/sticky/edge lastValue is always identical across FMs, the `state` bit is dead in non-active FMs, and delay/duration divergence is a bug (background timer expiry) not a feature. During FM fade transitions, the mixer evaluates mixes for multiple FMs in a single cycle. To preserve the existing behavior where fading-out FMs see the LS state from the moment of transition (smooth cross-fade for LS-conditioned mixes), a uint32_t bitmap array per FM captures the frozen LS state at transition time. getSwitch() reads from the frozen bitmap for non-active FMs and from live lswCtx[] for the active FM. This replaces ~2 KB of per-FM context with 9 × 8 = 72 bytes of bitmaps + 1 byte for mixerActiveFlightMode. Changes: - Flatten lswFm[MAX_FLIGHT_MODES] → lswCtx[MAX_LOGICAL_SWITCHES] - Remove LogicalSwitchesFlightModeContext wrapper struct - Hide LogicalSwitchContext struct and lswCtx[] as implementation details in switches.cpp; expose lswGetState/lswGetLastValue/lswSetState accessors - Collapse logicalSwitchesTimerTick from FM×LS nested loop to single LS loop - Replace logicalSwitchesCopyState with lswFreezeState bitmap snapshot called from mixer on FM transition - Add mixerActiveFlightMode to distinguish active FM from temporarily assigned mixerCurrentFlightMode during fade evaluation - Replace 16-test LswPerFmTest suite with 8-test LswTest suite verifying single-context behavior (delay/duration survive FM switches correctly) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bc17fb1 commit 8ac7525

File tree

7 files changed

+308
-286
lines changed

7 files changed

+308
-286
lines changed

radio/src/edgetx.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ enum PerOutMode {
265265
};
266266

267267
extern uint8_t mixerCurrentFlightMode;
268+
extern uint8_t mixerActiveFlightMode;
268269
extern uint8_t lastFlightMode;
269270
extern uint8_t flightModeTransitionLast;
270271

radio/src/mixer.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,7 @@ static inline bitfield_channels_t upper_channels_mask(uint16_t ch)
755755
}
756756

757757
uint8_t mixerCurrentFlightMode;
758+
uint8_t mixerActiveFlightMode;
758759

759760
void evalFlightModeMixes(uint8_t mode, uint8_t tick10ms)
760761
{
@@ -1165,7 +1166,7 @@ void evalMixes(uint8_t tick10ms)
11651166
fp_act[lastFlightMode] = 0;
11661167
fp_act[fm] = MAX_ACT;
11671168
}
1168-
logicalSwitchesCopyState(lastFlightMode, fm); // push last logical switches state from old to new flight mode
1169+
lswFreezeState(lastFlightMode);
11691170
}
11701171
lastFlightMode = fm;
11711172
}
@@ -1181,6 +1182,8 @@ void evalMixes(uint8_t tick10ms)
11811182
}
11821183
}
11831184

1185+
mixerActiveFlightMode = fm;
1186+
11841187
int32_t weight = 0;
11851188
if (flightModesFade) {
11861189
memclear(sum_chans512, sizeof(sum_chans512));

radio/src/model_arena.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,14 @@
8181
// ARENA_EXPOS ~22 (none — expos share the input pipeline)
8282
// ARENA_CURVES 4 4-8 (curveEnd[] pointer)
8383
// ARENA_POINTS 1 (none)
84-
// ARENA_LOGICAL_SW ~20 4 (LogicalSwitchContext) × MAX_FLIGHT_MODES = 36 B
84+
// ARENA_LOGICAL_SW ~20 4 (LogicalSwitchContext) per LS = 4 × count B
8585
// ARENA_CUSTOM_FN 16 4 (lastFunctionTime[]) per context × 2 contexts = 8 B
86-
// ARENA_FLIGHT_MODES ~28 2 (fp_act[]) + MAX_LOGICAL_SWITCHES×4 (lswFm[]) ≈ 258 B
86+
// ARENA_FLIGHT_MODES ~28 2 (fp_act[])
8787
// ARENA_GVAR_DATA 7 (none)
8888
// ARENA_GVAR_VALUES 2 (none)
8989
//
90-
// Largest runtime cost: MAX_FLIGHT_MODES × lswFm = FM × LS × 4 bytes.
91-
// With 9 FM × 64 LS × 4 = 2304 bytes. Raising FM to 16 → 4096 bytes.
90+
// lswCtx[] is a flat global array (not per-FM): LS × 4 bytes.
91+
// With 64 LS × 4 = 256 bytes (independent of FM count).
9292
//
9393
// MAX_OUTPUT_CHANNELS has no arena section but carries runtime cost:
9494
// chans[32×4] + channelOutputs[32×2] + ex_chans[32×2] + safetyCh[32×2]

radio/src/switches.cpp

Lines changed: 137 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,38 @@ enum LogicalSwitchContextState {
5555
SWITCH_ENABLE
5656
};
5757

58-
LogicalSwitchesFlightModeContext lswFm[MAX_FLIGHT_MODES];
58+
PACK(struct LogicalSwitchContext {
59+
uint8_t state:1;
60+
uint8_t timerState:2;
61+
uint8_t spare:1;
62+
uint8_t deltaTimer:4;
63+
uint8_t timer;
64+
int16_t lastValue;
65+
});
66+
67+
static LogicalSwitchContext lswCtx[MAX_LOGICAL_SWITCHES];
68+
69+
// Frozen LS state bitmaps for FM fade transitions.
70+
// When transitioning between flight modes with fade, the fading-out FM's
71+
// LS state is frozen at the moment of transition so that mix evaluation
72+
// during the fade sees the pre-transition LS values (preserving smooth
73+
// cross-fade behavior for LS-conditioned mixes).
74+
#define FROZEN_LS_WORDS ((MAX_LOGICAL_SWITCHES + 31) / 32)
75+
static uint32_t frozenLsState[MAX_FLIGHT_MODES][FROZEN_LS_WORDS];
76+
77+
static inline void frozenLsSet(uint8_t fm, uint8_t idx)
78+
{
79+
frozenLsState[fm][idx >> 5] |= (1u << (idx & 31));
80+
}
81+
82+
static inline bool frozenLsGet(uint8_t fm, uint8_t idx)
83+
{
84+
return (frozenLsState[fm][idx >> 5] >> (idx & 31)) & 1;
85+
}
86+
5987
CircularBuffer<uint8_t, 8> luaSetStickySwitchBuffer;
6088

61-
#define LS_LAST_VALUE(fm, idx) lswFm[fm].lsw[idx].lastValue
89+
#define LS_LAST_VALUE(idx) lswCtx[idx].lastValue
6290

6391
tmr10ms_t switchesMidposStart[MAX_SWITCHES];
6492
uint64_t switchesPos = 0;
@@ -475,23 +503,23 @@ PACK(typedef struct {
475503

476504
bool getLSStickyState(uint8_t idx)
477505
{
478-
return lswFm[mixerCurrentFlightMode].lsw[idx].lastValue & 1;
506+
return lswCtx[idx].lastValue & 1;
479507
}
480508

481509
void logicalSwitchesInit(bool force)
482510
{
483511
for (unsigned int idx=0; idx<MAX_LOGICAL_SWITCHES; idx++) {
484512
LogicalSwitchData * ls = lswAddress(idx);
485513
if (ls->func == LS_FUNC_STICKY && (force || ls->lsPersist)) {
486-
lswFm[mixerCurrentFlightMode].lsw[idx].lastValue = ls->lsState;
514+
lswCtx[idx].lastValue = ls->lsState;
487515
}
488516
}
489517
}
490518

491519
bool getLogicalSwitch(uint8_t idx)
492520
{
493521
LogicalSwitchData * ls = lswAddress(idx);
494-
LogicalSwitchContext &context = lswFm[mixerCurrentFlightMode].lsw[idx];
522+
LogicalSwitchContext &context = lswCtx[idx];
495523
bool result;
496524

497525
if (ls->func == LS_FUNC_NONE || (!ls->andsw.isNone() && !getSwitch(ls->andsw))) {
@@ -713,7 +741,10 @@ bool getSwitch(const SwitchRef& ref, uint8_t flags)
713741
}
714742

715743
case SWITCH_TYPE_LOGICAL:
716-
result = lswFm[mixerCurrentFlightMode].lsw[ref.index].state;
744+
if (mixerCurrentFlightMode != mixerActiveFlightMode)
745+
result = frozenLsGet(mixerCurrentFlightMode, ref.index);
746+
else
747+
result = lswCtx[ref.index].state;
717748
break;
718749

719750
case SWITCH_TYPE_FLIGHT_MODE: {
@@ -767,7 +798,7 @@ uint8_t getXPotPosition(uint8_t idx)
767798
void evalLogicalSwitches(bool isCurrentFlightmode)
768799
{
769800
for (unsigned int idx=0; idx<MAX_LOGICAL_SWITCHES; idx++) {
770-
LogicalSwitchContext & context = lswFm[mixerCurrentFlightMode].lsw[idx];
801+
LogicalSwitchContext & context = lswCtx[idx];
771802
bool result = getLogicalSwitch(idx);
772803
if (isCurrentFlightmode) {
773804
if (result) {
@@ -1044,91 +1075,86 @@ void logicalSwitchesTimerTick()
10441075
uint8_t s = msg >> 7;
10451076
LogicalSwitchData * ls = lswAddress(i);
10461077
if (ls->func == LS_FUNC_STICKY) {
1047-
for (uint8_t fm=0; fm<MAX_FLIGHT_MODES; fm++) {
1048-
ls_sticky_struct & lastValue = (ls_sticky_struct &)LS_LAST_VALUE(fm, i);
1049-
lastValue.state = s;
1050-
bool now;
1051-
if (s)
1052-
now = getSwitch(ls->v2.swtch);
1053-
else
1054-
now = getSwitch(ls->v1.swtch);
1055-
if (now)
1056-
lastValue.last |= 1;
1057-
else
1058-
lastValue.last &= ~1;
1059-
}
1078+
ls_sticky_struct & lastValue = (ls_sticky_struct &)LS_LAST_VALUE(i);
1079+
lastValue.state = s;
1080+
bool now;
1081+
if (s)
1082+
now = getSwitch(ls->v2.swtch);
1083+
else
1084+
now = getSwitch(ls->v1.swtch);
1085+
if (now)
1086+
lastValue.last |= 1;
1087+
else
1088+
lastValue.last &= ~1;
10601089
}
10611090
msg = luaSetStickySwitchBuffer.read();
10621091
}
10631092

10641093
// Update logical switches
1065-
for (uint8_t fm=0; fm<MAX_FLIGHT_MODES; fm++) {
1066-
for (uint8_t i=0; i<MAX_LOGICAL_SWITCHES; i++) {
1067-
LogicalSwitchData * ls = lswAddress(i);
1068-
if (ls->func == LS_FUNC_TIMER) {
1069-
int16_t *lastValue = &LS_LAST_VALUE(fm, i);
1070-
if (*lastValue == 0 || *lastValue == CS_LAST_VALUE_INIT) {
1071-
*lastValue = -lswTimerValue(ls->v1.value);
1072-
} else if (*lastValue < 0) {
1073-
if (++(*lastValue) == 0) *lastValue = lswTimerValue(ls->v2.value);
1074-
} else { // if (*lastValue > 0)
1075-
if (--(*lastValue) == 0) *lastValue = -lswTimerValue(ls->v1.value);
1076-
}
1077-
} else if (ls->func == LS_FUNC_STICKY) {
1078-
ls_sticky_struct & lastValue = (ls_sticky_struct &)LS_LAST_VALUE(fm, i);
1079-
bool before = lastValue.last & 0x01;
1080-
if (lastValue.state) {
1081-
if (!ls->v2.swtch.isNone()) { // only if used / source set
1082-
bool now = getSwitch(ls->v2.swtch);
1083-
if (now != before) {
1084-
lastValue.last ^= 1;
1085-
if (!before) {
1086-
lastValue.state = 0;
1087-
}
1094+
for (uint8_t i=0; i<MAX_LOGICAL_SWITCHES; i++) {
1095+
LogicalSwitchData * ls = lswAddress(i);
1096+
if (ls->func == LS_FUNC_TIMER) {
1097+
int16_t *lastValue = &LS_LAST_VALUE(i);
1098+
if (*lastValue == 0 || *lastValue == CS_LAST_VALUE_INIT) {
1099+
*lastValue = -lswTimerValue(ls->v1.value);
1100+
} else if (*lastValue < 0) {
1101+
if (++(*lastValue) == 0) *lastValue = lswTimerValue(ls->v2.value);
1102+
} else { // if (*lastValue > 0)
1103+
if (--(*lastValue) == 0) *lastValue = -lswTimerValue(ls->v1.value);
1104+
}
1105+
} else if (ls->func == LS_FUNC_STICKY) {
1106+
ls_sticky_struct & lastValue = (ls_sticky_struct &)LS_LAST_VALUE(i);
1107+
bool before = lastValue.last & 0x01;
1108+
if (lastValue.state) {
1109+
if (!ls->v2.swtch.isNone()) { // only if used / source set
1110+
bool now = getSwitch(ls->v2.swtch);
1111+
if (now != before) {
1112+
lastValue.last ^= 1;
1113+
if (!before) {
1114+
lastValue.state = 0;
10881115
}
1089-
}
1090-
}
1091-
else {
1092-
if (!ls->v1.swtch.isNone()) { // only if used / source set
1093-
bool now = getSwitch(ls->v1.swtch);
1094-
if (before != now) {
1095-
lastValue.last ^= 1;
1096-
if (!before) {
1097-
lastValue.state = 1;
1098-
}
1116+
}
1117+
}
1118+
}
1119+
else {
1120+
if (!ls->v1.swtch.isNone()) { // only if used / source set
1121+
bool now = getSwitch(ls->v1.swtch);
1122+
if (before != now) {
1123+
lastValue.last ^= 1;
1124+
if (!before) {
1125+
lastValue.state = 1;
10991126
}
1100-
}
1101-
}
1102-
} else if (ls->func == LS_FUNC_EDGE) {
1103-
ls_stay_struct & lastValue = (ls_stay_struct &)LS_LAST_VALUE(fm, i);
1104-
// if this ls was reset by the logicalSwitchesReset() the lastValue will be set to CS_LAST_VALUE_INIT(0x8000)
1105-
// when it is unpacked into ls_stay_struct the lastValue.duration will have a value of 0x4000
1106-
// this will produce an instant true for edge logical switch if the second parameter is big enough.
1107-
// So we reset it here.
1108-
if (LS_LAST_VALUE(fm, i) == CS_LAST_VALUE_INIT) {
1109-
lastValue.duration = 0;
1110-
}
1111-
lastValue.state = false;
1112-
bool state = getSwitch(ls->v1.swtch);
1113-
if (state) {
1114-
if (ls->v3 == -1 && lastValue.duration == lswTimerValue(ls->v2.value))
1115-
lastValue.state = true;
1116-
if (lastValue.duration < 1000)
1117-
lastValue.duration++;
1118-
}
1119-
else {
1120-
if (lastValue.duration > lswTimerValue(ls->v2.value) && (ls->v3 == 0 || lastValue.duration <= lswTimerValue(ls->v2.value+ls->v3)))
1121-
lastValue.state = true;
1122-
lastValue.duration = 0;
1123-
}
1127+
}
1128+
}
11241129
}
1125-
1126-
// decrement delay/duration timer
1127-
LogicalSwitchContext &context = lswFm[fm].lsw[i];
1128-
if (context.timer) {
1129-
context.timer--;
1130+
} else if (ls->func == LS_FUNC_EDGE) {
1131+
ls_stay_struct & lastValue = (ls_stay_struct &)LS_LAST_VALUE(i);
1132+
// if this ls was reset by the logicalSwitchesReset() the lastValue will be set to CS_LAST_VALUE_INIT(0x8000)
1133+
// when it is unpacked into ls_stay_struct the lastValue.duration will have a value of 0x4000
1134+
// this will produce an instant true for edge logical switch if the second parameter is big enough.
1135+
// So we reset it here.
1136+
if (LS_LAST_VALUE(i) == CS_LAST_VALUE_INIT) {
1137+
lastValue.duration = 0;
1138+
}
1139+
lastValue.state = false;
1140+
bool state = getSwitch(ls->v1.swtch);
1141+
if (state) {
1142+
if (ls->v3 == -1 && lastValue.duration == lswTimerValue(ls->v2.value))
1143+
lastValue.state = true;
1144+
if (lastValue.duration < 1000)
1145+
lastValue.duration++;
1146+
}
1147+
else {
1148+
if (lastValue.duration > lswTimerValue(ls->v2.value) && (ls->v3 == 0 || lastValue.duration <= lswTimerValue(ls->v2.value+ls->v3)))
1149+
lastValue.state = true;
1150+
lastValue.duration = 0;
11301151
}
11311152
}
1153+
1154+
// decrement delay/duration timer
1155+
if (lswCtx[i].timer) {
1156+
lswCtx[i].timer--;
1157+
}
11321158
}
11331159
}
11341160

@@ -1189,28 +1215,48 @@ int16_t lswTimerValue(delayval_t val)
11891215

11901216
void logicalSwitchesReset()
11911217
{
1192-
memset(lswFm, 0, sizeof(lswFm));
1218+
memset(lswCtx, 0, sizeof(lswCtx));
11931219

1194-
for (uint8_t fm=0; fm<MAX_FLIGHT_MODES; fm++) {
1195-
for (uint8_t i=0; i<MAX_LOGICAL_SWITCHES; i++) {
1196-
LS_LAST_VALUE(fm, i) = CS_LAST_VALUE_INIT;
1197-
}
1220+
for (uint8_t i=0; i<MAX_LOGICAL_SWITCHES; i++) {
1221+
LS_LAST_VALUE(i) = CS_LAST_VALUE_INIT;
11981222
}
11991223

12001224
luaSetStickySwitchBuffer.clear();
12011225
}
12021226

1227+
void lswFreezeState(uint8_t fm)
1228+
{
1229+
memset(frozenLsState[fm], 0, sizeof(frozenLsState[fm]));
1230+
for (uint8_t i = 0; i < MAX_LOGICAL_SWITCHES; i++) {
1231+
if (lswCtx[i].state)
1232+
frozenLsSet(fm, i);
1233+
}
1234+
}
1235+
1236+
bool lswGetState(uint8_t idx)
1237+
{
1238+
return lswCtx[idx].state;
1239+
}
1240+
1241+
int16_t lswGetLastValue(uint8_t idx)
1242+
{
1243+
return lswCtx[idx].lastValue;
1244+
}
1245+
1246+
void lswSetState(uint8_t idx, uint8_t state, uint8_t timer, int16_t lastValue)
1247+
{
1248+
lswCtx[idx].state = state;
1249+
lswCtx[idx].timer = timer;
1250+
lswCtx[idx].lastValue = lastValue;
1251+
}
1252+
12031253
getvalue_t convertLswTelemValue(LogicalSwitchData * ls)
12041254
{
12051255
getvalue_t val;
12061256
val = convert16bitsTelemValue(ls->v1.source.index + 1, ls->v2.value);
12071257
return val;
12081258
}
12091259

1210-
void logicalSwitchesCopyState(uint8_t src, uint8_t dst)
1211-
{
1212-
lswFm[dst] = lswFm[src];
1213-
}
12141260

12151261
void setAllPreflightSwitchStates()
12161262
{

0 commit comments

Comments
 (0)