An I/O controller for virtual pinball machines: accelerometer nudge sensing, analog plunger input, button input encoding, LedWiz compatible output controls, and more.

Dependencies:   mbed FastIO FastPWM USBDevice

Fork of Pinscape_Controller by Mike R


This is Version 2 of the Pinscape Controller, an I/O controller for virtual pinball machines. (You can find the old version 1 software here.) Pinscape is software for the KL25Z that turns the board into a full-featured I/O controller for virtual pinball, with support for accelerometer-based nudging, a real plunger, button inputs, and feedback device control.

In case you haven't heard of the concept before, a "virtual pinball machine" is basically a video pinball simulator that's built into a real pinball machine body. A TV monitor goes in place of the pinball playfield, and a second TV goes in the backbox to serve as the "backglass" display. A third smaller monitor can serve as the "DMD" (the Dot Matrix Display used for scoring on newer machines), or you can even install a real pinball plasma DMD. A computer is hidden inside the cabinet, running pinball emulation software that displays a life-sized playfield on the main TV. The cabinet has all of the usual buttons, too, so it not only looks like the real thing, but plays like it too. That's a picture of my own machine to the right. On the outside, it's built exactly like a real arcade pinball machine, with the same overall dimensions and all of the standard pinball cabinet hardware.

A few small companies build and sell complete, finished virtual pinball machines, but I think it's more fun as a DIY project. If you have some basic wood-working skills and know your way around PCs, you can build one from scratch. The computer part is just an ordinary Windows PC, and all of the pinball emulation can be built out of free, open-source software. In that spirit, the Pinscape Controller is an open-source software/hardware project that offers a no-compromises, all-in-one control center for all of the unique input/output needs of a virtual pinball cabinet. If you've been thinking about building one of these, but you're not sure how to connect a plunger, flipper buttons, lights, nudge sensor, and whatever else you can think of, this project might be just what you're looking for.

You can find much more information about DIY Pin Cab building in general in the Virtual Cabinet Forum on Also visit my Pinscape Resources page for more about this project and other virtual pinball projects I'm working on.


  • Pinscape Release Builds: This page has download links for all of the Pinscape software. To get started, install and run the Pinscape Config Tool on your Windows computer. It will lead you through the steps for installing the Pinscape firmware on the KL25Z.
  • Config Tool Source Code. The complete C# source code for the config tool. You don't need this to run the tool, but it's available if you want to customize anything or see how it works inside.


The new Version 2 Build Guide is now complete! This new version aims to be a complete guide to building a virtual pinball machine, including not only the Pinscape elements but all of the basics, from sourcing parts to building all of the hardware.

You can also refer to the original Hardware Build Guide (PDF), but that's out of date now, since it refers to the old version 1 software, which was rather different (especially when it comes to configuration).

System Requirements

The new Config Tool requires a fairly up-to-date Microsoft .NET installation. If you use Windows Update to keep your system current, you should be fine. A modern version of Internet Explorer (IE) is required, even if you don't use it as your main browser, because the Config Tool uses some system components that Microsoft packages into the IE install set. I test with IE11, so that's known to work. IE8 doesn't work. IE9 and 10 are unknown at this point.

The Windows requirements are only for the config tool. The firmware doesn't care about anything on the Windows side, so if you can make do without the config tool, you can use almost any Windows setup.

Main Features

Plunger: The Pinscape Controller started out as a "mechanical plunger" controller: a device for attaching a real pinball plunger to the video game software so that you could launch the ball the natural way. This is still, of course, a central feature of the project. The software supports several types of sensors: a high-resolution optical sensor (which works by essentially taking pictures of the plunger as it moves); a slide potentiometer (which determines the position via the changing electrical resistance in the pot); a quadrature sensor (which counts bars printed on a special guide rail that it moves along); and an IR distance sensor (which determines the position by sending pulses of light at the plunger and measuring the round-trip travel time). The Build Guide explains how to set up each type of sensor.

Nudging: The KL25Z (the little microcontroller that the software runs on) has a built-in accelerometer. The Pinscape software uses it to sense when you nudge the cabinet, and feeds the acceleration data to the pinball software on the PC. This turns physical nudges into virtual English on the ball. The accelerometer is quite sensitive and accurate, so we can measure the difference between little bumps and hard shoves, and everything in between. The result is natural and immersive.

