![](/media/cache/img/default_profile.jpg.50x50_q85.jpg)
work in progress
Dependencies: FastAnalogIn FastIO USBDevice mbed FastPWM SimpleDMA
Fork of Pinscape_Controller by
Diff: main.cpp
- Revision:
- 17:ab3cec0c8bf4
- Parent:
- 16:c35f905c3311
- Child:
- 18:5e890ebd0023
diff -r c35f905c3311 -r ab3cec0c8bf4 main.cpp --- a/main.cpp Mon Dec 29 19:27:52 2014 +0000 +++ b/main.cpp Fri Feb 27 04:14:04 2015 +0000 @@ -19,96 +19,81 @@ // // Pinscape Controller // -// "Pinscape" is the name of my custom-built virtual pinball cabinet. I wrote this -// software to perform a number of tasks that I needed for my cabinet. It runs on a -// Freescale KL25Z microcontroller, which is a small and inexpensive device that -// attaches to the host PC via USB and can interface with numerous types of external -// hardware. +// "Pinscape" is the name of my custom-built virtual pinball cabinet, so I call this +// software the Pinscape Controller. I wrote it to handle several tasks that I needed +// for my cabinet. It runs on a Freescale KL25Z microcontroller, which is a small and +// inexpensive device that attaches to the cabinet PC via a USB cable, and can attach +// via custom wiring to sensors, buttons, and other devices in the cabinet. // -// I designed the software and hardware in this project especially for Pinscape, but -// it uses standard interfaces in Windows and Visual Pinball, so it should be -// readily usable in anyone else's VP-based cabinet. I've tried to document the -// hardware in enough detail for anyone else to duplicate the entire project, and -// the full software is open source. +// I designed the software and hardware in this project especially for my own +// cabinet, but it uses standard interfaces in Windows and Visual Pinball, so it should +// work in any VP-based cabinet, as long as you're using the usual VP software suite. +// I've tried to document the hardware in enough detail for anyone else to duplicate +// the entire project, and the full software is open source. // -// The device appears to the host computer as a USB joystick. This works with the -// standard Windows joystick device drivers, so there's no need to install any -// software on the PC - Windows should recognize it as a joystick when you plug -// it in and shouldn't ask you to install anything. If you bring up the control -// panel for USB Game Controllers, this device will appear as "Pinscape Controller". -// *Don't* do any calibration with the Windows control panel or third-part -// calibration tools. The device calibrates itself automatically for the -// accelerometer data, and has its own special calibration procedure for the -// plunger (see below). -// -// The controller provides the following functions. It should be possible to use -// any subet of the features without using all of them. External hardware for any -// particular function can simply be omitted if that feature isn't needed. +// The Freescale board appears to the host PC as a standard USB joystick. This works +// with the built-in Windows joystick device drivers, so there's no need to install any +// new drivers or other software on the PC. Windows should recognize the Freescale +// as a joystick when you plug it into the USB port, and Windows shouldn't ask you to +// install any drivers. If you bring up the Windows control panel for USB Game +// Controllers, this device will appear as "Pinscape Controller". *Don't* do any +// calibration with the Windows control panel or third-part calibration tools. The +// software calibrates the accelerometer portion automatically, and has its own special +// calibration procedure for the plunger sensor, if you're using that (see below). // -// - Nudge sensing via the KL25Z's on-board accelerometer. Nudge accelerations are -// processed into a physics model of a rolling ball, and changes to the ball's -// motion are sent to the host computer via the joystick interface. This is designed -// especially to work with Visuall Pinball's nudge handling to produce realistic -// on-screen results in VP. By doing some physics modeling right on the device, -// rather than sending raw accelerometer data to VP, we can produce better results -// using our awareness of the real physical parameters of a pinball cabinet. -// VP's nudge handling has to be more generic, so it can't make the same sorts -// of assumptions that we can about the dynamics of a real cabinet. +// This software provides a whole bunch of separate features. You can use any of these +// features individually or all together. If you're not using a particular feature, you +// can simply omit the extra wiring and/or hardware for that feature. You can use +// the nudging feature by itself without any extra hardware attached, since the +// accelerometer is built in to the KL25Z board. // -// The nudge data reports are compatible with the built-in Windows USB joystick -// drivers and with VP's own joystick input scheme, so the nudge sensing is almost -// plug-and-play. There are no Windiows drivers to install, and the only VP work -// needed is to customize a few global preference settings. +// - Nudge sensing via the KL25Z's on-board accelerometer. Nudging the cabinet +// causes small accelerations that the accelerometer can detect; these are sent to +// Visual Pinball via the joystick interface so that VP can simulate the effect +// of the real physical nudges on its simulated ball. VP has native handling for +// this type of input, so all you have to do is set some preferences in VP to tell +// it that an accelerometer is attached. // // - Plunger position sensing via an attached TAOS TSL 1410R CCD linear array sensor. -// The sensor must be wired to a particular set of I/O ports on the KL25Z, and must -// be positioned adjacent to the plunger with proper lighting. The physical and -// electronic installation details are desribed in the project documentation. We read -// the CCD to determine how far back the plunger is pulled, and report this to Visual -// Pinball via the joystick interface. As with the nudge data, this is all nearly -// plug-and-play, in that it works with the default Windows USB drivers and works -// with the existing VP handling for analog plunger input. A few VP settings are -// needed to tell VP to allow the plunger. +// To use this feature, you need to buy the TAOS device (it's not built in to the +// KL25Z, obviously), wire it to the KL25Z (5 wire connections between the two +// devices are required), and mount the TAOS sensor in your cabinet so that it's +// positioned properly to capture images of the physical plunger shooter rod. +// +// The physical mounting and wiring details are desribed in the project +// documentation. +// +// If the CCD is attached, the software constantly captures images from the CCD +// and analyzes them to determine how far back the plunger is pulled. It reports +// this to Visual Pinball via the joystick interface. This allows VP to make the +// simulated on-screen plunger track the motion of the physical plunger in real +// time. As with the nudge data, VP has native handling for the plunger input, +// so you just need to set the VP preferences to tell it that an analog plunger +// device is attached. One caveat, though: although VP itself has built-in +// support for an analog plunger, not all existing tables take advantage of it. +// Many existing tables have their own custom plunger scripting that doesn't +// cooperate with the VP plunger input. All tables *can* be made to work with +// the plunger, and in most cases it only requires some simple script editing, +// but in some cases it requires some more extensive surgery. // // For best results, the plunger sensor should be calibrated. The calibration // is stored in non-volatile memory on board the KL25Z, so it's only necessary // to do the calibration once, when you first install everything. (You might // also want to re-calibrate if you physically remove and reinstall the CCD -// sensor or the mechanical plunger, since their alignment might change slightly -// when you put everything back together.) To calibrate, you have to attach a -// momentary switch (e.g., a push-button switch) between one of the KL25Z ground -// pins (e.g., jumper J9 pin 12) and PTE29 (J10 pin 9). Press and hold the -// button for about two seconds - the LED on the KL25Z wlil flash blue while -// you hold the button, and will turn solid blue when you've held it down long -// enough to enter calibration mode. This mode will last about 15 seconds. -// Simply pull the plunger all the way back, hold it for a few moments, and -// gradually return it to the starting position. *Don't* release it - we want -// to measure the maximum retracted position and the rest position, but NOT -// the maximum forward position when the outer barrel spring is compressed. -// After about 15 seconds, the device will save the new calibration settings -// to its flash memory, and the LED will return to the regular "heartbeat" -// flashes. If this is the first time you calibrated, you should observe the -// color of the flashes change from yellow/green to blue/green to indicate -// that the plunger has been calibrated. +// sensor or the mechanical plunger, since their alignment shift change slightly +// when you put everything back together.) You can optionally install a +// dedicated momentary switch or pushbutton to activate the calibration mode; +// this is describe in the project documentation. If you don't want to bother +// with the extra button, you can also trigger calibration using the Windows +// setup software, which you can find on the Pinscape project page. // -// Note that while Visual Pinball itself has good native support for analog -// plungers, most of the VP tables in circulation don't implement the necessary -// scripting features to make this work properly. Therefore, you'll have to do -// a little scripting work for each table you download to add the required code -// to that individual table. The work has to be customized for each table, so -// I haven't been able to automate this process, but I have tried to reduce it -// to a relatively simple recipe that I've documented separately. -// -// - In addition to the CCD sensor, a button should be attached (also described in -// the project documentation) to activate calibration mode for the plunger. When -// calibration mode is activated, the software reads the plunger position for about -// 10 seconds when to note the limits of travel, and uses these limits to ensure -// accurate reports to VP that properly report the actual position of the physical -// plunger. The calibration is stored in non-volatile memory on the KL25Z, so it's -// only necessary to calibrate once - the calibration will survive power cycling -// and reboots of the PC. It's only necessary to recalibrate if the CCD sensor or -// the plunger are removed and reinstalled, since the relative alignment of the -// parts could cahnge slightly when reinstalling. +// The calibration procedure is described in the project documentation. Briefly, +// when you trigger calibration mode, the software will scan the CCD for about +// 15 seconds, during which you should simply pull the physical plunger back +// all the way, hold it for a moment, and then slowly return it to the rest +// position. (DON'T just release it from the retracted position, since that +// let it shoot forward too far. We want to measure the range from the park +// position to the fully retracted position only.) // // - Button input wiring. 24 of the KL25Z's GPIO ports are mapped as digital inputs // for buttons and switches. The software reports these as joystick buttons when @@ -229,13 +214,20 @@ #include "FreescaleIAP.h" #include "crc32.h" +// our local configuration file +#include "config.h" + // --------------------------------------------------------------------------- -// -// Configuration details +// utilities + +// number of elements in an array +#define countof(x) (sizeof(x)/sizeof((x)[0])) + + +// --------------------------------------------------------------------------- +// USB device vendor ID, product ID, and version. // - -// Our USB device vendor ID, product ID, and version. // We use the vendor ID for the LedWiz, so that the PC-side software can // identify us as capable of performing LedWiz commands. The LedWiz uses // a product ID value from 0xF0 to 0xFF; the last four bits identify the @@ -249,13 +241,6 @@ // single LedWiz already installed in your cabinet, and you didn't ask for // a non-default unit number, your existing LedWiz will be unit 0. // -// We use unit #7 by default. There doesn't seem to be a requirement that -// unit numbers be contiguous (DirectOutput Framework and other software -// seem happy to have units 0 and 7 installed, without 1-6 existing). -// Marking this unit as #7 should work for almost everybody out of the box; -// the most common case seems to be to have a single LedWiz installed, and -// it's probably extremely rare to more than two. -// // Note that the USB_PRODUCT_ID value set here omits the unit number. We // take the unit number from the saved configuration. We provide a // configuration command that can be sent via the USB connection to change @@ -266,235 +251,29 @@ const uint16_t USB_VENDOR_ID = 0xFAFA; const uint16_t USB_PRODUCT_ID = 0x00F0; const uint16_t USB_VERSION_NO = 0x0006; -const uint8_t DEFAULT_LEDWIZ_UNIT_NUMBER = 0x07; -// Number of pixels we read from the sensor on each frame. This can be -// less than the physical pixel count if desired; we'll read every nth -// piexl if so. E.g., with a 1280-pixel physical sensor, if npix is 320, -// we'll read every 4th pixel. It takes time to read each pixel, so the -// fewer pixels we read, the higher the refresh rate we can achieve. -// It's therefore better not to read more pixels than we have to. -// -// VP seems to have an internal resolution in the 8-bit range, so there's -// no apparent benefit to reading more than 128-256 pixels when using VP. -// Empirically, 160 pixels seems about right. The overall travel of a -// standard pinball plunger is about 3", so 160 pixels gives us resolution -// of about 1/50". This seems to take full advantage of VP's modeling -// ability, and is probably also more precise than a human player's -// perception of the plunger position. -const int npix = 160; - -// On-board RGB LED elements - we use these for diagnostic displays. -DigitalOut ledR(LED1), ledG(LED2), ledB(LED3); - -// calibration button - switch input and LED output -DigitalIn calBtn(PTE29); -DigitalOut calBtnLed(PTE23); - -// Joystick button input pin assignments. You can wire up to -// 32 GPIO ports to buttons (equipped with momentary switches). -// Connect each switch between the desired GPIO port and ground -// (J9 pin 12 or 14). When the button is pressed, we'll tell the -// host PC that the corresponding joystick button is pressed. We -// debounce the keystrokes in software, so you can simply wire -// directly to pushbuttons with no additional external hardware. -// -// Note that we assign 24 buttons by default, even though the USB -// joystick interface can handle up to 32 buttons. VP itself only -// allows mapping of up to 24 buttons in the preferences dialog -// (although it can recognize 32 buttons internally). If you want -// more buttons, you can reassign pins that are assigned by default -// as LedWiz outputs. To reassign a pin, find the pin you wish to -// reassign in the LedWizPortMap array below, and change the pin name -// there to NC (for Not Connected). You can then change one of the -// "NC" entries below to the reallocated pin name. The limit is 32 -// buttons total. -// -// Note: PTD1 (pin J2-12) should NOT be assigned as a button input, -// as this pin is physically connected on the KL25Z to the on-board -// indicator LED's blue segment. This precludes any other use of -// the pin. -PinName buttonMap[] = { - PTC2, // J10 pin 10, joystick button 1 - PTB3, // J10 pin 8, joystick button 2 - PTB2, // J10 pin 6, joystick button 3 - PTB1, // J10 pin 4, joystick button 4 - - PTE30, // J10 pin 11, joystick button 5 - PTE22, // J10 pin 5, joystick button 6 - - PTE5, // J9 pin 15, joystick button 7 - PTE4, // J9 pin 13, joystick button 8 - PTE3, // J9 pin 11, joystick button 9 - PTE2, // J9 pin 9, joystick button 10 - PTB11, // J9 pin 7, joystick button 11 - PTB10, // J9 pin 5, joystick button 12 - PTB9, // J9 pin 3, joystick button 13 - PTB8, // J9 pin 1, joystick button 14 - - PTC12, // J2 pin 1, joystick button 15 - PTC13, // J2 pin 3, joystick button 16 - PTC16, // J2 pin 5, joystick button 17 - PTC17, // J2 pin 7, joystick button 18 - PTA16, // J2 pin 9, joystick button 19 - PTA17, // J2 pin 11, joystick button 20 - PTE31, // J2 pin 13, joystick button 21 - PTD6, // J2 pin 17, joystick button 22 - PTD7, // J2 pin 19, joystick button 23 - - PTE1, // J2 pin 20, joystick button 24 - - NC, // not used, joystick button 25 - NC, // not used, joystick button 26 - NC, // not used, joystick button 27 - NC, // not used, joystick button 28 - NC, // not used, joystick button 29 - NC, // not used, joystick button 30 - NC, // not used, joystick button 31 - NC // not used, joystick button 32 -}; - -// LED-Wiz emulation output pin assignments. -// -// The LED-Wiz protocol allows setting individual intensity levels -// on all outputs, with 48 levels of intensity. This can be used -// to control lamp brightness and motor speeds, among other things. -// Unfortunately, the KL25Z only has 10 PWM channels, so while we -// can support the full complement of 32 outputs, we can only provide -// PWM dimming/speed control on 10 of them. The remaining outputs -// can only be switched fully on and fully off - we can't support -// dimming on these, so they'll ignore any intensity level setting -// requested by the host. Use these for devices that don't have any -// use for intensity settings anyway, such as contactors and knockers. -// -// Ports with pins assigned as "NC" are not connected. That is, -// there's no physical pin for that LedWiz port number. You can -// send LedWiz commands to turn NC ports on and off, but doing so -// will have no effect. The reason we leave some ports unassigned -// is that we don't have enough physical GPIO pins to fill out the -// full LedWiz complement of 32 ports. Many pins are already taken -// for other purposes, such as button inputs or the plunger CCD -// interface. -// -// The mapping between physical output pins on the KL25Z and the -// assigned LED-Wiz port numbers is essentially arbitrary - you can -// customize this by changing the entries in the array below if you -// wish to rearrange the pins for any reason. Be aware that some -// of the physical outputs are already used for other purposes -// (e.g., some of the GPIO pins on header J10 are used for the -// CCD sensor - but you can of course reassign those as well by -// changing the corresponding declarations elsewhere in this module). -// The assignments we make here have two main objectives: first, -// to group the outputs on headers J1 and J2 (to facilitate neater -// wiring by keeping the output pins together physically), and -// second, to make the physical pin layout match the LED-Wiz port -// numbering order to the extent possible. There's one big wrench -// in the works, though, which is the limited number and discontiguous -// placement of the KL25Z PWM-capable output pins. This prevents -// us from doing the most obvious sequential ordering of the pins, -// so we end up with the outputs arranged into several blocks. -// Hopefully this isn't too confusing; for more detailed rationale, -// read on... -// -// With the LED-Wiz, the host software configuration usually -// assumes that each RGB LED is hooked up to three consecutive ports -// (for the red, green, and blue components, which need to be -// physically wired to separate outputs to allow each color to be -// controlled independently). To facilitate this, we arrange the -// PWM-enabled outputs so that they're grouped together in the -// port numbering scheme. Unfortunately, these outputs aren't -// together in a single group in the physical pin layout, so to -// group them logically in the LED-Wiz port numbering scheme, we -// have to break up the overall numbering scheme into several blocks. -// So our port numbering goes sequentially down each column of -// header pins, but there are several break points where we have -// to interrupt the obvious sequence to keep the PWM pins grouped -// logically. -// -// In the list below, "pin J1-2" refers to pin 2 on header J1 on -// the KL25Z, using the standard pin numbering in the KL25Z -// documentation - this is the physical pin that the port controls. -// "LW port 1" means LED-Wiz port 1 - this is the LED-Wiz port -// number that you use on the PC side (in the DirectOutput config -// file, for example) to address the port. PWM-capable ports are -// marked as such - we group the PWM-capable ports into the first -// 10 LED-Wiz port numbers. -// -// If you wish to reallocate a pin in the array below to some other -// use, such as a button input port, simply change the pin name in -// the entry to NC (for Not Connected). This will disable the given -// logical LedWiz port number and free up the physical pin. -// -// If you wish to reallocate a pin currently assigned to the button -// input array, simply change the entry for the pin in the buttonMap[] -// array above to NC (for "not connected"), and plug the pin name into -// a slot of your choice in the array below. -// -// Note: PTD1 (pin J2-12) should NOT be assigned as an LedWiz output, -// as this pin is physically connected on the KL25Z to the on-board -// indicator LED's blue segment. This precludes any other use of -// the pin. -// -struct { - PinName pin; - bool isPWM; -} ledWizPortMap[32] = { - { PTA1, true }, // pin J1-2, LW port 1 (PWM capable - TPM 2.0 = channel 9) - { PTA2, true }, // pin J1-4, LW port 2 (PWM capable - TPM 2.1 = channel 10) - { PTD4, true }, // pin J1-6, LW port 3 (PWM capable - TPM 0.4 = channel 5) - { PTA12, true }, // pin J1-8, LW port 4 (PWM capable - TPM 1.0 = channel 7) - { PTA4, true }, // pin J1-10, LW port 5 (PWM capable - TPM 0.1 = channel 2) - { PTA5, true }, // pin J1-12, LW port 6 (PWM capable - TPM 0.2 = channel 3) - { PTA13, true }, // pin J2-2, LW port 7 (PWM capable - TPM 1.1 = channel 13) - { PTD5, true }, // pin J2-4, LW port 8 (PWM capable - TPM 0.5 = channel 6) - { PTD0, true }, // pin J2-6, LW port 9 (PWM capable - TPM 0.0 = channel 1) - { PTD3, true }, // pin J2-10, LW port 10 (PWM capable - TPM 0.3 = channel 4) - { PTD2, false }, // pin J2-8, LW port 11 - { PTC8, false }, // pin J1-14, LW port 12 - { PTC9, false }, // pin J1-16, LW port 13 - { PTC7, false }, // pin J1-1, LW port 14 - { PTC0, false }, // pin J1-3, LW port 15 - { PTC3, false }, // pin J1-5, LW port 16 - { PTC4, false }, // pin J1-7, LW port 17 - { PTC5, false }, // pin J1-9, LW port 18 - { PTC6, false }, // pin J1-11, LW port 19 - { PTC10, false }, // pin J1-13, LW port 20 - { PTC11, false }, // pin J1-15, LW port 21 - { PTE0, false }, // pin J2-18, LW port 22 - { NC, false }, // Not used, LW port 23 - { NC, false }, // Not used, LW port 24 - { NC, false }, // Not used, LW port 25 - { NC, false }, // Not used, LW port 26 - { NC, false }, // Not used, LW port 27 - { NC, false }, // Not used, LW port 28 - { NC, false }, // Not used, LW port 29 - { NC, false }, // Not used, LW port 30 - { NC, false }, // Not used, LW port 31 - { NC, false } // Not used, LW port 32 -}; - - -// I2C address of the accelerometer (this is a constant of the KL25Z) -const int MMA8451_I2C_ADDRESS = (0x1d<<1); - -// SCL and SDA pins for the accelerometer (constant for the KL25Z) -#define MMA8451_SCL_PIN PTE25 -#define MMA8451_SDA_PIN PTE24 - -// Digital in pin to use for the accelerometer interrupt. For the KL25Z, -// this can be either PTA14 or PTA15, since those are the pins physically -// wired on this board to the MMA8451 interrupt controller. -#define MMA8451_INT_PIN PTA15 // Joystick axis report range - we report from -JOYMAX to +JOYMAX #define JOYMAX 4096 -// --------------------------------------------------------------------------- -// utilities +// -------------------------------------------------------------------------- +// +// Potentiometer configuration +// +#ifdef POT_SENSOR_ENABLED +#define IF_POT(x) x +#else +#define IF_POT(x) +#endif -// number of elements in an array -#define countof(x) (sizeof(x)/sizeof((x)[0])) + +// --------------------------------------------------------------------------- +// +// On-board RGB LED elements - we use these for diagnostic displays. +// +DigitalOut ledR(LED1), ledG(LED2), ledB(LED3); + // --------------------------------------------------------------------------- // @@ -701,48 +480,79 @@ return buttons; } -// Read buttons with debouncing. We keep a circular buffer -// of recent input readings. We'll AND together the status of -// each button over the past 50ms. A button that has been on -// continuously for 50ms will be reported as ON. All others -// will be reported as OFF. +// Read buttons with debouncing. +// +// Debouncing is the process of filtering out transients from button +// state changes. When an electrical switch is closed or opened, the +// signal can have a brief period of instability that makes the switch +// appear to turn on and off very rapidly. This is known as "bouncing". +// +// To remove the transients, we filter the signal by requiring each +// change to stick for at least a minimum interval (we use 50ms). We +// keep a short recent history of each button's state for this purpose. +// If we see a button change state, we ignore the change if we saw the +// same button make another change within the same interval. uint32_t readButtonsDebounced() { struct reading { - int dt; // time since previous reading - uint32_t b; // button state at this reading + // elapsed time between this reading and the previous reading + int dt; + + // Final button state for each button that changed on this + // report. OR this with a new report (after applying the + // mask 'm') to carry forward the changes that occurred in + // this report to the new report. + uint32_t b; + + // Change mask at this report. This is a bit mask of the buttons + // that *didn't* change on this report. AND this mask with a + // new reading to filter buttons out of the new reading that + // changed on this report. + uint32_t m; }; static reading readings[8]; // circular buffer of readings static int ri = 0; // reading buffer index (next write position) + static int bPrv = 0; // immediately previous report // get the write pointer reading *r = &readings[ri]; // figure the time since the last reading, and read the raw button state - r->dt = buttonTimer.read_ms(); - uint32_t b = r->b = readButtonsRaw(); + int ms = r->dt = buttonTimer.read_ms(); + uint32_t b = readButtonsRaw(); // start timing the next interval buttonTimer.reset(); - // AND together readings over 25ms - int ms = 0; - for (int i = 1 ; i < countof(readings) && ms < 25 ; ++i) + // mask out changes for any buttons that changed state within the + // past 50ms + for (int i = 1 ; i < countof(readings) && ms < 50 ; ++i) { // find the next prior reading, wrapping in the circular buffer int j = ri - i; if (j < 0) j = countof(readings) - 1; - reading *rj = &readings[j]; - - // AND the buttons for this reading - b &= rj->b; - - // count the time + + // For any button that changed state in the prior reading 'rj', + // remove any new change and restore it to its 'rj' state. + b &= rj->m; + b |= rj->b; + + // add in the time to the next prior report ms += rj->dt; } + // figure which buttons changed on this report vs the prior report + uint32_t m = b ^ bPrv; + + // save the change mask and changed button vector in our history entry + r->m = ~m; + r->b = b & m; + + // save this as the prior report + bPrv = b; + // advance the write position for next time ri += 1; if (ri >= countof(readings)) @@ -754,88 +564,6 @@ // --------------------------------------------------------------------------- // -// Non-volatile memory (NVM) -// - -// Structure defining our NVM storage layout. We store a small -// amount of persistent data in flash memory to retain calibration -// data when powered off. -struct NVM -{ - // checksum - we use this to determine if the flash record - // has been properly initialized - uint32_t checksum; - - // signature value - static const uint32_t SIGNATURE = 0x4D4A522A; - static const uint16_t VERSION = 0x0003; - - // Is the data structure valid? We test the signature and - // checksum to determine if we've been properly stored. - int valid() const - { - return (d.sig == SIGNATURE - && d.vsn == VERSION - && d.sz == sizeof(NVM) - && checksum == CRC32(&d, sizeof(d))); - } - - // save to non-volatile memory - void save(FreescaleIAP &iap, int addr) - { - // update the checksum and structure size - checksum = CRC32(&d, sizeof(d)); - d.sz = sizeof(NVM); - - // erase the sector - iap.erase_sector(addr); - - // save the data - iap.program_flash(addr, this, sizeof(*this)); - } - - // reset calibration data for calibration mode - void resetPlunger() - { - // set extremes for the calibration data - d.plungerMax = 0; - d.plungerZero = npix; - d.plungerMin = npix; - } - - // stored data (excluding the checksum) - struct - { - // Signature, structure version, and structure size - further verification - // that we have valid initialized data. The size is a simple proxy for a - // structure version, as the most common type of change to the structure as - // the software evolves will be the addition of new elements. We also - // provide an explicit version number that we can update manually if we - // make any changes that don't affect the structure size but would affect - // compatibility with a saved record (e.g., swapping two existing elements). - uint32_t sig; - uint16_t vsn; - int sz; - - // has the plunger been manually calibrated? - int plungerCal; - - // plunger calibration min and max - int plungerMin; - int plungerZero; - int plungerMax; - - // is the CCD enabled? - int ccdEnabled; - - // LedWiz unit number - uint8_t ledWizUnitNo; - } d; -}; - - -// --------------------------------------------------------------------------- -// // Customization joystick subbclass // @@ -904,6 +632,19 @@ // of nudging, say). // +// I2C address of the accelerometer (this is a constant of the KL25Z) +const int MMA8451_I2C_ADDRESS = (0x1d<<1); + +// SCL and SDA pins for the accelerometer (constant for the KL25Z) +#define MMA8451_SCL_PIN PTE25 +#define MMA8451_SDA_PIN PTE24 + +// Digital in pin to use for the accelerometer interrupt. For the KL25Z, +// this can be either PTA14 or PTA15, since those are the pins physically +// wired on this board to the MMA8451 interrupt controller. +#define MMA8451_INT_PIN PTA15 + + // accelerometer input history item, for gathering calibration data struct AccHist { @@ -1191,6 +932,113 @@ // --------------------------------------------------------------------------- // +// Include the appropriate plunger sensor definition. This will define a +// class called PlungerSensor, with a standard interface that we use in +// the main loop below. This is *kind of* like a virtual class interface, +// but it actually defines the methods statically, which is a little more +// efficient at run-time. There's no need for a true virtual interface +// because we don't need to be able to change sensor types on the fly. +// + +#ifdef ENABLE_CCD_SENSOR +#include "ccdSensor.h" +#elif ENABLE_POT_SENSOR +#include "potSensor.h" +#else +#include "nullSensor.h" +#endif + + +// --------------------------------------------------------------------------- +// +// Non-volatile memory (NVM) +// + +// Structure defining our NVM storage layout. We store a small +// amount of persistent data in flash memory to retain calibration +// data when powered off. +struct NVM +{ + // checksum - we use this to determine if the flash record + // has been properly initialized + uint32_t checksum; + + // signature value + static const uint32_t SIGNATURE = 0x4D4A522A; + static const uint16_t VERSION = 0x0003; + + // Is the data structure valid? We test the signature and + // checksum to determine if we've been properly stored. + int valid() const + { + return (d.sig == SIGNATURE + && d.vsn == VERSION + && d.sz == sizeof(NVM) + && checksum == CRC32(&d, sizeof(d))); + } + + // save to non-volatile memory + void save(FreescaleIAP &iap, int addr) + { + // update the checksum and structure size + checksum = CRC32(&d, sizeof(d)); + d.sz = sizeof(NVM); + + // erase the sector + iap.erase_sector(addr); + + // save the data + iap.program_flash(addr, this, sizeof(*this)); + } + + // reset calibration data for calibration mode + void resetPlunger() + { + // set extremes for the calibration data + d.plungerMax = 0; + d.plungerZero = npix; + d.plungerMin = npix; + } + + // stored data (excluding the checksum) + struct + { + // Signature, structure version, and structure size - further verification + // that we have valid initialized data. The size is a simple proxy for a + // structure version, as the most common type of change to the structure as + // the software evolves will be the addition of new elements. We also + // provide an explicit version number that we can update manually if we + // make any changes that don't affect the structure size but would affect + // compatibility with a saved record (e.g., swapping two existing elements). + uint32_t sig; + uint16_t vsn; + int sz; + + // has the plunger been manually calibrated? + int plungerCal; + + // Plunger calibration min, zero, and max. The zero point is the + // rest position (aka park position), where it's in equilibrium between + // the main spring and the barrel spring. It can travel a small distance + // forward of the rest position, because the barrel spring can be + // compressed by the user pushing on the plunger or by the momentum + // of a release motion. The minimum is the maximum forward point where + // the barrel spring can't be compressed any further. + int plungerMin; + int plungerZero; + int plungerMax; + + // is the plunger sensor enabled? + int plungerEnabled; + + // LedWiz unit number + uint8_t ledWizUnitNo; + } d; +}; + + +// --------------------------------------------------------------------------- +// // Main program loop. This is invoked on startup and runs forever. Our // main work is to read our devices (the accelerometer and the CCD), process // the readings into nudge and plunger position data, and send the results @@ -1239,11 +1087,11 @@ cfg.d.sig = cfg.SIGNATURE; cfg.d.vsn = cfg.VERSION; cfg.d.plungerCal = 0; - cfg.d.plungerZero = 0; - cfg.d.plungerMin = 0; - cfg.d.plungerMax = npix; + cfg.d.plungerMin = 0; // assume we can go all the way forward... + cfg.d.plungerMax = npix; // ...and all the way back + cfg.d.plungerZero = npix/6; // the rest position is usually around 1/2" back cfg.d.ledWizUnitNo = DEFAULT_LEDWIZ_UNIT_NUMBER; - cfg.d.ccdEnabled = true; + cfg.d.plungerEnabled = true; } // Create the joystick USB client. Note that we use the LedWiz unit @@ -1252,6 +1100,15 @@ USB_VENDOR_ID, USB_PRODUCT_ID | cfg.d.ledWizUnitNo, USB_VERSION_NO); + + // last report timer - we use this to throttle reports, since VP + // doesn't want to hear from us more than about every 10ms + Timer reportTimer; + reportTimer.start(); + + // initialize the calibration buttons, if present + DigitalIn *calBtn = (CAL_BUTTON_PIN == NC ? 0 : new DigitalIn(CAL_BUTTON_PIN)); + DigitalOut *calBtnLed = (CAL_BUTTON_LED == NC ? 0 : new DigitalOut(CAL_BUTTON_LED)); // plunger calibration button debounce timer Timer calBtnTimer; @@ -1278,32 +1135,108 @@ // create the accelerometer object Accel accel(MMA8451_SCL_PIN, MMA8451_SDA_PIN, MMA8451_I2C_ADDRESS, MMA8451_INT_PIN); - // create the CCD array object - TSL1410R ccd(PTE20, PTE21, PTB0); + // last accelerometer report, in joystick units (we report the nudge + // acceleration via the joystick x & y axes, per the VP convention) + int x = 0, y = 0; + + // create our plunger sensor object + PlungerSensor plungerSensor; + + // last plunger report position, in 'npix' normalized pixel units + int pos = 0; + + // last plunger report, in joystick units (we report the plunger as the + // "z" axis of the joystick, per the VP convention) + int z = 0; + + // most recent prior plunger readings, for tracking release events(z0 is + // reading just before the last one we reported, z1 is the one before that, + // z2 the next before that) + int z0 = 0, z1 = 0, z2 = 0; + + // Simulated "bounce" position when firing. We model the bounce off of + // the barrel spring when the plunger is released as proportional to the + // distance it was retracted just before being released. + int zBounce = 0; - // last accelerometer report, in mouse coordinates - int x = 0, y = 0, z = 0; + // Simulated Launch Ball button state. If a "ZB Launch Ball" port is + // defined for our LedWiz port mapping, any time that port is turned ON, + // we'll simulate pushing the Launch Ball button if the player pulls + // back and releases the plunger, or simply pushes on the plunger from + // the rest position. This allows the plunger to be used in lieu of a + // physical Launch Ball button for tables that don't have plungers. + // + // States: + // 0 = default + // 1 = cocked (plunger has been pulled back about 1" from state 0) + // 2 = uncocked (plunger is pulled back less than 1" from state 1) + // 3 = launching (plunger has been released from state 1 or 2, or + // pushed forward about 1/4" from state 0) + // 4 = launching, plunger is no longer pushed forward + int lbState = 0; - // previous two plunger readings, for "debouncing" the results (z0 is - // the most recent, z1 is the one before that) - int z0 = 0, z1 = 0, z2 = 0; + // Time since last lbState transition. Some of the states are time- + // sensitive. In the "uncocked" state, we'll return to state 0 if + // we remain in this state for more than a few milliseconds, since + // it indicates that the plunger is being slowly returned to rest + // rather than released. In the "launching" state, we need to release + // the Launch Ball button after a moment, and we need to wait for + // the plunger to come to rest before returning to state 0. + Timer lbTimer; + lbTimer.start(); + + // Simulated button states. This is a vector of button states + // for the simulated buttons. We combine this with the physical + // button states on each USB joystick report, so we will report + // a button as pressed if either the physical button is being pressed + // or we're simulating a press on the button. This is used for the + // simulated Launch Ball button. + uint32_t simButtons = 0; // Firing in progress: we set this when we detect the start of rapid // plunger movement from a retracted position towards the rest position. - // The actual plunger spring return speed seems to be too slow for VP, - // so when we detect the start of this motion, we immediately tell VP - // to return the plunger to rest, then we monitor the real plunger - // until it atcually stops. + // + // When we detect a firing event, we send VP a series of synthetic + // reports simulating the idealized plunger motion. The actual physical + // motion is much too fast to report to VP; in the time between two USB + // reports, the plunger can shoot all the way forward, rebound off of + // the barrel spring, bounce back part way, and bounce forward again, + // or even do all of this more than once. This means that sampling the + // physical motion at the USB report rate would create a misleading + // picture of the plunger motion, since our samples would catch the + // plunger at random points in this oscillating motion. From the + // user's perspective, the physical action that occurred is simply that + // the plunger was released from a particular distance, so it's this + // high-level event that we want to convey to VP. To do this, we + // synthesize a series of reports to convey an idealized version of + // the release motion that's perfectly synchronized to the VP reports. + // Essentially we pretend that our USB position samples are exactly + // aligned in time with (1) the point of retraction just before the + // user released the plunger, (2) the point of maximum forward motion + // just after the user released the plunger (the point of maximum + // compression as the plunger bounces off of the barrel spring), and + // (3) the plunger coming to rest at the park position. This series + // of reports is synthetic in the sense that it's not what we actually + // see on the CCD at the times of these reports - the true plunger + // position is oscillating at high speed during this period. But at + // the same time it conveys a more faithful picture of the true physical + // motion to VP, and allows VP to reproduce the true physical motion + // more faithfully in its simulation model, by correcting for the + // relatively low sampling rate in the communication path between the + // real plunger and VP's model plunger. + // + // If 'firing' is non-zero, it's the index of our current report in + // the synthetic firing report series. int firing = 0; // start the first CCD integration cycle - ccd.clear(); + plungerSensor.init(); // Device status. We report this on each update so that the host config // tool can detect our current settings. This is a bit mask consisting // of these bits: // 0x01 -> plunger sensor enabled - uint16_t statusFlags = (cfg.d.ccdEnabled ? 0x01 : 0x00); + uint16_t statusFlags = (cfg.d.plungerEnabled ? 0x01 : 0x00); // flag: send a pixel dump after the next read bool reportPix = false; @@ -1366,13 +1299,13 @@ // set the configuration parameters from the message cfg.d.ledWizUnitNo = newUnitNo; - cfg.d.ccdEnabled = data[3] & 0x01; + cfg.d.plungerEnabled = data[3] & 0x01; // update the status flags statusFlags = (statusFlags & ~0x01) | (data[3] & 0x01); // if the ccd is no longer enabled, use 0 for z reports - if (!cfg.d.ccdEnabled) + if (!cfg.d.plungerEnabled) z = 0; // save the configuration @@ -1425,7 +1358,7 @@ } // check for plunger calibration - if (!calBtn) + if (calBtn != 0 && !calBtn->read()) { // check the state switch (calBtnState) @@ -1516,283 +1449,321 @@ { calBtnLit = newCalBtnLit; if (calBtnLit) { - calBtnLed = 1; + if (calBtnLed != 0) + calBtnLed->write(1); ledR = 1; ledG = 1; ledB = 0; } else { - calBtnLed = 0; + if (calBtnLed != 0) + calBtnLed->write(0); ledR = 1; ledG = 1; ledB = 1; } } + // If the plunger is enabled, and we're not already in a firing event, + // and the last plunger reading had the plunger pulled back at least + // a bit, watch for plunger release events until it's time for our next + // USB report. + if (!firing && cfg.d.plungerEnabled && z >= JOYMAX/6) + { + // monitor the plunger until it's time for our next report + while (reportTimer.read_ms() < 15) + { + // do a fast low-res scan; if it's at or past the zero point, + // start a firing event + if (plungerSensor.lowResScan() <= cfg.d.plungerZero) + firing = 1; + } + } + // read the plunger sensor, if it's enabled - uint16_t pix[npix]; - if (cfg.d.ccdEnabled) + if (cfg.d.plungerEnabled) { // start with the previous reading, in case we don't have a // clear result on this frame int znew = z; - - // read the array - ccd.read(pix, npix, ccdReadCB, 0, 3); - - // get the average brightness at each end of the sensor - long avg1 = (long(pix[0]) + long(pix[1]) + long(pix[2]) + long(pix[3]) + long(pix[4]))/5; - long avg2 = (long(pix[npix-1]) + long(pix[npix-2]) + long(pix[npix-3]) + long(pix[npix-4]) + long(pix[npix-5]))/5; - - // figure the midpoint in the brightness; multiply by 3 so that we can - // compare sums of three pixels at a time to smooth out noise - long midpt = (avg1 + avg2)/2 * 3; - - // Work from the bright end to the dark end. VP interprets the - // Z axis value as the amount the plunger is pulled: zero is the - // rest position, and the axis maximum is fully pulled. So we - // essentially want to report how much of the sensor is lit, - // since this increases as the plunger is pulled back. - int si = 1, di = 1; - if (avg1 < avg2) - si = npix - 2, di = -1; - - // If the bright end and dark end don't differ by enough, skip this - // reading entirely - we must have an overexposed or underexposed frame. - // Otherwise proceed with the scan. - if (labs(avg1 - avg2) > 0x1000) + if (plungerSensor.highResScan(pos)) { - uint16_t *pixp = pix + si; - for (int n = 1 ; n < npix - 1 ; ++n, pixp += di) + // We got a new reading. If we're in calibration mode, use it + // to figure the new calibration, otherwise adjust the new reading + // for the established calibration. + if (calBtnState == 3) { - // if we've crossed the midpoint, report this position - if (long(pixp[-1]) + long(pixp[0]) + long(pixp[1]) < midpt) - { - // note the new position - int pos = n; + // Calibration mode. If this reading is outside of the current + // calibration bounds, expand the bounds. + if (pos < cfg.d.plungerMin) + cfg.d.plungerMin = pos; + if (pos < cfg.d.plungerZero) + cfg.d.plungerZero = pos; + if (pos > cfg.d.plungerMax) + cfg.d.plungerMax = pos; - // Calibrate, or apply calibration, depending on the mode. - // In either case, normalize to our range. VP appears to - // ignore negative Z axis values. - if (calBtnState == 3) - { - // calibrating - note if we're expanding the calibration envelope - if (pos < cfg.d.plungerMin) - cfg.d.plungerMin = pos; - if (pos < cfg.d.plungerZero) - cfg.d.plungerZero = pos; - if (pos > cfg.d.plungerMax) - cfg.d.plungerMax = pos; - - // normalize to the full physical range while calibrating - znew = int(round(float(pos)/npix * JOYMAX)); - } - else - { - // Running normally - normalize to the calibration range. Note - // that values below the zero point are allowed - the zero point - // represents the park position, where the plunger sits when at - // rest, but a mechanical plunger has a smmall amount of travel - // in the "push" direction. We represent forward travel with - // negative z values. - if (pos > cfg.d.plungerMax) - pos = cfg.d.plungerMax; - znew = int(round(float(pos - cfg.d.plungerZero) - / (cfg.d.plungerMax - cfg.d.plungerZero + 1) * JOYMAX)); - } - - // done - break; - } + // normalize to the full physical range while calibrating + znew = int(round(float(pos)/npix * JOYMAX)); + } + else + { + // Not in calibration mode, so normalize the new reading to the + // established calibration range. + // + // Note that negative values are allowed. Zero represents the + // "park" position, where the plunger sits when at rest. A mechanical + // plunger has a smmall amount of travel in the "push" direction, + // since the barrel spring can be compressed slightly. Negative + // values represent travel in the push direction. + if (pos > cfg.d.plungerMax) + pos = cfg.d.plungerMax; + znew = int(round(float(pos - cfg.d.plungerZero) + / (cfg.d.plungerMax - cfg.d.plungerZero + 1) * JOYMAX)); } } - // Determine if the plunger is being fired - i.e., if the player - // has just released the plunger from a retracted position. - // - // We treat firing as an event. That is, we tell VP when the - // plunger is fired, and then stop sending data until the firing - // is complete, allowing VP to carry out the firing motion using - // its internal model plunger rather than trying to track the - // intermediate positions of the mechanical plunger throughout - // the firing motion. This is essential because the firing - // motion is too fast for us to track - in the time it takes us - // to read one frame, the plunger can make it all the way to the - // zero position and bounce back halfway. Fortunately, the range - // of motions for the plunger is limited, so if we see any rapid - // change of position toward the rest position, it's reasonably - // safe to interpret it as a firing event. - // - // This isn't foolproof. The user can trick us by doing a - // controlled rapid forward push but stopping short of the rest - // position. We'll misinterpret that as a firing event. But - // that's not a natural motion that a user would make with a - // plunger, so it's probably an acceptable false positive. - // - // Possible future enhancement: we could add a second physical - // sensor that detects when the plunger reaches the zero position - // and asserts an interrupt. In the interrupt handler, set a - // flag indicating the zero position signal. On each scan of - // the CCD, also check that flag; if it's set, enter firing - // event mode just as we do now. The key here is that the - // secondary sensor would have to be something much faster - // than our CCD scan - it would have to react on, say, the - // millisecond time scale. A simple mechanical switch or a - // proximity sensor could work. This would let us detect - // with certainty when the plunger physically fires, eliminating - // the case where the use can fool us with motion that's fast - // enough to look like a release but doesn't actually reach the - // starting position. + // If we're not already in a firing event, check to see if the + // new position is forward of the last report. If it is, a firing + // event might have started during the high-res scan. This might + // seem unlikely given that the scan only takes about 5ms, but that + // 5ms represents about 25-30% of our total time between reports, + // there's about a 1 in 4 chance that a release starts during a + // scan. + if (!firing && z0 > 0 && znew < z0) + { + // The plunger has moved forward since the previous report. + // Watch it for a few more ms to see if we can get a stable + // new position. + int pos1 = plungerSensor.lowResScan(); + Timer tw; + tw.start(); + while (tw.read_ms() < 6) + { + // if we've crossed the rest position, it's a firing event + if (pos1 < cfg.d.plungerZero) + { + firing = 1; + break; + } + + // read the new position + int pos2 = plungerSensor.lowResScan(); + + // if it's stable, stop looping + if (abs(pos2 - pos1) < int(npix/(3.2*8))) + break; + + // the new reading is now the prior reading + pos1 = pos2; + } + } + + // Check for a simulated Launch Ball button press, if enabled + if (ZBLaunchBallPort != 0 && wizOn[ZBLaunchBallPort-1]) + { + int newState = lbState; + switch (lbState) + { + case 0: + // Base state. If the plunger is pulled back by an inch + // or more, go to "cocked" state. If the plunger is pushed + // forward by 1/4" or more, go to "launch" state. + if (znew >= JOYMAX/3) + newState = 1; + else if (znew < -JOYMAX/12) + newState = 3; + break; + + case 1: + // Cocked state. If a firing event is now in progress, + // go to "launch" state. Otherwise, if the plunger is less + // than 1" retracted, go to "uncocked" state - the player + // might be slowly returning the plunger to rest so as not + // to trigger a launch. + if (firing || znew <= 0) + newState = 3; + else if (znew < JOYMAX/3) + newState = 2; + break; + + case 2: + // Uncocked state. If the plunger is more than an inch + // retracted, return to cocked state. If we've been in + // the uncocked state for more than half a second, return + // to the base state. + if (znew >= JOYMAX/3) + newState = 1; + else if (lbTimer.read_ms() > 500) + newState = 0; + break; + + case 3: + // Launch state. If the plunger is no longer pushed + // forward, switch to launch rest state. + if (znew > -JOYMAX/24) + newState = 4; + break; + + case 4: + // Launch rest state. If the plunger is pushed forward + // again, switch back to launch state. If not, and we've + // been in this state for at least 200ms, return to the + // default state. + if (znew < -JOYMAX/12) + newState = 3; + else if (lbTimer.read_ms() > 200) + newState = 0; + break; + } + + // change states if desired + if (newState != lbState) + { + // if we're entering Launch state, press the Launch Ball button + if (newState == 3 && lbState != 4) + simButtons |= (1 << (LaunchBallButton - 1)); + + // if we're switching to state 0, release the button + if (newState == 0) + simButtons &= ~(1 << (LaunchBallButton - 1)); + + // switch to the new state + lbState = newState; + + // start timing in the new state + lbTimer.reset(); + } + } + + // If a firing event is in progress, generate synthetic reports to + // describe an idealized version of the plunger motion to VP rather + // than reporting the actual physical plunger position. // - // To detremine when a firing even occurs, we watch for rapid - // motion from a retracted position towards the rest position - - // that is, large position changes in the negative direction over - // a couple of consecutive readings. When we see a rapid move - // toward zero, we set our internal 'firing' flag, immediately - // report to VP that the plunger has returned to the zero - // position, and then suspend reports until the mechanical - // readings indicate that the plunger has come to rest (indicated - // by several readings in a row at roughly the same position). + // We use the synthetic reports during a release event because the + // physical plunger motion when released is too fast for VP to track. + // VP only syncs its internal physics model with the outside world + // about every 10ms. In that amount of time, the plunger moves + // fast enough when released that it can shoot all the way forward, + // bounce off of the barrel spring, and rebound part of the way + // back. The result is the classic analog-to-digital problem of + // sample aliasing. If we happen to time our sample during the + // release motion so that we catch the plunger at the peak of a + // bounce, the digital signal incorrectly looks like the plunger + // is moving slowly forward - VP thinks we went from fully + // retracted to half retracted in the sample interval, whereas + // we actually traveled all the way forward and half way back, + // so the speed VP infers is about 1/3 of the actual speed. // - // Tolerance for firing is 1/3 of the current pull distance, or - // about 1/2", whichever is greater. Making this value too small - // makes for too many false positives. Empirically, 1/4" is too - // twitchy, so set a floor at about 1/2". But we can be less - // sensitive the further back the plunger is pulled, since even - // a long pull will snap back quickly. Note that JOYMAX always - // corresponds to about 3", no matter how many pixels we're - // reading, since the physical sensor is about 3" long; so we - // factor out the pixel count calculate (approximate) physical - // distances based on the normalized axis range. - // - // Firing pattern: when firing, don't simply report a solid 0, - // but instead report a series of pseudo-bouces. This looks - // more realistic, beacause the real plunger is also bouncing - // around during this time. To get maximum firing power in - // the simulation, though, our pseudo-bounces are tiny cmopared - // to the real thing. - const int restTol = JOYMAX/24; - int fireTol = z/3 > JOYMAX/6 ? z/3 : JOYMAX/6; - static const int firePattern[] = { - -JOYMAX/12, -JOYMAX/12, -JOYMAX/12, - }; - if (firing != 0) + // To correct this, we take advantage of our ability to sample + // the CCD image several times in the course of a VP report. If + // we catch the plunger near the origin after we've seen it + // retracted, we go into Release Event mode. During this mode, + // we stop reporting the true physical plunger position, and + // instead report an idealized pattern: we report the plunger + // immediately shooting forward to a position in front of the + // park position that's in proportion to how far back the plunger + // was just before the release, and we then report it stationary + // at the park position. We continue to report the stationary + // park position until the actual physical plunger motion has + // stabilized on a new position. We then exit Release Event + // mode and return to reporting the true physical position. + if (firing) { - // Firing in progress - we've already told VP to send its - // model plunger all the way back to the rest position, so - // send no further reports until the mechanical plunger - // actually comes to rest somewhere. - if (abs(z0 - z2) < restTol && abs(znew - z2) < restTol) + // Firing in progress. Keep reporting the park position + // until the physical plunger position comes to rest. + const int restTol = JOYMAX/24; + if (firing == 1) { - // the plunger is back at rest - firing is done - firing = 0; - - // resume normal reporting - z = z2; + // For the first couple of frames, show the plunger shooting + // forward past the zero point, to simulate the momentum carrying + // it forward to bounce off of the barrel spring. Show the + // bounce as proportional to the distance it was retracted + // in the prior report. + z = zBounce = -z0/6; + ++firing; } - else if (firing < countof(firePattern)) + else if (firing == 2) { - // firing - report the next position in the pseudo-bounce - // pattern - z = firePattern[firing++]; + // second frame - keep the bounce a little longer + z = zBounce; + ++firing; + } + else if (firing > 4 + && abs(znew - z0) < restTol + && abs(znew - z1) < restTol + && abs(znew - z2) < restTol) + { + // The physical plunger has come to rest. Exit firing + // mode and resume reporting the actual position. + firing = false; + z = znew; } else { - // firing, out of pseudo-bounce items - just report the - // rest position + // until the physical plunger comes to rest, simply + // report the park position z = 0; + ++firing; } } - else if (z0 < z2 && z1 < z2 && znew < z2 - && (z0 < z2 - fireTol - || z1 < z2 - fireTol - || znew < z2 - fireTol)) - { - // Big jumps toward rest position in last two readings - - // firing has begun. Report an immediate return to the - // rest position, and send no further reports until the - // physical plunger has come to rest. This effectively - // detaches VP's model plunger from the real world for - // the duration of the spring return, letting VP evolve - // its model without trying to synchronize with the - // mechanical version. The release motion is too fast - // for that to work well; we can't take samples quickly - // enough to get prcise velocity or acceleration - // readings. It's better to let VP figure the speed - // and acceleration through modeling. Plus, that lets - // each virtual table set the desired parameters for its - // virtual plunger, rather than imposing the actual - // mechanical charateristics of the physical plunger on - // every table. - firing = 1; - - // report the first firing pattern position - z = firePattern[0]; - } else { - // everything normal; report the 3rd recent position on - // tape delay - z = z2; + // not in firing mode - report the true physical position + z = znew; } - - // shift in the new reading + + // shift the new reading into the recent history buffer z2 = z1; z1 = z0; z0 = znew; } - else - { - // plunger disabled - pause 10ms to throttle updates to a - // reasonable pace - wait_ms(10); - } - // read the accelerometer - int xa, ya; - accel.get(xa, ya); - - // confine the results to our joystick axis range - if (xa < -JOYMAX) xa = -JOYMAX; - if (xa > JOYMAX) xa = JOYMAX; - if (ya < -JOYMAX) ya = -JOYMAX; - if (ya > JOYMAX) ya = JOYMAX; - - // store the updated accelerometer coordinates - x = xa; - y = ya; - // update the buttons uint32_t buttons = readButtonsDebounced(); - - // Send the status report. Note that the nominal x and y axes - // are reversed - this makes it more intuitive to set up in VP. - // If we mount the Freesale card flat on the floor of the cabinet - // with the USB connectors facing the front of the cabinet, this - // arrangement of our nominal axes aligns with VP's standard - // setting, so that we can configure VP with X Axis = X on the - // joystick and Y Axis = Y on the joystick. - js.update(y, x, z, buttons, statusFlags); + + // If it's been long enough since our last USB status report, + // send the new report. We throttle the report rate because + // it can overwhelm the PC side if we report too frequently. + // VP only wants to sync with the real world in 10ms intervals, + // so reporting more frequently only creates i/o overhead + // without doing anything to improve the simulation. + if (reportTimer.read_ms() > 15) + { + // read the accelerometer + int xa, ya; + accel.get(xa, ya); + + // confine the results to our joystick axis range + if (xa < -JOYMAX) xa = -JOYMAX; + if (xa > JOYMAX) xa = JOYMAX; + if (ya < -JOYMAX) ya = -JOYMAX; + if (ya > JOYMAX) ya = JOYMAX; + + // store the updated accelerometer coordinates + x = xa; + y = ya; + + // Send the status report. Note that the nominal x and y axes + // are reversed - this makes it more intuitive to set up in VP. + // If we mount the Freesale card flat on the floor of the cabinet + // with the USB connectors facing the front of the cabinet, this + // arrangement of our nominal axes aligns with VP's standard + // setting, so that we can configure VP with X Axis = X on the + // joystick and Y Axis = Y on the joystick. + js.update(y, x, z, buttons | simButtons, statusFlags); + + // we've just started a new report interval, so reset the timer + reportTimer.reset(); + } // If we're in pixel dump mode, report all pixel exposure values if (reportPix) { + // send the report + plungerSensor.sendExposureReport(js); + // we have satisfied this request reportPix = false; - - // send reports for all pixels - int idx = 0; - while (idx < npix) - js.updateExposure(idx, npix, pix); - - // The pixel dump requires many USB reports, since each report - // can only send a few pixel values. An integration cycle has - // been running all this time, since each read starts a new - // cycle. Our timing is longer than usual on this round, so - // the integration won't be comparable to a normal cycle. Throw - // this one away by doing a read now, and throwing it away - that - // will get the timing of the *next* cycle roughly back to normal. - ccd.read(pix, npix); } #ifdef DEBUG_PRINTF @@ -1832,7 +1803,7 @@ ledG = (hb ? 1 : 0); ledB = 0; } - else if (cfg.d.ccdEnabled && !cfg.d.plungerCal) + else if (cfg.d.plungerEnabled && !cfg.d.plungerCal) { // connected, plunger calibration needed - flash yellow/green hb = !hb;