speeduino/speeduino/table3d_interpolate.cpp

214 lines
8.6 KiB
C++

#include "table3d_interpolate.h"
#include "maths.h"
// ============================= Axis Bin Searching =========================
static inline bool is_in_bin(const table3d_axis_t &testValue, const table3d_axis_t &min, const table3d_axis_t &max)
{
return testValue > min && testValue <= max;
}
// Find the axis index for the top of the bin that covers the test value.
// E.g. 4 in { 1, 3, 5, 7, 9 } would be 2
// We assume the axis is in order.
static inline table3d_dim_t find_bin_max(
table3d_axis_t &value, // Value to search for
const table3d_axis_t *pAxis, // The axis to search
table3d_dim_t minElement, // Axis index of the element with the lowest value (at one end of the array)
table3d_dim_t maxElement, // Axis index of the element with the highest value (at the other end of the array)
table3d_dim_t lastBinMax) // The last result from this call - used to speed up searches
{
// It's quicker to increment/adjust this pointer than to repeatedly
// index the array - minimum 2%, often >5%
const table3d_axis_t *pMax = nullptr;
// minElement is at one end of the array, so the "lowest" bin
// is [minElement, minElement+stride]. Since we're working with the upper
// index of the bin pair, we can't go below minElement + stride.
table3d_dim_t minBinIndex = minElement - 1U;
// Check the cached last bin and either side first - it's likely that this will give a hit under
// real world conditions
// Check if we're still in the same bin as last time
pMax = pAxis + lastBinMax;
if (is_in_bin(value, *(pMax + 1U), *pMax))
{
return lastBinMax;
}
// Check the bin above the last one
pMax = pMax + 1U;
if (lastBinMax!=minBinIndex && is_in_bin(value, *(pMax + 1U), *pMax))
{
return lastBinMax + 1U;
}
// Check the bin below the last one
pMax -= 2U;
if (lastBinMax!=maxElement && is_in_bin(value, *(pMax + 1U), *pMax))
{
return lastBinMax - 1U;
}
// Check if outside array limits - won't happen often in the real world
// so check after the cache check
// At or above maximum - clamp to final value
if (value>=pAxis[maxElement])
{
value = pAxis[maxElement];
return maxElement;
}
// At or below minimum - clamp to lowest value
if (value<=pAxis[minElement])
{
value = pAxis[minElement];
return minElement - 1U;
}
// No hits above, so run a linear search.
// We start at the maximum & work down, rather than looping from [0] up to [max]
// This is because the important tables (fuel and injection) will have the highest
// RPM at the top of the X axis, so starting there will mean the best case occurs
// when the RPM is highest (and hence the CPU is needed most)
lastBinMax = maxElement;
pMax = pAxis + lastBinMax;
while (lastBinMax!=minBinIndex && !is_in_bin(value, *(pMax + 1U), *pMax))
{
++lastBinMax;
++pMax;
}
return lastBinMax;
}
table3d_dim_t find_xbin(table3d_axis_t &value, const table3d_axis_t *pAxis, table3d_dim_t size, table3d_dim_t lastBin)
{
return find_bin_max(value, pAxis, size-1U, 0U, lastBin);
}
table3d_dim_t find_ybin(table3d_axis_t &value, const table3d_axis_t *pAxis, table3d_dim_t size, table3d_dim_t lastBin)
{
// Y axis is stored in reverse for performance purposes (not sure that's still valid).
// The minimum value is at the end & max at the start. So need to adjust for that.
return find_bin_max(value, pAxis, size-1U, 0U, lastBin);
}
// ========================= Fixed point math =========================
// An unsigned fixed point number type with 1 integer bit & 8 fractional bits.
// See https://en.wikipedia.org/wiki/Q_(number_format).
// This is specialised for the number range 0..1 - a generic fixed point
// class would miss some important optimisations. Specifically, we can avoid
// type promotion during multiplication.
typedef uint16_t QU1X8_t;
static constexpr QU1X8_t QU1X8_INTEGER_SHIFT = 8;
static constexpr QU1X8_t QU1X8_ONE = (QU1X8_t)1U << QU1X8_INTEGER_SHIFT;
static constexpr QU1X8_t QU1X8_HALF = (QU1X8_t)1U << (QU1X8_INTEGER_SHIFT-1U);
static inline QU1X8_t mulQU1X8(QU1X8_t a, QU1X8_t b)
{
// 1x1 == 1....but the real reason for this is to avoid 16-bit multiplication overflow.
//
// We are using uint16_t as our underlying fixed point type. If we follow the regular
// code path, we'd need to promote to uint32_t to avoid overflow.
//
// The overflow can only happen when *both* the X & Y inputs
// are at the edge of a bin.
//
// This is a rare condition, so most of the time we can use 16-bit multiplication and gain performance
if (a==QU1X8_ONE && b==QU1X8_ONE)
{
return QU1X8_ONE;
}
// Add the equivalent of 0.5 to the final calculation pre-rounding.
// This will have the effect of rounding to the nearest integer, rather
// than always rounding down.
return ((a * b) + QU1X8_HALF) >> QU1X8_INTEGER_SHIFT;
}
// ============================= Axis value to bin % =========================
static inline QU1X8_t compute_bin_position(table3d_axis_t value, const table3d_dim_t &bin, const table3d_axis_t *pAxis)
{
table3d_axis_t binMinValue = pAxis[bin+1U];
if (value==binMinValue) { return 0; }
table3d_axis_t binMaxValue = pAxis[bin];
if (value==binMaxValue) { return QU1X8_ONE; }
table3d_axis_t binWidth = binMaxValue-binMinValue;
// Since we can have bins of any width, we need to use
// 24.8 fixed point to avoid overflow
table3d_axis_t binPosition = value - binMinValue;
uint32_t p = (uint32_t)binPosition << QU1X8_INTEGER_SHIFT;
// But since we are computing the ratio (0 to 1), p is guaranteed to be
// less than binWidth and thus the division below will result in a value
// <=1. So we can reduce the data type from 24.8 (uint32_t) to 1.8 (uint16_t)
return udiv_32_16(p, (uint16_t)binWidth);
}
// ============================= End internal support functions =========================
//This function pulls a value from a 3D table given a target for X and Y coordinates.
//It performs a 2D linear interpolation as described in: www.megamanual.com/v22manual/ve_tuner.pdf
table3d_value_t __attribute__((noclone)) get3DTableValue(struct table3DGetValueCache *pValueCache,
table3d_dim_t axisSize,
const table3d_value_t *pValues,
const table3d_axis_t *pXAxis,
const table3d_axis_t *pYAxis,
table3d_axis_t Y_in, table3d_axis_t X_in)
{
//0th check is whether the same X and Y values are being sent as last time.
// If they are, this not only prevents a lookup of the axis, but prevents the
//interpolation calcs being performed
if( X_in == pValueCache->last_lookup.x &&
Y_in == pValueCache->last_lookup.y)
{
return pValueCache->lastOutput;
}
// Assign this here, as we might modify coords below.
pValueCache->last_lookup.x = X_in;
pValueCache->last_lookup.y = Y_in;
// Figure out where on the axes the incoming coord are
pValueCache->lastXBinMax = find_xbin(X_in, pXAxis, axisSize, pValueCache->lastXBinMax);
pValueCache->lastYBinMax = find_ybin(Y_in, pYAxis, axisSize, pValueCache->lastYBinMax);
/*
At this point we have the 4 corners of the map where the interpolated value will fall in
Eg: (yMax,xMin) (yMax,xMax)
(yMin,xMin) (yMin,xMax)
In the following calculation the table values are referred to by the following variables:
A B
C D
*/
table3d_dim_t rowMax = pValueCache->lastYBinMax * axisSize;
table3d_dim_t rowMin = rowMax + axisSize;
table3d_dim_t colMax = axisSize - pValueCache->lastXBinMax - 1U;
table3d_dim_t colMin = colMax - 1U;
table3d_value_t A = pValues[rowMax + colMin];
table3d_value_t B = pValues[rowMax + colMax];
table3d_value_t C = pValues[rowMin + colMin];
table3d_value_t D = pValues[rowMin + colMax];
//Check that all values aren't just the same (This regularly happens with things like the fuel trim maps)
if( (A == B) && (A == C) && (A == D) ) { pValueCache->lastOutput = A; }
else
{
//Create some normalised position values
//These are essentially percentages (between 0 and 1) of where the desired value falls between the nearest bins on each axis
const QU1X8_t p = compute_bin_position(X_in, pValueCache->lastXBinMax, pXAxis);
const QU1X8_t q = compute_bin_position(Y_in, pValueCache->lastYBinMax, pYAxis);
const QU1X8_t m = mulQU1X8(QU1X8_ONE-p, q);
const QU1X8_t n = mulQU1X8(p, q);
const QU1X8_t o = mulQU1X8(QU1X8_ONE-p, QU1X8_ONE-q);
const QU1X8_t r = mulQU1X8(p, QU1X8_ONE-q);
pValueCache->lastOutput = ( (A * m) + (B * n) + (C * o) + (D * r) ) >> QU1X8_INTEGER_SHIFT;
}
return pValueCache->lastOutput;
}