Staged injection support; use two sets of injectors on the same engine fix #5247

This commit is contained in:
Matthew Kennedy 2023-10-14 17:27:19 -04:00 committed by Andrey
parent 0938239974
commit 8f42f8ccc8
9 changed files with 196 additions and 42 deletions

View File

@ -33,6 +33,7 @@ Release template (copy/paste this for new release):
### Added
- Allow brief operation over 100% injector duty cycle and add configurable duty cycle limits #4798
- Staged injection support #5247
### Removed
- Narrow to Wideband approximation

View File

@ -627,6 +627,7 @@ void updateTunerStudioState() {
#if EFI_ENGINE_CONTROL
tsOutputChannels->injectorDutyCycle = minF(/*let's avoid scaled "uint8_t, 2" overflow*/127, getInjectorDutyCycle(rpm));
tsOutputChannels->injectorDutyCycleStage2 = getInjectorDutyCycleStage2(rpm);
#endif
efitimesec_t timeSeconds = getTimeNowS();

View File

@ -8,8 +8,15 @@
#if EFI_ENGINE_CONTROL
void turnInjectionPinHigh(InjectionEvent *event) {
void turnInjectionPinHigh(uintptr_t arg) {
efitick_t nowNt = getTimeNowNt();
// clear last bit to recover the pointer
InjectionEvent *event = reinterpret_cast<InjectionEvent*>(arg & ~(1UL));
// extract last bit
bool stage2Active = arg & 1;
for (size_t i = 0; i < efi::size(event->outputs); i++) {
InjectorOutputPin *output = event->outputs[i];
@ -17,6 +24,16 @@ void turnInjectionPinHigh(InjectionEvent *event) {
output->open(nowNt);
}
}
if (stage2Active) {
for (size_t i = 0; i < efi::size(event->outputsStage2); i++) {
InjectorOutputPin *output = event->outputsStage2[i];
if (output) {
output->open(nowNt);
}
}
}
}
FuelSchedule::FuelSchedule() {
@ -150,6 +167,7 @@ bool InjectionEvent::update() {
}
InjectorOutputPin *secondOutput;
InjectorOutputPin* secondOutputStage2;
if (mode == IM_BATCH) {
/**
@ -161,8 +179,10 @@ bool InjectionEvent::update() {
int secondOrder = (ownIndex + (engineConfiguration->cylindersCount / 2)) % engineConfiguration->cylindersCount;
int secondIndex = ID2INDEX(getCylinderId(secondOrder));
secondOutput = &enginePins.injectors[secondIndex];
secondOutputStage2 = &enginePins.injectorsStage2[secondIndex];
} else {
secondOutput = nullptr;
secondOutputStage2 = nullptr;
}
InjectorOutputPin *output = &enginePins.injectors[injectorIndex];
@ -173,6 +193,9 @@ bool InjectionEvent::update() {
// Stash the cylinder number so we can select the correct fueling bank later
cylinderNumber = injectorIndex;
outputsStage2[0] = &enginePins.injectorsStage2[injectorIndex];
outputsStage2[1] = secondOutputStage2;
if (!isSimultaneous && !output->isInitialized()) {
// todo: extract method for this index math
warning(ObdCode::CUSTOM_OBD_INJECTION_NO_PIN_ASSIGNED, "no_pin_inj #%s", output->getName());

View File

@ -52,7 +52,7 @@ public:
float injectionStartAngle = 0;
};
void turnInjectionPinHigh(InjectionEvent *event);
void turnInjectionPinHigh(uintptr_t arg);
/**

View File

@ -64,6 +64,17 @@ void turnInjectionPinLow(InjectionEvent *event) {
event->update();
}
void turnInjectionPinLowStage2(InjectionEvent* event) {
efitick_t nowNt = getTimeNowNt();
for (size_t i = 0; i < efi::size(event->outputsStage2); i++) {
InjectorOutputPin *output = event->outputsStage2[i];
if (output) {
output->close(nowNt);
}
}
}
void InjectionEvent::onTriggerTooth(efitick_t nowNt, float currentPhase, float nextPhase) {
auto eventAngle = injectionStartAngle;
@ -77,38 +88,51 @@ void InjectionEvent::onTriggerTooth(efitick_t nowNt, float currentPhase, float n
// Perform wall wetting adjustment on fuel mass, not duration, so that
// it's correct during fuel pressure (injector flow) or battery voltage (deadtime) transients
// TODO: is it correct to wall wet on both pulses?
injectionMassGrams = wallFuel.adjust(injectionMassGrams);
const floatms_t injectionDuration = engine->module<InjectorModelPrimary>()->getInjectionDuration(injectionMassGrams);
// Disable staging in simultaneous mode
float stage2Fraction = isSimultaneous ? 0 : getEngineState()->injectionStage2Fraction;
// Compute fraction of fuel on stage 2, remainder goes on stage 1
const float injectionMassStage2 = stage2Fraction * injectionMassGrams;
float injectionMassStage1 = injectionMassGrams - injectionMassStage2;
#if EFI_VEHICLE_SPEED
{
// Log this fuel as consumed
bool isCranking = getEngineRotationState()->isCranking();
int numberOfInjections = isCranking ? getNumberOfInjections(engineConfiguration->crankingInjectionMode) : getNumberOfInjections(engineConfiguration->injectionMode);
float actualInjectedMass = numberOfInjections * (injectionMassStage1 + injectionMassStage2);
engine->module<TripOdometer>()->consumeFuel(actualInjectedMass, nowNt);
}
#endif // EFI_VEHICLE_SPEED
const floatms_t injectionDurationStage1 = engine->module<InjectorModelPrimary>()->getInjectionDuration(injectionMassStage1);
const floatms_t injectionDurationStage2 = injectionMassStage2 > 0 ? engine->module<InjectorModelSecondary>()->getInjectionDuration(injectionMassStage2) : 0;
#if EFI_PRINTF_FUEL_DETAILS
if (printFuelDebug) {
printf("fuel injectionDuration=%.2fms adjusted=%.2fms\n",
getEngineState()->injectionDuration,
injectionDuration);
injectionDurationStage1);
}
#endif /*EFI_PRINTF_FUEL_DETAILS */
#if EFI_VEHICLE_SPEED
bool isCranking = getEngineRotationState()->isCranking();
/**
* todo: pre-calculate 'numberOfInjections'
* see also injectorDutyCycle
*/
int numberOfInjections = isCranking ? getNumberOfInjections(engineConfiguration->crankingInjectionMode) : getNumberOfInjections(engineConfiguration->injectionMode);
engine->module<TripOdometer>()->consumeFuel(injectionMassGrams * numberOfInjections, nowNt);
#endif // EFI_VEHICLE_SPEED
if (this->cylinderNumber == 0) {
engine->outputChannels.actualLastInjection = injectionDuration;
engine->outputChannels.actualLastInjection = injectionDurationStage1;
engine->outputChannels.actualLastInjectionStage2 = injectionDurationStage2;
}
if (cisnan(injectionDuration)) {
if (cisnan(injectionDurationStage1) || cisnan(injectionDurationStage2)) {
warning(ObdCode::CUSTOM_OBD_NAN_INJECTION, "NaN injection pulse");
return;
}
if (injectionDuration < 0) {
warning(ObdCode::CUSTOM_OBD_NEG_INJECTION, "Negative injection pulse %.2f", injectionDuration);
if (injectionDurationStage1 < 0) {
warning(ObdCode::CUSTOM_OBD_NEG_INJECTION, "Negative injection pulse %.2f", injectionDurationStage1);
return;
}
@ -116,48 +140,70 @@ void InjectionEvent::onTriggerTooth(efitick_t nowNt, float currentPhase, float n
// Durations under 50us-ish aren't safe for the scheduler
// as their order may be swapped, resulting in a stuck open injector
// see https://github.com/rusefi/rusefi/pull/596 for more details
if (injectionDuration < 0.050f)
if (injectionDurationStage1 < 0.050f)
{
return;
}
floatus_t durationUs = MS2US(injectionDuration);
floatus_t durationUsStage1 = MS2US(injectionDurationStage1);
floatus_t durationUsStage2 = MS2US(injectionDurationStage2);
// Only bother with the second stage if it's long enough to be relevant
bool hasStage2Injection = durationUsStage2 > 50;
#if EFI_PRINTF_FUEL_DETAILS
if (printFuelDebug) {
InjectorOutputPin *output = outputs[0];
printf("handleFuelInjectionEvent fuelout %s injection_duration %dus engineCycleDuration=%.1fms\t\n", output->getName(), (int)durationUs,
printf("handleFuelInjectionEvent fuelout %s injection_duration %dus engineCycleDuration=%.1fms\t\n", output->getName(), (int)durationUsStage1,
(int)MS2US(getCrankshaftRevolutionTimeMs(Sensor::getOrZero(SensorType::Rpm))) / 1000.0);
}
#endif /*EFI_PRINTF_FUEL_DETAILS */
action_s startAction, endAction;
action_s startAction, endActionStage1, endActionStage2;
// We use different callbacks based on whether we're running sequential mode or not - everything else is the same
if (isSimultaneous) {
startAction = startSimultaneousInjection;
endAction = { &endSimultaneousInjection, this };
endActionStage1 = { &endSimultaneousInjection, this };
} else {
uintptr_t startActionPtr = reinterpret_cast<uintptr_t>(this);
if (hasStage2Injection) {
// Set the low bit in the arg if there's a secondary injection to start too
startActionPtr |= 1;
}
// sequential or batch
startAction = { &turnInjectionPinHigh, this };
endAction = { &turnInjectionPinLow, this };
startAction = { &turnInjectionPinHigh, startActionPtr };
endActionStage1 = { &turnInjectionPinLow, this };
endActionStage2 = { &turnInjectionPinLowStage2, this };
}
// Correctly wrap injection start angle
float angleFromNow = eventAngle - currentPhase;
if (angleFromNow < 0) {
angleFromNow += getEngineState()->engineCycle;
}
// Schedule opening (stage 1 + stage 2 open together)
efitick_t startTime = scheduleByAngle(nullptr, nowNt, angleFromNow, startAction);
efitick_t turnOffTime = startTime + US2NT((int)durationUs);
getExecutorInterface()->scheduleByTimestampNt("inj", nullptr, turnOffTime, endAction);
// Schedule closing stage 1
efitick_t turnOffTimeStage1 = startTime + US2NT((int)durationUsStage1);
getExecutorInterface()->scheduleByTimestampNt("inj", nullptr, turnOffTimeStage1, endActionStage1);
// Schedule closing stage 2 (if applicable)
if (hasStage2Injection && endActionStage2) {
efitick_t turnOffTimeStage2 = startTime + US2NT((int)durationUsStage2);
getExecutorInterface()->scheduleByTimestampNt("inj stage 2", nullptr, turnOffTimeStage2, endActionStage2);
}
#if EFI_UNIT_TEST
printf("scheduling injection angle=%.2f/delay=%.2f injectionDuration=%.2f\r\n", angleFromNow, NT2US(startTime - nowNt), injectionDuration);
printf("scheduling injection angle=%.2f/delay=%d injectionDuration=%d %d\r\n", angleFromNow, (int)NT2US(startTime - nowNt), (int)durationUsStage1, (int)durationUsStage2);
#endif
#if EFI_DEFAILED_LOGGING
efiPrintf("handleFuel pin=%s eventIndex %d duration=%.2fms %d", outputs[0]->name,
injEventIndex,
injectionDuration,
injectionDurationStage1,
getRevolutionCounter());
efiPrintf("handleFuel pin=%s delay=%.2f %d", outputs[0]->name, NT2US(startTime - nowNt),
getRevolutionCounter());

View File

@ -15,3 +15,4 @@ void mainTriggerCallback(uint32_t trgEventIndex, efitick_t edgeTimestamp, angle_
void endSimultaneousInjection(InjectionEvent *event);
void turnInjectionPinLow(InjectionEvent *event);
void turnInjectionPinLowStage2(InjectionEvent* event);

View File

@ -343,12 +343,6 @@ void EnginePins::startInjectionPins() {
output->initPin(output->getName(), engineConfiguration->injectionPinsStage2[i],
engineConfiguration->injectionPinMode);
}
output = &enginePins.injectorsStage2[i];
if (isPinOrModeChanged(injectionPinsStage2[i], injectionPinMode)) {
output->initPin(output->getName(), engineConfiguration->injectionPinsStage2[i],
engineConfiguration->injectionPinMode);
}
}
#endif /* EFI_PROD_CODE */
}

View File

@ -1284,6 +1284,12 @@ curve = rangeMatrix, "Range Switch Input Matrix"
yBins = gppwm4_loadBins, gppwmYAxis4
zBins = gppwm4_table
table = stagedInjectionTbl, stagedInjectionMap, "Staged Injection %", 1
xyLabels = "RPM", ""
xBins = injectorStagingRpmBins, RPMValue
yBins = injectorStagingLoadBins, fuelingLoad
zBins = injectorStagingTable
table = tcuSolenoidTableTbl, tcuSolenoidTableMap, "Solenoids Active By Gear", 1
xBins = gearCountArray, tcuCurrentGear
yBins = solenoidCountArray, tcuCurrentGear
@ -1495,7 +1501,9 @@ gaugeCategory = Fueling
iatCorrectionGauge = running_intakeTemperatureCoefficient, @@GAUGE_NAME_FUEL_IAT_CORR@@, "mult", 0, 3, 0, 0, 3, 3, 2, 2
cltCorrectionGauge = running_coolantTemperatureCoefficient, @@GAUGE_NAME_FUEL_CLT_CORR@@, "mult", 0, 3, 0, 0, 3, 3, 2, 2
injectorDutyCycleGauge=injectorDutyCycle, @@GAUGE_NAME_FUEL_INJ_DUTY@@,"%", 0, 120, 10, 10, 100, 100, 1, 1
injectorDutyCycleStg2Gauge=injectorDutyCycleStage2, @@GAUGE_NAME_FUEL_INJ_DUTY_STAGE_2@@,"%", 0, 120, 10, 10, 100, 100, 1, 1
actualLastInjectionGauge = actualLastInjection, @@GAUGE_NAME_FUEL_LAST_INJECTION@@, "mSec", 0, 25.5, 1.0, 1.2, 20, 25, 3, 1
actualLastInjectionStg2Gauge = actualLastInjectionStage2, @@GAUGE_NAME_FUEL_LAST_INJECTION_STAGE_2@@, "mSec", 0, 25.5, 1.0, 1.2, 20, 25, 3, 1
veValueGauge = veValue, "fuel: VE", "", 0, 120, 10, 10, 100, 100, 1, 1
injectorLagMsGauge = m_deadtime, @@GAUGE_NAME_INJECTOR_LAG@@, "mSec", 0, 10, 0, 0, 10, 10, 3, 1
@ -1737,6 +1745,7 @@ menuDialog = main
subMenu = injectionSettings, "Injection hardware", 0, {isInjectionEnabled == 1}
subMenu = cylinderBankSelect, "Cylinder bank selection", 0, {isInjectionEnabled == 1}
subMenu = injectorNonlinear, "Injector small-pulse correction", 0, {isInjectionEnabled == 1}
subMenu = stagedInjection, "Staged injection", 0, {isInjectionEnabled}
groupMenu = "Cylinder fuel trims"
groupChildMenu = fuelTrimTbl1, "Fuel trim cyl 1"
@ -2587,6 +2596,26 @@ cmd_set_engine_type_default = "@@TS_IO_TEST_COMMAND_char@@@@ts_command_e_TS_
field = "Offset cyl 11", timing_offset_cylinder11, {cylindersCount > 10}
field = "Offset cyl 12", timing_offset_cylinder12, {cylindersCount > 11}
dialog = stagedInjectionLeft, "", yAxis
field = "Enable", enableStagedInjection, {isInjectionEnabled}
field = ""
field = "Injection Stage 2 Output 1", injectionPinsStage21, {isInjectionEnabled && enableStagedInjection}
field = "Injection Stage 2 Output 2", injectionPinsStage22, {isInjectionEnabled && enableStagedInjection && injectionMode != 3 && cylindersCount > 1}
field = "Injection Stage 2 Output 3", injectionPinsStage23, {isInjectionEnabled && enableStagedInjection && injectionMode != @@injection_mode_e_IM_SINGLE_POINT@@ && cylindersCount > 2}
field = "Injection Stage 2 Output 4", injectionPinsStage24, {isInjectionEnabled && enableStagedInjection && injectionMode != 3 && cylindersCount > 3}
field = "Injection Stage 2 Output 5 ", injectionPinsStage25, {isInjectionEnabled && enableStagedInjection && injectionMode != @@injection_mode_e_IM_SINGLE_POINT@@ && cylindersCount > 4}
field = "Injection Stage 2 Output 6 ", injectionPinsStage26, {isInjectionEnabled && enableStagedInjection && injectionMode != @@injection_mode_e_IM_SINGLE_POINT@@ && cylindersCount > 5}
field = "Injection Stage 2 Output 7 ", injectionPinsStage27, {isInjectionEnabled && enableStagedInjection && injectionMode != 3 && cylindersCount > 6}
field = "Injection Stage 2 Output 8 ", injectionPinsStage28, {isInjectionEnabled && enableStagedInjection && injectionMode != 3 && cylindersCount > 7}
field = "Injection Stage 2 Output 9 ", injectionPinsStage29, {isInjectionEnabled && enableStagedInjection && cylindersCount > 8}
field = "Injection Stage 2 Output 10 ", injectionPinsStage210, {isInjectionEnabled && enableStagedInjection && cylindersCount > 9}
field = "Injection Stage 2 Output 11 ", injectionPinsStage211, {isInjectionEnabled && enableStagedInjection && cylindersCount > 10}
field = "Injection Stage 2 Output 12 ", injectionPinsStage212, {isInjectionEnabled && enableStagedInjection && cylindersCount > 11}
dialog = stagedInjection, "", xAxis
panel = stagedInjectionLeft
panel = stagedInjectionTbl, {isInjectionEnabled && enableStagedInjection}
dialog = multisparkDwellParams, "Delay & Dwell"
field = "Spark duration", multisparkSparkDuration, {multisparkEnable}
field = "Subsequent spark dwell", multisparkDwell, {multisparkEnable}

View File

@ -6,6 +6,15 @@ using ::testing::_;
using ::testing::StrictMock;
using ::testing::InSequence;
using ::testing::Eq;
using ::testing::Not;
using ::testing::Property;
using ::testing::Truly;
static bool ActionArgumentHasLowBitSet(const action_s& a) {
return (reinterpret_cast<uintptr_t>(a.getArgument()) & 1) != 0;
}
TEST(injectionScheduling, InjectionIsScheduled) {
StrictMock<MockExecutor> mockExec;
@ -33,11 +42,61 @@ TEST(injectionScheduling, InjectionIsScheduled) {
// rising edge 5 degrees from now
float nt5deg = USF2NT(engine->rpmCalculator.oneDegreeUs * 5);
efitick_t startTime = nowNt + nt5deg;
EXPECT_CALL(mockExec, scheduleByTimestampNt(testing::NotNull(), _, startTime, _));
EXPECT_CALL(mockExec, scheduleByTimestampNt(testing::NotNull(), _, startTime, Not(Truly(ActionArgumentHasLowBitSet))));
// falling edge 20ms later
EXPECT_CALL(mockExec, scheduleByTimestampNt(testing::NotNull(), _, startTime + MS2NT(20), _));
EXPECT_CALL(mockExec, scheduleByTimestampNt(testing::NotNull(), _, startTime + MS2NT(20), Property(&action_s::getArgument, Eq(&event))));
}
// Event scheduled at 125 degrees
event.injectionStartAngle = 125;
// We are at 120 degrees now, next tooth 130
event.onTriggerTooth(nowNt, 120, 130);
}
TEST(injectionScheduling, InjectionIsScheduledDualStage) {
StrictMock<MockExecutor> mockExec;
StrictMock<MockInjectorModel2> im;
EngineTestHelper eth(engine_type_e::TEST_ENGINE);
engine->executor.setMockExecutor(&mockExec);
engine->module<InjectorModelPrimary>().set(&im);
engine->module<InjectorModelSecondary>().set(&im);
efitick_t nowNt = 1000000;
InjectionEvent event;
InjectorOutputPin pin;
pin.injectorIndex = 0;
event.outputs[0] = &pin;
engine->rpmCalculator.oneDegreeUs = 100;
// Some nonzero fuel quantity on both stages
engine->engineState.injectionMass[0] = 50;
engine->engineState.injectionStage2Fraction = 0.2;
{
InSequence is;
// Primary injection duration of 20ms, secondary 10ms
EXPECT_CALL(im, getInjectionDuration(40)).WillOnce(Return(20.0f));
EXPECT_CALL(im, getInjectionDuration(10)).WillOnce(Return(10.0f));
}
{
InSequence is;
// Should schedule one normal injection:
// rising edge 5 degrees from now
float nt5deg = USF2NT(engine->rpmCalculator.oneDegreeUs * 5);
efitick_t startTime = nowNt + nt5deg;
EXPECT_CALL(mockExec, scheduleByTimestampNt(testing::NotNull(), _, startTime, Truly(ActionArgumentHasLowBitSet)));
// falling edge (primary) 20ms later
EXPECT_CALL(mockExec, scheduleByTimestampNt(testing::NotNull(), _, startTime + MS2NT(20), Property(&action_s::getArgument, Eq(&event))));
// falling edge (secondary) 10ms later
EXPECT_CALL(mockExec, scheduleByTimestampNt(testing::NotNull(), _, startTime + MS2NT(10), Property(&action_s::getArgument, Eq(&event))));
}
// Event scheduled at 125 degrees
event.injectionStartAngle = 125;
@ -73,9 +132,9 @@ TEST(injectionScheduling, InjectionIsScheduledBeforeWraparound) {
// rising edge 5 degrees from now
float nt5deg = USF2NT(engine->rpmCalculator.oneDegreeUs * 5);
efitick_t startTime = nowNt + nt5deg;
EXPECT_CALL(mockExec, scheduleByTimestampNt(testing::NotNull(), _, startTime, _));
EXPECT_CALL(mockExec, scheduleByTimestampNt(testing::NotNull(), _, startTime, Not(Truly(ActionArgumentHasLowBitSet))));
// falling edge 20ms later
EXPECT_CALL(mockExec, scheduleByTimestampNt(testing::NotNull(), _, startTime + MS2NT(20), _));
EXPECT_CALL(mockExec, scheduleByTimestampNt(testing::NotNull(), _, startTime + MS2NT(20), Property(&action_s::getArgument, Eq(&event))));
}
// Event scheduled at 715 degrees
@ -112,9 +171,9 @@ TEST(injectionScheduling, InjectionIsScheduledAfterWraparound) {
// rising edge 15 degrees from now
float nt5deg = USF2NT(engine->rpmCalculator.oneDegreeUs * 15);
efitick_t startTime = nowNt + nt5deg;
EXPECT_CALL(mockExec, scheduleByTimestampNt(testing::NotNull(), _, startTime, _));
EXPECT_CALL(mockExec, scheduleByTimestampNt(testing::NotNull(), _, startTime, Not(Truly(ActionArgumentHasLowBitSet))));
// falling edge 20ms later
EXPECT_CALL(mockExec, scheduleByTimestampNt(testing::NotNull(), _, startTime + MS2NT(20), _));
EXPECT_CALL(mockExec, scheduleByTimestampNt(testing::NotNull(), _, startTime + MS2NT(20), Property(&action_s::getArgument, Eq(&event))));
}
// Event scheduled at 5 degrees