Mirror with some correction

Dependencies:   mbed FastIO FastPWM USBDevice

VCNL4010/VCNL4010.cpp

Committer:
arnoz
Date:
2021-10-01
Revision:
116:7a67265d7c19
Parent:
113:7330439f2ffc

File content as of revision 116:7a67265d7c19:

// VCNL4010 IR proximity sensor

#include "mbed.h"
#include "math.h"
#include "VCNL4010.h"


VCNL4010::VCNL4010(PinName sda, PinName scl, bool internalPullups, int iredCurrent)
    : i2c(sda, scl, internalPullups)
{
    // Calculate the scaling factor with a minimum proximitiy count of 5.
    // In actual practice, the minimum will usually be a lot higher, but
    // this is a safe default that gives us valid distance calculations
    // across almost the whole possible range of count values.  (Why not
    // zero?  Because of the inverse relationship between distance and
    // brightness == proximity count.  1/0 isn't meaningful, so we have
    // to use a non-zero minimum in the scaling calculation.  5 is so
    // low that it'll probably never actually happen in real readings,
    // but still gives us a reasonable scaled range.)
    calibrating = false;
    minProxCount = 100;
    maxProxCount = 65535;
    parkProxCount = 20000;
    dcOffset = 0;
    lastProxCount = 0;
    calcScalingFactor();
    
    // remember the desired IRED current setting
    this->iredCurrent = iredCurrent;
}

// Initialize the sensor device
void VCNL4010::init()
{
    // debugging instrumentation
    printf("VCNL4010 initializing\r\n");
    
    // reset the I2C bus
    i2c.reset();

    // Set the proximity sampling rate to the fastest available rate of
    // 250 samples/second (4ms/sample).  This isn't quite fast enough for
    // perfect plunger motion tracking - a minimum sampling frequency of
    // 400/s is needed to avoid aliasing during the bounce-back phase of
    // release motions.  But the plunger-independent part of the code
    // does some data processing to tolerate aliasing for even slower
    // sensors than this one, so this isn't a showstopper.  Apart from
    // the potential for aliasing during fast motion, 250/s is plenty
    // fast enough for responsive input and smooth animation.
    writeReg(0x82, 0x07);
    
    // Set the current for the IR LED (the light source for proximity
    // measurements).  This is in units of 10mA, up to 200mA.  If the
    // parameter is zero in the configuration, apply a default.  Make
    // sure it's in range (1..20).
    //
    // Note that the nominal current level isn't the same as the actual
    // current load on the sensor's power supply.  The nominal current
    // set here is the instantaneous current the chip uses to generate
    // IR pulses.  The pulses have a low duty cycle, so the continuous
    // current drawn on the chip's power inputs is much lower.  The
    // data sheet says that the total continuous power supply current
    // drawn with the most power-hungry settings (IRED maxed out at
    // 200mA, sampling frequency maxed at 250 Hz) is only 4mA.  So
    // there's no need to worry about blowing a fuse on the USB port
    // or frying the KL25Z 3.3V regulator - the chip draws negligible
    // power in those terms, even at the maximum IRED setting.
    uint8_t cur = static_cast<uint8_t>(iredCurrent);
    cur = (cur == 0 ? 10 : cur < 1 ? 1 : cur > 20 ? 20 : cur);
    writeReg(0x83, cur);

    // disable self-timed measurements - we'll start measurements on demand
    writeReg(0x80, 0x00);
    
    // start the sample timer, which we use to gather timing statistics 
    sampleTimer.start();

    // debugging instrumentation
    printf("VCNL4010 initialization done\r\n");
}

// Start a proximity measurement.  This initiates a proximity reading
// in the chip, and returns immediately, allowing the KL25Z to tend to
// other tasks while waiting for the reading to complete.  proxReady()
// can be used to poll for completion.
void VCNL4010::startProxReading()
{
    // set the prox_od (initiate proximity on demand) bit (0x08) in
    // the command register, if it's not already set
    uint8_t b = readReg(0x80);
    if ((b & 0x08) == 0)
    {
        tSampleStart = sampleTimer.read_us();
        writeReg(0x80, b | 0x08);
    }
}

// Check if a proximity sample is ready.  Implicitly starts a new reading
// if one isn't already either completed or in progress.  Returns true if
// a reading is ready, false if not.
bool VCNL4010::proxReady()
{
    // read the command register to get the status bits
    uint8_t b = readReg(0x80);
    
    // if the prox_data_rdy bit (0x20) is set, a reading is ready
    if ((b & 0x20) != 0)
        return true;
        
    // Not ready.  Since the caller is polling, they must expect a reading
    // to be in progress; if not, start one now.  A reading in progress is
    // indicated and initiated by the prox_od bit 
    if ((b & 0x08) == 0)
    {
        tSampleStart = sampleTimer.read_us();
        writeReg(0x80, b | 0x08);
    }
        
    // a reading is available if the prox_data_rdy (0x08) is set
    return (b & 0x20) != 0;
}

