An I/O controller for virtual pinball machines: accelerometer nudge sensing, analog plunger input, button input encoding, LedWiz compatible output controls, and more.

Dependencies:   mbed FastIO FastPWM USBDevice

Fork of Pinscape_Controller by Mike R

Embed: (wiki syntax)

« Back to documentation index

Show/hide line numbers VCNL4010.cpp Source File

VCNL4010.cpp

00001 // VCNL4010 IR proximity sensor
00002 
00003 #include "mbed.h"
00004 #include "math.h"
00005 #include "VCNL4010.h"
00006 
00007 
00008 VCNL4010::VCNL4010(PinName sda, PinName scl, bool internalPullups, int iredCurrent)
00009     : i2c(sda, scl, internalPullups)
00010 {
00011     // Calculate the scaling factor with a minimum proximitiy count of 5.
00012     // In actual practice, the minimum will usually be a lot higher, but
00013     // this is a safe default that gives us valid distance calculations
00014     // across almost the whole possible range of count values.  (Why not
00015     // zero?  Because of the inverse relationship between distance and
00016     // brightness == proximity count.  1/0 isn't meaningful, so we have
00017     // to use a non-zero minimum in the scaling calculation.  5 is so
00018     // low that it'll probably never actually happen in real readings,
00019     // but still gives us a reasonable scaled range.)
00020     calibrating = false;
00021     minProxCount = 100;
00022     maxProxCount = 65535;
00023     parkProxCount = 20000;
00024     dcOffset = 0;
00025     lastProxCount = 0;
00026     calcScalingFactor();
00027     
00028     // remember the desired IRED current setting
00029     this->iredCurrent = iredCurrent;
00030 }
00031 
00032 // Initialize the sensor device
00033 void VCNL4010::init()
00034 {
00035     // debugging instrumentation
00036     printf("VCNL4010 initializing\r\n");
00037     
00038     // reset the I2C bus
00039     i2c.reset();
00040 
00041     // Set the proximity sampling rate to the fastest available rate of
00042     // 250 samples/second (4ms/sample).  This isn't quite fast enough for
00043     // perfect plunger motion tracking - a minimum sampling frequency of
00044     // 400/s is needed to avoid aliasing during the bounce-back phase of
00045     // release motions.  But the plunger-independent part of the code
00046     // does some data processing to tolerate aliasing for even slower
00047     // sensors than this one, so this isn't a showstopper.  Apart from
00048     // the potential for aliasing during fast motion, 250/s is plenty
00049     // fast enough for responsive input and smooth animation.
00050     writeReg(0x82, 0x07);
00051     
00052     // Set the current for the IR LED (the light source for proximity
00053     // measurements).  This is in units of 10mA, up to 200mA.  If the
00054     // parameter is zero in the configuration, apply a default.  Make
00055     // sure it's in range (1..20).
00056     //
00057     // Note that the nominal current level isn't the same as the actual
00058     // current load on the sensor's power supply.  The nominal current
00059     // set here is the instantaneous current the chip uses to generate
00060     // IR pulses.  The pulses have a low duty cycle, so the continuous
00061     // current drawn on the chip's power inputs is much lower.  The
00062     // data sheet says that the total continuous power supply current
00063     // drawn with the most power-hungry settings (IRED maxed out at
00064     // 200mA, sampling frequency maxed at 250 Hz) is only 4mA.  So
00065     // there's no need to worry about blowing a fuse on the USB port
00066     // or frying the KL25Z 3.3V regulator - the chip draws negligible
00067     // power in those terms, even at the maximum IRED setting.
00068     uint8_t cur = static_cast<uint8_t>(iredCurrent);
00069     cur = (cur == 0 ? 10 : cur < 1 ? 1 : cur > 20 ? 20 : cur);
00070     writeReg(0x83, cur);
00071 
00072     // disable self-timed measurements - we'll start measurements on demand
00073     writeReg(0x80, 0x00);
00074     
00075     // start the sample timer, which we use to gather timing statistics 
00076     sampleTimer.start();
00077 
00078     // debugging instrumentation
00079     printf("VCNL4010 initialization done\r\n");
00080 }
00081 
00082 // Start a proximity measurement.  This initiates a proximity reading
00083 // in the chip, and returns immediately, allowing the KL25Z to tend to
00084 // other tasks while waiting for the reading to complete.  proxReady()
00085 // can be used to poll for completion.
00086 void VCNL4010::startProxReading()
00087 {
00088     // set the prox_od (initiate proximity on demand) bit (0x08) in
00089     // the command register, if it's not already set
00090     uint8_t b = readReg(0x80);
00091     if ((b & 0x08) == 0)
00092     {
00093         tSampleStart = sampleTimer.read_us();
00094         writeReg(0x80, b | 0x08);
00095     }
00096 }
00097 
00098 // Check if a proximity sample is ready.  Implicitly starts a new reading
00099 // if one isn't already either completed or in progress.  Returns true if
00100 // a reading is ready, false if not.
00101 bool VCNL4010::proxReady()
00102 {
00103     // read the command register to get the status bits
00104     uint8_t b = readReg(0x80);
00105     
00106     // if the prox_data_rdy bit (0x20) is set, a reading is ready
00107     if ((b & 0x20) != 0)
00108         return true;
00109         
00110     // Not ready.  Since the caller is polling, they must expect a reading
00111     // to be in progress; if not, start one now.  A reading in progress is
00112     // indicated and initiated by the prox_od bit 
00113     if ((b & 0x08) == 0)
00114     {
00115         tSampleStart = sampleTimer.read_us();
00116         writeReg(0x80, b | 0x08);
00117     }
00118         
00119     // a reading is available if the prox_data_rdy (0x08) is set
00120     return (b & 0x20) != 0;
00121 }
00122 
00123 // Read the current proximity reading.  If a reading isn't ready, 
00124 // we'll block until one is, up to the specified timeout interval.
00125 // Returns zero if a reading was successfully retrieved, or a
00126 // non-zero error code if a timeout or error occurs.
00127 //
00128 // Note that the returned proximity count value is the raw reading
00129 // from the sensor, which indicates the intensity of the reflected
00130 // light detected on the sensor, on an abstract scale from 0 to
00131 // 65535.  The proximity count is inversely related to the distance
00132 // to the target, but the relationship also depends upon many other
00133 // factors, such as the size and reflectivity of the target, ambient
00134 // light, and internal reflections within the sensor itself and
00135 // within the overall apparatus.
00136 int VCNL4010::getProx(int &proxCount,
00137     uint32_t &tMid, uint32_t &dt, uint32_t timeout_us)
00138 {
00139     // If the chip isn't responding, try resetting it.  I2C will
00140     // generally report 0xFF on all byte reads when a device isn't
00141     // responding to commands, since the pull-up resistors on SDA
00142     // will make all data bits look like '1' on read.  It's
00143     // conceivable that a device could lock up while holding SDA
00144     // low, too, so a value of 0x00 could also be reported.  So to
00145     // sense if the device is answering, we should try reading a
00146     // register that, when things are working properly, should
00147     // always hold a value that's not either 0x00 or 0xFF.  For
00148     // the VCNL4010, we can read the product ID register, which
00149     // should report ID value 0x21 per the data sheet.  The low
00150     // nybble is a product revision number, so we shouldn't
00151     // insist on the value 0x21 - it could be 0x22 or 0x23, etc,
00152     // in future revisions of this chip.  But in any case, the
00153     // register should definitely not be 0x00 or 0xFF, so it's
00154     // a good solid test.
00155     uint8_t prodId = readReg(0x81);
00156     if (prodId == 0x00 || prodId == 0xFF)
00157     {
00158         // try resetting the chip
00159         init();
00160         
00161         // check if that cleared the problem; if not, give up and
00162         // return an error
00163         prodId = readReg(0x81);
00164         if (prodId == 0x00 || prodId == 0xFF)
00165             return 1;
00166     }
00167     
00168     // wait for the sample
00169     Timer t;
00170     t.start();
00171     for (;;)
00172     {
00173         // check for a sample
00174         if (proxReady())
00175             break;
00176             
00177         // if we've exceeded the timeout, return failure
00178         if (t.read_us() > timeout_us)
00179             return -1;
00180     }
00181     
00182     // figure the time since we initiated the reading
00183     dt = sampleTimer.read_us() - tSampleStart;
00184     
00185     // figure the midpoint time
00186     tMid = tSampleStart + dt/2;
00187 
00188     // read the result from the sensor, as a 16-bit proximity count value    
00189     int N = (static_cast<int>(readReg(0x87)) << 8) | readReg(0x88);
00190     
00191     // remember the last raw reading
00192     lastProxCount = N;
00193     
00194     // start a new reading, so that the sensor is collecting the next
00195     // reading concurrently with the time-consuming floating-point math
00196     // we're about to do
00197     startProxReading();
00198     
00199     // if calibration is in progress, note the new min/max proximity
00200     // count readings, if applicable
00201     if (calibrating) 
00202     {
00203         if (N < minProxCount)
00204             minProxCount = N;
00205         if (N > maxProxCount)
00206             maxProxCount = N;
00207     }
00208     
00209     // report the raw count back to the caller
00210     proxCount = N;
00211     
00212     // success
00213     return 0;
00214 }
00215 
00216 // Restore the saved calibration data from the configuration
00217 void VCNL4010::restoreCalibration(Config &config)
00218 {
00219     // remember the calibrated minimum proximity count
00220     this->minProxCount = config.plunger.cal.raw0;
00221     this->maxProxCount = config.plunger.cal.raw1;
00222     this->parkProxCount = config.plunger.cal.raw2;
00223     
00224     // figure the scaling factor for distance calculations
00225     calcScalingFactor();
00226 }
00227 
00228 // Begin calibration    
00229 void VCNL4010::beginCalibration()
00230 {
00231     // reset the min/max proximity count to the last reading
00232     calibrating = true;
00233     minProxCount = lastProxCount;
00234     maxProxCount = lastProxCount;
00235     parkProxCount = lastProxCount;
00236 }
00237 
00238 // End calibration
00239 void VCNL4010::endCalibration(Config &config)
00240 {
00241     // save the proximity count range data from the calibration in the
00242     // caller's configuration, so that we can restore the scaling
00243     // factor calculation on the next boot
00244     config.plunger.cal.raw0 = minProxCount;
00245     config.plunger.cal.raw1 = maxProxCount;
00246     config.plunger.cal.raw2 = parkProxCount;
00247     
00248     // calculate the new scaling factor for conversions to distance
00249     calcScalingFactor();
00250     
00251     // Set the new calibration range in distance units.  The range
00252     // in distance units is fixed, since we choose the scaling factor
00253     // specifically to cover the fixed range.
00254     config.plunger.cal.zero = 10922;
00255     config.plunger.cal.min = 0;
00256     config.plunger.cal.max = 65535;
00257     
00258     // we're no longer calibrating
00259     calibrating = false;
00260 }
00261 
00262 // Power law function for the relationship between sensor count
00263 // readings and distance.  For our distance calculations, we use
00264 // this relationship:
00265 //
00266 //    distance = <scaling factor> * 1/power(count - <DC offset>) + <scaling offset>
00267 //
00268 // where all of the constants in <angle brackets> are determined
00269 // through calibration.
00270 //
00271 // We use the square root of the count as our power law relation.
00272 // This was determined empirically (based on observation).  This is
00273 // also the power law we'd expect from a naive application of physics,
00274 // on the principle that the observed brightness of a point light
00275 // source varies inversely with the square of the distance.
00276 //
00277 // The VCNL4010 data sheet doesn't specify a formulaic relationship,
00278 // which isn't surprising given that the relationship is undoubtedly
00279 // much more complex than just a power law equation, and also because
00280 // Vishay doesn't market this chip as a distance sensor in the first
00281 // place.  It's a *proximity* sensor, which means it's only meant to
00282 // answer a yes/no question, "is an object within range?", and not
00283 // the quantitative question "how far?".  So there's no reason for
00284 // Vishay to specify a precise relationship between distance and
00285 // brightness; all we have to know is that there's some kind of
00286 // inverse relationship, since beyond that, everything's just
00287 // relative.  The data sheet does at least offer a (low-res) graph
00288 // of the distance-vs-proximity-count relationship under one set of
00289 // test conditions, and interestingly, that graph suggests a rather
00290 // different power law, more like ~1/distance^3.1.  The graph also
00291 // makes it clear that the response isn't uniform - it doesn't
00292 // follow *any* power law exactly, but is something more complex
00293 // than that.  This is another non-surprise, given that environmental
00294 // factors will inevitably confound the readings to some degree. 
00295 //
00296 // At any rate, in the data I've gathered, it seems that a simple 1/R^2
00297 // power law is pretty close to reality, so I'm using that.  (Brightness
00298 // varies with 1/R^2, so distance varies with 1/sqrt(brightness).)  If
00299 // this turns out to produce noticeably non-linear results in other
00300 // people's installations, we might have to revisit this with something
00301 // more customized to the local setup.  For example, we could gather
00302 // calibration data points across the whole plunger travel range and
00303 // then do a best-fit calculation to determine the best exponent
00304 // (which would still assume that there's *some* 1/R^x relationship
00305 // for some exponent x, but it wouldn't assume it's necessarily R^2.)
00306 // 
00307 static inline float power(int x)
00308 {
00309     return sqrtf(static_cast<float>(x)); 
00310 }
00311 
00312 // convert from a raw sensor count value to distance units, using our
00313 // current calibration data
00314 int VCNL4010::countToDistance(int count)
00315 {
00316     // remove the DC offset from teh signal
00317     count -= dcOffset;
00318     
00319     // if the adjusted count (excess of DC offset) is zero or negative,
00320     // peg it to the minimum end = maximum retraction point
00321     if (count <= 0)
00322         return 65535;
00323 
00324     // figure the distance based on our inverse power curve
00325     float d = scalingFactor/power(count) + scalingOffset;
00326     
00327     // constrain it to the valid range and convert to int for return
00328     return d < 0.0f ? 0 : d > 65535.0f ? 65535 : static_cast<int>(d);
00329 }
00330 
00331 // Calculate the scaling factors for our power-law formula for
00332 // converting proximity count (brightness) readings to distances.
00333 // We call this upon completing a new calibration pass, and during
00334 // initialization, when loading saved calibration data.
00335 void VCNL4010::calcScalingFactor()
00336 {
00337     // Don't let the minimum go below 100.  The inverse relationship makes
00338     // the calculation meaningless at zero and unstable at very small
00339     // count values, so we need a reasonable floor to keep things in a
00340     // usable range.  In practice, the minimum observed value will usually
00341     // be quite a lot higher (2000 to 20000 in my testing), which the
00342     // Vishay application note attributes to stray reflections from the
00343     // chip's mounting apparatus, ambient light, and noise within the
00344     // detector itself.  But just in case, set a floor that will ensure
00345     // reasonable calculations.
00346     if (minProxCount < 100)
00347         minProxCount = 100;
00348         
00349     // Set a ceiling of 65535, since the sensor can't go higher
00350     if (maxProxCount > 65535)
00351         maxProxCount = 65535;
00352         
00353     // Figure the scaling factor and offset over the range from the park
00354     // position to the maximum retracted position, which corresponds to
00355     // the minimum count (lowest intensity reflection) we've observed.
00356     //
00357     // Do all calculations with the counts *after* subtracting out the
00358     // signal's DC offset, which is the brightness level registered on the
00359     // sensor when there's no reflective target in range.  We can't directly
00360     // measure the DC offset in a plunger setup, since that would require
00361     // removing the plunger entirely, but we can guess that the minimum
00362     // reading observed during calibration is approximately equal to the
00363     // DC offset.  The minimum brightness occurs when the plunger is at the
00364     // most distance point in its travel range from the sensor, which is
00365     // when it's pulled all the way back.  The plunger travel distance is
00366     // just about at the limit of the VCNL4010's sensitivity, so the inverse
00367     // curve should be very nearly flat at this point, thus this is a very
00368     // close approximation of the true DC offset.
00369     const int dcOffsetDelta = 50;
00370     dcOffset = minProxCount > dcOffsetDelta ? minProxCount - dcOffsetDelta : 0;
00371     int park = parkProxCount - dcOffset;
00372     float parkInv = 1.0f/power(park);
00373     scalingFactor = 54612.5f / (1.0f/power(minProxCount - dcOffset) - parkInv);
00374     scalingOffset = 10922.5f - (scalingFactor * parkInv);
00375 }
00376 
00377 // Read an I2C register on the device
00378 uint8_t VCNL4010::readReg(uint8_t registerAddr)
00379 {
00380     // write the request
00381     uint8_t data_write[1] = { registerAddr };
00382     if (i2c.write(I2C_ADDR, data_write, 1, false))
00383         return 0x00;
00384 
00385     // read the result
00386     uint8_t data_read[1];
00387     if (i2c.read(I2C_ADDR, data_read, 1))
00388         return 0x00;
00389     
00390     // return the result
00391     return data_read[0];
00392 }
00393 
00394 // Write to an I2C register on the device 
00395 void VCNL4010::writeReg(uint8_t registerAddr, uint8_t data)
00396 {
00397     // set up the write: register number, data byte
00398     uint8_t data_write[2] = { registerAddr, data };
00399     i2c.write(I2C_ADDR, data_write, 2);
00400 }