Mirror with some correction
Dependencies: mbed FastIO FastPWM USBDevice
Plunger/quadSensor.h@116:7a67265d7c19, 2021-10-01 (annotated)
- Committer:
- arnoz
- Date:
- Fri Oct 01 08:19:46 2021 +0000
- Revision:
- 116:7a67265d7c19
- Parent:
- 108:bd5d4bd4383b
- Correct information regarding your last merge
Who changed what in which revision?
User | Revision | Line number | New contents of line |
---|---|---|---|
mjr | 82:4f6209cb5c33 | 1 | // AEDR-8300-1K2 optical encoder / generic quadrature sensor plunger |
mjr | 82:4f6209cb5c33 | 2 | // implementation |
mjr | 82:4f6209cb5c33 | 3 | // |
mjr | 82:4f6209cb5c33 | 4 | // This class implements the Pinscape plunger interface for the |
mjr | 82:4f6209cb5c33 | 5 | // AEDR-8300-1K2 optical encoder in particular, and quadrature sensors |
mjr | 82:4f6209cb5c33 | 6 | // in general. The code was written specifically for the AEDR-8300-1K2, |
mjr | 82:4f6209cb5c33 | 7 | // but it should work with any other quadrature sensor that's electrically |
mjr | 82:4f6209cb5c33 | 8 | // compatible and that doesn't exceed the maximum interrupt rate we can |
mjr | 82:4f6209cb5c33 | 9 | // handle on the KL25Z. To be electrically compatible, the device must |
mjr | 82:4f6209cb5c33 | 10 | // be 3.3V compatible, have logic type outputs (basically square waves |
mjr | 82:4f6209cb5c33 | 11 | // for the signals), and provide two outputs 90 degrees out of phase. |
mjr | 82:4f6209cb5c33 | 12 | // The maximum interrupt rate that the KL25Z can handle (with our |
mjr | 82:4f6209cb5c33 | 13 | // FastInterruptIn class) is about 150 kHz. |
mjr | 82:4f6209cb5c33 | 14 | // |
mjr | 82:4f6209cb5c33 | 15 | // A quadrature sensor works by detecting transitions along a bar-coded |
mjr | 82:4f6209cb5c33 | 16 | // scale. Most position encoders (including the AEDR-8300) are optical, |
mjr | 82:4f6209cb5c33 | 17 | // but the same principle can be used with other technologies, such as |
mjr | 82:4f6209cb5c33 | 18 | // magnetic pole strips. Whatever the underlying physical "bar" type, |
mjr | 82:4f6209cb5c33 | 19 | // the device detects transitions between the bars and the spaces between |
mjr | 82:4f6209cb5c33 | 20 | // the bars and relays them to the microcontroller via its outputs. A |
mjr | 82:4f6209cb5c33 | 21 | // quadrature device actually consists of two such sensors, slightly offset |
mjr | 82:4f6209cb5c33 | 22 | // from each other relative to the direction of motion of the scale, so |
mjr | 82:4f6209cb5c33 | 23 | // that their bar transitions are 90 degrees out of phase. The phase |
mjr | 82:4f6209cb5c33 | 24 | // shift in the two signals is what allows the microcontroller to sense |
mjr | 82:4f6209cb5c33 | 25 | // the direction of motion. The controller figures the current position |
mjr | 82:4f6209cb5c33 | 26 | // by counting bar transitions (incrementing the count when moving in one |
mjr | 82:4f6209cb5c33 | 27 | // direction and decrement it in the other direction), so it knows the |
mjr | 82:4f6209cb5c33 | 28 | // location at any given time as an offset in units of bar widths from the |
mjr | 82:4f6209cb5c33 | 29 | // starting position. The position reading is always relative, because |
mjr | 82:4f6209cb5c33 | 30 | // we can only count up or down from the initial point. |
mjr | 82:4f6209cb5c33 | 31 | // |
mjr | 82:4f6209cb5c33 | 32 | // In many applications involving quadrature sensors, the relative |
mjr | 82:4f6209cb5c33 | 33 | // quadrature reading is augmented with a separate sensor for absolute |
mjr | 82:4f6209cb5c33 | 34 | // positioning. This is usually something simple and low-res, like an |
mjr | 82:4f6209cb5c33 | 35 | // end-of-stroke switch or a zero-crossing switch. The idea is that you |
mjr | 82:4f6209cb5c33 | 36 | // use the low-res absolute sensor to tell when you're at a known reference |
mjr | 82:4f6209cb5c33 | 37 | // point, and then use the high-res quadrature data to get the precise |
mjr | 82:4f6209cb5c33 | 38 | // location relative to the reference point. To keep things simple, we |
mjr | 82:4f6209cb5c33 | 39 | // don't use any such supplemental absolute sensor. It's not really |
mjr | 82:4f6209cb5c33 | 40 | // necessary for a plunger, because a plunger has the special property |
mjr | 82:4f6209cb5c33 | 41 | // that it always returns to the same point when not being manipulated. |
mjr | 82:4f6209cb5c33 | 42 | // It's almost as good as having a sensor at the park position, because |
mjr | 82:4f6209cb5c33 | 43 | // even though we can't know for sure the plunger is there at any given |
mjr | 82:4f6209cb5c33 | 44 | // time, it's a good bet that that's where it is at startup and any time |
mjr | 82:4f6209cb5c33 | 45 | // we haven't seen any motion in a while. Note that we could easily add |
mjr | 82:4f6209cb5c33 | 46 | // support in the software for some kind of absolute sensing if it became |
mjr | 82:4f6209cb5c33 | 47 | // desirable; the only challenge is the complexity it would add to the |
mjr | 82:4f6209cb5c33 | 48 | // physical system. |
mjr | 82:4f6209cb5c33 | 49 | // |
mjr | 82:4f6209cb5c33 | 50 | // The AEDR-8300 lets us collect some very precise data on the |
mjr | 82:4f6209cb5c33 | 51 | // instantaneous speed of the plunger thanks to its high resolution and |
mjr | 82:4f6209cb5c33 | 52 | // real-time position updates. The shortest observed time between pulses |
mjr | 82:4f6209cb5c33 | 53 | // (so far, with my test rig) is 19us. Pulses are generated at 4 per |
mjr | 82:4f6209cb5c33 | 54 | // bar, with bars at 75 per inch, yielding 300 pulses per inch. The 19us |
mjr | 82:4f6209cb5c33 | 55 | // pulse time translates to an instantaneous plunger speed of 0.175 |
mjr | 82:4f6209cb5c33 | 56 | // inches/millisecond, or 4.46 mm/ms, or 4.46 m/s, or 9.97 mph. |
mjr | 82:4f6209cb5c33 | 57 | // |
mjr | 82:4f6209cb5c33 | 58 | // The peak interrupt rate of 19us is well within the KL25Z's comfort |
mjr | 82:4f6209cb5c33 | 59 | // zone, as long as we take reasonable measures to minimize latency. In |
mjr | 82:4f6209cb5c33 | 60 | // particular, we have to elevate the GPIO port IRQ priority above all |
mjr | 82:4f6209cb5c33 | 61 | // other hardware interrupts. That's vital because there are some |
mjr | 82:4f6209cb5c33 | 62 | // relatively long-running interrupt handlers in the system, particularly |
mjr | 82:4f6209cb5c33 | 63 | // the USB handlers and the microsecond timer. It's also vital to keep |
mjr | 82:4f6209cb5c33 | 64 | // other GPIO interrupt handlers very fast, since the ports all share |
mjr | 82:4f6209cb5c33 | 65 | // a priority level and thus can't preempt one another. Fortunately, the |
mjr | 82:4f6209cb5c33 | 66 | // rest of the Pinscape system make very little use of GPIO interrupts; |
mjr | 82:4f6209cb5c33 | 67 | // the only current use is in the IR receiver, and that code is designed |
mjr | 82:4f6209cb5c33 | 68 | // to do minimal work in IRQ context. |
mjr | 82:4f6209cb5c33 | 69 | // |
mjr | 82:4f6209cb5c33 | 70 | // We use our custom FastInterruptIn class instead of the original mbed |
mjr | 82:4f6209cb5c33 | 71 | // InterruptIn. FastInterruptIn gives us a modest speed improvement: it |
mjr | 82:4f6209cb5c33 | 72 | // has a measured overhead time per interrupt of about 6.5us compared with |
mjr | 82:4f6209cb5c33 | 73 | // the mbed libary's 8.9us, which gives us a maximum interrupt rate of |
mjr | 82:4f6209cb5c33 | 74 | // about 159kHz vs mbed's 112kHz. The AEDR-8300's maximum 19us is well |
mjr | 82:4f6209cb5c33 | 75 | // within both limits, but FastInterruptIn gives us a little more headroom |
mjr | 82:4f6209cb5c33 | 76 | // for substituting other sensors with higher pulse rates. |
mjr | 82:4f6209cb5c33 | 77 | // |
mjr | 82:4f6209cb5c33 | 78 | |
mjr | 82:4f6209cb5c33 | 79 | #ifndef _QUADSENSOR_H_ |
mjr | 82:4f6209cb5c33 | 80 | #define _QUADSENSOR_H_ |
mjr | 82:4f6209cb5c33 | 81 | |
mjr | 82:4f6209cb5c33 | 82 | #include "FastInterruptIn.h" |
mjr | 82:4f6209cb5c33 | 83 | |
mjr | 82:4f6209cb5c33 | 84 | class PlungerSensorQuad: public PlungerSensor |
mjr | 82:4f6209cb5c33 | 85 | { |
mjr | 82:4f6209cb5c33 | 86 | public: |
mjr | 82:4f6209cb5c33 | 87 | // Construct. |
mjr | 82:4f6209cb5c33 | 88 | // |
mjr | 82:4f6209cb5c33 | 89 | // 'dpi' is the approximate number of dots per inch of linear travel |
mjr | 82:4f6209cb5c33 | 90 | // that the sensor can distinguish. This is equivalent to the number |
mjr | 82:4f6209cb5c33 | 91 | // of pulses it generates per inch. This doesn't have to be exact, |
mjr | 82:4f6209cb5c33 | 92 | // since the main loop rescales it anyway via calibration. But it's |
mjr | 82:4f6209cb5c33 | 93 | // helpful to have the approximate figure so that we can scale the |
mjr | 82:4f6209cb5c33 | 94 | // raw data readings appropriately for the interface datatypes. |
mjr | 86:e30a1f60f783 | 95 | // |
mjr | 86:e30a1f60f783 | 96 | // For the native scale, we'll assume a 4" range at our dpi rating. |
mjr | 86:e30a1f60f783 | 97 | // The actual plunger travel is constrainted to about a 3" range, but |
mjr | 86:e30a1f60f783 | 98 | // we want to leave a little extra padding to reduce the chances of |
mjr | 86:e30a1f60f783 | 99 | // going out of range in unusual situations. |
mjr | 82:4f6209cb5c33 | 100 | PlungerSensorQuad(int dpi, PinName pinA, PinName pinB) |
mjr | 86:e30a1f60f783 | 101 | : PlungerSensor(dpi*4), |
mjr | 86:e30a1f60f783 | 102 | chA(pinA), chB(pinB) |
mjr | 82:4f6209cb5c33 | 103 | { |
mjr | 82:4f6209cb5c33 | 104 | // Use 1" as the reference park position |
mjr | 82:4f6209cb5c33 | 105 | parkPos = dpi; |
mjr | 82:4f6209cb5c33 | 106 | |
mjr | 82:4f6209cb5c33 | 107 | // start at the park position |
mjr | 82:4f6209cb5c33 | 108 | pos = parkPos; |
mjr | 82:4f6209cb5c33 | 109 | |
mjr | 82:4f6209cb5c33 | 110 | // get the initial pin states |
mjr | 82:4f6209cb5c33 | 111 | st = (chA.read() ? 0x01 : 0x00) |
mjr | 82:4f6209cb5c33 | 112 | | (chB.read() ? 0x02 : 0x00); |
mjr | 82:4f6209cb5c33 | 113 | |
mjr | 82:4f6209cb5c33 | 114 | // set up the interrupt handlers |
mjr | 82:4f6209cb5c33 | 115 | chA.rise(&PlungerSensorQuad::aUp, this); |
mjr | 82:4f6209cb5c33 | 116 | chA.fall(&PlungerSensorQuad::aDown, this); |
mjr | 82:4f6209cb5c33 | 117 | chB.rise(&PlungerSensorQuad::bUp, this); |
mjr | 82:4f6209cb5c33 | 118 | chB.fall(&PlungerSensorQuad::bDown, this); |
mjr | 82:4f6209cb5c33 | 119 | |
mjr | 82:4f6209cb5c33 | 120 | // start our sample timer with an arbitrary zero point of now |
mjr | 82:4f6209cb5c33 | 121 | timer.start(); |
mjr | 82:4f6209cb5c33 | 122 | } |
mjr | 82:4f6209cb5c33 | 123 | |
mjr | 105:6a25bbfae1e4 | 124 | // Auto-zero. Return to the park position. If we're using reverse |
mjr | 105:6a25bbfae1e4 | 125 | // orientation, go to the park position distance from the top end |
mjr | 105:6a25bbfae1e4 | 126 | // of the scale. |
mjr | 82:4f6209cb5c33 | 127 | virtual void autoZero() |
mjr | 82:4f6209cb5c33 | 128 | { |
mjr | 105:6a25bbfae1e4 | 129 | pos = reverseOrientation ? nativeScale - parkPos : parkPos; |
mjr | 82:4f6209cb5c33 | 130 | } |
mjr | 82:4f6209cb5c33 | 131 | |
mjr | 82:4f6209cb5c33 | 132 | // Begin calibration. We can assume that the plunger is at the |
mjr | 105:6a25bbfae1e4 | 133 | // park position when calibration starts, so perform an explicit |
mjr | 105:6a25bbfae1e4 | 134 | // auto-zeroing operation. |
mjr | 100:1ff35c07217c | 135 | virtual void beginCalibration(Config &) |
mjr | 82:4f6209cb5c33 | 136 | { |
mjr | 105:6a25bbfae1e4 | 137 | autoZero(); |
mjr | 82:4f6209cb5c33 | 138 | } |
mjr | 82:4f6209cb5c33 | 139 | |
mjr | 82:4f6209cb5c33 | 140 | // read the sensor |
mjr | 86:e30a1f60f783 | 141 | virtual bool readRaw(PlungerReading &r) |
mjr | 82:4f6209cb5c33 | 142 | { |
mjr | 86:e30a1f60f783 | 143 | // Get the current position in native units |
mjr | 86:e30a1f60f783 | 144 | r.pos = pos; |
mjr | 82:4f6209cb5c33 | 145 | |
mjr | 82:4f6209cb5c33 | 146 | // Set the timestamp on the reading to right now. Our internal |
mjr | 82:4f6209cb5c33 | 147 | // position counter reflects the position in real time, since it's |
mjr | 82:4f6209cb5c33 | 148 | // updated in the interrupt handlers for the change signals from |
mjr | 82:4f6209cb5c33 | 149 | // the sensor. |
mjr | 82:4f6209cb5c33 | 150 | r.t = timer.read_us(); |
mjr | 82:4f6209cb5c33 | 151 | |
mjr | 82:4f6209cb5c33 | 152 | // success |
mjr | 82:4f6209cb5c33 | 153 | return true; |
mjr | 82:4f6209cb5c33 | 154 | } |
mjr | 82:4f6209cb5c33 | 155 | |
mjr | 108:bd5d4bd4383b | 156 | virtual void sendStatusReport(class USBJoystick &js, uint8_t flags) |
mjr | 108:bd5d4bd4383b | 157 | { |
mjr | 108:bd5d4bd4383b | 158 | // send the common status report |
mjr | 108:bd5d4bd4383b | 159 | PlungerSensor::sendStatusReport(js, flags); |
mjr | 108:bd5d4bd4383b | 160 | |
mjr | 108:bd5d4bd4383b | 161 | // send the extra quadrature sensor status report |
mjr | 108:bd5d4bd4383b | 162 | js.sendPlungerStatusQuadrature((st & 0x01) != 0, (st & 0x02) != 0); |
mjr | 108:bd5d4bd4383b | 163 | } |
mjr | 108:bd5d4bd4383b | 164 | |
mjr | 82:4f6209cb5c33 | 165 | // figure the average scan time in microseconds |
mjr | 82:4f6209cb5c33 | 166 | virtual uint32_t getAvgScanTime() |
mjr | 82:4f6209cb5c33 | 167 | { |
mjr | 82:4f6209cb5c33 | 168 | // we're updated by interrupts rather than scanning, so our |
mjr | 82:4f6209cb5c33 | 169 | // "scan time" is exactly zero |
mjr | 82:4f6209cb5c33 | 170 | return 0; |
mjr | 82:4f6209cb5c33 | 171 | } |
mjr | 101:755f44622abc | 172 | |
mjr | 82:4f6209cb5c33 | 173 | private: |
mjr | 82:4f6209cb5c33 | 174 | // interrupt inputs for our channel pins |
mjr | 82:4f6209cb5c33 | 175 | FastInterruptIn chA, chB; |
mjr | 82:4f6209cb5c33 | 176 | |
mjr | 82:4f6209cb5c33 | 177 | // current position - this is the cumulate counter for all |
mjr | 82:4f6209cb5c33 | 178 | // transitions so far |
mjr | 82:4f6209cb5c33 | 179 | int pos; |
mjr | 82:4f6209cb5c33 | 180 | |
mjr | 82:4f6209cb5c33 | 181 | // Park position. This is essentially arbitrary, since our readings |
mjr | 82:4f6209cb5c33 | 182 | // are entirely relative, but for interface purposes we have to keep |
mjr | 82:4f6209cb5c33 | 183 | // our raw readings positive. We need an initial park position that's |
mjr | 82:4f6209cb5c33 | 184 | // non-zero so that plunger motion forward of the park position remains |
mjr | 82:4f6209cb5c33 | 185 | // positive. |
mjr | 82:4f6209cb5c33 | 186 | int parkPos; |
mjr | 82:4f6209cb5c33 | 187 | |
mjr | 82:4f6209cb5c33 | 188 | // Channel state on last read. This is a bit vector combining |
mjr | 82:4f6209cb5c33 | 189 | // the two channel states: |
mjr | 82:4f6209cb5c33 | 190 | // 0x01 = channel A state |
mjr | 82:4f6209cb5c33 | 191 | // 0x02 = channel B state |
mjr | 82:4f6209cb5c33 | 192 | uint8_t st; |
mjr | 82:4f6209cb5c33 | 193 | |
mjr | 82:4f6209cb5c33 | 194 | // interrupt handlers |
mjr | 82:4f6209cb5c33 | 195 | static void aUp(void *obj) { |
mjr | 82:4f6209cb5c33 | 196 | PlungerSensorQuad *self = (PlungerSensorQuad *)obj; |
mjr | 82:4f6209cb5c33 | 197 | self->transition(self->st | 0x01); |
mjr | 82:4f6209cb5c33 | 198 | } |
mjr | 82:4f6209cb5c33 | 199 | static void aDown(void *obj) { |
mjr | 82:4f6209cb5c33 | 200 | PlungerSensorQuad *self = (PlungerSensorQuad *)obj; |
mjr | 82:4f6209cb5c33 | 201 | self->transition(self->st & 0x02); |
mjr | 82:4f6209cb5c33 | 202 | } |
mjr | 82:4f6209cb5c33 | 203 | static void bUp(void *obj) { |
mjr | 82:4f6209cb5c33 | 204 | PlungerSensorQuad *self = (PlungerSensorQuad *)obj; |
mjr | 82:4f6209cb5c33 | 205 | self->transition(self->st | 0x02); |
mjr | 82:4f6209cb5c33 | 206 | } |
mjr | 82:4f6209cb5c33 | 207 | static void bDown(void *obj) { |
mjr | 82:4f6209cb5c33 | 208 | PlungerSensorQuad *self = (PlungerSensorQuad *)obj; |
mjr | 82:4f6209cb5c33 | 209 | self->transition(self->st & 0x01); |
mjr | 82:4f6209cb5c33 | 210 | } |
mjr | 82:4f6209cb5c33 | 211 | |
mjr | 82:4f6209cb5c33 | 212 | // Transition handler. The interrupt handlers call this, so |
mjr | 82:4f6209cb5c33 | 213 | // it's critical that this run as fast as possible. The observed |
mjr | 82:4f6209cb5c33 | 214 | // peak interrupt rate is one interrupt per 19us. Fortunately, |
mjr | 82:4f6209cb5c33 | 215 | // our work here is simple: we just have to count the pulse in |
mjr | 82:4f6209cb5c33 | 216 | // the appropriate direction according to the state transition |
mjr | 82:4f6209cb5c33 | 217 | // that the pulse represents. We can do this with a simple table |
mjr | 82:4f6209cb5c33 | 218 | // lookup. |
mjr | 82:4f6209cb5c33 | 219 | inline void transition(int stNew) |
mjr | 82:4f6209cb5c33 | 220 | { |
mjr | 108:bd5d4bd4383b | 221 | // Transition matrix: dir[n][m] gives the direction of |
mjr | 108:bd5d4bd4383b | 222 | // motion when we switch from state 'n' to state 'm'. |
mjr | 82:4f6209cb5c33 | 223 | // The state number is formed by the two-bit number B:A, |
mjr | 82:4f6209cb5c33 | 224 | // where each bit is 1 if the channel pulse is on and 0 |
mjr | 82:4f6209cb5c33 | 225 | // if the channel pulse is off. E.g., if chA is OFF and |
mjr | 82:4f6209cb5c33 | 226 | // chB is ON, B:A = 1:0, so the state number is 0b10 = 2. |
mjr | 82:4f6209cb5c33 | 227 | // Slots with 'NV' are Not Valid: it's impossible to make |
mjr | 82:4f6209cb5c33 | 228 | // this transition (unless we missed an interrupt). 'NC' |
mjr | 82:4f6209cb5c33 | 229 | // means No Change; these are the slots on the matrix |
mjr | 82:4f6209cb5c33 | 230 | // diagonal, which represent the same state on both input |
mjr | 82:4f6209cb5c33 | 231 | // and output. Like NV transitions, NC transitions should |
mjr | 82:4f6209cb5c33 | 232 | // never happen, in this case because no interrupt should |
mjr | 82:4f6209cb5c33 | 233 | // be generated when nothing has changed. |
mjr | 82:4f6209cb5c33 | 234 | const int NV = 0, NC = 0; |
mjr | 82:4f6209cb5c33 | 235 | static const int dir[][4] = { |
mjr | 82:4f6209cb5c33 | 236 | { NC, 1, -1, NV }, |
mjr | 82:4f6209cb5c33 | 237 | { -1, NC, NV, 1 }, |
mjr | 82:4f6209cb5c33 | 238 | { 1, NV, NC, -1 }, |
mjr | 82:4f6209cb5c33 | 239 | { NV, -1, 1, NC } |
mjr | 82:4f6209cb5c33 | 240 | }; |
mjr | 82:4f6209cb5c33 | 241 | |
mjr | 82:4f6209cb5c33 | 242 | // increment or decrement the position counter by one notch, |
mjr | 82:4f6209cb5c33 | 243 | // according to the direction of motion implied by the transition |
mjr | 82:4f6209cb5c33 | 244 | pos += dir[st][stNew]; |
mjr | 82:4f6209cb5c33 | 245 | |
mjr | 82:4f6209cb5c33 | 246 | // the new state is now the current state |
mjr | 82:4f6209cb5c33 | 247 | st = stNew; |
mjr | 82:4f6209cb5c33 | 248 | } |
mjr | 82:4f6209cb5c33 | 249 | |
mjr | 82:4f6209cb5c33 | 250 | // timer for input timestamps |
mjr | 82:4f6209cb5c33 | 251 | Timer timer; |
mjr | 82:4f6209cb5c33 | 252 | }; |
mjr | 82:4f6209cb5c33 | 253 | |
mjr | 82:4f6209cb5c33 | 254 | #endif |