// Read the current proximity reading.  If a reading isn't ready, 
// we'll block until one is, up to the specified timeout interval.
// Returns zero if a reading was successfully retrieved, or a
// non-zero error code if a timeout or error occurs.
//
// Note that the returned proximity count value is the raw reading
// from the sensor, which indicates the intensity of the reflected
// light detected on the sensor, on an abstract scale from 0 to
// 65535.  The proximity count is inversely related to the distance
// to the target, but the relationship also depends upon many other
// factors, such as the size and reflectivity of the target, ambient
// light, and internal reflections within the sensor itself and
// within the overall apparatus.
int VCNL4010::getProx(int &proxCount,
    uint32_t &tMid, uint32_t &dt, uint32_t timeout_us)
{
    // If the chip isn't responding, try resetting it.  I2C will
    // generally report 0xFF on all byte reads when a device isn't
    // responding to commands, since the pull-up resistors on SDA
    // will make all data bits look like '1' on read.  It's
    // conceivable that a device could lock up while holding SDA
    // low, too, so a value of 0x00 could also be reported.  So to
    // sense if the device is answering, we should try reading a
    // register that, when things are working properly, should
    // always hold a value that's not either 0x00 or 0xFF.  For
    // the VCNL4010, we can read the product ID register, which
    // should report ID value 0x21 per the data sheet.  The low
    // nybble is a product revision number, so we shouldn't
    // insist on the value 0x21 - it could be 0x22 or 0x23, etc,
    // in future revisions of this chip.  But in any case, the
    // register should definitely not be 0x00 or 0xFF, so it's
    // a good solid test.
    uint8_t prodId = readReg(0x81);
    if (prodId == 0x00 || prodId == 0xFF)
    {
        // try resetting the chip
        init();
        
        // check if that cleared the problem; if not, give up and
        // return an error
        prodId = readReg(0x81);
        if (prodId == 0x00 || prodId == 0xFF)
            return 1;
    }
    
    // wait for the sample
    Timer t;
    t.start();
    for (;;)
    {
        // check for a sample
        if (proxReady())
            break;
            
        // if we've exceeded the timeout, return failure
        if (t.read_us() > timeout_us)
            return -1;
    }
    
    // figure the time since we initiated the reading
    dt = sampleTimer.read_us() - tSampleStart;
    
    // figure the midpoint time
    tMid = tSampleStart + dt/2;

    // read the result from the sensor, as a 16-bit proximity count value    
    int N = (static_cast<int>(readReg(0x87)) << 8) | readReg(0x88);
    
    // remember the last raw reading
    lastProxCount = N;
    
    // start a new reading, so that the sensor is collecting the next
    // reading concurrently with the time-consuming floating-point math
    // we're about to do
    startProxReading();
    
    // if calibration is in progress, note the new min/max proximity
    // count readings, if applicable
    if (calibrating) 
    {
        if (N < minProxCount)
            minProxCount = N;
        if (N > maxProxCount)
            maxProxCount = N;
    }
    
    // report the raw count back to the caller
    proxCount = N;
    
    // success
    return 0;
}

// Restore the saved calibration data from the configuration
void VCNL4010::restoreCalibration(Config &config)
{
    // remember the calibrated minimum proximity count
    this->minProxCount = config.plunger.cal.raw0;
    this->maxProxCount = config.plunger.cal.raw1;
    this->parkProxCount = config.plunger.cal.raw2;
    
    // figure the scaling factor for distance calculations
    calcScalingFactor();
}

// Begin calibration    
void VCNL4010::beginCalibration()
{
    // reset the min/max proximity count to the last reading
    calibrating = true;
    minProxCount = lastProxCount;
    maxProxCount = lastProxCount;
    parkProxCount = lastProxCount;
}

// End calibration
void VCNL4010::endCalibration(Config &config)
{
    // save the proximity count range data from the calibration in the
    // caller's configuration, so that we can restore the scaling
    // factor calculation on the next boot
    config.plunger.cal.raw0 = minProxCount;
    config.plunger.cal.raw1 = maxProxCount;
    config.plunger.cal.raw2 = parkProxCount;
    
    // calculate the new scaling factor for conversions to distance
    calcScalingFactor();
    
    // Set the new calibration range in distance units.  The range
    // in distance units is fixed, since we choose the scaling factor
    // specifically to cover the fixed range.
    config.plunger.cal.zero = 10922;
    config.plunger.cal.min = 0;
    config.plunger.cal.max = 65535;
    
    // we're no longer calibrating
    calibrating = false;
}

