GCC Code Coverage Report


Directory: ./
File: firmware/util/math/efi_pid.cpp
Date: 2025-10-03 00:57:22
Coverage Exec Excl Total
Lines: 70.3% 109 0 155
Functions: 64.5% 20 0 31
Branches: 60.9% 28 0 46
Decisions: 63.3% 19 - 30

Line Branch Decision Exec Source
1 /**
2 * @file pid.cpp
3 *
4 * https://en.wikipedia.org/wiki/Feedback
5 * http://en.wikipedia.org/wiki/PID_controller
6 *
7 * @date Sep 16, 2014
8 * @author Andrey Belomutskiy, (c) 2012-2020
9 */
10
11 #include "pch.h"
12
13 #include "efi_pid.h"
14 #include "math.h"
15
16 2808 Pid::Pid() {
17 2808 initPidClass(nullptr);
18 2808 }
19
20 6 Pid::Pid(pid_s *p_parameters) {
21 6 initPidClass(p_parameters);
22 6 }
23
24 5319 void Pid::initPidClass(pid_s *p_parameters) {
25 5319 parameters = p_parameters;
26 5319 resetCounter = 0;
27
28 5319 Pid::reset();
29 5319 }
30
31 2206 static bool isClose(float a, float b) {
32 2206 bool result = (a == b);
33
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 2190 times.
2/2
✓ Decision 'true' taken 16 times.
✓ Decision 'false' taken 2190 times.
2206 if (!result) {
34 16 efiPrintf("PID: not same %f %f", a, b);
35 }
36 2206 return result;
37 }
38
39 454 bool Pid::isSame(const pid_s *p_parameters) const {
40
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 454 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 454 times.
454 if (!parameters) {
41 // this 'null' could happen on first execution during initialization
42 return false;
43 }
44
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 454 times.
454 efiAssert(ObdCode::OBD_PCM_Processor_Fault, p_parameters != nullptr, "PID::isSame nullptr", false);
45 454 return isClose(parameters->pFactor, p_parameters->pFactor)
46
1/2
✓ Branch 1 taken 438 times.
✗ Branch 2 not taken.
438 && isClose(parameters->iFactor, p_parameters->iFactor)
47
1/2
✓ Branch 1 taken 438 times.
✗ Branch 2 not taken.
438 && isClose(parameters->dFactor, p_parameters->dFactor)
48
1/2
✓ Branch 1 taken 438 times.
✗ Branch 2 not taken.
438 && isClose(parameters->offset, p_parameters->offset)
49
3/4
✓ Branch 0 taken 438 times.
✓ Branch 1 taken 16 times.
✓ Branch 3 taken 438 times.
✗ Branch 4 not taken.
892 && isClose(parameters->periodMs, p_parameters->periodMs);
50 }
51
52 /**
53 * @param Controller input / process output
54 * @returns Output from the PID controller / the input to the process
55 */
56 44 float Pid::getOutput(float p_target, float p_input) {
57
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 44 times.
44 efiAssert(ObdCode::OBD_PCM_Processor_Fault, parameters != nullptr, "PID::getOutput nullptr", 0);
58 44 float dTime = MS2SEC(GET_PERIOD_LIMITED(parameters));
59 44 return getOutput(p_target, p_input, dTime);
60 }
61
62 200 float Pid::getUnclampedOutput(float p_target, float p_input, float dTime) {
63 200 target = p_target;
64 200 input = p_input;
65 200 float error = (target - input) * errorAmplificationCoef;
66
67 200 float pTerm = parameters->pFactor * error;
68 200 updateITerm(parameters->iFactor * dTime * error);
69 200 dTerm = parameters->dFactor / dTime * (error - previousError);
70
71 200 previousError = error;
72
73
2/2
✓ Branch 0 taken 154 times.
✓ Branch 1 taken 46 times.
2/2
✓ Decision 'true' taken 154 times.
✓ Decision 'false' taken 46 times.
200 if (dTime <=0) {
74 154 warning(ObdCode::CUSTOM_PID_DTERM, "PID: unexpected dTime");
75 154 return pTerm + getOffset();
76 }
77
78 46 return pTerm + iTerm + dTerm + getOffset();
79 }
80
81 /**
82 * @param dTime seconds probably? :)
83 */
84 200 float Pid::getOutput(float p_target, float p_input, float dTime) {
85 200 float l_output = getUnclampedOutput(p_target, p_input, dTime);
86
87
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 194 times.
2/2
✓ Decision 'true' taken 6 times.
✓ Decision 'false' taken 194 times.
200 if (l_output > parameters->maxValue) {
88 6 l_output = parameters->maxValue;
89
2/2
✓ Branch 1 taken 11 times.
✓ Branch 2 taken 183 times.
2/2
✓ Decision 'true' taken 11 times.
✓ Decision 'false' taken 183 times.
194 } else if (l_output < getMinValue()) {
90 11 l_output = getMinValue();
91 }
92 200 output = l_output;
93 200 return output;
94 }
95
96 1 void Pid::updateFactors(float pFactor, float iFactor, float dFactor) {
97 1 parameters->pFactor = pFactor;
98 1 parameters->iFactor = iFactor;
99 1 parameters->dFactor = dFactor;
100 1 reset();
101 1 }
102
103 5479 void Pid::reset() {
104 5479 dTerm = iTerm = 0;
105 5479 output = input = target = previousError = 0;
106 5479 errorAmplificationCoef = 1.0f;
107 5479 resetCounter++;
108 5479 }
109
110 float Pid::getP() const {
111 return parameters->pFactor;
112 }
113
114 float Pid::getI() const {
115 return parameters->iFactor;
116 }
117
118 float Pid::getPrevError() const {
119 return previousError;
120 }
121
122 1132 float Pid::getIntegration() const {
123 1132 return iTerm;
124 }
125
126 float Pid::getD() const {
127 return parameters->dFactor;
128 }
129
130 229 float Pid::getOffset(void) const {
131 229 return parameters->offset;
132 }
133
134 234 float Pid::getMinValue(void) const {
135 234 return parameters->minValue;
136 }
137
138 9 void Pid::setErrorAmplification(float coef) {
139 9 errorAmplificationCoef = coef;
140 9 }
141
142 #if EFI_TUNER_STUDIO
143
144 224 void Pid::postState(pid_status_s& pidStatus) const {
145 224 pidStatus.output = output;
146 224 pidStatus.error = previousError;
147
1/2
✓ Branch 0 taken 224 times.
✗ Branch 1 not taken.
224 pidStatus.pTerm = parameters == nullptr ? 0 : parameters->pFactor * previousError;
148 224 pidStatus.iTerm = iTerm;
149 224 pidStatus.dTerm = dTerm;
150 224 }
151 #endif /* EFI_TUNER_STUDIO */
152
153 void Pid::sleep() {
154 #if !EFI_UNIT_TEST
155 int periodMs = maxI(10, parameters->periodMs);
156 chThdSleepMilliseconds(periodMs);
157 #endif /* EFI_UNIT_TEST */
158 }
159
160 void Pid::showPidStatus(const char*msg) const {
161 efiPrintf("%s settings: offset=%f P=%.5f I=%.5f D=%.5f period=%dms",
162 msg,
163 getOffset(),
164 parameters->pFactor,
165 parameters->iFactor,
166 parameters->dFactor,
167 parameters->periodMs);
168
169 efiPrintf("%s status: value=%.2f input=%.2f/target=%.2f iTerm=%.5f dTerm=%.5f",
170 msg,
171 output,
172 input,
173 target,
174 iTerm, dTerm);
175
176 }
177
178 229 void Pid::updateITerm(float value) {
179 229 iTerm += value;
180 /**
181 * If we have exceeded the ability of the controlled device to hit target, the I factor will keep accumulating and approach infinity.
182 * Here we limit the I-term #353
183 */
184
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 229 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 229 times.
229 if (iTerm > parameters->maxValue * 100) {
185 iTerm = parameters->maxValue * 100;
186 }
187
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 227 times.
2/2
✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 227 times.
229 if (iTerm > iTermMax) {
188 2 iTerm = iTermMax;
189 }
190
191 // this is kind of a hack. a proper fix would be having separate additional settings 'maxIValue' and 'minIValye'
192
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 229 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 229 times.
229 if (iTerm < -parameters->maxValue * 100)
193 iTerm = -parameters->maxValue * 100;
194
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 229 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 229 times.
229 if (iTerm < iTermMin) {
195 iTerm = iTermMin;
196 }
197 229 }
198
199
200 PidCic::PidCic() {
201 // call our derived reset()
202 PidCic::reset();
203 }
204
205 PidCic::PidCic(pid_s *p_parameters) : Pid(p_parameters) {
206 // call our derived reset()
207 PidCic::reset();
208 }
209
210 void PidCic::reset(void) {
211 Pid::reset();
212
213 totalItermCnt = 0;
214 for (int i = 0; i < PID_AVG_BUF_SIZE; i++)
215 iTermBuf[i] = 0;
216 iTermInvNum = 1.0f / (float)PID_AVG_BUF_SIZE;
217 }
218
219 float PidCic::getOutput(float p_target, float p_input, float dTime) {
220 return getUnclampedOutput(p_target, p_input, dTime);
221 }
222
223 void PidCic::updateITerm(float value) {
224 // use a variation of cascaded integrator-comb (CIC) filtering to get non-overflow iTerm
225 totalItermCnt++;
226 int localBufPos = (totalItermCnt >> PID_AVG_BUF_SIZE_SHIFT) % PID_AVG_BUF_SIZE;
227 int localPrevBufPos = ((totalItermCnt - 1) >> PID_AVG_BUF_SIZE_SHIFT) % PID_AVG_BUF_SIZE;
228
229 // reset old buffer cell
230 if (localPrevBufPos != localBufPos)
231 iTermBuf[localBufPos] = 0;
232 // integrator stage
233 iTermBuf[localBufPos] += value;
234
235 // return moving average of all sums, to smoothen the result
236 float iTermSum = 0;
237 for (int i = 0; i < PID_AVG_BUF_SIZE; i++) {
238 iTermSum += iTermBuf[i];
239 }
240 iTerm = iTermSum * iTermInvNum;
241 }
242
243 703 PidIndustrial::PidIndustrial() : Pid() {
244 703 }
245
246 1 PidIndustrial::PidIndustrial(pid_s *p_parameters) : Pid(p_parameters) {
247 1 }
248
249 29 float PidIndustrial::getOutput(float p_target, float p_input, float dTime) {
250 float ad, bd;
251 29 float error = (p_target - p_input) * errorAmplificationCoef;
252 29 float pTerm = parameters->pFactor * error;
253
254 // calculate dTerm coefficients
255
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 23 times.
2/2
✓ Decision 'true' taken 6 times.
✓ Decision 'false' taken 23 times.
29 if (fabsf(derivativeFilterLoss) > DBL_EPSILON) {
256 // restore Td in the Standard form from the Parallel form: Td = Kd / Kc
257 6 float Td = parameters->dFactor / parameters->pFactor;
258 // calculate the backward differences approximation of the derivative term
259 6 ad = Td / (Td + dTime / derivativeFilterLoss);
260 6 bd = parameters->pFactor * ad / derivativeFilterLoss;
261 } else {
262 // According to the Theory of limits, if p.derivativeFilterLoss -> 0, then
263 // lim(ad) = 0; lim(bd) = p.pFactor * Td / dTime = p.dFactor / dTime
264 // i.e. dTerm becomes equal to Pid's
265 23 ad = 0.0f;
266 23 bd = parameters->dFactor / dTime;
267 }
268
269 // (error - previousError) = (target-input) - (target-prevousInput) = -(input - prevousInput)
270 29 dTerm = dTerm * ad + (error - previousError) * bd;
271
272 29 updateITerm(parameters->iFactor * dTime * error);
273
274 // calculate output and apply the limits
275 29 float l_output = pTerm + iTerm + dTerm + getOffset();
276 29 float limitedOutput = limitOutput(l_output);
277
278 // apply the integrator anti-windup on top of the "normal" iTerm change above
279 // If p.antiwindupFreq = 0, then iTerm is equal to PidParallelController's
280 29 iTerm += dTime * antiwindupFreq * (limitedOutput - l_output);
281
282 // update the state
283 29 previousError = error;
284
285 29 return limitedOutput;
286 }
287
288 29 float PidIndustrial::limitOutput(float v) const {
289
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 29 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 29 times.
29 if (v < getMinValue())
290 v = getMinValue();
291
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 26 times.
2/2
✓ Decision 'true' taken 3 times.
✓ Decision 'false' taken 26 times.
29 if (v > parameters->maxValue)
292 3 v = parameters->maxValue;
293 29 return v;
294 }
295