Buttons: You can wire real pinball buttons to the KL25Z, and the software will translate the buttons into PC input. You have the option to map each button to a keyboard key or joystick button. You can wire up your flipper buttons, Magna Save buttons, Start button, coin slots, operator buttons, and whatever else you need.

Feedback devices: You can also attach "feedback devices" to the KL25Z. Feedback devices are things that create tactile, sound, and lighting effects in sync with the game action. The most popular PC pinball emulators know how to address a wide variety of these devices, and know how to match them to on-screen action in each virtual table. You just need an I/O controller that translates commands from the PC into electrical signals that turn the devices on and off. The Pinscape Controller can do that for you.

Expansion Boards

There are two main ways to run the Pinscape Controller: standalone, or using the "expansion boards".

In the basic standalone setup, you just need the KL25Z, plus whatever buttons, sensors, and feedback devices you want to attach to it. This mode lets you take advantage of everything the software can do, but for some features, you'll have to build some ad hoc external circuitry to interface external devices with the KL25Z. The Build Guide has detailed plans for exactly what you need to build.

The other option is the Pinscape Expansion Boards. The expansion boards are a companion project, which is also totally free and open-source, that provides Printed Circuit Board (PCB) layouts that are designed specifically to work with the Pinscape software. The PCB designs are in the widely used EAGLE format, which many PCB manufacturers can turn directly into physical boards for you. The expansion boards organize all of the external connections more neatly than on the standalone KL25Z, and they add all of the interface circuitry needed for all of the advanced software functions. The big thing they bring to the table is lots of high-power outputs. The boards provide a modular system that lets you add boards to add more outputs. If you opt for the basic core setup, you'll have enough outputs for all of the toys in a really well-equipped cabinet. If your ambitions go beyond merely well-equipped and run to the ridiculously extravagant, just add an extra board or two. The modular design also means that you can add to the system over time.

Expansion Board project page

Update notes

If you have a Pinscape V1 setup already installed, you should be able to switch to the new version pretty seamlessly. There are just a couple of things to be aware of.

First, the "configuration" procedure is completely different in the new version. Way better and way easier, but it's not what you're used to from V1. In V1, you had to edit the project source code and compile your own custom version of the program. No more! With V2, you simply install the standard, pre-compiled .bin file, and select options using the Pinscape Config Tool on Windows.

Second, if you're using the TSL1410R optical sensor for your plunger, there's a chance you'll need to boost your light source's brightness a little bit. The "shutter speed" is faster in this version, which means that it doesn't spend as much time collecting light per frame as before. The software actually does "auto exposure" adaptation on every frame, so the increased shutter speed really shouldn't bother it, but it does require a certain minimum level of contrast, which requires a certain minimal level of lighting. Check the plunger viewer in the setup tool if you have any problems; if the image looks totally dark, try increasing the light level to see if that helps.

New Features

V2 has numerous new features. Here are some of the highlights...

Dynamic configuration: as explained above, configuration is now handled through the Config Tool on Windows. It's no longer necessary to edit the source code or compile your own modified binary.

Improved plunger sensing: the software now reads the TSL1410R optical sensor about 15x faster than it did before. This allows reading the sensor at full resolution (400dpi), about 400 times per second. The faster frame rate makes a big difference in how accurately we can read the plunger position during the fast motion of a release, which allows for more precise position sensing and faster response. The differences aren't dramatic, since the sensing was already pretty good even with the slower V1 scan rate, but you might notice a little better precision in tricky skill shots.

Keyboard keys: button inputs can now be mapped to keyboard keys. The joystick button option is still available as well, of course. Keyboard keys have the advantage of being closer to universal for PC pinball software: some pinball software can be set up to take joystick input, but nearly all PC pinball emulators can take keyboard input, and nearly all of them use the same key mappings.

Local shift button: one physical button can be designed as the local shift button. This works like a Shift button on a keyboard, but with cabinet buttons. It allows each physical button on the cabinet to have two PC keys assigned, one normal and one shifted. Hold down the local shift button, then press another key, and the other key's shifted key mapping is sent to the PC. The shift button can have a regular key mapping of its own as well, so it can do double duty. The shift feature lets you access more functions without cluttering your cabinet with extra buttons. It's especially nice for less frequently used functions like adjusting the volume or activating night mode.

