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
plunger.h
00001 // Plunger Sensor Interface 00002 // 00003 // This module defines the abstract interface to the plunger sensors. 00004 // We support several different physical sensor types, so we need a 00005 // common interface for use in the main code. 00006 // 00007 // In case it's helpful in developing code for new sensor types, I've 00008 // measured the maximum instantaneous speed of a plunger at .175 inches 00009 // per millisecond, or 4.46 mm/ms. (I measured that with an AEDR-8300; 00010 // see that code for more details.) 00011 // 00012 00013 #ifndef PLUNGER_H 00014 #define PLUNGER_H 00015 00016 #include "config.h" 00017 00018 // Plunger reading with timestamp 00019 struct PlungerReading 00020 { 00021 // Raw sensor reading, normalied to 0x0000..0xFFFF range 00022 int pos; 00023 00024 // Rimestamp of reading, in microseconds, relative to an arbitrary 00025 // zero point. Note that a 32-bit int can only represent about 71.5 00026 // minutes worth of microseconds, so this value is only meaningful 00027 // to compute a delta from other recent readings. As long as two 00028 // readings are within 71.5 minutes of each other, the time difference 00029 // calculated from the timestamps using 32-bit math will be correct 00030 // *even if a rollover occurs* between the two readings, since the 00031 // calculation is done mod 2^32-1. 00032 uint32_t t; 00033 }; 00034 00035 class PlungerSensor 00036 { 00037 public: 00038 PlungerSensor(int nativeScale) 00039 { 00040 // use the joystick scale as our native scale by default 00041 this->nativeScale = nativeScale; 00042 00043 // figure the scaling factor 00044 scalingFactor = (65535UL*65536UL) / nativeScale; 00045 00046 // presume no jitter filter 00047 jfWindow = 0; 00048 00049 // initialize the jitter filter 00050 jfLo = jfHi = jfLast = 0; 00051 00052 // presume normal orientation 00053 reverseOrientation = false; 00054 } 00055 00056 // ---------- Abstract sensor interface ---------- 00057 00058 // Initialize the physical sensor device. This is called at startup 00059 // to set up the device for first use. 00060 virtual void init() { } 00061 00062 // Auto-zero the plunger. Relative sensor types, such as quadrature 00063 // sensors, can lose sync with the absolute position over time if they 00064 // ever miss any motion. We can automatically correct for this by 00065 // resetting to the park position after periods of inactivity. It's 00066 // usually safe to assume that the plunger is at the park position if it 00067 // hasn't moved in a long time, since the spring always returns it to 00068 // that position when it isn't being manipulated. The main loop monitors 00069 // for motion, and calls this after a long enough time goes by without 00070 // seeing any movement. Sensor types that are inherently absolute 00071 // (TSL1410, potentiometers) shouldn't do anything here. 00072 virtual void autoZero() { } 00073 00074 // Is the sensor ready to take a reading? The optical sensor requires 00075 // a fairly long time (2.5ms) to transfer the data for each reading, but 00076 // this is done via DMA, so we can carry on other work while the transfer 00077 // takes place. This lets us poll the sensor to see if it's still busy 00078 // working on the current reading's data transfer. 00079 virtual bool ready() { return true; } 00080 00081 // Read the sensor position, if possible. Returns true on success, 00082 // false if it wasn't possible to take a reading. On success, fills 00083 // in 'r' with the current reading and timestamp and returns true. 00084 // Returns false if a reading couldn't be taken. 00085 // 00086 // r.pos is set to the normalized position reading, and r.t is set to 00087 // the timestamp of the reading. 00088 // 00089 // The normalized position is the sensor reading, corrected for jitter, 00090 // and adjusted to the abstract 0x0000..0xFFFF range. 00091 // 00092 // The timestamp is the time the sensor reading was taken, relative to 00093 // an arbitrary zero point. The arbitrary zero point makes this useful 00094 // only for calculating the time between readings. Note that the 32-bit 00095 // timestamp rolls over about every 71 minutes, so it should only be 00096 // used for time differences between readings taken fairly close together. 00097 // In practice, the higher level code only uses this for a few consecutive 00098 // readings to calculate (nearly) instantaneous velocities, so the time 00099 // spans are only tens of milliseconds. 00100 // 00101 // Timing requirements: for best results, readings should be taken 00102 // in well under 5ms. The release motion of the physical plunger 00103 // takes from 30ms to 50ms, so we need to collect samples much faster 00104 // than that to avoid aliasing during the bounce. 00105 bool read(PlungerReading &r) 00106 { 00107 // fail if the hardware scan isn't ready 00108 if (!ready()) 00109 return false; 00110 00111 // get the raw reading 00112 if (readRaw(r)) 00113 { 00114 // adjust for orientation 00115 r.pos = applyOrientation(r.pos); 00116 00117 // process it through the jitter filter 00118 r.pos = postJitterFilter(r.pos); 00119 00120 // adjust to the abstract scale via the scaling factor 00121 r.pos = uint16_t(uint32_t((scalingFactor * r.pos) + 32768) >> 16); 00122 00123 // success 00124 return true; 00125 } 00126 else 00127 { 00128 // no reading is available 00129 return false; 00130 } 00131 } 00132 00133 // Get a raw plunger reading. This gets the raw sensor reading with 00134 // timestamp, without jitter filtering and without any scale adjustment. 00135 virtual bool readRaw(PlungerReading &r) = 0; 00136 00137 // Restore the saved calibration data from the configuration. The main 00138 // loop calls this at startup to let us initialize internals from the 00139 // saved calibration data. This is called even if the plunger isn't 00140 // calibrated, which is flagged in the config. 00141 virtual void restoreCalibration(Config &) { } 00142 00143 // Begin calibration. The main loop calls this when the user activates 00144 // calibration mode. Sensors that work in terms of relative positions, 00145 // such as quadrature-based sensors, can use this to set the reference 00146 // point for the park position internally. 00147 virtual void beginCalibration(Config &) { } 00148 00149 // End calibration. The main loop calls this when calibration mode is 00150 // completed. 00151 virtual void endCalibration(Config &) { } 00152 00153 // Send a sensor status report to the host, via the joystick interface. 00154 // This provides some common information for all sensor types, and also 00155 // includes a full image snapshot of the current sensor pixels for 00156 // imaging sensor types. 00157 // 00158 // The default implementation here sends the common information 00159 // packet, with the pixel size set to 0. 00160 // 00161 // 'flags' is a combination of bit flags: 00162 // 0x01 -> low-res scan (default is high res scan) 00163 // 00164 // Low-res scan mode means that the sensor should send a scaled-down 00165 // image, at a reduced size determined by the sensor subtype. The 00166 // default if this flag isn't set is to send the full image, at the 00167 // sensor's native pixel size. The low-res version is a reduced size 00168 // image in the normal sense of scaling down a photo image, keeping the 00169 // image intact but at reduced resolution. Note that low-res mode 00170 // doesn't affect the ongoing sensor operation at all. It only applies 00171 // to this single pixel report. The purpose is simply to reduce the USB 00172 // transmission time for the image, to allow for a faster frame rate for 00173 // displaying the sensor image in real time on the PC. For a high-res 00174 // sensor like the TSL1410R, sending the full pixel array by USB takes 00175 // so long that the frame rate is way below regular video rates. 00176 // 00177 virtual void sendStatusReport(class USBJoystick &js, uint8_t flags) 00178 { 00179 // read the current position 00180 int pos = 0xFFFF; 00181 PlungerReading r; 00182 if (readRaw(r)) 00183 { 00184 // adjust for reverse orientation 00185 r.pos = applyOrientation(r.pos); 00186 00187 // success - apply the jitter filter 00188 pos = postJitterFilter(r.pos); 00189 } 00190 00191 // Send the common status information, indicating 0 pixels, standard 00192 // sensor orientation, and zero processing time. Non-imaging sensors 00193 // usually don't have any way to detect the orientation, so assume 00194 // normal orientation (flag 0x01). Also assume zero analysis time, 00195 // as most non-image sensors don't have to do anything CPU-intensive 00196 // with the raw readings (all they usually have to do is scale the 00197 // value to the abstract reporting range). 00198 js.sendPlungerStatus(0, pos, 0x01, getAvgScanTime(), 0); 00199 js.sendPlungerStatus2(nativeScale, jfLo, jfHi, r.pos, 0); 00200 } 00201 00202 // Set extra image integration time, in microseconds. This is only 00203 // meaningful for image-type sensors. This allows the PC client to 00204 // manually adjust the exposure time for testing and debugging 00205 // purposes. 00206 virtual void setExtraIntegrationTime(uint32_t us) { } 00207 00208 // Get the average sensor scan time in microseconds 00209 virtual uint32_t getAvgScanTime() = 0; 00210 00211 // Apply the orientation filter. The position is in unscaled 00212 // native sensor units. 00213 int applyOrientation(int pos) 00214 { 00215 return (reverseOrientation ? nativeScale - pos : pos); 00216 } 00217 00218 // Post-filter a raw reading through the mitter filter. Most plunger 00219 // sensor subclasses can use this default implementation, since the 00220 // jitter filter is usually applied to the raw position reading. 00221 // However, for some sensor types, it might be better to apply the 00222 // jitter filtering to the underlying physical sensor reading, before 00223 // readRaw() translates the reading into distance units. In that 00224 // case, the subclass can override this to simply return the argument 00225 // unchanged. This allows subclasses to use jitterFilter() if desired 00226 // on their physical sensor readings. It's not either/or, though; a 00227 // subclass that overrides jitter post-filtering isn't could use an 00228 // entirely different noise filtering algorithm on its sensor data. 00229 virtual int postJitterFilter(int pos) { return jitterFilter(pos); } 00230 00231 // Apply the jitter filter. The position is in unscaled native 00232 // sensor units. 00233 int jitterFilter(int pos) 00234 { 00235 // Check to see where the new reading is relative to the 00236 // current window 00237 if (pos < jfLo) 00238 { 00239 // the new position is below the current window, so move 00240 // the window down such that the new point is at the bottom 00241 // of the window 00242 jfLo = pos; 00243 jfHi = pos + jfWindow; 00244 00245 // figure the new position as the centerpoint of the new window 00246 jfLast = pos = (jfHi + jfLo)/2; 00247 return pos; 00248 } 00249 else if (pos > jfHi) 00250 { 00251 // the new position is above the current window, so move 00252 // the window up such that the new point is at the top of 00253 // the window 00254 jfHi = pos; 00255 jfLo = pos - jfWindow; 00256 00257 // figure the new position as the centerpoint of the new window 00258 jfLast = pos = (jfHi + jfLo)/2; 00259 return pos; 00260 } 00261 else 00262 { 00263 // the new position is inside the current window, so repeat 00264 // the last reading 00265 return jfLast; 00266 } 00267 } 00268 00269 // Process a configuration variable change. 'varno' is the 00270 // USB protocol variable number being updated; 'cfg' is the 00271 // updated configuration. 00272 virtual void onConfigChange(int varno, Config &cfg) 00273 { 00274 switch (varno) 00275 { 00276 case 19: 00277 // Plunger filters - jitter window and reverse orientation. 00278 setJitterWindow(cfg.plunger.jitterWindow); 00279 setReverseOrientation((cfg.plunger.reverseOrientation & 0x01) != 0); 00280 break; 00281 } 00282 } 00283 00284 // Set the jitter filter window size. This is specified in native 00285 // sensor units. 00286 void setJitterWindow(int w) 00287 { 00288 // set the new window size 00289 jfWindow = w; 00290 00291 // reset the running window 00292 jfHi = jfLo = jfLast; 00293 } 00294 00295 // Set reverse orientation 00296 void setReverseOrientation(bool f) { reverseOrientation = f; } 00297 00298 protected: 00299 // Native scale of the device. This is the scale used for the position 00300 // reading in status reports. This lets us report the position in the 00301 // same units the sensor itself uses, to avoid any rounding error 00302 // converting to an abstract scale. 00303 // 00304 // The nativeScale value is the number of units in the range of raw 00305 // sensor readings returned from readRaw(). Raw readings thus have a 00306 // valid range of 0 to nativeScale-1. 00307 // 00308 // Image edge detection sensors use the pixel size of the image, since 00309 // the position is determined by the pixel position of the shadow in 00310 // the image. Quadrature sensors and other sensors that report the 00311 // distance in terms of physical distance units should use the number 00312 // of quanta in the approximate total plunger travel distance of 3". 00313 // For example, the VL6180X uses millimeter quanta, so can report 00314 // about 77 quanta over 3"; a quadrature sensor that reports at 1/300" 00315 // intervals has about 900 quanta over 3". Absolute encoders (e.g., 00316 // bar code sensors) should use the bar code range. 00317 // 00318 // Sensors that are inherently analog (e.g., potentiometers, analog 00319 // distance sensors) can quantize on any arbitrary scale. In most cases, 00320 // it's best to use the same 0..65535 scale used for the regular plunger 00321 // reports. 00322 uint16_t nativeScale; 00323 00324 // Scaling factor to convert native readings to abstract units on the 00325 // 0x0000..0xFFFF scale used in the higher level sensor-independent 00326 // code. Multiply a raw sensor position reading by this value to 00327 // get the equivalent value on the abstract scale. This is expressed 00328 // as a fixed-point real number with a scale of 65536: calculate it as 00329 // 00330 // (65535U*65536U) / (nativeScale - 1); 00331 uint32_t scalingFactor; 00332 00333 // Jitter filtering 00334 int jfWindow; // window size, in native sensor units 00335 int jfLo, jfHi; // bounds of current window 00336 int jfLast; // last filtered reading 00337 00338 // Reverse the raw reading orientation. If set, raw readings will be 00339 // switched to the opposite orientation. This allows flipping the sensor 00340 // orientation virtually to correct for installing the physical device 00341 // backwards. 00342 bool reverseOrientation; 00343 }; 00344 00345 00346 // -------------------------------------------------------------------------- 00347 // 00348 // Generic image sensor interface for image-based plungers. 00349 // 00350 // This interface is designed to allow the underlying sensor code to work 00351 // asynchronously to transfer pixels from the sensor into memory using 00352 // multiple buffers arranged in a circular list. We have a "ready" state, 00353 // which lets the sensor tell us when a buffer is available, and we have 00354 // the notion of "ownership" of the buffer. When the client is done with 00355 // a frame, it must realease the frame back to the sensor so that the sensor 00356 // can use it for a subsequent frame transfer. 00357 // 00358 class PlungerSensorImageInterface 00359 { 00360 public: 00361 PlungerSensorImageInterface(int npix) 00362 { 00363 native_npix = npix; 00364 } 00365 00366 // initialize the sensor 00367 virtual void init() = 0; 00368 00369 // is the sensor ready? 00370 virtual bool ready() = 0; 00371 00372 // Read the image. This retrieves a pointer to the current frame 00373 // buffer, which is in memory space managed by the sensor. This 00374 // MUST only be called when ready() returns true. The buffer is 00375 // locked for the client's use until the client calls releasePix(). 00376 // The client MUST call releasePix() when done with the buffer, so 00377 // that the sensor can reuse it for another frame. 00378 virtual void readPix(uint8_t* &pix, uint32_t &t) = 0; 00379 00380 // Release the current frame buffer back to the sensor. 00381 virtual void releasePix() = 0; 00382 00383 // get the average sensor pixel scan time (the time it takes on average 00384 // to read one image frame from the sensor) 00385 virtual uint32_t getAvgScanTime() = 0; 00386 00387 // Set the minimum integration time (microseconds) 00388 virtual void setMinIntTime(uint32_t us) = 0; 00389 00390 protected: 00391 // number of pixels on sensor 00392 int native_npix; 00393 }; 00394 00395 00396 // ---------------------------------------------------------------------------- 00397 // 00398 // Plunger base class for image-based sensors 00399 // 00400 template<typename ProcessResult> 00401 class PlungerSensorImage: public PlungerSensor 00402 { 00403 public: 00404 PlungerSensorImage(PlungerSensorImageInterface &sensor, 00405 int npix, int nativeScale, bool negativeImage = false) : 00406 PlungerSensor(nativeScale), 00407 sensor(sensor), 00408 native_npix(npix), 00409 negativeImage(negativeImage), 00410 axcTime(0), 00411 extraIntTime(0) 00412 { 00413 } 00414 00415 // initialize the sensor 00416 virtual void init() { sensor.init(); } 00417 00418 // is the sensor ready? 00419 virtual bool ready() { return sensor.ready(); } 00420 00421 // get the pixel transfer time 00422 virtual uint32_t getAvgScanTime() { return sensor.getAvgScanTime(); } 00423 00424 // set extra integration time 00425 virtual void setExtraIntegrationTime(uint32_t us) { extraIntTime = us; } 00426 00427 // read the plunger position 00428 virtual bool readRaw(PlungerReading &r) 00429 { 00430 // read pixels from the sensor 00431 uint8_t *pix; 00432 uint32_t tpix; 00433 sensor.readPix(pix, tpix); 00434 00435 // process the pixels 00436 int pixpos; 00437 ProcessResult res; 00438 bool ok = process(pix, native_npix, pixpos, res); 00439 00440 // release the buffer back to the sensor 00441 sensor.releasePix(); 00442 00443 // adjust the exposure time 00444 sensor.setMinIntTime(axcTime + extraIntTime); 00445 00446 // if we successfully processed the frame, read the position 00447 if (ok) 00448 { 00449 r.pos = pixpos; 00450 r.t = tpix; 00451 } 00452 00453 // return the result 00454 return ok; 00455 } 00456 00457 // Send a status report to the joystick interface. 00458 // See plunger.h for details on the arguments. 00459 virtual void sendStatusReport(USBJoystick &js, uint8_t flags) 00460 { 00461 // start a timer to measure the processing time 00462 Timer pt; 00463 pt.start(); 00464 00465 // get pixels 00466 uint8_t *pix; 00467 uint32_t t; 00468 sensor.readPix(pix, t); 00469 00470 // process the pixels and read the position 00471 int pos, rawPos; 00472 int n = native_npix; 00473 ProcessResult res; 00474 if (process(pix, n, rawPos, res)) 00475 { 00476 // success - apply the post jitter filter 00477 pos = postJitterFilter(rawPos); 00478 } 00479 else 00480 { 00481 // report 0xFFFF to indicate that the position wasn't read 00482 pos = 0xFFFF; 00483 rawPos = 0xFFFF; 00484 } 00485 00486 // adjust the exposure time 00487 sensor.setMinIntTime(axcTime + extraIntTime); 00488 00489 // note the processing time 00490 uint32_t processTime = pt.read_us(); 00491 00492 // If a low-res scan is desired, reduce to a subset of pixels. Ignore 00493 // this for smaller sensors (below 512 pixels) 00494 if ((flags & 0x01) && n >= 512) 00495 { 00496 // figure how many sensor pixels we combine into each low-res pixel 00497 const int group = 8; 00498 int lowResPix = n / group; 00499 00500 // combine the pixels 00501 int src, dst; 00502 for (src = dst = 0 ; dst < lowResPix ; ++dst) 00503 { 00504 // average this block of pixels 00505 int a = 0; 00506 for (int j = 0 ; j < group ; ++j) 00507 a += pix[src++]; 00508 00509 // we have the sum, so get the average 00510 a /= group; 00511 00512 // store the down-res'd pixel in the array 00513 pix[dst] = uint8_t(a); 00514 } 00515 00516 // update the pixel count to the reduced array size 00517 n = lowResPix; 00518 } 00519 00520 // figure the report flags 00521 int jsflags = 0; 00522 00523 // add flags for the detected orientation: 0x01 for normal orientation, 00524 // 0x02 for reversed orientation; no flags if orientation is unknown 00525 int dir = getOrientation(); 00526 if (dir == 1) 00527 jsflags |= 0x01; 00528 else if (dir == -1) 00529 jsflags |= 0x02; 00530 00531 // send the sensor status report headers 00532 js.sendPlungerStatus(n, pos, jsflags, sensor.getAvgScanTime(), processTime); 00533 js.sendPlungerStatus2(nativeScale, jfLo, jfHi, rawPos, axcTime); 00534 00535 // send any extra status headers for subclasses 00536 extraStatusHeaders(js, res); 00537 00538 // If we're not in calibration mode, send the pixels 00539 extern bool plungerCalMode; 00540 if (!plungerCalMode) 00541 { 00542 // If the sensor uses a negative image format (brighter pixels are 00543 // represented by lower numbers in the pixel array), invert the scale 00544 // back to a normal photo-positive scale, so that the client doesn't 00545 // have to know these details. 00546 if (negativeImage) 00547 { 00548 // Invert the photo-negative 255..0 scale to a normal, 00549 // photo-positive 0..255 scale. This is just a matter of 00550 // calculating pos_pixel = 255 - neg_pixel for each pixel. 00551 // 00552 // There's a shortcut we can use here to make this loop go a 00553 // lot faster than the naive approach. Note that 255 decimal 00554 // is 1111111 binary. Subtracting any number in (0..255) from 00555 // 255 is the same as inverting the bits in the other number. 00556 // That is, 255 - X == ~X for all X in 0..255. That's useful 00557 // because it means that we can compute (255-X) as a purely 00558 // bitwise operation, which means that we can perform it on 00559 // blocks of bytes instead of individual bytes. On ARM, we 00560 // can perform bitwise operations four bytes at a time via 00561 // DWORD instructions. This lets us compute (255-X) for N 00562 // bytes using N/4 loop iterations. 00563 // 00564 // One other small optimization we can apply is to notice that 00565 // ~X == X ^ ~0, and that X ^= ~0 can be performed with a 00566 // single ARM instruction. So we can make the ARM C++ compiler 00567 // translate the loop body into just three instructions: XOR 00568 // with immediate data and auto-increment pointer, decrement 00569 // the counter, and jump if not zero. That's as fast we could 00570 // do it in hand-written assembly. I clocked this loop at 00571 // 60us for the 1536-pixel TCD1103 array. 00572 // 00573 // Note two important constraints: 00574 // 00575 // - 'pix' must be aligned on a DWORD (4-byte) boundary. 00576 // This is REQUIRED, because the XOR in the loop uses a 00577 // DWORD memory operand, which will halt the MCU with a 00578 // bus error if the pointer isn't DWORD-aligned. 00579 // 00580 // - 'n' must be a multiple of 4 bytes. This isn't strictly 00581 // required, but if it's not observed, the last (N - N/4) 00582 // bytes won't be inverted. 00583 // 00584 // The only sensor that uses a negative image is the TCD1103. 00585 // Its buffer is DWORD-aligned because it's allocated via 00586 // malloc(), which always does worst-case alignment. Its 00587 // buffer is 1546 bytes long, which violates the multiple-of-4 00588 // rule, but inconsequentially, as the last 14 bytes represent 00589 // dummy pixels that can be ignored (so it's okay that we'll 00590 // miss inverting the last two bytes). 00591 // 00592 uint32_t *pix32 = reinterpret_cast<uint32_t*>(pix); 00593 for (int i = n/4; i != 0; --i) 00594 *pix32++ ^= 0xFFFFFFFF; 00595 } 00596 00597 // send the pixels in report-sized chunks until we get them all 00598 int idx = 0; 00599 while (idx < n) 00600 js.sendPlungerPix(idx, n, pix); 00601 } 00602 00603 // release the pixel buffer 00604 sensor.releasePix(); 00605 } 00606 00607 protected: 00608 // process an image to read the plunger position 00609 virtual bool process(const uint8_t *pix, int npix, int &rawPos, ProcessResult &res) = 0; 00610 00611 // send extra status headers, following the standard headers (types 0 and 1) 00612 virtual void extraStatusHeaders(USBJoystick &js, ProcessResult &res) { } 00613 00614 // get the detected orientation 00615 virtual int getOrientation() const { return 0; } 00616 00617 // underlying hardware sensor interface 00618 PlungerSensorImageInterface &sensor; 00619 00620 // number of pixels 00621 int native_npix; 00622 00623 // Does the sensor report a "negative" image? This is like a photo 00624 // negative, where brighter pixels are represented by lower numbers in 00625 // the pixel array. 00626 bool negativeImage; 00627 00628 // Auto-exposure time. This is for use by process() in the subclass. 00629 // On each frame processing iteration, it can adjust this to optimize 00630 // the image quality. 00631 uint32_t axcTime; 00632 00633 // Extra exposure time. This is for use by the PC side, mostly for 00634 // debugging use to allow the PC user to manually adjust the exposure 00635 // when inspecting captured frames. 00636 uint32_t extraIntTime; 00637 }; 00638 00639 00640 #endif /* PLUNGER_H */
Generated on Wed Jul 13 2022 03:30:11 by 1.7.2