rusEFI
The most advanced open source ECU
Loading...
Searching...
No Matches
hellen_board_id.cpp
Go to the documentation of this file.
1/**
2 * @file boards/hellen/hellen_board_id.cpp
3 * @brief Board-Id detector for Hellen boards
4 *
5 * @author andreika <prometheus.pcb@gmail.com>
6 * @author Andrey Belomutskiy, (c) 2012-2022
7 *
8 * The main idea is to measure the capacitor charge/discharge time
9 * through a series resistors using standard digital I/O pins.
10 * One pin is used to provide a Vcc(3.3) or Vdd(0) voltage to the capacitor
11 * through a resistor, and another pin is used as a digital input. Then vice versa.
12 *
13 * The algo:
14 * 1) Completely discharge the capacitor (all pins are low)
15 * 2) Charge the capacitor until the voltage crosses the 0->1 voltage threshold (Vt) and measure the charging time #1 (Tc1).
16 * 3) Immediately discharge the capacitor to some unknown low voltage (Vl) - it should be well below the Vt threshold,
17 * using the same period of time used for charging as the discharge period (Td = Tc1).
18 * 4) Immediately charge the capacitor again and measure the time crossing the same 0->1 voltage threshold again (Tc2).
19 * 5) Repeat the procedure several times to get more precise timings.
20 * 6) Do some math and find the R and C values.
21 * 7) Board_Id = the unique combination of indices of the "measured" R1 and R2.
22 *
23 * The math proof:
24 * - Charging formula #1:
25 * Vt = V�� * (1 - exp(-Tc1 / RC))
26 * - Discharging formula:
27 * Vl = Vt * exp(-Td / RC)
28 * - Charging formula #2:
29 * Vl = V�� * (1 - exp(-Tl / (RC)))
30 * - Where Tl is a charging time from 0 to Vl:
31 * Tl = Tc1 - Tc2
32 * - Solve the equations:
33 * Vl = V�� * (1 - exp(-Tl / RC)) = Vt * exp(-Td / RC)
34 * V�� * (1 - exp(-Tl / RC)) = V�� * (1 - exp(-Tc1 / RC)) * exp(-Td / RC)
35 * (1 - exp(-Tl / RC)) = (1 - exp(-Tc1 / RC)) * exp(-Td / RC)
36 * - Simplify the equation:
37 * X = exp(-1/(RC))
38 * (1 - X^Tc1) * X^Td + X^Tl - 1 = 0
39 *
40 * X^Td - X^(Tc1+Td) + X^(Tc2-Tc1) - 1 = 0
41 *
42 * Td, Tc1 and Tc2 are known.
43 * - Solve the power function for X and get the desired R or C.
44 *
45 * We use Newton's method (a fast-converging numerical solver when the 1st derivative is known)
46 * with estimated initial values.
47 */
48
49#include "pch.h"
50#include "hellen_meta.h"
51#include "digital_input_exti.h"
52
53#include "hellen_board_id.h"
54
55/* We use known standard E24 series resistor values (1%) to find the closest match.
56 The 16 major values should have a guaranteed spacing of 15% in a row (1% R tolerance + 10% C tolerance)
57 These should match the values in the gen_board_id script!
58*/
60
61//#define HELLEN_BOARD_ID_DEBUG
62
63// todo: error: this use of "defined" may not be portable [-Werror=expansion-to-defined ?!
64// huh? #define HELLEN_BOARD_ID_CODE_NEEDED (defined( HELLEN_BOARD_ID_PIN_1) && !defined(HW_HELLEN_SKIP_BOARD_TYPE))
65
66#if EFI_PROD_CODE && defined( HELLEN_BOARD_ID_PIN_1) && !defined(HW_HELLEN_SKIP_BOARD_TYPE)
67
68static void hellenBoardIdInputCallback(void *arg, efitick_t nowNt) {
69 UNUSED(arg);
71 // Now start discharging immediately! This should be the first command in the interrupt handler.
72 palClearPad(state->rOutputPinPort, state->rOutputPinIdx);
73
74 state->timeChargeNt = nowNt;
75
76 chibios_rt::CriticalSectionLocker csl;
77 chSemSignalI(&state->boardId_wake); // no need to call chSchRescheduleS() because we're inside the ISR
78}
79
80#endif /* EFI_PROD_CODE && HELLEN_BOARD_ID_CODE_NEEDED */
81
82// Newton's numerical method (x is R and y is C, or vice-versa)
83float HellenBoardIdSolver::solve(float Tc1, float Tc2, float x0, float y, float deltaX) {
84 // the discharge time equals to the charge time
85 float Td = Tc1;
86
87 float iC = -1.0f / y;
88 k1 = iC * Td;
89 k2 = iC * (Tc1 + Td);
90 k3 = iC * (Tc1 - Tc2);
91
92 // the same method works for R (if C is known) or C (if R is known)
93 auto result = NewtonsMethodSolver::solve(x0, deltaX, 20);
94
95 // since we had https://github.com/rusefi/rusefi/issues/4084 let's add paranoia check
96 // All real cases seem to converge in <= 5 iterations, so we don't need to try more than 20.
97 if (!result) {
98 criticalError("hellen boardID is broken");
99 return 0;
100 }
101
102 return result.Value;
103}
104
105float HellenBoardIdFinderBase::findClosestResistor(float R, bool testOnlyMajorSeries, int *rIdx) {
106 // the first "major" resistor uses less values (with more spacing between them) so that even less precise method cannot fail.
107 static const float rOnlyMajorValues[] = {
108 HELLEN_BOARD_ID_MAJOR_RESISTORS
109 };
110 // the minor resistor is always measured after the major one, when the exact capacitance is already knows,
111 // so we can use more values and detect them with better precision.
112 static const float rAllValues[] = {
113 // these are equal to the major values and should be used first
114 HELLEN_BOARD_ID_MAJOR_RESISTORS
115 // these are extended series if 256 board IDs aren't enough (16*16).
116 HELLEN_BOARD_ID_MINOR_RESISTORS
117 };
118
119 size_t rValueSize = testOnlyMajorSeries ? efi::size(rOnlyMajorValues) : efi::size(rAllValues);
120
121 *rIdx = -1;
122 float minDelta = 1.e6f;
123 for (size_t i = 0; i < rValueSize; i++) {
124 // Find the nearest resistor by least ratio error
125 float delta = std::abs(1 - (R / rAllValues[i]));
126 if (delta < minDelta) {
127 minDelta = delta;
128 *rIdx = i;
129#ifdef HELLEN_BOARD_ID_DEBUG
130 efiPrintf("* [%d] R = %.0f, delta = %f", i, rAllValues[i], delta);
131#endif /* HELLEN_BOARD_ID_DEBUG */
132 }
133 }
134 return rAllValues[*rIdx];
135}
136
138 constexpr float Vcc = 3.3f - 0.1f; // STM32 digital I/O voltage (adjusted for minor voltage drop)
139 constexpr float V01 = Vcc * 0.5f; // let it be 1.6 volts (closer to the datasheet value), the exact value doesn't matter
140 // macos compiler doesn't like log() in constexpr
141 float log1V01Vcc = log(1.0f - V01 / Vcc);
142 // this is only an estimated value, we cannot use it for Board-ID detection!
143 float Rest = -Tc1_us / (C * log1V01Vcc);
144 return Rest;
145}
146
147float HellenBoardIdFinderBase::calc(float Tc1_us, float Tc2_us, float Rest, float C, bool testOnlyMajorSeries, float *Rmeasured, float *newC, int *rIdx) {
148 constexpr float Cest = HELLEN_BOARD_ID_CAPACITOR;
149 // Now calculate the resistance value
150 HellenBoardIdSolver rSolver;
151
152 // solve the equation for R (1 Ohm precision is more than enough)
153 *Rmeasured = rSolver.solve(Tc1_us, Tc2_us, Rest, C, 1.0f);
154
155 // add 22 Ohms for pin's internal resistance
156 // (according to the STM32 datasheets, the voltage drop on an output pin can be up to 0.4V for 8 mA current)
157 // Actual measured value was is in the low-20s on most chips.
158 constexpr float Rinternal = 22.0f;
159 float R = findClosestResistor(*Rmeasured - Rinternal, testOnlyMajorSeries, rIdx);
160
161 // Find the 'real' capacitance value and use it for the next resistor iteration (gives more precision)
162 HellenBoardIdSolver cSolver;
163
164 // We expect the capacitance to be +-10%
165 constexpr float capacitorPrecision = 0.1f;
166 constexpr float Cmin = Cest * (1.0f - capacitorPrecision);
167 constexpr float Cmax = Cest * (1.0f + capacitorPrecision);
168
169 // solve the equation for C (1% precision)
170 *newC = cSolver.solve(Tc1_us, Tc2_us, Cmin, R + Rinternal, 0.01f);
171 // in case something went wrong, we must be in the allowed range
172 *newC = clampF(Cmin, *newC, Cmax);
173
174 return R;
175}
176
177template <size_t NumPins>
178bool HellenBoardIdFinder<NumPins>::measureChargingTimes(int i, float & Tc1_us, float & Tc2_us) {
179#if EFI_PROD_CODE && defined( HELLEN_BOARD_ID_PIN_1) && !defined(HW_HELLEN_SKIP_BOARD_TYPE)
180 chSemReset(&state.boardId_wake, 0);
181
182 // full charge/discharge time, and also 'timeout' time
183 const int Tf_us = 50000; // 50 ms is more than enough to "fully" discharge the capacitor with any two resistors used at the same time.
184
185 // 1. Fully discharge the capacitor through both resistors (faster)
186 for (size_t k = 0; k < NumPins; k++) {
187 palClearPad(getBrainPinPort(rPins[k]), getBrainPinIndex(rPins[k]));
188 palSetPadMode(getBrainPinPort(rPins[k]), getBrainPinIndex(rPins[k]), PAL_MODE_OUTPUT_PUSHPULL);
189 }
190 // wait max. time because we don't know the resistor values yet
191 chThdSleepMicroseconds(Tf_us);
192
193 // use one pin as an charge/discharge controlling output
194 state.rOutputPinPort = getBrainPinPort(rPins[i]);
195 state.rOutputPinIdx = getBrainPinIndex(rPins[i]);
196 palSetPadMode(state.rOutputPinPort, state.rOutputPinIdx, PAL_MODE_OUTPUT_PUSHPULL);
197
198 // use another pin as an input to detect 0->1 crossings
199 int inputIdx = 1 - i;
200 state.rInputPinPort = getBrainPinPort(rPins[inputIdx]);
201 state.rInputPinIdx = getBrainPinIndex(rPins[inputIdx]);
202 // set only high-Z input mode, no pull-ups/pull-downs allowed!
203 palSetPadMode(state.rInputPinPort, state.rInputPinIdx, PAL_MODE_INPUT);
204 efiExtiEnablePin("boardId", rPins[inputIdx], PAL_EVENT_MODE_RISING_EDGE, hellenBoardIdInputCallback, (void *)&state);
205
206 int pinState = palReadPad(state.rInputPinPort, state.rInputPinIdx);
207 if (pinState != 0) {
208 // the input pin state should be low when the capacitor is fully discharged
209 efiPrintf("* Board detection error!");
210 return false;
211 }
212
213 // Timestamps:
214 // t1 = Starts charging from 0v
215 // t2 = Threshold reached, starts discharging
216 // t3 = Random voltage reached, starts charging again
217 // t4 = Threshold reached again, process finished.
218
219 // 2. Start charging until the input pin triggers (V01 threshold is reached)
220 state.timeChargeNt = 0;
221 efitick_t t1 = getTimeNowNt();
222 palSetPad(state.rOutputPinPort, state.rOutputPinIdx);
223 chSemWaitTimeout(&state.boardId_wake, TIME_US2I(Tf_us));
224 efitick_t t2 = state.timeChargeNt;
225
226 // 3. At the moment, the discharging has already been started!
227 // Meanwhile we need to do some checks - until some pre-selected voltage is presumably reached.
228
229 // if voltage didn't change on the input pin (or changed impossibly fast), then the charging didn't start,
230 // meaning there's no capacitor and/or resistors on these pins.
231 if (t2 - t1 < US2NT(100)) {
232 efiPrintf("* Hellen Board ID circuitry wasn't detected! Aborting!");
233 return false;
234 }
235
236 // 4. calculate the first charging time
237 efidur_t Tc1_nt = t2 - t1;
238 Tc1_us = NT2USF(Tc1_nt);
239 // We use the same 'charging time' to discharge the capacitor to some random voltage below the threshold voltage.
240 efidur_t Td_nt = Tc1_nt;
241
242 // 5. And now just wait for the rest of the discharge process...
243 // Spin wait since chThdSleepMicroseconds() lacks the resolution we need
244 efitick_t t3 = t2 + Td_nt;
245 while (getTimeNowNt() < t3) ;
246
247 // the input pin state should be low when the capacitor is discharged to Vl
248 pinState = palReadPad(state.rInputPinPort, state.rInputPinIdx);
249
250 // 6. And immediately begin charging again until the threshold voltage is reached!
251 state.timeChargeNt = 0;
252 palSetPad(state.rOutputPinPort, state.rOutputPinIdx);
253
254 // Wait for the charging completion
255 chSemReset(&state.boardId_wake, 0);
256 chSemWaitTimeout(&state.boardId_wake, TIME_US2I(Tf_us));
257 efitick_t t4 = state.timeChargeNt;
258
259 // 7. calculate the second charge time
260 Tc2_us = NT2USF(t4 - t3);
261
262 float Td_us = NT2USF(Td_nt);
263#ifdef HELLEN_BOARD_ID_DEBUG
264 efiPrintf("* dTime2-1 = %d", (int)(t2 - t1));
265 efiPrintf("* dTime3-2 = %d", (int)(t3 - t2));
266 efiPrintf("* dTime4-3 = %d", (int)(t4 - t3));
267 efiPrintf("* Tc1 = %f, Tc2 = %f, Td = %f", Tc1_us, Tc2_us, Td_us);
268#endif /* HELLEN_BOARD_ID_DEBUG */
269
270 // sanity checks
271 if (pinState != 0) {
272 efiPrintf("* Board detection error! (Td=%f is too small)", Td_us);
273 return false;
274 }
275
276 if (t4 <= t3) {
277 efiPrintf("* Estimates are out of limit! Something went wrong. Aborting!");
278 return false;
279 }
280
281 efiExtiDisablePin(rPins[inputIdx]);
282#endif /* EFI_PROD_CODE */
283 return true;
284}
285
286template <size_t NumPins>
287bool HellenBoardIdFinder<NumPins>::measureChargingTimesAveraged(int i, float & Tc1_us, float & Tc2_us) {
288 const int numTries = 3;
289
290 Tc1_us = 0;
291 Tc2_us = 0;
292 for (int tries = 0; tries < numTries; tries++) {
293 // get the charging times
294 float Tc1i_us = 0, Tc2i_us = 0;
295 if (!measureChargingTimes(i, Tc1i_us, Tc2i_us))
296 return false;
297 Tc1_us += Tc1i_us;
298 Tc2_us += Tc2i_us;
299 }
300
301 // averaging
302 Tc1_us /= numTries;
303 Tc2_us /= numTries;
304
305 return true;
306}
307
309 int boardId = -1;
310#if defined( HELLEN_BOARD_ID_PIN_1) && !defined(HW_HELLEN_SKIP_BOARD_TYPE)
311 efiPrintf("Starting Hellen Board ID detection...");
312 Timer t;
313 t.reset();
314
315 const int numPins = 2;
316 Gpio rPins[numPins] = { HELLEN_BOARD_ID_PIN_1, HELLEN_BOARD_ID_PIN_2};
317
318 // We start from the estimated capacitance, but the real one can be +-10%
319 float C = HELLEN_BOARD_ID_CAPACITOR;
320
321 // we need to find the resistor values connected to the mcu pins and to the capacitor.
322 float R[numPins] = { 0 };
323 int rIdx[numPins] = { 0 };
324
325 HellenBoardIdFinder<numPins> finder(rPins);
326
327 // init some ChibiOs objects
328 chSemObjectInit(&finder.state.boardId_wake, 0);
329
330 // R1 is the first, R2 is the second
331 for (int i = 0; i < numPins; i++) {
332#ifdef HELLEN_BOARD_ID_DEBUG
333 efiPrintf("*** Resistor R%d...", i + 1);
334#endif /* HELLEN_BOARD_ID_DEBUG */
335
336 float Tc1_us = 0, Tc2_us = 0;
337 // We need several measurements for each resistor to increase the precision.
338 // But if any of the measurements fails, then abort!
339 if (!finder.measureChargingTimesAveraged(i, Tc1_us, Tc2_us))
340 break;
341
342 // Now roughly estimate the resistor value using the approximate threshold voltage.
343 float Rest = finder.calcEstimatedResistance(Tc1_us, C);
344 // check if we are inside the range
345 if (Rest < 300.0f || Rest > 15000.0f) {
346 efiPrintf("* Unrealistic estimated resistor value (%f)! Aborting!", Rest);
347 break;
348 }
349
350 // for the first resistor, we test only "major" values because we don't know the exact capacitance yet
351 bool testOnlyMajorSeries = (i == 0);
352
353 float Rmeasured, newC;
354 // Now calculate the R and C
355 R[i] = finder.calc(Tc1_us, Tc2_us, Rest, C, testOnlyMajorSeries, &Rmeasured, &newC, &rIdx[i]);
356 C = newC;
357
358#ifdef HELLEN_BOARD_ID_DEBUG
359 efiPrintf("* R = %f, Rmeasured = %f, Rest = %f, Creal = %f", R[i], Rmeasured, Rest, C);
360#endif /* HELLEN_BOARD_ID_DEBUG */
361 }
362
363 // in case the process was aborted
364 for (size_t k = 0; k < numPins; k++) {
365 efiExtiDisablePin(rPins[k]);
366 // release the pins
367 palSetPadMode(getBrainPinPort(rPins[k]), getBrainPinIndex(rPins[k]), PAL_MODE_RESET);
368 }
369
370 float elapsed_Ms = t.getElapsedSeconds() * 1000;
371
372 // Check that all resistors were actually detected
373 bool allRValid = true;
374 for (size_t i = 0; i < numPins; i++) {
375 allRValid &= R[i] != 0;
376 }
377
378 // Decode board ID only if all resistors could be decoded, otherwise we return -1
379 if (allRValid) {
380 boardId = HELLEN_GET_BOARD_ID(rIdx[0], rIdx[1]);
381 } else {
382 boardId = -1;
383 }
384
385 efiPrintf("* RESULT: BoardId = %d, R1 = %.0f, R2 = %.0f (Elapsed time: %.1f ms)", boardId, R[0], R[1], elapsed_Ms);
386#endif /* HELLEN_BOARD_ID_PIN_1 */
387 return boardId;
388}
float findClosestResistor(float R, bool testOnlyMajorSeries, int *rIdx)
float calc(float Tc1_us, float Tc2_us, float Rest, float C, bool testOnlyMajorSeries, float *Rmeasured, float *Cest, int *rIdx)
float calcEstimatedResistance(float Tc1_us, float C)
bool measureChargingTimesAveraged(int i, float &Tc1_us, float &Tc2_us)
HellenBoardIdFinderState state
bool measureChargingTimes(int i, float &Tc1_us, float &Tc2_us)
float solve(float Tc1, float Tc2, float x0, float y, float deltaX)
void efiExtiDisablePin(brain_pin_e brainPin)
int efiExtiEnablePin(const char *msg, brain_pin_e brainPin, uint32_t mode, ExtiCallback cb, void *cb_data)
efitick_t getTimeNowNt()
Definition efitime.cpp:19
C
static void hellenBoardIdInputCallback(void *arg, efitick_t nowNt)
int detectHellenBoardId()
Board-Id detector for Hellen boards.
UNUSED(samplingTimeSeconds)
int getBrainPinIndex(Gpio brainPin)
ioportid_t getBrainPinPort(brain_pin_e brainPin)
efitick_t efidur_t
state("state", SensorCategory.SENSOR_INPUTS, FieldType.INT8, 1871, 1.0, -1.0, -1.0, "")