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 plunger.h Source File

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 = jitterFilter(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 = jitterFilter(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     // Apply the jitter filter.  The position is in unscaled native 
00219     // sensor units.
00220     int jitterFilter(int pos)
00221     {
00222         // Check to see where the new reading is relative to the
00223         // current window
00224         if (pos < jfLo)
00225         {
00226             // the new position is below the current window, so move
00227             // the window down such that the new point is at the bottom 
00228             // of the window
00229             jfLo = pos;
00230             jfHi = pos + jfWindow;
00231             
00232             // figure the new position as the centerpoint of the new window
00233             jfLast = pos = (jfHi + jfLo)/2;
00234             return pos;
00235         }
00236         else if (pos > jfHi)
00237         {
00238             // the new position is above the current window, so move
00239             // the window up such that the new point is at the top of
00240             // the window
00241             jfHi = pos;
00242             jfLo = pos - jfWindow;
00243 
00244             // figure the new position as the centerpoint of the new window
00245             jfLast = pos = (jfHi + jfLo)/2;
00246             return pos;
00247         }
00248         else
00249         {
00250             // the new position is inside the current window, so repeat
00251             // the last reading
00252             return jfLast;
00253         }
00254     }
00255     
00256     // Process a configuration variable change.  'varno' is the
00257     // USB protocol variable number being updated; 'cfg' is the
00258     // updated configuration.
00259     virtual void onConfigChange(int varno, Config &cfg)
00260     {
00261         switch (varno)
00262         {
00263         case 19:
00264             // Plunger filters - jitter window and reverse orientation.
00265             setJitterWindow(cfg.plunger.jitterWindow);
00266             setReverseOrientation((cfg.plunger.reverseOrientation & 0x01) != 0);
00267             break;
00268         }
00269     }
00270     
00271     // Set the jitter filter window size.  This is specified in native
00272     // sensor units.
00273     void setJitterWindow(int w)
00274     {
00275         // set the new window size
00276         jfWindow = w;
00277         
00278         // reset the running window
00279         jfHi = jfLo = jfLast;
00280     }
00281     
00282     // Set reverse orientation
00283     void setReverseOrientation(bool f) { reverseOrientation = f; }
00284         
00285 protected:
00286     // Native scale of the device.  This is the scale used for the position
00287     // reading in status reports.  This lets us report the position in the
00288     // same units the sensor itself uses, to avoid any rounding error 
00289     // converting to an abstract scale.
00290     //
00291     // The nativeScale value is the number of units in the range of raw
00292     // sensor readings returned from readRaw().  Raw readings thus have a
00293     // valid range of 0 to nativeScale-1.
00294     //
00295     // Image edge detection sensors use the pixel size of the image, since
00296     // the position is determined by the pixel position of the shadow in
00297     // the image.  Quadrature sensors and other sensors that report the
00298     // distance in terms of physical distance units should use the number
00299     // of quanta in the approximate total plunger travel distance of 3".
00300     // For example, the VL6180X uses millimeter quanta, so can report
00301     // about 77 quanta over 3"; a quadrature sensor that reports at 1/300"
00302     // intervals has about 900 quanta over 3".  Absolute encoders (e.g., 
00303     // bar code sensors) should use the bar code range.
00304     //
00305     // Sensors that are inherently analog (e.g., potentiometers, analog
00306     // distance sensors) can quantize on any arbitrary scale.  In most cases,
00307     // it's best to use the same 0..65535 scale used for the regular plunger
00308     // reports.
00309     uint16_t nativeScale;
00310     
00311     // Scaling factor to convert native readings to abstract units on the
00312     // 0x0000..0xFFFF scale used in the higher level sensor-independent
00313     // code.  Multiply a raw sensor position reading by this value to
00314     // get the equivalent value on the abstract scale.  This is expressed 
00315     // as a fixed-point real number with a scale of 65536: calculate it as
00316     //
00317     //   (65535U*65536U) / (nativeScale - 1);
00318     uint32_t scalingFactor;
00319     
00320     // Jitter filtering
00321     int jfWindow;                // window size, in native sensor units
00322     int jfLo, jfHi;              // bounds of current window
00323     int jfLast;                  // last filtered reading
00324     
00325     // Reverse the raw reading orientation.  If set, raw readings will be
00326     // switched to the opposite orientation.  This allows flipping the sensor
00327     // orientation virtually to correct for installing the physical device
00328     // backwards.
00329     bool reverseOrientation;
00330 };
00331 
00332 
00333 // --------------------------------------------------------------------------
00334 //
00335 // Generic image sensor interface for image-based plungers.
00336 //
00337 // This interface is designed to allow the underlying sensor code to work
00338 // asynchronously to transfer pixels from the sensor into memory using
00339 // multiple buffers arranged in a circular list.  We have a "ready" state,
00340 // which lets the sensor tell us when a buffer is available, and we have
00341 // the notion of "ownership" of the buffer.  When the client is done with
00342 // a frame, it must realease the frame back to the sensor so that the sensor
00343 // can use it for a subsequent frame transfer.
00344 //
00345 class PlungerSensorImageInterface
00346 {
00347 public:
00348     PlungerSensorImageInterface(int npix)
00349     {
00350         native_npix = npix;
00351     }
00352     
00353     // initialize the sensor
00354     virtual void init() = 0;
00355 
00356     // is the sensor ready?
00357     virtual bool ready() = 0;
00358     
00359     // Read the image.  This retrieves a pointer to the current frame
00360     // buffer, which is in memory space managed by the sensor.  This
00361     // MUST only be called when ready() returns true.  The buffer is
00362     // locked for the client's use until the client calls releasePix().
00363     // The client MUST call releasePix() when done with the buffer, so
00364     // that the sensor can reuse it for another frame.
00365     virtual void readPix(uint8_t* &pix, uint32_t &t) = 0;
00366     
00367     // Release the current frame buffer back to the sensor.  
00368     virtual void releasePix() = 0;
00369     
00370     // get the average sensor pixel scan time (the time it takes on average
00371     // to read one image frame from the sensor)
00372     virtual uint32_t getAvgScanTime() = 0;
00373     
00374     // Set the minimum integration time (microseconds)
00375     virtual void setMinIntTime(uint32_t us) = 0;
00376     
00377 protected:
00378     // number of pixels on sensor
00379     int native_npix;
00380 };
00381 
00382 
00383 // ----------------------------------------------------------------------------
00384 //
00385 // Plunger base class for image-based sensors
00386 //
00387 template<typename ProcessResult>
00388 class PlungerSensorImage: public PlungerSensor
00389 {
00390 public:
00391     PlungerSensorImage(PlungerSensorImageInterface &sensor, 
00392         int npix, int nativeScale, bool negativeImage = false) :
00393         PlungerSensor(nativeScale), 
00394         sensor(sensor),
00395         native_npix(npix),
00396         negativeImage(negativeImage),
00397         axcTime(0),
00398         extraIntTime(0)
00399     {
00400     }
00401     
00402     // initialize the sensor
00403     virtual void init() { sensor.init(); }
00404 
00405     // is the sensor ready?
00406     virtual bool ready() { return sensor.ready(); }
00407     
00408     // get the pixel transfer time
00409     virtual uint32_t getAvgScanTime() { return sensor.getAvgScanTime(); }
00410 
00411     // set extra integration time
00412     virtual void setExtraIntegrationTime(uint32_t us) { extraIntTime = us; }
00413     
00414     // read the plunger position
00415     virtual bool readRaw(PlungerReading &r)
00416     {
00417         // read pixels from the sensor
00418         uint8_t *pix;
00419         uint32_t tpix;
00420         sensor.readPix(pix, tpix);
00421         
00422         // process the pixels
00423         int pixpos;
00424         ProcessResult res;
00425         bool ok = process(pix, native_npix, pixpos, res);
00426         
00427         // release the buffer back to the sensor
00428         sensor.releasePix();
00429         
00430         // adjust the exposure time
00431         sensor.setMinIntTime(axcTime + extraIntTime);
00432 
00433         // if we successfully processed the frame, read the position
00434         if (ok)
00435         {            
00436             r.pos = pixpos;
00437             r.t = tpix;
00438         }
00439         
00440         // return the result
00441         return ok;
00442     }
00443 
00444     // Send a status report to the joystick interface.
00445     // See plunger.h for details on the arguments.
00446     virtual void sendStatusReport(USBJoystick &js, uint8_t flags)
00447     {
00448         // start a timer to measure the processing time
00449         Timer pt;
00450         pt.start();
00451 
00452         // get pixels
00453         uint8_t *pix;
00454         uint32_t t;
00455         sensor.readPix(pix, t);
00456 
00457         // process the pixels and read the position
00458         int pos, rawPos;
00459         int n = native_npix;
00460         ProcessResult res;
00461         if (process(pix, n, rawPos, res))
00462         {
00463             // success - apply the jitter filter
00464             pos = jitterFilter(rawPos);
00465         }
00466         else
00467         {
00468             // report 0xFFFF to indicate that the position wasn't read
00469             pos = 0xFFFF;
00470             rawPos = 0xFFFF;
00471         }
00472         
00473         // adjust the exposure time
00474         sensor.setMinIntTime(axcTime + extraIntTime);
00475 
00476         // note the processing time
00477         uint32_t processTime = pt.read_us();
00478         
00479         // If a low-res scan is desired, reduce to a subset of pixels.  Ignore
00480         // this for smaller sensors (below 512 pixels)
00481         if ((flags & 0x01) && n >= 512)
00482         {
00483             // figure how many sensor pixels we combine into each low-res pixel
00484             const int group = 8;
00485             int lowResPix = n / group;
00486             
00487             // combine the pixels
00488             int src, dst;
00489             for (src = dst = 0 ; dst < lowResPix ; ++dst)
00490             {
00491                 // average this block of pixels
00492                 int a = 0;
00493                 for (int j = 0 ; j < group ; ++j)
00494                     a += pix[src++];
00495                         
00496                 // we have the sum, so get the average
00497                 a /= group;
00498 
00499                 // store the down-res'd pixel in the array
00500                 pix[dst] = uint8_t(a);
00501             }
00502             
00503             // update the pixel count to the reduced array size
00504             n = lowResPix;
00505         }
00506         
00507         // figure the report flags
00508         int jsflags = 0;
00509         
00510         // add flags for the detected orientation: 0x01 for normal orientation,
00511         // 0x02 for reversed orientation; no flags if orientation is unknown
00512         int dir = getOrientation();
00513         if (dir == 1) 
00514             jsflags |= 0x01; 
00515         else if (dir == -1)
00516             jsflags |= 0x02;
00517             
00518         // send the sensor status report headers
00519         js.sendPlungerStatus(n, pos, jsflags, sensor.getAvgScanTime(), processTime);
00520         js.sendPlungerStatus2(nativeScale, jfLo, jfHi, rawPos, axcTime);
00521         
00522         // send any extra status headers for subclasses
00523         extraStatusHeaders(js, res);
00524         
00525         // If we're not in calibration mode, send the pixels
00526         extern bool plungerCalMode;
00527         if (!plungerCalMode)
00528         {
00529             // If the sensor uses a negative image format (brighter pixels are
00530             // represented by lower numbers in the pixel array), invert the scale
00531             // back to a normal photo-positive scale, so that the client doesn't
00532             // have to know these details.
00533             if (negativeImage)
00534             {
00535                 // Invert the photo-negative 255..0 scale to a normal,
00536                 // photo-positive 0..255 scale.  This is just a matter of
00537                 // calculating pos_pixel = 255 - neg_pixel for each pixel.
00538                 //
00539                 // There's a shortcut we can use here to make this loop go a
00540                 // lot faster than the naive approach.  Note that 255 decimal
00541                 // is 1111111 binary.  Subtracting any number in (0..255) from
00542                 // 255 is the same as inverting the bits in the other number.
00543                 // That is, 255 - X == ~X for all X in 0..255.  That's useful
00544                 // because it means that we can compute (255-X) as a purely
00545                 // bitwise operation, which means that we can perform it on
00546                 // blocks of bytes instead of individual bytes.  On ARM, we
00547                 // can perform bitwise operations four bytes at a time via
00548                 // DWORD instructions.  This lets us compute (255-X) for N
00549                 // bytes using N/4 loop iterations.
00550                 //
00551                 // One other small optimization we can apply is to notice that
00552                 // ~X == X ^ ~0, and that X ^= ~0 can be performed with a
00553                 // single ARM instruction.  So we can make the ARM C++ compiler
00554                 // translate the loop body into just three instructions:  XOR 
00555                 // with immediate data and auto-increment pointer, decrement 
00556                 // the counter, and jump if not zero.  That's as fast we could
00557                 // do it in hand-written assembly.  I clocked this loop at 
00558                 // 60us for the 1536-pixel TCD1103 array.
00559                 //
00560                 // Note two important constraints:
00561                 //
00562                 //  - 'pix' must be aligned on a DWORD (4-byte) boundary.
00563                 //    This is REQUIRED, because the XOR in the loop uses a
00564                 //    DWORD memory operand, which will halt the MCU with a
00565                 //    bus error if the pointer isn't DWORD-aligned.
00566                 //
00567                 //  - 'n' must be a multiple of 4 bytes.  This isn't strictly
00568                 //    required, but if it's not observed, the last (N - N/4)
00569                 //    bytes won't be inverted.
00570                 //
00571                 // The only sensor that uses a negative image is the TCD1103.
00572                 // Its buffer is DWORD-aligned because it's allocated via
00573                 // malloc(), which always does worst-case alignment.  Its
00574                 // buffer is 1546 bytes long, which violates the multiple-of-4
00575                 // rule, but inconsequentially, as the last 14 bytes represent
00576                 // dummy pixels that can be ignored (so it's okay that we'll 
00577                 // miss inverting the last two bytes).
00578                 //
00579                 uint32_t *pix32 = reinterpret_cast<uint32_t*>(pix);
00580                 for (int i = n/4; i != 0; --i)
00581                     *pix32++ ^= 0xFFFFFFFF;
00582             }            
00583 
00584             // send the pixels in report-sized chunks until we get them all
00585             int idx = 0;
00586             while (idx < n)
00587                 js.sendPlungerPix(idx, n, pix);
00588         }
00589         
00590         // release the pixel buffer
00591         sensor.releasePix();
00592     }
00593     
00594 protected:
00595     // process an image to read the plunger position
00596     virtual bool process(const uint8_t *pix, int npix, int &rawPos, ProcessResult &res) = 0;
00597     
00598     // send extra status headers, following the standard headers (types 0 and 1)
00599     virtual void extraStatusHeaders(USBJoystick &js, ProcessResult &res) { }
00600     
00601     // get the detected orientation
00602     virtual int getOrientation() const { return 0; }
00603     
00604     // underlying hardware sensor interface
00605     PlungerSensorImageInterface &sensor;
00606     
00607     // number of pixels
00608     int native_npix;
00609     
00610     // Does the sensor report a "negative" image?  This is like a photo
00611     // negative, where brighter pixels are represented by lower numbers in
00612     // the pixel array.
00613     bool negativeImage;
00614     
00615     // Auto-exposure time.  This is for use by process() in the subclass.
00616     // On each frame processing iteration, it can adjust this to optimize
00617     // the image quality.
00618     uint32_t axcTime;
00619     
00620     // Extra exposure time.  This is for use by the PC side, mostly for
00621     // debugging use to allow the PC user to manually adjust the exposure
00622     // when inspecting captured frames.
00623     uint32_t extraIntTime;
00624 };
00625 
00626 
00627 #endif /* PLUNGER_H */