Skip to content

Commit dbc2497

Browse files
authored
Separate fuel calcs out from speeduino.ino (speeduino#1351)
* Split out unit tested fueling calcs from speeduino.ino * Remove speeduino.h - place contents directly in speeduino.ino for simpler unit testing. * Remove unecessay #include * Performance - force inline
1 parent 1f64693 commit dbc2497

File tree

11 files changed

+357
-370
lines changed

11 files changed

+357
-370
lines changed

speeduino/comms.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ A full copy of the license may be found in the projects root directory
1919
#include "logger.h"
2020
#include "comms_legacy.h"
2121
#include "src/FastCRC/FastCRC.h"
22-
#include <avr/pgmspace.h>
2322
#ifdef RTC_ENABLED
2423
#include "rtc_common.h"
2524
#include "comms_sd.h"

speeduino/corrections.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ There are 2 top level functions that call more detailed corrections for Fuel and
2525

2626
#include "globals.h"
2727
#include "corrections.h"
28-
#include "speeduino.h"
2928
#include "timers.h"
3029
#include "maths.h"
3130
#include "sensors.h"
3231
#include "unit_testing.h"
3332
#include "utilities.h"
3433
#include "src/PID_v1/PID_v1.h"
3534
#include "units.h"
35+
#include "fuel_calcs.h"
3636

3737
long PID_O2, PID_output, PID_AFRTarget;
3838
/** Instance of the PID object in case that algorithm is used (Always instantiated).

speeduino/fuel_calcs.cpp

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
#include "fuel_calcs.h"
2+
#include "globals.h"
3+
4+
uint16_t req_fuel_uS = 0; /**< The required fuel variable (As calculated by TunerStudio) in uS */
5+
uint16_t inj_opentime_uS = 0;
6+
uint16_t staged_req_fuel_mult_pri = 0;
7+
uint16_t staged_req_fuel_mult_sec = 0;
8+
9+
// Force this to be inlined via LTO: it's worth 40 loop/sec on AVR
10+
#pragma GCC diagnostic push
11+
#pragma GCC diagnostic ignored "-Wattributes"
12+
uint16_t __attribute__((always_inline)) PW(int REQ_FUEL, byte VE, long MAP, uint16_t corrections, int injOpen)
13+
{
14+
//Standard float version of the calculation
15+
//return (REQ_FUEL * (float)(VE/100.0) * (float)(MAP/100.0) * (float)(TPS/100.0) * (float)(corrections/100.0) + injOpen);
16+
//Note: The MAP and TPS portions are currently disabled, we use VE and corrections only
17+
uint16_t iVE;
18+
uint16_t iMAP = 100;
19+
uint16_t iAFR = 147;
20+
21+
//100% float free version, does sacrifice a little bit of accuracy, but not much.
22+
23+
//iVE = ((unsigned int)VE << 7) / 100;
24+
iVE = div100(((uint16_t)VE << 7U));
25+
26+
//Check whether either of the multiply MAP modes is turned on
27+
//if ( configPage2.multiplyMAP == MULTIPLY_MAP_MODE_100) { iMAP = ((unsigned int)MAP << 7) / 100; }
28+
if ( configPage2.multiplyMAP == MULTIPLY_MAP_MODE_100) { iMAP = div100( ((uint16_t)MAP << 7U) ); }
29+
else if( configPage2.multiplyMAP == MULTIPLY_MAP_MODE_BARO) { iMAP = ((unsigned int)MAP << 7U) / currentStatus.baro; }
30+
31+
if ( (configPage2.includeAFR == true) && (configPage6.egoType == EGO_TYPE_WIDE) && (currentStatus.runSecs > configPage6.ego_sdelay) ) {
32+
iAFR = ((unsigned int)currentStatus.O2 << 7U) / currentStatus.afrTarget; //Include AFR (vs target) if enabled
33+
}
34+
if ( (configPage2.incorporateAFR == true) && (configPage2.includeAFR == false) ) {
35+
iAFR = ((unsigned int)configPage2.stoich << 7U) / currentStatus.afrTarget; //Incorporate stoich vs target AFR, if enabled.
36+
}
37+
38+
uint32_t intermediate = rshift<7U>((uint32_t)REQ_FUEL * (uint32_t)iVE); //Need to use an intermediate value to avoid overflowing the long
39+
if ( configPage2.multiplyMAP > 0 ) { intermediate = rshift<7U>(intermediate * (uint32_t)iMAP); }
40+
41+
if ( (configPage2.includeAFR == true) && (configPage6.egoType == EGO_TYPE_WIDE) && (currentStatus.runSecs > configPage6.ego_sdelay) ) {
42+
//EGO type must be set to wideband and the AFR warmup time must've elapsed for this to be used
43+
intermediate = rshift<7U>(intermediate * (uint32_t)iAFR);
44+
}
45+
if ( (configPage2.incorporateAFR == true) && (configPage2.includeAFR == false) ) {
46+
intermediate = rshift<7U>(intermediate * (uint32_t)iAFR);
47+
}
48+
49+
//If corrections are huge, use less bitshift to avoid overflow. Sacrifices a bit more accuracy (basically only during very cold temp cranking)
50+
if (corrections < 512 ) {
51+
intermediate = rshift<7U>(intermediate * div100(lshift<7U>(corrections)));
52+
} else if (corrections < 1024 ) {
53+
intermediate = rshift<6U>(intermediate * div100(lshift<6U>(corrections)));
54+
} else {
55+
intermediate = rshift<5U>(intermediate * div100(lshift<5U>(corrections)));
56+
}
57+
58+
if (intermediate != 0)
59+
{
60+
//If intermediate is not 0, we need to add the opening time (0 typically indicates that one of the full fuel cuts is active)
61+
intermediate += injOpen; //Add the injector opening time
62+
//AE calculation only when ACC is active.
63+
if ( currentStatus.isAcceleratingTPS )
64+
{
65+
//AE Adds % of req_fuel
66+
if ( configPage2.aeApplyMode == AE_MODE_ADDER )
67+
{
68+
intermediate += div100(((uint32_t)REQ_FUEL) * (currentStatus.AEamount - 100U));
69+
}
70+
}
71+
72+
if ( intermediate > UINT16_MAX)
73+
{
74+
intermediate = UINT16_MAX; //Make sure this won't overflow when we convert to uInt. This means the maximum pulsewidth possible is 65.535mS
75+
}
76+
}
77+
return (unsigned int)(intermediate);
78+
}
79+
#pragma GCC diagnostic pop
80+
81+
uint16_t calculatePWLimit()
82+
{
83+
uint32_t tempLimit = percentage(configPage2.dutyLim, revolutionTime); //The pulsewidth limit is determined to be the duty cycle limit (Eg 85%) by the total time it takes to perform 1 revolution
84+
//Handle multiple squirts per rev
85+
if (configPage2.strokes == FOUR_STROKE) { tempLimit = tempLimit * 2; }
86+
//Optimise for power of two divisions where possible
87+
switch(currentStatus.nSquirts)
88+
{
89+
case 1:
90+
//No action needed
91+
break;
92+
case 2:
93+
tempLimit = tempLimit / 2;
94+
break;
95+
case 4:
96+
tempLimit = tempLimit / 4;
97+
break;
98+
case 8:
99+
tempLimit = tempLimit / 8;
100+
break;
101+
default:
102+
//Non-PoT squirts value. Perform (slow) uint32_t division
103+
tempLimit = tempLimit / currentStatus.nSquirts;
104+
break;
105+
}
106+
if(tempLimit > UINT16_MAX) { tempLimit = UINT16_MAX; }
107+
108+
return tempLimit;
109+
}
110+
111+
// Force this to be inlined via LTO: it's worth 40 loop/sec on AVR
112+
#pragma GCC diagnostic push
113+
#pragma GCC diagnostic ignored "-Wattributes"
114+
void __attribute__((always_inline)) calculateStaging(uint32_t pwLimit)
115+
{
116+
//Calculate staging pulsewidths if used
117+
//To run staged injection, the number of cylinders must be less than or equal to the injector channels (ie Assuming you're running paired injection, you need at least as many injector channels as you have cylinders, half for the primaries and half for the secondaries)
118+
if( (configPage10.stagingEnabled == true) && (configPage2.nCylinders <= INJ_CHANNELS || configPage2.injType == INJ_TYPE_TBODY) && (currentStatus.PW1 > inj_opentime_uS) ) //Final check is to ensure that DFCO isn't active, which would cause an overflow below (See #267)
119+
{
120+
//Scale the 'full' pulsewidth by each of the injector capacities
121+
currentStatus.PW1 -= inj_opentime_uS; //Subtract the opening time from PW1 as it needs to be multiplied out again by the pri/sec req_fuel values below. It is added on again after that calculation.
122+
uint32_t tempPW1 = div100((uint32_t)currentStatus.PW1 * staged_req_fuel_mult_pri);
123+
124+
if(configPage10.stagingMode == STAGING_MODE_TABLE)
125+
{
126+
uint32_t tempPW3 = div100((uint32_t)currentStatus.PW1 * staged_req_fuel_mult_sec); //This is ONLY needed in in table mode. Auto mode only calculates the difference.
127+
128+
uint8_t stagingSplit = get3DTableValue(&stagingTable, currentStatus.fuelLoad, currentStatus.RPM);
129+
currentStatus.PW1 = div100((100U - stagingSplit) * tempPW1);
130+
currentStatus.PW1 += inj_opentime_uS;
131+
132+
//PW2 is used temporarily to hold the secondary injector pulsewidth. It will be assigned to the correct channel below
133+
if(stagingSplit > 0)
134+
{
135+
currentStatus.stagingActive = true; //Set the staging active flag
136+
currentStatus.PW2 = div100(stagingSplit * tempPW3);
137+
currentStatus.PW2 += inj_opentime_uS;
138+
}
139+
else
140+
{
141+
currentStatus.stagingActive = false; //Clear the staging active flag
142+
currentStatus.PW2 = 0;
143+
}
144+
}
145+
else if(configPage10.stagingMode == STAGING_MODE_AUTO)
146+
{
147+
currentStatus.PW1 = tempPW1;
148+
//If automatic mode, the primary injectors are used all the way up to their limit (Configured by the pulsewidth limit setting)
149+
//If they exceed their limit, the extra duty is passed to the secondaries
150+
if(tempPW1 > pwLimit)
151+
{
152+
currentStatus.stagingActive = true; //Set the staging active flag
153+
uint32_t extraPW = tempPW1 - pwLimit + inj_opentime_uS; //The open time must be added here AND below because tempPW1 does not include an open time. The addition of it here takes into account the fact that pwLlimit does not contain an allowance for an open time.
154+
currentStatus.PW1 = pwLimit;
155+
currentStatus.PW2 = udiv_32_16(extraPW * staged_req_fuel_mult_sec, staged_req_fuel_mult_pri); //Convert the 'left over' fuel amount from primary injector scaling to secondary
156+
currentStatus.PW2 += inj_opentime_uS;
157+
}
158+
else
159+
{
160+
//If tempPW1 < pwLImit it means that the entire fuel load can be handled by the primaries and staging is inactive.
161+
currentStatus.PW1 += inj_opentime_uS; //Add the open time back in
162+
currentStatus.stagingActive = false; //Clear the staging active flag
163+
currentStatus.PW2 = 0; //Secondary PW is simply set to 0 as it is not required
164+
}
165+
}
166+
167+
//Allocate the primary and secondary pulse widths based on the fuel configuration
168+
switch (configPage2.nCylinders)
169+
{
170+
case 1:
171+
//Nothing required for 1 cylinder, channels are correct already
172+
break;
173+
case 2:
174+
//Primary pulsewidth on channels 1 and 2, secondary on channels 3 and 4
175+
currentStatus.PW3 = currentStatus.PW2;
176+
currentStatus.PW4 = currentStatus.PW2;
177+
currentStatus.PW2 = currentStatus.PW1;
178+
break;
179+
case 3:
180+
//6 channels required for 'normal' 3 cylinder staging support
181+
#if INJ_CHANNELS >= 6
182+
//Primary pulsewidth on channels 1, 2 and 3, secondary on channels 4, 5 and 6
183+
currentStatus.PW4 = currentStatus.PW2;
184+
currentStatus.PW5 = currentStatus.PW2;
185+
currentStatus.PW6 = currentStatus.PW2;
186+
#else
187+
//If there are not enough channels, then primary pulsewidth is on channels 1, 2 and 3, secondary on channel 4
188+
currentStatus.PW4 = currentStatus.PW2;
189+
#endif
190+
currentStatus.PW2 = currentStatus.PW1;
191+
currentStatus.PW3 = currentStatus.PW1;
192+
break;
193+
case 4:
194+
if( (configPage2.injLayout == INJ_SEQUENTIAL) || (configPage2.injLayout == INJ_SEMISEQUENTIAL) )
195+
{
196+
//Staging with 4 cylinders semi/sequential requires 8 total channels
197+
#if INJ_CHANNELS >= 8
198+
currentStatus.PW5 = currentStatus.PW2;
199+
currentStatus.PW6 = currentStatus.PW2;
200+
currentStatus.PW7 = currentStatus.PW2;
201+
currentStatus.PW8 = currentStatus.PW2;
202+
203+
currentStatus.PW2 = currentStatus.PW1;
204+
currentStatus.PW3 = currentStatus.PW1;
205+
currentStatus.PW4 = currentStatus.PW1;
206+
#else
207+
//This is an invalid config as there are not enough outputs to support sequential + staging
208+
//Put the staging output to the non-existent channel 5
209+
currentStatus.PW5 = currentStatus.PW2;
210+
#endif
211+
}
212+
else
213+
{
214+
currentStatus.PW3 = currentStatus.PW2;
215+
currentStatus.PW4 = currentStatus.PW2;
216+
currentStatus.PW2 = currentStatus.PW1;
217+
}
218+
break;
219+
220+
case 5:
221+
//No easily supportable 5 cylinder staging option unless there are at least 5 channels
222+
#if INJ_CHANNELS >= 5
223+
if (configPage2.injLayout != INJ_SEQUENTIAL)
224+
{
225+
currentStatus.PW5 = currentStatus.PW2;
226+
}
227+
#if INJ_CHANNELS >= 6
228+
currentStatus.PW6 = currentStatus.PW2;
229+
#endif
230+
#endif
231+
232+
currentStatus.PW2 = currentStatus.PW1;
233+
currentStatus.PW3 = currentStatus.PW1;
234+
currentStatus.PW4 = currentStatus.PW1;
235+
break;
236+
237+
case 6:
238+
#if INJ_CHANNELS >= 6
239+
//8 cylinder staging only if not sequential
240+
if (configPage2.injLayout != INJ_SEQUENTIAL)
241+
{
242+
currentStatus.PW4 = currentStatus.PW2;
243+
currentStatus.PW5 = currentStatus.PW2;
244+
currentStatus.PW6 = currentStatus.PW2;
245+
}
246+
#if INJ_CHANNELS >= 8
247+
else
248+
{
249+
//If there are 8 channels, then the 6 cylinder sequential option is available by using channels 7 + 8 for staging
250+
currentStatus.PW7 = currentStatus.PW2;
251+
currentStatus.PW8 = currentStatus.PW2;
252+
253+
currentStatus.PW4 = currentStatus.PW1;
254+
currentStatus.PW5 = currentStatus.PW1;
255+
currentStatus.PW6 = currentStatus.PW1;
256+
}
257+
#endif
258+
#endif
259+
currentStatus.PW2 = currentStatus.PW1;
260+
currentStatus.PW3 = currentStatus.PW1;
261+
break;
262+
263+
case 8:
264+
#if INJ_CHANNELS >= 8
265+
//8 cylinder staging only if not sequential
266+
if (configPage2.injLayout != INJ_SEQUENTIAL)
267+
{
268+
currentStatus.PW5 = currentStatus.PW2;
269+
currentStatus.PW6 = currentStatus.PW2;
270+
currentStatus.PW7 = currentStatus.PW2;
271+
currentStatus.PW8 = currentStatus.PW2;
272+
}
273+
#endif
274+
currentStatus.PW2 = currentStatus.PW1;
275+
currentStatus.PW3 = currentStatus.PW1;
276+
currentStatus.PW4 = currentStatus.PW1;
277+
break;
278+
279+
default:
280+
//Assume 4 cylinder non-seq for default
281+
currentStatus.PW3 = currentStatus.PW2;
282+
currentStatus.PW4 = currentStatus.PW2;
283+
currentStatus.PW2 = currentStatus.PW1;
284+
break;
285+
}
286+
}
287+
else
288+
{
289+
if(maxInjOutputs >= 2) { currentStatus.PW2 = currentStatus.PW1; }
290+
else { currentStatus.PW2 = 0; }
291+
if(maxInjOutputs >= 3) { currentStatus.PW3 = currentStatus.PW1; }
292+
else { currentStatus.PW3 = 0; }
293+
if(maxInjOutputs >= 4) { currentStatus.PW4 = currentStatus.PW1; }
294+
else { currentStatus.PW4 = 0; }
295+
if(maxInjOutputs >= 5) { currentStatus.PW5 = currentStatus.PW1; }
296+
else { currentStatus.PW5 = 0; }
297+
if(maxInjOutputs >= 6) { currentStatus.PW6 = currentStatus.PW1; }
298+
else { currentStatus.PW6 = 0; }
299+
if(maxInjOutputs >= 7) { currentStatus.PW7 = currentStatus.PW1; }
300+
else { currentStatus.PW7 = 0; }
301+
if(maxInjOutputs >= 8) { currentStatus.PW8 = currentStatus.PW1; }
302+
else { currentStatus.PW8 = 0; }
303+
304+
currentStatus.stagingActive = false; //Clear the staging active flag
305+
306+
}
307+
308+
}
309+
#pragma GCC diagnostic pop
Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,22 @@
1-
/** \file speeduino.h
2-
* @brief Speeduino main file containing initial setup and system loop functions
3-
* @author Josh Stewart
4-
*
5-
* This file contains the main system loop of the Speeduino core and thus much of the logic of the fuel and ignition algorithms is contained within this
6-
* It is where calls to all the auxiliary control systems, sensor reads, comms etc are made
7-
*
8-
* It also contains the setup() function that is called by the bootloader on system startup
9-
*
10-
*/
1+
#pragma once
112

12-
#ifndef SPEEDUINO_H
13-
#define SPEEDUINO_H
14-
//#include "globals.h"
3+
#include <stdint.h>
154

16-
#define CRANK_RUN_HYSTER 15
5+
/**
6+
* @brief This function calculates the required pulsewidth time (in us) given the current system state
7+
*
8+
* @param REQ_FUEL The required fuel value in uS, as calculated by TunerStudio
9+
* @param VE Lookup from the main fuel table. This can either have been MAP or TPS based, depending on the algorithm used
10+
* @param MAP In KPa, read from the sensor (This is used when performing a multiply of the map only. It is applicable in both Speed density and Alpha-N)
11+
* @param corrections Sum of Enrichment factors (Cold start, acceleration). This is a multiplication factor (Eg to add 10%, this should be 110)
12+
* @param injOpen Injector opening time. The time the injector take to open minus the time it takes to close (Both in uS)
13+
* @return uint16_t The injector pulse width in uS
14+
*/
15+
uint16_t PW(int REQ_FUEL, uint8_t VE, long MAP, uint16_t corrections, int injOpen);
1716

18-
void setup(void);
19-
void loop(void);
20-
uint16_t PW(int REQ_FUEL, byte VE, long MAP, uint16_t corrections, int injOpen);
21-
uint8_t getVE1(void);
22-
int8_t getAdvance1(void);
2317
uint16_t calculatePWLimit();
18+
2419
void calculateStaging(uint32_t);
25-
void calculateIgnitionAngles(uint16_t dwellAngle);
26-
void checkLaunchAndFlatShift();
2720

2821
extern uint16_t req_fuel_uS; /**< The required fuel variable (As calculated by TunerStudio) in uS */
2922
extern uint16_t inj_opentime_uS; /**< The injector opening time. This is set within Tuner Studio, but stored here in uS rather than mS */
@@ -39,11 +32,5 @@ extern uint16_t inj_opentime_uS; /**< The injector opening time. This is set wit
3932
* - staged_req_fuel_mult_pri = 300% (The primary injectors would have to run 3x the overall PW in order to be the equivalent of the full 750cc capacity
4033
* - staged_req_fuel_mult_sec = 150% (The secondary injectors would have to run 1.5x the overall PW in order to be the equivalent of the full 750cc capacity
4134
*/
42-
///@{
4335
extern uint16_t staged_req_fuel_mult_pri;
4436
extern uint16_t staged_req_fuel_mult_sec;
45-
///@}
46-
47-
48-
49-
#endif

0 commit comments

Comments
 (0)