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
quadSensor.h
00001 // AEDR-8300-1K2 optical encoder / generic quadrature sensor plunger 00002 // implementation 00003 // 00004 // This class implements the Pinscape plunger interface for the 00005 // AEDR-8300-1K2 optical encoder in particular, and quadrature sensors 00006 // in general. The code was written specifically for the AEDR-8300-1K2, 00007 // but it should work with any other quadrature sensor that's electrically 00008 // compatible and that doesn't exceed the maximum interrupt rate we can 00009 // handle on the KL25Z. To be electrically compatible, the device must 00010 // be 3.3V compatible, have logic type outputs (basically square waves 00011 // for the signals), and provide two outputs 90 degrees out of phase. 00012 // The maximum interrupt rate that the KL25Z can handle (with our 00013 // FastInterruptIn class) is about 150 kHz. 00014 // 00015 // A quadrature sensor works by detecting transitions along a bar-coded 00016 // scale. Most position encoders (including the AEDR-8300) are optical, 00017 // but the same principle can be used with other technologies, such as 00018 // magnetic pole strips. Whatever the underlying physical "bar" type, 00019 // the device detects transitions between the bars and the spaces between 00020 // the bars and relays them to the microcontroller via its outputs. A 00021 // quadrature device actually consists of two such sensors, slightly offset 00022 // from each other relative to the direction of motion of the scale, so 00023 // that their bar transitions are 90 degrees out of phase. The phase 00024 // shift in the two signals is what allows the microcontroller to sense 00025 // the direction of motion. The controller figures the current position 00026 // by counting bar transitions (incrementing the count when moving in one 00027 // direction and decrement it in the other direction), so it knows the 00028 // location at any given time as an offset in units of bar widths from the 00029 // starting position. The position reading is always relative, because 00030 // we can only count up or down from the initial point. 00031 // 00032 // In many applications involving quadrature sensors, the relative 00033 // quadrature reading is augmented with a separate sensor for absolute 00034 // positioning. This is usually something simple and low-res, like an 00035 // end-of-stroke switch or a zero-crossing switch. The idea is that you 00036 // use the low-res absolute sensor to tell when you're at a known reference 00037 // point, and then use the high-res quadrature data to get the precise 00038 // location relative to the reference point. To keep things simple, we 00039 // don't use any such supplemental absolute sensor. It's not really 00040 // necessary for a plunger, because a plunger has the special property 00041 // that it always returns to the same point when not being manipulated. 00042 // It's almost as good as having a sensor at the park position, because 00043 // even though we can't know for sure the plunger is there at any given 00044 // time, it's a good bet that that's where it is at startup and any time 00045 // we haven't seen any motion in a while. Note that we could easily add 00046 // support in the software for some kind of absolute sensing if it became 00047 // desirable; the only challenge is the complexity it would add to the 00048 // physical system. 00049 // 00050 // The AEDR-8300 lets us collect some very precise data on the 00051 // instantaneous speed of the plunger thanks to its high resolution and 00052 // real-time position updates. The shortest observed time between pulses 00053 // (so far, with my test rig) is 19us. Pulses are generated at 4 per 00054 // bar, with bars at 75 per inch, yielding 300 pulses per inch. The 19us 00055 // pulse time translates to an instantaneous plunger speed of 0.175 00056 // inches/millisecond, or 4.46 mm/ms, or 4.46 m/s, or 9.97 mph. 00057 // 00058 // The peak interrupt rate of 19us is well within the KL25Z's comfort 00059 // zone, as long as we take reasonable measures to minimize latency. In 00060 // particular, we have to elevate the GPIO port IRQ priority above all 00061 // other hardware interrupts. That's vital because there are some 00062 // relatively long-running interrupt handlers in the system, particularly 00063 // the USB handlers and the microsecond timer. It's also vital to keep 00064 // other GPIO interrupt handlers very fast, since the ports all share 00065 // a priority level and thus can't preempt one another. Fortunately, the 00066 // rest of the Pinscape system make very little use of GPIO interrupts; 00067 // the only current use is in the IR receiver, and that code is designed 00068 // to do minimal work in IRQ context. 00069 // 00070 // We use our custom FastInterruptIn class instead of the original mbed 00071 // InterruptIn. FastInterruptIn gives us a modest speed improvement: it 00072 // has a measured overhead time per interrupt of about 6.5us compared with 00073 // the mbed libary's 8.9us, which gives us a maximum interrupt rate of 00074 // about 159kHz vs mbed's 112kHz. The AEDR-8300's maximum 19us is well 00075 // within both limits, but FastInterruptIn gives us a little more headroom 00076 // for substituting other sensors with higher pulse rates. 00077 // 00078 00079 #ifndef _QUADSENSOR_H_ 00080 #define _QUADSENSOR_H_ 00081 00082 #include "FastInterruptIn.h" 00083 00084 class PlungerSensorQuad: public PlungerSensor 00085 { 00086 public: 00087 // Construct. 00088 // 00089 // 'dpi' is the approximate number of dots per inch of linear travel 00090 // that the sensor can distinguish. This is equivalent to the number 00091 // of pulses it generates per inch. This doesn't have to be exact, 00092 // since the main loop rescales it anyway via calibration. But it's 00093 // helpful to have the approximate figure so that we can scale the 00094 // raw data readings appropriately for the interface datatypes. 00095 // 00096 // For the native scale, we'll assume a 4" range at our dpi rating. 00097 // The actual plunger travel is constrainted to about a 3" range, but 00098 // we want to leave a little extra padding to reduce the chances of 00099 // going out of range in unusual situations. 00100 PlungerSensorQuad(int dpi, PinName pinA, PinName pinB) 00101 : PlungerSensor(dpi*4), 00102 chA(pinA), chB(pinB) 00103 { 00104 // Use 1" as the reference park position 00105 parkPos = dpi; 00106 00107 // start at the park position 00108 pos = parkPos; 00109 00110 // get the initial pin states 00111 st = (chA.read() ? 0x01 : 0x00) 00112 | (chB.read() ? 0x02 : 0x00); 00113 00114 // set up the interrupt handlers 00115 chA.rise(&PlungerSensorQuad::aUp, this); 00116 chA.fall(&PlungerSensorQuad::aDown, this); 00117 chB.rise(&PlungerSensorQuad::bUp, this); 00118 chB.fall(&PlungerSensorQuad::bDown, this); 00119 00120 // start our sample timer with an arbitrary zero point of now 00121 timer.start(); 00122 } 00123 00124 // Auto-zero. Return to the park position. If we're using reverse 00125 // orientation, go to the park position distance from the top end 00126 // of the scale. 00127 virtual void autoZero() 00128 { 00129 pos = reverseOrientation ? nativeScale - parkPos : parkPos; 00130 } 00131 00132 // Begin calibration. We can assume that the plunger is at the 00133 // park position when calibration starts, so perform an explicit 00134 // auto-zeroing operation. 00135 virtual void beginCalibration(Config &) 00136 { 00137 autoZero(); 00138 } 00139 00140 // read the sensor 00141 virtual bool readRaw(PlungerReading &r) 00142 { 00143 // Get the current position in native units 00144 r.pos = pos; 00145 00146 // Set the timestamp on the reading to right now. Our internal 00147 // position counter reflects the position in real time, since it's 00148 // updated in the interrupt handlers for the change signals from 00149 // the sensor. 00150 r.t = timer.read_us(); 00151 00152 // success 00153 return true; 00154 } 00155 00156 virtual void sendStatusReport(class USBJoystick &js, uint8_t flags) 00157 { 00158 // send the common status report 00159 PlungerSensor::sendStatusReport(js, flags); 00160 00161 // send the extra quadrature sensor status report 00162 js.sendPlungerStatusQuadrature((st & 0x01) != 0, (st & 0x02) != 0); 00163 } 00164 00165 // figure the average scan time in microseconds 00166 virtual uint32_t getAvgScanTime() 00167 { 00168 // we're updated by interrupts rather than scanning, so our 00169 // "scan time" is exactly zero 00170 return 0; 00171 } 00172 00173 private: 00174 // interrupt inputs for our channel pins 00175 FastInterruptIn chA, chB; 00176 00177 // current position - this is the cumulate counter for all 00178 // transitions so far 00179 int pos; 00180 00181 // Park position. This is essentially arbitrary, since our readings 00182 // are entirely relative, but for interface purposes we have to keep 00183 // our raw readings positive. We need an initial park position that's 00184 // non-zero so that plunger motion forward of the park position remains 00185 // positive. 00186 int parkPos; 00187 00188 // Channel state on last read. This is a bit vector combining 00189 // the two channel states: 00190 // 0x01 = channel A state 00191 // 0x02 = channel B state 00192 uint8_t st; 00193 00194 // interrupt handlers 00195 static void aUp(void *obj) { 00196 PlungerSensorQuad *self = (PlungerSensorQuad *)obj; 00197 self->transition(self->st | 0x01); 00198 } 00199 static void aDown(void *obj) { 00200 PlungerSensorQuad *self = (PlungerSensorQuad *)obj; 00201 self->transition(self->st & 0x02); 00202 } 00203 static void bUp(void *obj) { 00204 PlungerSensorQuad *self = (PlungerSensorQuad *)obj; 00205 self->transition(self->st | 0x02); 00206 } 00207 static void bDown(void *obj) { 00208 PlungerSensorQuad *self = (PlungerSensorQuad *)obj; 00209 self->transition(self->st & 0x01); 00210 } 00211 00212 // Transition handler. The interrupt handlers call this, so 00213 // it's critical that this run as fast as possible. The observed 00214 // peak interrupt rate is one interrupt per 19us. Fortunately, 00215 // our work here is simple: we just have to count the pulse in 00216 // the appropriate direction according to the state transition 00217 // that the pulse represents. We can do this with a simple table 00218 // lookup. 00219 inline void transition(int stNew) 00220 { 00221 // Transition matrix: dir[n][m] gives the direction of 00222 // motion when we switch from state 'n' to state 'm'. 00223 // The state number is formed by the two-bit number B:A, 00224 // where each bit is 1 if the channel pulse is on and 0 00225 // if the channel pulse is off. E.g., if chA is OFF and 00226 // chB is ON, B:A = 1:0, so the state number is 0b10 = 2. 00227 // Slots with 'NV' are Not Valid: it's impossible to make 00228 // this transition (unless we missed an interrupt). 'NC' 00229 // means No Change; these are the slots on the matrix 00230 // diagonal, which represent the same state on both input 00231 // and output. Like NV transitions, NC transitions should 00232 // never happen, in this case because no interrupt should 00233 // be generated when nothing has changed. 00234 const int NV = 0, NC = 0; 00235 static const int dir[][4] = { 00236 { NC, 1, -1, NV }, 00237 { -1, NC, NV, 1 }, 00238 { 1, NV, NC, -1 }, 00239 { NV, -1, 1, NC } 00240 }; 00241 00242 // increment or decrement the position counter by one notch, 00243 // according to the direction of motion implied by the transition 00244 pos += dir[st][stNew]; 00245 00246 // the new state is now the current state 00247 st = stNew; 00248 } 00249 00250 // timer for input timestamps 00251 Timer timer; 00252 }; 00253 00254 #endif
Generated on Wed Jul 13 2022 03:30:11 by 1.7.2