Mirror with some correction
Dependencies: mbed FastIO FastPWM USBDevice
Plunger/plunger.h
- Committer:
- arnoz
- Date:
- 2021-10-01
- Revision:
- 116:7a67265d7c19
- Parent:
- 113:7330439f2ffc
File content as of revision 116:7a67265d7c19:
// Plunger Sensor Interface // // This module defines the abstract interface to the plunger sensors. // We support several different physical sensor types, so we need a // common interface for use in the main code. // // In case it's helpful in developing code for new sensor types, I've // measured the maximum instantaneous speed of a plunger at .175 inches // per millisecond, or 4.46 mm/ms. (I measured that with an AEDR-8300; // see that code for more details.) // #ifndef PLUNGER_H #define PLUNGER_H #include "config.h" // Plunger reading with timestamp struct PlungerReading { // Raw sensor reading, normalied to 0x0000..0xFFFF range int pos; // Rimestamp of reading, in microseconds, relative to an arbitrary // zero point. Note that a 32-bit int can only represent about 71.5 // minutes worth of microseconds, so this value is only meaningful // to compute a delta from other recent readings. As long as two // readings are within 71.5 minutes of each other, the time difference // calculated from the timestamps using 32-bit math will be correct // *even if a rollover occurs* between the two readings, since the // calculation is done mod 2^32-1. uint32_t t; }; class PlungerSensor { public: PlungerSensor(int nativeScale) { // use the joystick scale as our native scale by default this->nativeScale = nativeScale; // figure the scaling factor scalingFactor = (65535UL*65536UL) / nativeScale; // presume no jitter filter jfWindow = 0; // initialize the jitter filter jfLo = jfHi = jfLast = 0; // presume normal orientation reverseOrientation = false; } // ---------- Abstract sensor interface ---------- // Initialize the physical sensor device. This is called at startup // to set up the device for first use. virtual void init() { } // Auto-zero the plunger. Relative sensor types, such as quadrature // sensors, can lose sync with the absolute position over time if they // ever miss any motion. We can automatically correct for this by // resetting to the park position after periods of inactivity. It's // usually safe to assume that the plunger is at the park position if it // hasn't moved in a long time, since the spring always returns it to // that position when it isn't being manipulated. The main loop monitors // for motion, and calls this after a long enough time goes by without // seeing any movement. Sensor types that are inherently absolute // (TSL1410, potentiometers) shouldn't do anything here. virtual void autoZero() { } // Is the sensor ready to take a reading? The optical sensor requires // a fairly long time (2.5ms) to transfer the data for each reading, but // this is done via DMA, so we can carry on other work while the transfer // takes place. This lets us poll the sensor to see if it's still busy // working on the current reading's data transfer. virtual bool ready() { return true; } // Read the sensor position, if possible. Returns true on success, // false if it wasn't possible to take a reading. On success, fills // in 'r' with the current reading and timestamp and returns true. // Returns false if a reading couldn't be taken. // // r.pos is set to the normalized position reading, and r.t is set to // the timestamp of the reading. // // The normalized position is the sensor reading, corrected for jitter, // and adjusted to the abstract 0x0000..0xFFFF range. // // The timestamp is the time the sensor reading was taken, relative to // an arbitrary zero point. The arbitrary zero point makes this useful // only for calculating the time between readings. Note that the 32-bit // timestamp rolls over about every 71 minutes, so it should only be // used for time differences between readings taken fairly close together. // In practice, the higher level code only uses this for a few consecutive // readings to calculate (nearly) instantaneous velocities, so the time // spans are only tens of milliseconds. // // Timing requirements: for best results, readings should be taken // in well under 5ms. The release motion of the physical plunger // takes from 30ms to 50ms, so we need to collect samples much faster // than that to avoid aliasing during the bounce. bool read(PlungerReading &r) { // fail if the hardware scan isn't ready if (!ready()) return false; // get the raw reading if (readRaw(r)) { // adjust for orientation r.pos = applyOrientation(r.pos); // process it through the jitter filter r.pos = postJitterFilter(r.pos); // adjust to the abstract scale via the scaling factor r.pos = uint16_t(uint32_t((scalingFactor * r.pos) + 32768) >> 16); // success return true; } else { // no reading is available return false; } } // Get a raw plunger reading. This gets the raw sensor reading with // timestamp, without jitter filtering and without any scale adjustment. virtual bool readRaw(PlungerReading &r) = 0; // Restore the saved calibration data from the configuration. The main // loop calls this at startup to let us initialize internals from the // saved calibration data. This is called even if the plunger isn't // calibrated, which is flagged in the config. virtual void restoreCalibration(Config &) { } // Begin calibration. The main loop calls this when the user activates // calibration mode. Sensors that work in terms of relative positions, // such as quadrature-based sensors, can use this to set the reference // point for the park position internally. virtual void beginCalibration(Config &) { } // End calibration. The main loop calls this when calibration mode is // completed. virtual void endCalibration(Config &) { } // Send a sensor status report to the host, via the joystick interface. // This provides some common information for all sensor types, and also // includes a full image snapshot of the current sensor pixels for // imaging sensor types. // // The default implementation here sends the common information // packet, with the pixel size set to 0. // // 'flags' is a combination of bit flags: // 0x01 -> low-res scan (default is high res scan) // // Low-res scan mode means that the sensor should send a scaled-down // image, at a reduced size determined by the sensor subtype. The // default if this flag isn't set is to send the full image, at the // sensor's native pixel size. The low-res version is a reduced size // image in the normal sense of scaling down a photo image, keeping the // image intact but at reduced resolution. Note that low-res mode // doesn't affect the ongoing sensor operation at all. It only applies // to this single pixel report. The purpose is simply to reduce the USB // transmission time for the image, to allow for a faster frame rate for // displaying the sensor image in real time on the PC. For a high-res // sensor like the TSL1410R, sending the full pixel array by USB takes // so long that the frame rate is way below regular video rates. // virtual void sendStatusReport(class USBJoystick &js, uint8_t flags) { // read the current position int pos = 0xFFFF; PlungerReading r; if (readRaw(r)) { // adjust for reverse orientation r.pos = applyOrientation(r.pos); // success - apply the jitter filter pos = postJitterFilter(r.pos); } // Send the common status information, indicating 0 pixels, standard // sensor orientation, and zero processing time. Non-imaging sensors // usually don't have any way to detect the orientation, so assume // normal orientation (flag 0x01). Also assume zero analysis time, // as most non-image sensors don't have to do anything CPU-intensive // with the raw readings (all they usually have to do is scale the // value to the abstract reporting range). js.sendPlungerStatus(0, pos, 0x01, getAvgScanTime(), 0); js.sendPlungerStatus2(nativeScale, jfLo, jfHi, r.pos, 0); } // Set extra image integration time, in microseconds. This is only // meaningful for image-type sensors. This allows the PC client to // manually adjust the exposure time for testing and debugging // purposes. virtual void setExtraIntegrationTime(uint32_t us) { } // Get the average sensor scan time in microseconds virtual uint32_t getAvgScanTime() = 0; // Apply the orientation filter. The position is in unscaled // native sensor units. int applyOrientation(int pos) { return (reverseOrientation ? nativeScale - pos : pos); } // Post-filter a raw reading through the mitter filter. Most plunger // sensor subclasses can use this default implementation, since the // jitter filter is usually applied to the raw position reading. // However, for some sensor types, it might be better to apply the // jitter filtering to the underlying physical sensor reading, before // readRaw() translates the reading into distance units. In that // case, the subclass can override this to simply return the argument // unchanged. This allows subclasses to use jitterFilter() if desired // on their physical sensor readings. It's not either/or, though; a // subclass that overrides jitter post-filtering isn't could use an // entirely different noise filtering algorithm on its sensor data. virtual int postJitterFilter(int pos) { return jitterFilter(pos); } // Apply the jitter filter. The position is in unscaled native // sensor units. int jitterFilter(int pos) { // Check to see where the new reading is relative to the // current window if (pos < jfLo) { // the new position is below the current window, so move // the window down such that the new point is at the bottom // of the window jfLo = pos; jfHi = pos + jfWindow; // figure the new position as the centerpoint of the new window jfLast = pos = (jfHi + jfLo)/2; return pos; } else if (pos > jfHi) { // the new position is above the current window, so move // the window up such that the new point is at the top of // the window jfHi = pos; jfLo = pos - jfWindow; // figure the new position as the centerpoint of the new window jfLast = pos = (jfHi + jfLo)/2; return pos; } else { // the new position is inside the current window, so repeat // the last reading return jfLast; } } // Process a configuration variable change. 'varno' is the // USB protocol variable number being updated; 'cfg' is the // updated configuration. virtual void onConfigChange(int varno, Config &cfg) { switch (varno) { case 19: // Plunger filters - jitter window and reverse orientation. setJitterWindow(cfg.plunger.jitterWindow); setReverseOrientation((cfg.plunger.reverseOrientation & 0x01) != 0); break; } } // Set the jitter filter window size. This is specified in native // sensor units. void setJitterWindow(int w) { // set the new window size jfWindow = w; // reset the running window jfHi = jfLo = jfLast; } // Set reverse orientation void setReverseOrientation(bool f) { reverseOrientation = f; } protected: // Native scale of the device. This is the scale used for the position // reading in status reports. This lets us report the position in the // same units the sensor itself uses, to avoid any rounding error // converting to an abstract scale. // // The nativeScale value is the number of units in the range of raw // sensor readings returned from readRaw(). Raw readings thus have a // valid range of 0 to nativeScale-1. // // Image edge detection sensors use the pixel size of the image, since // the position is determined by the pixel position of the shadow in // the image. Quadrature sensors and other sensors that report the // distance in terms of physical distance units should use the number // of quanta in the approximate total plunger travel distance of 3". // For example, the VL6180X uses millimeter quanta, so can report // about 77 quanta over 3"; a quadrature sensor that reports at 1/300" // intervals has about 900 quanta over 3". Absolute encoders (e.g., // bar code sensors) should use the bar code range. // // Sensors that are inherently analog (e.g., potentiometers, analog // distance sensors) can quantize on any arbitrary scale. In most cases, // it's best to use the same 0..65535 scale used for the regular plunger // reports. uint16_t nativeScale; // Scaling factor to convert native readings to abstract units on the // 0x0000..0xFFFF scale used in the higher level sensor-independent // code. Multiply a raw sensor position reading by this value to // get the equivalent value on the abstract scale. This is expressed // as a fixed-point real number with a scale of 65536: calculate it as // // (65535U*65536U) / (nativeScale - 1); uint32_t scalingFactor; // Jitter filtering int jfWindow; // window size, in native sensor units int jfLo, jfHi; // bounds of current window int jfLast; // last filtered reading // Reverse the raw reading orientation. If set, raw readings will be // switched to the opposite orientation. This allows flipping the sensor // orientation virtually to correct for installing the physical device // backwards. bool reverseOrientation; }; // -------------------------------------------------------------------------- // // Generic image sensor interface for image-based plungers. // // This interface is designed to allow the underlying sensor code to work // asynchronously to transfer pixels from the sensor into memory using // multiple buffers arranged in a circular list. We have a "ready" state, // which lets the sensor tell us when a buffer is available, and we have // the notion of "ownership" of the buffer. When the client is done with // a frame, it must realease the frame back to the sensor so that the sensor // can use it for a subsequent frame transfer. // class PlungerSensorImageInterface { public: PlungerSensorImageInterface(int npix) { native_npix = npix; } // initialize the sensor virtual void init() = 0; // is the sensor ready? virtual bool ready() = 0; // Read the image. This retrieves a pointer to the current frame // buffer, which is in memory space managed by the sensor. This // MUST only be called when ready() returns true. The buffer is // locked for the client's use until the client calls releasePix(). // The client MUST call releasePix() when done with the buffer, so // that the sensor can reuse it for another frame. virtual void readPix(uint8_t* &pix, uint32_t &t) = 0; // Release the current frame buffer back to the sensor. virtual void releasePix() = 0; // get the average sensor pixel scan time (the time it takes on average // to read one image frame from the sensor) virtual uint32_t getAvgScanTime() = 0; // Set the minimum integration time (microseconds) virtual void setMinIntTime(uint32_t us) = 0; protected: // number of pixels on sensor int native_npix; }; // ---------------------------------------------------------------------------- // // Plunger base class for image-based sensors // template<typename ProcessResult> class PlungerSensorImage: public PlungerSensor { public: PlungerSensorImage(PlungerSensorImageInterface &sensor, int npix, int nativeScale, bool negativeImage = false) : PlungerSensor(nativeScale), sensor(sensor), native_npix(npix), negativeImage(negativeImage), axcTime(0), extraIntTime(0) { } // initialize the sensor virtual void init() { sensor.init(); } // is the sensor ready? virtual bool ready() { return sensor.ready(); } // get the pixel transfer time virtual uint32_t getAvgScanTime() { return sensor.getAvgScanTime(); } // set extra integration time virtual void setExtraIntegrationTime(uint32_t us) { extraIntTime = us; } // read the plunger position virtual bool readRaw(PlungerReading &r) { // read pixels from the sensor uint8_t *pix; uint32_t tpix; sensor.readPix(pix, tpix); // process the pixels int pixpos; ProcessResult res; bool ok = process(pix, native_npix, pixpos, res); // release the buffer back to the sensor sensor.releasePix(); // adjust the exposure time sensor.setMinIntTime(axcTime + extraIntTime); // if we successfully processed the frame, read the position if (ok) { r.pos = pixpos; r.t = tpix; } // return the result return ok; } // Send a status report to the joystick interface. // See plunger.h for details on the arguments. virtual void sendStatusReport(USBJoystick &js, uint8_t flags) { // start a timer to measure the processing time Timer pt; pt.start(); // get pixels uint8_t *pix; uint32_t t; sensor.readPix(pix, t); // process the pixels and read the position int pos, rawPos; int n = native_npix; ProcessResult res; if (process(pix, n, rawPos, res)) { // success - apply the post jitter filter pos = postJitterFilter(rawPos); } else { // report 0xFFFF to indicate that the position wasn't read pos = 0xFFFF; rawPos = 0xFFFF; } // adjust the exposure time sensor.setMinIntTime(axcTime + extraIntTime); // note the processing time uint32_t processTime = pt.read_us(); // If a low-res scan is desired, reduce to a subset of pixels. Ignore // this for smaller sensors (below 512 pixels) if ((flags & 0x01) && n >= 512) { // figure how many sensor pixels we combine into each low-res pixel const int group = 8; int lowResPix = n / group; // combine the pixels int src, dst; for (src = dst = 0 ; dst < lowResPix ; ++dst) { // average this block of pixels int a = 0; for (int j = 0 ; j < group ; ++j) a += pix[src++]; // we have the sum, so get the average a /= group; // store the down-res'd pixel in the array pix[dst] = uint8_t(a); } // update the pixel count to the reduced array size n = lowResPix; } // figure the report flags int jsflags = 0; // add flags for the detected orientation: 0x01 for normal orientation, // 0x02 for reversed orientation; no flags if orientation is unknown int dir = getOrientation(); if (dir == 1) jsflags |= 0x01; else if (dir == -1) jsflags |= 0x02; // send the sensor status report headers js.sendPlungerStatus(n, pos, jsflags, sensor.getAvgScanTime(), processTime); js.sendPlungerStatus2(nativeScale, jfLo, jfHi, rawPos, axcTime); // send any extra status headers for subclasses extraStatusHeaders(js, res); // If we're not in calibration mode, send the pixels extern bool plungerCalMode; if (!plungerCalMode) { // If the sensor uses a negative image format (brighter pixels are // represented by lower numbers in the pixel array), invert the scale // back to a normal photo-positive scale, so that the client doesn't // have to know these details. if (negativeImage) { // Invert the photo-negative 255..0 scale to a normal, // photo-positive 0..255 scale. This is just a matter of // calculating pos_pixel = 255 - neg_pixel for each pixel. // // There's a shortcut we can use here to make this loop go a // lot faster than the naive approach. Note that 255 decimal // is 1111111 binary. Subtracting any number in (0..255) from // 255 is the same as inverting the bits in the other number. // That is, 255 - X == ~X for all X in 0..255. That's useful // because it means that we can compute (255-X) as a purely // bitwise operation, which means that we can perform it on // blocks of bytes instead of individual bytes. On ARM, we // can perform bitwise operations four bytes at a time via // DWORD instructions. This lets us compute (255-X) for N // bytes using N/4 loop iterations. // // One other small optimization we can apply is to notice that // ~X == X ^ ~0, and that X ^= ~0 can be performed with a // single ARM instruction. So we can make the ARM C++ compiler // translate the loop body into just three instructions: XOR // with immediate data and auto-increment pointer, decrement // the counter, and jump if not zero. That's as fast we could // do it in hand-written assembly. I clocked this loop at // 60us for the 1536-pixel TCD1103 array. // // Note two important constraints: // // - 'pix' must be aligned on a DWORD (4-byte) boundary. // This is REQUIRED, because the XOR in the loop uses a // DWORD memory operand, which will halt the MCU with a // bus error if the pointer isn't DWORD-aligned. // // - 'n' must be a multiple of 4 bytes. This isn't strictly // required, but if it's not observed, the last (N - N/4) // bytes won't be inverted. // // The only sensor that uses a negative image is the TCD1103. // Its buffer is DWORD-aligned because it's allocated via // malloc(), which always does worst-case alignment. Its // buffer is 1546 bytes long, which violates the multiple-of-4 // rule, but inconsequentially, as the last 14 bytes represent // dummy pixels that can be ignored (so it's okay that we'll // miss inverting the last two bytes). // uint32_t *pix32 = reinterpret_cast<uint32_t*>(pix); for (int i = n/4; i != 0; --i) *pix32++ ^= 0xFFFFFFFF; } // send the pixels in report-sized chunks until we get them all int idx = 0; while (idx < n) js.sendPlungerPix(idx, n, pix); } // release the pixel buffer sensor.releasePix(); } protected: // process an image to read the plunger position virtual bool process(const uint8_t *pix, int npix, int &rawPos, ProcessResult &res) = 0; // send extra status headers, following the standard headers (types 0 and 1) virtual void extraStatusHeaders(USBJoystick &js, ProcessResult &res) { } // get the detected orientation virtual int getOrientation() const { return 0; } // underlying hardware sensor interface PlungerSensorImageInterface &sensor; // number of pixels int native_npix; // Does the sensor report a "negative" image? This is like a photo // negative, where brighter pixels are represented by lower numbers in // the pixel array. bool negativeImage; // Auto-exposure time. This is for use by process() in the subclass. // On each frame processing iteration, it can adjust this to optimize // the image quality. uint32_t axcTime; // Extra exposure time. This is for use by the PC side, mostly for // debugging use to allow the PC user to manually adjust the exposure // when inspecting captured frames. uint32_t extraIntTime; }; #endif /* PLUNGER_H */