Compare commits

..

3 Commits

Author SHA1 Message Date
Matthew Kennedy 51a23364e2
Odd cylinder engine wasted spark (#479)
* allow wasted spark spin-up

* allow firing without phase sync on odd cyl engines

* support odd cyl wasted spark

* changelog

* unit test it

* wow, it's easy to support odd-fire too
2024-08-31 02:22:31 -07:00
Matthew Kennedy 397e3dcd04 hoist dwellMs/angle/sparkangle 2024-08-31 00:44:21 -07:00
Matthew Kennedy 3fc42222cc hoist dwell check out of loop 2024-08-31 00:37:27 -07:00
6 changed files with 126 additions and 25 deletions

View File

@ -43,6 +43,7 @@ or
- TunerStudio UI improvements (#436, etc) - TunerStudio UI improvements (#436, etc)
- Dropdown selector for popular gearbox ratios (#358, thank you @alrijleh and @nmschulte!) - Dropdown selector for popular gearbox ratios (#358, thank you @alrijleh and @nmschulte!)
- Add two more aux linear sensors #476 - Add two more aux linear sensors #476
- Support wasted spark on odd cylinder count 4-stroke engines. Improves startup and allows running without a cam sensor!
### Fixed ### Fixed
- Improve performance with Lua CAN reception of a high volume of frames - Improve performance with Lua CAN reception of a high volume of frames

View File

@ -24,6 +24,8 @@ public:
*/ */
angle_t engineCycle; angle_t engineCycle;
bool useOddFireWastedSpark = false;
/** /**
* this is based on sensorChartMode and sensorSnifferRpmThreshold settings * this is based on sensorChartMode and sensorSnifferRpmThreshold settings
*/ */

View File

@ -104,6 +104,14 @@ static void prepareCylinderIgnitionSchedule(angle_t dwellAngleDuration, floatms_
event->sparkAngle = sparkAngle; event->sparkAngle = sparkAngle;
auto ignitionMode = getCurrentIgnitionMode(); auto ignitionMode = getCurrentIgnitionMode();
// On an odd cylinder (or odd fire) wasted spark engine, map outputs as if in sequential.
// During actual scheduling, the events just get scheduled every 360 deg instead
// of every 720 deg.
if (ignitionMode == IM_WASTED_SPARK && engine->engineState.useOddFireWastedSpark) {
ignitionMode = IM_INDIVIDUAL_COILS;
}
engine->outputChannels.currentIgnitionMode = static_cast<uint8_t>(ignitionMode); engine->outputChannels.currentIgnitionMode = static_cast<uint8_t>(ignitionMode);
const int index = getIgnitionPinForIndex(event->cylinderIndex, ignitionMode); const int index = getIgnitionPinForIndex(event->cylinderIndex, ignitionMode);
@ -309,20 +317,9 @@ void turnSparkPinHigh(IgnitionEvent *event) {
} }
static void scheduleSparkEvent(bool limitedSpark, IgnitionEvent *event, static void scheduleSparkEvent(bool limitedSpark, IgnitionEvent *event,
int rpm, efitick_t edgeTimestamp, float currentPhase, float nextPhase) { int rpm, float dwellMs, float dwellAngle, float sparkAngle, efitick_t edgeTimestamp, float currentPhase, float nextPhase) {
angle_t sparkAngle = event->sparkAngle; float angleOffset = dwellAngle - currentPhase;
const floatms_t dwellMs = engine->ignitionState.sparkDwell;
if (std::isnan(dwellMs) || dwellMs <= 0) {
warning(ObdCode::CUSTOM_DWELL, "invalid dwell to handle: %.2f at %d", dwellMs, rpm);
return;
}
if (std::isnan(sparkAngle)) {
warning(ObdCode::CUSTOM_ADVANCE_SPARK, "NaN advance");
return;
}
float angleOffset = event->dwellAngle - currentPhase;
if (angleOffset < 0) { if (angleOffset < 0) {
angleOffset += engine->engineState.engineCycle; angleOffset += engine->engineState.engineCycle;
} }
@ -457,6 +454,12 @@ void onTriggerEventSparkLogic(int rpm, efitick_t edgeTimestamp, float currentPha
engine->outputChannels.sparkCutReason = (int8_t)limitedSparkState.reason; engine->outputChannels.sparkCutReason = (int8_t)limitedSparkState.reason;
bool limitedSpark = !limitedSparkState.value; bool limitedSpark = !limitedSparkState.value;
const floatms_t dwellMs = engine->ignitionState.sparkDwell;
if (std::isnan(dwellMs) || dwellMs <= 0) {
warning(ObdCode::CUSTOM_DWELL, "invalid dwell to handle: %.2f at %d", dwellMs, rpm);
return;
}
if (!engine->ignitionEvents.isReady) { if (!engine->ignitionEvents.isReady) {
prepareIgnitionSchedule(); prepareIgnitionSchedule();
} }
@ -467,13 +470,48 @@ void onTriggerEventSparkLogic(int rpm, efitick_t edgeTimestamp, float currentPha
* See initializeIgnitionActions() * See initializeIgnitionActions()
*/ */
// Only apply odd cylinder count wasted logic if:
// - odd cyl count
// - current mode is wasted spark
// - four stroke
bool enableOddCylinderWastedSpark =
engine->engineState.useOddFireWastedSpark
&& getCurrentIgnitionMode() == IM_WASTED_SPARK;
// scheduleSimpleMsg(&logger, "eventId spark ", eventIndex);
if (engine->ignitionEvents.isReady) { if (engine->ignitionEvents.isReady) {
for (size_t i = 0; i < engineConfiguration->cylindersCount; i++) { for (size_t i = 0; i < engineConfiguration->cylindersCount; i++) {
IgnitionEvent *event = &engine->ignitionEvents.elements[i]; IgnitionEvent *event = &engine->ignitionEvents.elements[i];
if (!isPhaseInRange(event->dwellAngle, currentPhase, nextPhase)) { angle_t dwellAngle = event->dwellAngle;
angle_t sparkAngle = event->sparkAngle;
if (std::isnan(sparkAngle)) {
warning(ObdCode::CUSTOM_ADVANCE_SPARK, "NaN advance");
continue;
}
bool isOddCylWastedEvent = false;
if (enableOddCylinderWastedSpark) {
auto dwellAngleWastedEvent = dwellAngle + 360;
if (dwellAngleWastedEvent > 720) {
dwellAngleWastedEvent -= 720;
}
// Check whether this event hits 360 degrees out from now (ie, wasted spark),
// and if so, twiddle the dwell and spark angles so it happens now instead
isOddCylWastedEvent = isPhaseInRange(dwellAngleWastedEvent, currentPhase, nextPhase);
if (isOddCylWastedEvent) {
dwellAngle = dwellAngleWastedEvent;
sparkAngle += 360;
if (sparkAngle > 720) {
sparkAngle -= 720;
}
}
}
if (!isOddCylWastedEvent && !isPhaseInRange(dwellAngle, currentPhase, nextPhase)) {
continue; continue;
} }
@ -498,7 +536,7 @@ void onTriggerEventSparkLogic(int rpm, efitick_t edgeTimestamp, float currentPha
engine->ALSsoftSparkLimiter.setTargetSkipRatio(ALSSkipRatio); engine->ALSsoftSparkLimiter.setTargetSkipRatio(ALSSkipRatio);
#endif // EFI_ANTILAG_SYSTEM #endif // EFI_ANTILAG_SYSTEM
scheduleSparkEvent(limitedSpark, event, rpm, edgeTimestamp, currentPhase, nextPhase); scheduleSparkEvent(limitedSpark, event, rpm, dwellMs, dwellAngle, sparkAngle, edgeTimestamp, currentPhase, nextPhase);
} }
} }
} }

View File

@ -19,12 +19,6 @@ static bool noFiringUntilVvtSync(vvt_mode_e vvtMode) {
return true; return true;
} }
// Odd cylinder count engines don't work properly with wasted spark, so wait for full sync (so that sequential works)
// See https://github.com/rusefi/rusefi/issues/4195 for the issue to properly support this case
if (engineConfiguration->cylindersCount > 1 && engineConfiguration->cylindersCount % 2 == 1) {
return true;
}
// Symmetrical crank modes require cam sync before firing // Symmetrical crank modes require cam sync before firing
// non-symmetrical cranks can use faster spin-up mode (firing in wasted/batch before VVT sync) // non-symmetrical cranks can use faster spin-up mode (firing in wasted/batch before VVT sync)
// Examples include Nissan MR/VQ, Miata NB, etc // Examples include Nissan MR/VQ, Miata NB, etc

View File

@ -386,8 +386,7 @@ ignition_mode_e getCurrentIgnitionMode() {
ignition_mode_e ignitionMode = engineConfiguration->ignitionMode; ignition_mode_e ignitionMode = engineConfiguration->ignitionMode;
#if EFI_SHAFT_POSITION_INPUT #if EFI_SHAFT_POSITION_INPUT
// In spin-up cranking mode we don't have full phase sync info yet, so wasted spark mode is better // In spin-up cranking mode we don't have full phase sync info yet, so wasted spark mode is better
// However, only do this on even cylinder count engines: odd cyl count doesn't fire at all if (ignitionMode == IM_INDIVIDUAL_COILS) {
if (ignitionMode == IM_INDIVIDUAL_COILS && (engineConfiguration->cylindersCount % 2 == 0)) {
bool missingPhaseInfoForSequential = bool missingPhaseInfoForSequential =
!engine->triggerCentral.triggerState.hasSynchronizedPhase(); !engine->triggerCentral.triggerState.hasSynchronizedPhase();
@ -405,7 +404,20 @@ ignition_mode_e getCurrentIgnitionMode() {
* This heavy method is only invoked in case of a configuration change or initialization. * This heavy method is only invoked in case of a configuration change or initialization.
*/ */
void prepareOutputSignals() { void prepareOutputSignals() {
getEngineState()->engineCycle = getEngineCycle(getEngineRotationState()->getOperationMode()); auto operationMode = getEngineRotationState()->getOperationMode();
getEngineState()->engineCycle = getEngineCycle(operationMode);
bool isOddFire = false;
for (size_t i = 0; i < engineConfiguration->cylindersCount; i++) {
if (engineConfiguration->timing_offset_cylinder[i] != 0) {
isOddFire = true;
break;
}
}
// Use odd fire wasted spark logic if not two stroke, and an odd fire or odd cylinder # engine
getEngineState()->useOddFireWastedSpark = operationMode != TWO_STROKE
&& (isOddFire | (engineConfiguration->cylindersCount % 2 == 1));
#if EFI_UNIT_TEST #if EFI_UNIT_TEST
if (verboseMode) { if (verboseMode) {

View File

@ -9,6 +9,8 @@
#include "spark_logic.h" #include "spark_logic.h"
using ::testing::_; using ::testing::_;
using ::testing::InSequence;
using ::testing::StrictMock;
TEST(ignition, twoCoils) { TEST(ignition, twoCoils) {
EngineTestHelper eth(engine_type_e::FRANKENSO_BMW_M73_F); EngineTestHelper eth(engine_type_e::FRANKENSO_BMW_M73_F);
@ -148,3 +150,55 @@ TEST(ignition, CylinderTimingTrim) {
EXPECT_NEAR(engine->engineState.timingAdvance[2], unadjusted + 2, EPS4D); EXPECT_NEAR(engine->engineState.timingAdvance[2], unadjusted + 2, EPS4D);
EXPECT_NEAR(engine->engineState.timingAdvance[3], unadjusted + 4, EPS4D); EXPECT_NEAR(engine->engineState.timingAdvance[3], unadjusted + 4, EPS4D);
} }
TEST(ignition, oddCylinderWastedSpark) {
StrictMock<MockExecutor> mockExec;
EngineTestHelper eth(engine_type_e::TEST_ENGINE);
engine->scheduler.setMockExecutor(&mockExec);
engineConfiguration->cylindersCount = 1;
engineConfiguration->firingOrder = FO_1;
engineConfiguration->ignitionMode = IM_WASTED_SPARK;
efitick_t nowNt1 = 1000000;
efitick_t nowNt2 = 2222222;
engine->rpmCalculator.oneDegreeUs = 100;
{
InSequence is;
// Should schedule one dwell+fire pair:
// Dwell 5 deg from now
float nt1deg = USF2NT(engine->rpmCalculator.oneDegreeUs);
efitick_t startTime = nowNt1 + nt1deg * 5;
EXPECT_CALL(mockExec, schedule(testing::NotNull(), _, startTime, _));
// Spark 15 deg from now
efitick_t endTime = startTime + nt1deg * 10;
EXPECT_CALL(mockExec, schedule(testing::NotNull(), _, endTime, _));
// Should schedule second dwell+fire pair, the out of phase copy
// Dwell 5 deg from now
startTime = nowNt2 + nt1deg * 5;
EXPECT_CALL(mockExec, schedule(testing::NotNull(), _, startTime, _));
// Spark 15 deg from now
endTime = startTime + nt1deg * 10;
EXPECT_CALL(mockExec, schedule(testing::NotNull(), _, endTime, _));
}
engine->ignitionState.sparkDwell = 1;
// dwell should start at 15 degrees ATDC and firing at 25 deg ATDC
engine->ignitionState.dwellAngle = 10;
engine->engineState.timingAdvance[0] = -25;
engine->engineState.useOddFireWastedSpark = true;
engineConfiguration->minimumIgnitionTiming = -25;
// expect to schedule the on-phase dwell and spark (not the wasted spark copy)
onTriggerEventSparkLogic(1200, nowNt1, 10, 30);
// expect to schedule second events, the out-of-phase dwell and spark (the wasted spark copy)
onTriggerEventSparkLogic(1200, nowNt2, 360 + 10, 360 + 30);
}