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 26:cb71c4af2912, committed 2015-09-23
- Comitter:
- mjr
- Date:
- Wed Sep 23 05:06:39 2015 +0000
- Parent:
- 25:e22b88bd783a
- Child:
- 27:26de4b0917a7
- Commit message:
- Initial TLC5940 PWM controller chip support.
Changed in this revision
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/FastPWM.lib Wed Sep 23 05:06:39 2015 +0000 @@ -0,0 +1,1 @@ +http://mbed.org/users/Sissors/code/FastPWM/#1f451660d8c0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/TLC5940/TLC5940.h Wed Sep 23 05:06:39 2015 +0000 @@ -0,0 +1,298 @@ +// Pinscape Controller TLC5940 interface +// +// Based on Spencer Davis's mbed TLC5940 library. Adapted for the +// KL25Z, and simplified to just the functions needed for this +// application. In particular, this version doesn't include support +// for dot correction programming or status input. This version also +// uses a different approach for sending the grayscale data updates, +// sending updates during the blanking interval rather than overlapping +// them with the PWM cycle. This results in very slightly longer +// blanking intervals when updates are pending, effectively reducing +// the PWM "on" duty cycle (and thus the output brightness) by about +// 0.3%. This shouldn't be perceptible to users, so it's a small +// trade-off for the advantage gained, which is much better signal +// stability when using multiple TLC5940s daisy-chained together. +// I saw a lot of instability when using the overlapped approach, +// which seems to be eliminated entirely when sending updates during +// the blanking interval. + + +#ifndef TLC5940_H +#define TLC5940_H + +#include "mbed.h" +#include "FastPWM.h" + +/** + * 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. + * + * The SPI clock must be fast enough that the data transmission + * time for a full update is comfortably less than the blanking + * cycle time. The grayscale refresh requires 192 bits per TLC5940 + * in the daisy chain, and each bit takes one SPI clock to send. + * Our reference setup in the Pinscape controller allows for up to + * 4 TLC5940s, so a full refresh cycle on a fully populated system + * would be 768 SPI clocks. The blanking cycle is 4096 GSCLK cycles. + * + * t(blank) = 4096 * 1/GSCLK_SPEED + * t(refresh) = 768 * 1/SPI_SPEED + * Therefore: SPI_SPEED must be > 768/4096 * GSCLK_SPEED + * + * Since the SPI speed can be so high, and since we want to keep + * the GSCLK speed relatively low, the constraint above simply + * 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 + +/** + * The rate at which the GSCLK pin is pulsed. This also controls + * how often the reset function is called. The reset function call + * rate is (1/GSCLK_SPEED) * 4096. The maximum reliable rate is + * around 32Mhz. It's best to keep this rate as low as possible: + * the higher the rate, the higher the refresh() call frequency, + * so the higher the CPU load. + * + * The lower bound is probably dependent on the application. For + * driving LEDs, the limiting factor is that lower rates will increase + * visible flicker. 200 kHz seems to be a good lower bound for LEDs. + * That provides about 48 cycles per second - that's about the same as + * the 50 Hz A/C cycle rate in many countries, which was itself chosen + * so that incandescent lights don't flicker. (This rate is a function + * of human eye physiology, which has its own refresh cycle of sorts + * that runs at about 50 Hz. If you're designing an LED system for + * viewing by cats or drosophila, you might want to look into your + * target species' eye physiology, since the persistence of vision + * rate varies quite a bit from species to species.) Flicker tends to + * be more noticeable in LEDs than in incandescents, since LEDs don't + * have the thermal inertia of incandescents, so we use a slightly + * higher default here. 500 kHz = 122 full grayscale cycles per + * second = 122 reset calls per second (call every 8ms). + */ +#define GSCLK_SPEED 500000 + +/** + * This class controls a TLC5940 PWM driver IC. + * + * Using the TLC5940 class to control an LED: + * @code + * #include "mbed.h" + * #include "TLC5940.h" + * + * // Create the TLC5940 instance + * TLC5940 tlc(p7, p5, p21, p9, p10, p11, p12, 1); + * + * int main() + * { + * // Enable the first LED + * tlc.set(0, 0xfff); + * + * while(1) + * { + * } + * } + * @endcode + */ +class TLC5940 +{ +public: + /** + * Set up the TLC5940 + * @param SCLK - The SCK pin of the SPI bus + * @param MOSI - The MOSI pin of the SPI bus + * @param GSCLK - The GSCLK pin of the TLC5940(s) + * @param BLANK - The BLANK pin of the TLC5940(s) + * @param XLAT - The XLAT pin of the TLC5940(s) + * @param nchips - The number of TLC5940s (if you are daisy chaining) + */ + TLC5940(PinName SCLK, PinName MOSI, PinName GSCLK, PinName BLANK, PinName XLAT, int nchips) + : spi(MOSI, NC, SCLK), + gsclk(GSCLK), + blank(BLANK), + xlat(XLAT), + nchips(nchips), + newGSData(false) + { + // allocate the grayscale buffer + gs = new unsigned short[nchips*16]; + + // Configure SPI format and speed. Note that KL25Z ONLY supports 8-bit + // mode. The TLC5940 nominally requires 12-bit data blocks for the + // grayscale levels, but SPI is ultimately just a bit-level serial format, + // so we can reformat the 12-bit blocks into 8-bit bytes to fit the + // KL25Z's limits. This should work equally well on other microcontrollers + // that are more flexible. The TLC5940 appears to require polarity/phase + // format 0. + spi.format(8, 0); + spi.frequency(SPI_SPEED); + + // Set output pin states + xlat = 0; + blank = 1; + + // Configure PWM output for GSCLK frequency at 50% duty cycle + gsclk.period(1.0/GSCLK_SPEED); + gsclk.write(.5); + blank = 0; + + // Set up the first call to the reset function, which asserts BLANK to + // end the PWM cycle and handles new grayscale data output and latching. + // The original version of this library uses a timer to call reset + // periodically, but that approach is somewhat problematic because the + // reset function itself takes a small amount of time to run, so the + // *actual* cycle is slightly longer than what we get from counting + // GS clocks. Running reset on a timer therefore causes the calls to + // slip out of phase with the actual full cycles, which causes + // premature blanking that shows up as visible flicker. To get the + // reset cycle to line up exactly with a full PWM cycle, it works + // better to set up a new timer on each cycle, *after* we've finished + // with the somewhat unpredictable overhead of the interrupt handler. + // This ensures that we'll get much closer to exact alignment of the + // cycle phase, and in any case the worst that happens is that some + // cycles are very slightly too long or short (due to imperfections + // in the timer clock vs the PWM clock that determines the GSCLCK + // output to the TLC5940), which is far less noticeable than a + // constantly rotating phase misalignment. + reset_timer.attach(this, &TLC5940::reset, (1.0/GSCLK_SPEED)*4096.0); + } + + ~TLC5940() + { + delete [] gs; + } + + /** + * Set the next chunk of grayscale data to be sent + * @param data - Array of 16 bit shorts containing 16 12 bit grayscale data chunks per TLC5940 + * @note These must be in intervals of at least (1/GSCLK_SPEED) * 4096 to be sent + */ + void set(int idx, unsigned short data) + { + // store the data, and flag the pending update for the interrupt handler to carry out + gs[idx] = data; + newGSData = true; + } + +private: + // current level for each output + unsigned short *gs; + + // SPI port - only MOSI and SCK are used + SPI spi; + + // use a PWM out for the grayscale clock - this provides a stable + // square wave signal without consuming CPU + FastPWM gsclk; + + // Digital out pins used for the TLC5940 + DigitalOut blank; + DigitalOut xlat; + + // number of daisy-chained TLC5940s we're controlling + int nchips; + + // Timeout to end each PWM cycle. This is a one-shot timer that we reset + // on each cycle. + Timeout reset_timer; + + // Has new GS/DC data been loaded? + volatile bool newGSData; + + // Function to reset the display and send the next chunks of data + void reset() + { + // turn off the grayscale clock, and assert BLANK to end the grayscale cycle + gsclk.write(0); + blank = 1; + + // If we have new GS data, send it now + if (newGSData) + { + // 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. + // + // Note that there's no CPU performance penalty to this + // approach. The KL25Z SPI implementation isn't capable of + // asynchronous DMA, so the CPU has to wait for the + // transmission no matter when it happens. The only downside + // I see to this approach is that it decreases the duty cycle + // of the PWM during updates - but very slightly. With the + // SPI clock at 30 MHz and the PWM clock at 500 kHz, the full + // PWM cycle is 8192us, and the data refresh time is 25us. + // So by doing the data refersh in the blanking interval, + // we're effectively extending the PWM cycle to 8217us, + // which is 0.3% longer. Since the outputs are all off + // during the blanking cycle, this is equivalent to + // decreasing all of the output brightnesses by 0.3%. That + // should be imperceptible to users. + update(); + + // the chips are now in sync with our data, so we have no more + // pending update + newGSData = false; + + // latch the new data while we're still blanked + xlat = 1; + xlat = 0; + } + + // end the blanking interval and restart the grayscale clock + blank = 0; + gsclk.write(.5); + + // set up the next blanking interrupt + reset_timer.attach(this, &TLC5940::reset, (1.0/GSCLK_SPEED)*4096.0); + } + + void update() + { + // Send GS data. The serial format orders the outputs from last to first + // (output #15 on the last chip in the daisy-chain to output #0 on the + // first chip). For each output, we send 12 bits containing the grayscale + // level (0 = fully off, 0xFFF = fully on). Bit order is most significant + // bit first. + // + // The KL25Z SPI can only send in 8-bit increments, so we need to divvy up + // the 12-bit outputs into 8-bit bytes. Each pair of 12-bit outputs adds up + // to 24 bits, which divides evenly into 3 bytes, so send each pairs of + // outputs as three bytes: + // + // [ element i+1 bits ] [ element i bits ] + // 11 10 9 8 7 6 5 4 3 2 1 0 11 10 9 8 7 6 5 4 3 2 1 0 + // [ first byte ] [ second byte ] [ third byte ] + for (int i = (16 * nchips) - 2 ; i >= 0 ; i -= 2) + { + // first byte - element i+1 bits 4-11 + spi.write(((gs[i+1] & 0xFF0) >> 4) & 0xff); + + // second byte - element i+1 bits 0-3, then element i bits 8-11 + spi.write((((gs[i+1] & 0x00F) << 4) | ((gs[i] & 0xF00) >> 8)) & 0xFF); + + // third byte - element i bits 0-7 + spi.write(gs[i] & 0x0FF); + } + } +}; + + +#endif
--- a/config.h Tue Sep 01 04:27:15 2015 +0000 +++ b/config.h Wed Sep 23 05:06:39 2015 +0000 @@ -105,6 +105,52 @@ // -------------------------------------------------------------------------- // +// TLC5940 PWM controller chip setup - Enhanced LedWiz emulation +// +// By default, the Pinscape Controller software can provide limited LedWiz +// emulation through the KL25Z's on-board GPIO ports. This lets you hook +// up external devices, such as LED flashers or solenoids, to the KL25Z +// outputs (using external circuitry to boost power - KL25Z GPIO ports +// are limited to a meager 4mA per port). This capability is limited by +// the number of available GPIO ports on the KL25Z, and even smaller limit +// of 10 PWM-capable GPIO ports. +// +// As an alternative, the controller software lets you use external PWM +// controller chips to control essentially unlimited channels with full +// PWM control on all channels. This requires building external circuitry +// using TLC5940 chips. Each TLC5940 chip provides 16 full PWM channels, +// and you can daisy-chain multiple TLC5940 chips together to set up 32, +// 48, 64, or more channels. +// +// 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 + +// 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 3 + +// 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. +#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 + +// -------------------------------------------------------------------------- +// // Plunger CCD sensor. // // If you're NOT using the CCD sensor, comment out the next line (by adding @@ -325,6 +371,10 @@ // "NC" entries below to the reallocated pin name. The limit is 32 // buttons total. // +// (If you're using TLC5940 chips to control outputs, ALL of the +// LedWiz mapped ports can be reassigned as keys, except, of course, +// those taken over for the 5940 interface.) +// // Note: PTD1 (pin J2-12) should NOT be assigned as a button input, // as this pin is physically connected on the KL25Z to the on-board // indicator LED's blue segment. This precludes any other use of @@ -373,6 +423,11 @@ // // 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. +// // 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.
--- a/main.cpp Tue Sep 01 04:27:15 2015 +0000 +++ b/main.cpp Wed Sep 23 05:06:39 2015 +0000 @@ -137,6 +137,15 @@ // but with a slight practical need for a handful of extra ports (I'm using the // cutting-edge 10-contactor setup, so my real LedWiz is full!). // +// - Enhanced LedWiz emulation with TLC5940 PWM controller chips. You can attach +// external PWM controller chips for controlling device outputs, instead of using +// the limited LedWiz emulation through the on-board GPIO ports as described above. +// 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. +// +// // The on-board LED on the KL25Z flashes to indicate the current device status: // // two short red flashes = the device is powered but hasn't successfully @@ -214,6 +223,7 @@ #include "tsl1410r.h" #include "FreescaleIAP.h" #include "crc32.h" +#include "TLC5940.h" // our local configuration file #define DECL_EXTERNS @@ -226,6 +236,12 @@ // number of elements in an array #define countof(x) (sizeof(x)/sizeof((x)[0])) +// floating point square of a number +inline float square(float x) { return x*x; } + +// floating point rounding +inline float round(float x) { return x > 0 ? floor(x + 0.5) : ceil(x - 0.5); } + // --------------------------------------------------------------------------- // USB device vendor ID, product ID, and version. @@ -309,6 +325,13 @@ // // On-board RGB LED elements - we use these for diagnostic displays. // +// Note that LED3 (the blue segment) is hard-wired on the KL25Z to PTD1, +// so PTD1 shouldn't be used for any other purpose (e.g., as a keyboard +// input or a device output). (This is kind of unfortunate in that it's +// one of only two ports exposed on the jumper pins that can be muxed to +// SPI0 SCLK. This effectively limits us to PTC5 if we want to use the +// SPI capability.) +// DigitalOut ledR(LED1), ledG(LED2), ledB(LED3); @@ -316,20 +339,77 @@ // // LedWiz emulation // +// There are two modes for this feature. The default mode uses the on-board +// GPIO ports to implement device outputs - each LedWiz software port is +// connected to a physical GPIO pin on the KL25Z. The KL25Z only has 10 +// PWM channels, so in this mode only 10 LedWiz ports will be dimmable; the +// rest are strictly on/off. The KL25Z also has a limited number of GPIO +// ports overall - not enough for the full complement of 32 LedWiz ports +// and 24 VP joystick inputs, so it's necessary to trade one against the +// other if both features are to be used. +// +// The alternative, enhanced mode uses external TLC5940 PWM controller +// chips to control device outputs. In this mode, each LedWiz software +// port is mapped to an output on one of the external TLC5940 chips. +// Two 5940s is enough for the full set of 32 LedWiz ports, and we can +// support even more chips for even more outputs (although doing so requires +// breaking LedWiz compatibility, since the LedWiz USB protocol is hardwired +// for 32 outputs). Every port in this mode has full PWM support. +// +// Current starting output index for "PBA" messages from the PC (using +// the LedWiz USB protocol). Each PBA message implicitly uses the +// current index as the starting point for the ports referenced in +// the message, and increases it (by 8) for the next call. static int pbaIdx = 0; -// LedWiz output pin interface. We create a cover class to virtualize -// digital vs PWM outputs and give them a common interface. The KL25Z -// unfortunately doesn't have enough hardware PWM channels to support -// PWM on all 32 LedWiz outputs, so we provide as many PWM channels as -// we can (10), and fill out the rest of the outputs with plain digital -// outs. +// Generic LedWiz output port interface. We create a cover class to +// virtualize digital vs PWM outputs, and on-board KL25Z GPIO vs external +// TLC5940 outputs, and give them all a common interface. class LwOut { public: + // Set the output intensity. 'val' is 0.0 for fully off, 1.0 for + // fully on, and fractional values for intermediate intensities. virtual void set(float val) = 0; }; + + +#ifdef ENABLE_TLC5940 + +// The TLC5940 interface object. +TLC5940 tlc5940(TLC5940_SCLK, TLC5940_SIN, TLC5940_GSCLK, TLC5940_BLANK, + TLC5940_XLAT, TLC5940_NCHIPS); + +// LwOut class for TLC5940 outputs. These are fully PWM capable. +// The 'idx' value in the constructor is the output index in the +// daisy-chained TLC5940 array. 0 is output #0 on the first chip, +// 1 is #1 on the first chip, 15 is #15 on the first chip, 16 is +// #0 on the second chip, 32 is #0 on the third chip, etc. +class Lw5940Out: public LwOut +{ +public: + Lw5940Out(int idx) : idx(idx) { prv = -1; } + virtual void set(float val) + { + if (val != prv) + tlc5940.set(idx, (int)(val * 4095)); + } + int idx; + float prv; +}; + +#else // ENABLE_TLC5940 + +// +// Default LedWiz mode - using on-board GPIO ports. In this mode, we +// assign a KL25Z GPIO port to each LedWiz output. We have to use a +// mix of PWM-capable and Digital-Only ports in this configuration, +// since the KL25Z hardware only has 10 PWM channels, which isn't +// enough to fill out the full complement of 32 LedWiz outputs. +// + +// LwOut class for a PWM-capable GPIO port class LwPwmOut: public LwOut { public: @@ -342,6 +422,8 @@ PwmOut p; float prv; }; + +// LwOut class for a Digital-Only (Non-PWM) GPIO port class LwDigOut: public LwOut { public: @@ -354,6 +436,17 @@ DigitalOut p; 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 { public: @@ -361,7 +454,11 @@ virtual void set(float val) { } }; -// output pin array +// Array of output assignments. This array is indexed by the LedWiz +// output port number; that protocol is hardwired for 32 ports, so we +// need 32 elements in the array. Each element is an LwOut object +// that provides the mapping to the physical output corresponding to +// the software port. static LwOut *lwPin[32]; // initialize the output pin array @@ -369,6 +466,18 @@ { for (int i = 0 ; i < countof(lwPin) ; ++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(); + +#else // ENABLE_TLC5940 + // Set up the GPIO pin, according to whether it's PWM-capable or + // digital-only, and whether or not it's assigned at all. PinName p = (i < countof(ledWizPortMap) ? ledWizPortMap[i].pin : NC); if (p == NC) lwPin[i] = new LwUnusedOut(); @@ -376,6 +485,9 @@ lwPin[i] = new LwPwmOut(p); else lwPin[i] = new LwDigOut(p); + +#endif // ENABLE_TLC5940 + } } @@ -601,14 +713,6 @@ }; // --------------------------------------------------------------------------- -// -// Some simple math service routines -// - -inline float square(float x) { return x*x; } -inline float round(float x) { return x > 0 ? floor(x + 0.5) : ceil(x - 0.5); } - -// --------------------------------------------------------------------------- // // Accelerometer (MMA8451Q) //