work in progress
Dependencies: FastAnalogIn FastIO USBDevice mbed FastPWM SimpleDMA
Fork of Pinscape_Controller by
main.cpp@5:a70c0bce770d, 2014-07-27 (annotated)
- Committer:
- mjr
- Date:
- Sun Jul 27 18:24:51 2014 +0000
- Revision:
- 5:a70c0bce770d
- Parent:
- 4:02c7cd7b2183
- Child:
- 6:cc35eb643e8f
Somewhat working with ball-model damping. About to change to cabinet model.
Who changed what in which revision?
User | Revision | Line number | New contents of line |
---|---|---|---|
mjr | 5:a70c0bce770d | 1 | /* Copyright 2014 M J Roberts, MIT License |
mjr | 5:a70c0bce770d | 2 | * |
mjr | 5:a70c0bce770d | 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software |
mjr | 5:a70c0bce770d | 4 | * and associated documentation files (the "Software"), to deal in the Software without |
mjr | 5:a70c0bce770d | 5 | * restriction, including without limitation the rights to use, copy, modify, merge, publish, |
mjr | 5:a70c0bce770d | 6 | * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the |
mjr | 5:a70c0bce770d | 7 | * Software is furnished to do so, subject to the following conditions: |
mjr | 5:a70c0bce770d | 8 | * |
mjr | 5:a70c0bce770d | 9 | * The above copyright notice and this permission notice shall be included in all copies or |
mjr | 5:a70c0bce770d | 10 | * substantial portions of the Software. |
mjr | 5:a70c0bce770d | 11 | * |
mjr | 5:a70c0bce770d | 12 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING |
mjr | 5:a70c0bce770d | 13 | * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
mjr | 5:a70c0bce770d | 14 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, |
mjr | 5:a70c0bce770d | 15 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
mjr | 5:a70c0bce770d | 16 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
mjr | 5:a70c0bce770d | 17 | */ |
mjr | 5:a70c0bce770d | 18 | |
mjr | 5:a70c0bce770d | 19 | // |
mjr | 5:a70c0bce770d | 20 | // Pinscape Controller |
mjr | 5:a70c0bce770d | 21 | // |
mjr | 5:a70c0bce770d | 22 | // "Pinscape" is the name of my custom-built virtual pinball cabinet. I wrote this |
mjr | 5:a70c0bce770d | 23 | // software to perform a number of tasks that I needed for my cabinet. It runs on a |
mjr | 5:a70c0bce770d | 24 | // Freescale KL25Z microcontroller, which is a small and inexpensive device that |
mjr | 5:a70c0bce770d | 25 | // attaches to the host PC via USB and can interface with numerous types of external |
mjr | 5:a70c0bce770d | 26 | // hardware. |
mjr | 5:a70c0bce770d | 27 | // |
mjr | 5:a70c0bce770d | 28 | // I designed the software and hardware in this project especially for Pinscape, but |
mjr | 5:a70c0bce770d | 29 | // it uses standard interfaces in Windows and Visual Pinball, so it should be |
mjr | 5:a70c0bce770d | 30 | // readily usable in anyone else's VP-based cabinet. I've tried to document the |
mjr | 5:a70c0bce770d | 31 | // hardware in enough detail for anyone else to duplicate the entire project, and |
mjr | 5:a70c0bce770d | 32 | // the full software is open source. |
mjr | 5:a70c0bce770d | 33 | // |
mjr | 5:a70c0bce770d | 34 | // The controller provides the following functions. It should be possible to use |
mjr | 5:a70c0bce770d | 35 | // any subet of the features without using all of them. External hardware for any |
mjr | 5:a70c0bce770d | 36 | // particular function can simply be omitted if that feature isn't needed. |
mjr | 5:a70c0bce770d | 37 | // |
mjr | 5:a70c0bce770d | 38 | // - Nudge sensing via the KL25Z's on-board accelerometer. Nudge accelerations are |
mjr | 5:a70c0bce770d | 39 | // processed into a physics model of a rolling ball, and changes to the ball's |
mjr | 5:a70c0bce770d | 40 | // motion are sent to the host computer via the joystick interface. This is designed |
mjr | 5:a70c0bce770d | 41 | // especially to work with Visuall Pinball's nudge handling to produce realistic |
mjr | 5:a70c0bce770d | 42 | // on-screen results in VP. By doing some physics modeling right on the device, |
mjr | 5:a70c0bce770d | 43 | // rather than sending raw accelerometer data to VP, we can produce better results |
mjr | 5:a70c0bce770d | 44 | // using our awareness of the real physical parameters of a pinball cabinet. |
mjr | 5:a70c0bce770d | 45 | // VP's nudge handling has to be more generic, so it can't make the same sorts |
mjr | 5:a70c0bce770d | 46 | // of assumptions that we can about the dynamics of a real cabinet. |
mjr | 5:a70c0bce770d | 47 | // |
mjr | 5:a70c0bce770d | 48 | // The nudge data reports are compatible with the built-in Windows USB joystick |
mjr | 5:a70c0bce770d | 49 | // drivers and with VP's own joystick input scheme, so the nudge sensing is almost |
mjr | 5:a70c0bce770d | 50 | // plug-and-play. There are no Windiows drivers to install, and the only VP work |
mjr | 5:a70c0bce770d | 51 | // needed is to customize a few global preference settings. |
mjr | 5:a70c0bce770d | 52 | // |
mjr | 5:a70c0bce770d | 53 | // - Plunger position sensing via an attached TAOS TSL 1410R CCD linear array sensor. |
mjr | 5:a70c0bce770d | 54 | // The sensor must be wired to a particular set of I/O ports on the KL25Z, and must |
mjr | 5:a70c0bce770d | 55 | // be positioned adjacent to the plunger with proper lighting. The physical and |
mjr | 5:a70c0bce770d | 56 | // electronic installation details are desribed in the project documentation. We read |
mjr | 5:a70c0bce770d | 57 | // the CCD to determine how far back the plunger is pulled, and report this to Visual |
mjr | 5:a70c0bce770d | 58 | // Pinball via the joystick interface. As with the nudge data, this is all nearly |
mjr | 5:a70c0bce770d | 59 | // plug-and-play, in that it works with the default Windows USB drivers and works |
mjr | 5:a70c0bce770d | 60 | // with the existing VP handling for analog plunger input. A few VP settings are |
mjr | 5:a70c0bce770d | 61 | // needed to tell VP to allow the plunger. |
mjr | 5:a70c0bce770d | 62 | // |
mjr | 5:a70c0bce770d | 63 | // Unfortunately, analog plungers are not well supported by individual tables, |
mjr | 5:a70c0bce770d | 64 | // so some work is required for each table to give it proper support. I've tried |
mjr | 5:a70c0bce770d | 65 | // to reduce this to a recipe and document it in the project documentation. |
mjr | 5:a70c0bce770d | 66 | // |
mjr | 5:a70c0bce770d | 67 | // - In addition to the CCD sensor, a button should be attached (also described in |
mjr | 5:a70c0bce770d | 68 | // the project documentation) to activate calibration mode for the plunger. When |
mjr | 5:a70c0bce770d | 69 | // calibration mode is activated, the software reads the plunger position for about |
mjr | 5:a70c0bce770d | 70 | // 10 seconds when to note the limits of travel, and uses these limits to ensure |
mjr | 5:a70c0bce770d | 71 | // accurate reports to VP that properly report the actual position of the physical |
mjr | 5:a70c0bce770d | 72 | // plunger. The calibration is stored in non-volatile memory on the KL25Z, so it's |
mjr | 5:a70c0bce770d | 73 | // only necessary to calibrate once - the calibration will survive power cycling |
mjr | 5:a70c0bce770d | 74 | // and reboots of the PC. It's only necessary to recalibrate if the CCD sensor or |
mjr | 5:a70c0bce770d | 75 | // the plunger are removed and reinstalled, since the relative alignment of the |
mjr | 5:a70c0bce770d | 76 | // parts could cahnge slightly when reinstalling. |
mjr | 5:a70c0bce770d | 77 | // |
mjr | 5:a70c0bce770d | 78 | // - LedWiz emulation. The KL25Z can appear to the PC as an LedWiz device, and will |
mjr | 5:a70c0bce770d | 79 | // accept and process LedWiz commands from the host. The software can turn digital |
mjr | 5:a70c0bce770d | 80 | // output ports on and off, and can set varying PWM intensitiy levels on a subset |
mjr | 5:a70c0bce770d | 81 | // of ports. (The KL25Z can only provide 6 PWM ports. Intensity level settings on |
mjr | 5:a70c0bce770d | 82 | // other ports is ignored, so non-PWM ports can only be used for simple on/off |
mjr | 5:a70c0bce770d | 83 | // devices such as contactors and solenoids.) The KL25Z can only supply 4mA on its |
mjr | 5:a70c0bce770d | 84 | // output ports, so external hardware is required to take advantage of the LedWiz |
mjr | 5:a70c0bce770d | 85 | // emulation. Many different hardware designs are possible, but there's a simple |
mjr | 5:a70c0bce770d | 86 | // reference design in the documentation that uses a Darlington array IC to |
mjr | 5:a70c0bce770d | 87 | // increase the output from each port to 500mA (the same level as the LedWiz), |
mjr | 5:a70c0bce770d | 88 | // plus an extended design that adds an optocoupler and MOSFET to provide very |
mjr | 5:a70c0bce770d | 89 | // high power handling, up to about 45A or 150W, with voltages up to 100V. |
mjr | 5:a70c0bce770d | 90 | // That will handle just about any DC device directly (wtihout relays or other |
mjr | 5:a70c0bce770d | 91 | // amplifiers), and switches fast enough to support PWM devices. |
mjr | 5:a70c0bce770d | 92 | // |
mjr | 5:a70c0bce770d | 93 | // The device can report any desired LedWiz unit number to the host, which makes |
mjr | 5:a70c0bce770d | 94 | // it possible to use the LedWiz emulation on a machine that also has one or more |
mjr | 5:a70c0bce770d | 95 | // actual LedWiz devices intalled. The LedWiz design allows for up to 16 units |
mjr | 5:a70c0bce770d | 96 | // to be installed in one machine - each one is invidually addressable by its |
mjr | 5:a70c0bce770d | 97 | // distinct unit number. |
mjr | 5:a70c0bce770d | 98 | // |
mjr | 5:a70c0bce770d | 99 | // The LedWiz emulation features are of course optional. There's no need to |
mjr | 5:a70c0bce770d | 100 | // build any of the external port hardware (or attach anything to the output |
mjr | 5:a70c0bce770d | 101 | // ports at all) if the LedWiz features aren't needed. Most people won't have |
mjr | 5:a70c0bce770d | 102 | // any use for the LedWiz features. I built them mostly as a learning exercise, |
mjr | 5:a70c0bce770d | 103 | // but with a slight practical need for a handful of extra ports (I'm using the |
mjr | 5:a70c0bce770d | 104 | // cutting-edge 10-contactor setup, so my real LedWiz is full!). |
mjr | 5:a70c0bce770d | 105 | |
mjr | 5:a70c0bce770d | 106 | |
mjr | 0:5acbbe3f4cf4 | 107 | #include "mbed.h" |
mjr | 0:5acbbe3f4cf4 | 108 | #include "USBJoystick.h" |
mjr | 0:5acbbe3f4cf4 | 109 | #include "MMA8451Q.h" |
mjr | 1:d913e0afb2ac | 110 | #include "tsl1410r.h" |
mjr | 1:d913e0afb2ac | 111 | #include "FreescaleIAP.h" |
mjr | 2:c174f9ee414a | 112 | #include "crc32.h" |
mjr | 2:c174f9ee414a | 113 | |
mjr | 5:a70c0bce770d | 114 | |
mjr | 5:a70c0bce770d | 115 | // --------------------------------------------------------------------------- |
mjr | 5:a70c0bce770d | 116 | // |
mjr | 5:a70c0bce770d | 117 | // Configuration details |
mjr | 5:a70c0bce770d | 118 | // |
mjr | 2:c174f9ee414a | 119 | |
mjr | 5:a70c0bce770d | 120 | // Our USB device vendor ID, product ID, and version. |
mjr | 5:a70c0bce770d | 121 | // We use the vendor ID for the LedWiz, so that the PC-side software can |
mjr | 5:a70c0bce770d | 122 | // identify us as capable of performing LedWiz commands. The LedWiz uses |
mjr | 5:a70c0bce770d | 123 | // a product ID value from 0xF0 to 0xFF; the last four bits identify the |
mjr | 5:a70c0bce770d | 124 | // unit number (e.g., product ID 0xF7 means unit #7). This allows multiple |
mjr | 5:a70c0bce770d | 125 | // LedWiz units to be installed in a single PC; the software on the PC side |
mjr | 5:a70c0bce770d | 126 | // uses the unit number to route commands to the devices attached to each |
mjr | 5:a70c0bce770d | 127 | // unit. On the real LedWiz, the unit number must be set in the firmware |
mjr | 5:a70c0bce770d | 128 | // at the factory; it's not configurable by the end user. Most LedWiz's |
mjr | 5:a70c0bce770d | 129 | // ship with the unit number set to 0, but the vendor will set different |
mjr | 5:a70c0bce770d | 130 | // unit numbers if requested at the time of purchase. So if you have a |
mjr | 5:a70c0bce770d | 131 | // single LedWiz already installed in your cabinet, and you didn't ask for |
mjr | 5:a70c0bce770d | 132 | // a non-default unit number, your existing LedWiz will be unit 0. |
mjr | 5:a70c0bce770d | 133 | // |
mjr | 5:a70c0bce770d | 134 | // We use unit #7 by default. There doesn't seem to be a requirement that |
mjr | 5:a70c0bce770d | 135 | // unit numbers be contiguous (DirectOutput Framework and other software |
mjr | 5:a70c0bce770d | 136 | // seem happy to have units 0 and 7 installed, without 1-6 existing). |
mjr | 5:a70c0bce770d | 137 | // Marking this unit as #7 should work for almost everybody out of the box; |
mjr | 5:a70c0bce770d | 138 | // the most common case seems to be to have a single LedWiz installed, and |
mjr | 5:a70c0bce770d | 139 | // it's probably extremely rare to more than two. |
mjr | 5:a70c0bce770d | 140 | const uint16_t USB_VENDOR_ID = 0xFAFA; |
mjr | 5:a70c0bce770d | 141 | const uint16_t USB_PRODUCT_ID = 0x00F7; |
mjr | 5:a70c0bce770d | 142 | const uint16_t USB_VERSION_NO = 0x0004; |
mjr | 0:5acbbe3f4cf4 | 143 | |
mjr | 4:02c7cd7b2183 | 144 | // On-board RGB LED elements - we use these for diagnostic displays. |
mjr | 4:02c7cd7b2183 | 145 | DigitalOut ledR(LED1), ledG(LED2), ledB(LED3); |
mjr | 0:5acbbe3f4cf4 | 146 | |
mjr | 1:d913e0afb2ac | 147 | // calibration button - switch input and LED output |
mjr | 1:d913e0afb2ac | 148 | DigitalIn calBtn(PTE29); |
mjr | 1:d913e0afb2ac | 149 | DigitalOut calBtnLed(PTE23); |
mjr | 0:5acbbe3f4cf4 | 150 | |
mjr | 5:a70c0bce770d | 151 | // I2C address of the accelerometer (this is a constant of the KL25Z) |
mjr | 5:a70c0bce770d | 152 | const int MMA8451_I2C_ADDRESS = (0x1d<<1); |
mjr | 5:a70c0bce770d | 153 | |
mjr | 5:a70c0bce770d | 154 | // SCL and SDA pins for the accelerometer (constant for the KL25Z) |
mjr | 5:a70c0bce770d | 155 | #define MMA8451_SCL_PIN PTE25 |
mjr | 5:a70c0bce770d | 156 | #define MMA8451_SDA_PIN PTE24 |
mjr | 5:a70c0bce770d | 157 | |
mjr | 5:a70c0bce770d | 158 | // Digital in pin to use for the accelerometer interrupt. For the KL25Z, |
mjr | 5:a70c0bce770d | 159 | // this can be either PTA14 or PTA15, since those are the pins physically |
mjr | 5:a70c0bce770d | 160 | // wired on this board to the MMA8451 interrupt controller. |
mjr | 5:a70c0bce770d | 161 | #define MMA8451_INT_PIN PTA15 |
mjr | 5:a70c0bce770d | 162 | |
mjr | 5:a70c0bce770d | 163 | |
mjr | 5:a70c0bce770d | 164 | // --------------------------------------------------------------------------- |
mjr | 5:a70c0bce770d | 165 | // |
mjr | 5:a70c0bce770d | 166 | // LedWiz emulation |
mjr | 5:a70c0bce770d | 167 | // |
mjr | 5:a70c0bce770d | 168 | |
mjr | 0:5acbbe3f4cf4 | 169 | static int pbaIdx = 0; |
mjr | 0:5acbbe3f4cf4 | 170 | |
mjr | 0:5acbbe3f4cf4 | 171 | // on/off state for each LedWiz output |
mjr | 1:d913e0afb2ac | 172 | static uint8_t wizOn[32]; |
mjr | 0:5acbbe3f4cf4 | 173 | |
mjr | 0:5acbbe3f4cf4 | 174 | // profile (brightness/blink) state for each LedWiz output |
mjr | 1:d913e0afb2ac | 175 | static uint8_t wizVal[32] = { |
mjr | 0:5acbbe3f4cf4 | 176 | 0, 0, 0, 0, 0, 0, 0, 0, |
mjr | 0:5acbbe3f4cf4 | 177 | 0, 0, 0, 0, 0, 0, 0, 0, |
mjr | 0:5acbbe3f4cf4 | 178 | 0, 0, 0, 0, 0, 0, 0, 0, |
mjr | 0:5acbbe3f4cf4 | 179 | 0, 0, 0, 0, 0, 0, 0, 0 |
mjr | 0:5acbbe3f4cf4 | 180 | }; |
mjr | 0:5acbbe3f4cf4 | 181 | |
mjr | 1:d913e0afb2ac | 182 | static float wizState(int idx) |
mjr | 0:5acbbe3f4cf4 | 183 | { |
mjr | 1:d913e0afb2ac | 184 | if (wizOn[idx]) { |
mjr | 0:5acbbe3f4cf4 | 185 | // on - map profile brightness state to PWM level |
mjr | 1:d913e0afb2ac | 186 | uint8_t val = wizVal[idx]; |
mjr | 0:5acbbe3f4cf4 | 187 | if (val >= 1 && val <= 48) |
mjr | 0:5acbbe3f4cf4 | 188 | return 1.0 - val/48.0; |
mjr | 0:5acbbe3f4cf4 | 189 | else if (val >= 129 && val <= 132) |
mjr | 0:5acbbe3f4cf4 | 190 | return 0.0; |
mjr | 0:5acbbe3f4cf4 | 191 | else |
mjr | 0:5acbbe3f4cf4 | 192 | return 1.0; |
mjr | 0:5acbbe3f4cf4 | 193 | } |
mjr | 0:5acbbe3f4cf4 | 194 | else { |
mjr | 0:5acbbe3f4cf4 | 195 | // off |
mjr | 0:5acbbe3f4cf4 | 196 | return 1.0; |
mjr | 0:5acbbe3f4cf4 | 197 | } |
mjr | 0:5acbbe3f4cf4 | 198 | } |
mjr | 0:5acbbe3f4cf4 | 199 | |
mjr | 1:d913e0afb2ac | 200 | static void updateWizOuts() |
mjr | 1:d913e0afb2ac | 201 | { |
mjr | 4:02c7cd7b2183 | 202 | ledR = wizState(0); |
mjr | 4:02c7cd7b2183 | 203 | ledG = wizState(1); |
mjr | 4:02c7cd7b2183 | 204 | ledB = wizState(2); |
mjr | 1:d913e0afb2ac | 205 | } |
mjr | 1:d913e0afb2ac | 206 | |
mjr | 5:a70c0bce770d | 207 | // --------------------------------------------------------------------------- |
mjr | 5:a70c0bce770d | 208 | // |
mjr | 5:a70c0bce770d | 209 | // Non-volatile memory (NVM) |
mjr | 5:a70c0bce770d | 210 | // |
mjr | 0:5acbbe3f4cf4 | 211 | |
mjr | 5:a70c0bce770d | 212 | // Structure defining our NVM storage layout. We store a small |
mjr | 2:c174f9ee414a | 213 | // amount of persistent data in flash memory to retain calibration |
mjr | 5:a70c0bce770d | 214 | // data when powered off. |
mjr | 2:c174f9ee414a | 215 | struct NVM |
mjr | 2:c174f9ee414a | 216 | { |
mjr | 2:c174f9ee414a | 217 | // checksum - we use this to determine if the flash record |
mjr | 2:c174f9ee414a | 218 | // has been initialized |
mjr | 2:c174f9ee414a | 219 | uint32_t checksum; |
mjr | 2:c174f9ee414a | 220 | |
mjr | 2:c174f9ee414a | 221 | // signature value |
mjr | 2:c174f9ee414a | 222 | static const uint32_t SIGNATURE = 0x4D4A522A; |
mjr | 2:c174f9ee414a | 223 | static const uint16_t VERSION = 0x0002; |
mjr | 2:c174f9ee414a | 224 | |
mjr | 2:c174f9ee414a | 225 | // stored data (excluding the checksum) |
mjr | 2:c174f9ee414a | 226 | struct |
mjr | 2:c174f9ee414a | 227 | { |
mjr | 2:c174f9ee414a | 228 | // signature and version - further verification that we have valid |
mjr | 2:c174f9ee414a | 229 | // initialized data |
mjr | 2:c174f9ee414a | 230 | uint32_t sig; |
mjr | 2:c174f9ee414a | 231 | uint16_t vsn; |
mjr | 2:c174f9ee414a | 232 | |
mjr | 2:c174f9ee414a | 233 | // direction - 0 means unknown, 1 means bright end is pixel 0, 2 means reversed |
mjr | 2:c174f9ee414a | 234 | uint8_t dir; |
mjr | 2:c174f9ee414a | 235 | |
mjr | 2:c174f9ee414a | 236 | // plunger calibration min and max |
mjr | 2:c174f9ee414a | 237 | int plungerMin; |
mjr | 2:c174f9ee414a | 238 | int plungerMax; |
mjr | 2:c174f9ee414a | 239 | } d; |
mjr | 2:c174f9ee414a | 240 | }; |
mjr | 2:c174f9ee414a | 241 | |
mjr | 5:a70c0bce770d | 242 | |
mjr | 5:a70c0bce770d | 243 | // --------------------------------------------------------------------------- |
mjr | 5:a70c0bce770d | 244 | // |
mjr | 5:a70c0bce770d | 245 | // Customization joystick subbclass |
mjr | 5:a70c0bce770d | 246 | // |
mjr | 5:a70c0bce770d | 247 | |
mjr | 5:a70c0bce770d | 248 | class MyUSBJoystick: public USBJoystick |
mjr | 5:a70c0bce770d | 249 | { |
mjr | 5:a70c0bce770d | 250 | public: |
mjr | 5:a70c0bce770d | 251 | MyUSBJoystick(uint16_t vendor_id, uint16_t product_id, uint16_t product_release) |
mjr | 5:a70c0bce770d | 252 | : USBJoystick(vendor_id, product_id, product_release, true) |
mjr | 5:a70c0bce770d | 253 | { |
mjr | 5:a70c0bce770d | 254 | suspended_ = false; |
mjr | 5:a70c0bce770d | 255 | } |
mjr | 5:a70c0bce770d | 256 | |
mjr | 5:a70c0bce770d | 257 | // are we connected? |
mjr | 5:a70c0bce770d | 258 | int isConnected() { return configured(); } |
mjr | 5:a70c0bce770d | 259 | |
mjr | 5:a70c0bce770d | 260 | // Are we in suspend mode? |
mjr | 5:a70c0bce770d | 261 | int isSuspended() const { return suspended_; } |
mjr | 5:a70c0bce770d | 262 | |
mjr | 5:a70c0bce770d | 263 | protected: |
mjr | 5:a70c0bce770d | 264 | virtual void suspendStateChanged(unsigned int suspended) |
mjr | 5:a70c0bce770d | 265 | { suspended_ = suspended; } |
mjr | 5:a70c0bce770d | 266 | |
mjr | 5:a70c0bce770d | 267 | // are we suspended? |
mjr | 5:a70c0bce770d | 268 | int suspended_; |
mjr | 5:a70c0bce770d | 269 | }; |
mjr | 5:a70c0bce770d | 270 | |
mjr | 5:a70c0bce770d | 271 | // --------------------------------------------------------------------------- |
mjr | 5:a70c0bce770d | 272 | // |
mjr | 5:a70c0bce770d | 273 | // Accelerometer (MMA8451Q) |
mjr | 5:a70c0bce770d | 274 | // |
mjr | 5:a70c0bce770d | 275 | |
mjr | 5:a70c0bce770d | 276 | // The MMA8451Q is the KL25Z's on-board 3-axis accelerometer. |
mjr | 5:a70c0bce770d | 277 | // |
mjr | 5:a70c0bce770d | 278 | // This is a custom wrapper for the library code to interface to the |
mjr | 5:a70c0bce770d | 279 | // MMA8451Q. This class encapsulates an interrupt handler and some |
mjr | 5:a70c0bce770d | 280 | // special data processing to produce more realistic results in |
mjr | 5:a70c0bce770d | 281 | // Visual Pinball. |
mjr | 5:a70c0bce770d | 282 | // |
mjr | 5:a70c0bce770d | 283 | // We install an interrupt handler on the accelerometer "data ready" |
mjr | 5:a70c0bce770d | 284 | // interrupt in order to ensure that we fetch each sample immediately |
mjr | 5:a70c0bce770d | 285 | // when it becomes available. Since our main program loop is busy |
mjr | 5:a70c0bce770d | 286 | // reading the CCD virtually all of the time, it wouldn't be practical |
mjr | 5:a70c0bce770d | 287 | // to keep up with the accelerometer data stream by polling. |
mjr | 5:a70c0bce770d | 288 | // |
mjr | 5:a70c0bce770d | 289 | // Visual Pinball is nominally designed to accept raw accelerometer |
mjr | 5:a70c0bce770d | 290 | // data as nudge input, but in practice, this doesn't produce |
mjr | 5:a70c0bce770d | 291 | // very realistic results. VP simply applies accelerations from a |
mjr | 5:a70c0bce770d | 292 | // physical accelerometer directly to its modeled ball(s), but the |
mjr | 5:a70c0bce770d | 293 | // data stream coming from a real accelerometer isn't as clean as |
mjr | 5:a70c0bce770d | 294 | // an idealized physics simulation. The problem seems to be that the |
mjr | 5:a70c0bce770d | 295 | // accelerometer samples capture instantaneous accelerations, not |
mjr | 5:a70c0bce770d | 296 | // integrated acceleration over time. In other words, adding samples |
mjr | 5:a70c0bce770d | 297 | // over time doesn't accurately reflect the actual net acceleration |
mjr | 5:a70c0bce770d | 298 | // experienced. The longer the sampling period, the greater the |
mjr | 5:a70c0bce770d | 299 | // divergence between the sum of a series of samples and the actual |
mjr | 5:a70c0bce770d | 300 | // net acceleration. The effect in VP is to leave the ball with |
mjr | 5:a70c0bce770d | 301 | // an unrealistically high residual velocity over the course of a |
mjr | 5:a70c0bce770d | 302 | // nudge event. |
mjr | 5:a70c0bce770d | 303 | // |
mjr | 5:a70c0bce770d | 304 | // This is where our custom data processing comes into play. Rather |
mjr | 5:a70c0bce770d | 305 | // than sending raw accelerometer samples, we apply the samples to |
mjr | 5:a70c0bce770d | 306 | // our own virtual model ball. What we send VP is the accelerations |
mjr | 5:a70c0bce770d | 307 | // experienced by the ball in our model, not the actual accelerations |
mjr | 5:a70c0bce770d | 308 | // we read from the MMA8451Q. Now, that might seem like an unnecessary |
mjr | 5:a70c0bce770d | 309 | // middleman, because VP is just going to apply the accelerations to |
mjr | 5:a70c0bce770d | 310 | // its own model ball. But it's a useful middleman: what we can do |
mjr | 5:a70c0bce770d | 311 | // in our model that VP can't do in its model is take into account |
mjr | 5:a70c0bce770d | 312 | // our special knowledge of the physical cabinet configuration. VP |
mjr | 5:a70c0bce770d | 313 | // has to work generically with any sort of nudge input device, but |
mjr | 5:a70c0bce770d | 314 | // we can make assumptions about what kind of physical environment |
mjr | 5:a70c0bce770d | 315 | // we're operating in. |
mjr | 5:a70c0bce770d | 316 | // |
mjr | 5:a70c0bce770d | 317 | // The key assumption we make about our physical environment is that |
mjr | 5:a70c0bce770d | 318 | // accelerations from nudges should net out to zero over intervals on |
mjr | 5:a70c0bce770d | 319 | // the order of a couple of seconds. Nudging a pinball cabinet makes |
mjr | 5:a70c0bce770d | 320 | // the cabinet accelerate briefly in the nudge direction, then rebound, |
mjr | 5:a70c0bce770d | 321 | // then re-rebound, and so on until the swaying motion damps out and |
mjr | 5:a70c0bce770d | 322 | // the table returns roughly to rest. The table doesn't actually go |
mjr | 5:a70c0bce770d | 323 | // anywhere in these transactions, so the net acceleration experienced |
mjr | 5:a70c0bce770d | 324 | // is zero by the time the motion has damped out. The damping time |
mjr | 5:a70c0bce770d | 325 | // depends on the degree of force of the nudge, but is a second or |
mjr | 5:a70c0bce770d | 326 | // two in most cases. |
mjr | 5:a70c0bce770d | 327 | // |
mjr | 5:a70c0bce770d | 328 | // We can't just assume that all motion and/or acceleration must stop |
mjr | 5:a70c0bce770d | 329 | // in a second or two, though. For one thing, the player can nudge |
mjr | 5:a70c0bce770d | 330 | // the table repeatedly for long periods. (Doing this too aggressivly |
mjr | 5:a70c0bce770d | 331 | // will trigger a tilt, so there are limits, but a skillful player |
mjr | 5:a70c0bce770d | 332 | // can keep nudging a table almost continuously without tilting it.) |
mjr | 5:a70c0bce770d | 333 | // For another, a player could actually pick up one end of the table |
mjr | 5:a70c0bce770d | 334 | // for an extended period, applying a continuous acceleration the |
mjr | 5:a70c0bce770d | 335 | // whole time. |
mjr | 5:a70c0bce770d | 336 | // |
mjr | 5:a70c0bce770d | 337 | // The strategy we use to cope with these possibilities is to model a |
mjr | 5:a70c0bce770d | 338 | // ball, rather like VP does, but with damping that scales with the |
mjr | 5:a70c0bce770d | 339 | // current speed. We'll choose a damping function that will bring |
mjr | 5:a70c0bce770d | 340 | // the ball to rest from any reasonable speed within a second or two |
mjr | 5:a70c0bce770d | 341 | // if there are no ongoing accelerations. The damping function must |
mjr | 5:a70c0bce770d | 342 | // also be weak enough that new accelerations dominate - that is, |
mjr | 5:a70c0bce770d | 343 | // the damping function must not be so strong that it cancels out |
mjr | 5:a70c0bce770d | 344 | // ongoing physical acceleration input, such as when the player |
mjr | 5:a70c0bce770d | 345 | // lifts one end of the table and holds it up for a while. |
mjr | 5:a70c0bce770d | 346 | // |
mjr | 5:a70c0bce770d | 347 | // What we report to VP is the acceleration experienced by our model |
mjr | 5:a70c0bce770d | 348 | // ball between samples. Our model ball starts at rest, and our damping |
mjr | 5:a70c0bce770d | 349 | // function ensures that when it's in motion, it will return to rest in |
mjr | 5:a70c0bce770d | 350 | // a short time in the absence of further physical accelerations. The |
mjr | 5:a70c0bce770d | 351 | // sum or our reports to VP from a rest state to a subsequent rest state |
mjr | 5:a70c0bce770d | 352 | // will thus necessarily equal exactly zero. This will ensure that we |
mjr | 5:a70c0bce770d | 353 | // don't leave VP's model ball with any residual velocity after an |
mjr | 5:a70c0bce770d | 354 | // isolated nudge. |
mjr | 5:a70c0bce770d | 355 | // |
mjr | 5:a70c0bce770d | 356 | // We do one more bit of data processing: automatic calibration. When |
mjr | 5:a70c0bce770d | 357 | // we observe the accelerometer input staying constant (within a noise |
mjr | 5:a70c0bce770d | 358 | // window) for a few seconds continously, we'll assume that the cabinet |
mjr | 5:a70c0bce770d | 359 | // is at rest. It's safe to assume that the accelerometer isn't |
mjr | 5:a70c0bce770d | 360 | // installed in such a way that it's perfectly level, so at the |
mjr | 5:a70c0bce770d | 361 | // cabinet's neutral rest position, we can expect to read non-zero |
mjr | 5:a70c0bce770d | 362 | // accelerations on the x and y axes from the component along that |
mjr | 5:a70c0bce770d | 363 | // axis of the Earth's gravity. By watching for constant acceleration |
mjr | 5:a70c0bce770d | 364 | // values over time, we can infer the reseting position of the device |
mjr | 5:a70c0bce770d | 365 | // and take that as our zero point. By doing this continuously, we |
mjr | 5:a70c0bce770d | 366 | // don't have to assume that the machine is perfectly motionless when |
mjr | 5:a70c0bce770d | 367 | // initially powered on - we'll organically find the zero point as soon |
mjr | 5:a70c0bce770d | 368 | // as the machine is undisturbed for a few moments. We'll also deal |
mjr | 5:a70c0bce770d | 369 | // gracefully with situations where the machine is jolted so much in |
mjr | 5:a70c0bce770d | 370 | // the course of play that its position is changed slightly. The result |
mjr | 5:a70c0bce770d | 371 | // should be to make the zeroing process reliable and completely |
mjr | 5:a70c0bce770d | 372 | // transparent to the user. |
mjr | 5:a70c0bce770d | 373 | // |
mjr | 5:a70c0bce770d | 374 | |
mjr | 5:a70c0bce770d | 375 | // point structure |
mjr | 5:a70c0bce770d | 376 | struct FPoint |
mjr | 5:a70c0bce770d | 377 | { |
mjr | 5:a70c0bce770d | 378 | float x, y; |
mjr | 5:a70c0bce770d | 379 | |
mjr | 5:a70c0bce770d | 380 | FPoint() { } |
mjr | 5:a70c0bce770d | 381 | FPoint(float x, float y) { this->x = x; this->y = y; } |
mjr | 5:a70c0bce770d | 382 | |
mjr | 5:a70c0bce770d | 383 | void set(float x, float y) { this->x = x; this->y = y; } |
mjr | 5:a70c0bce770d | 384 | void zero() { this->x = this->y = 0; } |
mjr | 5:a70c0bce770d | 385 | |
mjr | 5:a70c0bce770d | 386 | FPoint &operator=(FPoint &pt) { this->x = pt.x; this->y = pt.y; return *this; } |
mjr | 5:a70c0bce770d | 387 | FPoint &operator-=(FPoint &pt) { this->x -= pt.x; this->y -= pt.y; return *this; } |
mjr | 5:a70c0bce770d | 388 | FPoint &operator+=(FPoint &pt) { this->x += pt.x; this->y += pt.y; return *this; } |
mjr | 5:a70c0bce770d | 389 | FPoint &operator*=(float f) { this->x *= f; this->y *= f; return *this; } |
mjr | 5:a70c0bce770d | 390 | FPoint &operator/=(float f) { this->x /= f; this->y /= f; return *this; } |
mjr | 5:a70c0bce770d | 391 | float magnitude() const { return sqrt(x*x + y*y); } |
mjr | 5:a70c0bce770d | 392 | |
mjr | 5:a70c0bce770d | 393 | float distance(FPoint &b) |
mjr | 5:a70c0bce770d | 394 | { |
mjr | 5:a70c0bce770d | 395 | float dx = x - b.x; |
mjr | 5:a70c0bce770d | 396 | float dy = y - b.y; |
mjr | 5:a70c0bce770d | 397 | return sqrt(dx*dx + dy*dy); |
mjr | 5:a70c0bce770d | 398 | } |
mjr | 5:a70c0bce770d | 399 | }; |
mjr | 5:a70c0bce770d | 400 | |
mjr | 5:a70c0bce770d | 401 | |
mjr | 5:a70c0bce770d | 402 | // accelerometer wrapper class |
mjr | 3:3514575d4f86 | 403 | class Accel |
mjr | 3:3514575d4f86 | 404 | { |
mjr | 3:3514575d4f86 | 405 | public: |
mjr | 3:3514575d4f86 | 406 | Accel(PinName sda, PinName scl, int i2cAddr, PinName irqPin) |
mjr | 3:3514575d4f86 | 407 | : mma_(sda, scl, i2cAddr), intIn_(irqPin) |
mjr | 3:3514575d4f86 | 408 | { |
mjr | 5:a70c0bce770d | 409 | // remember the interrupt pin assignment |
mjr | 5:a70c0bce770d | 410 | irqPin_ = irqPin; |
mjr | 5:a70c0bce770d | 411 | |
mjr | 5:a70c0bce770d | 412 | // reset and initialize |
mjr | 5:a70c0bce770d | 413 | reset(); |
mjr | 5:a70c0bce770d | 414 | } |
mjr | 5:a70c0bce770d | 415 | |
mjr | 5:a70c0bce770d | 416 | void reset() |
mjr | 5:a70c0bce770d | 417 | { |
mjr | 5:a70c0bce770d | 418 | // assume initially that the device is perfectly level |
mjr | 5:a70c0bce770d | 419 | center_.zero(); |
mjr | 5:a70c0bce770d | 420 | tCenter_.start(); |
mjr | 5:a70c0bce770d | 421 | iAccPrv_ = nAccPrv_ = 0; |
mjr | 5:a70c0bce770d | 422 | |
mjr | 5:a70c0bce770d | 423 | // reset and initialize the MMA8451Q |
mjr | 5:a70c0bce770d | 424 | mma_.init(); |
mjr | 5:a70c0bce770d | 425 | |
mjr | 3:3514575d4f86 | 426 | // set the initial ball velocity to zero |
mjr | 5:a70c0bce770d | 427 | v_.zero(); |
mjr | 3:3514575d4f86 | 428 | |
mjr | 3:3514575d4f86 | 429 | // set the initial raw acceleration reading to zero |
mjr | 5:a70c0bce770d | 430 | araw_.zero(); |
mjr | 5:a70c0bce770d | 431 | vsum_.zero(); |
mjr | 3:3514575d4f86 | 432 | |
mjr | 3:3514575d4f86 | 433 | // enable the interrupt |
mjr | 5:a70c0bce770d | 434 | mma_.setInterruptMode(irqPin_ == PTA14 ? 1 : 2); |
mjr | 3:3514575d4f86 | 435 | |
mjr | 3:3514575d4f86 | 436 | // set up the interrupt handler |
mjr | 3:3514575d4f86 | 437 | intIn_.rise(this, &Accel::isr); |
mjr | 3:3514575d4f86 | 438 | |
mjr | 3:3514575d4f86 | 439 | // read the current registers to clear the data ready flag |
mjr | 3:3514575d4f86 | 440 | float z; |
mjr | 5:a70c0bce770d | 441 | mma_.getAccXYZ(araw_.x, araw_.y, z); |
mjr | 3:3514575d4f86 | 442 | |
mjr | 3:3514575d4f86 | 443 | // start our timers |
mjr | 3:3514575d4f86 | 444 | tGet_.start(); |
mjr | 3:3514575d4f86 | 445 | tInt_.start(); |
mjr | 5:a70c0bce770d | 446 | tRest_.start(); |
mjr | 3:3514575d4f86 | 447 | } |
mjr | 3:3514575d4f86 | 448 | |
mjr | 3:3514575d4f86 | 449 | void get(float &x, float &y, float &rx, float &ry) |
mjr | 3:3514575d4f86 | 450 | { |
mjr | 3:3514575d4f86 | 451 | // disable interrupts while manipulating the shared data |
mjr | 3:3514575d4f86 | 452 | __disable_irq(); |
mjr | 3:3514575d4f86 | 453 | |
mjr | 3:3514575d4f86 | 454 | // read the shared data and store locally for calculations |
mjr | 5:a70c0bce770d | 455 | FPoint vsum = vsum_, araw = araw_; |
mjr | 5:a70c0bce770d | 456 | |
mjr | 5:a70c0bce770d | 457 | // reset the velocity sum |
mjr | 5:a70c0bce770d | 458 | vsum_.zero(); |
mjr | 3:3514575d4f86 | 459 | |
mjr | 3:3514575d4f86 | 460 | // get the time since the last get() sample |
mjr | 3:3514575d4f86 | 461 | float dt = tGet_.read_us()/1.0e6; |
mjr | 3:3514575d4f86 | 462 | tGet_.reset(); |
mjr | 3:3514575d4f86 | 463 | |
mjr | 3:3514575d4f86 | 464 | // done manipulating the shared data |
mjr | 3:3514575d4f86 | 465 | __enable_irq(); |
mjr | 3:3514575d4f86 | 466 | |
mjr | 5:a70c0bce770d | 467 | // check for auto-centering every so often |
mjr | 5:a70c0bce770d | 468 | if (tCenter_.read_ms() > 1000) |
mjr | 5:a70c0bce770d | 469 | { |
mjr | 5:a70c0bce770d | 470 | // add the latest raw sample to the history list |
mjr | 5:a70c0bce770d | 471 | accPrv_[iAccPrv_] = araw_; |
mjr | 5:a70c0bce770d | 472 | |
mjr | 5:a70c0bce770d | 473 | // commit the history entry |
mjr | 5:a70c0bce770d | 474 | iAccPrv_ = (iAccPrv_ + 1) % maxAccPrv; |
mjr | 5:a70c0bce770d | 475 | |
mjr | 5:a70c0bce770d | 476 | // if we have a full complement, check for stability |
mjr | 5:a70c0bce770d | 477 | if (nAccPrv_ >= maxAccPrv) |
mjr | 5:a70c0bce770d | 478 | { |
mjr | 5:a70c0bce770d | 479 | // check if we've been stable for all recent samples |
mjr | 5:a70c0bce770d | 480 | static const float accTol = .005; |
mjr | 5:a70c0bce770d | 481 | if (accPrv_[0].distance(accPrv_[1]) < accTol |
mjr | 5:a70c0bce770d | 482 | && accPrv_[0].distance(accPrv_[2]) < accTol |
mjr | 5:a70c0bce770d | 483 | && accPrv_[0].distance(accPrv_[3]) < accTol |
mjr | 5:a70c0bce770d | 484 | && accPrv_[0].distance(accPrv_[4]) < accTol) |
mjr | 5:a70c0bce770d | 485 | { |
mjr | 5:a70c0bce770d | 486 | // figure the new center as the average of these samples |
mjr | 5:a70c0bce770d | 487 | center_.set( |
mjr | 5:a70c0bce770d | 488 | (accPrv_[0].x + accPrv_[1].x + accPrv_[2].x + accPrv_[3].x + accPrv_[4].x)/5.0, |
mjr | 5:a70c0bce770d | 489 | (accPrv_[0].y + accPrv_[1].y + accPrv_[2].y + accPrv_[3].y + accPrv_[4].y)/5.0); |
mjr | 5:a70c0bce770d | 490 | } |
mjr | 5:a70c0bce770d | 491 | } |
mjr | 5:a70c0bce770d | 492 | else |
mjr | 5:a70c0bce770d | 493 | { |
mjr | 5:a70c0bce770d | 494 | // not enough samples yet; just up the count |
mjr | 5:a70c0bce770d | 495 | ++nAccPrv_; |
mjr | 5:a70c0bce770d | 496 | } |
mjr | 5:a70c0bce770d | 497 | |
mjr | 5:a70c0bce770d | 498 | // reset the timer |
mjr | 5:a70c0bce770d | 499 | tCenter_.reset(); |
mjr | 5:a70c0bce770d | 500 | } |
mjr | 5:a70c0bce770d | 501 | |
mjr | 5:a70c0bce770d | 502 | // Calculate the velocity vector for the model ball. Start |
mjr | 5:a70c0bce770d | 503 | // with the accumulated velocity from the accelerations since |
mjr | 5:a70c0bce770d | 504 | // the last reading. |
mjr | 5:a70c0bce770d | 505 | FPoint dv = vsum; |
mjr | 5:a70c0bce770d | 506 | |
mjr | 5:a70c0bce770d | 507 | // remember the previous velocity of the model ball |
mjr | 5:a70c0bce770d | 508 | FPoint vprv = v_; |
mjr | 5:a70c0bce770d | 509 | |
mjr | 5:a70c0bce770d | 510 | // If we have residual motion, check for damping. |
mjr | 5:a70c0bce770d | 511 | // |
mjr | 5:a70c0bce770d | 512 | // The dmaping we model here isn't friction - we leave that sort of |
mjr | 5:a70c0bce770d | 513 | // detail to the pinball simulator on the PC. Instead, our form of |
mjr | 5:a70c0bce770d | 514 | // damping is just an attempt to compensate for measurement errors |
mjr | 5:a70c0bce770d | 515 | // from the accelerometer. During a nudge event, we should see a |
mjr | 5:a70c0bce770d | 516 | // series of accelerations back and forth, as the table sways in |
mjr | 5:a70c0bce770d | 517 | // response to the push, rebounds from the sway, rebounds from the |
mjr | 5:a70c0bce770d | 518 | // rebound, etc. We know that in reality, the table itself doesn't |
mjr | 5:a70c0bce770d | 519 | // actually go anywhere - it just sways, and when the swaying stops, |
mjr | 5:a70c0bce770d | 520 | // it ends up where it started. If we use the accelerometer input |
mjr | 5:a70c0bce770d | 521 | // to do dead reckoning on the location of the table, we know that |
mjr | 5:a70c0bce770d | 522 | // it has to end up where it started. This means that the series of |
mjr | 5:a70c0bce770d | 523 | // position changes over the course of the event should cancel out - |
mjr | 5:a70c0bce770d | 524 | // the displacements should add up to zero. |
mjr | 3:3514575d4f86 | 525 | |
mjr | 5:a70c0bce770d | 526 | to model friction and other forces |
mjr | 5:a70c0bce770d | 527 | // on the ball. Instead, the damping we apply is to compensate for |
mjr | 5:a70c0bce770d | 528 | // measurement errors in the accelerometer. During a nudge event, |
mjr | 5:a70c0bce770d | 529 | // a real pinball cabinet typically ends up at the same place it |
mjr | 5:a70c0bce770d | 530 | // started - it sways in response to the nudge, but the swaying |
mjr | 5:a70c0bce770d | 531 | // quickly damps out and leaves the table unmoved. You don't |
mjr | 5:a70c0bce770d | 532 | // typically apply enough force to actually pick up the cabinet |
mjr | 5:a70c0bce770d | 533 | // and move it, or slide it across the floor - and doing so would |
mjr | 5:a70c0bce770d | 534 | // trigger a tilt, in which case the ball goes out of play and we |
mjr | 5:a70c0bce770d | 535 | // don't really have to worry about how realistically it behaves |
mjr | 5:a70c0bce770d | 536 | // in response to the acceleration. |
mjr | 5:a70c0bce770d | 537 | if (vprv.magnitude() != 0) |
mjr | 5:a70c0bce770d | 538 | { |
mjr | 5:a70c0bce770d | 539 | // The model ball is moving. If the current motion has been |
mjr | 5:a70c0bce770d | 540 | // going on for long enough, apply damping. We wait a short |
mjr | 5:a70c0bce770d | 541 | // time before we apply damping to allow small continuous |
mjr | 5:a70c0bce770d | 542 | // accelerations (from tiling the table) to get the ball |
mjr | 5:a70c0bce770d | 543 | // rolling. |
mjr | 5:a70c0bce770d | 544 | if (tRest_.read_ms() > 100) |
mjr | 5:a70c0bce770d | 545 | { |
mjr | 5:a70c0bce770d | 546 | } |
mjr | 5:a70c0bce770d | 547 | } |
mjr | 5:a70c0bce770d | 548 | else |
mjr | 5:a70c0bce770d | 549 | { |
mjr | 5:a70c0bce770d | 550 | // the model ball is at rest; if the instantaneous acceleration |
mjr | 5:a70c0bce770d | 551 | // is also near zero, reset the rest timer |
mjr | 5:a70c0bce770d | 552 | if (dv.magnitude() < 0.025) |
mjr | 5:a70c0bce770d | 553 | tRest_.reset(); |
mjr | 5:a70c0bce770d | 554 | } |
mjr | 5:a70c0bce770d | 555 | |
mjr | 5:a70c0bce770d | 556 | // If the current velocity change is near zero, damp the ball's |
mjr | 5:a70c0bce770d | 557 | // velocity. The idea is that the total series of accelerations |
mjr | 5:a70c0bce770d | 558 | // from a nudge should net to zero, since a nudge doesn't |
mjr | 5:a70c0bce770d | 559 | // actually move the table anywhere. |
mjr | 5:a70c0bce770d | 560 | // |
mjr | 5:a70c0bce770d | 561 | // Ideally, this wouldn't be necessary, because the raw |
mjr | 5:a70c0bce770d | 562 | // accelerometer readings should organically add up to zero over |
mjr | 5:a70c0bce770d | 563 | // the course of a nudge. In practice, the accelerometer isn't |
mjr | 5:a70c0bce770d | 564 | // perfect; it can only sample so fast, so it can't capture every |
mjr | 5:a70c0bce770d | 565 | // instantaneous change; and each reading has some small measurement |
mjr | 5:a70c0bce770d | 566 | // error, which becomes significant when many readings are added |
mjr | 5:a70c0bce770d | 567 | // together. The damping is an attempt to reconcile the imperfect |
mjr | 5:a70c0bce770d | 568 | // measurements with what how expect the real physical system to |
mjr | 5:a70c0bce770d | 569 | // behave - we know what the outcome of an event should be, so we |
mjr | 5:a70c0bce770d | 570 | // adjust our measurements to get the expected outcome. |
mjr | 5:a70c0bce770d | 571 | // |
mjr | 5:a70c0bce770d | 572 | // If the ball's velocity is large at this point, assume that this |
mjr | 5:a70c0bce770d | 573 | // wasn't a nudge event at all, but a sustained inclination - as |
mjr | 5:a70c0bce770d | 574 | // though the player picked up one end of the table and held it |
mjr | 5:a70c0bce770d | 575 | // up for a while, to accelerate the ball down the sloped table. |
mjr | 5:a70c0bce770d | 576 | // In this case just reset the velocity to zero without doing |
mjr | 5:a70c0bce770d | 577 | // any damping, so that we don't pass through any deceleration |
mjr | 5:a70c0bce770d | 578 | // to the pinball simulation. In this case we want to leave it |
mjr | 5:a70c0bce770d | 579 | // to the pinball simulation to do its own modeling of friction |
mjr | 5:a70c0bce770d | 580 | // or bouncing to decelerate the ball. Our correction is only |
mjr | 5:a70c0bce770d | 581 | // realistic for brief events that naturally net out to neutral |
mjr | 5:a70c0bce770d | 582 | // accelerations. |
mjr | 5:a70c0bce770d | 583 | if (dv.magnitude() < .025) |
mjr | 5:a70c0bce770d | 584 | { |
mjr | 5:a70c0bce770d | 585 | // check the ball's speed |
mjr | 5:a70c0bce770d | 586 | if (v_.magnitude() < .25) |
mjr | 5:a70c0bce770d | 587 | { |
mjr | 5:a70c0bce770d | 588 | // apply the damping |
mjr | 5:a70c0bce770d | 589 | FPoint damp(damping(v_.x), damping(v_.y)); |
mjr | 5:a70c0bce770d | 590 | dv -= damp; |
mjr | 5:a70c0bce770d | 591 | ledB = 0; |
mjr | 5:a70c0bce770d | 592 | } |
mjr | 5:a70c0bce770d | 593 | else |
mjr | 5:a70c0bce770d | 594 | { |
mjr | 5:a70c0bce770d | 595 | // the ball is going too fast - simply reset it |
mjr | 5:a70c0bce770d | 596 | v_ = dv; |
mjr | 5:a70c0bce770d | 597 | vprv = dv; |
mjr | 5:a70c0bce770d | 598 | ledB = 1; |
mjr | 5:a70c0bce770d | 599 | } |
mjr | 5:a70c0bce770d | 600 | } |
mjr | 5:a70c0bce770d | 601 | else |
mjr | 5:a70c0bce770d | 602 | ledB = 1; |
mjr | 5:a70c0bce770d | 603 | |
mjr | 5:a70c0bce770d | 604 | // apply the velocity change for this interval |
mjr | 5:a70c0bce770d | 605 | v_ += dv; |
mjr | 5:a70c0bce770d | 606 | |
mjr | 5:a70c0bce770d | 607 | // return the acceleration since the last update (change in velocity |
mjr | 5:a70c0bce770d | 608 | // over time) in x,y |
mjr | 5:a70c0bce770d | 609 | dv /= dt; |
mjr | 5:a70c0bce770d | 610 | x = (v_.x - vprv.x) / dt; |
mjr | 5:a70c0bce770d | 611 | y = (v_.y - vprv.y) / dt; |
mjr | 5:a70c0bce770d | 612 | |
mjr | 5:a70c0bce770d | 613 | // report the calibrated instantaneous acceleration in rx,ry |
mjr | 5:a70c0bce770d | 614 | rx = araw.x - center_.x; |
mjr | 5:a70c0bce770d | 615 | ry = araw.y - center_.y; |
mjr | 3:3514575d4f86 | 616 | } |
mjr | 3:3514575d4f86 | 617 | |
mjr | 3:3514575d4f86 | 618 | private: |
mjr | 5:a70c0bce770d | 619 | // velocity damping function |
mjr | 5:a70c0bce770d | 620 | float damping(float v) |
mjr | 5:a70c0bce770d | 621 | { |
mjr | 5:a70c0bce770d | 622 | // scale to -2048..2048 range, and get the absolute value |
mjr | 5:a70c0bce770d | 623 | float a = fabs(v*2048.0); |
mjr | 5:a70c0bce770d | 624 | |
mjr | 5:a70c0bce770d | 625 | // damp out small velocities immediately |
mjr | 5:a70c0bce770d | 626 | if (a < 20) |
mjr | 5:a70c0bce770d | 627 | return v; |
mjr | 5:a70c0bce770d | 628 | |
mjr | 5:a70c0bce770d | 629 | // calculate the cube root of the scaled value |
mjr | 5:a70c0bce770d | 630 | float r = exp(log(a)/3.0); |
mjr | 5:a70c0bce770d | 631 | |
mjr | 5:a70c0bce770d | 632 | // rescale |
mjr | 5:a70c0bce770d | 633 | r /= 2048.0; |
mjr | 5:a70c0bce770d | 634 | |
mjr | 5:a70c0bce770d | 635 | // apply the sign and return the result |
mjr | 5:a70c0bce770d | 636 | return (v < 0 ? -r : r); |
mjr | 5:a70c0bce770d | 637 | } |
mjr | 5:a70c0bce770d | 638 | |
mjr | 3:3514575d4f86 | 639 | // interrupt handler |
mjr | 3:3514575d4f86 | 640 | void isr() |
mjr | 3:3514575d4f86 | 641 | { |
mjr | 3:3514575d4f86 | 642 | // Read the axes. Note that we have to read all three axes |
mjr | 3:3514575d4f86 | 643 | // (even though we only really use x and y) in order to clear |
mjr | 3:3514575d4f86 | 644 | // the "data ready" status bit in the accelerometer. The |
mjr | 3:3514575d4f86 | 645 | // interrupt only occurs when the "ready" bit transitions from |
mjr | 3:3514575d4f86 | 646 | // off to on, so we have to make sure it's off. |
mjr | 5:a70c0bce770d | 647 | float x, y, z; |
mjr | 5:a70c0bce770d | 648 | mma_.getAccXYZ(x, y, z); |
mjr | 5:a70c0bce770d | 649 | |
mjr | 5:a70c0bce770d | 650 | // store the raw results |
mjr | 5:a70c0bce770d | 651 | araw_.set(x, y); |
mjr | 5:a70c0bce770d | 652 | zraw_ = z; |
mjr | 3:3514575d4f86 | 653 | |
mjr | 3:3514575d4f86 | 654 | // calculate the time since the last interrupt |
mjr | 3:3514575d4f86 | 655 | float dt = tInt_.read_us()/1.0e6; |
mjr | 3:3514575d4f86 | 656 | tInt_.reset(); |
mjr | 3:3514575d4f86 | 657 | |
mjr | 5:a70c0bce770d | 658 | // Add the velocity to the running total. First, calibrate the |
mjr | 5:a70c0bce770d | 659 | // raw acceleration to our centerpoint, then multiply by the time |
mjr | 5:a70c0bce770d | 660 | // since the last sample to get the velocity resulting from |
mjr | 5:a70c0bce770d | 661 | // applying this acceleration for the sample time. |
mjr | 5:a70c0bce770d | 662 | FPoint rdt((x - center_.x)*dt, (y - center_.y)*dt); |
mjr | 5:a70c0bce770d | 663 | vsum_ += rdt; |
mjr | 3:3514575d4f86 | 664 | } |
mjr | 3:3514575d4f86 | 665 | |
mjr | 3:3514575d4f86 | 666 | // underlying accelerometer object |
mjr | 3:3514575d4f86 | 667 | MMA8451Q mma_; |
mjr | 3:3514575d4f86 | 668 | |
mjr | 5:a70c0bce770d | 669 | // last raw acceleration readings |
mjr | 5:a70c0bce770d | 670 | FPoint araw_; |
mjr | 5:a70c0bce770d | 671 | float zraw_; |
mjr | 5:a70c0bce770d | 672 | |
mjr | 5:a70c0bce770d | 673 | // total velocity change since the last get() sample |
mjr | 5:a70c0bce770d | 674 | FPoint vsum_; |
mjr | 5:a70c0bce770d | 675 | |
mjr | 5:a70c0bce770d | 676 | // current modeled ball velocity |
mjr | 5:a70c0bce770d | 677 | FPoint v_; |
mjr | 3:3514575d4f86 | 678 | |
mjr | 3:3514575d4f86 | 679 | // timer for measuring time between get() samples |
mjr | 3:3514575d4f86 | 680 | Timer tGet_; |
mjr | 3:3514575d4f86 | 681 | |
mjr | 3:3514575d4f86 | 682 | // timer for measuring time between interrupts |
mjr | 3:3514575d4f86 | 683 | Timer tInt_; |
mjr | 5:a70c0bce770d | 684 | |
mjr | 5:a70c0bce770d | 685 | // time since last rest |
mjr | 5:a70c0bce770d | 686 | Timer tRest_; |
mjr | 5:a70c0bce770d | 687 | |
mjr | 5:a70c0bce770d | 688 | // calibrated center point - this is the position where we observe |
mjr | 5:a70c0bce770d | 689 | // constant input for a few seconds, telling us the orientation of |
mjr | 5:a70c0bce770d | 690 | // the accelerometer device when at rest |
mjr | 5:a70c0bce770d | 691 | FPoint center_; |
mjr | 5:a70c0bce770d | 692 | |
mjr | 5:a70c0bce770d | 693 | // timer for atuo-centering |
mjr | 5:a70c0bce770d | 694 | Timer tCenter_; |
mjr | 5:a70c0bce770d | 695 | |
mjr | 5:a70c0bce770d | 696 | // recent accelerometer readings, for auto centering |
mjr | 5:a70c0bce770d | 697 | int iAccPrv_, nAccPrv_; |
mjr | 5:a70c0bce770d | 698 | static const int maxAccPrv = 5; |
mjr | 5:a70c0bce770d | 699 | FPoint accPrv_[maxAccPrv]; |
mjr | 5:a70c0bce770d | 700 | |
mjr | 5:a70c0bce770d | 701 | // interurupt pin name |
mjr | 5:a70c0bce770d | 702 | PinName irqPin_; |
mjr | 5:a70c0bce770d | 703 | |
mjr | 5:a70c0bce770d | 704 | // interrupt router |
mjr | 5:a70c0bce770d | 705 | InterruptIn intIn_; |
mjr | 3:3514575d4f86 | 706 | }; |
mjr | 3:3514575d4f86 | 707 | |
mjr | 5:a70c0bce770d | 708 | |
mjr | 5:a70c0bce770d | 709 | // --------------------------------------------------------------------------- |
mjr | 5:a70c0bce770d | 710 | // |
mjr | 5:a70c0bce770d | 711 | // Clear the I2C bus for the MMA8451!. This seems necessary some of the time |
mjr | 5:a70c0bce770d | 712 | // for reasons that aren't clear to me. Doing a hard power cycle has the same |
mjr | 5:a70c0bce770d | 713 | // effect, but when we do a soft reset, the hardware sometimes seems to leave |
mjr | 5:a70c0bce770d | 714 | // the MMA's SDA line stuck low. Forcing a series of 9 clock pulses through |
mjr | 5:a70c0bce770d | 715 | // the SCL line is supposed to clear this conidtion. |
mjr | 5:a70c0bce770d | 716 | // |
mjr | 5:a70c0bce770d | 717 | void clear_i2c() |
mjr | 5:a70c0bce770d | 718 | { |
mjr | 5:a70c0bce770d | 719 | // assume a general-purpose output pin to the I2C clock |
mjr | 5:a70c0bce770d | 720 | DigitalOut scl(MMA8451_SCL_PIN); |
mjr | 5:a70c0bce770d | 721 | DigitalIn sda(MMA8451_SDA_PIN); |
mjr | 5:a70c0bce770d | 722 | |
mjr | 5:a70c0bce770d | 723 | // clock the SCL 9 times |
mjr | 5:a70c0bce770d | 724 | for (int i = 0 ; i < 9 ; ++i) |
mjr | 5:a70c0bce770d | 725 | { |
mjr | 5:a70c0bce770d | 726 | scl = 1; |
mjr | 5:a70c0bce770d | 727 | wait_us(20); |
mjr | 5:a70c0bce770d | 728 | scl = 0; |
mjr | 5:a70c0bce770d | 729 | wait_us(20); |
mjr | 5:a70c0bce770d | 730 | } |
mjr | 5:a70c0bce770d | 731 | } |
mjr | 5:a70c0bce770d | 732 | |
mjr | 5:a70c0bce770d | 733 | // --------------------------------------------------------------------------- |
mjr | 5:a70c0bce770d | 734 | // |
mjr | 5:a70c0bce770d | 735 | // Main program loop. This is invoked on startup and runs forever. Our |
mjr | 5:a70c0bce770d | 736 | // main work is to read our devices (the accelerometer and the CCD), process |
mjr | 5:a70c0bce770d | 737 | // the readings into nudge and plunger position data, and send the results |
mjr | 5:a70c0bce770d | 738 | // to the host computer via the USB joystick interface. We also monitor |
mjr | 5:a70c0bce770d | 739 | // the USB connection for incoming LedWiz commands and process those into |
mjr | 5:a70c0bce770d | 740 | // port outputs. |
mjr | 5:a70c0bce770d | 741 | // |
mjr | 0:5acbbe3f4cf4 | 742 | int main(void) |
mjr | 0:5acbbe3f4cf4 | 743 | { |
mjr | 1:d913e0afb2ac | 744 | // turn off our on-board indicator LED |
mjr | 4:02c7cd7b2183 | 745 | ledR = 1; |
mjr | 4:02c7cd7b2183 | 746 | ledG = 1; |
mjr | 4:02c7cd7b2183 | 747 | ledB = 1; |
mjr | 1:d913e0afb2ac | 748 | |
mjr | 5:a70c0bce770d | 749 | // clear the I2C bus for the accelerometer |
mjr | 5:a70c0bce770d | 750 | clear_i2c(); |
mjr | 5:a70c0bce770d | 751 | |
mjr | 5:a70c0bce770d | 752 | // Create the joystick USB client |
mjr | 5:a70c0bce770d | 753 | MyUSBJoystick js(USB_VENDOR_ID, USB_PRODUCT_ID, USB_VERSION_NO); |
mjr | 5:a70c0bce770d | 754 | |
mjr | 2:c174f9ee414a | 755 | // set up a flash memory controller |
mjr | 2:c174f9ee414a | 756 | FreescaleIAP iap; |
mjr | 2:c174f9ee414a | 757 | |
mjr | 2:c174f9ee414a | 758 | // use the last sector of flash for our non-volatile memory structure |
mjr | 2:c174f9ee414a | 759 | int flash_addr = (iap.flash_size() - SECTOR_SIZE); |
mjr | 2:c174f9ee414a | 760 | NVM *flash = (NVM *)flash_addr; |
mjr | 2:c174f9ee414a | 761 | NVM cfg; |
mjr | 2:c174f9ee414a | 762 | |
mjr | 2:c174f9ee414a | 763 | // check for valid flash |
mjr | 2:c174f9ee414a | 764 | bool flash_valid = (flash->d.sig == flash->SIGNATURE |
mjr | 2:c174f9ee414a | 765 | && flash->d.vsn == flash->VERSION |
mjr | 2:c174f9ee414a | 766 | && flash->checksum == CRC32(&flash->d, sizeof(flash->d))); |
mjr | 2:c174f9ee414a | 767 | |
mjr | 2:c174f9ee414a | 768 | // Number of pixels we read from the sensor on each frame. This can be |
mjr | 2:c174f9ee414a | 769 | // less than the physical pixel count if desired; we'll read every nth |
mjr | 2:c174f9ee414a | 770 | // piexl if so. E.g., with a 1280-pixel physical sensor, if npix is 320, |
mjr | 5:a70c0bce770d | 771 | // we'll read every 4th pixel. It takes time to read each pixel, so the |
mjr | 5:a70c0bce770d | 772 | // fewer pixels we read, the higher the refresh rate we can achieve. |
mjr | 5:a70c0bce770d | 773 | // It's therefore better not to read more pixels than we have to. |
mjr | 5:a70c0bce770d | 774 | // |
mjr | 5:a70c0bce770d | 775 | // VP seems to have an internal resolution in the 8-bit range, so there's |
mjr | 5:a70c0bce770d | 776 | // no apparent benefit to reading more than 128-256 pixels when using VP. |
mjr | 5:a70c0bce770d | 777 | // Empirically, 160 pixels seems about right. The overall travel of a |
mjr | 5:a70c0bce770d | 778 | // standard pinball plunger is about 3", so 160 pixels gives us resolution |
mjr | 5:a70c0bce770d | 779 | // of about 1/50". This seems to take full advantage of VP's modeling |
mjr | 5:a70c0bce770d | 780 | // ability, and is probably also more precise than a human player's |
mjr | 5:a70c0bce770d | 781 | // perception of the plunger position. |
mjr | 2:c174f9ee414a | 782 | const int npix = 160; |
mjr | 2:c174f9ee414a | 783 | |
mjr | 2:c174f9ee414a | 784 | // if the flash is valid, load it; otherwise initialize to defaults |
mjr | 2:c174f9ee414a | 785 | if (flash_valid) { |
mjr | 2:c174f9ee414a | 786 | memcpy(&cfg, flash, sizeof(cfg)); |
mjr | 2:c174f9ee414a | 787 | printf("Flash restored: plunger min=%d, max=%d\r\n", |
mjr | 2:c174f9ee414a | 788 | cfg.d.plungerMin, cfg.d.plungerMax); |
mjr | 2:c174f9ee414a | 789 | } |
mjr | 2:c174f9ee414a | 790 | else { |
mjr | 2:c174f9ee414a | 791 | printf("Factory reset\r\n"); |
mjr | 2:c174f9ee414a | 792 | cfg.d.sig = cfg.SIGNATURE; |
mjr | 2:c174f9ee414a | 793 | cfg.d.vsn = cfg.VERSION; |
mjr | 2:c174f9ee414a | 794 | cfg.d.plungerMin = 0; |
mjr | 2:c174f9ee414a | 795 | cfg.d.plungerMax = npix; |
mjr | 2:c174f9ee414a | 796 | } |
mjr | 1:d913e0afb2ac | 797 | |
mjr | 1:d913e0afb2ac | 798 | // plunger calibration button debounce timer |
mjr | 1:d913e0afb2ac | 799 | Timer calBtnTimer; |
mjr | 1:d913e0afb2ac | 800 | calBtnTimer.start(); |
mjr | 1:d913e0afb2ac | 801 | int calBtnDownTime = 0; |
mjr | 1:d913e0afb2ac | 802 | int calBtnLit = false; |
mjr | 1:d913e0afb2ac | 803 | |
mjr | 1:d913e0afb2ac | 804 | // Calibration button state: |
mjr | 1:d913e0afb2ac | 805 | // 0 = not pushed |
mjr | 1:d913e0afb2ac | 806 | // 1 = pushed, not yet debounced |
mjr | 1:d913e0afb2ac | 807 | // 2 = pushed, debounced, waiting for hold time |
mjr | 1:d913e0afb2ac | 808 | // 3 = pushed, hold time completed - in calibration mode |
mjr | 1:d913e0afb2ac | 809 | int calBtnState = 0; |
mjr | 1:d913e0afb2ac | 810 | |
mjr | 1:d913e0afb2ac | 811 | // set up a timer for our heartbeat indicator |
mjr | 1:d913e0afb2ac | 812 | Timer hbTimer; |
mjr | 1:d913e0afb2ac | 813 | hbTimer.start(); |
mjr | 1:d913e0afb2ac | 814 | int hb = 0; |
mjr | 5:a70c0bce770d | 815 | uint16_t hbcnt = 0; |
mjr | 1:d913e0afb2ac | 816 | |
mjr | 1:d913e0afb2ac | 817 | // set a timer for accelerometer auto-centering |
mjr | 1:d913e0afb2ac | 818 | Timer acTimer; |
mjr | 1:d913e0afb2ac | 819 | acTimer.start(); |
mjr | 1:d913e0afb2ac | 820 | |
mjr | 0:5acbbe3f4cf4 | 821 | // create the accelerometer object |
mjr | 5:a70c0bce770d | 822 | Accel accel(MMA8451_SCL_PIN, MMA8451_SDA_PIN, MMA8451_I2C_ADDRESS, MMA8451_INT_PIN); |
mjr | 0:5acbbe3f4cf4 | 823 | |
mjr | 0:5acbbe3f4cf4 | 824 | // create the CCD array object |
mjr | 1:d913e0afb2ac | 825 | TSL1410R ccd(PTE20, PTE21, PTB0); |
mjr | 2:c174f9ee414a | 826 | |
mjr | 1:d913e0afb2ac | 827 | // last accelerometer report, in mouse coordinates |
mjr | 1:d913e0afb2ac | 828 | int x = 127, y = 127, z = 0; |
mjr | 2:c174f9ee414a | 829 | |
mjr | 2:c174f9ee414a | 830 | // start the first CCD integration cycle |
mjr | 2:c174f9ee414a | 831 | ccd.clear(); |
mjr | 1:d913e0afb2ac | 832 | |
mjr | 1:d913e0afb2ac | 833 | // we're all set up - now just loop, processing sensor reports and |
mjr | 1:d913e0afb2ac | 834 | // host requests |
mjr | 0:5acbbe3f4cf4 | 835 | for (;;) |
mjr | 0:5acbbe3f4cf4 | 836 | { |
mjr | 0:5acbbe3f4cf4 | 837 | // Look for an incoming report. Continue processing input as |
mjr | 0:5acbbe3f4cf4 | 838 | // long as there's anything pending - this ensures that we |
mjr | 0:5acbbe3f4cf4 | 839 | // handle input in as timely a fashion as possible by deferring |
mjr | 0:5acbbe3f4cf4 | 840 | // output tasks as long as there's input to process. |
mjr | 0:5acbbe3f4cf4 | 841 | HID_REPORT report; |
mjr | 0:5acbbe3f4cf4 | 842 | while (js.readNB(&report) && report.length == 8) |
mjr | 0:5acbbe3f4cf4 | 843 | { |
mjr | 0:5acbbe3f4cf4 | 844 | uint8_t *data = report.data; |
mjr | 1:d913e0afb2ac | 845 | if (data[0] == 64) |
mjr | 1:d913e0afb2ac | 846 | { |
mjr | 0:5acbbe3f4cf4 | 847 | // LWZ-SBA - first four bytes are bit-packed on/off flags |
mjr | 0:5acbbe3f4cf4 | 848 | // for the outputs; 5th byte is the pulse speed (0-7) |
mjr | 0:5acbbe3f4cf4 | 849 | //printf("LWZ-SBA %02x %02x %02x %02x ; %02x\r\n", |
mjr | 0:5acbbe3f4cf4 | 850 | // data[1], data[2], data[3], data[4], data[5]); |
mjr | 0:5acbbe3f4cf4 | 851 | |
mjr | 0:5acbbe3f4cf4 | 852 | // update all on/off states |
mjr | 0:5acbbe3f4cf4 | 853 | for (int i = 0, bit = 1, ri = 1 ; i < 32 ; ++i, bit <<= 1) |
mjr | 0:5acbbe3f4cf4 | 854 | { |
mjr | 0:5acbbe3f4cf4 | 855 | if (bit == 0x100) { |
mjr | 0:5acbbe3f4cf4 | 856 | bit = 1; |
mjr | 0:5acbbe3f4cf4 | 857 | ++ri; |
mjr | 0:5acbbe3f4cf4 | 858 | } |
mjr | 1:d913e0afb2ac | 859 | wizOn[i] = ((data[ri] & bit) != 0); |
mjr | 0:5acbbe3f4cf4 | 860 | } |
mjr | 0:5acbbe3f4cf4 | 861 | |
mjr | 1:d913e0afb2ac | 862 | // update the physical outputs |
mjr | 1:d913e0afb2ac | 863 | updateWizOuts(); |
mjr | 0:5acbbe3f4cf4 | 864 | |
mjr | 0:5acbbe3f4cf4 | 865 | // reset the PBA counter |
mjr | 0:5acbbe3f4cf4 | 866 | pbaIdx = 0; |
mjr | 0:5acbbe3f4cf4 | 867 | } |
mjr | 1:d913e0afb2ac | 868 | else |
mjr | 1:d913e0afb2ac | 869 | { |
mjr | 0:5acbbe3f4cf4 | 870 | // LWZ-PBA - full state dump; each byte is one output |
mjr | 0:5acbbe3f4cf4 | 871 | // in the current bank. pbaIdx keeps track of the bank; |
mjr | 0:5acbbe3f4cf4 | 872 | // this is incremented implicitly by each PBA message. |
mjr | 0:5acbbe3f4cf4 | 873 | //printf("LWZ-PBA[%d] %02x %02x %02x %02x %02x %02x %02x %02x\r\n", |
mjr | 0:5acbbe3f4cf4 | 874 | // pbaIdx, data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]); |
mjr | 0:5acbbe3f4cf4 | 875 | |
mjr | 0:5acbbe3f4cf4 | 876 | // update all output profile settings |
mjr | 0:5acbbe3f4cf4 | 877 | for (int i = 0 ; i < 8 ; ++i) |
mjr | 1:d913e0afb2ac | 878 | wizVal[pbaIdx + i] = data[i]; |
mjr | 0:5acbbe3f4cf4 | 879 | |
mjr | 0:5acbbe3f4cf4 | 880 | // update the physical LED state if this is the last bank |
mjr | 0:5acbbe3f4cf4 | 881 | if (pbaIdx == 24) |
mjr | 1:d913e0afb2ac | 882 | updateWizOuts(); |
mjr | 0:5acbbe3f4cf4 | 883 | |
mjr | 0:5acbbe3f4cf4 | 884 | // advance to the next bank |
mjr | 0:5acbbe3f4cf4 | 885 | pbaIdx = (pbaIdx + 8) & 31; |
mjr | 0:5acbbe3f4cf4 | 886 | } |
mjr | 0:5acbbe3f4cf4 | 887 | } |
mjr | 1:d913e0afb2ac | 888 | |
mjr | 1:d913e0afb2ac | 889 | // check for plunger calibration |
mjr | 1:d913e0afb2ac | 890 | if (!calBtn) |
mjr | 0:5acbbe3f4cf4 | 891 | { |
mjr | 1:d913e0afb2ac | 892 | // check the state |
mjr | 1:d913e0afb2ac | 893 | switch (calBtnState) |
mjr | 0:5acbbe3f4cf4 | 894 | { |
mjr | 1:d913e0afb2ac | 895 | case 0: |
mjr | 1:d913e0afb2ac | 896 | // button not yet pushed - start debouncing |
mjr | 1:d913e0afb2ac | 897 | calBtnTimer.reset(); |
mjr | 1:d913e0afb2ac | 898 | calBtnDownTime = calBtnTimer.read_ms(); |
mjr | 1:d913e0afb2ac | 899 | calBtnState = 1; |
mjr | 1:d913e0afb2ac | 900 | break; |
mjr | 1:d913e0afb2ac | 901 | |
mjr | 1:d913e0afb2ac | 902 | case 1: |
mjr | 1:d913e0afb2ac | 903 | // pushed, not yet debounced - if the debounce time has |
mjr | 1:d913e0afb2ac | 904 | // passed, start the hold period |
mjr | 1:d913e0afb2ac | 905 | if (calBtnTimer.read_ms() - calBtnDownTime > 50) |
mjr | 1:d913e0afb2ac | 906 | calBtnState = 2; |
mjr | 1:d913e0afb2ac | 907 | break; |
mjr | 1:d913e0afb2ac | 908 | |
mjr | 1:d913e0afb2ac | 909 | case 2: |
mjr | 1:d913e0afb2ac | 910 | // in the hold period - if the button has been held down |
mjr | 1:d913e0afb2ac | 911 | // for the entire hold period, move to calibration mode |
mjr | 1:d913e0afb2ac | 912 | if (calBtnTimer.read_ms() - calBtnDownTime > 2050) |
mjr | 1:d913e0afb2ac | 913 | { |
mjr | 1:d913e0afb2ac | 914 | // enter calibration mode |
mjr | 1:d913e0afb2ac | 915 | calBtnState = 3; |
mjr | 1:d913e0afb2ac | 916 | |
mjr | 1:d913e0afb2ac | 917 | // reset the calibration limits |
mjr | 2:c174f9ee414a | 918 | cfg.d.plungerMax = 0; |
mjr | 2:c174f9ee414a | 919 | cfg.d.plungerMin = npix; |
mjr | 1:d913e0afb2ac | 920 | } |
mjr | 1:d913e0afb2ac | 921 | break; |
mjr | 2:c174f9ee414a | 922 | |
mjr | 2:c174f9ee414a | 923 | case 3: |
mjr | 2:c174f9ee414a | 924 | // Already in calibration mode - pushing the button in this |
mjr | 2:c174f9ee414a | 925 | // state doesn't change the current state, but we won't leave |
mjr | 2:c174f9ee414a | 926 | // this state as long as it's held down. We can simply do |
mjr | 2:c174f9ee414a | 927 | // nothing here. |
mjr | 2:c174f9ee414a | 928 | break; |
mjr | 0:5acbbe3f4cf4 | 929 | } |
mjr | 0:5acbbe3f4cf4 | 930 | } |
mjr | 1:d913e0afb2ac | 931 | else |
mjr | 1:d913e0afb2ac | 932 | { |
mjr | 2:c174f9ee414a | 933 | // Button released. If we're in calibration mode, and |
mjr | 2:c174f9ee414a | 934 | // the calibration time has elapsed, end the calibration |
mjr | 2:c174f9ee414a | 935 | // and save the results to flash. |
mjr | 2:c174f9ee414a | 936 | // |
mjr | 2:c174f9ee414a | 937 | // Otherwise, return to the base state without saving anything. |
mjr | 2:c174f9ee414a | 938 | // If the button is released before we make it to calibration |
mjr | 2:c174f9ee414a | 939 | // mode, it simply cancels the attempt. |
mjr | 2:c174f9ee414a | 940 | if (calBtnState == 3 |
mjr | 2:c174f9ee414a | 941 | && calBtnTimer.read_ms() - calBtnDownTime > 17500) |
mjr | 2:c174f9ee414a | 942 | { |
mjr | 2:c174f9ee414a | 943 | // exit calibration mode |
mjr | 1:d913e0afb2ac | 944 | calBtnState = 0; |
mjr | 2:c174f9ee414a | 945 | |
mjr | 2:c174f9ee414a | 946 | // Save the current configuration state to flash, so that it |
mjr | 2:c174f9ee414a | 947 | // will be preserved through power off. Update the checksum |
mjr | 2:c174f9ee414a | 948 | // first so that we recognize the flash record as valid. |
mjr | 2:c174f9ee414a | 949 | cfg.checksum = CRC32(&cfg.d, sizeof(cfg.d)); |
mjr | 2:c174f9ee414a | 950 | iap.erase_sector(flash_addr); |
mjr | 2:c174f9ee414a | 951 | iap.program_flash(flash_addr, &cfg, sizeof(cfg)); |
mjr | 2:c174f9ee414a | 952 | |
mjr | 2:c174f9ee414a | 953 | // the flash state is now valid |
mjr | 2:c174f9ee414a | 954 | flash_valid = true; |
mjr | 2:c174f9ee414a | 955 | } |
mjr | 2:c174f9ee414a | 956 | else if (calBtnState != 3) |
mjr | 2:c174f9ee414a | 957 | { |
mjr | 2:c174f9ee414a | 958 | // didn't make it to calibration mode - cancel the operation |
mjr | 1:d913e0afb2ac | 959 | calBtnState = 0; |
mjr | 2:c174f9ee414a | 960 | } |
mjr | 1:d913e0afb2ac | 961 | } |
mjr | 1:d913e0afb2ac | 962 | |
mjr | 1:d913e0afb2ac | 963 | // light/flash the calibration button light, if applicable |
mjr | 1:d913e0afb2ac | 964 | int newCalBtnLit = calBtnLit; |
mjr | 1:d913e0afb2ac | 965 | switch (calBtnState) |
mjr | 0:5acbbe3f4cf4 | 966 | { |
mjr | 1:d913e0afb2ac | 967 | case 2: |
mjr | 1:d913e0afb2ac | 968 | // in the hold period - flash the light |
mjr | 1:d913e0afb2ac | 969 | newCalBtnLit = (((calBtnTimer.read_ms() - calBtnDownTime)/250) & 1); |
mjr | 1:d913e0afb2ac | 970 | break; |
mjr | 1:d913e0afb2ac | 971 | |
mjr | 1:d913e0afb2ac | 972 | case 3: |
mjr | 1:d913e0afb2ac | 973 | // calibration mode - show steady on |
mjr | 1:d913e0afb2ac | 974 | newCalBtnLit = true; |
mjr | 1:d913e0afb2ac | 975 | break; |
mjr | 1:d913e0afb2ac | 976 | |
mjr | 1:d913e0afb2ac | 977 | default: |
mjr | 1:d913e0afb2ac | 978 | // not calibrating/holding - show steady off |
mjr | 1:d913e0afb2ac | 979 | newCalBtnLit = false; |
mjr | 1:d913e0afb2ac | 980 | break; |
mjr | 1:d913e0afb2ac | 981 | } |
mjr | 3:3514575d4f86 | 982 | |
mjr | 3:3514575d4f86 | 983 | // light or flash the external calibration button LED, and |
mjr | 3:3514575d4f86 | 984 | // do the same with the on-board blue LED |
mjr | 1:d913e0afb2ac | 985 | if (calBtnLit != newCalBtnLit) |
mjr | 1:d913e0afb2ac | 986 | { |
mjr | 1:d913e0afb2ac | 987 | calBtnLit = newCalBtnLit; |
mjr | 2:c174f9ee414a | 988 | if (calBtnLit) { |
mjr | 2:c174f9ee414a | 989 | calBtnLed = 1; |
mjr | 4:02c7cd7b2183 | 990 | ledR = 1; |
mjr | 4:02c7cd7b2183 | 991 | ledG = 1; |
mjr | 4:02c7cd7b2183 | 992 | ledB = 1; |
mjr | 2:c174f9ee414a | 993 | } |
mjr | 2:c174f9ee414a | 994 | else { |
mjr | 2:c174f9ee414a | 995 | calBtnLed = 0; |
mjr | 4:02c7cd7b2183 | 996 | ledR = 1; |
mjr | 4:02c7cd7b2183 | 997 | ledG = 1; |
mjr | 4:02c7cd7b2183 | 998 | ledB = 0; |
mjr | 2:c174f9ee414a | 999 | } |
mjr | 1:d913e0afb2ac | 1000 | } |
mjr | 1:d913e0afb2ac | 1001 | |
mjr | 1:d913e0afb2ac | 1002 | // read the plunger sensor |
mjr | 1:d913e0afb2ac | 1003 | int znew = z; |
mjr | 2:c174f9ee414a | 1004 | uint16_t pix[npix]; |
mjr | 2:c174f9ee414a | 1005 | ccd.read(pix, npix); |
mjr | 2:c174f9ee414a | 1006 | |
mjr | 2:c174f9ee414a | 1007 | // get the average brightness at each end of the sensor |
mjr | 2:c174f9ee414a | 1008 | long avg1 = (long(pix[0]) + long(pix[1]) + long(pix[2]) + long(pix[3]) + long(pix[4]))/5; |
mjr | 2:c174f9ee414a | 1009 | long avg2 = (long(pix[npix-1]) + long(pix[npix-2]) + long(pix[npix-3]) + long(pix[npix-4]) + long(pix[npix-5]))/5; |
mjr | 2:c174f9ee414a | 1010 | |
mjr | 2:c174f9ee414a | 1011 | // figure the midpoint in the brightness; multiply by 3 so that we can |
mjr | 2:c174f9ee414a | 1012 | // compare sums of three pixels at a time to smooth out noise |
mjr | 2:c174f9ee414a | 1013 | long midpt = (avg1 + avg2)/2 * 3; |
mjr | 2:c174f9ee414a | 1014 | |
mjr | 2:c174f9ee414a | 1015 | // Work from the bright end to the dark end. VP interprets the |
mjr | 2:c174f9ee414a | 1016 | // Z axis value as the amount the plunger is pulled: the minimum |
mjr | 2:c174f9ee414a | 1017 | // is the rest position, the maximum is fully pulled. So we |
mjr | 2:c174f9ee414a | 1018 | // essentially want to report how much of the sensor is lit, |
mjr | 2:c174f9ee414a | 1019 | // since this increases as the plunger is pulled back. |
mjr | 2:c174f9ee414a | 1020 | int si = 1, di = 1; |
mjr | 2:c174f9ee414a | 1021 | if (avg1 < avg2) |
mjr | 2:c174f9ee414a | 1022 | si = npix - 2, di = -1; |
mjr | 2:c174f9ee414a | 1023 | |
mjr | 2:c174f9ee414a | 1024 | // scan for the midpoint |
mjr | 2:c174f9ee414a | 1025 | uint16_t *pixp = pix + si; |
mjr | 2:c174f9ee414a | 1026 | for (int n = 1 ; n < npix - 1 ; ++n, pixp += di) |
mjr | 1:d913e0afb2ac | 1027 | { |
mjr | 2:c174f9ee414a | 1028 | // if we've crossed the midpoint, report this position |
mjr | 2:c174f9ee414a | 1029 | if (long(pixp[-1]) + long(pixp[0]) + long(pixp[1]) < midpt) |
mjr | 1:d913e0afb2ac | 1030 | { |
mjr | 2:c174f9ee414a | 1031 | // note the new position |
mjr | 2:c174f9ee414a | 1032 | int pos = n; |
mjr | 2:c174f9ee414a | 1033 | |
mjr | 2:c174f9ee414a | 1034 | // if the bright end and dark end don't differ by enough, skip this |
mjr | 2:c174f9ee414a | 1035 | // reading entirely - we must have an overexposed or underexposed frame |
mjr | 2:c174f9ee414a | 1036 | if (labs(avg1 - avg2) < 0x3333) |
mjr | 2:c174f9ee414a | 1037 | break; |
mjr | 2:c174f9ee414a | 1038 | |
mjr | 2:c174f9ee414a | 1039 | // Calibrate, or apply calibration, depending on the mode. |
mjr | 2:c174f9ee414a | 1040 | // In either case, normalize to a 0-127 range. VP appears to |
mjr | 2:c174f9ee414a | 1041 | // ignore negative Z axis values. |
mjr | 2:c174f9ee414a | 1042 | if (calBtnState == 3) |
mjr | 1:d913e0afb2ac | 1043 | { |
mjr | 2:c174f9ee414a | 1044 | // calibrating - note if we're expanding the calibration envelope |
mjr | 2:c174f9ee414a | 1045 | if (pos < cfg.d.plungerMin) |
mjr | 2:c174f9ee414a | 1046 | cfg.d.plungerMin = pos; |
mjr | 2:c174f9ee414a | 1047 | if (pos > cfg.d.plungerMax) |
mjr | 2:c174f9ee414a | 1048 | cfg.d.plungerMax = pos; |
mjr | 2:c174f9ee414a | 1049 | |
mjr | 2:c174f9ee414a | 1050 | // normalize to the full physical range while calibrating |
mjr | 2:c174f9ee414a | 1051 | znew = int(float(pos)/npix * 127); |
mjr | 1:d913e0afb2ac | 1052 | } |
mjr | 2:c174f9ee414a | 1053 | else |
mjr | 2:c174f9ee414a | 1054 | { |
mjr | 2:c174f9ee414a | 1055 | // running normally - normalize to the calibration range |
mjr | 2:c174f9ee414a | 1056 | if (pos < cfg.d.plungerMin) |
mjr | 2:c174f9ee414a | 1057 | pos = cfg.d.plungerMin; |
mjr | 2:c174f9ee414a | 1058 | if (pos > cfg.d.plungerMax) |
mjr | 2:c174f9ee414a | 1059 | pos = cfg.d.plungerMax; |
mjr | 2:c174f9ee414a | 1060 | znew = int(float(pos - cfg.d.plungerMin) |
mjr | 2:c174f9ee414a | 1061 | / (cfg.d.plungerMax - cfg.d.plungerMin + 1) * 127); |
mjr | 2:c174f9ee414a | 1062 | } |
mjr | 2:c174f9ee414a | 1063 | |
mjr | 2:c174f9ee414a | 1064 | // done |
mjr | 2:c174f9ee414a | 1065 | break; |
mjr | 1:d913e0afb2ac | 1066 | } |
mjr | 2:c174f9ee414a | 1067 | } |
mjr | 1:d913e0afb2ac | 1068 | |
mjr | 1:d913e0afb2ac | 1069 | // read the accelerometer |
mjr | 3:3514575d4f86 | 1070 | float xa, ya, rxa, rya; |
mjr | 3:3514575d4f86 | 1071 | accel.get(xa, ya, rxa, rya); |
mjr | 1:d913e0afb2ac | 1072 | |
mjr | 5:a70c0bce770d | 1073 | // confine the accelerometer results to the unit interval |
mjr | 1:d913e0afb2ac | 1074 | if (xa < -1.0) xa = -1.0; |
mjr | 1:d913e0afb2ac | 1075 | if (xa > 1.0) xa = 1.0; |
mjr | 1:d913e0afb2ac | 1076 | if (ya < -1.0) ya = -1.0; |
mjr | 1:d913e0afb2ac | 1077 | if (ya > 1.0) ya = 1.0; |
mjr | 0:5acbbe3f4cf4 | 1078 | |
mjr | 5:a70c0bce770d | 1079 | // scale to our -127..127 reporting range |
mjr | 5:a70c0bce770d | 1080 | int xnew = int(127 * xa); |
mjr | 5:a70c0bce770d | 1081 | int ynew = int(127 * ya); |
mjr | 2:c174f9ee414a | 1082 | |
mjr | 2:c174f9ee414a | 1083 | // store the updated joystick coordinates |
mjr | 2:c174f9ee414a | 1084 | x = xnew; |
mjr | 2:c174f9ee414a | 1085 | y = ynew; |
mjr | 2:c174f9ee414a | 1086 | z = znew; |
mjr | 1:d913e0afb2ac | 1087 | |
mjr | 3:3514575d4f86 | 1088 | // Send the status report. It doesn't really matter what |
mjr | 3:3514575d4f86 | 1089 | // coordinate system we use, since Visual Pinball has config |
mjr | 3:3514575d4f86 | 1090 | // options for rotations and axis reversals, but reversing y |
mjr | 3:3514575d4f86 | 1091 | // at the device level seems to produce the most intuitive |
mjr | 3:3514575d4f86 | 1092 | // results for the Windows joystick control panel view, which |
mjr | 3:3514575d4f86 | 1093 | // is an easy way to check that the device is working. |
mjr | 5:a70c0bce770d | 1094 | // |
mjr | 5:a70c0bce770d | 1095 | // $$$ button updates are for diagnostics, so we can see that the |
mjr | 5:a70c0bce770d | 1096 | // device is sending data properly if the accelerometer gets stuck |
mjr | 5:a70c0bce770d | 1097 | js.update(x, -y, z, int(rxa*127), int(rya*127), hb ? 0x5500 : 0xAA00); |
mjr | 1:d913e0afb2ac | 1098 | |
mjr | 2:c174f9ee414a | 1099 | // show a heartbeat flash in blue every so often if not in |
mjr | 2:c174f9ee414a | 1100 | // calibration mode |
mjr | 5:a70c0bce770d | 1101 | if (calBtnState < 2 && hbTimer.read_ms() > 1000) |
mjr | 1:d913e0afb2ac | 1102 | { |
mjr | 5:a70c0bce770d | 1103 | if (js.isSuspended() || !js.isConnected()) |
mjr | 2:c174f9ee414a | 1104 | { |
mjr | 5:a70c0bce770d | 1105 | // suspended - turn off the LED |
mjr | 4:02c7cd7b2183 | 1106 | ledR = 1; |
mjr | 4:02c7cd7b2183 | 1107 | ledG = 1; |
mjr | 4:02c7cd7b2183 | 1108 | ledB = 1; |
mjr | 5:a70c0bce770d | 1109 | |
mjr | 5:a70c0bce770d | 1110 | // show a status flash every so often |
mjr | 5:a70c0bce770d | 1111 | if (hbcnt % 3 == 0) |
mjr | 5:a70c0bce770d | 1112 | { |
mjr | 5:a70c0bce770d | 1113 | // disconnected = red flash; suspended = red-red |
mjr | 5:a70c0bce770d | 1114 | for (int n = js.isConnected() ? 1 : 2 ; n > 0 ; --n) |
mjr | 5:a70c0bce770d | 1115 | { |
mjr | 5:a70c0bce770d | 1116 | ledR = 0; |
mjr | 5:a70c0bce770d | 1117 | wait(0.05); |
mjr | 5:a70c0bce770d | 1118 | ledR = 1; |
mjr | 5:a70c0bce770d | 1119 | wait(0.25); |
mjr | 5:a70c0bce770d | 1120 | } |
mjr | 5:a70c0bce770d | 1121 | } |
mjr | 2:c174f9ee414a | 1122 | } |
mjr | 2:c174f9ee414a | 1123 | else if (flash_valid) |
mjr | 2:c174f9ee414a | 1124 | { |
mjr | 2:c174f9ee414a | 1125 | // connected, NVM valid - flash blue/green |
mjr | 2:c174f9ee414a | 1126 | hb = !hb; |
mjr | 4:02c7cd7b2183 | 1127 | ledR = 1; |
mjr | 4:02c7cd7b2183 | 1128 | ledG = (hb ? 0 : 1); |
mjr | 4:02c7cd7b2183 | 1129 | ledB = (hb ? 1 : 0); |
mjr | 2:c174f9ee414a | 1130 | } |
mjr | 2:c174f9ee414a | 1131 | else |
mjr | 2:c174f9ee414a | 1132 | { |
mjr | 2:c174f9ee414a | 1133 | // connected, factory reset - flash yellow/green |
mjr | 2:c174f9ee414a | 1134 | hb = !hb; |
mjr | 5:a70c0bce770d | 1135 | //ledR = (hb ? 0 : 1); |
mjr | 5:a70c0bce770d | 1136 | //ledG = 0; |
mjr | 4:02c7cd7b2183 | 1137 | ledB = 1; |
mjr | 2:c174f9ee414a | 1138 | } |
mjr | 1:d913e0afb2ac | 1139 | |
mjr | 1:d913e0afb2ac | 1140 | // reset the heartbeat timer |
mjr | 1:d913e0afb2ac | 1141 | hbTimer.reset(); |
mjr | 5:a70c0bce770d | 1142 | ++hbcnt; |
mjr | 1:d913e0afb2ac | 1143 | } |
mjr | 1:d913e0afb2ac | 1144 | } |
mjr | 0:5acbbe3f4cf4 | 1145 | } |