GCC Code Coverage Report


Directory: ./
File: firmware/controllers/engine_cycle/fuel_schedule.cpp
Date: 2025-10-03 00:57:22
Coverage Exec Excl Total
Lines: 89.5% 94 0 105
Functions: 100.0% 11 0 11
Branches: 69.7% 62 0 89
Decisions: 72.3% 34 - 47

Line Branch Decision Exec Source
1 /**
2 * @file fuel_schedule.cpp
3 *
4 * Handles injection scheduling
5 */
6
7 #include "pch.h"
8
9 #if EFI_ENGINE_CONTROL
10
11 681 void turnInjectionPinHigh(scheduler_arg_t const arg) {
12
1/1
✓ Branch 1 taken 681 times.
681 auto const nowNt{ getTimeNowNt() };
13
14 681 auto const taggedPointer{ TaggedPointer<InjectionEvent>::fromRaw(arg) };
15 681 auto const event{ taggedPointer.getOriginalPointer() };
16 681 auto const hasStage2Injection{ taggedPointer.getFlag() };
17
18
2/2
✓ Branch 0 taken 1362 times.
✓ Branch 1 taken 681 times.
2/2
✓ Decision 'true' taken 1362 times.
✓ Decision 'false' taken 681 times.
2043 for (auto const& output: event->outputs) {
19
2/2
✓ Branch 0 taken 891 times.
✓ Branch 1 taken 471 times.
2/2
✓ Decision 'true' taken 891 times.
✓ Decision 'false' taken 471 times.
1362 if (output) {
20
1/1
✓ Branch 1 taken 891 times.
891 output->open(nowNt);
21 }
22 }
23
24
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 681 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 681 times.
681 if (hasStage2Injection) {
25 for (auto const& output: event->outputsStage2) {
26 if (output) {
27 output->open(nowNt);
28 }
29 }
30 }
31 681 }
32
33 677 FuelSchedule::FuelSchedule() {
34
2/2
✓ Branch 0 taken 8124 times.
✓ Branch 1 taken 677 times.
2/2
✓ Decision 'true' taken 8124 times.
✓ Decision 'false' taken 677 times.
8801 for (int cylinderIndex = 0; cylinderIndex < MAX_CYLINDER_COUNT; cylinderIndex++) {
35 8124 elements[cylinderIndex].setIndex(cylinderIndex);
36 }
37 677 }
38
39 522964 WallFuel& InjectionEvent::getWallFuel() {
40 522964 return wallFuel;
41 }
42
43 2130 void FuelSchedule::invalidate() {
44 2130 isReady = false;
45 2130 }
46
47 250 void FuelSchedule::resetOverlapping() {
48
2/2
✓ Branch 0 taken 3000 times.
✓ Branch 1 taken 250 times.
2/2
✓ Decision 'true' taken 3000 times.
✓ Decision 'false' taken 250 times.
3250 for (auto& inj : enginePins.injectors) {
49 3000 inj.reset();
50 }
51 250 }
52
53 // Determines how much to adjust injection opening angle based on the injection's duration and the current phasing mode
54 2512 static float getInjectionAngleCorrection(float fuelMs, float oneDegreeUs) {
55 2512 auto mode = engineConfiguration->injectionTimingMode;
56
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2512 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 2512 times.
2512 if (mode == InjectionTimingMode::Start) {
57 // Start of injection gets no correction for duration
58 return 0;
59 }
60
61
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 2512 times.
2512 efiAssert(ObdCode::CUSTOM_ERR_ASSERT, !std::isnan(fuelMs), "NaN fuelMs", false);
62
63 2512 angle_t injectionDurationAngle = MS2US(fuelMs) / oneDegreeUs;
64
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 2512 times.
2512 efiAssert(ObdCode::CUSTOM_ERR_ASSERT, !std::isnan(injectionDurationAngle), "NaN injectionDurationAngle", false);
65
2/4
✓ Branch 0 taken 2512 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2512 times.
2512 assertAngleRange(injectionDurationAngle, "injectionDuration_r", ObdCode::CUSTOM_INJ_DURATION);
66
67
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2512 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 2512 times.
2512 if (mode == InjectionTimingMode::Center) {
68 // Center of injection is half-corrected for duration
69 return injectionDurationAngle * 0.5f;
70 } else {
71 // End of injection gets "full correction" so we advance opening by the full duration
72 2512 return injectionDurationAngle;
73 }
74 }
75
76 // Returns the start angle of this injector in engine coordinates (0-720 for a 4 stroke),
77 // or unexpected if unable to calculate the start angle due to missing information.
78 3152 expected<float> InjectionEvent::computeInjectionAngle() const {
79
2/2
✓ Branch 1 taken 3152 times.
✓ Branch 4 taken 3152 times.
3152 floatus_t oneDegreeUs = getEngineRotationState()->getOneDegreeUs(); // local copy
80
2/2
✓ Branch 1 taken 640 times.
✓ Branch 2 taken 2512 times.
2/2
✓ Decision 'true' taken 640 times.
✓ Decision 'false' taken 2512 times.
3152 if (std::isnan(oneDegreeUs)) {
81 // in order to have fuel schedule we need to have current RPM
82 // wonder if this line slows engine startup?
83 640 return unexpected;
84 }
85
86
1/1
✓ Branch 1 taken 2512 times.
2512 float fuelMs = getEngineState()->injectionDuration;
87
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 2512 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 2512 times.
2512 if (std::isnan(fuelMs)) {
88 return unexpected;
89 }
90
91 // injection phase may be scheduled by injection end, so we need to step the angle back
92 // for the duration of the injection
93
1/1
✓ Branch 1 taken 2512 times.
2512 angle_t injectionDurationAngle = getInjectionAngleCorrection(fuelMs, oneDegreeUs);
94
95 // User configured offset - degrees after TDC combustion
96
1/1
✓ Branch 1 taken 2512 times.
2512 floatus_t injectionOffset = getEngineState()->injectionOffset;
97
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 2512 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 2512 times.
2512 if (std::isnan(injectionOffset)) {
98 // injection offset map not ready - we are not ready to schedule fuel events
99 return unexpected;
100 }
101
102 2512 angle_t openingAngle = injectionOffset - injectionDurationAngle;
103
2/5
✓ Branch 0 taken 2512 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2512 times.
✗ Branch 5 not taken.
2512 assertAngleRange(openingAngle, "openingAngle_r", ObdCode::CUSTOM_ERR_6554);
104
1/1
✓ Branch 1 taken 2512 times.
2512 wrapAngle(openingAngle, "addFuel#1", ObdCode::CUSTOM_ERR_6555);
105 // TODO: should we log per-cylinder injection timing? #76
106
1/1
✓ Branch 1 taken 2512 times.
2512 getTunerStudioOutputChannels()->injectionOffset = openingAngle;
107
108 // Convert from cylinder-relative to cylinder-1-relative
109
1/1
✓ Branch 1 taken 2512 times.
2512 openingAngle += getPerCylinderFiringOrderOffset(ownIndex, cylinderNumber);
110
111
1/3
✗ Branch 1 not taken.
✓ Branch 2 taken 2512 times.
✗ Branch 4 not taken.
2512 efiAssert(ObdCode::CUSTOM_ERR_ASSERT, !std::isnan(openingAngle), "findAngle#3", false);
112
2/5
✓ Branch 0 taken 2512 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2512 times.
✗ Branch 5 not taken.
2512 assertAngleRange(openingAngle, "findAngle#a33", ObdCode::CUSTOM_ERR_6544);
113
114
1/1
✓ Branch 1 taken 2512 times.
2512 wrapAngle(openingAngle, "addFuel#2", ObdCode::CUSTOM_ERR_6555);
115
116 #if EFI_UNIT_TEST
117 // printf("registerInjectionEvent openingAngle=%.2f inj %d\r\n", openingAngle, cylinderNumber);
118 #endif
119
120 2512 return openingAngle;
121 }
122
123 3152 bool InjectionEvent::updateInjectionAngle() {
124
1/1
✓ Branch 2 taken 3152 times.
3152 auto result = computeInjectionAngle();
125
126
2/2
✓ Branch 1 taken 2512 times.
✓ Branch 2 taken 640 times.
2/2
✓ Decision 'true' taken 2512 times.
✓ Decision 'false' taken 640 times.
3152 if (result) {
127 // If injector duty cycle is high, lock injection SOI so that we
128 // don't miss injections at or above 100% duty
129
2/3
✓ Branch 1 taken 2512 times.
✓ Branch 3 taken 2512 times.
✗ Branch 4 not taken.
1/2
✓ Decision 'true' taken 2512 times.
✗ Decision 'false' not taken.
2512 if (getEngineState()->shouldUpdateInjectionTiming) {
130 2512 injectionStartAngle = result.Value;
131 }
132
133 2512 return true;
134 } else {
135 640 return false;
136 }
137 }
138
139 /**
140 * @returns false in case of error, true if success
141 */
142
1/1
✓ Decision 'true' taken 3152 times.
3152 bool InjectionEvent::update() {
143 3152 bool updatedAngle = updateInjectionAngle();
144
145
2/2
✓ Branch 0 taken 640 times.
✓ Branch 1 taken 2512 times.
2/2
✓ Decision 'true' taken 640 times.
✓ Decision 'false' taken 2512 times.
3152 if (!updatedAngle) {
146 640 return false;
147 }
148
149 2512 injection_mode_e mode = getCurrentInjectionMode();
150 2512 engine->outputChannels.currentInjectionMode = static_cast<uint8_t>(mode);
151
152 int injectorIndex;
153
4/4
✓ Branch 0 taken 1304 times.
✓ Branch 1 taken 1208 times.
✓ Branch 2 taken 12 times.
✓ Branch 3 taken 1292 times.
2/2
✓ Decision 'true' taken 1220 times.
✓ Decision 'false' taken 1292 times.
2512 if (mode == IM_SIMULTANEOUS || mode == IM_SINGLE_POINT) {
154 // These modes only have one injector
155 1220 injectorIndex = 0;
156
3/4
✓ Branch 0 taken 307 times.
✓ Branch 1 taken 985 times.
✓ Branch 2 taken 307 times.
✗ Branch 3 not taken.
1/2
✓ Decision 'true' taken 1292 times.
✗ Decision 'false' not taken.
1292 } else if (mode == IM_SEQUENTIAL || mode == IM_BATCH) {
157 // Map order index -> cylinder index (firing order)
158 1292 injectorIndex = getCylinderNumberAtIndex(ownIndex);
159 } else {
160 firmwareError(ObdCode::CUSTOM_OBD_UNEXPECTED_INJECTION_MODE, "Unexpected injection mode %d", mode);
161 injectorIndex = 0;
162 }
163
164 InjectorOutputPin *secondOutput;
165 InjectorOutputPin* secondOutputStage2;
166
167
2/2
✓ Branch 0 taken 307 times.
✓ Branch 1 taken 2205 times.
2/2
✓ Decision 'true' taken 307 times.
✓ Decision 'false' taken 2205 times.
2512 if (mode == IM_BATCH) {
168 /**
169 * also fire the 2nd half of the injectors so that we can implement a batch mode on individual wires
170 */
171 // Compute the position of this cylinder's twin in the firing order
172 // Each injector gets fired as a primary (the same as sequential), but also
173 // fires the injector 360 degrees later in the firing order.
174 307 int secondOrder = (ownIndex + (engineConfiguration->cylindersCount / 2)) % engineConfiguration->cylindersCount;
175 307 int secondIndex = getCylinderNumberAtIndex(secondOrder);
176 307 secondOutput = &enginePins.injectors[secondIndex];
177 307 secondOutputStage2 = &enginePins.injectorsStage2[secondIndex];
178 } else {
179 2205 secondOutput = nullptr;
180 2205 secondOutputStage2 = nullptr;
181 }
182
183 2512 InjectorOutputPin *output = &enginePins.injectors[injectorIndex];
184
185 2512 outputs[0] = output;
186 2512 outputs[1] = secondOutput;
187 2512 isSimultaneous = mode == IM_SIMULTANEOUS;
188 // Stash the cylinder number so we can select the correct fueling bank later
189 2512 cylinderNumber = injectorIndex;
190
191 2512 outputsStage2[0] = &enginePins.injectorsStage2[injectorIndex];
192 2512 outputsStage2[1] = secondOutputStage2;
193
194
4/6
✓ Branch 0 taken 1304 times.
✓ Branch 1 taken 1208 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1304 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2512 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 2512 times.
2512 if (!isSimultaneous && !output->isInitialized()) {
195 // todo: extract method for this index math
196 warning(ObdCode::CUSTOM_OBD_INJECTION_NO_PIN_ASSIGNED, "no_pin_inj #%s", output->getName());
197 }
198
199 2512 return true;
200 }
201
202 932 void FuelSchedule::addFuelEvents() {
203
2/2
✓ Branch 0 taken 1821 times.
✓ Branch 1 taken 292 times.
2/2
✓ Decision 'true' taken 1821 times.
✓ Decision 'false' taken 292 times.
2113 for (size_t cylinderIndex = 0; cylinderIndex < engineConfiguration->cylindersCount; cylinderIndex++) {
204 1821 bool result = elements[cylinderIndex].update();
205
206
2/2
✓ Branch 0 taken 640 times.
✓ Branch 1 taken 1181 times.
2/2
✓ Decision 'true' taken 640 times.
✓ Decision 'false' taken 1181 times.
1821 if (!result) {
207 640 invalidate();
208 640 return;
209 }
210 }
211
212 // We made it through all cylinders, mark the schedule as ready so it can be used
213 292 isReady = true;
214 }
215
216 28839 void FuelSchedule::onTriggerTooth(efitick_t nowNt, float currentPhase, float nextPhase) {
217 // Wait for schedule to be built - this happens the first time we get RPM
218
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 28839 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 28839 times.
28839 if (!isReady) {
219 return;
220 }
221
222
2/2
✓ Branch 0 taken 111886 times.
✓ Branch 1 taken 28839 times.
2/2
✓ Decision 'true' taken 111886 times.
✓ Decision 'false' taken 28839 times.
140725 for (size_t i = 0; i < engineConfiguration->cylindersCount; i++) {
223 111886 elements[i].onTriggerTooth(nowNt, currentPhase, nextPhase);
224 }
225 }
226
227 #endif // EFI_ENGINE_CONTROL
228