Skip to content

Commit

Permalink
Odd cylinder engine wasted spark (#479)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mck1117 authored Aug 31, 2024
1 parent 397e3dc commit 51a2336
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 17 deletions.
1 change: 1 addition & 0 deletions firmware/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ or
- TunerStudio UI improvements (#436, etc)
- Dropdown selector for popular gearbox ratios (#358, thank you @alrijleh and @nmschulte!)
- 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
- Improve performance with Lua CAN reception of a high volume of frames
Expand Down
2 changes: 2 additions & 0 deletions firmware/controllers/algo/engine_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class EngineState : public engine_state_s {
*/
angle_t engineCycle;

bool useOddFireWastedSpark = false;

/**
* this is based on sensorChartMode and sensorSnifferRpmThreshold settings
*/
Expand Down
52 changes: 44 additions & 8 deletions firmware/controllers/engine_cycle/spark_logic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ static void prepareCylinderIgnitionSchedule(angle_t dwellAngleDuration, floatms_
event->sparkAngle = sparkAngle;

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);

const int index = getIgnitionPinForIndex(event->cylinderIndex, ignitionMode);
Expand Down Expand Up @@ -462,14 +470,48 @@ void onTriggerEventSparkLogic(int rpm, efitick_t edgeTimestamp, float currentPha
* 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) {
for (size_t i = 0; i < engineConfiguration->cylindersCount; i++) {
IgnitionEvent *event = &engine->ignitionEvents.elements[i];

angle_t dwellAngle = event->dwellAngle;
if (!isPhaseInRange(dwellAngle, currentPhase, nextPhase)) {

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;
}

Expand All @@ -494,12 +536,6 @@ void onTriggerEventSparkLogic(int rpm, efitick_t edgeTimestamp, float currentPha
engine->ALSsoftSparkLimiter.setTargetSkipRatio(ALSSkipRatio);
#endif // EFI_ANTILAG_SYSTEM

angle_t sparkAngle = event->sparkAngle;
if (std::isnan(sparkAngle)) {
warning(ObdCode::CUSTOM_ADVANCE_SPARK, "NaN advance");
continue;
}

scheduleSparkEvent(limitedSpark, event, rpm, dwellMs, dwellAngle, sparkAngle, edgeTimestamp, currentPhase, nextPhase);
}
}
Expand Down
6 changes: 0 additions & 6 deletions firmware/controllers/limp_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,6 @@ static bool noFiringUntilVvtSync(vvt_mode_e vvtMode) {
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
// non-symmetrical cranks can use faster spin-up mode (firing in wasted/batch before VVT sync)
// Examples include Nissan MR/VQ, Miata NB, etc
Expand Down
18 changes: 15 additions & 3 deletions firmware/controllers/math/engine_math.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,7 @@ ignition_mode_e getCurrentIgnitionMode() {
ignition_mode_e ignitionMode = engineConfiguration->ignitionMode;
#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
// However, only do this on even cylinder count engines: odd cyl count doesn't fire at all
if (ignitionMode == IM_INDIVIDUAL_COILS && (engineConfiguration->cylindersCount % 2 == 0)) {
if (ignitionMode == IM_INDIVIDUAL_COILS) {
bool missingPhaseInfoForSequential =
!engine->triggerCentral.triggerState.hasSynchronizedPhase();

Expand All @@ -405,7 +404,20 @@ ignition_mode_e getCurrentIgnitionMode() {
* This heavy method is only invoked in case of a configuration change or initialization.
*/
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 (verboseMode) {
Expand Down
54 changes: 54 additions & 0 deletions unit_tests/tests/ignition_injection/test_ignition_scheduling.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
#include "spark_logic.h"

using ::testing::_;
using ::testing::InSequence;
using ::testing::StrictMock;

TEST(ignition, twoCoils) {
EngineTestHelper eth(engine_type_e::FRANKENSO_BMW_M73_F);
Expand Down Expand Up @@ -148,3 +150,55 @@ TEST(ignition, CylinderTimingTrim) {
EXPECT_NEAR(engine->engineState.timingAdvance[2], unadjusted + 2, 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);
}

0 comments on commit 51a2336

Please sign in to comment.