Skip to content

Commit 84c0d3f

Browse files
authored
Unit test fuel & ignition channel cut code (speeduino#1376)
* Add calculateFuelIgnitionChannelCut() + unit tests * Keep tests DRY * Compute oil protection end time in millis, instead of calling div100() on every pass. * Add rolling cut test cases * Capture the scheduler channel cut state in statuses. This feeds the prior state into calculateFuelIgnitionChannelCut(), which allows the rolling cut code to set pending ignition channels. * Add pending ignition cut test * Remove redundant tests, consolidate others * Enhance checkOilPressureLimit tests * Enhance checkBoostLimit test * Enhanced checkAFRLimit() tests * Deterministic rolling cut tests - requires injecting RNG * afrProtectCount -> afrProtectedActivateTime. Since this is now a non-zero future time, we can remove afrProtectCountEnabled * Enhanced checkAFRLimit() tests * Force calculateFuelIgnitionChannelCut() inline
1 parent 9091a4f commit 84c0d3f

File tree

5 files changed

+789
-547
lines changed

5 files changed

+789
-547
lines changed

speeduino/engineProtection.cpp

Lines changed: 190 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@
44
#include "units.h"
55
#include "unit_testing.h"
66
#include "preprocessor.h"
7+
#include "decoders.h"
8+
#include "units.h"
9+
#include "preprocessor.h"
710

8-
TESTABLE_STATIC uint8_t oilProtStartTime = 0;
11+
TESTABLE_STATIC uint32_t oilProtEndTime;
912
TESTABLE_CONSTEXPR table2D_u8_u8_4 oilPressureProtectTable(&configPage10.oilPressureProtRPM, &configPage10.oilPressureProtMins);
1013
TESTABLE_CONSTEXPR table2D_u8_u8_6 coolantProtectTable(&configPage9.coolantProtTemp, &configPage9.coolantProtRPM);
1114

1215
/* AFR protection state moved to file scope so unit tests can control/reset it */
1316
TESTABLE_STATIC bool checkAFRLimitActive = false;
14-
TESTABLE_STATIC bool afrProtectCountEnabled = false;
15-
TESTABLE_STATIC unsigned long afrProtectCount = 0;
17+
TESTABLE_STATIC unsigned long afrProtectedActivateTime = 0;
1618

1719
TESTABLE_INLINE_STATIC bool checkOilPressureLimit(const statuses &current, const config6 &page6, const config10 &page10, uint32_t currMillis)
1820
{
@@ -26,16 +28,13 @@ TESTABLE_INLINE_STATIC bool checkOilPressureLimit(const statuses &current, const
2628
if(current.oilPressure < oilLimit)
2729
{
2830
//Check if this is the first time we've been below the limit
29-
if(oilProtStartTime == 0U) { oilProtStartTime = div100(currMillis); }
31+
if(oilProtEndTime == 0U) { oilProtEndTime = currMillis + TIME_TEN_MILLIS.toUser(page10.oilPressureProtTime); }
3032
/* Check if countdown has reached its target, if so then instruct to cut */
31-
if( (uint8_t(div100(currMillis)) >= (uint16_t(oilProtStartTime + page10.oilPressureProtTime)) ) || (current.engineProtectOil) )
32-
{
33-
engineProtectOil = true;
34-
}
33+
engineProtectOil = (currMillis >= oilProtEndTime) || (current.engineProtectOil);
3534
}
3635
else
3736
{
38-
oilProtStartTime = 0; //Reset the timer
37+
oilProtEndTime = 0; //Reset the timer
3938
}
4039
}
4140

@@ -49,34 +48,35 @@ TESTABLE_INLINE_STATIC bool checkBoostLimit(const statuses &current, const confi
4948
&& (current.MAP > ((long)page6.boostLimit * 2L));
5049
}
5150

52-
static inline uint8_t getAfrO2Limit(const statuses &current, const config9 &page9)
51+
static inline bool canApplyAfrLimit(const config6 &page6, const config9 &page9)
52+
{
53+
return (page6.engineProtectType != PROTECT_CUT_OFF)
54+
&& (page9.afrProtectEnabled != AFR_PROTECT_OFF)
55+
&& (page6.egoType == EGO_TYPE_WIDE);
56+
}
57+
58+
static inline uint16_t getAfrO2Limit(const statuses &current, const config9 &page9)
5359
{
5460
if (page9.afrProtectEnabled==AFR_PROTECT_FIXED) {
5561
return page9.afrProtectDeviation;
56-
} if (page9.afrProtectEnabled==AFR_PROTECT_TABLE) {
57-
return current.afrTarget + page9.afrProtectDeviation;
58-
} else {
59-
return 0U;
62+
}
63+
if (page9.afrProtectEnabled==AFR_PROTECT_TABLE) {
64+
return current.afrTarget + (uint16_t)page9.afrProtectDeviation;
6065
}
66+
67+
return UINT16_MAX;
6168
}
6269

63-
static inline bool afrLimitAfrCondition(const statuses &current, const config9 &page9)
70+
static inline bool isAfrLimitCondtionActive(const statuses &current, const config9 &page9)
6471
{
65-
/*
66-
Depending on selected mode, this could either be fixed AFR value or a
67-
value set to be the maximum deviation from AFR target table.
68-
69-
1 = fixed value mode, 2 = target table mode
70-
*/
71-
return (page9.afrProtectEnabled!=AFR_PROTECT_OFF)
72-
&& (current.O2 >=getAfrO2Limit(current, page9));
72+
return (current.MAP >= (long)(page9.afrProtectMinMAP * UINT16_C(2)))
73+
&& (current.RPMdiv100 >= page9.afrProtectMinRPM)
74+
&& (current.TPS >= page9.afrProtectMinTPS)
75+
&& (current.O2 >= getAfrO2Limit(current, page9));
7376
}
7477

7578
TESTABLE_INLINE_STATIC bool checkAFRLimit(const statuses &current, const config6 &page6, const config9 &page9, uint32_t currMillis)
7679
{
77-
static constexpr char X2_MULTIPLIER = 2;
78-
static constexpr char X100_MULTIPLIER = 100;
79-
8080
/*
8181
To use this function, a wideband sensor is required.
8282
@@ -101,54 +101,39 @@ TESTABLE_INLINE_STATIC bool checkAFRLimit(const statuses &current, const config6
101101
For reactivation, the following condition has to be met:
102102
- TPS below x %
103103
*/
104-
105-
/*
106-
Do 3 checks here;
107-
- whether engine protection is enabled
108-
- whether AFR protection is enabled
109-
- whether wideband sensor is used
110-
*/
111-
if((page6.engineProtectType != PROTECT_CUT_OFF) && (page9.afrProtectEnabled!=AFR_PROTECT_OFF) && (page6.egoType == EGO_TYPE_WIDE)) {
112-
/* Conditions */
113-
bool mapCondition = (current.MAP >= (page9.afrProtectMinMAP * X2_MULTIPLIER));
114-
bool rpmCondition = (current.RPMdiv100 >= page9.afrProtectMinRPM);
115-
bool tpsCondition = (current.TPS >= page9.afrProtectMinTPS);
116-
bool afrCondition = afrLimitAfrCondition(current, page9);
117-
118-
/* Check if conditions above are fulfilled */
119-
if(mapCondition && rpmCondition && tpsCondition && afrCondition)
104+
if ( canApplyAfrLimit(page6, page9) )
105+
{
106+
if (isAfrLimitCondtionActive(current, page9))
120107
{
121-
/* All conditions fulfilled - start counter for 'protection delay' */
122-
if(!afrProtectCountEnabled)
108+
// All conditions fulfilled - start counter for 'protection delay'
109+
if(afrProtectedActivateTime==0U)
123110
{
124-
afrProtectCountEnabled = true;
125-
afrProtectCount = currMillis;
111+
afrProtectedActivateTime = currMillis + (page9.afrProtectCutTime * UINT16_C(100));
126112
}
127113

128-
/* Check if countdown has reached its target, if so then instruct to cut */
129-
if(currMillis >= (afrProtectCount + (page9.afrProtectCutTime * X100_MULTIPLIER)))
130-
{
131-
checkAFRLimitActive = true;
132-
}
114+
// Check if countdown has reached its target, if so then instruct to cut
115+
checkAFRLimitActive = currMillis >= afrProtectedActivateTime;
133116
}
134117
else
135118
{
136-
/* Conditions have presumably changed - deactivate and reset counter */
137-
if(afrProtectCountEnabled)
138-
{
139-
afrProtectCountEnabled = false;
140-
afrProtectCount = 0;
141-
}
119+
// NOTE: we deliberately do not reset checkAFRLimitActive here
120+
// Once AFR protection is in effect, user must reduce throttle
121+
// to below the reactivation limit to reset manually (below)
122+
123+
// Do nothing
142124
}
143125

144-
/* Check if condition for reactivation is fulfilled */
145-
if(checkAFRLimitActive && (current.TPS <= page9.afrProtectReactivationTPS))
126+
// Check if condition for reactivation is fulfilled
127+
if(current.TPS <= page9.afrProtectReactivationTPS)
146128
{
147129
checkAFRLimitActive = false;
148-
afrProtectCountEnabled = false;
130+
afrProtectedActivateTime = 0U;
149131
}
150132
}
151-
133+
else
134+
{
135+
checkAFRLimitActive = false;
136+
}
152137
return checkAFRLimitActive;
153138
}
154139

@@ -197,3 +182,147 @@ uint8_t checkRevLimit(statuses &current, const config4 &page4, const config6 &pa
197182

198183
return currentLimitRPM;
199184
}
185+
186+
TESTABLE_STATIC uint32_t rollingCutLastRev = 0; /**< Tracks whether we're on the same or a different rev for the rolling cut */
187+
TESTABLE_CONSTEXPR table2D_i8_u8_4 rollingCutTable(&configPage15.rollingProtRPMDelta, &configPage15.rollingProtCutPercent);
188+
189+
// Test-hookable RNG for rolling cut (defaults to existing random1to100)
190+
TESTABLE_STATIC uint8_t (*rollingCutRandFunc)(void) = random1to100;
191+
192+
BEGIN_LTO_ALWAYS_INLINE(statuses::scheduler_cut_t) calculateFuelIgnitionChannelCut(statuses &current, const config2 &page2, const config4 &page4, const config6 &page6, const config9 &page9, const config10 &page10)
193+
{
194+
if (getDecoderStatus().syncStatus==SyncStatus::None)
195+
{
196+
return { 0x0, 0x0, 0x0 };
197+
}
198+
199+
statuses::scheduler_cut_t cutState = current.schedulerCutState;
200+
201+
//Check for any of the engine protections or rev limiters being turned on
202+
uint16_t maxAllowedRPM = checkRevLimit(current, page4, page6, page9); //The maximum RPM allowed by all the potential limiters (Engine protection, 2-step, flat shift etc). Divided by 100. `checkRevLimit()` returns the current maximum RPM allow (divided by 100) based on either the fixed hard limit or the current coolant temp
203+
//Check each of the functions that has an RPM limit. Update the max allowed RPM if the function is active and has a lower RPM than already set
204+
if( checkEngineProtect(current, page4, page6, page9, page10) && (page4.engineProtectMaxRPM < maxAllowedRPM)) { maxAllowedRPM = page4.engineProtectMaxRPM; }
205+
if ( (current.launchingHard == true) && (page6.lnchHardLim < maxAllowedRPM) ) { maxAllowedRPM = page6.lnchHardLim; }
206+
maxAllowedRPM = maxAllowedRPM * 100U; //All of the above limits are divided by 100, convert back to RPM
207+
if ( (current.flatShiftingHard == true) && (current.clutchEngagedRPM < maxAllowedRPM) ) { maxAllowedRPM = current.clutchEngagedRPM; } //Flat shifting is a special case as the RPM limit is based on when the clutch was engaged. It is not divided by 100 as it is set with the actual RPM
208+
209+
if(current.RPM >= maxAllowedRPM)
210+
{
211+
current.hardLimitActive = true;
212+
}
213+
else if(current.hardLimitActive)
214+
{
215+
current.hardLimitActive = false;
216+
}
217+
218+
if( (page2.hardCutType == HARD_CUT_FULL) && current.hardLimitActive)
219+
{
220+
//Full hard cut turns outputs off completely.
221+
switch(page6.engineProtectType)
222+
{
223+
case PROTECT_CUT_OFF:
224+
//Make sure all channels are turned on
225+
cutState.ignitionChannels = 0xFF;
226+
cutState.fuelChannels = 0xFF;
227+
resetEngineProtect(current);
228+
break;
229+
case PROTECT_CUT_IGN:
230+
cutState.ignitionChannels = 0;
231+
break;
232+
case PROTECT_CUT_FUEL:
233+
cutState.fuelChannels = 0;
234+
break;
235+
case PROTECT_CUT_BOTH:
236+
cutState.ignitionChannels = 0;
237+
cutState.fuelChannels = 0;
238+
break;
239+
default:
240+
cutState.ignitionChannels = 0;
241+
cutState.fuelChannels = 0;
242+
break;
243+
}
244+
} //Hard cut check
245+
else if( (page2.hardCutType == HARD_CUT_ROLLING) && (current.RPM > (maxAllowedRPM + (rollingCutTable.axis[0] * 10))) ) //Limit for rolling is the max allowed RPM minus the lowest value in the delta table (Delta values are negative!)
246+
{
247+
uint8_t revolutionsToCut = 1;
248+
if(page2.strokes == FOUR_STROKE) { revolutionsToCut *= 2; } //4 stroke needs to cut for at least 2 revolutions
249+
if( (page4.sparkMode != IGN_MODE_SEQUENTIAL) || (page2.injLayout != INJ_SEQUENTIAL) ) { revolutionsToCut *= 2; } //4 stroke and non-sequential will cut for 4 revolutions minimum. This is to ensure no half fuel ignition cycles take place
250+
251+
if(rollingCutLastRev == 0) { rollingCutLastRev = current.startRevolutions; } //First time check
252+
if ( (current.startRevolutions >= (rollingCutLastRev + revolutionsToCut)) || (current.RPM > maxAllowedRPM) ) //If current RPM is over the max allowed RPM always cut, otherwise check if the required number of revolutions have passed since the last cut
253+
{
254+
uint8_t cutPercent = 0;
255+
int16_t rpmDelta = current.RPM - maxAllowedRPM;
256+
if(rpmDelta >= 0) { cutPercent = 100; } //If the current RPM is over the max allowed RPM then cut is full (100%)
257+
else { cutPercent = table2D_getValue(&rollingCutTable, (int8_t)(rpmDelta / 10) ); } //
258+
259+
260+
for(uint8_t x=0; x<max(current.maxIgnOutputs, current.maxInjOutputs); x++)
261+
{
262+
if( (cutPercent == 100) || (rollingCutRandFunc() < cutPercent) )
263+
{
264+
switch(page6.engineProtectType)
265+
{
266+
case PROTECT_CUT_OFF:
267+
//Make sure all channels are turned on
268+
cutState.ignitionChannels = 0xFF;
269+
cutState.fuelChannels = 0xFF;
270+
break;
271+
case PROTECT_CUT_IGN:
272+
BIT_CLEAR(cutState.ignitionChannels, x); //Turn off this ignition channel
273+
break;
274+
case PROTECT_CUT_FUEL:
275+
BIT_CLEAR(cutState.fuelChannels, x); //Turn off this fuel channel
276+
break;
277+
case PROTECT_CUT_BOTH:
278+
BIT_CLEAR(cutState.ignitionChannels, x); //Turn off this ignition channel
279+
BIT_CLEAR(cutState.fuelChannels, x); //Turn off this fuel channel
280+
break;
281+
default:
282+
BIT_CLEAR(cutState.ignitionChannels, x); //Turn off this ignition channel
283+
BIT_CLEAR(cutState.fuelChannels, x); //Turn off this fuel channel
284+
break;
285+
}
286+
}
287+
else
288+
{
289+
//Turn fuel and ignition channels on
290+
291+
//Special case for non-sequential, 4-stroke where both fuel and ignition are cut. The ignition pulses should wait 1 cycle after the fuel channels are turned back on before firing again
292+
if( (revolutionsToCut == 4) && //4 stroke and non-sequential
293+
(BIT_CHECK(cutState.fuelChannels, x) == false) && //Fuel on this channel is currently off, meaning it is the first revolution after a cut
294+
(page6.engineProtectType == PROTECT_CUT_BOTH) //Both fuel and ignition are cut
295+
)
296+
{ BIT_SET(cutState.ignitionChannelsPending, x); } //Set this ignition channel as pending
297+
else { BIT_SET(cutState.ignitionChannels, x); } //Turn on this ignition channel
298+
299+
300+
BIT_SET(cutState.fuelChannels, x); //Turn on this fuel channel
301+
}
302+
}
303+
rollingCutLastRev = current.startRevolutions;
304+
}
305+
306+
//Check whether there are any ignition channels that are waiting for injection pulses to occur before being turned back on. This can only occur when at least 2 revolutions have taken place since the fuel was turned back on
307+
//Note that ignitionChannelsPending can only be >0 on 4 stroke, non-sequential fuel when protect type is Both
308+
if( (cutState.ignitionChannelsPending > 0) && (current.startRevolutions >= (rollingCutLastRev + 2)) )
309+
{
310+
cutState.ignitionChannels = cutState.fuelChannels;
311+
cutState.ignitionChannelsPending = 0;
312+
}
313+
} //Rolling cut check
314+
else
315+
{
316+
resetEngineProtect(current);
317+
//No engine protection active, so turn all the channels on
318+
if(current.startRevolutions >= page4.StgCycles)
319+
{
320+
//Enable the fuel and ignition, assuming staging revolutions are complete
321+
cutState.ignitionChannels = 0xff;
322+
cutState.fuelChannels = 0xff;
323+
}
324+
}
325+
326+
return cutState;
327+
}
328+
END_LTO_INLINE()

speeduino/engineProtection.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@
55

66
bool checkEngineProtect(statuses &current, const config4 &page4, const config6 &page6, const config9 &page9, const config10 &page10);
77

8-
uint8_t checkRevLimit(statuses &current, const config4 &page4, const config6 &page6, const config9 &page9);
8+
uint8_t checkRevLimit(statuses &current, const config4 &page4, const config6 &page6, const config9 &page9);
9+
10+
statuses::scheduler_cut_t calculateFuelIgnitionChannelCut(statuses &current, const config2 &page2, const config4 &page4, const config6 &page6, const config9 &page9, const config10 &page10);

0 commit comments

Comments
 (0)