// Power law function for the relationship between sensor count
// readings and distance.  For our distance calculations, we use
// this relationship:
//
//    distance = <scaling factor> * 1/power(count - <DC offset>) + <scaling offset>
//
// where all of the constants in <angle brackets> are determined
// through calibration.
//
// We use the square root of the count as our power law relation.
// This was determined empirically (based on observation).  This is
// also the power law we'd expect from a naive application of physics,
// on the principle that the observed brightness of a point light
// source varies inversely with the square of the distance.
//
// The VCNL4010 data sheet doesn't specify a formulaic relationship,
// which isn't surprising given that the relationship is undoubtedly
// much more complex than just a power law equation, and also because
// Vishay doesn't market this chip as a distance sensor in the first
// place.  It's a *proximity* sensor, which means it's only meant to
// answer a yes/no question, "is an object within range?", and not
// the quantitative question "how far?".  So there's no reason for
// Vishay to specify a precise relationship between distance and
// brightness; all we have to know is that there's some kind of
// inverse relationship, since beyond that, everything's just
// relative.  The data sheet does at least offer a (low-res) graph
// of the distance-vs-proximity-count relationship under one set of
// test conditions, and interestingly, that graph suggests a rather
// different power law, more like ~1/distance^3.1.  The graph also
// makes it clear that the response isn't uniform - it doesn't
// follow *any* power law exactly, but is something more complex
// than that.  This is another non-surprise, given that environmental
// factors will inevitably confound the readings to some degree. 
//
// At any rate, in the data I've gathered, it seems that a simple 1/R^2
// power law is pretty close to reality, so I'm using that.  (Brightness
// varies with 1/R^2, so distance varies with 1/sqrt(brightness).)  If
// this turns out to produce noticeably non-linear results in other
// people's installations, we might have to revisit this with something
// more customized to the local setup.  For example, we could gather
// calibration data points across the whole plunger travel range and
// then do a best-fit calculation to determine the best exponent
// (which would still assume that there's *some* 1/R^x relationship
// for some exponent x, but it wouldn't assume it's necessarily R^2.)
// 
static inline float power(int x)
{
    return sqrtf(static_cast<float>(x)); 
}

// convert from a raw sensor count value to distance units, using our
// current calibration data
int VCNL4010::countToDistance(int count)
{
    // remove the DC offset from teh signal
    count -= dcOffset;
    
    // if the adjusted count (excess of DC offset) is zero or negative,
    // peg it to the minimum end = maximum retraction point
    if (count <= 0)
        return 65535;

    // figure the distance based on our inverse power curve
    float d = scalingFactor/power(count) + scalingOffset;
    
    // constrain it to the valid range and convert to int for return
    return d < 0.0f ? 0 : d > 65535.0f ? 65535 : static_cast<int>(d);
}

// Calculate the scaling factors for our power-law formula for
// converting proximity count (brightness) readings to distances.
// We call this upon completing a new calibration pass, and during
// initialization, when loading saved calibration data.
void VCNL4010::calcScalingFactor()
{
    // Don't let the minimum go below 100.  The inverse relationship makes
    // the calculation meaningless at zero and unstable at very small
    // count values, so we need a reasonable floor to keep things in a
    // usable range.  In practice, the minimum observed value will usually
    // be quite a lot higher (2000 to 20000 in my testing), which the
    // Vishay application note attributes to stray reflections from the
    // chip's mounting apparatus, ambient light, and noise within the
    // detector itself.  But just in case, set a floor that will ensure
    // reasonable calculations.
    if (minProxCount < 100)
        minProxCount = 100;
        
    // Set a ceiling of 65535, since the sensor can't go higher
    if (maxProxCount > 65535)
        maxProxCount = 65535;
        
    // Figure the scaling factor and offset over the range from the park
    // position to the maximum retracted position, which corresponds to
    // the minimum count (lowest intensity reflection) we've observed.
    //
    // Do all calculations with the counts *after* subtracting out the
    // signal's DC offset, which is the brightness level registered on the
    // sensor when there's no reflective target in range.  We can't directly
    // measure the DC offset in a plunger setup, since that would require
    // removing the plunger entirely, but we can guess that the minimum
    // reading observed during calibration is approximately equal to the
    // DC offset.  The minimum brightness occurs when the plunger is at the
    // most distance point in its travel range from the sensor, which is
    // when it's pulled all the way back.  The plunger travel distance is
    // just about at the limit of the VCNL4010's sensitivity, so the inverse
    // curve should be very nearly flat at this point, thus this is a very
    // close approximation of the true DC offset.
    const int dcOffsetDelta = 50;
    dcOffset = minProxCount > dcOffsetDelta ? minProxCount - dcOffsetDelta : 0;
    int park = parkProxCount - dcOffset;
    float parkInv = 1.0f/power(park);
    scalingFactor = 54612.5f / (1.0f/power(minProxCount - dcOffset) - parkInv);
    scalingOffset = 10922.5f - (scalingFactor * parkInv);
}

// Read an I2C register on the device
uint8_t VCNL4010::readReg(uint8_t registerAddr)
{
    // write the request
    uint8_t data_write[1] = { registerAddr };
    if (i2c.write(I2C_ADDR, data_write, 1, false))
        return 0x00;

    // read the result
    uint8_t data_read[1];
    if (i2c.read(I2C_ADDR, data_read, 1))
        return 0x00;
    
    // return the result
    return data_read[0];
}

// Write to an I2C register on the device 
void VCNL4010::writeReg(uint8_t registerAddr, uint8_t data)
{
    // set up the write: register number, data byte
    uint8_t data_write[2] = { registerAddr, data };
    i2c.write(I2C_ADDR, data_write, 2);
}