Mirror with some correction
Dependencies: mbed FastIO FastPWM USBDevice
Plunger/quadSensor.h
- Committer:
- arnoz
- Date:
- 2021-10-01
- Revision:
- 116:7a67265d7c19
- Parent:
- 108:bd5d4bd4383b
File content as of revision 116:7a67265d7c19:
// AEDR-8300-1K2 optical encoder / generic quadrature sensor plunger // implementation // // This class implements the Pinscape plunger interface for the // AEDR-8300-1K2 optical encoder in particular, and quadrature sensors // in general. The code was written specifically for the AEDR-8300-1K2, // but it should work with any other quadrature sensor that's electrically // compatible and that doesn't exceed the maximum interrupt rate we can // handle on the KL25Z. To be electrically compatible, the device must // be 3.3V compatible, have logic type outputs (basically square waves // for the signals), and provide two outputs 90 degrees out of phase. // The maximum interrupt rate that the KL25Z can handle (with our // FastInterruptIn class) is about 150 kHz. // // A quadrature sensor works by detecting transitions along a bar-coded // scale. Most position encoders (including the AEDR-8300) are optical, // but the same principle can be used with other technologies, such as // magnetic pole strips. Whatever the underlying physical "bar" type, // the device detects transitions between the bars and the spaces between // the bars and relays them to the microcontroller via its outputs. A // quadrature device actually consists of two such sensors, slightly offset // from each other relative to the direction of motion of the scale, so // that their bar transitions are 90 degrees out of phase. The phase // shift in the two signals is what allows the microcontroller to sense // the direction of motion. The controller figures the current position // by counting bar transitions (incrementing the count when moving in one // direction and decrement it in the other direction), so it knows the // location at any given time as an offset in units of bar widths from the // starting position. The position reading is always relative, because // we can only count up or down from the initial point. // // In many applications involving quadrature sensors, the relative // quadrature reading is augmented with a separate sensor for absolute // positioning. This is usually something simple and low-res, like an // end-of-stroke switch or a zero-crossing switch. The idea is that you // use the low-res absolute sensor to tell when you're at a known reference // point, and then use the high-res quadrature data to get the precise // location relative to the reference point. To keep things simple, we // don't use any such supplemental absolute sensor. It's not really // necessary for a plunger, because a plunger has the special property // that it always returns to the same point when not being manipulated. // It's almost as good as having a sensor at the park position, because // even though we can't know for sure the plunger is there at any given // time, it's a good bet that that's where it is at startup and any time // we haven't seen any motion in a while. Note that we could easily add // support in the software for some kind of absolute sensing if it became // desirable; the only challenge is the complexity it would add to the // physical system. // // The AEDR-8300 lets us collect some very precise data on the // instantaneous speed of the plunger thanks to its high resolution and // real-time position updates. The shortest observed time between pulses // (so far, with my test rig) is 19us. Pulses are generated at 4 per // bar, with bars at 75 per inch, yielding 300 pulses per inch. The 19us // pulse time translates to an instantaneous plunger speed of 0.175 // inches/millisecond, or 4.46 mm/ms, or 4.46 m/s, or 9.97 mph. // // The peak interrupt rate of 19us is well within the KL25Z's comfort // zone, as long as we take reasonable measures to minimize latency. In // particular, we have to elevate the GPIO port IRQ priority above all // other hardware interrupts. That's vital because there are some // relatively long-running interrupt handlers in the system, particularly // the USB handlers and the microsecond timer. It's also vital to keep // other GPIO interrupt handlers very fast, since the ports all share // a priority level and thus can't preempt one another. Fortunately, the // rest of the Pinscape system make very little use of GPIO interrupts; // the only current use is in the IR receiver, and that code is designed // to do minimal work in IRQ context. // // We use our custom FastInterruptIn class instead of the original mbed // InterruptIn. FastInterruptIn gives us a modest speed improvement: it // has a measured overhead time per interrupt of about 6.5us compared with // the mbed libary's 8.9us, which gives us a maximum interrupt rate of // about 159kHz vs mbed's 112kHz. The AEDR-8300's maximum 19us is well // within both limits, but FastInterruptIn gives us a little more headroom // for substituting other sensors with higher pulse rates. // #ifndef _QUADSENSOR_H_ #define _QUADSENSOR_H_ #include "FastInterruptIn.h" class PlungerSensorQuad: public PlungerSensor { public: // Construct. // // 'dpi' is the approximate number of dots per inch of linear travel // that the sensor can distinguish. This is equivalent to the number // of pulses it generates per inch. This doesn't have to be exact, // since the main loop rescales it anyway via calibration. But it's // helpful to have the approximate figure so that we can scale the // raw data readings appropriately for the interface datatypes. // // For the native scale, we'll assume a 4" range at our dpi rating. // The actual plunger travel is constrainted to about a 3" range, but // we want to leave a little extra padding to reduce the chances of // going out of range in unusual situations. PlungerSensorQuad(int dpi, PinName pinA, PinName pinB) : PlungerSensor(dpi*4), chA(pinA), chB(pinB) { // Use 1" as the reference park position parkPos = dpi; // start at the park position pos = parkPos; // get the initial pin states st = (chA.read() ? 0x01 : 0x00) | (chB.read() ? 0x02 : 0x00); // set up the interrupt handlers chA.rise(&PlungerSensorQuad::aUp, this); chA.fall(&PlungerSensorQuad::aDown, this); chB.rise(&PlungerSensorQuad::bUp, this); chB.fall(&PlungerSensorQuad::bDown, this); // start our sample timer with an arbitrary zero point of now timer.start(); } // Auto-zero. Return to the park position. If we're using reverse // orientation, go to the park position distance from the top end // of the scale. virtual void autoZero() { pos = reverseOrientation ? nativeScale - parkPos : parkPos; } // Begin calibration. We can assume that the plunger is at the // park position when calibration starts, so perform an explicit // auto-zeroing operation. virtual void beginCalibration(Config &) { autoZero(); } // read the sensor virtual bool readRaw(PlungerReading &r) { // Get the current position in native units r.pos = pos; // Set the timestamp on the reading to right now. Our internal // position counter reflects the position in real time, since it's // updated in the interrupt handlers for the change signals from // the sensor. r.t = timer.read_us(); // success return true; } virtual void sendStatusReport(class USBJoystick &js, uint8_t flags) { // send the common status report PlungerSensor::sendStatusReport(js, flags); // send the extra quadrature sensor status report js.sendPlungerStatusQuadrature((st & 0x01) != 0, (st & 0x02) != 0); } // figure the average scan time in microseconds virtual uint32_t getAvgScanTime() { // we're updated by interrupts rather than scanning, so our // "scan time" is exactly zero return 0; } private: // interrupt inputs for our channel pins FastInterruptIn chA, chB; // current position - this is the cumulate counter for all // transitions so far int pos; // Park position. This is essentially arbitrary, since our readings // are entirely relative, but for interface purposes we have to keep // our raw readings positive. We need an initial park position that's // non-zero so that plunger motion forward of the park position remains // positive. int parkPos; // Channel state on last read. This is a bit vector combining // the two channel states: // 0x01 = channel A state // 0x02 = channel B state uint8_t st; // interrupt handlers static void aUp(void *obj) { PlungerSensorQuad *self = (PlungerSensorQuad *)obj; self->transition(self->st | 0x01); } static void aDown(void *obj) { PlungerSensorQuad *self = (PlungerSensorQuad *)obj; self->transition(self->st & 0x02); } static void bUp(void *obj) { PlungerSensorQuad *self = (PlungerSensorQuad *)obj; self->transition(self->st | 0x02); } static void bDown(void *obj) { PlungerSensorQuad *self = (PlungerSensorQuad *)obj; self->transition(self->st & 0x01); } // Transition handler. The interrupt handlers call this, so // it's critical that this run as fast as possible. The observed // peak interrupt rate is one interrupt per 19us. Fortunately, // our work here is simple: we just have to count the pulse in // the appropriate direction according to the state transition // that the pulse represents. We can do this with a simple table // lookup. inline void transition(int stNew) { // Transition matrix: dir[n][m] gives the direction of // motion when we switch from state 'n' to state 'm'. // The state number is formed by the two-bit number B:A, // where each bit is 1 if the channel pulse is on and 0 // if the channel pulse is off. E.g., if chA is OFF and // chB is ON, B:A = 1:0, so the state number is 0b10 = 2. // Slots with 'NV' are Not Valid: it's impossible to make // this transition (unless we missed an interrupt). 'NC' // means No Change; these are the slots on the matrix // diagonal, which represent the same state on both input // and output. Like NV transitions, NC transitions should // never happen, in this case because no interrupt should // be generated when nothing has changed. const int NV = 0, NC = 0; static const int dir[][4] = { { NC, 1, -1, NV }, { -1, NC, NV, 1 }, { 1, NV, NC, -1 }, { NV, -1, 1, NC } }; // increment or decrement the position counter by one notch, // according to the direction of motion implied by the transition pos += dir[st][stNew]; // the new state is now the current state st = stNew; } // timer for input timestamps Timer timer; }; #endif