An input/output controller for virtual pinball machines, with plunger position tracking, accelerometer-based nudge sensing, button input encoding, and feedback device control.

Dependencies:   USBDevice mbed FastAnalogIn FastIO FastPWM SimpleDMA

/media/uploads/mjr/pinscape_no_background_small_L7Miwr6.jpg

The Pinscape Controller is a special-purpose software project that I wrote for my virtual pinball machine.

New version: V2 is now available! The information below is for version 1, which will continue to be available for people who prefer the original setup.

What exactly is a virtual pinball machine? It's basically a video-game pinball emulator built to look like a real pinball machine. (The picture at right is the one I built.) You start with a standard pinball cabinet, either built from scratch or salvaged from a real machine. Inside, you install a PC motherboard to run the software, and install TVs in place of the playfield and backglass. Several Windows pinball programs can take advantage of this setup, including the open-source project Visual Pinball, which has hundreds of tables available. Building one of these makes a great DIY project, and it's a good way to add to your skills at woodworking, computers, and electronics. Check out the Cabinet Builders' Forum on vpforums.org for lots of examples and advice.

This controller project is a key piece in my setup that helps integrate the video game into the pinball cabinet. It handles several input/output tasks that are unique to virtual pinball machines. First, it lets you connect a mechanical plunger to the software, so you can launch the ball like on a real machine. Second, it sends "nudge" data to the software, based on readings from an accelerometer. This lets you interact with the game physically, which makes the playing experience more realistic and immersive. Third, the software can handle button input (for wiring flipper buttons and other cabinet buttons), and fourth, it can control output devices (for tactile feedback, button lights, flashers, and other special effects).

Documentation

The Hardware Build Guide (PDF) has detailed instructions on how to set up a Pinscape Controller for your own virtual pinball cabinet.

Update notes

December 2015 version: This version fully supports the new Expansion Board project, but it'll also run without it. The default configuration settings haven't changed, so existing setups should continue to work as before.

August 2015 version: Be sure to get the latest version of the Config Tool for windows if you're upgrading from an older version of the firmware. This update adds support for TSL1412R sensors (a version of the 1410 sensor with a slightly larger pixel array), and a config option to set the mounting orientation of the board in the firmware rather than in VP (for better support for FP and other pinball programs that don't have VP's flexibility for setting the rotation).

Feb/March 2015 software versions: If you have a CCD plunger that you've been using with the older versions, and the plunger stops working (or doesn't work as well) after you update to the latest version, you might need to increase the brightness of your light source slightly. Check the CCD exposure with the Windows config tool to see if it looks too dark. The new software reads the CCD much more quickly than the old versions did. This makes the "shutter speed" faster, which might require a little more light to get the same readings. The CCD is actually really tolerant of varying light levels, so you probably won't have to change anything for the update - I didn't. But if you do have any trouble, have a look at the exposure meter and try a slightly brighter light source if the exposure looks too dark.

Downloads

  • Config tool for Windows (.exe and C# source): this is a Windows program that lets you view the raw pixel data from the CCD sensor, trigger plunger calibration mode, and configure some of the software options on the controller.
  • Custom VP builds: I created modified versions of Visual Pinball 9.9 and Physmod5 that you might want to use in combination with this controller. The modified versions have special handling for plunger calibration specific to the Pinscape Controller, as well as some enhancements to the nudge physics. If you're not using the plunger, you might still want it for the nudge improvements. The modified version also works with any other input controller, so you can get the enhanced nudging effects even if you're using a different plunger/nudge kit. The big change in the modified versions is a "filter" for accelerometer input that's designed to make the response to cabinet nudges more realistic. It also makes the response more subdued than in the standard VP, so it's not to everyone's taste. The downloads include both the updated executables and the source code changes, in case you want to merge the changes into your own custom version(s).

    Note! These features are now standard in the official VP 9.9.1 and VP 10 releases, so you don't need my custom builds if you're using 9.9.1 or 10 or later. I don't think there's any reason to use my 9.9 instead of the official 9.9.1, but I'm leaving it here just in case. In the official VP releases, look for the checkbox "Enable Nudge Filter" in the Keys preferences dialog. (There's no checkbox in my custom builds, though; the filter is simply always on in those.)
  • Output circuit shopping list: This is a saved shopping cart at mouser.com with the parts needed for each output driver, if you want to use the LedWiz emulator feature. Note that quantities in the cart are for one output channel, so multiply everything by the number of channels you plan to use, except that you only need one of the ULN2803 transistor array chips for each eight output circuits.
  • Lemming77's potentiometer mounting bracket and shooter rod connecter: Sketchup designs for 3D-printable parts for mounting a slide potentiometer as the plunger sensor. These were designed for a particular slide potentiometer that used to be available from an Aliexpress.com seller but is no longer listed. You can probably use this design as a starting point for other similar devices; just check the dimensions before committing the design to plastic.