Night mode: the output controller has a new "night mode" option, which lets you turn off all of your noisy devices with a single button, switch, or PC command. You can designate individual ports as noisy or not. Night mode only disables the noisemakers, so you still get the benefit of your flashers, button lights, and other quiet devices. This lets you play late into the night without disturbing your housemates or neighbors.

Gamma correction: you can designate individual output ports for gamma correction. This adjusts the intensity level of an output to make it match the way the human eye perceives brightness, so that fades and color mixes look more natural in lighting devices. You can apply this to individual ports, so that it only affects ports that actually have lights of some kind attached.

IR Remote Control: the controller software can transmit and/or receive IR remote control commands if you attach appropriate parts (an IR LED to send, an IR sensor chip to receive). This can be used to turn on your TV(s) when the system powers on, if they don't turn on automatically, and for any other functions you can think of requiring IR send/receive capabilities. You can assign IR commands to cabinet buttons, so that pressing a button on your cabinet sends a remote control command from the attached IR LED, and you can have the controller generate virtual key presses on your PC in response to received IR commands. If you have the IR sensor attached, the system can use it to learn commands from your existing remotes.

Yet more USB fixes: I've been gradually finding and fixing USB bugs in the mbed library for months now. This version has all of the fixes of the last couple of releases, of course, plus some new ones. It also has a new "last resort" feature, since there always seems to be "just one more" USB bug. The last resort is that you can tell the device to automatically reboot itself if it loses the USB connection and can't restore it within a given time limit.

More Downloads

  • 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 releases, so you don't need my custom builds if you're using 9.9.1 or later and/or VP 10. I don't think there's any reason to use my versions instead of the latest official ones, and in fact I'd encourage you to use the official releases since they're more up to date, but I'm leaving my builds available just in case. In the official versions, look for the checkbox "Enable Nudge Filter" in the Keys preferences dialog. My custom versions don't include that checkbox; they just enable the filter unconditionally.
  • Output circuit shopping list: This is a saved shopping cart at with the parts needed to build one copy of the high-power output circuit for the LedWiz emulator feature, for use with the standalone KL25Z (that is, without the expansion boards). The quantities in the cart are for one output channel, so if you want N outputs, simply multiply the quantities by the N, with one exception: you only need one ULN2803 transistor array chip for each eight output circuits. If you're using the expansion boards, you won't need any of this, since the boards provide their own high-power outputs.
  • Cary Owens' optical sensor housing: A 3D-printable design for a housing/mounting bracket for the optical plunger sensor, designed by Cary Owens. This makes it easy to mount the sensor.
  • 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 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.

Copyright and License

The Pinscape firmware is copyright 2014, 2021 by Michael J Roberts. It's released under an MIT open-source license. See License.

Warning to VirtuaPin Kit Owners

This software isn't designed as a replacement for the VirtuaPin plunger kit's firmware. If you bought the VirtuaPin kit, I recommend that you don't install this software. The KL25Z can only run one firmware program at a time, so if you install the Pinscape firmware on your KL25Z, it will replace and erase your existing VirtuaPin proprietary firmware. If you do this, the only way to restore your VirtuaPin firmware is to physically ship the KL25Z back to VirtuaPin and ask them to re-flash it. They don't allow you to do this at home, and they don't even allow you to back up your firmware, since they want to protect their proprietary software from copying. For all of these reasons, if you want to run the Pinscape software, I strongly recommend that you buy a "blank" retail KL25Z to use with Pinscape. They only cost about $15 and are available at several online retailers, including Amazon, Mouser, and eBay. The blank retail boards don't come with any proprietary firmware pre-installed, so installing Pinscape won't delete anything that you paid extra for.

With those warnings in mind, if you're absolutely sure that you don't mind permanently erasing your VirtuaPin firmware, it is at least possible to use Pinscape as a replacement for the VirtuaPin firmware. Pinscape uses the same button wiring conventions as the VirtuaPin setup, so you can keep your buttons (although you'll have to update the GPIO pin mappings in the Config Tool to match your physical wiring). As of the June, 2021 firmware, the Vishay VCNL4010 plunger sensor that comes with the VirtuaPin v3 plunger kit is supported, so you can also keep your plunger, if you have that chip. (You should check to be sure that's the sensor chip you have before committing to this route, if keeping the plunger sensor is important to you. The older VirtuaPin plunger kits came with different IR sensors that the Pinscape software doesn't handle.)

Sun Jul 27 18:24:51 2014 +0000
Somewhat working with ball-model damping. About to change to cabinet model.

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