Pinscape Controller version 1 fork. This is a fork to allow for ongoing bug fixes to the original controller version, from before the major changes for the expansion board project.
Dependencies: FastIO FastPWM SimpleDMA mbed
Fork of Pinscape_Controller by
ccdSensor.h@17:ab3cec0c8bf4, 2015-02-27 (annotated)
- Committer:
- mjr
- Date:
- Fri Feb 27 04:14:04 2015 +0000
- Revision:
- 17:ab3cec0c8bf4
- Child:
- 18:5e890ebd0023
FastIO and FastAnalogIn; better firing event sensing; potentiometer plunger sensor option; new key debouncing; ZB Launch Ball feature
Who changed what in which revision?
User | Revision | Line number | New contents of line |
---|---|---|---|
mjr | 17:ab3cec0c8bf4 | 1 | // CCD plunger sensor |
mjr | 17:ab3cec0c8bf4 | 2 | // |
mjr | 17:ab3cec0c8bf4 | 3 | // This file implements our generic plunger sensor interface for the |
mjr | 17:ab3cec0c8bf4 | 4 | // TAOS TSL1410R CCD array sensor. |
mjr | 17:ab3cec0c8bf4 | 5 | |
mjr | 17:ab3cec0c8bf4 | 6 | |
mjr | 17:ab3cec0c8bf4 | 7 | |
mjr | 17:ab3cec0c8bf4 | 8 | // Number of pixels we read from the CCD on each frame. This can be |
mjr | 17:ab3cec0c8bf4 | 9 | // less than the actual sensor size if desired; if so, we'll read every |
mjr | 17:ab3cec0c8bf4 | 10 | // nth pixel. E.g., with a 1280-pixel physical sensor, if npix is 320, |
mjr | 17:ab3cec0c8bf4 | 11 | // we'll read every 4th pixel. Reading a pixel is fairly time-consuming, |
mjr | 17:ab3cec0c8bf4 | 12 | // because it requires waiting for the pixel's electric charge to |
mjr | 17:ab3cec0c8bf4 | 13 | // stabilize on the CCD output, for the charge to transfer to the KL25Z |
mjr | 17:ab3cec0c8bf4 | 14 | // input, and then for the KL25Z analog voltage sampler to get a stable |
mjr | 17:ab3cec0c8bf4 | 15 | // reading. This all takes about 15us per pixel, which adds up to |
mjr | 17:ab3cec0c8bf4 | 16 | // a relatively long time in such a large array. However, we can skip |
mjr | 17:ab3cec0c8bf4 | 17 | // a pixel without waiting for all of that charge stabilization time, |
mjr | 17:ab3cec0c8bf4 | 18 | // so we can get higher frame rates by only sampling a subset of the |
mjr | 17:ab3cec0c8bf4 | 19 | // pixels. The array is so dense (400dpi) that we can still get |
mjr | 17:ab3cec0c8bf4 | 20 | // excellent resolution by reading a fraction of the total pixels. |
mjr | 17:ab3cec0c8bf4 | 21 | // |
mjr | 17:ab3cec0c8bf4 | 22 | // Empirically, 160 pixels seems to be the point of diminishing returns |
mjr | 17:ab3cec0c8bf4 | 23 | // for resolution - going higher will only improve the apparent smoothness |
mjr | 17:ab3cec0c8bf4 | 24 | // slightly, if at all. 160 pixels gives us 50dpi on the sensor, which |
mjr | 17:ab3cec0c8bf4 | 25 | // is roughly the same as the physical pixel pitch of a typical cabinet |
mjr | 17:ab3cec0c8bf4 | 26 | // playfield monitor. (1080p HDTV displayed 1920x1080 pixels, and a 40" |
mjr | 17:ab3cec0c8bf4 | 27 | // TV is about 35" wide, so the dot pitch is about 55dpi across the width |
mjr | 17:ab3cec0c8bf4 | 28 | // of the TV. If on-screen plunger is displayed at roughly the true |
mjr | 17:ab3cec0c8bf4 | 29 | // physical size, it's about 3" on the screen, or about 165 pixels. So at |
mjr | 17:ab3cec0c8bf4 | 30 | // 160 pixels on the sensor, one pixel on the sensor translates to almost |
mjr | 17:ab3cec0c8bf4 | 31 | // exactly one on-screen pixel on the TV, which makes the animated motion |
mjr | 17:ab3cec0c8bf4 | 32 | // on-screen about as smooth as it can be. Looked at another way, 50dpi |
mjr | 17:ab3cec0c8bf4 | 33 | // means that we're measuring the physical shooter rod position in about |
mjr | 17:ab3cec0c8bf4 | 34 | // half-millimeter increments, which is probably better than I can |
mjr | 17:ab3cec0c8bf4 | 35 | // discern by feel or sight. |
mjr | 17:ab3cec0c8bf4 | 36 | const int npix = 160; |
mjr | 17:ab3cec0c8bf4 | 37 | |
mjr | 17:ab3cec0c8bf4 | 38 | |
mjr | 17:ab3cec0c8bf4 | 39 | class PlungerSensor |
mjr | 17:ab3cec0c8bf4 | 40 | { |
mjr | 17:ab3cec0c8bf4 | 41 | public: |
mjr | 17:ab3cec0c8bf4 | 42 | PlungerSensor() : ccd(CCD_SO_PIN) |
mjr | 17:ab3cec0c8bf4 | 43 | { |
mjr | 17:ab3cec0c8bf4 | 44 | } |
mjr | 17:ab3cec0c8bf4 | 45 | |
mjr | 17:ab3cec0c8bf4 | 46 | // initialize |
mjr | 17:ab3cec0c8bf4 | 47 | void init() |
mjr | 17:ab3cec0c8bf4 | 48 | { |
mjr | 17:ab3cec0c8bf4 | 49 | // flush any random power-on values from the CCD's integration |
mjr | 17:ab3cec0c8bf4 | 50 | // capacitors, and start the first integration cycle |
mjr | 17:ab3cec0c8bf4 | 51 | ccd.clear(); |
mjr | 17:ab3cec0c8bf4 | 52 | } |
mjr | 17:ab3cec0c8bf4 | 53 | |
mjr | 17:ab3cec0c8bf4 | 54 | // Perform a low-res scan of the sensor. |
mjr | 17:ab3cec0c8bf4 | 55 | int lowResScan() |
mjr | 17:ab3cec0c8bf4 | 56 | { |
mjr | 17:ab3cec0c8bf4 | 57 | |
mjr | 17:ab3cec0c8bf4 | 58 | // read the pixels at low resolution |
mjr | 17:ab3cec0c8bf4 | 59 | const int nlpix = 32; |
mjr | 17:ab3cec0c8bf4 | 60 | uint16_t pix[nlpix]; |
mjr | 17:ab3cec0c8bf4 | 61 | ccd.read(pix, nlpix); |
mjr | 17:ab3cec0c8bf4 | 62 | |
mjr | 17:ab3cec0c8bf4 | 63 | // determine which end is brighter |
mjr | 17:ab3cec0c8bf4 | 64 | uint16_t p1 = pix[0]; |
mjr | 17:ab3cec0c8bf4 | 65 | uint16_t p2 = pix[nlpix-1]; |
mjr | 17:ab3cec0c8bf4 | 66 | int si = 1, di = 1; |
mjr | 17:ab3cec0c8bf4 | 67 | if (p1 < p2) |
mjr | 17:ab3cec0c8bf4 | 68 | si = nlpix, di = -1; |
mjr | 17:ab3cec0c8bf4 | 69 | |
mjr | 17:ab3cec0c8bf4 | 70 | // figure the shadow edge threshold - just use the midpoint |
mjr | 17:ab3cec0c8bf4 | 71 | // of the levels at the bright and dark ends |
mjr | 17:ab3cec0c8bf4 | 72 | uint16_t shadow = uint16_t((long(p1) + long(p2))/2); |
mjr | 17:ab3cec0c8bf4 | 73 | |
mjr | 17:ab3cec0c8bf4 | 74 | // find the current tip position |
mjr | 17:ab3cec0c8bf4 | 75 | for (int n = 0 ; n < nlpix ; ++n, si += di) |
mjr | 17:ab3cec0c8bf4 | 76 | { |
mjr | 17:ab3cec0c8bf4 | 77 | // check to see if we found the shadow |
mjr | 17:ab3cec0c8bf4 | 78 | if (pix[si] <= shadow) |
mjr | 17:ab3cec0c8bf4 | 79 | { |
mjr | 17:ab3cec0c8bf4 | 80 | // got it - normalize it to normal 'npix' resolution and |
mjr | 17:ab3cec0c8bf4 | 81 | // return the result |
mjr | 17:ab3cec0c8bf4 | 82 | return n*npix/nlpix; |
mjr | 17:ab3cec0c8bf4 | 83 | } |
mjr | 17:ab3cec0c8bf4 | 84 | } |
mjr | 17:ab3cec0c8bf4 | 85 | |
mjr | 17:ab3cec0c8bf4 | 86 | // didn't find a shadow - assume the whole array is in shadow (so |
mjr | 17:ab3cec0c8bf4 | 87 | // the edge is at the zero pixel point) |
mjr | 17:ab3cec0c8bf4 | 88 | return 0; |
mjr | 17:ab3cec0c8bf4 | 89 | } |
mjr | 17:ab3cec0c8bf4 | 90 | |
mjr | 17:ab3cec0c8bf4 | 91 | // Perform a high-res scan of the sensor. |
mjr | 17:ab3cec0c8bf4 | 92 | bool highResScan(int &pos) |
mjr | 17:ab3cec0c8bf4 | 93 | { |
mjr | 17:ab3cec0c8bf4 | 94 | // read the array |
mjr | 17:ab3cec0c8bf4 | 95 | ccd.read(pix, npix, ccdReadCB, 0, 3); |
mjr | 17:ab3cec0c8bf4 | 96 | |
mjr | 17:ab3cec0c8bf4 | 97 | // get the average brightness at each end of the sensor |
mjr | 17:ab3cec0c8bf4 | 98 | long avg1 = (long(pix[0]) + long(pix[1]) + long(pix[2]) + long(pix[3]) + long(pix[4]))/5; |
mjr | 17:ab3cec0c8bf4 | 99 | long avg2 = (long(pix[npix-1]) + long(pix[npix-2]) + long(pix[npix-3]) + long(pix[npix-4]) + long(pix[npix-5]))/5; |
mjr | 17:ab3cec0c8bf4 | 100 | |
mjr | 17:ab3cec0c8bf4 | 101 | // Work from the bright end to the dark end. VP interprets the |
mjr | 17:ab3cec0c8bf4 | 102 | // Z axis value as the amount the plunger is pulled: zero is the |
mjr | 17:ab3cec0c8bf4 | 103 | // rest position, and the axis maximum is fully pulled. So we |
mjr | 17:ab3cec0c8bf4 | 104 | // essentially want to report how much of the sensor is lit, |
mjr | 17:ab3cec0c8bf4 | 105 | // since this increases as the plunger is pulled back. |
mjr | 17:ab3cec0c8bf4 | 106 | int si = 1, di = 1; |
mjr | 17:ab3cec0c8bf4 | 107 | long avgHi = avg1; |
mjr | 17:ab3cec0c8bf4 | 108 | if (avg1 < avg2) |
mjr | 17:ab3cec0c8bf4 | 109 | si = npix - 2, di = -1, avgHi = avg2; |
mjr | 17:ab3cec0c8bf4 | 110 | |
mjr | 17:ab3cec0c8bf4 | 111 | // Figure the shadow threshold. In practice, the portion of the |
mjr | 17:ab3cec0c8bf4 | 112 | // sensor that's not in shadow has all pixels consistently near |
mjr | 17:ab3cec0c8bf4 | 113 | // saturation; the first drop in brightness is pretty reliably the |
mjr | 17:ab3cec0c8bf4 | 114 | // start of the shadow. So set the threshold level to be closer |
mjr | 17:ab3cec0c8bf4 | 115 | // to the bright end's brightness level, so that we detect the leading |
mjr | 17:ab3cec0c8bf4 | 116 | // edge if the shadow isn't perfectly sharp. Use the point 1/3 of |
mjr | 17:ab3cec0c8bf4 | 117 | // the way down from the high top the low side, so: |
mjr | 17:ab3cec0c8bf4 | 118 | // |
mjr | 17:ab3cec0c8bf4 | 119 | // threshold = lo + (hi - lo)*2/3 |
mjr | 17:ab3cec0c8bf4 | 120 | // = lo + hi*2/3 - lo*2/3 |
mjr | 17:ab3cec0c8bf4 | 121 | // = lo - lo*2/3 + hi*2/3 |
mjr | 17:ab3cec0c8bf4 | 122 | // = lo*1/3 + hi*2/3 |
mjr | 17:ab3cec0c8bf4 | 123 | // = (lo + hi*2)/3 |
mjr | 17:ab3cec0c8bf4 | 124 | // |
mjr | 17:ab3cec0c8bf4 | 125 | // Then multiply the whole thing by 3 to factor out the averaging |
mjr | 17:ab3cec0c8bf4 | 126 | // of each three adjacent pixels that we do in the loop (to save a |
mjr | 17:ab3cec0c8bf4 | 127 | // little time on a mulitply on each loop): |
mjr | 17:ab3cec0c8bf4 | 128 | // |
mjr | 17:ab3cec0c8bf4 | 129 | // threshold' = lo + 2*hi |
mjr | 17:ab3cec0c8bf4 | 130 | // |
mjr | 17:ab3cec0c8bf4 | 131 | // Now, 'lo' is always one of avg1 or avg2, and 'hi' is the other |
mjr | 17:ab3cec0c8bf4 | 132 | // one, so we can rewrite this as hi + avg1 + avg2. We also already |
mjr | 17:ab3cec0c8bf4 | 133 | // pulled out 'hi' as avgHi, so we finally come to the final |
mjr | 17:ab3cec0c8bf4 | 134 | // simplified expression: |
mjr | 17:ab3cec0c8bf4 | 135 | long midpt = avg1 + avg2 + avgHi; |
mjr | 17:ab3cec0c8bf4 | 136 | |
mjr | 17:ab3cec0c8bf4 | 137 | // If we have enough contrast, proceed with the scan. |
mjr | 17:ab3cec0c8bf4 | 138 | // |
mjr | 17:ab3cec0c8bf4 | 139 | // If the bright end and dark end don't differ by enough, skip this |
mjr | 17:ab3cec0c8bf4 | 140 | // reading entirely. Either we have an overexposed or underexposed frame, |
mjr | 17:ab3cec0c8bf4 | 141 | // or the sensor is misaligned and is either fully in or out of shadow |
mjr | 17:ab3cec0c8bf4 | 142 | // (it's supposed to be mounted such that the edge of the shadow always |
mjr | 17:ab3cec0c8bf4 | 143 | // falls within the sensor, for any possible plunger position). |
mjr | 17:ab3cec0c8bf4 | 144 | if (labs(avg1 - avg2) > 0x1000) |
mjr | 17:ab3cec0c8bf4 | 145 | { |
mjr | 17:ab3cec0c8bf4 | 146 | uint16_t *pixp = pix + si; |
mjr | 17:ab3cec0c8bf4 | 147 | for (int n = 1 ; n < npix - 1 ; ++n, pixp += di) |
mjr | 17:ab3cec0c8bf4 | 148 | { |
mjr | 17:ab3cec0c8bf4 | 149 | // if we've crossed the midpoint, report this position |
mjr | 17:ab3cec0c8bf4 | 150 | if (long(pixp[-1]) + long(pixp[0]) + long(pixp[1]) < midpt) |
mjr | 17:ab3cec0c8bf4 | 151 | { |
mjr | 17:ab3cec0c8bf4 | 152 | // note the new position |
mjr | 17:ab3cec0c8bf4 | 153 | pos = n; |
mjr | 17:ab3cec0c8bf4 | 154 | return true; |
mjr | 17:ab3cec0c8bf4 | 155 | } |
mjr | 17:ab3cec0c8bf4 | 156 | } |
mjr | 17:ab3cec0c8bf4 | 157 | } |
mjr | 17:ab3cec0c8bf4 | 158 | |
mjr | 17:ab3cec0c8bf4 | 159 | // we didn't find a shadow - return no reading |
mjr | 17:ab3cec0c8bf4 | 160 | return false; |
mjr | 17:ab3cec0c8bf4 | 161 | } |
mjr | 17:ab3cec0c8bf4 | 162 | |
mjr | 17:ab3cec0c8bf4 | 163 | // send an exposure report to the joystick interface |
mjr | 17:ab3cec0c8bf4 | 164 | void sendExposureReport(USBJoystick &js) |
mjr | 17:ab3cec0c8bf4 | 165 | { |
mjr | 17:ab3cec0c8bf4 | 166 | // send reports for all pixels |
mjr | 17:ab3cec0c8bf4 | 167 | int idx = 0; |
mjr | 17:ab3cec0c8bf4 | 168 | while (idx < npix) |
mjr | 17:ab3cec0c8bf4 | 169 | js.updateExposure(idx, npix, pix); |
mjr | 17:ab3cec0c8bf4 | 170 | |
mjr | 17:ab3cec0c8bf4 | 171 | // The pixel dump requires many USB reports, since each report |
mjr | 17:ab3cec0c8bf4 | 172 | // can only send a few pixel values. An integration cycle has |
mjr | 17:ab3cec0c8bf4 | 173 | // been running all this time, since each read starts a new |
mjr | 17:ab3cec0c8bf4 | 174 | // cycle. Our timing is longer than usual on this round, so |
mjr | 17:ab3cec0c8bf4 | 175 | // the integration won't be comparable to a normal cycle. Throw |
mjr | 17:ab3cec0c8bf4 | 176 | // this one away by doing a read now, and throwing it away - that |
mjr | 17:ab3cec0c8bf4 | 177 | // will get the timing of the *next* cycle roughly back to normal. |
mjr | 17:ab3cec0c8bf4 | 178 | ccd.read(pix, npix); |
mjr | 17:ab3cec0c8bf4 | 179 | } |
mjr | 17:ab3cec0c8bf4 | 180 | |
mjr | 17:ab3cec0c8bf4 | 181 | private: |
mjr | 17:ab3cec0c8bf4 | 182 | // pixel buffer |
mjr | 17:ab3cec0c8bf4 | 183 | uint16_t pix[npix]; |
mjr | 17:ab3cec0c8bf4 | 184 | |
mjr | 17:ab3cec0c8bf4 | 185 | // the low-level interface to the CCD hardware |
mjr | 17:ab3cec0c8bf4 | 186 | TSL1410R<CCD_SI_PIN, CCD_CLOCK_PIN> ccd; |
mjr | 17:ab3cec0c8bf4 | 187 | }; |