Features

  • Plunger position sensing, using a TAOS TSL 1410R CCD linear array sensor. This sensor is a 1280 x 1 pixel array at 400 dpi, which makes it about 3" long - almost exactly the travel distance of a standard pinball plunger. The idea is that you install the sensor just above (within a few mm of) the shooter rod on the inside of the cabinet, with the CCD window facing down, aligned with and centered on the long axis of the shooter rod, and positioned so that the rest position of the tip is about 1/2" from one end of the window. As you pull back the plunger, the tip will travel down the length of the window, and the maximum retraction point will put the tip just about at the far end of the window. Put a light source below, facing the sensor - I'm using two typical 20 mA blue LEDs about 8" away (near the floor of the cabinet) with good results. The principle of operation is that the shooter rod casts a shadow on the CCD, so pixels behind the rod will register lower brightness than pixels that aren't in the shadow. We scan down the length of the sensor for the edge between darker and brighter, and this tells us how far back the rod has been pulled. We can read the CCD at about 25-30 ms intervals, so we can get rapid updates. We pass the readings reports to VP via our USB joystick reports.

    The hardware build guide includes schematics showing how to wire the CCD to the KL25Z. It's pretty straightforward - five wires between the two devices, no external components needed. Two GPIO ports are used as outputs to send signals to the device and one is used as an ADC in to read the pixel brightness inputs. The config tool has a feature that lets you display the raw pixel readings across the array, so you can test that the CCD is working and adjust the light source to get the right exposure level.

    Alternatively, you can use a slide potentiometer as the plunger sensor. This is a cheaper and somewhat simpler option that seems to work quite nicely, as you can see in Lemming77's video of this setup in action. This option is also explained more fully in the build guide.
  • Nudge sensing via the KL25Z's on-board accelerometer. Mounting the board in your cabinet makes it feel the same accelerations the cabinet experiences when you nudge it. Visual Pinball already knows how to interpret accelerometer input as nudging, so we simply feed the acceleration readings to VP via the joystick interface.
  • Cabinet button wiring. Up to 24 pushbuttons and switches can be wired to the controller for input controls (for example, flipper buttons, the Start button, the tilt bob, coin slot switches, and service door buttons). These appear to Windows as joystick buttons. VP can map joystick buttons to pinball inputs via its keyboard preferences dialog. (You can raise the 24-button limit by editing the source code, but since all of the GPIO pins are allocated, you'll have to reassign pins currently used for other functions.)
  • LedWiz emulation (limited). In addition to emulating a joystick, the device emulates the LedWiz USB interface, so controllers on the PC side such as DirectOutput Framework can recognize it and send it commands to control lights, solenoids, and other feedback devices. 22 GPIO ports are assigned by default as feedback device outputs. This feature has some limitations. The big one is that the KL25Z hardware only has 10 PWM channels, which isn't enough for a fully decked-out cabinet. You also need to build some external power driver circuitry to use this feature, because of the paltry 4mA output capacity of the KL25Z GPIO ports. The build guide includes instructions for a simple and robust output circuit, including part numbers for the exact components you need. It's not hard if you know your way around a soldering iron, but just be aware that it'll take a little work.

Warning: This is not replacement software for the VirtuaPin plunger kit. If you bought the VirtuaPin kit, please don't try to install this software. The VP kit happens to use the same microcontroller board, but the rest of its hardware is incompatible. The VP kit uses a different type of sensor for its plunger and has completely different button wiring, so the Pinscape software won't work properly with it.

Committer:
mjr
Date:
Fri Feb 27 07:48:58 2015 +0000
Revision:
19:054f8af32fce
Parent:
18:5e890ebd0023
Child:
20:4c43877327ab
New key debouncing

Who changed what in which revision?

UserRevisionLine numberNew 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 17:ab3cec0c8bf4 22 // "Pinscape" is the name of my custom-built virtual pinball cabinet, so I call this
mjr 17:ab3cec0c8bf4 23 // software the Pinscape Controller. I wrote it to handle several tasks that I needed
mjr 17:ab3cec0c8bf4 24 // for my cabinet. It runs on a Freescale KL25Z microcontroller, which is a small and
mjr 17:ab3cec0c8bf4 25 // inexpensive device that attaches to the cabinet PC via a USB cable, and can attach
mjr 17:ab3cec0c8bf4 26 // via custom wiring to sensors, buttons, and other devices in the cabinet.
mjr 5:a70c0bce770d 27 //
mjr 17:ab3cec0c8bf4 28 // I designed the software and hardware in this project especially for my own
mjr 17:ab3cec0c8bf4 29 // cabinet, but it uses standard interfaces in Windows and Visual Pinball, so it should
mjr 17:ab3cec0c8bf4 30 // work in any VP-based cabinet, as long as you're using the usual VP software suite.
mjr 17:ab3cec0c8bf4 31 // I've tried to document the hardware in enough detail for anyone else to duplicate
mjr 17:ab3cec0c8bf4 32 // the entire project, and the full software is open source.
mjr 5:a70c0bce770d 33 //
mjr 17:ab3cec0c8bf4 34 // The Freescale board appears to the host PC as a standard USB joystick. This works
mjr 17:ab3cec0c8bf4 35 // with the built-in Windows joystick device drivers, so there's no need to install any
mjr 17:ab3cec0c8bf4 36 // new drivers or other software on the PC. Windows should recognize the Freescale
mjr 17:ab3cec0c8bf4 37 // as a joystick when you plug it into the USB port, and Windows shouldn't ask you to
mjr 17:ab3cec0c8bf4 38 // install any drivers. If you bring up the Windows control panel for USB Game
mjr 17:ab3cec0c8bf4 39 // Controllers, this device will appear as "Pinscape Controller". *Don't* do any
mjr 17:ab3cec0c8bf4 40 // calibration with the Windows control panel or third-part calibration tools. The
mjr 17:ab3cec0c8bf4 41 // software calibrates the accelerometer portion automatically, and has its own special
mjr 17:ab3cec0c8bf4 42 // calibration procedure for the plunger sensor, if you're using that (see below).
mjr 5:a70c0bce770d 43 //
mjr 17:ab3cec0c8bf4 44 // This software provides a whole bunch of separate features. You can use any of these
mjr 17:ab3cec0c8bf4 45 // features individually or all together. If you're not using a particular feature, you
mjr 17:ab3cec0c8bf4 46 // can simply omit the extra wiring and/or hardware for that feature. You can use
mjr 17:ab3cec0c8bf4 47 // the nudging feature by itself without any extra hardware attached, since the
mjr 17:ab3cec0c8bf4 48 // accelerometer is built in to the KL25Z board.
mjr 5:a70c0bce770d 49 //
mjr 17:ab3cec0c8bf4 50 // - Nudge sensing via the KL25Z's on-board accelerometer. Nudging the cabinet
mjr 17:ab3cec0c8bf4 51 // causes small accelerations that the accelerometer can detect; these are sent to
mjr 17:ab3cec0c8bf4 52 // Visual Pinball via the joystick interface so that VP can simulate the effect
mjr 17:ab3cec0c8bf4 53 // of the real physical nudges on its simulated ball. VP has native handling for
mjr 17:ab3cec0c8bf4 54 // this type of input, so all you have to do is set some preferences in VP to tell
mjr 17:ab3cec0c8bf4 55 // it that an accelerometer is attached.
mjr 5:a70c0bce770d 56 //
mjr 5:a70c0bce770d 57 // - Plunger position sensing via an attached TAOS TSL 1410R CCD linear array sensor.
mjr 17:ab3cec0c8bf4 58 // To use this feature, you need to buy the TAOS device (it's not built in to the
mjr 17:ab3cec0c8bf4 59 // KL25Z, obviously), wire it to the KL25Z (5 wire connections between the two
mjr 17:ab3cec0c8bf4 60 // devices are required), and mount the TAOS sensor in your cabinet so that it's
mjr 17:ab3cec0c8bf4 61 // positioned properly to capture images of the physical plunger shooter rod.
mjr 17:ab3cec0c8bf4 62 //
mjr 17:ab3cec0c8bf4 63 // The physical mounting and wiring details are desribed in the project
mjr 17:ab3cec0c8bf4 64 // documentation.
mjr 17:ab3cec0c8bf4 65 //
mjr 17:ab3cec0c8bf4 66 // If the CCD is attached, the software constantly captures images from the CCD
mjr 17:ab3cec0c8bf4 67 // and analyzes them to determine how far back the plunger is pulled. It reports
mjr 17:ab3cec0c8bf4 68 // this to Visual Pinball via the joystick interface. This allows VP to make the
mjr 17:ab3cec0c8bf4 69 // simulated on-screen plunger track the motion of the physical plunger in real
mjr 17:ab3cec0c8bf4 70 // time. As with the nudge data, VP has native handling for the plunger input,
mjr 17:ab3cec0c8bf4 71 // so you just need to set the VP preferences to tell it that an analog plunger
mjr 17:ab3cec0c8bf4 72 // device is attached. One caveat, though: although VP itself has built-in
mjr 17:ab3cec0c8bf4 73 // support for an analog plunger, not all existing tables take advantage of it.
mjr 17:ab3cec0c8bf4 74 // Many existing tables have their own custom plunger scripting that doesn't
mjr 17:ab3cec0c8bf4 75 // cooperate with the VP plunger input. All tables *can* be made to work with
mjr 17:ab3cec0c8bf4 76 // the plunger, and in most cases it only requires some simple script editing,
mjr 17:ab3cec0c8bf4 77 // but in some cases it requires some more extensive surgery.
mjr 5:a70c0bce770d 78 //
mjr 6:cc35eb643e8f 79 // For best results, the plunger sensor should be calibrated. The calibration
mjr 6:cc35eb643e8f 80 // is stored in non-volatile memory on board the KL25Z, so it's only necessary
mjr 6:cc35eb643e8f 81 // to do the calibration once, when you first install everything. (You might
mjr 6:cc35eb643e8f 82 // also want to re-calibrate if you physically remove and reinstall the CCD
mjr 17:ab3cec0c8bf4 83 // sensor or the mechanical plunger, since their alignment shift change slightly
mjr 17:ab3cec0c8bf4 84 // when you put everything back together.) You can optionally install a
mjr 17:ab3cec0c8bf4 85 // dedicated momentary switch or pushbutton to activate the calibration mode;
mjr 17:ab3cec0c8bf4 86 // this is describe in the project documentation. If you don't want to bother
mjr 17:ab3cec0c8bf4 87 // with the extra button, you can also trigger calibration using the Windows
mjr 17:ab3cec0c8bf4 88 // setup software, which you can find on the Pinscape project page.
mjr 6:cc35eb643e8f 89 //
mjr 17:ab3cec0c8bf4 90 // The calibration procedure is described in the project documentation. Briefly,
mjr 17:ab3cec0c8bf4 91 // when you trigger calibration mode, the software will scan the CCD for about
mjr 17:ab3cec0c8bf4 92 // 15 seconds, during which you should simply pull the physical plunger back
mjr 17:ab3cec0c8bf4 93 // all the way, hold it for a moment, and then slowly return it to the rest
mjr 17:ab3cec0c8bf4 94 // position. (DON'T just release it from the retracted position, since that
mjr 17:ab3cec0c8bf4 95 // let it shoot forward too far. We want to measure the range from the park
mjr 17:ab3cec0c8bf4 96 // position to the fully retracted position only.)
mjr 5:a70c0bce770d 97 //
mjr 13:72dda449c3c0 98 // - Button input wiring. 24 of the KL25Z's GPIO ports are mapped as digital inputs
mjr 13:72dda449c3c0 99 // for buttons and switches. The software reports these as joystick buttons when
mjr 13:72dda449c3c0 100 // it sends reports to the PC. These can be used to wire physical pinball-style
mjr 13:72dda449c3c0 101 // buttons in the cabinet (e.g., flipper buttons, the Start button) and miscellaneous
mjr 13:72dda449c3c0 102 // switches (such as a tilt bob) to the PC. Visual Pinball can use joystick buttons
mjr 13:72dda449c3c0 103 // for input - you just have to assign a VP function to each button using VP's
mjr 13:72dda449c3c0 104 // keyboard options dialog. To wire a button physically, connect one terminal of
mjr 13:72dda449c3c0 105 // the button switch to the KL25Z ground, and connect the other terminal to the
mjr 13:72dda449c3c0 106 // the GPIO port you wish to assign to the button. See the buttonMap[] array
mjr 13:72dda449c3c0 107 // below for the available GPIO ports and their assigned joystick button numbers.
mjr 13:72dda449c3c0 108 // If you're not using a GPIO port, you can just leave it unconnected - the digital
mjr 13:72dda449c3c0 109 // inputs have built-in pull-up resistors, so an unconnected port is the same as
mjr 13:72dda449c3c0 110 // an open switch (an "off" state for the button).
mjr 13:72dda449c3c0 111 //
mjr 5:a70c0bce770d 112 // - LedWiz emulation. The KL25Z can appear to the PC as an LedWiz device, and will
mjr 5:a70c0bce770d 113 // accept and process LedWiz commands from the host. The software can turn digital
mjr 5:a70c0bce770d 114 // output ports on and off, and can set varying PWM intensitiy levels on a subset
mjr 5:a70c0bce770d 115 // of ports. (The KL25Z can only provide 6 PWM ports. Intensity level settings on
mjr 5:a70c0bce770d 116 // other ports is ignored, so non-PWM ports can only be used for simple on/off
mjr 5:a70c0bce770d 117 // devices such as contactors and solenoids.) The KL25Z can only supply 4mA on its
mjr 5:a70c0bce770d 118 // output ports, so external hardware is required to take advantage of the LedWiz
mjr 5:a70c0bce770d 119 // emulation. Many different hardware designs are possible, but there's a simple
mjr 5:a70c0bce770d 120 // reference design in the documentation that uses a Darlington array IC to
mjr 5:a70c0bce770d 121 // increase the output from each port to 500mA (the same level as the LedWiz),
mjr 5:a70c0bce770d 122 // plus an extended design that adds an optocoupler and MOSFET to provide very
mjr 5:a70c0bce770d 123 // high power handling, up to about 45A or 150W, with voltages up to 100V.
mjr 5:a70c0bce770d 124 // That will handle just about any DC device directly (wtihout relays or other
mjr 5:a70c0bce770d 125 // amplifiers), and switches fast enough to support PWM devices.
mjr 5:a70c0bce770d 126 //
mjr 5:a70c0bce770d 127 // The device can report any desired LedWiz unit number to the host, which makes
mjr 5:a70c0bce770d 128 // it possible to use the LedWiz emulation on a machine that also has one or more
mjr 5:a70c0bce770d 129 // actual LedWiz devices intalled. The LedWiz design allows for up to 16 units
mjr 5:a70c0bce770d 130 // to be installed in one machine - each one is invidually addressable by its
mjr 5:a70c0bce770d 131 // distinct unit number.
mjr 5:a70c0bce770d 132 //
mjr 5:a70c0bce770d 133 // The LedWiz emulation features are of course optional. There's no need to
mjr 5:a70c0bce770d 134 // build any of the external port hardware (or attach anything to the output
mjr 5:a70c0bce770d 135 // ports at all) if the LedWiz features aren't needed. Most people won't have
mjr 5:a70c0bce770d 136 // any use for the LedWiz features. I built them mostly as a learning exercise,
mjr 5:a70c0bce770d 137 // but with a slight practical need for a handful of extra ports (I'm using the
mjr 5:a70c0bce770d 138 // cutting-edge 10-contactor setup, so my real LedWiz is full!).
mjr 6:cc35eb643e8f 139 //
mjr 6:cc35eb643e8f 140 // The on-board LED on the KL25Z flashes to indicate the current device status:
mjr 6:cc35eb643e8f 141 //
mjr 6:cc35eb643e8f 142 // two short red flashes = the device is powered but hasn't successfully
mjr 6:cc35eb643e8f 143 // connected to the host via USB (either it's not physically connected
mjr 6:cc35eb643e8f 144 // to the USB port, or there was a problem with the software handshake
mjr 6:cc35eb643e8f 145 // with the USB device driver on the computer)
mjr 6:cc35eb643e8f 146 //
mjr 6:cc35eb643e8f 147 // short red flash = the host computer is in sleep/suspend mode
mjr 6:cc35eb643e8f 148 //
mjr 6:cc35eb643e8f 149 // long red/green = the LedWiz unti number has been changed, so a reset
mjr 6:cc35eb643e8f 150 // is needed. You can simply unplug the device and plug it back in,
mjr 6:cc35eb643e8f 151 // or presss and hold the reset button on the device for a few seconds.
mjr 6:cc35eb643e8f 152 //
mjr 6:cc35eb643e8f 153 // long yellow/green = everything's working, but the plunger hasn't
mjr 6:cc35eb643e8f 154 // been calibrated; follow the calibration procedure described above.
mjr 6:cc35eb643e8f 155 // This flash mode won't appear if the CCD has been disabled. Note
mjr 18:5e890ebd0023 156 // that the device can't tell whether a CCD is physically attached;
mjr 18:5e890ebd0023 157 // if you don't have a CCD attached, you can set the appropriate option
mjr 18:5e890ebd0023 158 // in config.h or use the Windows config tool to disable the CCD
mjr 18:5e890ebd0023 159 // software features.
mjr 6:cc35eb643e8f 160 //
mjr 6:cc35eb643e8f 161 // alternating blue/green = everything's working
mjr 6:cc35eb643e8f 162 //
mjr 6:cc35eb643e8f 163 // Software configuration: you can change option settings by sending special
mjr 6:cc35eb643e8f 164 // USB commands from the PC. I've provided a Windows program for this purpose;
mjr 6:cc35eb643e8f 165 // refer to the documentation for details. For reference, here's the format
mjr 6:cc35eb643e8f 166 // of the USB command for option changes:
mjr 6:cc35eb643e8f 167 //
mjr 6:cc35eb643e8f 168 // length of report = 8 bytes
mjr 6:cc35eb643e8f 169 // byte 0 = 65 (0x41)
mjr 6:cc35eb643e8f 170 // byte 1 = 1 (0x01)
mjr 6:cc35eb643e8f 171 // byte 2 = new LedWiz unit number, 0x01 to 0x0f
mjr 6:cc35eb643e8f 172 // byte 3 = feature enable bit mask:
mjr 6:cc35eb643e8f 173 // 0x01 = enable CCD (default = on)
mjr 9:fd65b0a94720 174 //
mjr 9:fd65b0a94720 175 // Plunger calibration mode: the host can activate plunger calibration mode
mjr 9:fd65b0a94720 176 // by sending this packet. This has the same effect as pressing and holding
mjr 9:fd65b0a94720 177 // the plunger calibration button for two seconds, to allow activating this
mjr 9:fd65b0a94720 178 // mode without attaching a physical button.
mjr 9:fd65b0a94720 179 //
mjr 9:fd65b0a94720 180 // length = 8 bytes
mjr 9:fd65b0a94720 181 // byte 0 = 65 (0x41)
mjr 9:fd65b0a94720 182 // byte 1 = 2 (0x02)
mjr 9:fd65b0a94720 183 //
mjr 10:976666ffa4ef 184 // Exposure reports: the host can request a report of the full set of pixel
mjr 10:976666ffa4ef 185 // values for the next frame by sending this special packet:
mjr 10:976666ffa4ef 186 //
mjr 10:976666ffa4ef 187 // length = 8 bytes
mjr 10:976666ffa4ef 188 // byte 0 = 65 (0x41)
mjr 10:976666ffa4ef 189 // byte 1 = 3 (0x03)
mjr 10:976666ffa4ef 190 //
mjr 10:976666ffa4ef 191 // We'll respond with a series of special reports giving the exposure status.
mjr 10:976666ffa4ef 192 // Each report has the following structure:
mjr 10:976666ffa4ef 193 //
mjr 10:976666ffa4ef 194 // bytes 0:1 = 11-bit index, with high 5 bits set to 10000. For
mjr 10:976666ffa4ef 195 // example, 0x04 0x80 indicates index 4. This is the
mjr 10:976666ffa4ef 196 // starting pixel number in the report. The first report
mjr 10:976666ffa4ef 197 // will be 0x00 0x80 to indicate pixel #0.
mjr 10:976666ffa4ef 198 // bytes 2:3 = 16-bit unsigned int brightness level of pixel at index
mjr 10:976666ffa4ef 199 // bytes 4:5 = brightness of pixel at index+1
mjr 10:976666ffa4ef 200 // etc for the rest of the packet
mjr 10:976666ffa4ef 201 //
mjr 10:976666ffa4ef 202 // This still has the form of a joystick packet at the USB level, but
mjr 10:976666ffa4ef 203 // can be differentiated by the host via the status bits. It would have
mjr 10:976666ffa4ef 204 // been cleaner to use a different Report ID at the USB level, but this
mjr 10:976666ffa4ef 205 // would have necessitated a different container structure in the report
mjr 10:976666ffa4ef 206 // descriptor, which would have broken LedWiz compatibility. Given that
mjr 10:976666ffa4ef 207 // constraint, we have to re-use the joystick report type, making for
mjr 10:976666ffa4ef 208 // this somewhat kludgey approach.
mjr 6:cc35eb643e8f 209
mjr 0:5acbbe3f4cf4 210 #include "mbed.h"
mjr 6:cc35eb643e8f 211 #include "math.h"
mjr 0:5acbbe3f4cf4 212 #include "USBJoystick.h"
mjr 0:5acbbe3f4cf4 213 #include "MMA8451Q.h"
mjr 1:d913e0afb2ac 214 #include "tsl1410r.h"
mjr 1:d913e0afb2ac 215 #include "FreescaleIAP.h"
mjr 2:c174f9ee414a 216 #include "crc32.h"
mjr 2:c174f9ee414a 217
mjr 17:ab3cec0c8bf4 218 // our local configuration file
mjr 17:ab3cec0c8bf4 219 #include "config.h"
mjr 17:ab3cec0c8bf4 220
mjr 5:a70c0bce770d 221
mjr 5:a70c0bce770d 222 // ---------------------------------------------------------------------------
mjr 17:ab3cec0c8bf4 223 // utilities
mjr 17:ab3cec0c8bf4 224
mjr 17:ab3cec0c8bf4 225 // number of elements in an array
mjr 17:ab3cec0c8bf4 226 #define countof(x) (sizeof(x)/sizeof((x)[0]))
mjr 17:ab3cec0c8bf4 227
mjr 17:ab3cec0c8bf4 228
mjr 17:ab3cec0c8bf4 229 // ---------------------------------------------------------------------------
mjr 17:ab3cec0c8bf4 230 // USB device vendor ID, product ID, and version.
mjr 5:a70c0bce770d 231 //
mjr 5:a70c0bce770d 232 // We use the vendor ID for the LedWiz, so that the PC-side software can
mjr 5:a70c0bce770d 233 // identify us as capable of performing LedWiz commands. The LedWiz uses
mjr 5:a70c0bce770d 234 // a product ID value from 0xF0 to 0xFF; the last four bits identify the
mjr 5:a70c0bce770d 235 // unit number (e.g., product ID 0xF7 means unit #7). This allows multiple
mjr 5:a70c0bce770d 236 // LedWiz units to be installed in a single PC; the software on the PC side
mjr 5:a70c0bce770d 237 // uses the unit number to route commands to the devices attached to each
mjr 5:a70c0bce770d 238 // unit. On the real LedWiz, the unit number must be set in the firmware
mjr 5:a70c0bce770d 239 // at the factory; it's not configurable by the end user. Most LedWiz's
mjr 5:a70c0bce770d 240 // ship with the unit number set to 0, but the vendor will set different
mjr 5:a70c0bce770d 241 // unit numbers if requested at the time of purchase. So if you have a
mjr 5:a70c0bce770d 242 // single LedWiz already installed in your cabinet, and you didn't ask for
mjr 5:a70c0bce770d 243 // a non-default unit number, your existing LedWiz will be unit 0.
mjr 5:a70c0bce770d 244 //
mjr 6:cc35eb643e8f 245 // Note that the USB_PRODUCT_ID value set here omits the unit number. We
mjr 6:cc35eb643e8f 246 // take the unit number from the saved configuration. We provide a
mjr 6:cc35eb643e8f 247 // configuration command that can be sent via the USB connection to change
mjr 6:cc35eb643e8f 248 // the unit number, so that users can select the unit number without having
mjr 6:cc35eb643e8f 249 // to install a different version of the software. We'll combine the base
mjr 6:cc35eb643e8f 250 // product ID here with the unit number to get the actual product ID that
mjr 6:cc35eb643e8f 251 // we send to the USB controller.
mjr 5:a70c0bce770d 252 const uint16_t USB_VENDOR_ID = 0xFAFA;
mjr 6:cc35eb643e8f 253 const uint16_t USB_PRODUCT_ID = 0x00F0;
mjr 6:cc35eb643e8f 254 const uint16_t USB_VERSION_NO = 0x0006;
mjr 0:5acbbe3f4cf4 255
mjr 5:a70c0bce770d 256
mjr 6:cc35eb643e8f 257 // Joystick axis report range - we report from -JOYMAX to +JOYMAX
mjr 6:cc35eb643e8f 258 #define JOYMAX 4096
mjr 6:cc35eb643e8f 259
mjr 5:a70c0bce770d 260
mjr 17:ab3cec0c8bf4 261 // --------------------------------------------------------------------------
mjr 17:ab3cec0c8bf4 262 //
mjr 17:ab3cec0c8bf4 263 // Potentiometer configuration
mjr 17:ab3cec0c8bf4 264 //
mjr 17:ab3cec0c8bf4 265 #ifdef POT_SENSOR_ENABLED
mjr 17:ab3cec0c8bf4 266 #define IF_POT(x) x
mjr 17:ab3cec0c8bf4 267 #else
mjr 17:ab3cec0c8bf4 268 #define IF_POT(x)
mjr 17:ab3cec0c8bf4 269 #endif
mjr 9:fd65b0a94720 270
mjr 17:ab3cec0c8bf4 271
mjr 17:ab3cec0c8bf4 272 // ---------------------------------------------------------------------------
mjr 17:ab3cec0c8bf4 273 //
mjr 17:ab3cec0c8bf4 274 // On-board RGB LED elements - we use these for diagnostic displays.
mjr 17:ab3cec0c8bf4 275 //
mjr 17:ab3cec0c8bf4 276 DigitalOut ledR(LED1), ledG(LED2), ledB(LED3);
mjr 17:ab3cec0c8bf4 277
mjr 9:fd65b0a94720 278
mjr 9:fd65b0a94720 279 // ---------------------------------------------------------------------------
mjr 5:a70c0bce770d 280 //
mjr 5:a70c0bce770d 281 // LedWiz emulation
mjr 5:a70c0bce770d 282 //
mjr 5:a70c0bce770d 283
mjr 0:5acbbe3f4cf4 284 static int pbaIdx = 0;
mjr 0:5acbbe3f4cf4 285
mjr 6:cc35eb643e8f 286 // LedWiz output pin interface. We create a cover class to virtualize
mjr 6:cc35eb643e8f 287 // digital vs PWM outputs and give them a common interface. The KL25Z
mjr 6:cc35eb643e8f 288 // unfortunately doesn't have enough hardware PWM channels to support
mjr 6:cc35eb643e8f 289 // PWM on all 32 LedWiz outputs, so we provide as many PWM channels as
mjr 6:cc35eb643e8f 290 // we can (10), and fill out the rest of the outputs with plain digital
mjr 6:cc35eb643e8f 291 // outs.
mjr 6:cc35eb643e8f 292 class LwOut
mjr 6:cc35eb643e8f 293 {
mjr 6:cc35eb643e8f 294 public:
mjr 6:cc35eb643e8f 295 virtual void set(float val) = 0;
mjr 6:cc35eb643e8f 296 };
mjr 6:cc35eb643e8f 297 class LwPwmOut: public LwOut
mjr 6:cc35eb643e8f 298 {
mjr 6:cc35eb643e8f 299 public:
mjr 13:72dda449c3c0 300 LwPwmOut(PinName pin) : p(pin) { prv = -1; }
mjr 13:72dda449c3c0 301 virtual void set(float val)
mjr 13:72dda449c3c0 302 {
mjr 13:72dda449c3c0 303 if (val != prv)
mjr 13:72dda449c3c0 304 p.write(prv = val);
mjr 13:72dda449c3c0 305 }
mjr 6:cc35eb643e8f 306 PwmOut p;
mjr 13:72dda449c3c0 307 float prv;
mjr 6:cc35eb643e8f 308 };
mjr 6:cc35eb643e8f 309 class LwDigOut: public LwOut
mjr 6:cc35eb643e8f 310 {
mjr 6:cc35eb643e8f 311 public:
mjr 13:72dda449c3c0 312 LwDigOut(PinName pin) : p(pin) { prv = -1; }
mjr 13:72dda449c3c0 313 virtual void set(float val)
mjr 13:72dda449c3c0 314 {
mjr 13:72dda449c3c0 315 if (val != prv)
mjr 13:72dda449c3c0 316 p.write((prv = val) == 0.0 ? 0 : 1);
mjr 13:72dda449c3c0 317 }
mjr 6:cc35eb643e8f 318 DigitalOut p;
mjr 13:72dda449c3c0 319 float prv;
mjr 6:cc35eb643e8f 320 };
mjr 11:bd9da7088e6e 321 class LwUnusedOut: public LwOut
mjr 11:bd9da7088e6e 322 {
mjr 11:bd9da7088e6e 323 public:
mjr 11:bd9da7088e6e 324 LwUnusedOut() { }
mjr 11:bd9da7088e6e 325 virtual void set(float val) { }
mjr 11:bd9da7088e6e 326 };
mjr 6:cc35eb643e8f 327
mjr 6:cc35eb643e8f 328 // output pin array
mjr 6:cc35eb643e8f 329 static LwOut *lwPin[32];
mjr 6:cc35eb643e8f 330
mjr 6:cc35eb643e8f 331 // initialize the output pin array
mjr 6:cc35eb643e8f 332 void initLwOut()
mjr 6:cc35eb643e8f 333 {
mjr 9:fd65b0a94720 334 for (int i = 0 ; i < countof(lwPin) ; ++i)
mjr 6:cc35eb643e8f 335 {
mjr 11:bd9da7088e6e 336 PinName p = (i < countof(ledWizPortMap) ? ledWizPortMap[i].pin : NC);
mjr 11:bd9da7088e6e 337 if (p == NC)
mjr 11:bd9da7088e6e 338 lwPin[i] = new LwUnusedOut();
mjr 11:bd9da7088e6e 339 else if (ledWizPortMap[i].isPWM)
mjr 11:bd9da7088e6e 340 lwPin[i] = new LwPwmOut(p);
mjr 11:bd9da7088e6e 341 else
mjr 11:bd9da7088e6e 342 lwPin[i] = new LwDigOut(p);
mjr 6:cc35eb643e8f 343 }
mjr 6:cc35eb643e8f 344 }
mjr 6:cc35eb643e8f 345
mjr 0:5acbbe3f4cf4 346 // on/off state for each LedWiz output
mjr 1:d913e0afb2ac 347 static uint8_t wizOn[32];
mjr 0:5acbbe3f4cf4 348
mjr 0:5acbbe3f4cf4 349 // profile (brightness/blink) state for each LedWiz output
mjr 1:d913e0afb2ac 350 static uint8_t wizVal[32] = {
mjr 13:72dda449c3c0 351 48, 48, 48, 48, 48, 48, 48, 48,
mjr 13:72dda449c3c0 352 48, 48, 48, 48, 48, 48, 48, 48,
mjr 13:72dda449c3c0 353 48, 48, 48, 48, 48, 48, 48, 48,
mjr 13:72dda449c3c0 354 48, 48, 48, 48, 48, 48, 48, 48
mjr 0:5acbbe3f4cf4 355 };
mjr 0:5acbbe3f4cf4 356
mjr 1:d913e0afb2ac 357 static float wizState(int idx)
mjr 0:5acbbe3f4cf4 358 {
mjr 13:72dda449c3c0 359 if (wizOn[idx])
mjr 13:72dda449c3c0 360 {
mjr 0:5acbbe3f4cf4 361 // on - map profile brightness state to PWM level
mjr 1:d913e0afb2ac 362 uint8_t val = wizVal[idx];
mjr 13:72dda449c3c0 363 if (val <= 48)
mjr 13:72dda449c3c0 364 {
mjr 15:944bbc29c4dd 365 // PWM brightness/intensity level. Rescale from the LedWiz
mjr 15:944bbc29c4dd 366 // 0..48 integer range to our internal PwmOut 0..1 float range.
mjr 15:944bbc29c4dd 367 // Note that on the actual LedWiz, level 48 is actually about
mjr 15:944bbc29c4dd 368 // 98% on - contrary to the LedWiz documentation, level 49 is
mjr 15:944bbc29c4dd 369 // the true 100% level. (In the documentation, level 49 is
mjr 15:944bbc29c4dd 370 // simply not a valid setting.) Even so, we treat level 48 as
mjr 15:944bbc29c4dd 371 // 100% on to match the documentation. This won't be perfectly
mjr 15:944bbc29c4dd 372 // ocmpatible with the actual LedWiz, but it makes for such a
mjr 15:944bbc29c4dd 373 // small difference in brightness (if the output device is an
mjr 15:944bbc29c4dd 374 // LED, say) that no one should notice. It seems better to
mjr 15:944bbc29c4dd 375 // err in this direction, because while the difference in
mjr 15:944bbc29c4dd 376 // brightness when attached to an LED won't be noticeable, the
mjr 15:944bbc29c4dd 377 // difference in duty cycle when attached to something like a
mjr 15:944bbc29c4dd 378 // contactor *can* be noticeable - anything less than 100%
mjr 15:944bbc29c4dd 379 // can cause a contactor or relay to chatter. There's almost
mjr 15:944bbc29c4dd 380 // never a situation where you'd want values other than 0% and
mjr 15:944bbc29c4dd 381 // 100% for a contactor or relay, so treating level 48 as 100%
mjr 15:944bbc29c4dd 382 // makes us work properly with software that's expecting the
mjr 15:944bbc29c4dd 383 // documented LedWiz behavior and therefore uses level 48 to
mjr 15:944bbc29c4dd 384 // turn a contactor or relay fully on.
mjr 13:72dda449c3c0 385 return val/48.0;
mjr 13:72dda449c3c0 386 }
mjr 13:72dda449c3c0 387 else if (val == 49)
mjr 13:72dda449c3c0 388 {
mjr 15:944bbc29c4dd 389 // 49 is undefined in the LedWiz documentation, but actually
mjr 15:944bbc29c4dd 390 // means 100% on. The documentation says that levels 1-48 are
mjr 15:944bbc29c4dd 391 // the full PWM range, but empirically it appears that the real
mjr 15:944bbc29c4dd 392 // range implemented in the firmware is 1-49. Some software on
mjr 15:944bbc29c4dd 393 // the PC side (notably DOF) is aware of this and uses level 49
mjr 15:944bbc29c4dd 394 // to mean "100% on". To ensure compatibility with existing
mjr 15:944bbc29c4dd 395 // PC-side software, we need to recognize level 49.
mjr 13:72dda449c3c0 396 return 1.0;
mjr 13:72dda449c3c0 397 }
mjr 0:5acbbe3f4cf4 398 else if (val >= 129 && val <= 132)
mjr 13:72dda449c3c0 399 {
mjr 13:72dda449c3c0 400 // Values of 129-132 select different flashing modes. We don't
mjr 13:72dda449c3c0 401 // support any of these. Instead, simply treat them as fully on.
mjr 13:72dda449c3c0 402 // Note that DOF doesn't ever use modes 129-132, as it implements
mjr 13:72dda449c3c0 403 // all flashing modes itself on the host side, so this limitation
mjr 13:72dda449c3c0 404 // won't have any effect on DOF users. You can observe it using
mjr 13:72dda449c3c0 405 // LedBlinky, though.
mjr 13:72dda449c3c0 406 return 1.0;
mjr 13:72dda449c3c0 407 }
mjr 0:5acbbe3f4cf4 408 else
mjr 13:72dda449c3c0 409 {
mjr 13:72dda449c3c0 410 // Other values are undefined in the LedWiz documentation. Hosts
mjr 13:72dda449c3c0 411 // *should* never send undefined values, since whatever behavior an
mjr 13:72dda449c3c0 412 // LedWiz unit exhibits in response is accidental and could change
mjr 13:72dda449c3c0 413 // in a future version. We'll treat all undefined values as equivalent
mjr 13:72dda449c3c0 414 // to 48 (fully on).
mjr 13:72dda449c3c0 415 //
mjr 13:72dda449c3c0 416 // NB: the 49 and 129-132 cases are broken out above for the sake
mjr 13:72dda449c3c0 417 // of documentation. We end up using 1.0 as the return value for
mjr 13:72dda449c3c0 418 // everything outside of the defined 0-48 range, so we could collapse
mjr 13:72dda449c3c0 419 // this whole thing to a single 'else' branch, but I wanted to call
mjr 13:72dda449c3c0 420 // out the specific reasons for handling the settings above as we do.
mjr 0:5acbbe3f4cf4 421 return 1.0;
mjr 13:72dda449c3c0 422 }
mjr 0:5acbbe3f4cf4 423 }
mjr 13:72dda449c3c0 424 else
mjr 13:72dda449c3c0 425 {
mjr 13:72dda449c3c0 426 // off - show at 0 intensity
mjr 13:72dda449c3c0 427 return 0.0;
mjr 0:5acbbe3f4cf4 428 }
mjr 0:5acbbe3f4cf4 429 }
mjr 0:5acbbe3f4cf4 430
mjr 1:d913e0afb2ac 431 static void updateWizOuts()
mjr 1:d913e0afb2ac 432 {
mjr 6:cc35eb643e8f 433 for (int i = 0 ; i < 32 ; ++i)
mjr 6:cc35eb643e8f 434 lwPin[i]->set(wizState(i));
mjr 1:d913e0afb2ac 435 }
mjr 1:d913e0afb2ac 436
mjr 11:bd9da7088e6e 437
mjr 11:bd9da7088e6e 438 // ---------------------------------------------------------------------------
mjr 11:bd9da7088e6e 439 //
mjr 11:bd9da7088e6e 440 // Button input
mjr 11:bd9da7088e6e 441 //
mjr 11:bd9da7088e6e 442
mjr 11:bd9da7088e6e 443 // button input map array
mjr 11:bd9da7088e6e 444 DigitalIn *buttonDigIn[32];
mjr 11:bd9da7088e6e 445
mjr 18:5e890ebd0023 446 // button state
mjr 18:5e890ebd0023 447 struct ButtonState
mjr 18:5e890ebd0023 448 {
mjr 18:5e890ebd0023 449 // current on/off state
mjr 18:5e890ebd0023 450 int pressed;
mjr 18:5e890ebd0023 451
mjr 18:5e890ebd0023 452 // Sticky time remaining for current state. When a
mjr 18:5e890ebd0023 453 // state transition occurs, we set this to a debounce
mjr 18:5e890ebd0023 454 // period. Future state transitions will be ignored
mjr 18:5e890ebd0023 455 // until the debounce time elapses.
mjr 18:5e890ebd0023 456 int t;
mjr 18:5e890ebd0023 457 } buttonState[32];
mjr 18:5e890ebd0023 458
mjr 12:669df364a565 459 // timer for button reports
mjr 12:669df364a565 460 static Timer buttonTimer;
mjr 12:669df364a565 461
mjr 11:bd9da7088e6e 462 // initialize the button inputs
mjr 11:bd9da7088e6e 463 void initButtons()
mjr 11:bd9da7088e6e 464 {
mjr 11:bd9da7088e6e 465 // create the digital inputs
mjr 11:bd9da7088e6e 466 for (int i = 0 ; i < countof(buttonDigIn) ; ++i)
mjr 11:bd9da7088e6e 467 {
mjr 11:bd9da7088e6e 468 if (i < countof(buttonMap) && buttonMap[i] != NC)
mjr 11:bd9da7088e6e 469 buttonDigIn[i] = new DigitalIn(buttonMap[i]);
mjr 11:bd9da7088e6e 470 else
mjr 11:bd9da7088e6e 471 buttonDigIn[i] = 0;
mjr 11:bd9da7088e6e 472 }
mjr 12:669df364a565 473
mjr 12:669df364a565 474 // start the button timer
mjr 12:669df364a565 475 buttonTimer.start();
mjr 11:bd9da7088e6e 476 }
mjr 11:bd9da7088e6e 477
mjr 11:bd9da7088e6e 478
mjr 18:5e890ebd0023 479 // read the button input state
mjr 18:5e890ebd0023 480 uint32_t readButtons()
mjr 11:bd9da7088e6e 481 {
mjr 11:bd9da7088e6e 482 // start with all buttons off
mjr 11:bd9da7088e6e 483 uint32_t buttons = 0;
mjr 11:bd9da7088e6e 484
mjr 18:5e890ebd0023 485 // figure the time elapsed since the last scan
mjr 18:5e890ebd0023 486 int dt = buttonTimer.read_ms();
mjr 18:5e890ebd0023 487
mjr 18:5e890ebd0023 488 // reset the timef for the next scan
mjr 18:5e890ebd0023 489 buttonTimer.reset();
mjr 18:5e890ebd0023 490
mjr 11:bd9da7088e6e 491 // scan the button list
mjr 11:bd9da7088e6e 492 uint32_t bit = 1;
mjr 18:5e890ebd0023 493 DigitalIn **di = buttonDigIn;
mjr 18:5e890ebd0023 494 ButtonState *bs = buttonState;
mjr 18:5e890ebd0023 495 for (int i = 0 ; i < countof(buttonDigIn) ; ++i, ++di, ++bs, bit <<= 1)
mjr 11:bd9da7088e6e 496 {
mjr 18:5e890ebd0023 497 // read this button
mjr 18:5e890ebd0023 498 if (*di != 0)
mjr 18:5e890ebd0023 499 {
mjr 18:5e890ebd0023 500 // deduct the elapsed time since the last update
mjr 18:5e890ebd0023 501 // from the button's remaining sticky time
mjr 18:5e890ebd0023 502 bs->t -= dt;
mjr 18:5e890ebd0023 503 if (bs->t < 0)
mjr 18:5e890ebd0023 504 bs->t = 0;
mjr 18:5e890ebd0023 505
mjr 18:5e890ebd0023 506 // If the sticky time has elapsed, note the new physical
mjr 18:5e890ebd0023 507 // state of the button. If we still have sticky time
mjr 18:5e890ebd0023 508 // remaining, ignore the physical state; the last state
mjr 18:5e890ebd0023 509 // change persists until the sticky time elapses so that
mjr 18:5e890ebd0023 510 // we smooth out any "bounce" (electrical transients that
mjr 18:5e890ebd0023 511 // occur when the switch contact is opened or closed).
mjr 18:5e890ebd0023 512 if (bs->t == 0)
mjr 18:5e890ebd0023 513 {
mjr 18:5e890ebd0023 514 // get the new physical state
mjr 18:5e890ebd0023 515 int pressed = !(*di)->read();
mjr 18:5e890ebd0023 516
mjr 18:5e890ebd0023 517 // update the button's logical state if this is a change
mjr 18:5e890ebd0023 518 if (pressed != bs->pressed)
mjr 18:5e890ebd0023 519 {
mjr 18:5e890ebd0023 520 // store the new state
mjr 18:5e890ebd0023 521 bs->pressed = pressed;
mjr 18:5e890ebd0023 522
mjr 18:5e890ebd0023 523 // start a new sticky period for debouncing this
mjr 18:5e890ebd0023 524 // state change
mjr 19:054f8af32fce 525 bs->t = 25;
mjr 18:5e890ebd0023 526 }
mjr 18:5e890ebd0023 527 }
mjr 18:5e890ebd0023 528
mjr 18:5e890ebd0023 529 // if it's pressed, OR its bit into the state
mjr 18:5e890ebd0023 530 if (bs->pressed)
mjr 18:5e890ebd0023 531 buttons |= bit;
mjr 18:5e890ebd0023 532 }
mjr 11:bd9da7088e6e 533 }
mjr 11:bd9da7088e6e 534
mjr 18:5e890ebd0023 535 // return the new button list
mjr 11:bd9da7088e6e 536 return buttons;
mjr 11:bd9da7088e6e 537 }
mjr 11:bd9da7088e6e 538
mjr 5:a70c0bce770d 539 // ---------------------------------------------------------------------------
mjr 5:a70c0bce770d 540 //
mjr 5:a70c0bce770d 541 // Customization joystick subbclass
mjr 5:a70c0bce770d 542 //
mjr 5:a70c0bce770d 543
mjr 5:a70c0bce770d 544 class MyUSBJoystick: public USBJoystick
mjr 5:a70c0bce770d 545 {
mjr 5:a70c0bce770d 546 public:
mjr 5:a70c0bce770d 547 MyUSBJoystick(uint16_t vendor_id, uint16_t product_id, uint16_t product_release)
mjr 5:a70c0bce770d 548 : USBJoystick(vendor_id, product_id, product_release, true)
mjr 5:a70c0bce770d 549 {
mjr 5:a70c0bce770d 550 suspended_ = false;
mjr 5:a70c0bce770d 551 }
mjr 5:a70c0bce770d 552
mjr 5:a70c0bce770d 553 // are we connected?
mjr 5:a70c0bce770d 554 int isConnected() { return configured(); }
mjr 5:a70c0bce770d 555
mjr 5:a70c0bce770d 556 // Are we in suspend mode?
mjr 5:a70c0bce770d 557 int isSuspended() const { return suspended_; }
mjr 5:a70c0bce770d 558
mjr 5:a70c0bce770d 559 protected:
mjr 5:a70c0bce770d 560 virtual void suspendStateChanged(unsigned int suspended)
mjr 5:a70c0bce770d 561 { suspended_ = suspended; }
mjr 5:a70c0bce770d 562
mjr 5:a70c0bce770d 563 // are we suspended?
mjr 5:a70c0bce770d 564 int suspended_;
mjr 5:a70c0bce770d 565 };
mjr 5:a70c0bce770d 566
mjr 5:a70c0bce770d 567 // ---------------------------------------------------------------------------
mjr 6:cc35eb643e8f 568 //
mjr 6:cc35eb643e8f 569 // Some simple math service routines
mjr 6:cc35eb643e8f 570 //
mjr 6:cc35eb643e8f 571
mjr 6:cc35eb643e8f 572 inline float square(float x) { return x*x; }
mjr 6:cc35eb643e8f 573 inline float round(float x) { return x > 0 ? floor(x + 0.5) : ceil(x - 0.5); }
mjr 6:cc35eb643e8f 574
mjr 6:cc35eb643e8f 575 // ---------------------------------------------------------------------------
mjr 5:a70c0bce770d 576 //
mjr 5:a70c0bce770d 577 // Accelerometer (MMA8451Q)
mjr 5:a70c0bce770d 578 //
mjr 5:a70c0bce770d 579
mjr 5:a70c0bce770d 580 // The MMA8451Q is the KL25Z's on-board 3-axis accelerometer.
mjr 5:a70c0bce770d 581 //
mjr 5:a70c0bce770d 582 // This is a custom wrapper for the library code to interface to the
mjr 6:cc35eb643e8f 583 // MMA8451Q. This class encapsulates an interrupt handler and
mjr 6:cc35eb643e8f 584 // automatic calibration.
mjr 5:a70c0bce770d 585 //
mjr 5:a70c0bce770d 586 // We install an interrupt handler on the accelerometer "data ready"
mjr 6:cc35eb643e8f 587 // interrupt to ensure that we fetch each sample immediately when it
mjr 6:cc35eb643e8f 588 // becomes available. The accelerometer data rate is fiarly high
mjr 6:cc35eb643e8f 589 // (800 Hz), so it's not practical to keep up with it by polling.
mjr 6:cc35eb643e8f 590 // Using an interrupt handler lets us respond quickly and read
mjr 6:cc35eb643e8f 591 // every sample.
mjr 5:a70c0bce770d 592 //
mjr 6:cc35eb643e8f 593 // We automatically calibrate the accelerometer so that it's not
mjr 6:cc35eb643e8f 594 // necessary to get it exactly level when installing it, and so
mjr 6:cc35eb643e8f 595 // that it's also not necessary to calibrate it manually. There's
mjr 6:cc35eb643e8f 596 // lots of experience that tells us that manual calibration is a
mjr 6:cc35eb643e8f 597 // terrible solution, mostly because cabinets tend to shift slightly
mjr 6:cc35eb643e8f 598 // during use, requiring frequent recalibration. Instead, we
mjr 6:cc35eb643e8f 599 // calibrate automatically. We continuously monitor the acceleration
mjr 6:cc35eb643e8f 600 // data, watching for periods of constant (or nearly constant) values.
mjr 6:cc35eb643e8f 601 // Any time it appears that the machine has been at rest for a while
mjr 6:cc35eb643e8f 602 // (about 5 seconds), we'll average the readings during that rest
mjr 6:cc35eb643e8f 603 // period and use the result as the level rest position. This is
mjr 6:cc35eb643e8f 604 // is ongoing, so we'll quickly find the center point again if the
mjr 6:cc35eb643e8f 605 // machine is moved during play (by an especially aggressive bout
mjr 6:cc35eb643e8f 606 // of nudging, say).
mjr 5:a70c0bce770d 607 //
mjr 5:a70c0bce770d 608
mjr 17:ab3cec0c8bf4 609 // I2C address of the accelerometer (this is a constant of the KL25Z)
mjr 17:ab3cec0c8bf4 610 const int MMA8451_I2C_ADDRESS = (0x1d<<1);
mjr 17:ab3cec0c8bf4 611
mjr 17:ab3cec0c8bf4 612 // SCL and SDA pins for the accelerometer (constant for the KL25Z)
mjr 17:ab3cec0c8bf4 613 #define MMA8451_SCL_PIN PTE25
mjr 17:ab3cec0c8bf4 614 #define MMA8451_SDA_PIN PTE24
mjr 17:ab3cec0c8bf4 615
mjr 17:ab3cec0c8bf4 616 // Digital in pin to use for the accelerometer interrupt. For the KL25Z,
mjr 17:ab3cec0c8bf4 617 // this can be either PTA14 or PTA15, since those are the pins physically
mjr 17:ab3cec0c8bf4 618 // wired on this board to the MMA8451 interrupt controller.
mjr 17:ab3cec0c8bf4 619 #define MMA8451_INT_PIN PTA15
mjr 17:ab3cec0c8bf4 620
mjr 17:ab3cec0c8bf4 621
mjr 6:cc35eb643e8f 622 // accelerometer input history item, for gathering calibration data
mjr 6:cc35eb643e8f 623 struct AccHist
mjr 5:a70c0bce770d 624 {
mjr 6:cc35eb643e8f 625 AccHist() { x = y = d = 0.0; xtot = ytot = 0.0; cnt = 0; }
mjr 6:cc35eb643e8f 626 void set(float x, float y, AccHist *prv)
mjr 6:cc35eb643e8f 627 {
mjr 6:cc35eb643e8f 628 // save the raw position
mjr 6:cc35eb643e8f 629 this->x = x;
mjr 6:cc35eb643e8f 630 this->y = y;
mjr 6:cc35eb643e8f 631 this->d = distance(prv);
mjr 6:cc35eb643e8f 632 }
mjr 6:cc35eb643e8f 633
mjr 6:cc35eb643e8f 634 // reading for this entry
mjr 5:a70c0bce770d 635 float x, y;
mjr 5:a70c0bce770d 636
mjr 6:cc35eb643e8f 637 // distance from previous entry
mjr 6:cc35eb643e8f 638 float d;
mjr 5:a70c0bce770d 639
mjr 6:cc35eb643e8f 640 // total and count of samples averaged over this period
mjr 6:cc35eb643e8f 641 float xtot, ytot;
mjr 6:cc35eb643e8f 642 int cnt;
mjr 6:cc35eb643e8f 643
mjr 6:cc35eb643e8f 644 void clearAvg() { xtot = ytot = 0.0; cnt = 0; }
mjr 6:cc35eb643e8f 645 void addAvg(float x, float y) { xtot += x; ytot += y; ++cnt; }
mjr 6:cc35eb643e8f 646 float xAvg() const { return xtot/cnt; }
mjr 6:cc35eb643e8f 647 float yAvg() const { return ytot/cnt; }
mjr 5:a70c0bce770d 648
mjr 6:cc35eb643e8f 649 float distance(AccHist *p)
mjr 6:cc35eb643e8f 650 { return sqrt(square(p->x - x) + square(p->y - y)); }
mjr 5:a70c0bce770d 651 };
mjr 5:a70c0bce770d 652
mjr 5:a70c0bce770d 653 // accelerometer wrapper class
mjr 3:3514575d4f86 654 class Accel
mjr 3:3514575d4f86 655 {
mjr 3:3514575d4f86 656 public:
mjr 3:3514575d4f86 657 Accel(PinName sda, PinName scl, int i2cAddr, PinName irqPin)
mjr 3:3514575d4f86 658 : mma_(sda, scl, i2cAddr), intIn_(irqPin)
mjr 3:3514575d4f86 659 {
mjr 5:a70c0bce770d 660 // remember the interrupt pin assignment
mjr 5:a70c0bce770d 661 irqPin_ = irqPin;
mjr 5:a70c0bce770d 662
mjr 5:a70c0bce770d 663 // reset and initialize
mjr 5:a70c0bce770d 664 reset();
mjr 5:a70c0bce770d 665 }
mjr 5:a70c0bce770d 666
mjr 5:a70c0bce770d 667 void reset()
mjr 5:a70c0bce770d 668 {
mjr 6:cc35eb643e8f 669 // clear the center point
mjr 6:cc35eb643e8f 670 cx_ = cy_ = 0.0;
mjr 6:cc35eb643e8f 671
mjr 6:cc35eb643e8f 672 // start the calibration timer
mjr 5:a70c0bce770d 673 tCenter_.start();
mjr 5:a70c0bce770d 674 iAccPrv_ = nAccPrv_ = 0;
mjr 6:cc35eb643e8f 675
mjr 5:a70c0bce770d 676 // reset and initialize the MMA8451Q
mjr 5:a70c0bce770d 677 mma_.init();
mjr 6:cc35eb643e8f 678
mjr 6:cc35eb643e8f 679 // set the initial integrated velocity reading to zero
mjr 6:cc35eb643e8f 680 vx_ = vy_ = 0;
mjr 3:3514575d4f86 681
mjr 6:cc35eb643e8f 682 // set up our accelerometer interrupt handling
mjr 6:cc35eb643e8f 683 intIn_.rise(this, &Accel::isr);
mjr 5:a70c0bce770d 684 mma_.setInterruptMode(irqPin_ == PTA14 ? 1 : 2);
mjr 3:3514575d4f86 685
mjr 3:3514575d4f86 686 // read the current registers to clear the data ready flag
mjr 6:cc35eb643e8f 687 mma_.getAccXYZ(ax_, ay_, az_);
mjr 3:3514575d4f86 688
mjr 3:3514575d4f86 689 // start our timers
mjr 3:3514575d4f86 690 tGet_.start();
mjr 3:3514575d4f86 691 tInt_.start();
mjr 3:3514575d4f86 692 }
mjr 3:3514575d4f86 693
mjr 9:fd65b0a94720 694 void get(int &x, int &y)
mjr 3:3514575d4f86 695 {
mjr 3:3514575d4f86 696 // disable interrupts while manipulating the shared data
mjr 3:3514575d4f86 697 __disable_irq();
mjr 3:3514575d4f86 698
mjr 3:3514575d4f86 699 // read the shared data and store locally for calculations
mjr 6:cc35eb643e8f 700 float ax = ax_, ay = ay_;
mjr 6:cc35eb643e8f 701 float vx = vx_, vy = vy_;
mjr 5:a70c0bce770d 702
mjr 6:cc35eb643e8f 703 // reset the velocity sum for the next run
mjr 6:cc35eb643e8f 704 vx_ = vy_ = 0;
mjr 3:3514575d4f86 705
mjr 3:3514575d4f86 706 // get the time since the last get() sample
mjr 3:3514575d4f86 707 float dt = tGet_.read_us()/1.0e6;
mjr 3:3514575d4f86 708 tGet_.reset();
mjr 3:3514575d4f86 709
mjr 3:3514575d4f86 710 // done manipulating the shared data
mjr 3:3514575d4f86 711 __enable_irq();
mjr 3:3514575d4f86 712
mjr 6:cc35eb643e8f 713 // adjust the readings for the integration time
mjr 6:cc35eb643e8f 714 vx /= dt;
mjr 6:cc35eb643e8f 715 vy /= dt;
mjr 6:cc35eb643e8f 716
mjr 6:cc35eb643e8f 717 // add this sample to the current calibration interval's running total
mjr 6:cc35eb643e8f 718 AccHist *p = accPrv_ + iAccPrv_;
mjr 6:cc35eb643e8f 719 p->addAvg(ax, ay);
mjr 6:cc35eb643e8f 720
mjr 5:a70c0bce770d 721 // check for auto-centering every so often
mjr 5:a70c0bce770d 722 if (tCenter_.read_ms() > 1000)
mjr 5:a70c0bce770d 723 {
mjr 5:a70c0bce770d 724 // add the latest raw sample to the history list
mjr 6:cc35eb643e8f 725 AccHist *prv = p;
mjr 5:a70c0bce770d 726 iAccPrv_ = (iAccPrv_ + 1) % maxAccPrv;
mjr 6:cc35eb643e8f 727 p = accPrv_ + iAccPrv_;
mjr 6:cc35eb643e8f 728 p->set(ax, ay, prv);
mjr 5:a70c0bce770d 729
mjr 5:a70c0bce770d 730 // if we have a full complement, check for stability
mjr 5:a70c0bce770d 731 if (nAccPrv_ >= maxAccPrv)
mjr 5:a70c0bce770d 732 {
mjr 5:a70c0bce770d 733 // check if we've been stable for all recent samples
mjr 6:cc35eb643e8f 734 static const float accTol = .01;
mjr 6:cc35eb643e8f 735 AccHist *p0 = accPrv_;
mjr 6:cc35eb643e8f 736 if (p0[0].d < accTol
mjr 6:cc35eb643e8f 737 && p0[1].d < accTol
mjr 6:cc35eb643e8f 738 && p0[2].d < accTol
mjr 6:cc35eb643e8f 739 && p0[3].d < accTol
mjr 6:cc35eb643e8f 740 && p0[4].d < accTol)
mjr 5:a70c0bce770d 741 {
mjr 6:cc35eb643e8f 742 // Figure the new calibration point as the average of
mjr 6:cc35eb643e8f 743 // the samples over the rest period
mjr 6:cc35eb643e8f 744 cx_ = (p0[0].xAvg() + p0[1].xAvg() + p0[2].xAvg() + p0[3].xAvg() + p0[4].xAvg())/5.0;
mjr 6:cc35eb643e8f 745 cy_ = (p0[0].yAvg() + p0[1].yAvg() + p0[2].yAvg() + p0[3].yAvg() + p0[4].yAvg())/5.0;
mjr 5:a70c0bce770d 746 }
mjr 5:a70c0bce770d 747 }
mjr 5:a70c0bce770d 748 else
mjr 5:a70c0bce770d 749 {
mjr 5:a70c0bce770d 750 // not enough samples yet; just up the count
mjr 5:a70c0bce770d 751 ++nAccPrv_;
mjr 5:a70c0bce770d 752 }
mjr 6:cc35eb643e8f 753
mjr 6:cc35eb643e8f 754 // clear the new item's running totals
mjr 6:cc35eb643e8f 755 p->clearAvg();
mjr 5:a70c0bce770d 756
mjr 5:a70c0bce770d 757 // reset the timer
mjr 5:a70c0bce770d 758 tCenter_.reset();
mjr 5:a70c0bce770d 759 }
mjr 5:a70c0bce770d 760
mjr 6:cc35eb643e8f 761 // report our integrated velocity reading in x,y
mjr 6:cc35eb643e8f 762 x = rawToReport(vx);
mjr 6:cc35eb643e8f 763 y = rawToReport(vy);
mjr 5:a70c0bce770d 764
mjr 6:cc35eb643e8f 765 #ifdef DEBUG_PRINTF
mjr 6:cc35eb643e8f 766 if (x != 0 || y != 0)
mjr 6:cc35eb643e8f 767 printf("%f %f %d %d %f\r\n", vx, vy, x, y, dt);
mjr 6:cc35eb643e8f 768 #endif
mjr 3:3514575d4f86 769 }
mjr 3:3514575d4f86 770
mjr 3:3514575d4f86 771 private:
mjr 6:cc35eb643e8f 772 // adjust a raw acceleration figure to a usb report value
mjr 6:cc35eb643e8f 773 int rawToReport(float v)
mjr 5:a70c0bce770d 774 {
mjr 6:cc35eb643e8f 775 // scale to the joystick report range and round to integer
mjr 6:cc35eb643e8f 776 int i = int(round(v*JOYMAX));
mjr 5:a70c0bce770d 777
mjr 6:cc35eb643e8f 778 // if it's near the center, scale it roughly as 20*(i/20)^2,
mjr 6:cc35eb643e8f 779 // to suppress noise near the rest position
mjr 6:cc35eb643e8f 780 static const int filter[] = {
mjr 6:cc35eb643e8f 781 -18, -16, -14, -13, -11, -10, -8, -7, -6, -5, -4, -3, -2, -2, -1, -1, 0, 0, 0, 0,
mjr 6:cc35eb643e8f 782 0,
mjr 6:cc35eb643e8f 783 0, 0, 0, 0, 1, 1, 2, 2, 3, 4, 5, 6, 7, 8, 10, 11, 13, 14, 16, 18
mjr 6:cc35eb643e8f 784 };
mjr 6:cc35eb643e8f 785 return (i > 20 || i < -20 ? i : filter[i+20]);
mjr 5:a70c0bce770d 786 }
mjr 5:a70c0bce770d 787
mjr 3:3514575d4f86 788 // interrupt handler
mjr 3:3514575d4f86 789 void isr()
mjr 3:3514575d4f86 790 {
mjr 3:3514575d4f86 791 // Read the axes. Note that we have to read all three axes
mjr 3:3514575d4f86 792 // (even though we only really use x and y) in order to clear
mjr 3:3514575d4f86 793 // the "data ready" status bit in the accelerometer. The
mjr 3:3514575d4f86 794 // interrupt only occurs when the "ready" bit transitions from
mjr 3:3514575d4f86 795 // off to on, so we have to make sure it's off.
mjr 5:a70c0bce770d 796 float x, y, z;
mjr 5:a70c0bce770d 797 mma_.getAccXYZ(x, y, z);
mjr 3:3514575d4f86 798
mjr 3:3514575d4f86 799 // calculate the time since the last interrupt
mjr 3:3514575d4f86 800 float dt = tInt_.read_us()/1.0e6;
mjr 3:3514575d4f86 801 tInt_.reset();
mjr 6:cc35eb643e8f 802
mjr 6:cc35eb643e8f 803 // integrate the time slice from the previous reading to this reading
mjr 6:cc35eb643e8f 804 vx_ += (x + ax_ - 2*cx_)*dt/2;
mjr 6:cc35eb643e8f 805 vy_ += (y + ay_ - 2*cy_)*dt/2;
mjr 3:3514575d4f86 806
mjr 6:cc35eb643e8f 807 // store the updates
mjr 6:cc35eb643e8f 808 ax_ = x;
mjr 6:cc35eb643e8f 809 ay_ = y;
mjr 6:cc35eb643e8f 810 az_ = z;
mjr 3:3514575d4f86 811 }
mjr 3:3514575d4f86 812
mjr 3:3514575d4f86 813 // underlying accelerometer object
mjr 3:3514575d4f86 814 MMA8451Q mma_;
mjr 3:3514575d4f86 815
mjr 5:a70c0bce770d 816 // last raw acceleration readings
mjr 6:cc35eb643e8f 817 float ax_, ay_, az_;
mjr 5:a70c0bce770d 818
mjr 6:cc35eb643e8f 819 // integrated velocity reading since last get()
mjr 6:cc35eb643e8f 820 float vx_, vy_;
mjr 6:cc35eb643e8f 821
mjr 3:3514575d4f86 822 // timer for measuring time between get() samples
mjr 3:3514575d4f86 823 Timer tGet_;
mjr 3:3514575d4f86 824
mjr 3:3514575d4f86 825 // timer for measuring time between interrupts
mjr 3:3514575d4f86 826 Timer tInt_;
mjr 5:a70c0bce770d 827
mjr 6:cc35eb643e8f 828 // Calibration reference point for accelerometer. This is the
mjr 6:cc35eb643e8f 829 // average reading on the accelerometer when in the neutral position
mjr 6:cc35eb643e8f 830 // at rest.
mjr 6:cc35eb643e8f 831 float cx_, cy_;
mjr 5:a70c0bce770d 832
mjr 5:a70c0bce770d 833 // timer for atuo-centering
mjr 5:a70c0bce770d 834 Timer tCenter_;
mjr 6:cc35eb643e8f 835
mjr 6:cc35eb643e8f 836 // Auto-centering history. This is a separate history list that
mjr 6:cc35eb643e8f 837 // records results spaced out sparesely over time, so that we can
mjr 6:cc35eb643e8f 838 // watch for long-lasting periods of rest. When we observe nearly
mjr 6:cc35eb643e8f 839 // no motion for an extended period (on the order of 5 seconds), we
mjr 6:cc35eb643e8f 840 // take this to mean that the cabinet is at rest in its neutral
mjr 6:cc35eb643e8f 841 // position, so we take this as the calibration zero point for the
mjr 6:cc35eb643e8f 842 // accelerometer. We update this history continuously, which allows
mjr 6:cc35eb643e8f 843 // us to continuously re-calibrate the accelerometer. This ensures
mjr 6:cc35eb643e8f 844 // that we'll automatically adjust to any actual changes in the
mjr 6:cc35eb643e8f 845 // cabinet's orientation (e.g., if it gets moved slightly by an
mjr 6:cc35eb643e8f 846 // especially strong nudge) as well as any systematic drift in the
mjr 6:cc35eb643e8f 847 // accelerometer measurement bias (e.g., from temperature changes).
mjr 5:a70c0bce770d 848 int iAccPrv_, nAccPrv_;
mjr 5:a70c0bce770d 849 static const int maxAccPrv = 5;
mjr 6:cc35eb643e8f 850 AccHist accPrv_[maxAccPrv];
mjr 6:cc35eb643e8f 851
mjr 5:a70c0bce770d 852 // interurupt pin name
mjr 5:a70c0bce770d 853 PinName irqPin_;
mjr 5:a70c0bce770d 854
mjr 5:a70c0bce770d 855 // interrupt router
mjr 5:a70c0bce770d 856 InterruptIn intIn_;
mjr 3:3514575d4f86 857 };
mjr 3:3514575d4f86 858
mjr 5:a70c0bce770d 859
mjr 5:a70c0bce770d 860 // ---------------------------------------------------------------------------
mjr 5:a70c0bce770d 861 //
mjr 14:df700b22ca08 862 // Clear the I2C bus for the MMA8451Q. This seems necessary some of the time
mjr 5:a70c0bce770d 863 // for reasons that aren't clear to me. Doing a hard power cycle has the same
mjr 5:a70c0bce770d 864 // effect, but when we do a soft reset, the hardware sometimes seems to leave
mjr 5:a70c0bce770d 865 // the MMA's SDA line stuck low. Forcing a series of 9 clock pulses through
mjr 14:df700b22ca08 866 // the SCL line is supposed to clear this condition. I'm not convinced this
mjr 14:df700b22ca08 867 // actually works with the way this component is wired on the KL25Z, but it
mjr 14:df700b22ca08 868 // seems harmless, so we'll do it on reset in case it does some good. What
mjr 14:df700b22ca08 869 // we really seem to need is a way to power cycle the MMA8451Q if it ever
mjr 14:df700b22ca08 870 // gets stuck, but this is simply not possible in software on the KL25Z.
mjr 14:df700b22ca08 871 //
mjr 14:df700b22ca08 872 // If the accelerometer does get stuck, and a software reboot doesn't reset
mjr 14:df700b22ca08 873 // it, the only workaround is to manually power cycle the whole KL25Z by
mjr 14:df700b22ca08 874 // unplugging both of its USB connections.
mjr 5:a70c0bce770d 875 //
mjr 5:a70c0bce770d 876 void clear_i2c()
mjr 5:a70c0bce770d 877 {
mjr 5:a70c0bce770d 878 // assume a general-purpose output pin to the I2C clock
mjr 5:a70c0bce770d 879 DigitalOut scl(MMA8451_SCL_PIN);
mjr 5:a70c0bce770d 880 DigitalIn sda(MMA8451_SDA_PIN);
mjr 5:a70c0bce770d 881
mjr 5:a70c0bce770d 882 // clock the SCL 9 times
mjr 5:a70c0bce770d 883 for (int i = 0 ; i < 9 ; ++i)
mjr 5:a70c0bce770d 884 {
mjr 5:a70c0bce770d 885 scl = 1;
mjr 5:a70c0bce770d 886 wait_us(20);
mjr 5:a70c0bce770d 887 scl = 0;
mjr 5:a70c0bce770d 888 wait_us(20);
mjr 5:a70c0bce770d 889 }
mjr 5:a70c0bce770d 890 }
mjr 14:df700b22ca08 891
mjr 14:df700b22ca08 892 // ---------------------------------------------------------------------------
mjr 14:df700b22ca08 893 //
mjr 17:ab3cec0c8bf4 894 // Include the appropriate plunger sensor definition. This will define a
mjr 17:ab3cec0c8bf4 895 // class called PlungerSensor, with a standard interface that we use in
mjr 17:ab3cec0c8bf4 896 // the main loop below. This is *kind of* like a virtual class interface,
mjr 17:ab3cec0c8bf4 897 // but it actually defines the methods statically, which is a little more
mjr 17:ab3cec0c8bf4 898 // efficient at run-time. There's no need for a true virtual interface
mjr 17:ab3cec0c8bf4 899 // because we don't need to be able to change sensor types on the fly.
mjr 17:ab3cec0c8bf4 900 //
mjr 17:ab3cec0c8bf4 901
mjr 17:ab3cec0c8bf4 902 #ifdef ENABLE_CCD_SENSOR
mjr 17:ab3cec0c8bf4 903 #include "ccdSensor.h"
mjr 17:ab3cec0c8bf4 904 #elif ENABLE_POT_SENSOR
mjr 17:ab3cec0c8bf4 905 #include "potSensor.h"
mjr 17:ab3cec0c8bf4 906 #else
mjr 17:ab3cec0c8bf4 907 #include "nullSensor.h"
mjr 17:ab3cec0c8bf4 908 #endif
mjr 17:ab3cec0c8bf4 909
mjr 17:ab3cec0c8bf4 910
mjr 17:ab3cec0c8bf4 911 // ---------------------------------------------------------------------------
mjr 17:ab3cec0c8bf4 912 //
mjr 17:ab3cec0c8bf4 913 // Non-volatile memory (NVM)
mjr 17:ab3cec0c8bf4 914 //
mjr 17:ab3cec0c8bf4 915
mjr 17:ab3cec0c8bf4 916 // Structure defining our NVM storage layout. We store a small
mjr 17:ab3cec0c8bf4 917 // amount of persistent data in flash memory to retain calibration
mjr 17:ab3cec0c8bf4 918 // data when powered off.
mjr 17:ab3cec0c8bf4 919 struct NVM
mjr 17:ab3cec0c8bf4 920 {
mjr 17:ab3cec0c8bf4 921 // checksum - we use this to determine if the flash record
mjr 17:ab3cec0c8bf4 922 // has been properly initialized
mjr 17:ab3cec0c8bf4 923 uint32_t checksum;
mjr 17:ab3cec0c8bf4 924
mjr 17:ab3cec0c8bf4 925 // signature value
mjr 17:ab3cec0c8bf4 926 static const uint32_t SIGNATURE = 0x4D4A522A;
mjr 17:ab3cec0c8bf4 927 static const uint16_t VERSION = 0x0003;
mjr 17:ab3cec0c8bf4 928
mjr 17:ab3cec0c8bf4 929 // Is the data structure valid? We test the signature and
mjr 17:ab3cec0c8bf4 930 // checksum to determine if we've been properly stored.
mjr 17:ab3cec0c8bf4 931 int valid() const
mjr 17:ab3cec0c8bf4 932 {
mjr 17:ab3cec0c8bf4 933 return (d.sig == SIGNATURE
mjr 17:ab3cec0c8bf4 934 && d.vsn == VERSION
mjr 17:ab3cec0c8bf4 935 && d.sz == sizeof(NVM)
mjr 17:ab3cec0c8bf4 936 && checksum == CRC32(&d, sizeof(d)));
mjr 17:ab3cec0c8bf4 937 }
mjr 17:ab3cec0c8bf4 938
mjr 17:ab3cec0c8bf4 939 // save to non-volatile memory
mjr 17:ab3cec0c8bf4 940 void save(FreescaleIAP &iap, int addr)
mjr 17:ab3cec0c8bf4 941 {
mjr 17:ab3cec0c8bf4 942 // update the checksum and structure size
mjr 17:ab3cec0c8bf4 943 checksum = CRC32(&d, sizeof(d));
mjr 17:ab3cec0c8bf4 944 d.sz = sizeof(NVM);
mjr 17:ab3cec0c8bf4 945
mjr 17:ab3cec0c8bf4 946 // erase the sector
mjr 17:ab3cec0c8bf4 947 iap.erase_sector(addr);
mjr 17:ab3cec0c8bf4 948
mjr 17:ab3cec0c8bf4 949 // save the data
mjr 17:ab3cec0c8bf4 950 iap.program_flash(addr, this, sizeof(*this));
mjr 17:ab3cec0c8bf4 951 }
mjr 17:ab3cec0c8bf4 952
mjr 17:ab3cec0c8bf4 953 // reset calibration data for calibration mode
mjr 17:ab3cec0c8bf4 954 void resetPlunger()
mjr 17:ab3cec0c8bf4 955 {
mjr 17:ab3cec0c8bf4 956 // set extremes for the calibration data
mjr 17:ab3cec0c8bf4 957 d.plungerMax = 0;
mjr 17:ab3cec0c8bf4 958 d.plungerZero = npix;
mjr 17:ab3cec0c8bf4 959 d.plungerMin = npix;
mjr 17:ab3cec0c8bf4 960 }
mjr 17:ab3cec0c8bf4 961
mjr 17:ab3cec0c8bf4 962 // stored data (excluding the checksum)
mjr 17:ab3cec0c8bf4 963 struct
mjr 17:ab3cec0c8bf4 964 {
mjr 17:ab3cec0c8bf4 965 // Signature, structure version, and structure size - further verification
mjr 17:ab3cec0c8bf4 966 // that we have valid initialized data. The size is a simple proxy for a
mjr 17:ab3cec0c8bf4 967 // structure version, as the most common type of change to the structure as
mjr 17:ab3cec0c8bf4 968 // the software evolves will be the addition of new elements. We also
mjr 17:ab3cec0c8bf4 969 // provide an explicit version number that we can update manually if we
mjr 17:ab3cec0c8bf4 970 // make any changes that don't affect the structure size but would affect
mjr 17:ab3cec0c8bf4 971 // compatibility with a saved record (e.g., swapping two existing elements).
mjr 17:ab3cec0c8bf4 972 uint32_t sig;
mjr 17:ab3cec0c8bf4 973 uint16_t vsn;
mjr 17:ab3cec0c8bf4 974 int sz;
mjr 17:ab3cec0c8bf4 975
mjr 17:ab3cec0c8bf4 976 // has the plunger been manually calibrated?
mjr 17:ab3cec0c8bf4 977 int plungerCal;
mjr 17:ab3cec0c8bf4 978
mjr 17:ab3cec0c8bf4 979 // Plunger calibration min, zero, and max. The zero point is the
mjr 17:ab3cec0c8bf4 980 // rest position (aka park position), where it's in equilibrium between
mjr 17:ab3cec0c8bf4 981 // the main spring and the barrel spring. It can travel a small distance
mjr 17:ab3cec0c8bf4 982 // forward of the rest position, because the barrel spring can be
mjr 17:ab3cec0c8bf4 983 // compressed by the user pushing on the plunger or by the momentum
mjr 17:ab3cec0c8bf4 984 // of a release motion. The minimum is the maximum forward point where
mjr 17:ab3cec0c8bf4 985 // the barrel spring can't be compressed any further.
mjr 17:ab3cec0c8bf4 986 int plungerMin;
mjr 17:ab3cec0c8bf4 987 int plungerZero;
mjr 17:ab3cec0c8bf4 988 int plungerMax;
mjr 17:ab3cec0c8bf4 989
mjr 17:ab3cec0c8bf4 990 // is the plunger sensor enabled?
mjr 17:ab3cec0c8bf4 991 int plungerEnabled;
mjr 17:ab3cec0c8bf4 992
mjr 17:ab3cec0c8bf4 993 // LedWiz unit number
mjr 17:ab3cec0c8bf4 994 uint8_t ledWizUnitNo;
mjr 17:ab3cec0c8bf4 995 } d;
mjr 17:ab3cec0c8bf4 996 };
mjr 17:ab3cec0c8bf4 997
mjr 17:ab3cec0c8bf4 998
mjr 17:ab3cec0c8bf4 999 // ---------------------------------------------------------------------------
mjr 17:ab3cec0c8bf4 1000 //
mjr 5:a70c0bce770d 1001 // Main program loop. This is invoked on startup and runs forever. Our
mjr 5:a70c0bce770d 1002 // main work is to read our devices (the accelerometer and the CCD), process
mjr 5:a70c0bce770d 1003 // the readings into nudge and plunger position data, and send the results
mjr 5:a70c0bce770d 1004 // to the host computer via the USB joystick interface. We also monitor
mjr 5:a70c0bce770d 1005 // the USB connection for incoming LedWiz commands and process those into
mjr 5:a70c0bce770d 1006 // port outputs.
mjr 5:a70c0bce770d 1007 //
mjr 0:5acbbe3f4cf4 1008 int main(void)
mjr 0:5acbbe3f4cf4 1009 {
mjr 1:d913e0afb2ac 1010 // turn off our on-board indicator LED
mjr 4:02c7cd7b2183 1011 ledR = 1;
mjr 4:02c7cd7b2183 1012 ledG = 1;
mjr 4:02c7cd7b2183 1013 ledB = 1;
mjr 1:d913e0afb2ac 1014
mjr 6:cc35eb643e8f 1015 // initialize the LedWiz ports
mjr 6:cc35eb643e8f 1016 initLwOut();
mjr 6:cc35eb643e8f 1017
mjr 11:bd9da7088e6e 1018 // initialize the button input ports
mjr 11:bd9da7088e6e 1019 initButtons();
mjr 11:bd9da7088e6e 1020
mjr 6:cc35eb643e8f 1021 // we don't need a reset yet
mjr 6:cc35eb643e8f 1022 bool needReset = false;
mjr 6:cc35eb643e8f 1023
mjr 5:a70c0bce770d 1024 // clear the I2C bus for the accelerometer
mjr 5:a70c0bce770d 1025 clear_i2c();
mjr 5:a70c0bce770d 1026
mjr 2:c174f9ee414a 1027 // set up a flash memory controller
mjr 2:c174f9ee414a 1028 FreescaleIAP iap;
mjr 2:c174f9ee414a 1029
mjr 2:c174f9ee414a 1030 // use the last sector of flash for our non-volatile memory structure
mjr 2:c174f9ee414a 1031 int flash_addr = (iap.flash_size() - SECTOR_SIZE);
mjr 2:c174f9ee414a 1032 NVM *flash = (NVM *)flash_addr;
mjr 2:c174f9ee414a 1033 NVM cfg;
mjr 2:c174f9ee414a 1034
mjr 2:c174f9ee414a 1035 // check for valid flash
mjr 6:cc35eb643e8f 1036 bool flash_valid = flash->valid();
mjr 2:c174f9ee414a 1037
mjr 2:c174f9ee414a 1038 // if the flash is valid, load it; otherwise initialize to defaults
mjr 2:c174f9ee414a 1039 if (flash_valid) {
mjr 2:c174f9ee414a 1040 memcpy(&cfg, flash, sizeof(cfg));
mjr 6:cc35eb643e8f 1041 printf("Flash restored: plunger cal=%d, min=%d, zero=%d, max=%d\r\n",
mjr 6:cc35eb643e8f 1042 cfg.d.plungerCal, cfg.d.plungerMin, cfg.d.plungerZero, cfg.d.plungerMax);
mjr 2:c174f9ee414a 1043 }
mjr 2:c174f9ee414a 1044 else {
mjr 2:c174f9ee414a 1045 printf("Factory reset\r\n");
mjr 2:c174f9ee414a 1046 cfg.d.sig = cfg.SIGNATURE;
mjr 2:c174f9ee414a 1047 cfg.d.vsn = cfg.VERSION;
mjr 6:cc35eb643e8f 1048 cfg.d.plungerCal = 0;
mjr 17:ab3cec0c8bf4 1049 cfg.d.plungerMin = 0; // assume we can go all the way forward...
mjr 17:ab3cec0c8bf4 1050 cfg.d.plungerMax = npix; // ...and all the way back
mjr 17:ab3cec0c8bf4 1051 cfg.d.plungerZero = npix/6; // the rest position is usually around 1/2" back
mjr 6:cc35eb643e8f 1052 cfg.d.ledWizUnitNo = DEFAULT_LEDWIZ_UNIT_NUMBER;
mjr 17:ab3cec0c8bf4 1053 cfg.d.plungerEnabled = true;
mjr 2:c174f9ee414a 1054 }
mjr 1:d913e0afb2ac 1055
mjr 6:cc35eb643e8f 1056 // Create the joystick USB client. Note that we use the LedWiz unit
mjr 6:cc35eb643e8f 1057 // number from the saved configuration.
mjr 6:cc35eb643e8f 1058 MyUSBJoystick js(
mjr 6:cc35eb643e8f 1059 USB_VENDOR_ID,
mjr 6:cc35eb643e8f 1060 USB_PRODUCT_ID | cfg.d.ledWizUnitNo,
mjr 6:cc35eb643e8f 1061 USB_VERSION_NO);
mjr 17:ab3cec0c8bf4 1062
mjr 17:ab3cec0c8bf4 1063 // last report timer - we use this to throttle reports, since VP
mjr 17:ab3cec0c8bf4 1064 // doesn't want to hear from us more than about every 10ms
mjr 17:ab3cec0c8bf4 1065 Timer reportTimer;
mjr 17:ab3cec0c8bf4 1066 reportTimer.start();
mjr 17:ab3cec0c8bf4 1067
mjr 17:ab3cec0c8bf4 1068 // initialize the calibration buttons, if present
mjr 17:ab3cec0c8bf4 1069 DigitalIn *calBtn = (CAL_BUTTON_PIN == NC ? 0 : new DigitalIn(CAL_BUTTON_PIN));
mjr 17:ab3cec0c8bf4 1070 DigitalOut *calBtnLed = (CAL_BUTTON_LED == NC ? 0 : new DigitalOut(CAL_BUTTON_LED));
mjr 6:cc35eb643e8f 1071
mjr 1:d913e0afb2ac 1072 // plunger calibration button debounce timer
mjr 1:d913e0afb2ac 1073 Timer calBtnTimer;
mjr 1:d913e0afb2ac 1074 calBtnTimer.start();
mjr 1:d913e0afb2ac 1075 int calBtnLit = false;
mjr 1:d913e0afb2ac 1076
mjr 1:d913e0afb2ac 1077 // Calibration button state:
mjr 1:d913e0afb2ac 1078 // 0 = not pushed
mjr 1:d913e0afb2ac 1079 // 1 = pushed, not yet debounced
mjr 1:d913e0afb2ac 1080 // 2 = pushed, debounced, waiting for hold time
mjr 1:d913e0afb2ac 1081 // 3 = pushed, hold time completed - in calibration mode
mjr 1:d913e0afb2ac 1082 int calBtnState = 0;
mjr 1:d913e0afb2ac 1083
mjr 1:d913e0afb2ac 1084 // set up a timer for our heartbeat indicator
mjr 1:d913e0afb2ac 1085 Timer hbTimer;
mjr 1:d913e0afb2ac 1086 hbTimer.start();
mjr 1:d913e0afb2ac 1087 int hb = 0;
mjr 5:a70c0bce770d 1088 uint16_t hbcnt = 0;
mjr 1:d913e0afb2ac 1089
mjr 1:d913e0afb2ac 1090 // set a timer for accelerometer auto-centering
mjr 1:d913e0afb2ac 1091 Timer acTimer;
mjr 1:d913e0afb2ac 1092 acTimer.start();
mjr 1:d913e0afb2ac 1093
mjr 0:5acbbe3f4cf4 1094 // create the accelerometer object
mjr 5:a70c0bce770d 1095 Accel accel(MMA8451_SCL_PIN, MMA8451_SDA_PIN, MMA8451_I2C_ADDRESS, MMA8451_INT_PIN);
mjr 0:5acbbe3f4cf4 1096
mjr 17:ab3cec0c8bf4 1097 // last accelerometer report, in joystick units (we report the nudge
mjr 17:ab3cec0c8bf4 1098 // acceleration via the joystick x & y axes, per the VP convention)
mjr 17:ab3cec0c8bf4 1099 int x = 0, y = 0;
mjr 17:ab3cec0c8bf4 1100
mjr 17:ab3cec0c8bf4 1101 // create our plunger sensor object
mjr 17:ab3cec0c8bf4 1102 PlungerSensor plungerSensor;
mjr 17:ab3cec0c8bf4 1103
mjr 17:ab3cec0c8bf4 1104 // last plunger report position, in 'npix' normalized pixel units
mjr 17:ab3cec0c8bf4 1105 int pos = 0;
mjr 17:ab3cec0c8bf4 1106
mjr 17:ab3cec0c8bf4 1107 // last plunger report, in joystick units (we report the plunger as the
mjr 17:ab3cec0c8bf4 1108 // "z" axis of the joystick, per the VP convention)
mjr 17:ab3cec0c8bf4 1109 int z = 0;
mjr 17:ab3cec0c8bf4 1110
mjr 17:ab3cec0c8bf4 1111 // most recent prior plunger readings, for tracking release events(z0 is
mjr 17:ab3cec0c8bf4 1112 // reading just before the last one we reported, z1 is the one before that,
mjr 17:ab3cec0c8bf4 1113 // z2 the next before that)
mjr 17:ab3cec0c8bf4 1114 int z0 = 0, z1 = 0, z2 = 0;
mjr 17:ab3cec0c8bf4 1115
mjr 17:ab3cec0c8bf4 1116 // Simulated "bounce" position when firing. We model the bounce off of
mjr 17:ab3cec0c8bf4 1117 // the barrel spring when the plunger is released as proportional to the
mjr 17:ab3cec0c8bf4 1118 // distance it was retracted just before being released.
mjr 17:ab3cec0c8bf4 1119 int zBounce = 0;
mjr 2:c174f9ee414a 1120
mjr 17:ab3cec0c8bf4 1121 // Simulated Launch Ball button state. If a "ZB Launch Ball" port is
mjr 17:ab3cec0c8bf4 1122 // defined for our LedWiz port mapping, any time that port is turned ON,
mjr 17:ab3cec0c8bf4 1123 // we'll simulate pushing the Launch Ball button if the player pulls
mjr 17:ab3cec0c8bf4 1124 // back and releases the plunger, or simply pushes on the plunger from
mjr 17:ab3cec0c8bf4 1125 // the rest position. This allows the plunger to be used in lieu of a
mjr 17:ab3cec0c8bf4 1126 // physical Launch Ball button for tables that don't have plungers.
mjr 17:ab3cec0c8bf4 1127 //
mjr 17:ab3cec0c8bf4 1128 // States:
mjr 17:ab3cec0c8bf4 1129 // 0 = default
mjr 17:ab3cec0c8bf4 1130 // 1 = cocked (plunger has been pulled back about 1" from state 0)
mjr 17:ab3cec0c8bf4 1131 // 2 = uncocked (plunger is pulled back less than 1" from state 1)
mjr 17:ab3cec0c8bf4 1132 // 3 = launching (plunger has been released from state 1 or 2, or
mjr 17:ab3cec0c8bf4 1133 // pushed forward about 1/4" from state 0)
mjr 17:ab3cec0c8bf4 1134 // 4 = launching, plunger is no longer pushed forward
mjr 17:ab3cec0c8bf4 1135 int lbState = 0;
mjr 6:cc35eb643e8f 1136
mjr 17:ab3cec0c8bf4 1137 // Time since last lbState transition. Some of the states are time-
mjr 17:ab3cec0c8bf4 1138 // sensitive. In the "uncocked" state, we'll return to state 0 if
mjr 17:ab3cec0c8bf4 1139 // we remain in this state for more than a few milliseconds, since
mjr 17:ab3cec0c8bf4 1140 // it indicates that the plunger is being slowly returned to rest
mjr 17:ab3cec0c8bf4 1141 // rather than released. In the "launching" state, we need to release
mjr 17:ab3cec0c8bf4 1142 // the Launch Ball button after a moment, and we need to wait for
mjr 17:ab3cec0c8bf4 1143 // the plunger to come to rest before returning to state 0.
mjr 17:ab3cec0c8bf4 1144 Timer lbTimer;
mjr 17:ab3cec0c8bf4 1145 lbTimer.start();
mjr 17:ab3cec0c8bf4 1146
mjr 18:5e890ebd0023 1147 // Launch Ball simulated push timer. We start this when we simulate
mjr 18:5e890ebd0023 1148 // the button push, and turn off the simulated button when enough time
mjr 18:5e890ebd0023 1149 // has elapsed.
mjr 18:5e890ebd0023 1150 Timer lbBtnTimer;
mjr 18:5e890ebd0023 1151
mjr 17:ab3cec0c8bf4 1152 // Simulated button states. This is a vector of button states
mjr 17:ab3cec0c8bf4 1153 // for the simulated buttons. We combine this with the physical
mjr 17:ab3cec0c8bf4 1154 // button states on each USB joystick report, so we will report
mjr 17:ab3cec0c8bf4 1155 // a button as pressed if either the physical button is being pressed
mjr 17:ab3cec0c8bf4 1156 // or we're simulating a press on the button. This is used for the
mjr 17:ab3cec0c8bf4 1157 // simulated Launch Ball button.
mjr 17:ab3cec0c8bf4 1158 uint32_t simButtons = 0;
mjr 6:cc35eb643e8f 1159
mjr 6:cc35eb643e8f 1160 // Firing in progress: we set this when we detect the start of rapid
mjr 6:cc35eb643e8f 1161 // plunger movement from a retracted position towards the rest position.
mjr 17:ab3cec0c8bf4 1162 //
mjr 17:ab3cec0c8bf4 1163 // When we detect a firing event, we send VP a series of synthetic
mjr 17:ab3cec0c8bf4 1164 // reports simulating the idealized plunger motion. The actual physical
mjr 17:ab3cec0c8bf4 1165 // motion is much too fast to report to VP; in the time between two USB
mjr 17:ab3cec0c8bf4 1166 // reports, the plunger can shoot all the way forward, rebound off of
mjr 17:ab3cec0c8bf4 1167 // the barrel spring, bounce back part way, and bounce forward again,
mjr 17:ab3cec0c8bf4 1168 // or even do all of this more than once. This means that sampling the
mjr 17:ab3cec0c8bf4 1169 // physical motion at the USB report rate would create a misleading
mjr 17:ab3cec0c8bf4 1170 // picture of the plunger motion, since our samples would catch the
mjr 17:ab3cec0c8bf4 1171 // plunger at random points in this oscillating motion. From the
mjr 17:ab3cec0c8bf4 1172 // user's perspective, the physical action that occurred is simply that
mjr 17:ab3cec0c8bf4 1173 // the plunger was released from a particular distance, so it's this
mjr 17:ab3cec0c8bf4 1174 // high-level event that we want to convey to VP. To do this, we
mjr 17:ab3cec0c8bf4 1175 // synthesize a series of reports to convey an idealized version of
mjr 17:ab3cec0c8bf4 1176 // the release motion that's perfectly synchronized to the VP reports.
mjr 17:ab3cec0c8bf4 1177 // Essentially we pretend that our USB position samples are exactly
mjr 17:ab3cec0c8bf4 1178 // aligned in time with (1) the point of retraction just before the
mjr 17:ab3cec0c8bf4 1179 // user released the plunger, (2) the point of maximum forward motion
mjr 17:ab3cec0c8bf4 1180 // just after the user released the plunger (the point of maximum
mjr 17:ab3cec0c8bf4 1181 // compression as the plunger bounces off of the barrel spring), and
mjr 17:ab3cec0c8bf4 1182 // (3) the plunger coming to rest at the park position. This series
mjr 17:ab3cec0c8bf4 1183 // of reports is synthetic in the sense that it's not what we actually
mjr 17:ab3cec0c8bf4 1184 // see on the CCD at the times of these reports - the true plunger
mjr 17:ab3cec0c8bf4 1185 // position is oscillating at high speed during this period. But at
mjr 17:ab3cec0c8bf4 1186 // the same time it conveys a more faithful picture of the true physical
mjr 17:ab3cec0c8bf4 1187 // motion to VP, and allows VP to reproduce the true physical motion
mjr 17:ab3cec0c8bf4 1188 // more faithfully in its simulation model, by correcting for the
mjr 17:ab3cec0c8bf4 1189 // relatively low sampling rate in the communication path between the
mjr 17:ab3cec0c8bf4 1190 // real plunger and VP's model plunger.
mjr 17:ab3cec0c8bf4 1191 //
mjr 17:ab3cec0c8bf4 1192 // If 'firing' is non-zero, it's the index of our current report in
mjr 17:ab3cec0c8bf4 1193 // the synthetic firing report series.
mjr 9:fd65b0a94720 1194 int firing = 0;
mjr 2:c174f9ee414a 1195
mjr 2:c174f9ee414a 1196 // start the first CCD integration cycle
mjr 17:ab3cec0c8bf4 1197 plungerSensor.init();
mjr 9:fd65b0a94720 1198
mjr 9:fd65b0a94720 1199 // Device status. We report this on each update so that the host config
mjr 9:fd65b0a94720 1200 // tool can detect our current settings. This is a bit mask consisting
mjr 9:fd65b0a94720 1201 // of these bits:
mjr 9:fd65b0a94720 1202 // 0x01 -> plunger sensor enabled
mjr 17:ab3cec0c8bf4 1203 uint16_t statusFlags = (cfg.d.plungerEnabled ? 0x01 : 0x00);
mjr 10:976666ffa4ef 1204
mjr 10:976666ffa4ef 1205 // flag: send a pixel dump after the next read
mjr 10:976666ffa4ef 1206 bool reportPix = false;
mjr 1:d913e0afb2ac 1207
mjr 1:d913e0afb2ac 1208 // we're all set up - now just loop, processing sensor reports and
mjr 1:d913e0afb2ac 1209 // host requests
mjr 0:5acbbe3f4cf4 1210 for (;;)
mjr 0:5acbbe3f4cf4 1211 {
mjr 18:5e890ebd0023 1212 // Look for an incoming report. Process a few input reports in
mjr 18:5e890ebd0023 1213 // a row, but stop after a few so that a barrage of inputs won't
mjr 18:5e890ebd0023 1214 // starve our output event processing.
mjr 0:5acbbe3f4cf4 1215 HID_REPORT report;
mjr 18:5e890ebd0023 1216 for (int rr = 0 ; rr < 4 && js.readNB(&report) ; ++rr)
mjr 0:5acbbe3f4cf4 1217 {
mjr 6:cc35eb643e8f 1218 // all Led-Wiz reports are 8 bytes exactly
mjr 6:cc35eb643e8f 1219 if (report.length == 8)
mjr 1:d913e0afb2ac 1220 {
mjr 6:cc35eb643e8f 1221 uint8_t *data = report.data;
mjr 6:cc35eb643e8f 1222 if (data[0] == 64)
mjr 0:5acbbe3f4cf4 1223 {
mjr 6:cc35eb643e8f 1224 // LWZ-SBA - first four bytes are bit-packed on/off flags
mjr 6:cc35eb643e8f 1225 // for the outputs; 5th byte is the pulse speed (0-7)
mjr 6:cc35eb643e8f 1226 //printf("LWZ-SBA %02x %02x %02x %02x ; %02x\r\n",
mjr 6:cc35eb643e8f 1227 // data[1], data[2], data[3], data[4], data[5]);
mjr 0:5acbbe3f4cf4 1228
mjr 6:cc35eb643e8f 1229 // update all on/off states
mjr 6:cc35eb643e8f 1230 for (int i = 0, bit = 1, ri = 1 ; i < 32 ; ++i, bit <<= 1)
mjr 6:cc35eb643e8f 1231 {
mjr 6:cc35eb643e8f 1232 if (bit == 0x100) {
mjr 6:cc35eb643e8f 1233 bit = 1;
mjr 6:cc35eb643e8f 1234 ++ri;
mjr 6:cc35eb643e8f 1235 }
mjr 6:cc35eb643e8f 1236 wizOn[i] = ((data[ri] & bit) != 0);
mjr 6:cc35eb643e8f 1237 }
mjr 6:cc35eb643e8f 1238
mjr 6:cc35eb643e8f 1239 // update the physical outputs
mjr 1:d913e0afb2ac 1240 updateWizOuts();
mjr 6:cc35eb643e8f 1241
mjr 6:cc35eb643e8f 1242 // reset the PBA counter
mjr 6:cc35eb643e8f 1243 pbaIdx = 0;
mjr 6:cc35eb643e8f 1244 }
mjr 6:cc35eb643e8f 1245 else if (data[0] == 65)
mjr 6:cc35eb643e8f 1246 {
mjr 6:cc35eb643e8f 1247 // Private control message. This isn't an LedWiz message - it's
mjr 6:cc35eb643e8f 1248 // an extension for this device. 65 is an invalid PBA setting,
mjr 6:cc35eb643e8f 1249 // and isn't used for any other LedWiz message, so we appropriate
mjr 6:cc35eb643e8f 1250 // it for our own private use. The first byte specifies the
mjr 6:cc35eb643e8f 1251 // message type.
mjr 6:cc35eb643e8f 1252 if (data[1] == 1)
mjr 6:cc35eb643e8f 1253 {
mjr 9:fd65b0a94720 1254 // 1 = Set Configuration:
mjr 6:cc35eb643e8f 1255 // data[2] = LedWiz unit number (0x00 to 0x0f)
mjr 6:cc35eb643e8f 1256 // data[3] = feature enable bit mask:
mjr 6:cc35eb643e8f 1257 // 0x01 = enable CCD
mjr 6:cc35eb643e8f 1258
mjr 6:cc35eb643e8f 1259 // we'll need a reset if the LedWiz unit number is changing
mjr 6:cc35eb643e8f 1260 uint8_t newUnitNo = data[2] & 0x0f;
mjr 6:cc35eb643e8f 1261 needReset |= (newUnitNo != cfg.d.ledWizUnitNo);
mjr 6:cc35eb643e8f 1262
mjr 6:cc35eb643e8f 1263 // set the configuration parameters from the message
mjr 6:cc35eb643e8f 1264 cfg.d.ledWizUnitNo = newUnitNo;
mjr 17:ab3cec0c8bf4 1265 cfg.d.plungerEnabled = data[3] & 0x01;
mjr 6:cc35eb643e8f 1266
mjr 9:fd65b0a94720 1267 // update the status flags
mjr 9:fd65b0a94720 1268 statusFlags = (statusFlags & ~0x01) | (data[3] & 0x01);
mjr 9:fd65b0a94720 1269
mjr 9:fd65b0a94720 1270 // if the ccd is no longer enabled, use 0 for z reports
mjr 17:ab3cec0c8bf4 1271 if (!cfg.d.plungerEnabled)
mjr 9:fd65b0a94720 1272 z = 0;
mjr 9:fd65b0a94720 1273
mjr 6:cc35eb643e8f 1274 // save the configuration
mjr 6:cc35eb643e8f 1275 cfg.save(iap, flash_addr);
mjr 6:cc35eb643e8f 1276 }
mjr 9:fd65b0a94720 1277 else if (data[1] == 2)
mjr 9:fd65b0a94720 1278 {
mjr 9:fd65b0a94720 1279 // 2 = Calibrate plunger
mjr 9:fd65b0a94720 1280 // (No parameters)
mjr 9:fd65b0a94720 1281
mjr 9:fd65b0a94720 1282 // enter calibration mode
mjr 9:fd65b0a94720 1283 calBtnState = 3;
mjr 9:fd65b0a94720 1284 calBtnTimer.reset();
mjr 9:fd65b0a94720 1285 cfg.resetPlunger();
mjr 9:fd65b0a94720 1286 }
mjr 10:976666ffa4ef 1287 else if (data[1] == 3)
mjr 10:976666ffa4ef 1288 {
mjr 10:976666ffa4ef 1289 // 3 = pixel dump
mjr 10:976666ffa4ef 1290 // (No parameters)
mjr 10:976666ffa4ef 1291 reportPix = true;
mjr 10:976666ffa4ef 1292
mjr 10:976666ffa4ef 1293 // show purple until we finish sending the report
mjr 10:976666ffa4ef 1294 ledR = 0;
mjr 10:976666ffa4ef 1295 ledB = 0;
mjr 10:976666ffa4ef 1296 ledG = 1;
mjr 10:976666ffa4ef 1297 }
mjr 6:cc35eb643e8f 1298 }
mjr 6:cc35eb643e8f 1299 else
mjr 6:cc35eb643e8f 1300 {
mjr 6:cc35eb643e8f 1301 // LWZ-PBA - full state dump; each byte is one output
mjr 6:cc35eb643e8f 1302 // in the current bank. pbaIdx keeps track of the bank;
mjr 6:cc35eb643e8f 1303 // this is incremented implicitly by each PBA message.
mjr 6:cc35eb643e8f 1304 //printf("LWZ-PBA[%d] %02x %02x %02x %02x %02x %02x %02x %02x\r\n",
mjr 6:cc35eb643e8f 1305 // pbaIdx, data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]);
mjr 6:cc35eb643e8f 1306
mjr 6:cc35eb643e8f 1307 // update all output profile settings
mjr 6:cc35eb643e8f 1308 for (int i = 0 ; i < 8 ; ++i)
mjr 6:cc35eb643e8f 1309 wizVal[pbaIdx + i] = data[i];
mjr 6:cc35eb643e8f 1310
mjr 6:cc35eb643e8f 1311 // update the physical LED state if this is the last bank
mjr 6:cc35eb643e8f 1312 if (pbaIdx == 24)
mjr 13:72dda449c3c0 1313 {
mjr 6:cc35eb643e8f 1314 updateWizOuts();
mjr 13:72dda449c3c0 1315 pbaIdx = 0;
mjr 13:72dda449c3c0 1316 }
mjr 13:72dda449c3c0 1317 else
mjr 13:72dda449c3c0 1318 pbaIdx += 8;
mjr 6:cc35eb643e8f 1319 }
mjr 0:5acbbe3f4cf4 1320 }
mjr 0:5acbbe3f4cf4 1321 }
mjr 1:d913e0afb2ac 1322
mjr 1:d913e0afb2ac 1323 // check for plunger calibration
mjr 17:ab3cec0c8bf4 1324 if (calBtn != 0 && !calBtn->read())
mjr 0:5acbbe3f4cf4 1325 {
mjr 1:d913e0afb2ac 1326 // check the state
mjr 1:d913e0afb2ac 1327 switch (calBtnState)
mjr 0:5acbbe3f4cf4 1328 {
mjr 1:d913e0afb2ac 1329 case 0:
mjr 1:d913e0afb2ac 1330 // button not yet pushed - start debouncing
mjr 1:d913e0afb2ac 1331 calBtnTimer.reset();
mjr 1:d913e0afb2ac 1332 calBtnState = 1;
mjr 1:d913e0afb2ac 1333 break;
mjr 1:d913e0afb2ac 1334
mjr 1:d913e0afb2ac 1335 case 1:
mjr 1:d913e0afb2ac 1336 // pushed, not yet debounced - if the debounce time has
mjr 1:d913e0afb2ac 1337 // passed, start the hold period
mjr 9:fd65b0a94720 1338 if (calBtnTimer.read_ms() > 50)
mjr 1:d913e0afb2ac 1339 calBtnState = 2;
mjr 1:d913e0afb2ac 1340 break;
mjr 1:d913e0afb2ac 1341
mjr 1:d913e0afb2ac 1342 case 2:
mjr 1:d913e0afb2ac 1343 // in the hold period - if the button has been held down
mjr 1:d913e0afb2ac 1344 // for the entire hold period, move to calibration mode
mjr 9:fd65b0a94720 1345 if (calBtnTimer.read_ms() > 2050)
mjr 1:d913e0afb2ac 1346 {
mjr 1:d913e0afb2ac 1347 // enter calibration mode
mjr 1:d913e0afb2ac 1348 calBtnState = 3;
mjr 9:fd65b0a94720 1349 calBtnTimer.reset();
mjr 9:fd65b0a94720 1350 cfg.resetPlunger();
mjr 1:d913e0afb2ac 1351 }
mjr 1:d913e0afb2ac 1352 break;
mjr 2:c174f9ee414a 1353
mjr 2:c174f9ee414a 1354 case 3:
mjr 9:fd65b0a94720 1355 // Already in calibration mode - pushing the button here
mjr 9:fd65b0a94720 1356 // doesn't change the current state, but we won't leave this
mjr 9:fd65b0a94720 1357 // state as long as it's held down. So nothing changes here.
mjr 2:c174f9ee414a 1358 break;
mjr 0:5acbbe3f4cf4 1359 }
mjr 0:5acbbe3f4cf4 1360 }
mjr 1:d913e0afb2ac 1361 else
mjr 1:d913e0afb2ac 1362 {
mjr 2:c174f9ee414a 1363 // Button released. If we're in calibration mode, and
mjr 2:c174f9ee414a 1364 // the calibration time has elapsed, end the calibration
mjr 2:c174f9ee414a 1365 // and save the results to flash.
mjr 2:c174f9ee414a 1366 //
mjr 2:c174f9ee414a 1367 // Otherwise, return to the base state without saving anything.
mjr 2:c174f9ee414a 1368 // If the button is released before we make it to calibration
mjr 2:c174f9ee414a 1369 // mode, it simply cancels the attempt.
mjr 9:fd65b0a94720 1370 if (calBtnState == 3 && calBtnTimer.read_ms() > 15000)
mjr 2:c174f9ee414a 1371 {
mjr 2:c174f9ee414a 1372 // exit calibration mode
mjr 1:d913e0afb2ac 1373 calBtnState = 0;
mjr 2:c174f9ee414a 1374
mjr 6:cc35eb643e8f 1375 // save the updated configuration
mjr 6:cc35eb643e8f 1376 cfg.d.plungerCal = 1;
mjr 6:cc35eb643e8f 1377 cfg.save(iap, flash_addr);
mjr 2:c174f9ee414a 1378
mjr 2:c174f9ee414a 1379 // the flash state is now valid
mjr 2:c174f9ee414a 1380 flash_valid = true;
mjr 2:c174f9ee414a 1381 }
mjr 2:c174f9ee414a 1382 else if (calBtnState != 3)
mjr 2:c174f9ee414a 1383 {
mjr 2:c174f9ee414a 1384 // didn't make it to calibration mode - cancel the operation
mjr 1:d913e0afb2ac 1385 calBtnState = 0;
mjr 2:c174f9ee414a 1386 }
mjr 1:d913e0afb2ac 1387 }
mjr 1:d913e0afb2ac 1388
mjr 1:d913e0afb2ac 1389 // light/flash the calibration button light, if applicable
mjr 1:d913e0afb2ac 1390 int newCalBtnLit = calBtnLit;
mjr 1:d913e0afb2ac 1391 switch (calBtnState)
mjr 0:5acbbe3f4cf4 1392 {
mjr 1:d913e0afb2ac 1393 case 2:
mjr 1:d913e0afb2ac 1394 // in the hold period - flash the light
mjr 9:fd65b0a94720 1395 newCalBtnLit = ((calBtnTimer.read_ms()/250) & 1);
mjr 1:d913e0afb2ac 1396 break;
mjr 1:d913e0afb2ac 1397
mjr 1:d913e0afb2ac 1398 case 3:
mjr 1:d913e0afb2ac 1399 // calibration mode - show steady on
mjr 1:d913e0afb2ac 1400 newCalBtnLit = true;
mjr 1:d913e0afb2ac 1401 break;
mjr 1:d913e0afb2ac 1402
mjr 1:d913e0afb2ac 1403 default:
mjr 1:d913e0afb2ac 1404 // not calibrating/holding - show steady off
mjr 1:d913e0afb2ac 1405 newCalBtnLit = false;
mjr 1:d913e0afb2ac 1406 break;
mjr 1:d913e0afb2ac 1407 }
mjr 3:3514575d4f86 1408
mjr 3:3514575d4f86 1409 // light or flash the external calibration button LED, and
mjr 3:3514575d4f86 1410 // do the same with the on-board blue LED
mjr 1:d913e0afb2ac 1411 if (calBtnLit != newCalBtnLit)
mjr 1:d913e0afb2ac 1412 {
mjr 1:d913e0afb2ac 1413 calBtnLit = newCalBtnLit;
mjr 2:c174f9ee414a 1414 if (calBtnLit) {
mjr 17:ab3cec0c8bf4 1415 if (calBtnLed != 0)
mjr 17:ab3cec0c8bf4 1416 calBtnLed->write(1);
mjr 4:02c7cd7b2183 1417 ledR = 1;
mjr 4:02c7cd7b2183 1418 ledG = 1;
mjr 9:fd65b0a94720 1419 ledB = 0;
mjr 2:c174f9ee414a 1420 }
mjr 2:c174f9ee414a 1421 else {
mjr 17:ab3cec0c8bf4 1422 if (calBtnLed != 0)
mjr 17:ab3cec0c8bf4 1423 calBtnLed->write(0);
mjr 4:02c7cd7b2183 1424 ledR = 1;
mjr 4:02c7cd7b2183 1425 ledG = 1;
mjr 9:fd65b0a94720 1426 ledB = 1;
mjr 2:c174f9ee414a 1427 }
mjr 1:d913e0afb2ac 1428 }
mjr 1:d913e0afb2ac 1429
mjr 17:ab3cec0c8bf4 1430 // If the plunger is enabled, and we're not already in a firing event,
mjr 17:ab3cec0c8bf4 1431 // and the last plunger reading had the plunger pulled back at least
mjr 17:ab3cec0c8bf4 1432 // a bit, watch for plunger release events until it's time for our next
mjr 17:ab3cec0c8bf4 1433 // USB report.
mjr 17:ab3cec0c8bf4 1434 if (!firing && cfg.d.plungerEnabled && z >= JOYMAX/6)
mjr 17:ab3cec0c8bf4 1435 {
mjr 17:ab3cec0c8bf4 1436 // monitor the plunger until it's time for our next report
mjr 17:ab3cec0c8bf4 1437 while (reportTimer.read_ms() < 15)
mjr 17:ab3cec0c8bf4 1438 {
mjr 17:ab3cec0c8bf4 1439 // do a fast low-res scan; if it's at or past the zero point,
mjr 17:ab3cec0c8bf4 1440 // start a firing event
mjr 17:ab3cec0c8bf4 1441 if (plungerSensor.lowResScan() <= cfg.d.plungerZero)
mjr 17:ab3cec0c8bf4 1442 firing = 1;
mjr 17:ab3cec0c8bf4 1443 }
mjr 17:ab3cec0c8bf4 1444 }
mjr 17:ab3cec0c8bf4 1445
mjr 6:cc35eb643e8f 1446 // read the plunger sensor, if it's enabled
mjr 17:ab3cec0c8bf4 1447 if (cfg.d.plungerEnabled)
mjr 6:cc35eb643e8f 1448 {
mjr 6:cc35eb643e8f 1449 // start with the previous reading, in case we don't have a
mjr 6:cc35eb643e8f 1450 // clear result on this frame
mjr 6:cc35eb643e8f 1451 int znew = z;
mjr 17:ab3cec0c8bf4 1452 if (plungerSensor.highResScan(pos))
mjr 6:cc35eb643e8f 1453 {
mjr 17:ab3cec0c8bf4 1454 // We got a new reading. If we're in calibration mode, use it
mjr 17:ab3cec0c8bf4 1455 // to figure the new calibration, otherwise adjust the new reading
mjr 17:ab3cec0c8bf4 1456 // for the established calibration.
mjr 17:ab3cec0c8bf4 1457 if (calBtnState == 3)
mjr 6:cc35eb643e8f 1458 {
mjr 17:ab3cec0c8bf4 1459 // Calibration mode. If this reading is outside of the current
mjr 17:ab3cec0c8bf4 1460 // calibration bounds, expand the bounds.
mjr 17:ab3cec0c8bf4 1461 if (pos < cfg.d.plungerMin)
mjr 17:ab3cec0c8bf4 1462 cfg.d.plungerMin = pos;
mjr 17:ab3cec0c8bf4 1463 if (pos < cfg.d.plungerZero)
mjr 17:ab3cec0c8bf4 1464 cfg.d.plungerZero = pos;
mjr 17:ab3cec0c8bf4 1465 if (pos > cfg.d.plungerMax)
mjr 17:ab3cec0c8bf4 1466 cfg.d.plungerMax = pos;
mjr 6:cc35eb643e8f 1467
mjr 17:ab3cec0c8bf4 1468 // normalize to the full physical range while calibrating
mjr 17:ab3cec0c8bf4 1469 znew = int(round(float(pos)/npix * JOYMAX));
mjr 17:ab3cec0c8bf4 1470 }
mjr 17:ab3cec0c8bf4 1471 else
mjr 17:ab3cec0c8bf4 1472 {
mjr 17:ab3cec0c8bf4 1473 // Not in calibration mode, so normalize the new reading to the
mjr 17:ab3cec0c8bf4 1474 // established calibration range.
mjr 17:ab3cec0c8bf4 1475 //
mjr 17:ab3cec0c8bf4 1476 // Note that negative values are allowed. Zero represents the
mjr 17:ab3cec0c8bf4 1477 // "park" position, where the plunger sits when at rest. A mechanical
mjr 17:ab3cec0c8bf4 1478 // plunger has a smmall amount of travel in the "push" direction,
mjr 17:ab3cec0c8bf4 1479 // since the barrel spring can be compressed slightly. Negative
mjr 17:ab3cec0c8bf4 1480 // values represent travel in the push direction.
mjr 17:ab3cec0c8bf4 1481 if (pos > cfg.d.plungerMax)
mjr 17:ab3cec0c8bf4 1482 pos = cfg.d.plungerMax;
mjr 17:ab3cec0c8bf4 1483 znew = int(round(float(pos - cfg.d.plungerZero)
mjr 17:ab3cec0c8bf4 1484 / (cfg.d.plungerMax - cfg.d.plungerZero + 1) * JOYMAX));
mjr 6:cc35eb643e8f 1485 }
mjr 6:cc35eb643e8f 1486 }
mjr 7:100a25f8bf56 1487
mjr 17:ab3cec0c8bf4 1488 // If we're not already in a firing event, check to see if the
mjr 17:ab3cec0c8bf4 1489 // new position is forward of the last report. If it is, a firing
mjr 17:ab3cec0c8bf4 1490 // event might have started during the high-res scan. This might
mjr 17:ab3cec0c8bf4 1491 // seem unlikely given that the scan only takes about 5ms, but that
mjr 17:ab3cec0c8bf4 1492 // 5ms represents about 25-30% of our total time between reports,
mjr 17:ab3cec0c8bf4 1493 // there's about a 1 in 4 chance that a release starts during a
mjr 17:ab3cec0c8bf4 1494 // scan.
mjr 17:ab3cec0c8bf4 1495 if (!firing && z0 > 0 && znew < z0)
mjr 17:ab3cec0c8bf4 1496 {
mjr 17:ab3cec0c8bf4 1497 // The plunger has moved forward since the previous report.
mjr 17:ab3cec0c8bf4 1498 // Watch it for a few more ms to see if we can get a stable
mjr 17:ab3cec0c8bf4 1499 // new position.
mjr 17:ab3cec0c8bf4 1500 int pos1 = plungerSensor.lowResScan();
mjr 17:ab3cec0c8bf4 1501 Timer tw;
mjr 17:ab3cec0c8bf4 1502 tw.start();
mjr 17:ab3cec0c8bf4 1503 while (tw.read_ms() < 6)
mjr 17:ab3cec0c8bf4 1504 {
mjr 17:ab3cec0c8bf4 1505 // if we've crossed the rest position, it's a firing event
mjr 17:ab3cec0c8bf4 1506 if (pos1 < cfg.d.plungerZero)
mjr 17:ab3cec0c8bf4 1507 {
mjr 17:ab3cec0c8bf4 1508 firing = 1;
mjr 17:ab3cec0c8bf4 1509 break;
mjr 17:ab3cec0c8bf4 1510 }
mjr 17:ab3cec0c8bf4 1511
mjr 17:ab3cec0c8bf4 1512 // read the new position
mjr 17:ab3cec0c8bf4 1513 int pos2 = plungerSensor.lowResScan();
mjr 17:ab3cec0c8bf4 1514
mjr 17:ab3cec0c8bf4 1515 // if it's stable, stop looping
mjr 17:ab3cec0c8bf4 1516 if (abs(pos2 - pos1) < int(npix/(3.2*8)))
mjr 17:ab3cec0c8bf4 1517 break;
mjr 17:ab3cec0c8bf4 1518
mjr 17:ab3cec0c8bf4 1519 // the new reading is now the prior reading
mjr 17:ab3cec0c8bf4 1520 pos1 = pos2;
mjr 17:ab3cec0c8bf4 1521 }
mjr 17:ab3cec0c8bf4 1522 }
mjr 17:ab3cec0c8bf4 1523
mjr 17:ab3cec0c8bf4 1524 // Check for a simulated Launch Ball button press, if enabled
mjr 18:5e890ebd0023 1525 if (ZBLaunchBallPort != 0)
mjr 17:ab3cec0c8bf4 1526 {
mjr 18:5e890ebd0023 1527 const int cockThreshold = JOYMAX/3;
mjr 18:5e890ebd0023 1528 const int pushThreshold = int(-JOYMAX/3 * LaunchBallPushDistance);
mjr 17:ab3cec0c8bf4 1529 int newState = lbState;
mjr 17:ab3cec0c8bf4 1530 switch (lbState)
mjr 17:ab3cec0c8bf4 1531 {
mjr 17:ab3cec0c8bf4 1532 case 0:
mjr 17:ab3cec0c8bf4 1533 // Base state. If the plunger is pulled back by an inch
mjr 17:ab3cec0c8bf4 1534 // or more, go to "cocked" state. If the plunger is pushed
mjr 17:ab3cec0c8bf4 1535 // forward by 1/4" or more, go to "launch" state.
mjr 18:5e890ebd0023 1536 if (znew >= cockThreshold)
mjr 17:ab3cec0c8bf4 1537 newState = 1;
mjr 18:5e890ebd0023 1538 else if (znew <= pushThreshold)
mjr 17:ab3cec0c8bf4 1539 newState = 3;
mjr 17:ab3cec0c8bf4 1540 break;
mjr 17:ab3cec0c8bf4 1541
mjr 17:ab3cec0c8bf4 1542 case 1:
mjr 17:ab3cec0c8bf4 1543 // Cocked state. If a firing event is now in progress,
mjr 17:ab3cec0c8bf4 1544 // go to "launch" state. Otherwise, if the plunger is less
mjr 17:ab3cec0c8bf4 1545 // than 1" retracted, go to "uncocked" state - the player
mjr 17:ab3cec0c8bf4 1546 // might be slowly returning the plunger to rest so as not
mjr 17:ab3cec0c8bf4 1547 // to trigger a launch.
mjr 17:ab3cec0c8bf4 1548 if (firing || znew <= 0)
mjr 17:ab3cec0c8bf4 1549 newState = 3;
mjr 18:5e890ebd0023 1550 else if (znew < cockThreshold)
mjr 17:ab3cec0c8bf4 1551 newState = 2;
mjr 17:ab3cec0c8bf4 1552 break;
mjr 17:ab3cec0c8bf4 1553
mjr 17:ab3cec0c8bf4 1554 case 2:
mjr 17:ab3cec0c8bf4 1555 // Uncocked state. If the plunger is more than an inch
mjr 17:ab3cec0c8bf4 1556 // retracted, return to cocked state. If we've been in
mjr 17:ab3cec0c8bf4 1557 // the uncocked state for more than half a second, return
mjr 18:5e890ebd0023 1558 // to the base state. This allows the user to return the
mjr 18:5e890ebd0023 1559 // plunger to rest without triggering a launch, by moving
mjr 18:5e890ebd0023 1560 // it at manual speed to the rest position rather than
mjr 18:5e890ebd0023 1561 // releasing it.
mjr 18:5e890ebd0023 1562 if (znew >= cockThreshold)
mjr 17:ab3cec0c8bf4 1563 newState = 1;
mjr 17:ab3cec0c8bf4 1564 else if (lbTimer.read_ms() > 500)
mjr 17:ab3cec0c8bf4 1565 newState = 0;
mjr 17:ab3cec0c8bf4 1566 break;
mjr 17:ab3cec0c8bf4 1567
mjr 17:ab3cec0c8bf4 1568 case 3:
mjr 17:ab3cec0c8bf4 1569 // Launch state. If the plunger is no longer pushed
mjr 17:ab3cec0c8bf4 1570 // forward, switch to launch rest state.
mjr 18:5e890ebd0023 1571 if (znew >= 0)
mjr 17:ab3cec0c8bf4 1572 newState = 4;
mjr 17:ab3cec0c8bf4 1573 break;
mjr 17:ab3cec0c8bf4 1574
mjr 17:ab3cec0c8bf4 1575 case 4:
mjr 17:ab3cec0c8bf4 1576 // Launch rest state. If the plunger is pushed forward
mjr 17:ab3cec0c8bf4 1577 // again, switch back to launch state. If not, and we've
mjr 17:ab3cec0c8bf4 1578 // been in this state for at least 200ms, return to the
mjr 17:ab3cec0c8bf4 1579 // default state.
mjr 18:5e890ebd0023 1580 if (znew <= pushThreshold)
mjr 17:ab3cec0c8bf4 1581 newState = 3;
mjr 17:ab3cec0c8bf4 1582 else if (lbTimer.read_ms() > 200)
mjr 17:ab3cec0c8bf4 1583 newState = 0;
mjr 17:ab3cec0c8bf4 1584 break;
mjr 17:ab3cec0c8bf4 1585 }
mjr 17:ab3cec0c8bf4 1586
mjr 17:ab3cec0c8bf4 1587 // change states if desired
mjr 18:5e890ebd0023 1588 const uint32_t lbButtonBit = (1 << (LaunchBallButton - 1));
mjr 17:ab3cec0c8bf4 1589 if (newState != lbState)
mjr 17:ab3cec0c8bf4 1590 {
mjr 18:5e890ebd0023 1591 // if we're entering Launch state, and the ZB Launch Ball
mjr 18:5e890ebd0023 1592 // LedWiz signal is turned on, simulate a Launch Ball button
mjr 18:5e890ebd0023 1593 // press
mjr 18:5e890ebd0023 1594 if (newState == 3 && lbState != 4 && wizOn[ZBLaunchBallPort-1])
mjr 18:5e890ebd0023 1595 {
mjr 18:5e890ebd0023 1596 lbBtnTimer.reset();
mjr 18:5e890ebd0023 1597 lbBtnTimer.start();
mjr 18:5e890ebd0023 1598 simButtons |= lbButtonBit;
mjr 18:5e890ebd0023 1599 }
mjr 17:ab3cec0c8bf4 1600
mjr 17:ab3cec0c8bf4 1601 // if we're switching to state 0, release the button
mjr 17:ab3cec0c8bf4 1602 if (newState == 0)
mjr 17:ab3cec0c8bf4 1603 simButtons &= ~(1 << (LaunchBallButton - 1));
mjr 17:ab3cec0c8bf4 1604
mjr 17:ab3cec0c8bf4 1605 // switch to the new state
mjr 17:ab3cec0c8bf4 1606 lbState = newState;
mjr 17:ab3cec0c8bf4 1607
mjr 17:ab3cec0c8bf4 1608 // start timing in the new state
mjr 17:ab3cec0c8bf4 1609 lbTimer.reset();
mjr 17:ab3cec0c8bf4 1610 }
mjr 18:5e890ebd0023 1611
mjr 18:5e890ebd0023 1612 // if the simulated Launch Ball button press is in effect,
mjr 18:5e890ebd0023 1613 // and either it's been in effect too long or the ZB Launch
mjr 18:5e890ebd0023 1614 // Ball signal is no longer active, turn off the button
mjr 18:5e890ebd0023 1615 if ((simButtons & lbButtonBit) != 0
mjr 18:5e890ebd0023 1616 && (!wizOn[ZBLaunchBallPort-1] || lbBtnTimer.read_ms() > 250))
mjr 18:5e890ebd0023 1617 {
mjr 18:5e890ebd0023 1618 lbBtnTimer.stop();
mjr 18:5e890ebd0023 1619 simButtons &= ~lbButtonBit;
mjr 18:5e890ebd0023 1620 }
mjr 18:5e890ebd0023 1621
mjr 17:ab3cec0c8bf4 1622 }
mjr 17:ab3cec0c8bf4 1623
mjr 17:ab3cec0c8bf4 1624 // If a firing event is in progress, generate synthetic reports to
mjr 17:ab3cec0c8bf4 1625 // describe an idealized version of the plunger motion to VP rather
mjr 17:ab3cec0c8bf4 1626 // than reporting the actual physical plunger position.
mjr 6:cc35eb643e8f 1627 //
mjr 17:ab3cec0c8bf4 1628 // We use the synthetic reports during a release event because the
mjr 17:ab3cec0c8bf4 1629 // physical plunger motion when released is too fast for VP to track.
mjr 17:ab3cec0c8bf4 1630 // VP only syncs its internal physics model with the outside world
mjr 17:ab3cec0c8bf4 1631 // about every 10ms. In that amount of time, the plunger moves
mjr 17:ab3cec0c8bf4 1632 // fast enough when released that it can shoot all the way forward,
mjr 17:ab3cec0c8bf4 1633 // bounce off of the barrel spring, and rebound part of the way
mjr 17:ab3cec0c8bf4 1634 // back. The result is the classic analog-to-digital problem of
mjr 17:ab3cec0c8bf4 1635 // sample aliasing. If we happen to time our sample during the
mjr 17:ab3cec0c8bf4 1636 // release motion so that we catch the plunger at the peak of a
mjr 17:ab3cec0c8bf4 1637 // bounce, the digital signal incorrectly looks like the plunger
mjr 17:ab3cec0c8bf4 1638 // is moving slowly forward - VP thinks we went from fully
mjr 17:ab3cec0c8bf4 1639 // retracted to half retracted in the sample interval, whereas
mjr 17:ab3cec0c8bf4 1640 // we actually traveled all the way forward and half way back,
mjr 17:ab3cec0c8bf4 1641 // so the speed VP infers is about 1/3 of the actual speed.
mjr 9:fd65b0a94720 1642 //
mjr 17:ab3cec0c8bf4 1643 // To correct this, we take advantage of our ability to sample
mjr 17:ab3cec0c8bf4 1644 // the CCD image several times in the course of a VP report. If
mjr 17:ab3cec0c8bf4 1645 // we catch the plunger near the origin after we've seen it
mjr 17:ab3cec0c8bf4 1646 // retracted, we go into Release Event mode. During this mode,
mjr 17:ab3cec0c8bf4 1647 // we stop reporting the true physical plunger position, and
mjr 17:ab3cec0c8bf4 1648 // instead report an idealized pattern: we report the plunger
mjr 17:ab3cec0c8bf4 1649 // immediately shooting forward to a position in front of the
mjr 17:ab3cec0c8bf4 1650 // park position that's in proportion to how far back the plunger
mjr 17:ab3cec0c8bf4 1651 // was just before the release, and we then report it stationary
mjr 17:ab3cec0c8bf4 1652 // at the park position. We continue to report the stationary
mjr 17:ab3cec0c8bf4 1653 // park position until the actual physical plunger motion has
mjr 17:ab3cec0c8bf4 1654 // stabilized on a new position. We then exit Release Event
mjr 17:ab3cec0c8bf4 1655 // mode and return to reporting the true physical position.
mjr 17:ab3cec0c8bf4 1656 if (firing)
mjr 6:cc35eb643e8f 1657 {
mjr 17:ab3cec0c8bf4 1658 // Firing in progress. Keep reporting the park position
mjr 17:ab3cec0c8bf4 1659 // until the physical plunger position comes to rest.
mjr 17:ab3cec0c8bf4 1660 const int restTol = JOYMAX/24;
mjr 17:ab3cec0c8bf4 1661 if (firing == 1)
mjr 6:cc35eb643e8f 1662 {
mjr 17:ab3cec0c8bf4 1663 // For the first couple of frames, show the plunger shooting
mjr 17:ab3cec0c8bf4 1664 // forward past the zero point, to simulate the momentum carrying
mjr 17:ab3cec0c8bf4 1665 // it forward to bounce off of the barrel spring. Show the
mjr 17:ab3cec0c8bf4 1666 // bounce as proportional to the distance it was retracted
mjr 17:ab3cec0c8bf4 1667 // in the prior report.
mjr 17:ab3cec0c8bf4 1668 z = zBounce = -z0/6;
mjr 17:ab3cec0c8bf4 1669 ++firing;
mjr 6:cc35eb643e8f 1670 }
mjr 17:ab3cec0c8bf4 1671 else if (firing == 2)
mjr 9:fd65b0a94720 1672 {
mjr 17:ab3cec0c8bf4 1673 // second frame - keep the bounce a little longer
mjr 17:ab3cec0c8bf4 1674 z = zBounce;
mjr 17:ab3cec0c8bf4 1675 ++firing;
mjr 17:ab3cec0c8bf4 1676 }
mjr 17:ab3cec0c8bf4 1677 else if (firing > 4
mjr 17:ab3cec0c8bf4 1678 && abs(znew - z0) < restTol
mjr 17:ab3cec0c8bf4 1679 && abs(znew - z1) < restTol
mjr 17:ab3cec0c8bf4 1680 && abs(znew - z2) < restTol)
mjr 17:ab3cec0c8bf4 1681 {
mjr 17:ab3cec0c8bf4 1682 // The physical plunger has come to rest. Exit firing
mjr 17:ab3cec0c8bf4 1683 // mode and resume reporting the actual position.
mjr 17:ab3cec0c8bf4 1684 firing = false;
mjr 17:ab3cec0c8bf4 1685 z = znew;
mjr 9:fd65b0a94720 1686 }
mjr 9:fd65b0a94720 1687 else
mjr 9:fd65b0a94720 1688 {
mjr 17:ab3cec0c8bf4 1689 // until the physical plunger comes to rest, simply
mjr 17:ab3cec0c8bf4 1690 // report the park position
mjr 9:fd65b0a94720 1691 z = 0;
mjr 17:ab3cec0c8bf4 1692 ++firing;
mjr 9:fd65b0a94720 1693 }
mjr 6:cc35eb643e8f 1694 }
mjr 6:cc35eb643e8f 1695 else
mjr 6:cc35eb643e8f 1696 {
mjr 17:ab3cec0c8bf4 1697 // not in firing mode - report the true physical position
mjr 17:ab3cec0c8bf4 1698 z = znew;
mjr 6:cc35eb643e8f 1699 }
mjr 17:ab3cec0c8bf4 1700
mjr 17:ab3cec0c8bf4 1701 // shift the new reading into the recent history buffer
mjr 6:cc35eb643e8f 1702 z2 = z1;
mjr 6:cc35eb643e8f 1703 z1 = z0;
mjr 6:cc35eb643e8f 1704 z0 = znew;
mjr 2:c174f9ee414a 1705 }
mjr 6:cc35eb643e8f 1706
mjr 11:bd9da7088e6e 1707 // update the buttons
mjr 18:5e890ebd0023 1708 uint32_t buttons = readButtons();
mjr 17:ab3cec0c8bf4 1709
mjr 17:ab3cec0c8bf4 1710 // If it's been long enough since our last USB status report,
mjr 17:ab3cec0c8bf4 1711 // send the new report. We throttle the report rate because
mjr 17:ab3cec0c8bf4 1712 // it can overwhelm the PC side if we report too frequently.
mjr 17:ab3cec0c8bf4 1713 // VP only wants to sync with the real world in 10ms intervals,
mjr 17:ab3cec0c8bf4 1714 // so reporting more frequently only creates i/o overhead
mjr 17:ab3cec0c8bf4 1715 // without doing anything to improve the simulation.
mjr 17:ab3cec0c8bf4 1716 if (reportTimer.read_ms() > 15)
mjr 17:ab3cec0c8bf4 1717 {
mjr 17:ab3cec0c8bf4 1718 // read the accelerometer
mjr 17:ab3cec0c8bf4 1719 int xa, ya;
mjr 17:ab3cec0c8bf4 1720 accel.get(xa, ya);
mjr 17:ab3cec0c8bf4 1721
mjr 17:ab3cec0c8bf4 1722 // confine the results to our joystick axis range
mjr 17:ab3cec0c8bf4 1723 if (xa < -JOYMAX) xa = -JOYMAX;
mjr 17:ab3cec0c8bf4 1724 if (xa > JOYMAX) xa = JOYMAX;
mjr 17:ab3cec0c8bf4 1725 if (ya < -JOYMAX) ya = -JOYMAX;
mjr 17:ab3cec0c8bf4 1726 if (ya > JOYMAX) ya = JOYMAX;
mjr 17:ab3cec0c8bf4 1727
mjr 17:ab3cec0c8bf4 1728 // store the updated accelerometer coordinates
mjr 17:ab3cec0c8bf4 1729 x = xa;
mjr 17:ab3cec0c8bf4 1730 y = ya;
mjr 17:ab3cec0c8bf4 1731
mjr 17:ab3cec0c8bf4 1732 // Send the status report. Note that the nominal x and y axes
mjr 17:ab3cec0c8bf4 1733 // are reversed - this makes it more intuitive to set up in VP.
mjr 17:ab3cec0c8bf4 1734 // If we mount the Freesale card flat on the floor of the cabinet
mjr 17:ab3cec0c8bf4 1735 // with the USB connectors facing the front of the cabinet, this
mjr 17:ab3cec0c8bf4 1736 // arrangement of our nominal axes aligns with VP's standard
mjr 17:ab3cec0c8bf4 1737 // setting, so that we can configure VP with X Axis = X on the
mjr 17:ab3cec0c8bf4 1738 // joystick and Y Axis = Y on the joystick.
mjr 17:ab3cec0c8bf4 1739 js.update(y, x, z, buttons | simButtons, statusFlags);
mjr 17:ab3cec0c8bf4 1740
mjr 17:ab3cec0c8bf4 1741 // we've just started a new report interval, so reset the timer
mjr 17:ab3cec0c8bf4 1742 reportTimer.reset();
mjr 17:ab3cec0c8bf4 1743 }
mjr 1:d913e0afb2ac 1744
mjr 10:976666ffa4ef 1745 // If we're in pixel dump mode, report all pixel exposure values
mjr 10:976666ffa4ef 1746 if (reportPix)
mjr 10:976666ffa4ef 1747 {
mjr 17:ab3cec0c8bf4 1748 // send the report
mjr 17:ab3cec0c8bf4 1749 plungerSensor.sendExposureReport(js);
mjr 17:ab3cec0c8bf4 1750
mjr 10:976666ffa4ef 1751 // we have satisfied this request
mjr 10:976666ffa4ef 1752 reportPix = false;
mjr 10:976666ffa4ef 1753 }
mjr 10:976666ffa4ef 1754
mjr 6:cc35eb643e8f 1755 #ifdef DEBUG_PRINTF
mjr 6:cc35eb643e8f 1756 if (x != 0 || y != 0)
mjr 6:cc35eb643e8f 1757 printf("%d,%d\r\n", x, y);
mjr 6:cc35eb643e8f 1758 #endif
mjr 6:cc35eb643e8f 1759
mjr 6:cc35eb643e8f 1760 // provide a visual status indication on the on-board LED
mjr 5:a70c0bce770d 1761 if (calBtnState < 2 && hbTimer.read_ms() > 1000)
mjr 1:d913e0afb2ac 1762 {
mjr 5:a70c0bce770d 1763 if (js.isSuspended() || !js.isConnected())
mjr 2:c174f9ee414a 1764 {
mjr 5:a70c0bce770d 1765 // suspended - turn off the LED
mjr 4:02c7cd7b2183 1766 ledR = 1;
mjr 4:02c7cd7b2183 1767 ledG = 1;
mjr 4:02c7cd7b2183 1768 ledB = 1;
mjr 5:a70c0bce770d 1769
mjr 5:a70c0bce770d 1770 // show a status flash every so often
mjr 5:a70c0bce770d 1771 if (hbcnt % 3 == 0)
mjr 5:a70c0bce770d 1772 {
mjr 6:cc35eb643e8f 1773 // disconnected = red/red flash; suspended = red
mjr 5:a70c0bce770d 1774 for (int n = js.isConnected() ? 1 : 2 ; n > 0 ; --n)
mjr 5:a70c0bce770d 1775 {
mjr 5:a70c0bce770d 1776 ledR = 0;
mjr 5:a70c0bce770d 1777 wait(0.05);
mjr 5:a70c0bce770d 1778 ledR = 1;
mjr 5:a70c0bce770d 1779 wait(0.25);
mjr 5:a70c0bce770d 1780 }
mjr 5:a70c0bce770d 1781 }
mjr 2:c174f9ee414a 1782 }
mjr 6:cc35eb643e8f 1783 else if (needReset)
mjr 2:c174f9ee414a 1784 {
mjr 6:cc35eb643e8f 1785 // connected, need to reset due to changes in config parameters -
mjr 6:cc35eb643e8f 1786 // flash red/green
mjr 6:cc35eb643e8f 1787 hb = !hb;
mjr 6:cc35eb643e8f 1788 ledR = (hb ? 0 : 1);
mjr 6:cc35eb643e8f 1789 ledG = (hb ? 1 : 0);
mjr 6:cc35eb643e8f 1790 ledB = 0;
mjr 6:cc35eb643e8f 1791 }
mjr 17:ab3cec0c8bf4 1792 else if (cfg.d.plungerEnabled && !cfg.d.plungerCal)
mjr 6:cc35eb643e8f 1793 {
mjr 6:cc35eb643e8f 1794 // connected, plunger calibration needed - flash yellow/green
mjr 6:cc35eb643e8f 1795 hb = !hb;
mjr 6:cc35eb643e8f 1796 ledR = (hb ? 0 : 1);
mjr 6:cc35eb643e8f 1797 ledG = 0;
mjr 6:cc35eb643e8f 1798 ledB = 1;
mjr 6:cc35eb643e8f 1799 }
mjr 6:cc35eb643e8f 1800 else
mjr 6:cc35eb643e8f 1801 {
mjr 6:cc35eb643e8f 1802 // connected - flash blue/green
mjr 2:c174f9ee414a 1803 hb = !hb;
mjr 4:02c7cd7b2183 1804 ledR = 1;
mjr 4:02c7cd7b2183 1805 ledG = (hb ? 0 : 1);
mjr 4:02c7cd7b2183 1806 ledB = (hb ? 1 : 0);
mjr 2:c174f9ee414a 1807 }
mjr 1:d913e0afb2ac 1808
mjr 1:d913e0afb2ac 1809 // reset the heartbeat timer
mjr 1:d913e0afb2ac 1810 hbTimer.reset();
mjr 5:a70c0bce770d 1811 ++hbcnt;
mjr 1:d913e0afb2ac 1812 }
mjr 1:d913e0afb2ac 1813 }
mjr 0:5acbbe3f4cf4 1814 }