Mirror with some correction
Dependencies: mbed FastIO FastPWM USBDevice
VCNL4010/VCNL4010.cpp@116:7a67265d7c19, 2021-10-01 (annotated)
- Committer:
- arnoz
- Date:
- Fri Oct 01 08:19:46 2021 +0000
- Revision:
- 116:7a67265d7c19
- Parent:
- 113:7330439f2ffc
- Correct information regarding your last merge
Who changed what in which revision?
User | Revision | Line number | New contents of line |
---|---|---|---|
mjr | 111:42dc75fbe623 | 1 | // VCNL4010 IR proximity sensor |
mjr | 111:42dc75fbe623 | 2 | |
mjr | 111:42dc75fbe623 | 3 | #include "mbed.h" |
mjr | 111:42dc75fbe623 | 4 | #include "math.h" |
mjr | 111:42dc75fbe623 | 5 | #include "VCNL4010.h" |
mjr | 111:42dc75fbe623 | 6 | |
mjr | 113:7330439f2ffc | 7 | |
mjr | 113:7330439f2ffc | 8 | VCNL4010::VCNL4010(PinName sda, PinName scl, bool internalPullups, int iredCurrent) |
mjr | 111:42dc75fbe623 | 9 | : i2c(sda, scl, internalPullups) |
mjr | 111:42dc75fbe623 | 10 | { |
mjr | 113:7330439f2ffc | 11 | // Calculate the scaling factor with a minimum proximitiy count of 5. |
mjr | 113:7330439f2ffc | 12 | // In actual practice, the minimum will usually be a lot higher, but |
mjr | 113:7330439f2ffc | 13 | // this is a safe default that gives us valid distance calculations |
mjr | 113:7330439f2ffc | 14 | // across almost the whole possible range of count values. (Why not |
mjr | 113:7330439f2ffc | 15 | // zero? Because of the inverse relationship between distance and |
mjr | 113:7330439f2ffc | 16 | // brightness == proximity count. 1/0 isn't meaningful, so we have |
mjr | 113:7330439f2ffc | 17 | // to use a non-zero minimum in the scaling calculation. 5 is so |
mjr | 113:7330439f2ffc | 18 | // low that it'll probably never actually happen in real readings, |
mjr | 113:7330439f2ffc | 19 | // but still gives us a reasonable scaled range.) |
mjr | 113:7330439f2ffc | 20 | calibrating = false; |
mjr | 113:7330439f2ffc | 21 | minProxCount = 100; |
mjr | 113:7330439f2ffc | 22 | maxProxCount = 65535; |
mjr | 113:7330439f2ffc | 23 | parkProxCount = 20000; |
mjr | 113:7330439f2ffc | 24 | dcOffset = 0; |
mjr | 113:7330439f2ffc | 25 | lastProxCount = 0; |
mjr | 113:7330439f2ffc | 26 | calcScalingFactor(); |
mjr | 113:7330439f2ffc | 27 | |
mjr | 113:7330439f2ffc | 28 | // remember the desired IRED current setting |
mjr | 113:7330439f2ffc | 29 | this->iredCurrent = iredCurrent; |
mjr | 111:42dc75fbe623 | 30 | } |
mjr | 111:42dc75fbe623 | 31 | |
mjr | 113:7330439f2ffc | 32 | // Initialize the sensor device |
mjr | 111:42dc75fbe623 | 33 | void VCNL4010::init() |
mjr | 111:42dc75fbe623 | 34 | { |
mjr | 113:7330439f2ffc | 35 | // debugging instrumentation |
mjr | 111:42dc75fbe623 | 36 | printf("VCNL4010 initializing\r\n"); |
mjr | 111:42dc75fbe623 | 37 | |
mjr | 111:42dc75fbe623 | 38 | // reset the I2C bus |
mjr | 111:42dc75fbe623 | 39 | i2c.reset(); |
mjr | 111:42dc75fbe623 | 40 | |
mjr | 111:42dc75fbe623 | 41 | // Set the proximity sampling rate to the fastest available rate of |
mjr | 113:7330439f2ffc | 42 | // 250 samples/second (4ms/sample). This isn't quite fast enough for |
mjr | 113:7330439f2ffc | 43 | // perfect plunger motion tracking - a minimum sampling frequency of |
mjr | 113:7330439f2ffc | 44 | // 400/s is needed to avoid aliasing during the bounce-back phase of |
mjr | 113:7330439f2ffc | 45 | // release motions. But the plunger-independent part of the code |
mjr | 113:7330439f2ffc | 46 | // does some data processing to tolerate aliasing for even slower |
mjr | 113:7330439f2ffc | 47 | // sensors than this one, so this isn't a showstopper. Apart from |
mjr | 113:7330439f2ffc | 48 | // the potential for aliasing during fast motion, 250/s is plenty |
mjr | 113:7330439f2ffc | 49 | // fast enough for responsive input and smooth animation. |
mjr | 111:42dc75fbe623 | 50 | writeReg(0x82, 0x07); |
mjr | 111:42dc75fbe623 | 51 | |
mjr | 111:42dc75fbe623 | 52 | // Set the current for the IR LED (the light source for proximity |
mjr | 113:7330439f2ffc | 53 | // measurements). This is in units of 10mA, up to 200mA. If the |
mjr | 113:7330439f2ffc | 54 | // parameter is zero in the configuration, apply a default. Make |
mjr | 113:7330439f2ffc | 55 | // sure it's in range (1..20). |
mjr | 111:42dc75fbe623 | 56 | // |
mjr | 113:7330439f2ffc | 57 | // Note that the nominal current level isn't the same as the actual |
mjr | 113:7330439f2ffc | 58 | // current load on the sensor's power supply. The nominal current |
mjr | 113:7330439f2ffc | 59 | // set here is the instantaneous current the chip uses to generate |
mjr | 113:7330439f2ffc | 60 | // IR pulses. The pulses have a low duty cycle, so the continuous |
mjr | 113:7330439f2ffc | 61 | // current drawn on the chip's power inputs is much lower. The |
mjr | 113:7330439f2ffc | 62 | // data sheet says that the total continuous power supply current |
mjr | 113:7330439f2ffc | 63 | // drawn with the most power-hungry settings (IRED maxed out at |
mjr | 113:7330439f2ffc | 64 | // 200mA, sampling frequency maxed at 250 Hz) is only 4mA. So |
mjr | 113:7330439f2ffc | 65 | // there's no need to worry about blowing a fuse on the USB port |
mjr | 113:7330439f2ffc | 66 | // or frying the KL25Z 3.3V regulator - the chip draws negligible |
mjr | 113:7330439f2ffc | 67 | // power in those terms, even at the maximum IRED setting. |
mjr | 113:7330439f2ffc | 68 | uint8_t cur = static_cast<uint8_t>(iredCurrent); |
mjr | 113:7330439f2ffc | 69 | cur = (cur == 0 ? 10 : cur < 1 ? 1 : cur > 20 ? 20 : cur); |
mjr | 113:7330439f2ffc | 70 | writeReg(0x83, cur); |
mjr | 111:42dc75fbe623 | 71 | |
mjr | 111:42dc75fbe623 | 72 | // disable self-timed measurements - we'll start measurements on demand |
mjr | 111:42dc75fbe623 | 73 | writeReg(0x80, 0x00); |
mjr | 111:42dc75fbe623 | 74 | |
mjr | 111:42dc75fbe623 | 75 | // start the sample timer, which we use to gather timing statistics |
mjr | 111:42dc75fbe623 | 76 | sampleTimer.start(); |
mjr | 111:42dc75fbe623 | 77 | |
mjr | 113:7330439f2ffc | 78 | // debugging instrumentation |
mjr | 111:42dc75fbe623 | 79 | printf("VCNL4010 initialization done\r\n"); |
mjr | 111:42dc75fbe623 | 80 | } |
mjr | 111:42dc75fbe623 | 81 | |
mjr | 113:7330439f2ffc | 82 | // Start a proximity measurement. This initiates a proximity reading |
mjr | 113:7330439f2ffc | 83 | // in the chip, and returns immediately, allowing the KL25Z to tend to |
mjr | 113:7330439f2ffc | 84 | // other tasks while waiting for the reading to complete. proxReady() |
mjr | 113:7330439f2ffc | 85 | // can be used to poll for completion. |
mjr | 111:42dc75fbe623 | 86 | void VCNL4010::startProxReading() |
mjr | 111:42dc75fbe623 | 87 | { |
mjr | 111:42dc75fbe623 | 88 | // set the prox_od (initiate proximity on demand) bit (0x08) in |
mjr | 111:42dc75fbe623 | 89 | // the command register, if it's not already set |
mjr | 111:42dc75fbe623 | 90 | uint8_t b = readReg(0x80); |
mjr | 111:42dc75fbe623 | 91 | if ((b & 0x08) == 0) |
mjr | 111:42dc75fbe623 | 92 | { |
mjr | 111:42dc75fbe623 | 93 | tSampleStart = sampleTimer.read_us(); |
mjr | 111:42dc75fbe623 | 94 | writeReg(0x80, b | 0x08); |
mjr | 111:42dc75fbe623 | 95 | } |
mjr | 111:42dc75fbe623 | 96 | } |
mjr | 111:42dc75fbe623 | 97 | |
mjr | 113:7330439f2ffc | 98 | // Check if a proximity sample is ready. Implicitly starts a new reading |
mjr | 113:7330439f2ffc | 99 | // if one isn't already either completed or in progress. Returns true if |
mjr | 113:7330439f2ffc | 100 | // a reading is ready, false if not. |
mjr | 111:42dc75fbe623 | 101 | bool VCNL4010::proxReady() |
mjr | 111:42dc75fbe623 | 102 | { |
mjr | 111:42dc75fbe623 | 103 | // read the command register to get the status bits |
mjr | 111:42dc75fbe623 | 104 | uint8_t b = readReg(0x80); |
mjr | 111:42dc75fbe623 | 105 | |
mjr | 111:42dc75fbe623 | 106 | // if the prox_data_rdy bit (0x20) is set, a reading is ready |
mjr | 111:42dc75fbe623 | 107 | if ((b & 0x20) != 0) |
mjr | 111:42dc75fbe623 | 108 | return true; |
mjr | 111:42dc75fbe623 | 109 | |
mjr | 111:42dc75fbe623 | 110 | // Not ready. Since the caller is polling, they must expect a reading |
mjr | 111:42dc75fbe623 | 111 | // to be in progress; if not, start one now. A reading in progress is |
mjr | 111:42dc75fbe623 | 112 | // indicated and initiated by the prox_od bit |
mjr | 111:42dc75fbe623 | 113 | if ((b & 0x08) == 0) |
mjr | 111:42dc75fbe623 | 114 | { |
mjr | 111:42dc75fbe623 | 115 | tSampleStart = sampleTimer.read_us(); |
mjr | 111:42dc75fbe623 | 116 | writeReg(0x80, b | 0x08); |
mjr | 111:42dc75fbe623 | 117 | } |
mjr | 111:42dc75fbe623 | 118 | |
mjr | 111:42dc75fbe623 | 119 | // a reading is available if the prox_data_rdy (0x08) is set |
mjr | 111:42dc75fbe623 | 120 | return (b & 0x20) != 0; |
mjr | 111:42dc75fbe623 | 121 | } |
mjr | 111:42dc75fbe623 | 122 | |
mjr | 113:7330439f2ffc | 123 | // Read the current proximity reading. If a reading isn't ready, |
mjr | 113:7330439f2ffc | 124 | // we'll block until one is, up to the specified timeout interval. |
mjr | 113:7330439f2ffc | 125 | // Returns zero if a reading was successfully retrieved, or a |
mjr | 113:7330439f2ffc | 126 | // non-zero error code if a timeout or error occurs. |
mjr | 113:7330439f2ffc | 127 | // |
mjr | 113:7330439f2ffc | 128 | // Note that the returned proximity count value is the raw reading |
mjr | 113:7330439f2ffc | 129 | // from the sensor, which indicates the intensity of the reflected |
mjr | 113:7330439f2ffc | 130 | // light detected on the sensor, on an abstract scale from 0 to |
mjr | 113:7330439f2ffc | 131 | // 65535. The proximity count is inversely related to the distance |
mjr | 113:7330439f2ffc | 132 | // to the target, but the relationship also depends upon many other |
mjr | 113:7330439f2ffc | 133 | // factors, such as the size and reflectivity of the target, ambient |
mjr | 113:7330439f2ffc | 134 | // light, and internal reflections within the sensor itself and |
mjr | 113:7330439f2ffc | 135 | // within the overall apparatus. |
mjr | 113:7330439f2ffc | 136 | int VCNL4010::getProx(int &proxCount, |
mjr | 113:7330439f2ffc | 137 | uint32_t &tMid, uint32_t &dt, uint32_t timeout_us) |
mjr | 111:42dc75fbe623 | 138 | { |
mjr | 113:7330439f2ffc | 139 | // If the chip isn't responding, try resetting it. I2C will |
mjr | 113:7330439f2ffc | 140 | // generally report 0xFF on all byte reads when a device isn't |
mjr | 113:7330439f2ffc | 141 | // responding to commands, since the pull-up resistors on SDA |
mjr | 113:7330439f2ffc | 142 | // will make all data bits look like '1' on read. It's |
mjr | 113:7330439f2ffc | 143 | // conceivable that a device could lock up while holding SDA |
mjr | 113:7330439f2ffc | 144 | // low, too, so a value of 0x00 could also be reported. So to |
mjr | 113:7330439f2ffc | 145 | // sense if the device is answering, we should try reading a |
mjr | 113:7330439f2ffc | 146 | // register that, when things are working properly, should |
mjr | 113:7330439f2ffc | 147 | // always hold a value that's not either 0x00 or 0xFF. For |
mjr | 113:7330439f2ffc | 148 | // the VCNL4010, we can read the product ID register, which |
mjr | 113:7330439f2ffc | 149 | // should report ID value 0x21 per the data sheet. The low |
mjr | 113:7330439f2ffc | 150 | // nybble is a product revision number, so we shouldn't |
mjr | 113:7330439f2ffc | 151 | // insist on the value 0x21 - it could be 0x22 or 0x23, etc, |
mjr | 113:7330439f2ffc | 152 | // in future revisions of this chip. But in any case, the |
mjr | 113:7330439f2ffc | 153 | // register should definitely not be 0x00 or 0xFF, so it's |
mjr | 113:7330439f2ffc | 154 | // a good solid test. |
mjr | 113:7330439f2ffc | 155 | uint8_t prodId = readReg(0x81); |
mjr | 113:7330439f2ffc | 156 | if (prodId == 0x00 || prodId == 0xFF) |
mjr | 113:7330439f2ffc | 157 | { |
mjr | 113:7330439f2ffc | 158 | // try resetting the chip |
mjr | 113:7330439f2ffc | 159 | init(); |
mjr | 113:7330439f2ffc | 160 | |
mjr | 113:7330439f2ffc | 161 | // check if that cleared the problem; if not, give up and |
mjr | 113:7330439f2ffc | 162 | // return an error |
mjr | 113:7330439f2ffc | 163 | prodId = readReg(0x81); |
mjr | 113:7330439f2ffc | 164 | if (prodId == 0x00 || prodId == 0xFF) |
mjr | 113:7330439f2ffc | 165 | return 1; |
mjr | 113:7330439f2ffc | 166 | } |
mjr | 113:7330439f2ffc | 167 | |
mjr | 111:42dc75fbe623 | 168 | // wait for the sample |
mjr | 111:42dc75fbe623 | 169 | Timer t; |
mjr | 111:42dc75fbe623 | 170 | t.start(); |
mjr | 111:42dc75fbe623 | 171 | for (;;) |
mjr | 111:42dc75fbe623 | 172 | { |
mjr | 111:42dc75fbe623 | 173 | // check for a sample |
mjr | 111:42dc75fbe623 | 174 | if (proxReady()) |
mjr | 111:42dc75fbe623 | 175 | break; |
mjr | 111:42dc75fbe623 | 176 | |
mjr | 111:42dc75fbe623 | 177 | // if we've exceeded the timeout, return failure |
mjr | 111:42dc75fbe623 | 178 | if (t.read_us() > timeout_us) |
mjr | 111:42dc75fbe623 | 179 | return -1; |
mjr | 111:42dc75fbe623 | 180 | } |
mjr | 111:42dc75fbe623 | 181 | |
mjr | 111:42dc75fbe623 | 182 | // figure the time since we initiated the reading |
mjr | 111:42dc75fbe623 | 183 | dt = sampleTimer.read_us() - tSampleStart; |
mjr | 111:42dc75fbe623 | 184 | |
mjr | 111:42dc75fbe623 | 185 | // figure the midpoint time |
mjr | 111:42dc75fbe623 | 186 | tMid = tSampleStart + dt/2; |
mjr | 111:42dc75fbe623 | 187 | |
mjr | 111:42dc75fbe623 | 188 | // read the result from the sensor, as a 16-bit proximity count value |
mjr | 113:7330439f2ffc | 189 | int N = (static_cast<int>(readReg(0x87)) << 8) | readReg(0x88); |
mjr | 113:7330439f2ffc | 190 | |
mjr | 113:7330439f2ffc | 191 | // remember the last raw reading |
mjr | 113:7330439f2ffc | 192 | lastProxCount = N; |
mjr | 111:42dc75fbe623 | 193 | |
mjr | 111:42dc75fbe623 | 194 | // start a new reading, so that the sensor is collecting the next |
mjr | 111:42dc75fbe623 | 195 | // reading concurrently with the time-consuming floating-point math |
mjr | 111:42dc75fbe623 | 196 | // we're about to do |
mjr | 111:42dc75fbe623 | 197 | startProxReading(); |
mjr | 113:7330439f2ffc | 198 | |
mjr | 113:7330439f2ffc | 199 | // if calibration is in progress, note the new min/max proximity |
mjr | 113:7330439f2ffc | 200 | // count readings, if applicable |
mjr | 113:7330439f2ffc | 201 | if (calibrating) |
mjr | 113:7330439f2ffc | 202 | { |
mjr | 113:7330439f2ffc | 203 | if (N < minProxCount) |
mjr | 113:7330439f2ffc | 204 | minProxCount = N; |
mjr | 113:7330439f2ffc | 205 | if (N > maxProxCount) |
mjr | 113:7330439f2ffc | 206 | maxProxCount = N; |
mjr | 113:7330439f2ffc | 207 | } |
mjr | 113:7330439f2ffc | 208 | |
mjr | 113:7330439f2ffc | 209 | // report the raw count back to the caller |
mjr | 113:7330439f2ffc | 210 | proxCount = N; |
mjr | 113:7330439f2ffc | 211 | |
mjr | 111:42dc75fbe623 | 212 | // success |
mjr | 111:42dc75fbe623 | 213 | return 0; |
mjr | 111:42dc75fbe623 | 214 | } |
mjr | 111:42dc75fbe623 | 215 | |
mjr | 113:7330439f2ffc | 216 | // Restore the saved calibration data from the configuration |
mjr | 113:7330439f2ffc | 217 | void VCNL4010::restoreCalibration(Config &config) |
mjr | 113:7330439f2ffc | 218 | { |
mjr | 113:7330439f2ffc | 219 | // remember the calibrated minimum proximity count |
mjr | 113:7330439f2ffc | 220 | this->minProxCount = config.plunger.cal.raw0; |
mjr | 113:7330439f2ffc | 221 | this->maxProxCount = config.plunger.cal.raw1; |
mjr | 113:7330439f2ffc | 222 | this->parkProxCount = config.plunger.cal.raw2; |
mjr | 113:7330439f2ffc | 223 | |
mjr | 113:7330439f2ffc | 224 | // figure the scaling factor for distance calculations |
mjr | 113:7330439f2ffc | 225 | calcScalingFactor(); |
mjr | 113:7330439f2ffc | 226 | } |
mjr | 113:7330439f2ffc | 227 | |
mjr | 113:7330439f2ffc | 228 | // Begin calibration |
mjr | 113:7330439f2ffc | 229 | void VCNL4010::beginCalibration() |
mjr | 113:7330439f2ffc | 230 | { |
mjr | 113:7330439f2ffc | 231 | // reset the min/max proximity count to the last reading |
mjr | 113:7330439f2ffc | 232 | calibrating = true; |
mjr | 113:7330439f2ffc | 233 | minProxCount = lastProxCount; |
mjr | 113:7330439f2ffc | 234 | maxProxCount = lastProxCount; |
mjr | 113:7330439f2ffc | 235 | parkProxCount = lastProxCount; |
mjr | 113:7330439f2ffc | 236 | } |
mjr | 113:7330439f2ffc | 237 | |
mjr | 113:7330439f2ffc | 238 | // End calibration |
mjr | 113:7330439f2ffc | 239 | void VCNL4010::endCalibration(Config &config) |
mjr | 113:7330439f2ffc | 240 | { |
mjr | 113:7330439f2ffc | 241 | // save the proximity count range data from the calibration in the |
mjr | 113:7330439f2ffc | 242 | // caller's configuration, so that we can restore the scaling |
mjr | 113:7330439f2ffc | 243 | // factor calculation on the next boot |
mjr | 113:7330439f2ffc | 244 | config.plunger.cal.raw0 = minProxCount; |
mjr | 113:7330439f2ffc | 245 | config.plunger.cal.raw1 = maxProxCount; |
mjr | 113:7330439f2ffc | 246 | config.plunger.cal.raw2 = parkProxCount; |
mjr | 113:7330439f2ffc | 247 | |
mjr | 113:7330439f2ffc | 248 | // calculate the new scaling factor for conversions to distance |
mjr | 113:7330439f2ffc | 249 | calcScalingFactor(); |
mjr | 113:7330439f2ffc | 250 | |
mjr | 113:7330439f2ffc | 251 | // Set the new calibration range in distance units. The range |
mjr | 113:7330439f2ffc | 252 | // in distance units is fixed, since we choose the scaling factor |
mjr | 113:7330439f2ffc | 253 | // specifically to cover the fixed range. |
mjr | 113:7330439f2ffc | 254 | config.plunger.cal.zero = 10922; |
mjr | 113:7330439f2ffc | 255 | config.plunger.cal.min = 0; |
mjr | 113:7330439f2ffc | 256 | config.plunger.cal.max = 65535; |
mjr | 113:7330439f2ffc | 257 | |
mjr | 113:7330439f2ffc | 258 | // we're no longer calibrating |
mjr | 113:7330439f2ffc | 259 | calibrating = false; |
mjr | 113:7330439f2ffc | 260 | } |
mjr | 113:7330439f2ffc | 261 | |
mjr | 113:7330439f2ffc | 262 | // Power law function for the relationship between sensor count |
mjr | 113:7330439f2ffc | 263 | // readings and distance. For our distance calculations, we use |
mjr | 113:7330439f2ffc | 264 | // this relationship: |
mjr | 113:7330439f2ffc | 265 | // |
mjr | 113:7330439f2ffc | 266 | // distance = <scaling factor> * 1/power(count - <DC offset>) + <scaling offset> |
mjr | 113:7330439f2ffc | 267 | // |
mjr | 113:7330439f2ffc | 268 | // where all of the constants in <angle brackets> are determined |
mjr | 113:7330439f2ffc | 269 | // through calibration. |
mjr | 113:7330439f2ffc | 270 | // |
mjr | 113:7330439f2ffc | 271 | // We use the square root of the count as our power law relation. |
mjr | 113:7330439f2ffc | 272 | // This was determined empirically (based on observation). This is |
mjr | 113:7330439f2ffc | 273 | // also the power law we'd expect from a naive application of physics, |
mjr | 113:7330439f2ffc | 274 | // on the principle that the observed brightness of a point light |
mjr | 113:7330439f2ffc | 275 | // source varies inversely with the square of the distance. |
mjr | 113:7330439f2ffc | 276 | // |
mjr | 113:7330439f2ffc | 277 | // The VCNL4010 data sheet doesn't specify a formulaic relationship, |
mjr | 113:7330439f2ffc | 278 | // which isn't surprising given that the relationship is undoubtedly |
mjr | 113:7330439f2ffc | 279 | // much more complex than just a power law equation, and also because |
mjr | 113:7330439f2ffc | 280 | // Vishay doesn't market this chip as a distance sensor in the first |
mjr | 113:7330439f2ffc | 281 | // place. It's a *proximity* sensor, which means it's only meant to |
mjr | 113:7330439f2ffc | 282 | // answer a yes/no question, "is an object within range?", and not |
mjr | 113:7330439f2ffc | 283 | // the quantitative question "how far?". So there's no reason for |
mjr | 113:7330439f2ffc | 284 | // Vishay to specify a precise relationship between distance and |
mjr | 113:7330439f2ffc | 285 | // brightness; all we have to know is that there's some kind of |
mjr | 113:7330439f2ffc | 286 | // inverse relationship, since beyond that, everything's just |
mjr | 113:7330439f2ffc | 287 | // relative. The data sheet does at least offer a (low-res) graph |
mjr | 113:7330439f2ffc | 288 | // of the distance-vs-proximity-count relationship under one set of |
mjr | 113:7330439f2ffc | 289 | // test conditions, and interestingly, that graph suggests a rather |
mjr | 113:7330439f2ffc | 290 | // different power law, more like ~1/distance^3.1. The graph also |
mjr | 113:7330439f2ffc | 291 | // makes it clear that the response isn't uniform - it doesn't |
mjr | 113:7330439f2ffc | 292 | // follow *any* power law exactly, but is something more complex |
mjr | 113:7330439f2ffc | 293 | // than that. This is another non-surprise, given that environmental |
mjr | 113:7330439f2ffc | 294 | // factors will inevitably confound the readings to some degree. |
mjr | 113:7330439f2ffc | 295 | // |
mjr | 113:7330439f2ffc | 296 | // At any rate, in the data I've gathered, it seems that a simple 1/R^2 |
mjr | 113:7330439f2ffc | 297 | // power law is pretty close to reality, so I'm using that. (Brightness |
mjr | 113:7330439f2ffc | 298 | // varies with 1/R^2, so distance varies with 1/sqrt(brightness).) If |
mjr | 113:7330439f2ffc | 299 | // this turns out to produce noticeably non-linear results in other |
mjr | 113:7330439f2ffc | 300 | // people's installations, we might have to revisit this with something |
mjr | 113:7330439f2ffc | 301 | // more customized to the local setup. For example, we could gather |
mjr | 113:7330439f2ffc | 302 | // calibration data points across the whole plunger travel range and |
mjr | 113:7330439f2ffc | 303 | // then do a best-fit calculation to determine the best exponent |
mjr | 113:7330439f2ffc | 304 | // (which would still assume that there's *some* 1/R^x relationship |
mjr | 113:7330439f2ffc | 305 | // for some exponent x, but it wouldn't assume it's necessarily R^2.) |
mjr | 113:7330439f2ffc | 306 | // |
mjr | 113:7330439f2ffc | 307 | static inline float power(int x) |
mjr | 113:7330439f2ffc | 308 | { |
mjr | 113:7330439f2ffc | 309 | return sqrtf(static_cast<float>(x)); |
mjr | 113:7330439f2ffc | 310 | } |
mjr | 113:7330439f2ffc | 311 | |
mjr | 113:7330439f2ffc | 312 | // convert from a raw sensor count value to distance units, using our |
mjr | 113:7330439f2ffc | 313 | // current calibration data |
mjr | 113:7330439f2ffc | 314 | int VCNL4010::countToDistance(int count) |
mjr | 113:7330439f2ffc | 315 | { |
mjr | 113:7330439f2ffc | 316 | // remove the DC offset from teh signal |
mjr | 113:7330439f2ffc | 317 | count -= dcOffset; |
mjr | 113:7330439f2ffc | 318 | |
mjr | 113:7330439f2ffc | 319 | // if the adjusted count (excess of DC offset) is zero or negative, |
mjr | 113:7330439f2ffc | 320 | // peg it to the minimum end = maximum retraction point |
mjr | 113:7330439f2ffc | 321 | if (count <= 0) |
mjr | 113:7330439f2ffc | 322 | return 65535; |
mjr | 113:7330439f2ffc | 323 | |
mjr | 113:7330439f2ffc | 324 | // figure the distance based on our inverse power curve |
mjr | 113:7330439f2ffc | 325 | float d = scalingFactor/power(count) + scalingOffset; |
mjr | 113:7330439f2ffc | 326 | |
mjr | 113:7330439f2ffc | 327 | // constrain it to the valid range and convert to int for return |
mjr | 113:7330439f2ffc | 328 | return d < 0.0f ? 0 : d > 65535.0f ? 65535 : static_cast<int>(d); |
mjr | 113:7330439f2ffc | 329 | } |
mjr | 113:7330439f2ffc | 330 | |
mjr | 113:7330439f2ffc | 331 | // Calculate the scaling factors for our power-law formula for |
mjr | 113:7330439f2ffc | 332 | // converting proximity count (brightness) readings to distances. |
mjr | 113:7330439f2ffc | 333 | // We call this upon completing a new calibration pass, and during |
mjr | 113:7330439f2ffc | 334 | // initialization, when loading saved calibration data. |
mjr | 113:7330439f2ffc | 335 | void VCNL4010::calcScalingFactor() |
mjr | 113:7330439f2ffc | 336 | { |
mjr | 113:7330439f2ffc | 337 | // Don't let the minimum go below 100. The inverse relationship makes |
mjr | 113:7330439f2ffc | 338 | // the calculation meaningless at zero and unstable at very small |
mjr | 113:7330439f2ffc | 339 | // count values, so we need a reasonable floor to keep things in a |
mjr | 113:7330439f2ffc | 340 | // usable range. In practice, the minimum observed value will usually |
mjr | 113:7330439f2ffc | 341 | // be quite a lot higher (2000 to 20000 in my testing), which the |
mjr | 113:7330439f2ffc | 342 | // Vishay application note attributes to stray reflections from the |
mjr | 113:7330439f2ffc | 343 | // chip's mounting apparatus, ambient light, and noise within the |
mjr | 113:7330439f2ffc | 344 | // detector itself. But just in case, set a floor that will ensure |
mjr | 113:7330439f2ffc | 345 | // reasonable calculations. |
mjr | 113:7330439f2ffc | 346 | if (minProxCount < 100) |
mjr | 113:7330439f2ffc | 347 | minProxCount = 100; |
mjr | 113:7330439f2ffc | 348 | |
mjr | 113:7330439f2ffc | 349 | // Set a ceiling of 65535, since the sensor can't go higher |
mjr | 113:7330439f2ffc | 350 | if (maxProxCount > 65535) |
mjr | 113:7330439f2ffc | 351 | maxProxCount = 65535; |
mjr | 113:7330439f2ffc | 352 | |
mjr | 113:7330439f2ffc | 353 | // Figure the scaling factor and offset over the range from the park |
mjr | 113:7330439f2ffc | 354 | // position to the maximum retracted position, which corresponds to |
mjr | 113:7330439f2ffc | 355 | // the minimum count (lowest intensity reflection) we've observed. |
mjr | 113:7330439f2ffc | 356 | // |
mjr | 113:7330439f2ffc | 357 | // Do all calculations with the counts *after* subtracting out the |
mjr | 113:7330439f2ffc | 358 | // signal's DC offset, which is the brightness level registered on the |
mjr | 113:7330439f2ffc | 359 | // sensor when there's no reflective target in range. We can't directly |
mjr | 113:7330439f2ffc | 360 | // measure the DC offset in a plunger setup, since that would require |
mjr | 113:7330439f2ffc | 361 | // removing the plunger entirely, but we can guess that the minimum |
mjr | 113:7330439f2ffc | 362 | // reading observed during calibration is approximately equal to the |
mjr | 113:7330439f2ffc | 363 | // DC offset. The minimum brightness occurs when the plunger is at the |
mjr | 113:7330439f2ffc | 364 | // most distance point in its travel range from the sensor, which is |
mjr | 113:7330439f2ffc | 365 | // when it's pulled all the way back. The plunger travel distance is |
mjr | 113:7330439f2ffc | 366 | // just about at the limit of the VCNL4010's sensitivity, so the inverse |
mjr | 113:7330439f2ffc | 367 | // curve should be very nearly flat at this point, thus this is a very |
mjr | 113:7330439f2ffc | 368 | // close approximation of the true DC offset. |
mjr | 113:7330439f2ffc | 369 | const int dcOffsetDelta = 50; |
mjr | 113:7330439f2ffc | 370 | dcOffset = minProxCount > dcOffsetDelta ? minProxCount - dcOffsetDelta : 0; |
mjr | 113:7330439f2ffc | 371 | int park = parkProxCount - dcOffset; |
mjr | 113:7330439f2ffc | 372 | float parkInv = 1.0f/power(park); |
mjr | 113:7330439f2ffc | 373 | scalingFactor = 54612.5f / (1.0f/power(minProxCount - dcOffset) - parkInv); |
mjr | 113:7330439f2ffc | 374 | scalingOffset = 10922.5f - (scalingFactor * parkInv); |
mjr | 113:7330439f2ffc | 375 | } |
mjr | 113:7330439f2ffc | 376 | |
mjr | 113:7330439f2ffc | 377 | // Read an I2C register on the device |
mjr | 111:42dc75fbe623 | 378 | uint8_t VCNL4010::readReg(uint8_t registerAddr) |
mjr | 111:42dc75fbe623 | 379 | { |
mjr | 111:42dc75fbe623 | 380 | // write the request |
mjr | 111:42dc75fbe623 | 381 | uint8_t data_write[1] = { registerAddr }; |
mjr | 111:42dc75fbe623 | 382 | if (i2c.write(I2C_ADDR, data_write, 1, false)) |
mjr | 111:42dc75fbe623 | 383 | return 0x00; |
mjr | 111:42dc75fbe623 | 384 | |
mjr | 111:42dc75fbe623 | 385 | // read the result |
mjr | 111:42dc75fbe623 | 386 | uint8_t data_read[1]; |
mjr | 111:42dc75fbe623 | 387 | if (i2c.read(I2C_ADDR, data_read, 1)) |
mjr | 111:42dc75fbe623 | 388 | return 0x00; |
mjr | 111:42dc75fbe623 | 389 | |
mjr | 111:42dc75fbe623 | 390 | // return the result |
mjr | 111:42dc75fbe623 | 391 | return data_read[0]; |
mjr | 111:42dc75fbe623 | 392 | } |
mjr | 113:7330439f2ffc | 393 | |
mjr | 113:7330439f2ffc | 394 | // Write to an I2C register on the device |
mjr | 111:42dc75fbe623 | 395 | void VCNL4010::writeReg(uint8_t registerAddr, uint8_t data) |
mjr | 111:42dc75fbe623 | 396 | { |
mjr | 111:42dc75fbe623 | 397 | // set up the write: register number, data byte |
mjr | 111:42dc75fbe623 | 398 | uint8_t data_write[2] = { registerAddr, data }; |
mjr | 111:42dc75fbe623 | 399 | i2c.write(I2C_ADDR, data_write, 2); |
mjr | 111:42dc75fbe623 | 400 | } |