Pinscape Controller version 1 fork. This is a fork to allow for ongoing bug fixes to the original controller version, from before the major changes for the expansion board project.
Dependencies: FastIO FastPWM SimpleDMA mbed
Fork of Pinscape_Controller by
Revision 33:d832bcab089e, committed 2015-10-21
- Comitter:
- mjr
- Date:
- Wed Oct 21 21:53:07 2015 +0000
- Parent:
- 32:cbff13b98441
- Child:
- 34:6b981a2afab7
- Commit message:
- With expansion board 5940 "power enable" output; saving this feature, which is to be removed.
Changed in this revision
--- a/TLC5940/TLC5940.h Sat Sep 26 02:15:59 2015 +0000 +++ b/TLC5940/TLC5940.h Wed Oct 21 21:53:07 2015 +0000 @@ -20,6 +20,50 @@ #ifndef TLC5940_H #define TLC5940_H +// Should we do the grayscale update within the blanking interval? +// If this is set to 1, we'll send grayscale data during the blanking +// interval; if 0, we'll send grayscale during the PWM cycle. +// Mode 0 is the *intended* way of using these chips, but mode 1 +// produces a more stable signal in my test setup. +// +// In my breadboard testing, using the standard data-during-PWM +// mode causes some amount of signal instability with multiple +// daisy-chained TLC5940's. It appears that there's some signal +// interference (maybe RF or electrical ringing in the wires) that +// can make the bit data and/or clock prone to noise that causes +// random bits to propagate down the daisy chain. This happens +// frequently enough in my breadboard setup to be visible as +// regular flicker. Careful wiring, short wire runs, and decoupling +// capacitors noticeably improve it, but I haven't been able to +// eliminate it entirely in my test setup. Using the data-during- +// blanking mode, however, *does* eliminate it entirely. +// +// It clearly should be possible to eliminate the signal problems +// in a well-designed PCB layout, but for the time being, I'm +// making data-during-blanking the default, since it provides +// such a noticeable improvement in my test setup, and the cost +// is minimal. The cost is that it lengthens the blanking interval +// slightly. With four chips and the SPI clock at 28MHz, the +// full data update takes 27us; with the PWM clock at 500kHz, the +// grayscale cycle is 8192us. This means that the 27us data send +// keeps the BLANK asserted for an additional 0.3% of the cycle +// time, which in term reduces output brightness by the same amount. +// This brightness reduction isn't noticeable on its own, but it +// can be seen as a flicker on data cycles if we send data on +// some blanking cycles but not on others. To eliminate the +// flicker, the code sends a data update on *every* cycle when +// using this mode to ensure that the 0.3% brightness reduction +// is uniform across time. +// +// When using this code with TLC5940 chips on a PCB, I recommend +// doing a test: set this to 0, run the board, turn on all outputs +// (connected to LEDs), and observe the results. If you don't +// see any randomness or flicker in a minute or two of observation, +// you're getting a good clean signal throughout the daisy chain +// and don't need the workaround. If you do see any instability, +// set this back to 1. +#define DATA_UPDATE_INSIDE_BLANKING 1 + #include "mbed.h" #include "FastPWM.h" #include "SimpleDMA.h" @@ -27,10 +71,16 @@ /** * SPI speed used by the mbed to communicate with the TLC5940 * The TLC5940 supports up to 30Mhz. It's best to keep this as - * high as the microcontroller will allow, since a higher SPI - * speed yields a faster grayscale data update. However, if - * you have problems with unreliable signal transmission to the - * TLC5940s, reducing this speed might help. + * high as possible, since a higher SPI speed yields a faster + * grayscale data update. However, I've seen some slight + * instability in the signal in my breadboard setup using the + * full 30MHz, so I've reduced this slightly, which seems to + * yield a solid signal. The limit will vary according to how + * clean the signal path is to the chips; you can probably crank + * this up to full speed if you have a well-designed PCB, good + * decoupling capacitors near the 5940 VCC/GND pins, and short + * wires between the KL25Z and the PCB. A short, clean path to + * KL25Z ground seems especially important. * * The SPI clock must be fast enough that the data transmission * time for a full update is comfortably less than the blanking @@ -49,7 +99,7 @@ * isn't a factor. E.g., at SPI=30MHz and GSCLK=500kHz, * t(blank) is 8192us and t(refresh) is 25us. */ -#define SPI_SPEED 3000000 +#define SPI_SPEED 2800000 /** * The rate at which the GSCLK pin is pulsed. This also controls @@ -116,15 +166,19 @@ gsclk(GSCLK), blank(BLANK), xlat(XLAT), - nchips(nchips), - newGSData(true) + nchips(nchips) { - // Set initial output pin states - XLAT off, BLANK on (BLANK turns off - // all of the outputs while we're setting up) + // set XLAT to initially off xlat = 0; + + // Assert BLANK while starting up, to keep the outputs turned off until + // everything is stable. This helps prevent spurious flashes during startup. + // (That's not particularly important for lights, but it matters more for + // tactile devices. It's a bit alarming to fire a replay knocker on every + // power-on, for example.) blank = 1; - // allocate the grayscale buffer + // allocate the grayscale buffer, and set all outputs to fully off gs = new unsigned short[nchips*16]; memset(gs, 0, nchips*16*sizeof(gs[0])); @@ -137,6 +191,22 @@ // format 0. spi.format(8, 0); spi.frequency(SPI_SPEED); + + // Send out a full data set to the chips, to clear out any random + // startup data from the registers. Include some extra bits - there + // are some cases (such as after sending dot correct commands) where + // an extra bit per chip is required, and the initial state is + // somewhat unpredictable, so send extra just to make sure we cover + // all bases. This does no harm; extra bits just fall off the end of + // the daisy chain, and since we want all registers set to 0, we can + // send arbitrarily many extra 0's. + for (int i = 0 ; i < nchips*25 ; ++i) + spi.write(0); + + // do an initial XLAT to latch all of these "0" values into the + // grayscale registers + xlat = 1; + xlat = 0; // Allocate a DMA buffer. The transfer on each cycle is 192 bits per // chip = 24 bytes per chip. @@ -160,6 +230,10 @@ // Configure the GSCLK output's frequency gsclk.period(1.0/GSCLK_SPEED); + + // mark that we need an initial update + newGSData = true; + needXlat = false; } // Start the clock running @@ -236,55 +310,43 @@ // Has new GS/DC data been loaded? volatile bool newGSData; + + // Do we need an XLAT signal on the next blanking interval? + volatile bool needXlat; // Function to reset the display and send the next chunks of data void reset() { // start the blanking cycle startBlank(); - - // If we have new GS data, send it now - if (true) - { - // Send the new grayscale data. - // - // Note that ideally, we'd do this during the new PWM cycle - // rather than during the blanking interval. The TLC5940 is - // specifically designed to allow this. However, in my testing, - // I found that sending new data during the PWM cycle was - // unreliable - it seemed to cause a fair amount of glitching, - // which as far as I can tell is signal noise coming from - // crosstalk between the grayscale clock signal and the - // SPI signal. This seems to be a common problem with - // daisy-chained TLC5940s. It can in principle be solved with - // careful high-speed circuit design (good ground planes, - // short leads, decoupling capacitors), and indeed I was able - // to improve stability to some extent with circuit tweaks, - // but I wasn't able to eliminate it entirely. Moving the - // data refresh into the blanking interval, on the other - // hand, seems to entirely eliminate any instability. - // - // update() will format the current grayscale data into our - // DMA transfer buffer and kick off the DMA transfer, then - // return. At that point we can return from the interrupt, - // but WITHOUT ending the blanking cycle - we want to keep - // blanking the outputs until the DMA transfer finishes. When - // the transfer is complete, the DMA controller will fire an - // interrupt that will trigger our dmaDone() callback, at - // which point we'll finally complete the blanking cycle and - // start a new grayscale cycle. + +#if DATA_UPDATE_INSIDE_BLANKING + // We're configured to send the new GS data entirely within + // the blanking interval. Start the DMA transfer now, and + // return without ending the blanking interval. The DMA + // completion interrupt handler will do that when the data + // update has completed. + // + // Note that we do the data update/ unconditionally in the + // send-during-blanking case, whether or not we have new GS + // data. This is because the update causes a 0.3% reduction + // in brightness because of the elongated BLANK interval. + // That would be visible as a flicker on each update if we + // did updates on some cycles and not others. By doing an + // update on every cycle, we make the brightness reduction + // uniform across time, which makes it less perceptible. + update(); + +#else // DATA_UPDATE_INSIDE_BLANKING + + // end the blanking interval + endBlank(); + + // if we have pending grayscale data, start sending it + if (newGSData) update(); - // the chips are now in sync with our data, so we have no more - // pending update - newGSData = false; - } - else - { - // no new grayscale data - just end the blanking cycle without - // a new XLAT - endBlank(false); - } +#endif // DATA_UPDATE_INSIDE_BLANKING } void startBlank() @@ -294,13 +356,16 @@ blank = 1; } - void endBlank(bool needxlat) + void endBlank() { - if (needxlat) + // if we've sent new grayscale data since the last blanking + // interval, latch it by asserting XLAT + if (needXlat) { // latch the new data while we're still blanked xlat = 1; xlat = 0; + needXlat = false; } // end the blanking interval and restart the grayscale clock @@ -350,6 +415,9 @@ // Start the DMA transfer sdma.start(nchips*24); + + // we've now cleared the new GS data + newGSData = false; } // Interrupt handler for DMA completion. The DMA controller calls this @@ -358,8 +426,15 @@ // grayscale cycle. void dmaDone() { - // when the DMA transfer is finished, start the next grayscale cycle - endBlank(true); + // mark that we need to assert XLAT to latch the new + // grayscale data during the next blanking interval + needXlat = true; + +#if DATA_UPDATE_INSIDE_BLANKING + // we're doing the gs update within the blanking cycle, so end + // the blanking cycle now that the transfer has completed + endBlank(); +#endif } };
--- a/USBJoystick/USBJoystick.cpp Sat Sep 26 02:15:59 2015 +0000 +++ b/USBJoystick/USBJoystick.cpp Wed Oct 21 21:53:07 2015 +0000 @@ -94,18 +94,43 @@ return send(&report); } -bool USBJoystick::move(int16_t x, int16_t y) { +bool USBJoystick::reportConfig(int numOutputs, int unitNo) +{ + HID_REPORT report; + + // initially fill the report with zeros + memset(report.data, 0, sizeof(report.data)); + + // Set the special status bits to indicate that it's a config report. + uint16_t s = 0x8800; + put(0, s); + + // write the number of configured outputs + put(2, numOutputs); + + // write the unit number + put(4, unitNo); + + // send the report + report.length = reportLen; + return send(&report); +} + +bool USBJoystick::move(int16_t x, int16_t y) +{ _x = x; _y = y; return update(); } -bool USBJoystick::setZ(int16_t z) { +bool USBJoystick::setZ(int16_t z) +{ _z = z; return update(); } -bool USBJoystick::buttons(uint32_t buttons) { +bool USBJoystick::buttons(uint32_t buttons) +{ _buttonsLo = (uint16_t)(buttons & 0xffff); _buttonsHi = (uint16_t)((buttons >> 16) & 0xffff); return update();
--- a/USBJoystick/USBJoystick.h Sat Sep 26 02:15:59 2015 +0000 +++ b/USBJoystick/USBJoystick.h Wed Oct 21 21:53:07 2015 +0000 @@ -124,6 +124,14 @@ * @param pix pixel array */ bool updateExposure(int &idx, int npix, const uint16_t *pix); + + /** + * Write a configuration report. + * + * @param numOutputs the number of configured output channels + * @param unitNo the device unit number + */ + bool reportConfig(int numOutputs, int unitNo); /** * Write a state of the mouse
--- a/config.h Sat Sep 26 02:15:59 2015 +0000 +++ b/config.h Wed Oct 21 21:53:07 2015 +0000 @@ -8,6 +8,20 @@ #ifndef CONFIG_H #define CONFIG_H +// --------------------------------------------------------------------------- +// +// Expansion Board. If you're using the expansion board, un-comment the +// line below. This will select all of the correct defaults for the board. +// +// The expansion board settings are mostly automatic, so you shouldn't have +// to change much else. However, you should still look at and adjust the +// following as needed: +// - TV power on delay time +// - Plunger sensor settings, if you're using a plunger +// +//#define EXPANSION_BOARD + + // -------------------------------------------------------------------------- // // Enable/disable joystick functions. @@ -34,55 +48,83 @@ #define ENABLE_JOYSTICK -// Accelerometer orientation. The accelerometer feature lets Visual Pinball -// (and other pinball software) sense nudges to the cabinet, and simulate -// the effect on the ball's trajectory during play. We report the direction -// of the accelerometer readings as well as the strength, so it's important -// for VP and the KL25Z to agree on the physical orientation of the -// accelerometer relative to the cabinet. The accelerometer on the KL25Z -// is always mounted the same way on the board, but we still have to know -// which way you mount the board in your cabinet. We assume as default -// orientation where the KL25Z is mounted flat on the bottom of your -// cabinet with the USB ports pointing forward, toward the coin door. If -// it's more convenient for you to mount the board in a different direction, -// you simply need to select the matching direction here. Comment out the -// ORIENTATION_PORTS_AT_FRONT line and un-comment the line that matches -// your board's orientation. - -#define ORIENTATION_PORTS_AT_FRONT // USB ports pointing toward front of cabinet -// #define ORIENTATION_PORTS_AT_LEFT // USB ports pointing toward left side of cab -// #define ORIENTATION_PORTS_AT_RIGHT // USB ports pointing toward right side of cab -// #define ORIENTATION_PORTS_AT_REAR // USB ports pointing toward back of cabinet +// --------------------------------------------------------------------------- +// +// USB device vendor ID and product ID. These values identify the device +// to the host software on the PC. By default, we use the same settings as +// a real LedWiz so that host software will recognize us as an LedWiz. +// +// The standard settings *should* work without conflicts, even if you have +// a real LedWiz. My reference system is 64-bit Windows 7 with a real LedWiz +// on unit #1 and a Pinscape controller on unit #8 (the default), and the +// two coexist happily in my system. The LedWiz is designed specifically +// to allow multiple units in one system, using the unit number value +// (see below) to distinguish multiple units, so there should be no conflict +// between Pinscape and any real LedWiz devices you have. +// +// However, even though conflicts *shouldn't* happen, I've had one report +// from a user who experienced a Windows USB driver conflict that they could +// only resolve by changing the vendor ID. The real underlying cause is +// still a mystery, but whatever was going on, changing the vendor ID fixed +// it. If you run into a similar problem, you can try the same fix as a +// last resort. Before doing that, though, you should try changing the +// Pinscape unit number first - it's possible that your real LedWiz is using +// unit #8, which is our default setting. +// +// If you must change the vendor ID for any reason, you'll sacrifice LedWiz +// compatibility, which means that old programs like Future Pinball that use +// the LedWiz interface directly won't be able to access the LedWiz output +// controller features. However, all is not lost. All of the other functions +// (plunger, nudge, and key input) use the joystick interface, which will +// work regardless of the ID values. In addition, DOF R3 recognizes the +// "emergency fallback" ID below, so if you use that, *all* functions +// including the output controller will work in any DOF R3-enabled software, +// including Visual Pinball and PinballX. So the only loss will be that +// old LedWiz-only software won't be able to control the outputs. +// +// The "emergency fallback" ID below is officially registerd with +// http://pid.codes, a registry for open-source USB projects, which should +// all but guarantee that this alternative ID shouldn't conflict with +// any other devices in your system. -// -------------------------------------------------------------------------- -// -// LedWiz default unit number. +// STANDARD ID SETTINGS. These provide full, transparent LedWiz compatibility. +const uint16_t USB_VENDOR_ID = 0xFAFA; // LedWiz vendor ID = FAFA +const uint16_t USB_PRODUCT_ID = 0x00F0; // LedWiz start of product ID range = 00F0 + + +// EMERGENCY FALLBACK ID SETTINGS. These settings are not LedWiz-compatible, +// so older LedWiz-only software won't be able to access the output controller +// features. However, DOF R3 recognizes these IDs, so DOF-aware software (Visual +// Pinball, PinballX) will have full access to all features. +// +//const uint16_t USB_VENDOR_ID = 0x1209; // DOF R3-compatible vendor ID = 1209 +//const uint16_t USB_PRODUCT_ID = 0xEAEA; // DOF R3-compatible product ID = EAEA + + +// --------------------------------------------------------------------------- +// +// LedWiz unit number. // // Each LedWiz device has a unit number, from 1 to 16. This lets you install // more than one LedWiz in your system: as long as each one has a different // unit number, the software on the PC can tell them apart and route commands // to the right device. // -// A *real* LedWiz has its unit number set at the factory; they set it to -// unit 1 unless you specifically request a different number when you place -// your order. +// A real LedWiz has its unit number set at the factory. If you don't tell +// them otherwise when placing your order, they will set it to unit #1. Most +// real LedWiz units therefore are set to unit #1. There's no provision on +// a real LedWiz for users to change the unit number after it leaves the +// factory. // -// For our *emulated* LedWiz, we default to unit #8. However, if we're set -// up as a secondary Pinscape controller with the joystick functions turned -// off, we'll use unit #9 instead. +// For our *emulated* LedWiz, we default to unit #8 if we're the primary +// Pinscape controller in the system, or unit #9 if we're set up as the +// secondary controller with the joystick functions turned off. // // The reason we start at unit #8 is that we want to avoid conflicting with -// any real LedWiz devices you have in your system. If you have a real -// LedWiz, it's probably unit #1, since that's the default factory setting -// that they'll give you if you didn't specifically ask for something else -// when you ordered it. If you have two real LedWiz's, they're probably -// units #1 and #2. If you have three... well, I don't think anyone -// actually has three, but if you did it would probably be unit #3. And so -// on. That's why we start at #8: it seems really unlikely that anyone -// with a pin cab has a real LedWiz unit #8. On the off chance that you -// do, simply change the setting here to a different unit number that's not -// already used in your system. +// any real LedWiz devices in your system. Most real LedWiz devices are +// set up as unit #1, and in the rare cases where people have two of them, +// the second one is usually unit #2. // // Note 1: the unit number here is the *user visible* unit number that // you use on the PC side. It's the number you specify in your DOF @@ -106,6 +148,31 @@ 0x09; // joystick disabled - assume we're a secondary, output-only KL25Z, so use #9 #endif + +// -------------------------------------------------------------------------- +// +// Accelerometer orientation. The accelerometer feature lets Visual Pinball +// (and other pinball software) sense nudges to the cabinet, and simulate +// the effect on the ball's trajectory during play. We report the direction +// of the accelerometer readings as well as the strength, so it's important +// for VP and the KL25Z to agree on the physical orientation of the +// accelerometer relative to the cabinet. The accelerometer on the KL25Z +// is always mounted the same way on the board, but we still have to know +// which way you mount the board in your cabinet. We assume as default +// orientation where the KL25Z is mounted flat on the bottom of your +// cabinet with the USB ports pointing forward, toward the coin door. If +// it's more convenient for you to mount the board in a different direction, +// you simply need to select the matching direction here. Comment out the +// ORIENTATION_PORTS_AT_FRONT line and un-comment the line that matches +// your board's orientation. + +#define ORIENTATION_PORTS_AT_FRONT // USB ports pointing toward front of cabinet +// #define ORIENTATION_PORTS_AT_LEFT // USB ports pointing toward left side of cab +// #define ORIENTATION_PORTS_AT_RIGHT // USB ports pointing toward right side of cab +// #define ORIENTATION_PORTS_AT_REAR // USB ports pointing toward back of cabinet + + + // -------------------------------------------------------------------------- // // Plunger CCD sensor. @@ -142,11 +209,11 @@ // // Happily, the time needed to read the approximately 165 pixels required // for pixel-accurate positioning on the display is short enough that we can -// complete a scan within the cycle time for USB reports. USB gives us a -// whole separate timing factor; we can't go much *faster* with USB than -// sending a new report about every 10ms. The sensor timing is such that -// we can read about 165 pixels in well under 10ms. So that's really the -// sweet spot for our scans. +// complete a scan within the cycle time for USB reports. Visual Pinball +// only polls for input at about 10ms intervals, so there's no benefit +// to going much faster than this. The sensor timing is such that we can +// read about 165 pixels in well under 10ms. So that's really the sweet +// spot for our scans. // // Note that we distribute the sampled pixels evenly across the full range // of the sensor's pixels. That is, we read every nth pixel, and skip the @@ -241,6 +308,35 @@ const PinName CAL_BUTTON_LED = PTE23; +// --------------------------------------------------------------------------- +// +// TV Power-On Timer. This section lets you set up a delayed relay timer +// for turning on your TV monitor(s) shortly after you turn on power to the +// system. This requires some external circuitry, which is built in to the +// expansion board, or which you can build yourself - refer to the Build +// Guide for the circuit plan. +// +// If you're using this feature, un-comment the next line, and make any +// changes to the port assignments below. The default port assignments are +// suitable for the expansion board. Note that the TV timer is enabled +// automatically if you're using the expansion board, since it's built in. +//#define ENABLE_TV_TIMER + +#if defined(ENABLE_TV_TIMER) || defined(EXPANSION_BOARD) +# define PSU2_STATUS_SENSE PTD2 // Digital In pin to read latch status +# define PSU2_STATUS_SET PTE0 // Digital Out pin to set latch +# define TV_RELAY_PIN PTD3 // Digital Out pin to control TV switch relay + +// Amount of time (in seconds) to wait after system power-up before +// pulsing the TV ON switch relay. Adjust as needed for your TV(s). +// Most monitors won't respond to any buttons for the first few seconds +// after they're plugged in, so we need to wait long enough to make sure +// the TVs are ready to receive input before pressing the button. +#define TV_DELAY_TIME 7.0 + +#endif + + // -------------------------------------------------------------------------- // // Pseudo "Launch Ball" button. @@ -324,30 +420,45 @@ // If you do add TLC5940 circuits to your controller hardware, use this // section to configure the connection to the KL25Z. // -// Note that if you're using TLC5940 outputs, ALL of the outputs must go -// through the TLC5940s - you can't mix TLC5940s and the default GPIO -// device outputs. This lets us take GPIO ports that we'd normally use -// for device outputs and reassign them to control the TLC5940 hardware. - -// Uncomment this line if using TLC5940 chips -//#define ENABLE_TLC5940 +// Note that when using the TLC5940, you can still also use some GPIO +// pins for outputs as normal. See ledWizPinMap[] for // Number of TLC5940 chips you're using. For a full LedWiz-compatible -// setup, you need two of these chips, for 32 outputs. -#define TLC5940_NCHIPS 4 +// setup, you need two of these chips, for 32 outputs. The software +// will handle up to 8. The expansion board uses 4 of these chips; if +// you're not using the expansion board, we assume you're not using +// any of them. +#ifdef EXPANSION_BOARD +# define TLC5940_NCHIPS 4 +#else +# define TLC5940_NCHIPS 0 // change this if you're using TLC5940's without the expansion board +#endif // If you're using TLC5940s, change any of these as needed to match the // GPIO pins that you connected to the TLC5940 control pins. Note that // SIN and SCLK *must* be connected to the KL25Z SPI0 MOSI and SCLK // outputs, respectively, which effectively limits them to the default -// selections, and that the GSCLK pin must be PWM-capable. +// selections, and that the GSCLK pin must be PWM-capable. These defaults +// all match the expansion board wiring. #define TLC5940_SIN PTC6 // Must connect to SPI0 MOSI -> PTC6 or PTD2 #define TLC5940_SCLK PTC5 // Must connect to SPI0 SCLK -> PTC5 or PTD1; however, PTD1 isn't // recommended because it's hard-wired to the on-board blue LED #define TLC5940_XLAT PTC10 // Any GPIO pin can be used -#define TLC5940_BLANK PTC0 // Any GPIO pin can be used -#define TLC5940_GSCLK PTD4 // Must be a PWM-capable pin +#define TLC5940_BLANK PTC7 // Any GPIO pin can be used +#define TLC5940_GSCLK PTA1 // Must be a PWM-capable pin +// TLC5940 output power enable pin. This is a GPIO pin that controls +// a high-side transistor switch that controls power to the optos and +// LEDs connected to the TLC5940 outputs. This is a precaution against +// powering the chip's output pins before Vcc is powered. Vcc comes +// from the KL25Z, so when our program is running, we know for certain +// that Vcc is up. This means that we can simply enable this pin any +// time after entering our main(). Un-comment this line if using this +// circuit. +// #define TLC5940_PWRENA PTC11 // Any GPIO pin can be used +#ifdef EXPANSION_BOARD +# define TLC5940_PWRENA PTC11 +#endif #endif // CONFIG_H - end of include-once section (code below this point can be multiply included) @@ -427,41 +538,55 @@ // -------------------------------------------------------------------------- // -// LED-Wiz emulation output pin assignments - GPIO mode +// LED-Wiz emulation output pin assignments // -// NOTE! This section isn't used if you have TLC5940 outputs - ALL -// device outputs will be through the 5940s if you're using them. -// See the TLC5940 setup section above to configure your interface -// pins if you're using those chips. +// This sets the mapping from logical LedWiz port numbers, as used +// in the software on the PC side, to physical hardware pins on the +// KL25Z and/or the TLC5940 controllers. // -// 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. +// The LedWiz protocol lets the PC software set a "brightness" level +// for each output. This is used to control the intensity of LEDs +// and other lights, and can also control motor speeds. To implement +// the intensity level in hardware, we use PWM, or pulse width +// modulation, which switches the output on and off very rapidly +// to give the effect of a reduced voltage. Unfortunately, the KL25Z +// hardware is limited to 10 channels of PWM control for its GPIO +// outputs, so it's impossible to implement the LedWiz's full set +// of 32 adjustable outputs using only GPIO ports. However, you can +// create 10 adjustable ports and fill out the rest with "digital" +// GPIO pins, which are simple on/off switches. The intensity level +// of a digital port can't be adjusted - it's either fully on or +// fully off - but this is fine for devices that don't have +// different intensity settings anyway, such as replay knockers +// and flipper solenoids. // -// 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. +// In the mapping list below, you can decide how to dole out the +// PWM-capable and digital-only GPIO pins. To make it easier to +// remember which is which, the default mapping below groups all +// of the PWM-capable ports together in the first 10 logical LedWiz +// port numbers. Unfortunately, these ports aren't *physically* +// together on the KL25Z pin headers, so this layout may be simple +// in terms of the LedWiz numbering, but it's a little jumbled +// in the physical layout.t +// +// "NC" in the pin name slot means "not connected". This means +// that there's no physical output for this LedWiz port number. +// The device will still accept commands that control the port, +// but these will just be silently ignored, since there's no pin +// to turn on or off for these ports. The reason we leave some +// ports unconnected 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 +// 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). +// changing the corresponding declarations elsewhere in this file). // 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 @@ -508,47 +633,228 @@ // 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. -// +// Note: Don't assign PTD1 (pin J2-12) as an LedWiz output. That pin +// is hard-wired on the KL25Z to the on-board indicator LED's blue segment, +// which pretty much precludes any other use of the pin. +// +// ACTIVE-LOW PORTS: By default, when a logical port is turned on in +// the software, we set the physical GPIO voltage to "high" (3.3V), and +// set it "low" (0V) when the logical port is off. This is the right +// scheme for the booster circuit described in the build guide. Some +// third-party booster circuits want the opposite voltage scheme, where +// logical "on" is represented by 0V on the port and logical "off" is +// represented by 3.3V. If you're using an "active low" booster like +// that, set the PORT_ACTIVE_LOW flag in the array below for each +// affected port. +// +// TLC5940 PORTS: To assign an LedWiz output port number to a particular +// output on a TLC5940, set tlcPortNum to the non-zero port number, +// starting at 1 for the first output on the first chip, 16 for the +// last output on the first chip, 17 for the first output on the second +// chip, and so on. TLC ports are inherently PWM-capable only, so it's +// not necessary to set the PORT_IS_PWM flag for those. +// + +// ledWizPortMap 'flags' bits - combine these with '|' +const int PORT_IS_PWM = 0x0001; // this port is PWM-capable +const int PORT_ACTIVE_LOW = 0x0002; // use LOW voltage (0V) when port is ON + 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 connected, LW port 23 - { NC, false }, // Not connected, LW port 24 - { NC, false }, // Not connected, LW port 25 - { NC, false }, // Not connected, LW port 26 - { NC, false }, // Not connected, LW port 27 - { NC, false }, // Not connected, LW port 28 - { NC, false }, // Not connected, LW port 29 - { NC, false }, // Not connected, LW port 30 - { NC, false }, // Not connected, LW port 31 - { NC, false } // Not connected, LW port 32 + PinName pin; // the GPIO pin assigned to this output; NC if not connected or a TLC5940 port + int flags; // flags - a combination of PORT_xxx flag bits (see above) + int tlcPortNum; // for TLC5940 ports, the TLC output number (1 to number of chips*16); otherwise 0 +} ledWizPortMap[] = { + +#if TLC5940_NCHIPS == 0 + + // *** BASIC MODE - GPIO OUTPUTS ONLY *** + // This is the basic mapping, using entirely GPIO pins, for when you're + // not using external TLC5940 chips. We provide 22 physical outputs, 10 + // of which are PWM capable. + // + // Important! Note that the "isPWM" setting isn't just something we get to + // choose. It's a feature of the KL25Z hardware. Some pins are PWM capable + // and some aren't, and there's nothing we can do about that in the software. + // Refer to the KL25Z manual or schematics for the possible connections. Note + // that there are other PWM-capable pins besides the 10 shown below, BUT they + // all share TPM channels with the pins below. For example, TPM 2.0 can be + // connected to PTA1, PTB2, PTB18, PTE22 - but only one at a time. So if you + // want to use PTB2 as a PWM out, it means you CAN'T use PTA1 as a PWM out. + // We commented each PWM pin with its hardware channel number to help you keep + // track of available channels if you do need to rearrange any of these pins. + + { PTA1, PORT_IS_PWM }, // pin J1-2, LW port 1 (PWM capable - TPM 2.0 = channel 9) + { PTA2, PORT_IS_PWM }, // pin J1-4, LW port 2 (PWM capable - TPM 2.1 = channel 10) + { PTD4, PORT_IS_PWM }, // pin J1-6, LW port 3 (PWM capable - TPM 0.4 = channel 5) + { PTA12, PORT_IS_PWM }, // pin J1-8, LW port 4 (PWM capable - TPM 1.0 = channel 7) + { PTA4, PORT_IS_PWM }, // pin J1-10, LW port 5 (PWM capable - TPM 0.1 = channel 2) + { PTA5, PORT_IS_PWM }, // pin J1-12, LW port 6 (PWM capable - TPM 0.2 = channel 3) + { PTA13, PORT_IS_PWM }, // pin J2-2, LW port 7 (PWM capable - TPM 1.1 = channel 13) + { PTD5, PORT_IS_PWM }, // pin J2-4, LW port 8 (PWM capable - TPM 0.5 = channel 6) + { PTD0, PORT_IS_PWM }, // pin J2-6, LW port 9 (PWM capable - TPM 0.0 = channel 1) + { PTD3, PORT_IS_PWM }, // pin J2-10, LW port 10 (PWM capable - TPM 0.3 = channel 4) + { PTD2, 0 }, // pin J2-8, LW port 11 + { PTC8, 0 }, // pin J1-14, LW port 12 + { PTC9, 0 }, // pin J1-16, LW port 13 + { PTC7, 0 }, // pin J1-1, LW port 14 + { PTC0, 0 }, // pin J1-3, LW port 15 + { PTC3, 0 }, // pin J1-5, LW port 16 + { PTC4, 0 }, // pin J1-7, LW port 17 + { PTC5, 0 }, // pin J1-9, LW port 18 + { PTC6, 0 }, // pin J1-11, LW port 19 + { PTC10, 0 }, // pin J1-13, LW port 20 + { PTC11, 0 }, // pin J1-15, LW port 21 + { PTE0, 0 }, // pin J2-18, LW port 22 + { NC, 0 }, // Not connected, LW port 23 + { NC, 0 }, // Not connected, LW port 24 + { NC, 0 }, // Not connected, LW port 25 + { NC, 0 }, // Not connected, LW port 26 + { NC, 0 }, // Not connected, LW port 27 + { NC, 0 }, // Not connected, LW port 28 + { NC, 0 }, // Not connected, LW port 29 + { NC, 0 }, // Not connected, LW port 30 + { NC, 0 }, // Not connected, LW port 31 + { NC, 0 } // Not connected, LW port 32 + +#elif defined(EXPANSION_BOARD) + + // *** EXPANSION BOARD MODE *** + // + // This mapping is for the expansion board, which uses four TLC5940 + // chips to provide 64 outputs. The expansion board also uses + // one GPIO pin to provide a digital (non-PWM) output dedicated to + // the knocker circuit. That's on a digital pin because it's used + // to trigger an external timer circuit that limits the amount of + // time that the knocker coil can be continuously energized, to protect + // it against software faults on the PC that leave the port stuck on. + // (The knocker coil is unique among standard virtual cabinet output + // devices in this respect - it's the only device in common use that + // can be damaged if left on for too long. Other devices won't be + // damaged, so they don't require such elaborate precautions.) + // + // The specific device assignments in the last column are just + // recommendations - you can assign any port to any device with + // compatible power needs. The "General Purpose" ports are good to + // at least 5A, so you can use these for virtually anything. The + // "Button light" ports are good to about 1.5A, so these are most + // suitable for smaller loads like lamps, flashers, LEDs, etc. The + // flipper and magnasave ports will only provide 20mA, so these are + // only usable for small LEDs. + + // The first 32 ports are LedWiz-compatible, so they're universally + // accessible, even to older non-DOF software. Attach the most common + // devices to these ports. + { NC, 0, 1 }, // TLC port 1, LW output 1 - Flasher 1 R + { NC, 0, 2 }, // TLC port 2, LW output 2 - Flasher 1 G + { NC, 0, 3 }, // TLC port 3, LW output 3 - Flasher 1 B + { NC, 0, 4 }, // TLC port 4, LW output 4 - Flasher 2 R + { NC, 0, 5 }, // TLC port 5, LW output 5 - Flasher 2 G + { NC, 0, 6 }, // TLC port 6, LW output 6 - Flasher 2 B + { NC, 0, 7 }, // TLC port 7, LW output 7 - Flasher 3 R + { NC, 0, 8 }, // TLC port 8, LW output 8 - Flasher 3 G + { NC, 0, 9 }, // TLC port 9, LW output 9 - Flasher 3 B + { NC, 0, 10 }, // TLC port 10, LW output 10 - Flasher 4 R + { NC, 0, 11 }, // TLC port 11, LW output 11 - Flasher 4 G + { NC, 0, 12 }, // TLC port 12, LW output 12 - Flasher 4 B + { NC, 0, 13 }, // TLC port 13, LW output 13 - Flasher 5 R + { NC, 0, 14 }, // TLC port 14, LW output 14 - Flasher 5 G + { NC, 0, 15 }, // TLC port 15, LW output 15 - Flasher 5 B + { NC, 0, 16 }, // TLC port 16, LW output 16 - Strobe/Button light + { NC, 0, 17 }, // TLC port 17, LW output 17 - Button light 1 + { NC, 0, 18 }, // TLC port 18, LW output 18 - Button light 2 + { NC, 0, 19 }, // TLC port 19, LW output 19 - Button light 3 + { NC, 0, 20 }, // TLC port 20, LW output 20 - Button light 4 + { PTC8, 0, 0 }, // PTC8, LW output 21 - Replay Knocker + { NC, 0, 21 }, // TLC port 21, LW output 22 - Contactor 1/General purpose + { NC, 0, 22 }, // TLC port 22, LW output 23 - Contactor 2/General purpose + { NC, 0, 23 }, // TLC port 23, LW output 24 - Contactor 3/General purpose + { NC, 0, 24 }, // TLC port 24, LW output 25 - Contactor 4/General purpose + { NC, 0, 25 }, // TLC port 25, LW output 26 - Contactor 5/General purpose + { NC, 0, 26 }, // TLC port 26, LW output 27 - Contactor 6/General purpose + { NC, 0, 27 }, // TLC port 27, LW output 28 - Contactor 7/General purpose + { NC, 0, 28 }, // TLC port 28, LW output 29 - Contactor 8/General purpose + { NC, 0, 29 }, // TLC port 29, LW output 30 - Contactor 9/General purpose + { NC, 0, 30 }, // TLC port 30, LW output 31 - Contactor 10/General purpose + { NC, 0, 31 }, // TLC port 31, LW output 32 - Shaker Motor/General purpose + + // Ports 33+ are accessible only to DOF-based software. Older LedWiz-only + // software on the can't access these. Attach less common devices to these ports. + { NC, 0, 32 }, // TLC port 32, LW output 33 - Gear Motor/General purpose + { NC, 0, 33 }, // TLC port 33, LW output 34 - Fan/General purpose + { NC, 0, 34 }, // TLC port 34, LW output 35 - Beacon/General purpose + { NC, 0, 35 }, // TLC port 35, LW output 36 - Undercab RGB R/General purpose + { NC, 0, 36 }, // TLC port 36, LW output 37 - Undercab RGB G/General purpose + { NC, 0, 37 }, // TLC port 37, LW output 38 - Undercab RGB B/General purpose + { NC, 0, 38 }, // TLC port 38, LW output 39 - Bell/General purpose + { NC, 0, 39 }, // TLC port 39, LW output 40 - Chime 1/General purpose + { NC, 0, 40 }, // TLC port 40, LW output 41 - Chime 2/General purpose + { NC, 0, 41 }, // TLC port 41, LW output 42 - Chime 3/General purpose + { NC, 0, 42 }, // TLC port 42, LW output 43 - General purpose + { NC, 0, 43 }, // TLC port 43, LW output 44 - General purpose + { NC, 0, 44 }, // TLC port 44, LW output 45 - Button light 5 + { NC, 0, 45 }, // TLC port 45, LW output 46 - Button light 6 + { NC, 0, 46 }, // TLC port 46, LW output 47 - Button light 7 + { NC, 0, 47 }, // TLC port 47, LW output 48 - Button light 8 + { NC, 0, 49 }, // TLC port 49, LW output 49 - Flipper button RGB left R + { NC, 0, 50 }, // TLC port 50, LW output 50 - Flipper button RGB left G + { NC, 0, 51 }, // TLC port 51, LW output 51 - Flipper button RGB left B + { NC, 0, 52 }, // TLC port 52, LW output 52 - Flipper button RGB right R + { NC, 0, 53 }, // TLC port 53, LW output 53 - Flipper button RGB right G + { NC, 0, 54 }, // TLC port 54, LW output 54 - Flipper button RGB right B + { NC, 0, 55 }, // TLC port 55, LW output 55 - MagnaSave button RGB left R + { NC, 0, 56 }, // TLC port 56, LW output 56 - MagnaSave button RGB left G + { NC, 0, 57 }, // TLC port 57, LW output 57 - MagnaSave button RGB left B + { NC, 0, 58 }, // TLC port 58, LW output 58 - MagnaSave button RGB right R + { NC, 0, 59 }, // TLC port 59, LW output 59 - MagnaSave button RGB right G + { NC, 0, 60 } // TLC port 60, LW output 60 - MagnaSave button RGB right B + +#else + + // *** TLC5940 + GPIO OUTPUTS, Without the expansion board *** + // + // This is the mapping for the ehnanced mode, with one or more TLC5940 + // chips connected. Each TLC5940 chip provides 16 PWM channels. We + // can supplement the TLC5940 outputs with GPIO pins to get even more + // physical outputs. + // + // Because we've already declared the number of TLC5940 chips earlier + // in this file, we don't actually have to map out all of the TLC5940 + // ports here. The software will automatically assign all of the + // TLC5940 ports that aren't explicitly mentioned here to the next + // available LedWiz port numbers after the end of this array, assigning + // them sequentially in TLC5940 port order. + // + // In contrast to the basic mode arrangement, we're putting all of the + // NON PWM ports first in this mapping. The logic is that all of the + // TLC5940 ports are PWM-capable, and they'll all at the end of the list + // here, so by putting the PWM GPIO pins last here, we'll keep all of the + // PWM ports grouped in the final mapping. + // + // Note that the TLC5940 control wiring takes away several GPIO pins + // that we used as output ports in the basic mode. Further, because the + // TLC5940 makes ports so plentiful, we're intentionally omitting several + // more of the pins from the basic set, to make them available for other + // uses. To keep things more neatly grouped, we're only assigning J1 pins + // in this set. This leaves the following ports from the basic mode output + // set available for other users: PTA13, PTD0, PTD2, PTD3, PTD5, PTE0. + + { PTC8, 0 }, // pin J1-14, LW port 1 + { PTC9, 0 }, // pin J1-16, LW port 2 + { PTC0, 0 }, // pin J1-3, LW port 3 + { PTC3, 0 }, // pin J1-5, LW port 4 + { PTC4, 0 }, // pin J1-7, LW port 5 + { PTA2, PORT_IS_PWM }, // pin J1-4, LW port 6 (PWM capable - TPM 2.1 = channel 10) + { PTD4, PORT_IS_PWM }, // pin J1-6, LW port 7 (PWM capable - TPM 0.4 = channel 5) + { PTA12, PORT_IS_PWM }, // pin J1-8, LW port 8 (PWM capable - TPM 1.0 = channel 7) + { PTA4, PORT_IS_PWM }, // pin J1-10, LW port 9 (PWM capable - TPM 0.1 = channel 2) + { PTA5, PORT_IS_PWM } // pin J1-12, LW port 10 (PWM capable - TPM 0.2 = channel 3) + + // TLC5940 ports start here! + // First chip port 0 -> LW port 12 + // First chip port 1 -> LW port 13 + // ... etc, filling out all chip ports sequentially ... + +#endif // TLC5940_NCHIPS };
--- a/main.cpp Sat Sep 26 02:15:59 2015 +0000 +++ b/main.cpp Wed Oct 21 21:53:07 2015 +0000 @@ -143,10 +143,24 @@ // The software can control a set of daisy-chained TLC5940 chips, which provide // 16 PWM outputs per chip. Two of these chips give you the full complement // of 32 output ports of an actual LedWiz, and four give you 64 ports, which -// should be plenty for nearly any virtual pinball project. +// should be plenty for nearly any virtual pinball project. A private, extended +// version of the LedWiz protocol lets the host control the extra outputs, up to +// 128 outputs per KL25Z (8 TLC5940s). To take advantage of the extra outputs +// on the PC side, you need software that knows about the protocol extensions, +// which means you need the latest version of DirectOutput Framework (DOF). VP +// uses DOF for its output, so VP will be able to use the added ports without any +// extra work on your part. Older software (e.g., Future Pinball) that doesn't +// use DOF will still be able to use the LedWiz-compatible protocol, so it'll be +// able to control your first 32 ports (numbered 1-32 in the LedWiz scheme), but +// older software won't be able to address higher-numbered ports. That shouldn't +// be a problem because older software wouldn't know what to do with the extra +// devices anyway - FP, for example, is limited to a pre-defined set of outputs. +// As long as you put the most common devices on the first 32 outputs, and use +// higher numbered ports for the less common devices that older software can't +// use anyway, you'll get maximum functionality out of software new and old. // -// -// The on-board LED on the KL25Z flashes to indicate the current device status: +// STATUS LIGHTS: The on-board LED on the KL25Z flashes to indicate the current +// device status. The flash patterns are: // // two short red flashes = the device is powered but hasn't successfully // connected to the host via USB (either it's not physically connected @@ -169,14 +183,14 @@ // // alternating blue/green = everything's working // -// Software configuration: you can change option settings by sending special +// Software configuration: you can some change option settings by sending special // USB commands from the PC. I've provided a Windows program for this purpose; // refer to the documentation for details. For reference, here's the format // of the USB command for option changes: // // length of report = 8 bytes // byte 0 = 65 (0x41) -// byte 1 = 1 (0x01) +// byte 1 = 1 (0x01) // byte 2 = new LedWiz unit number, 0x01 to 0x0f // byte 3 = feature enable bit mask: // 0x01 = enable CCD (default = on) @@ -188,14 +202,14 @@ // // length = 8 bytes // byte 0 = 65 (0x41) -// byte 1 = 2 (0x02) +// byte 1 = 2 (0x02) // // Exposure reports: the host can request a report of the full set of pixel // values for the next frame by sending this special packet: // // length = 8 bytes // byte 0 = 65 (0x41) -// byte 1 = 3 (0x03) +// byte 1 = 3 (0x03) // // We'll respond with a series of special reports giving the exposure status. // Each report has the following structure: @@ -215,7 +229,33 @@ // descriptor, which would have broken LedWiz compatibility. Given that // constraint, we have to re-use the joystick report type, making for // this somewhat kludgey approach. - +// +// Configuration query: the host can request a full report of our hardware +// configuration with this message. +// +// length = 8 bytes +// byte 0 = 65 (0x41) +// byte 1 = 4 (0x04) +// +// We'll response with one report containing the configuration status: +// +// bytes 0:1 = 0x8800. This has the bit pattern 10001 in the high +// 5 bits, which distinguishes it from regular joystick +// reports and from exposure status reports. +// bytes 2:3 = number of outputs +// remaining bytes = reserved for future use; set to 0 in current version +// +// Turn off all outputs: this message tells the device to turn off all +// outputs and restore power-up LedWiz defaults. This sets outputs #1-32 +// to profile 48 (full brightness) and switch state Off, sets all extended +// outputs (#33 and above) to brightness 0, and sets the LedWiz flash rate +// to 2. +// +// length = 8 bytes +// byte 0 = 65 (0x41) +// byte 1 = 5 (0x05) + + #include "mbed.h" #include "math.h" #include "USBJoystick.h" @@ -225,7 +265,6 @@ #include "crc32.h" #include "TLC5940.h" -// our local configuration file #define DECL_EXTERNS #include "config.h" @@ -243,35 +282,27 @@ inline float round(float x) { return x > 0 ? floor(x + 0.5) : ceil(x - 0.5); } -// --------------------------------------------------------------------------- -// USB device vendor ID, product ID, and version. +// -------------------------------------------------------------------------- +// +// USB product version number // -// 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 -// unit number (e.g., product ID 0xF7 means unit #7). This allows multiple -// LedWiz units to be installed in a single PC; the software on the PC side -// uses the unit number to route commands to the devices attached to each -// unit. On the real LedWiz, the unit number must be set in the firmware -// at the factory; it's not configurable by the end user. Most LedWiz's -// ship with the unit number set to 0, but the vendor will set different -// unit numbers if requested at the time of purchase. So if you have a -// 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. -// -// 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 -// the unit number, so that users can select the unit number without having -// to install a different version of the software. We'll combine the base -// product ID here with the unit number to get the actual product ID that -// we send to the USB controller. -const uint16_t USB_VENDOR_ID = 0xFAFA; -const uint16_t USB_PRODUCT_ID = 0x00F0; -const uint16_t USB_VERSION_NO = 0x0006; +const uint16_t USB_VERSION_NO = 0x0007; +// +// Build the full USB product ID. If we're using the LedWiz compatible +// vendor ID, the full product ID is the combination of the LedWiz base +// product ID (0x00F0) and the 0-based unit number (0-15). If we're not +// trying to be LedWiz compatible, we just use the exact product ID +// specified in config.h. +#define MAKE_USB_PRODUCT_ID(vid, pidbase, unit) \ + ((vid) == 0xFAFA && (pidbase) == 0x00F0 ? (pidbase) | (unit) : (pidbase)) + + +// -------------------------------------------------------------------------- +// // Joystick axis report range - we report from -JOYMAX to +JOYMAX +// #define JOYMAX 4096 // -------------------------------------------------------------------------- @@ -357,16 +388,6 @@ // for 32 outputs). Every port in this mode has full PWM support. // -// Figure the number of outputs. If we're in the default LedWiz mode, -// we have a fixed set of 32 outputs. If we're in TLC5940 enhanced mode, -// we have 16 outputs per chip. To simplify the LedWiz compatibility code, -// always use a minimum of 32 outputs even if we have fewer than two of the -// TLC5940 chips. -#if !defined(ENABLE_TLC5940) || (TLC_NCHIPS) < 2 -# define NUM_OUTPUTS 32 -#else -# define NUM_OUTPUTS ((TLC5940_NCHIPS)*16) -#endif // Current starting output index for "PBA" messages from the PC (using // the LedWiz USB protocol). Each PBA message implicitly uses the @@ -385,10 +406,27 @@ virtual void set(float val) = 0; }; +// LwOut class for unmapped ports. The LedWiz protocol is hardwired +// for 32 ports, but we might not want to assign all 32 software ports +// to physical output pins - the KL25Z has a limited number of GPIO +// ports, so we might not have enough available GPIOs to fill out the +// full LedWiz complement after assigning GPIOs for other functions. +// This class is used to populate the LedWiz mapping array for ports +// that aren't connected to physical outputs; it simply ignores value +// changes. +class LwUnusedOut: public LwOut +{ +public: + LwUnusedOut() { } + virtual void set(float val) { } +}; -#ifdef ENABLE_TLC5940 -// The TLC5940 interface object. +#if TLC5940_NCHIPS +// +// The TLC5940 interface object. Set this up with the port assignments +// set in config.h. +// TLC5940 tlc5940(TLC5940_SCLK, TLC5940_SIN, TLC5940_GSCLK, TLC5940_BLANK, TLC5940_XLAT, TLC5940_NCHIPS); @@ -410,7 +448,31 @@ float prv; }; -#else // ENABLE_TLC5940 +// Inverted voltage version of TLC5940 class (Active Low - logical "on" +// is represented by 0V on output) +class Lw5940OutInv: public Lw5940Out +{ +public: + Lw5940OutInv(int idx) : Lw5940Out(idx) { } + virtual void set(float val) { Lw5940Out::set(1.0 - val); } +}; + +#else +// No TLC5940 chips are attached, so we shouldn't encounter any ports +// in the map marked for TLC5940 outputs. If we do, treat them as unused. +class Lw5940Out: public LwUnusedOut +{ +public: + Lw5940Out(int idx) { } +}; + +class Lw5940OutInv: public Lw5940Out +{ +public: + Lw5940OutInv(int idx) : Lw5940Out(idx) { } +}; + +#endif // TLC5940_NCHIPS // // Default LedWiz mode - using on-board GPIO ports. In this mode, we @@ -434,6 +496,16 @@ float prv; }; +// Inverted voltage PWM-capable GPIO port. This is the Active Low +// version of the port - logical "on" is represnted by 0V on the +// GPIO pin. +class LwPwmOutInv: public LwPwmOut +{ +public: + LwPwmOutInv(PinName pin) : LwPwmOut(pin) { } + virtual void set(float val) { LwPwmOut::set(1.0 - val); } +}; + // LwOut class for a Digital-Only (Non-PWM) GPIO port class LwDigOut: public LwOut { @@ -448,21 +520,12 @@ float prv; }; -#endif // ENABLE_TLC5940 - -// LwOut class for unmapped ports. The LedWiz protocol is hardwired -// for 32 ports, but we might not want to assign all 32 software ports -// to physical output pins - the KL25Z has a limited number of GPIO -// ports, so we might not have enough available GPIOs to fill out the -// full LedWiz complement after assigning GPIOs for other functions. -// This class is used to populate the LedWiz mapping array for ports -// that aren't connected to physical outputs; it simply ignores value -// changes. -class LwUnusedOut: public LwOut +// Inverted voltage digital out +class LwDigOutInv: public LwDigOut { public: - LwUnusedOut() { } - virtual void set(float val) { } + LwDigOutInv(PinName pin) : LwDigOut(pin) { } + virtual void set(float val) { LwDigOut::set(1.0 - val); } }; // Array of output physical pin assignments. This array is indexed @@ -472,48 +535,127 @@ // physical GPIO pin for the port specified in the ledWizPortMap[] // array in config.h. If we're using TLC5940 chips for the outputs, // we map each logical port to the corresponding TLC5940 output. -static LwOut *lwPin[NUM_OUTPUTS]; +static int numOutputs; +static LwOut **lwPin; + +// Current absolute brightness level for an output. This is a float +// value from 0.0 for fully off to 1.0 for fully on. This is the final +// derived value for the port. For outputs set by LedWiz messages, +// this is derived from the LedWiz state, and is updated on each pulse +// timer interrupt for lights in flashing states. For outputs set by +// extended protocol messages, this is simply the brightness last set. +static float *outLevel; // initialize the output pin array void initLwOut() { - for (int i = 0 ; i < countof(lwPin) ; ++i) + // Figure out how many outputs we have. We always have at least + // 32 outputs, since that's the number fixed by the original LedWiz + // protocol. If we're using TLC5940 chips, we use our own custom + // extended protocol that allows for many more ports. In this case, + // we have 16 outputs per TLC5940, plus any assigned to GPIO pins. + + // start with 16 ports per TLC5940 + numOutputs = TLC5940_NCHIPS * 16; + + // add outputs assigned to GPIO pins in the LedWiz-to-pin mapping + int i; + for (i = 0 ; i < countof(ledWizPortMap) ; ++i) { -#ifdef ENABLE_TLC5940 - // Set up a TLC5940 output. If the output is within range of - // the connected number of chips (16 outputs per chip), assign it - // to the current index, otherwise leave it unattached. - if (i < (TLC5940_NCHIPS)*16) - lwPin[i] = new Lw5940Out(i); - else - lwPin[i] = new LwUnusedOut(); + if (ledWizPortMap[i].pin != NC) + ++numOutputs; + } + + // always set up at least 32 outputs, so that we don't have to + // check bounds on commands from the basic LedWiz protocol + if (numOutputs < 32) + numOutputs = 32; + + // allocate the pin array + lwPin = new LwOut*[numOutputs]; + + // allocate the current brightness array + outLevel = new float[numOutputs]; + + // allocate a temporary array to keep track of which physical + // TLC5940 ports we've assigned so far + char *tlcasi = new char[TLC5940_NCHIPS*16+1]; + memset(tlcasi, 0, TLC5940_NCHIPS*16); -#else // ENABLE_TLC5940 - // Set up the GPIO pin. If the pin is not connected ("NC" in the - // pin map), set up a dummy "unused" output for it. If it's a - // real pin, set up a PWM-capable or Digital-Only output handler - // object, according to the pin type in the map. - PinName p = (i < countof(ledWizPortMap) ? ledWizPortMap[i].pin : NC); - if (p == NC) - lwPin[i] = new LwUnusedOut(); - else if (ledWizPortMap[i].isPWM) - lwPin[i] = new LwPwmOut(p); - else - lwPin[i] = new LwDigOut(p); + // assign all pins from the port map in config.h + for (i = 0 ; i < countof(ledWizPortMap) ; ++i) + { + // Figure out which type of pin to assign to this port: + // + // - If it has a valid GPIO pin (other than "NC"), create a PWM + // or Digital output pin according to the port type. + // + // - If the pin has a TLC5940 port number, set up a TLC5940 port. + // + // - Otherwise, the pin is unconnected, so set up an unused out. + // + PinName p = ledWizPortMap[i].pin; + int flags = ledWizPortMap[i].flags; + int tlcPortNum = ledWizPortMap[i].tlcPortNum; + int isPwm = flags & PORT_IS_PWM; + int activeLow = flags & PORT_ACTIVE_LOW; + if (p != NC) + { + // This output is a GPIO - set it up as PWM or Digital, and + // active high or low, as marked + if (isPwm) + lwPin[i] = activeLow ? new LwPwmOutInv(p) : new LwPwmOut(p); + else + lwPin[i] = activeLow ? new LwDigOutInv(p) : new LwDigOut(p); + } + else if (tlcPortNum != 0) + { + // It's a TLC5940 port. Note that the port numbering in the map + // starts at 1, but internally we number the ports starting at 0, + // so subtract one to get the correct numbering. + lwPin[i] = activeLow ? new Lw5940OutInv(tlcPortNum-1) : new Lw5940Out(tlcPortNum-1); -#endif // ENABLE_TLC5940 - + // mark this port as used, so that we don't reassign it when we + // fill out the remaining unassigned ports + tlcasi[tlcPortNum-1] = 1; + } + else + { + // it's not a GPIO or TLC5940 port -> it's not connected + lwPin[i] = new LwUnusedOut(); + } + lwPin[i]->set(0); } + + // find the next unassigned tlc port + int tlcnxt; + for (tlcnxt = 0 ; tlcnxt < TLC5940_NCHIPS*16 && tlcasi[tlcnxt] ; ++tlcnxt) ; + + // assign any remaining pins + for ( ; i < numOutputs ; ++i) + { + // If we have any more unassigned TLC5940 outputs, assign this LedWiz + // port to the next available TLC5940 output. Otherwise make it + // unconnected. + if (tlcnxt < TLC5940_NCHIPS*16) + { + // we have a TLC5940 output available - assign it + lwPin[i] = new Lw5940Out(tlcnxt); + + // find the next unassigned TLC5940 output, for the next port + for (++tlcnxt ; tlcnxt < TLC5940_NCHIPS*16 && tlcasi[tlcnxt] ; ++tlcnxt) ; + } + else + { + // no more ports available - set up this port as unconnected + lwPin[i] = new LwUnusedOut(); + } + } + + // done with the temporary TLC5940 port assignment list + delete [] tlcasi; } -// Current absolute brightness level for an output. This is a float -// value from 0.0 for fully off to 1.0 for fully on. This is the final -// derived value for the port. For outputs set by LedWiz messages, -// this is derived from te LedWiz state, and is updated on each pulse -// timer interrupt for lights in flashing states. For outputs set by -// extended protocol messages, this is simply the brightness last set. -static float outLevel[NUM_OUTPUTS]; - // LedWiz output states. // // The LedWiz protocol has two separate control axes for each output. @@ -1252,6 +1394,263 @@ } d; }; +// --------------------------------------------------------------------------- +// +// Simple binary (on/off) input debouncer. Requires an input to be stable +// for a given interval before allowing an update. +// +class Debouncer +{ +public: + Debouncer(bool initVal, float tmin) + { + t.start(); + this->stable = this->prv = initVal; + this->tmin = tmin; + } + + // Get the current stable value + bool val() const { return stable; } + + // Apply a new sample. This tells us the new raw reading from the + // input device. + void sampleIn(bool val) + { + // If the new raw reading is different from the previous + // raw reading, we've detected an edge - start the clock + // on the sample reader. + if (val != prv) + { + // we have an edge - reset the sample clock + t.reset(); + + // this is now the previous raw sample for nxt time + prv = val; + } + else if (val != stable) + { + // The new raw sample is the same as the last raw sample, + // and different from the stable value. This means that + // the sample value has been the same for the time currently + // indicated by our timer. If enough time has elapsed to + // consider the value stable, apply the new value. + if (t.read() > tmin) + stable = val; + } + } + +private: + // current stable value + bool stable; + + // last raw sample value + bool prv; + + // elapsed time since last raw input change + Timer t; + + // Minimum time interval for stability, in seconds. Input readings + // must be stable for this long before the stable value is updated. + float tmin; +}; + + +// --------------------------------------------------------------------------- +// +// Turn off all outputs and restore everything to the default LedWiz +// state. This sets outputs #1-32 to LedWiz profile value 48 (full +// brightness) and switch state Off, sets all extended outputs (#33 +// and above) to zero brightness, and sets the LedWiz flash rate to 2. +// This effectively restores the power-on conditions. +// +void allOutputsOff() +{ + // reset all LedWiz outputs to OFF/48 + for (int i = 0 ; i < 32 ; ++i) + { + outLevel[i] = 0; + wizOn[i] = 0; + wizVal[i] = 48; + lwPin[i]->set(0); + } + + // reset all extended outputs (ports >32) to full off (brightness 0) + for (int i = 32 ; i < numOutputs ; ++i) + { + outLevel[i] = 0; + lwPin[i]->set(0); + } + + // restore default LedWiz flash rate + wizSpeed = 2; +} + +// --------------------------------------------------------------------------- +// +// TV ON timer. If this feature is enabled, we toggle a TV power switch +// relay (connected to a GPIO pin) to turn on the cab's TV monitors shortly +// after the system is powered. This is useful for TVs that don't remember +// their power state and don't turn back on automatically after being +// unplugged and plugged in again. This feature requires external +// circuitry, which is built in to the expansion board and can also be +// built separately - see the Build Guide for the circuit plan. +// +// Theory of operation: to use this feature, the cabinet must have a +// secondary PC-style power supply (PSU2) for the feedback devices, and +// this secondary supply must be plugged in to the same power strip or +// switched outlet that controls power to the TVs. This lets us use PSU2 +// as a proxy for the TV power state - when PSU2 is on, the TV outlet is +// powered, and when PSU2 is off, the TV outlet is off. We use a little +// latch circuit powered by PSU2 to monitor the status. The latch has a +// current state, ON or OFF, that we can read via a GPIO input pin, and +// we can set the state to ON by pulsing a separate GPIO output pin. As +// long as PSU2 is powered off, the latch stays in the OFF state, even if +// we try to set it by pulsing the SET pin. When PSU2 is turned on after +// being off, the latch starts receiving power but stays in the OFF state, +// since this is the initial condition when the power first comes on. So +// if our latch state pin is reading OFF, we know that PSU2 is either off +// now or *was* off some time since we last checked. We use a timer to +// check the state periodically. Each time we see the state is OFF, we +// try pulsing the SET pin. If the state still reads as OFF, we know +// that PSU2 is currently off; if the state changes to ON, though, we +// know that PSU2 has gone from OFF to ON some time between now and the +// previous check. When we see this condition, we start a countdown +// timer, and pulse the TV switch relay when the countdown ends. +// +// This scheme might seem a little convoluted, but it neatly handles +// all of the different cases that can occur: +// +// - Most cabinets systems are set up with "soft" PC power switches, +// so that the PC goes into "Soft Off" mode (ACPI state S5, in Windows +// parlance) when the user turns off the cabinet. In this state, the +// motherboard supplies power to USB devices, so the KL25Z continues +// running without interruption. The latch system lets us monitor +// the power state even when we're never rebooted, since the latch +// will turn off when PSU2 is off regardless of what the KL25Z is doing. +// +// - Some cabinet builders might prefer to use "hard" power switches, +// cutting all power to the cabinet, including the PC motherboard (and +// thus the KL25Z) every time the machine is turned off. This also +// applies to the "soft" switch case above when the cabinet is unplugged, +// a power outage occurs, etc. In these cases, the KL25Z will do a cold +// boot when the PC is turned on. We don't know whether the KL25Z +// will power up before or after PSU2, so it's not good enough to +// observe the *current* state of PSU2 when we first check - if PSU2 +// were to come on first, checking the current state alone would fool +// us into thinking that no action is required, because we would never +// have known that PSU2 was ever off. The latch handles this case by +// letting us see that PSU2 *was* off before we checked. +// +// - If the KL25Z is rebooted while the main system is running, or the +// KL25Z is unplugged and plugged back in, we will correctly leave the +// TVs as they are. The latch state is independent of the KL25Z's +// power or software state, so it's won't affect the latch state when +// the KL25Z is unplugged or rebooted; when we boot, we'll see that +// the latch is already on and that we don't have to turn on the TVs. +// This is important because TV ON buttons are usually on/off toggles, +// so we don't want to push the button on a TV that's already on. +// +// +#ifdef ENABLE_TV_TIMER + +// Current PSU2 state: +// 1 -> default: latch was on at last check, or we haven't checked yet +// 2 -> latch was off at last check, SET pulsed high +// 3 -> SET pulsed low, ready to check status +// 4 -> TV timer countdown in progress +// 5 -> TV relay on +// +int psu2_state = 1; +DigitalIn psu2_status_sense(PSU2_STATUS_SENSE); +DigitalOut psu2_status_set(PSU2_STATUS_SET); +DigitalOut tv_relay(TV_RELAY_PIN); +Timer tv_timer; +void TVTimerInt() +{ + // Check our internal state + switch (psu2_state) + { + case 1: + // Default state. This means that the latch was on last + // time we checked or that this is the first check. In + // either case, if the latch is off, switch to state 2 and + // try pulsing the latch. Next time we check, if the latch + // stuck, it means that PSU2 is now on after being off. + if (!psu2_status_sense) + { + // switch to OFF state + psu2_state = 2; + + // try setting the latch + psu2_status_set = 1; + } + break; + + case 2: + // PSU2 was off last time we checked, and we tried setting + // the latch. Drop the SET signal and go to CHECK state. + psu2_status_set = 0; + psu2_state = 3; + break; + + case 3: + // CHECK state: we pulsed SET, and we're now ready to see + // if that stuck. If the latch is now on, PSU2 has transitioned + // from OFF to ON, so start the TV countdown. If the latch is + // off, our SET command didn't stick, so PSU2 is still off. + if (psu2_status_sense) + { + // The latch stuck, so PSU2 has transitioned from OFF + // to ON. Start the TV countdown timer. + tv_timer.reset(); + tv_timer.start(); + psu2_state = 4; + } + else + { + // The latch didn't stick, so PSU2 was still off at + // our last check. Try pulsing it again in case PSU2 + // was turned on since the last check. + psu2_status_set = 1; + psu2_state = 2; + } + break; + + case 4: + // TV timer countdown in progress. If we've reached the + // delay time, pulse the relay. + if (tv_timer.read() >= TV_DELAY_TIME) + { + // turn on the relay for one timer interval + tv_relay = 1; + psu2_state = 5; + } + break; + + case 5: + // TV timer relay on. We pulse this for one interval, so + // it's now time to turn it off and return to the default state. + tv_relay = 0; + psu2_state = 1; + break; + } +} + +Ticker tv_ticker; +void startTVTimer() +{ + // Set up our time routine to run every 1/4 second. + tv_ticker.attach(&TVTimerInt, 0.25); +} + + +#else // ENABLE_TV_TIMER +// +// TV timer not used - just provide a dummy startup function +void startTVTimer() { } +// +#endif // ENABLE_TV_TIMER + // --------------------------------------------------------------------------- // @@ -1269,12 +1668,31 @@ ledG = 1; ledB = 1; + // start the TV timer, if applicable + startTVTimer(); + + // we're not connected/awake yet + bool connected = false; + time_t connectChangeTime = time(0); + +#if TLC5940_NCHIPS + // start the TLC5940 clock + for (int i = 0 ; i < numOutputs ; ++i) lwPin[i]->set(1.0); + tlc5940.start(); + + // enable power to the TLC5940 opto/LED outputs +# ifdef TLC5940_PWRENA + DigitalOut tlcPwrEna(TLC5940_PWRENA); + tlcPwrEna = 1; +# endif +#endif + // initialize the LedWiz ports initLwOut(); // initialize the button input ports initButtons(); - + // we don't need a reset yet bool needReset = false; @@ -1314,7 +1732,7 @@ // number from the saved configuration. MyUSBJoystick js( USB_VENDOR_ID, - USB_PRODUCT_ID | cfg.d.ledWizUnitNo, + MAKE_USB_PRODUCT_ID(USB_VENDOR_ID, USB_PRODUCT_ID, cfg.d.ledWizUnitNo), USB_VERSION_NO); // last report timer - we use this to throttle reports, since VP @@ -1360,11 +1778,6 @@ bool reportPix = false; #endif -#ifdef ENABLE_TLC5940 - // start the TLC5940 clock - tlc5940.start(); -#endif - // create our plunger sensor object PlungerSensor plungerSensor; @@ -1467,7 +1880,11 @@ // 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 + // 0x0001 -> plunger sensor enabled + // 0x8000 -> RESERVED - must always be zero + // + // Note that the high bit (0x8000) must always be 0, since we use that + // to distinguish special request reply packets. uint16_t statusFlags = (cfg.d.plungerEnabled ? 0x01 : 0x00); // we're all set up - now just loop, processing sensor reports and @@ -1486,6 +1903,24 @@ // all Led-Wiz reports are 8 bytes exactly if (report.length == 8) { + // LedWiz commands come in two varieties: SBA and PBA. An + // SBA is marked by the first byte having value 64 (0x40). In + // the real LedWiz protocol, any other value in the first byte + // means it's a PBA message. However, *valid* PBA messages + // always have a first byte (and in fact all 8 bytes) in the + // range 0-49 or 129-132. Anything else is invalid. We take + // advantage of this to implement private protocol extensions. + // So our full protocol is as follows: + // + // first byte = + // 0-48 -> LWZ-PBA + // 64 -> LWZ SBA + // 65 -> private control message; second byte specifies subtype + // 129-132 -> LWZ-PBA + // 200-219 -> extended bank brightness set for outputs N to N+6, where + // N is (first byte - 200)*7 + // other -> reserved for future use + // uint8_t *data = report.data; if (data[0] == 64) { @@ -1497,11 +1932,27 @@ // update all on/off states for (int i = 0, bit = 1, ri = 1 ; i < 32 ; ++i, bit <<= 1) { + // figure the on/off state bit for this output if (bit == 0x100) { bit = 1; ++ri; } + + // set the on/off state wizOn[i] = ((data[ri] & bit) != 0); + + // If the wizVal setting is 255, it means that this + // output was last set to a brightness value with the + // extended protocol. Return it to LedWiz control by + // rescaling the brightness setting to the LedWiz range + // and updating wizVal with the result. If it's any + // other value, it was previously set by a PBA message, + // so simply retain the last setting - in the normal + // LedWiz protocol, the "profile" (brightness) and on/off + // states are independent, so an SBA just turns an output + // on or off but retains its last brightness level. + if (wizVal[i] == 255) + wizVal[i] = (uint8_t)round(outLevel[i]*48); } // set the flash speed - enforce the value range 1-7 @@ -1571,21 +2022,88 @@ ledB = 0; ledG = 1; } + else if (data[1] == 4) + { + // 4 = hardware configuration query + // (No parameters) + wait_ms(1); + js.reportConfig(numOutputs, cfg.d.ledWizUnitNo); + } + else if (data[1] == 5) + { + // 5 = all outputs off, reset to LedWiz defaults + allOutputsOff(); + } #endif // ENABLE_JOYSTICK } + else if (data[0] >= 200 && data[0] < 220) + { + // Extended protocol - banked brightness update. + // data[0]-200 gives us the bank of 7 outputs we're setting: + // 200 is outputs 0-6, 201 is outputs 7-13, 202 is 14-20, etc. + // The remaining bytes are brightness levels, 0-255, for the + // seven outputs in the selected bank. The LedWiz flashing + // modes aren't accessible in this message type; we can only + // set a fixed brightness, but in exchange we get 8-bit + // resolution rather than the paltry 0-48 scale that the real + // LedWiz uses. There's no separate on/off status for outputs + // adjusted with this message type, either, as there would be + // for a PBA message - setting a non-zero value immediately + // turns the output, overriding the last SBA setting. + // + // For outputs 0-31, this overrides any previous PBA/SBA + // settings for the port. Any subsequent PBA/SBA message will + // in turn override the setting made here. It's simple - the + // most recent message of either type takes precedence. For + // outputs above the LedWiz range, PBA/SBA messages can't + // address those ports anyway. + int i0 = (data[0] - 200)*7; + int i1 = i0 + 7 < numOutputs ? i0 + 7 : numOutputs; + for (int i = i0 ; i < i1 ; ++i) + { + // set the brightness level for the output + float b = data[i-i0+1]/255.0; + outLevel[i] = b; + + // if it's in the basic LedWiz output set, set the LedWiz + // profile value to 255, which means "use outLevel" + if (i < 32) + wizVal[i] = 255; + + // set the output + lwPin[i]->set(b); + } + } else { - // LWZ-PBA - full state dump; each byte is one output - // in the current bank. pbaIdx keeps track of the bank; - // this is incremented implicitly by each PBA message. + // Everything else is LWZ-PBA. This is a full "profile" + // dump from the host for one bank of 8 outputs. Each + // byte sets one output in the current bank. The current + // bank is implied; the bank starts at 0 and is reset to 0 + // by any LWZ-SBA message, and is incremented to the next + // bank by each LWZ-PBA message. Our variable pbaIdx keeps + // track of our notion of the current bank. There's no direct + // way for the host to select the bank; it just has to count + // on us staying in sync. In practice, the host will always + // send a full set of 4 PBA messages in a row to set all 32 + // outputs. + // + // Note that a PBA implicitly overrides our extended profile + // messages (message prefix 200-219), because this sets the + // wizVal[] entry for each output, and that takes precedence + // over the extended protocol settings. + // //printf("LWZ-PBA[%d] %02x %02x %02x %02x %02x %02x %02x %02x\r\n", // pbaIdx, data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]); - // update all output profile settings + // Update all output profile settings for (int i = 0 ; i < 8 ; ++i) wizVal[pbaIdx + i] = data[i]; - // update the physical LED state if this is the last bank + // Update the physical LED state if this is the last bank. + // Note that hosts always send a full set of four PBA + // messages, so there's no need to do a physical update + // until we've received the last bank's PBA message. if (pbaIdx == 24) { updateWizOuts(); @@ -2112,10 +2630,28 @@ printf("%d,%d\r\n", x, y); #endif + // check for connection status changes + int newConnected = js.isConnected() && !js.isSuspended(); + if (newConnected != connected) + { + // give it a few seconds to stabilize + time_t tc = time(0); + if (tc - connectChangeTime > 3) + { + // note the new status + connected = newConnected; + connectChangeTime = tc; + + // if we're no longer connected, turn off all outputs + if (!connected) + allOutputsOff(); + } + } + // provide a visual status indication on the on-board LED if (calBtnState < 2 && hbTimer.read_ms() > 1000) { - if (js.isSuspended() || !js.isConnected()) + if (!newConnected) { // suspended - turn off the LED ledR = 1;