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 38:091e511ce8a0, committed 2016-01-05
- Comitter:
- mjr
- Date:
- Tue Jan 05 05:23:07 2016 +0000
- Parent:
- 37:ed52738445fc
- Child:
- 39:b3815a1c3802
- Commit message:
- USB improvements
Changed in this revision
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Pinscape_Controller.lib Tue Jan 05 05:23:07 2016 +0000 @@ -0,0 +1,1 @@ +http://mbed.org/users/mjr/code/Pinscape_Controller/#ed52738445fc
--- a/TLC5940/TLC5940.h Thu Dec 24 01:37:40 2015 +0000 +++ b/TLC5940/TLC5940.h Tue Jan 05 05:23:07 2016 +0000 @@ -20,49 +20,47 @@ #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. +// Data Transmission Mode. +// +// NOTE! This section contains a possible workaround to try if you're +// having data signal stability problems with your TLC5940 chips. If +// your chips are working properly, you can ignore this part! // -// 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. +// The software has two options for sending data updates to the chips: +// +// Mode 0: Send data *during* the grayscale cycle. This is the way the +// chips are designed to be used. While the grayscale clock is running, +// we send data for the *next* cycle, then latch the updated data to the +// output registers during the blanking interval at the end of the cycle. // -// 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. +// Mode 1: Send data *between* grayscale cycles. In this mode, we send +// each complete update during a blanking period, then latch the update +// and start the next grayscale cycle. This isn't the way the chips were +// intended to be used, but it works. The disadvantage is that it requires +// the blanking interval to be extended to be long enough for the full +// data update (192 bits * the number of chips in the chain). Since the +// outputs are turned off for the entire blanking period, this reduces +// the overall brightness/intensity of the outputs by reducing the duty +// cycle. The TLC5940 chips can't achieve 100% duty cycle to begin with, +// since they require a certain minimum time in the blanking interval +// between grayscale cycles; however, the minimum is so short that the +// duty cycle is close to 100%. With the full data transmission stuffed +// into the blanking interval, we reduce the duty cycle further below +// 100%. With four chips in the chain, a 28 MHz data clock, and a +// 500 kHz grayscale clock, the reduction is about 0.3%. // -// 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 +// By default, we use Mode 0, because that's the timing model specified +// by the manufacturer, and empirically it works well with the Pinscape +// Expansion boards. +// +// So what's the point of Mode 1? In early testing, with a breadboard +// setup, I saw some problems with data signal stability, which manifested +// as sporadic flickering in the outputs. Switching to Mode 1 improved +// the signal stability considerably. I'm therefore leaving this code +// available as an option in case anyone runs into similar signal problems +// and wants to try the alternative mode as a workaround. +// +#define DATA_UPDATE_INSIDE_BLANKING 0 #include "mbed.h" #include "FastPWM.h" @@ -99,31 +97,28 @@ * isn't a factor. E.g., at SPI=30MHz and GSCLK=500kHz, * t(blank) is 8192us and t(refresh) is 25us. */ -#define SPI_SPEED 2800000 +#define SPI_SPEED 28000000 /** * 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 + * interval 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). + * The lower bound depends on the application. For driving LEDs, + * the limiting factor is that lower rates will increase visible flicker. + * A GSCLK speed of 200 kHz is about as low as you can go with LEDs + * without excessive flicker. That equals about 48 full grayscale + * cycles per second. That might seem perfectly good in that it's + * about the same as the standard 50Hz A/C cycle rate in many countries, + * but the 50Hz rate was chosen to minimize visible flicker in + * incandescent lamps, not LEDs. LEDs need a higher rate because they + * don't have thermal inertia as incandescents do. The default we use + * here is 500 kHz = 122 full grayscale cycles per second. That seems + * to produce excellent visual results. Higher rates would probably + * produce diminishing returns given that they also increase CPU load. */ #define GSCLK_SPEED 500000 @@ -187,8 +182,7 @@ // 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. + // that are more flexible. The TLC5940 requires polarity/phase format 0. spi.format(8, 0); spi.frequency(SPI_SPEED); @@ -211,6 +205,7 @@ // Allocate a DMA buffer. The transfer on each cycle is 192 bits per // chip = 24 bytes per chip. dmabuf = new char[nchips*24]; + memset(dmabuf, 0, nchips*24); // Set up the Simple DMA interface object. We use the DMA controller to // send grayscale data updates to the TLC5940 chips. This lets the CPU @@ -218,14 +213,14 @@ // allows our blanking interrupt handler return almost immediately. // The DMA transfer is from our internal DMA buffer to SPI0, which is // the SPI controller physically connected to the TLC5940s. - sdma.source(dmabuf, 1); - sdma.destination(&(SPI0->D), 0, 8); + sdma.source(dmabuf, true, 8); + sdma.destination(&(SPI0->D), false, 8); sdma.trigger(Trigger_SPI0_TX); sdma.attach(this, &TLC5940::dmaDone); // Enable DMA on SPI0. SimpleDMA doesn't do this for us; we have to // do it explicitly. This is just a matter of setting bit 5 (TXDMAE) - // in the SPI controllers Control Register 2 (C2). + // in the SPI controller's Control Register 2 (C2). SPI0->C2 |= 0x20; // set bit 5 = 0x20 = TXDMAE in SPI0 control register 2 // Configure the GSCLK output's frequency @@ -257,7 +252,7 @@ // 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); + resetTimer.attach(this, &TLC5940::reset, (1.0/GSCLK_SPEED)*4096.0); } ~TLC5940() @@ -306,7 +301,7 @@ // Timeout to end each PWM cycle. This is a one-shot timer that we reset // on each cycle. - Timeout reset_timer; + Timeout resetTimer; // Has new GS/DC data been loaded? volatile bool newGSData; @@ -336,15 +331,21 @@ // update on every cycle, we make the brightness reduction // uniform across time, which makes it less perceptible. update(); + sdma.start(nchips*24); + #else // DATA_UPDATE_INSIDE_BLANKING // end the blanking interval endBlank(); - // if we have pending grayscale data, start sending it + // if we have pending grayscale data, update the DMA data if (newGSData) update(); + + // send out the DMA contents + sdma.start(nchips*24); + #endif // DATA_UPDATE_INSIDE_BLANKING } @@ -373,7 +374,7 @@ gsclk.write(.5); // set up the next blanking interrupt - reset_timer.attach(this, &TLC5940::reset, (1.0/GSCLK_SPEED)*4096.0); + resetTimer.attach(this, &TLC5940::reset, (1.0/GSCLK_SPEED)*4096.0); } void update() @@ -413,9 +414,6 @@ dmabuf[dst++] = (gs[i] & 0x0FF); } - // Start the DMA transfer - sdma.start(nchips*24); - // we've now cleared the new GS data newGSData = false; }
--- a/USBDevice.lib Thu Dec 24 01:37:40 2015 +0000 +++ b/USBDevice.lib Tue Jan 05 05:23:07 2016 +0000 @@ -1,1 +1,1 @@ -http://mbed.org/users/mjr/code/USBDevice/#884405d998bb +http://mbed.org/users/mjr/code/USBDevice/#20bb47609697
--- a/USBJoystick/USBJoystick.cpp Thu Dec 24 01:37:40 2015 +0000 +++ b/USBJoystick/USBJoystick.cpp Tue Jan 05 05:23:07 2016 +0000 @@ -585,7 +585,34 @@ return true; } -// Handle messages on endpoint 4 - this is the keyboard interface. +// Handle incoming messages on the joystick/LedWiz interface = endpoint 1. +// This interface receives LedWiz protocol commands and commands using our +// custom LedWiz protocol extensions. +// +// We simply queue the messages in our circular buffer for processing in +// the main loop. The circular buffer object is designed for safe access +// from the interrupt handler using the rule that only the interrupt +// handler can change the write pointer, and only the regular code can +// change the read pointer. +bool USBJoystick::EP1_OUT_callback() +{ + // Read this message + union { + LedWizMsg msg; + uint8_t buf[MAX_HID_REPORT_SIZE]; + } buf; + uint32_t bytesRead = 0; + USBDevice::readEP(EP1OUT, buf.buf, &bytesRead, MAX_HID_REPORT_SIZE); + + // if it's the right length, queue it to our circular buffer + if (bytesRead == 8) + lwbuf.write(buf.msg); + + // start the next read + return readStart(EP1OUT, 9); +} + +// Handle incoming messages on the keyboard interface = endpoint 4. // The host uses this to send updates for the keyboard indicator LEDs // (caps lock, num lock, etc). We don't do anything with these, but // we have to read them to keep the pipe open.
--- a/USBJoystick/USBJoystick.h Thu Dec 24 01:37:40 2015 +0000 +++ b/USBJoystick/USBJoystick.h Tue Jan 05 05:23:07 2016 +0000 @@ -8,6 +8,58 @@ #include "USBHID.h" +struct LedWizMsg +{ + uint8_t data[8]; +}; + +// circular buffer for incoming reports +template<class T, int cnt> class CircBuf +{ +public: + CircBuf() + { + iRead = iWrite = 0; + } + + // Read an item from the buffer. Returns true if an item was available, + // false if the buffer was empty. + bool read(T &result) + { + if (iRead != iWrite) + { + memcpy(&result, &buf[iRead], sizeof(T)); + iRead = advance(iRead); + return true; + } + else + return false; + } + + bool write(const T &item) + { + int nxt = advance(iWrite); + if (nxt != iRead) + { + memcpy(&buf[nxt], &item, sizeof(T)); + iWrite = nxt; + return true; + } + else + return false; + } + +private: + int advance(int i) + { + return i + 1 >= cnt ? 0 : i + 1; + } + + int iRead; + int iWrite; + T buf[cnt]; +}; + // keyboard interface report IDs const uint8_t REPORT_ID_KB = 1; const uint8_t REPORT_ID_MEDIA = 2; @@ -99,8 +151,15 @@ _init(); this->useKB = useKB; this->enableJoystick = enableJoystick; + reqTimer.start(); connect(waitForConnect); }; + + /* read a report from the LedWiz buffer */ + bool readLedWizMsg(LedWizMsg &msg) + { + return lwbuf.read(msg); + } /** * Send a keyboard report. The argument gives the key state, in the standard @@ -196,9 +255,12 @@ virtual bool USBCallback_setConfiguration(uint8_t configuration); virtual bool USBCallback_setInterface(uint16_t interface, uint8_t alternate) { return interface == 0 || interface == 1; } + + virtual bool EP1_OUT_callback(); virtual bool EP4_OUT_callback(); - + private: + Timer reqTimer; bool enableJoystick; bool useKB; int16_t _x; @@ -207,8 +269,11 @@ uint16_t _buttonsLo; uint16_t _buttonsHi; uint16_t _status; + + // Incoming LedWiz message buffer. Each LedWiz message is exactly 8 bytes. + CircBuf<LedWizMsg, 64> lwbuf; void _init(); }; -#endif \ No newline at end of file +#endif
--- a/USBProtocol.h Thu Dec 24 01:37:40 2015 +0000 +++ b/USBProtocol.h Tue Jan 05 05:23:07 2016 +0000 @@ -341,7 +341,14 @@ // 2 = regular keyboard key -> byte 6 is the USB key code (see below) // 3 = keyboard modifier key -> byte 6 is the USB modifier code (see below) // 4 = media control key -> byte 6 is the USB key code (see below) +// 5 = special button -> byte 6 is the special button code (see below) // byte 6 = key code, which depends on the key type in byte 5 +// byte 7 = flags - a combination of these bit values: +// 0x01 = pulse mode. This reports a physical on/off switch's state +// to the host as a brief key press whenever the switch changes +// state. This is useful for the VPinMAME Coin Door button, +// which requires the End key to be pressed each time the +// door changes state. // // 13 -> LedWiz output port setup. This sets up one output port; it can be repeated // for each port to be configured. There are 203 possible slots for output ports, @@ -378,7 +385,33 @@ // (byte 5) is ignored for this port type. // byte 5 = physical output ID, interpreted according to the value in byte 4 // byte 6 = flags: a combination of these bit values: -// 1 = active-high output (0V on output turns attached device ON) +// 0x01 = active-high output (0V on output turns attached device ON) +// 0x02 = noisemaker device: disable this output when "night mode" is engaged +// +// Note that the on-board LED segments can be used as LedWiz output ports. This +// is useful for testing a new installation with DOF or other PC software without +// having to connect any external devices. Assigning the on-board LED segments to +// output ports overrides their normal status/diagnostic display use, so the normal +// status flash pattern won't appear when they're used this way. +// +// Special port numbers: if the LedWiz port number is one of these special values, +// the physical output is used for a special purpose. These ports aren't visible +// to the PC as LedWiz ports; they're for internal use by the controller. The +// special port numbers are: +// +// 254 = Night Mode indicator lamp. This port is turned on when night mode +// is engaged, and turned off when night mode is disengaged. This can +// be used, for example, to control an indicator LED inside a lighted +// momentary pushbutton switch used to activate night mode. The light +// provides visual feedback that the mode is turned on. +// +// +// 14 -> Engage/disengage Night Mode. When night mode is engaged, LedWiz outputs marked +// as "noisemaker" devices are disabled. Byte 3 is 1 to engage night mode, 0 to +// cancel night mode. Note that sending this command will override the current +// switch setting, if a toggle switch is configured to control Night Mode. Toggling +// the switch will take control via the switch again. + // --- PIN NUMBER MAPPINGS --- @@ -407,44 +440,45 @@ // 14 = PTB9 // 15 = PTB10 // 16 = PTB11 -// 17 = PTC0 -// 18 = PTC1 -// 19 = PTC2 -// 20 = PTC3 -// 21 = PTC4 -// 22 = PTC5 -// 23 = PTC6 -// 24 = PTC7 -// 25 = PTC8 -// 26 = PTC9 -// 27 = PTC10 -// 28 = PTC11 -// 29 = PTC12 -// 30 = PTC13 -// 31 = PTC16 -// 32 = PTC17 -// 33 = PTD0 -// 34 = PTD1 -// 35 = PTD2 -// 36 = PTD3 -// 37 = PTD4 -// 38 = PTD5 -// 39 = PTD6 -// 40 = PTD7 -// 41 = PTE0 -// 42 = PTE1 -// 43 = PTE2 -// 44 = PTE3 -// 45 = PTE4 -// 46 = PTE5 -// 47 = PTE20 -// 48 = PTE21 -// 49 = PTE22 -// 50 = PTE23 -// 51 = PTE29 -// 52 = PTE30 -// 53 = PTE31 - +// 17 = PTB18 (on-board LED Red segment - not exposed as a header pin) +// 18 = PTB19 (on-board LED Green segment - not exposed as a header pin) +// 19 = PTC0 +// 20 = PTC1 +// 21 = PTC2 +// 22 = PTC3 +// 23 = PTC4 +// 24 = PTC5 +// 25 = PTC6 +// 26 = PTC7 +// 27 = PTC8 +// 28 = PTC9 +// 29 = PTC10 +// 30 = PTC11 +// 31 = PTC12 +// 32 = PTC13 +// 33 = PTC16 +// 34 = PTC17 +// 35 = PTD0 +// 36 = PTD1 (on-board LED Blue segment) +// 37 = PTD2 +// 38 = PTD3 +// 39 = PTD4 +// 40 = PTD5 +// 41 = PTD6 +// 42 = PTD7 +// 43 = PTE0 +// 44 = PTE1 +// 45 = PTE2 +// 46 = PTE3 +// 47 = PTE4 +// 48 = PTE5 +// 49 = PTE20 +// 50 = PTE21 +// 51 = PTE22 +// 52 = PTE23 +// 53 = PTE29 +// 54 = PTE30 +// 55 = PTE31 // --- USB KEYBOARD SCAN CODES --- // @@ -517,3 +551,19 @@ // 0x02 = Volume Down // 0x04 = Mute on/off + +// --- SPECIAL BUTTON KEY CODES --- +// +// Use these for special keys in the button mappings +// +// 0x01 = Night mode switch, momentary switch mode. Pushing this button +// engages night mode, disabling all LedWiz outputs marked with the +// "noisemaker" flag. Other outputs are unaffected. Pushing +// the button again disengages night mode. Use this option if the +// physical button attached to the input is a momentary switch type. +// +// 0x02 = Night mode switch, toggle switch mode. When this switch is on, +// night mode is engaged; when the switch is off, night mode is +// disengaged. Use this option if the physical switch attached to +// to the input is a toggle switch (not a momentary switch). +
--- a/config.h Thu Dec 24 01:37:40 2015 +0000 +++ b/config.h Tue Jan 05 05:23:07 2016 +0000 @@ -48,6 +48,11 @@ const int BtnTypeKey = 2; // regular keyboard key const int BtnTypeModKey = 3; // keyboard modifier key (shift, ctrl, etc) const int BtnTypeMedia = 4; // media control key (volume up/down, etc) +const int BtnTypeSpecial = 5; // special button (night mode switch, etc) + +// input button flags +const uint8_t BtnFlagPulse = 0x01; // pulse mode - reports each change in the physical switch state + // as a brief press of the logical button/keyboard key // maximum number of input button mappings const int MAX_BUTTONS = 32; @@ -62,11 +67,24 @@ const int PortTypeVirtual = 5; // Virtual port - visible to host software, but not connected to a physical output // LedWiz output port flag bits -const uint8_t PortFlagActiveLow = 0x01; // physical output is active-low +const uint8_t PortFlagActiveLow = 0x01; // physical output is active-low +const uint8_t PortFlagNoisemaker = 0x02; // noisemaker device - disable when night mode is engaged // maximum number of output ports const int MAX_OUT_PORTS = 203; +// port configuration data +struct LedWizPortCfg +{ + uint8_t typ; // port type: a PortTypeXxx value + uint8_t pin; // physical output pin: for a GPIO port, this is an index in the + // USB-to-PinName mapping list; for a TLC5940 or 74HC595 port, it's + // the output number, starting from 0 for OUT0 on the first chip in + // the daisy chain. For inactive and virtual ports, it's unused. + uint8_t flags; // flags: a combination of PortFlagXxx values +} __attribute__((packed)); + + struct Config { // set all values to factory defaults @@ -82,7 +100,7 @@ // be changed from the config tool, but for the sake of convenience we want the // default to be a value that most people won't have to change. usbVendorID = 0xFAFA; // LedWiz vendor code - usbProductID = 0x00F7; // LedWiz product code for unit #8 + usbProductID = 0x00F0; // LedWiz product code for unit #1 psUnitNo = 8; // enable joystick reports @@ -107,39 +125,127 @@ plunger.zbLaunchBall.btn = 0; // assume no TV ON switch +#if 1 + TVON.statusPin = PTD2; + TVON.latchPin = PTE0; + TVON.relayPin = PTD3; + TVON.delayTime = 7; +#else TVON.statusPin = NC; TVON.latchPin = NC; TVON.relayPin = NC; TVON.delayTime = 0; +#endif // assume no TLC5940 chips +#if 1 // $$$ + tlc5940.nchips = 2; +#else tlc5940.nchips = 0; +#endif + + // default TLC5940 pin assignments + tlc5940.sin = PTC6; + tlc5940.sclk = PTC5; + tlc5940.xlat = PTC10; + tlc5940.blank = PTC7; + tlc5940.gsclk = PTA1; // assume no 74HC595 chips hc595.nchips = 0; + // default 74HC595 pin assignments + hc595.sin = PTA5; + hc595.sclk = PTA4; + hc595.latch = PTA12; + hc595.ena = PTD4; + // initially configure with no LedWiz output ports outPort[0].typ = PortTypeDisabled; + for (int i = 0 ; i < sizeof(specialPort)/sizeof(specialPort[0]) ; ++i) + specialPort[i].typ = PortTypeDisabled; // initially configure with no input buttons for (int i = 0 ; i < MAX_BUTTONS ; ++i) button[i].pin = 0; // 0 == index of NC in USB-to-PinName mapping + +#if 1 + for (int i = 0 ; i < 24 ; ++i) { + static int bp[] = { + 21, // 1 = PTC2 + 12, // 2 = PTB3 + 11, // 3 = PTB2 + 10, // 4 = PTB1 + 54, // 5 = PTE30 + 30, // 6 = PTC11 + 48, // 7 = PTE5 + 47, // 8 = PTE4 + 46, // 9 = PTE3 + 45, // 10 = PTE2 + 16, // 11 = PTB11 + 15, // 12 = PTB10 + 14, // 13 = PTB9 + 13, // 14 = PTB8 + 31, // 15 = PTC12 + 32, // 16 = PTC13 + 33, // 17 = PTC16 + 34, // 18 = PTC17 + 7, // 19 = PTA16 + 8, // 20 = PTA17 + 55, // 21 = PTE31 + 41, // 22 = PTD6 + 42, // 23 = PTD7 + 44 // 24 = PTE1 + }; + button[i].pin = bp[i]; + button[i].typ = BtnTypeKey; + button[i].val = i+4; // A, B, C... + } +#endif + +#if 0 + button[23].typ = BtnTypeJoystick; + button[23].val = 5; // B + button[23].flags = 0x01; // pulse button + + button[22].typ = BtnTypeModKey; + button[22].val = 0x02; // left shift + + button[21].typ = BtnTypeMedia; + button[21].val = 0x02; // vol down + + button[20].typ = BtnTypeMedia; + button[20].val = 0x01; // vol up +#endif + +#if 1 // $$$ + { + int n = 0; + for (int i = 0 ; i < 32 ; ++i, ++n) { + outPort[n].typ = PortTypeTLC5940; + outPort[n].pin = i; + outPort[n].flags = 0; + } + outPort[n].typ = PortTypeGPIODig; + outPort[n].pin = 27; // PTC8 + outPort[n++].flags = 0; - button[0].pin = 6; // PTA13 - button[0].typ = BtnTypeKey; - button[0].val = 4; // A - button[1].pin = 38; // PTD5 - button[1].typ = BtnTypeJoystick; - button[1].val = 5; // B - button[2].pin = 37; // PTD4 - button[2].typ = BtnTypeModKey; - button[2].val = 0x02; // left shift - button[3].pin = 5; // PTA12 - button[3].typ = BtnTypeMedia; - button[3].val = 0x01; // volume up - button[4].pin = 3; // PTA4 - button[4].typ = BtnTypeMedia; - button[4].val = 0x02; // volume down + outPort[n].typ = PortTypeDisabled; + } +#endif +#if 0 + outPort[0].typ = PortTypeGPIOPWM; + outPort[0].pin = 17; // PTB18 = LED1 = Red LED + outPort[0].flags = PortFlagActiveLow; + outPort[1].typ = PortTypeGPIOPWM; + outPort[1].pin = 18; // PTB19 = LED2 = Green LED + outPort[1].flags = PortFlagActiveLow; + outPort[2].typ = PortTypeGPIOPWM; + outPort[2].pin = 36; // PTD1 = LED3 = Blue LED + outPort[2].flags = PortFlagActiveLow; + + outPort[3].typ = PortTypeDisabled; +#endif } // --- USB DEVICE CONFIGURATION --- @@ -327,20 +433,14 @@ uint8_t pin; // physical input GPIO pin - a USB-to-PinName mapping index uint8_t typ; // key type reported to PC - a BtnTypeXxx value uint8_t val; // key value reported - meaning depends on 'typ' value + uint8_t flags; // key flags - a bitwise combination of BtnFlagXxx values - } button[MAX_BUTTONS]; + } __attribute__((packed)) button[MAX_BUTTONS] __attribute((packed)); // --- LedWiz Output Port Setup --- - struct - { - uint8_t typ; // port type: a PortTypeXxx value - uint8_t pin; // physical output pin: for a GPIO port, this is an index in the - // USB-to-PinName mapping list; for a TLC5940 or 74HC595 port, it's - // the output number, starting from 0 for OUT0 on the first chip in - // the daisy chain. For inactive and virtual ports, it's unused. - uint8_t flags; // flags: a combination of PortFlagXxx values - } outPort[MAX_OUT_PORTS]; + LedWizPortCfg outPort[MAX_OUT_PORTS] __attribute__((packed)); // LedWiz & extended output ports + LedWizPortCfg specialPort[1]; // special ports (Night Mode indicator, etc) }; #endif
--- a/main.cpp Thu Dec 24 01:37:40 2015 +0000 +++ b/main.cpp Tue Jan 05 05:23:07 2016 +0000 @@ -20,40 +20,41 @@ // The Pinscape Controller // A comprehensive input/output controller for virtual pinball machines // -// This project implements an I/O controller designed for use in custom-built virtual -// pinball cabinets. It can handle nearly all of the functions involved in connecting -// pinball simulation software on a Windows PC with devices in the cabinet, including -// input devices such as buttons and sensors, and output devices that generate visual -// or mechanical feedback during play, like lights, solenoids, and shaker motors. -// You can use one, some, or all of the functions, in any combination. You can select -// options and configure the controller using a setup tool that runs on Windows. +// This project implements an I/O controller for virtual pinball cabinets. Its +// function is to connect Windows pinball software, such as Visual Pinball, with +// physical devices in the cabinet: buttons, sensors, and feedback devices that +// create visual or mechanical effects during play. +// +// The software can perform several different functions, which can be used +// individually or in any combination: // -// The main functions are: +// - Nudge sensing. This uses the KL25Z's on-board accelerometer to sense the +// motion of the cabinet when you nudge it. Visual Pinball and other pinball +// emulators on the PC have native handling for this type of input, so that +// physical nudges on the cabinet turn into simulated effects on the virtual +// ball. The KL25Z measures accelerations as analog readings and is quite +// sensitive, so the effect of a nudge on the simulation is proportional +// to the strength of the nudge. Accelerations are reported to the PC via a +// simulated joystick (using the X and Y axes); you just have to set some +// preferences in your pinball software to tell it that an accelerometer +// is attached. // -// - Nudge sensing, via the KL25Z's on-board accelerometer. Nudging the cabinet -// causes small accelerations that the accelerometer can detect; these are sent to -// Visual Pinball (or other pinball emulator software) on the PC via the joystick -// interface, using the X and Y axes. VP and most other PC pinball emulators have -// native handling for this type of nudge input, so all you have to do is set some -// preferences in VP to let it know that an accelerometer is attached. -// -// - Plunger position sensing, via a number of sensor options. To use this feature, +// - Plunger position sensing, with mulitple sensor options. To use this feature, // you need to choose a sensor and set it up, connect the sensor electrically to // the KL25Z, and configure the Pinscape software on the KL25Z to let it know how // the sensor is hooked up. The Pinscape software monitors the sensor and sends // readings to Visual Pinball via the joystick Z axis. VP and other PC software -// has native support for this type of input as well; as with the nudge setup, -// you just have to set some options in VP to activate the plunger. +// have native support for this type of input; as with the nudge setup, you just +// have to set some options in VP to activate the plunger. // // The Pinscape software supports optical sensors (the TAOS TSL1410R and TSL1412R // linear sensor arrays) as well as slide potentiometers. The specific equipment // that's supported, along with physical mounting and wiring details, can be found // in the Build Guide. // -// Note that while VP has its own built-in support for plunger devices like this -// one, many existing VP tables will ignore it, because they use custom scripting -// that's only designed for keyboard plunger input. The Build Guide has advice on -// adjusting tables to add plunger support when necessary. +// Note VP has built-in support for plunger devices like this one, but some VP +// tables can't use it without some additional scripting work. The Build Guide has +// advice on adjusting tables to add plunger support when necessary. // // For best results, the plunger sensor should be calibrated. The calibration // is stored in non-volatile memory on board the KL25Z, so it's only necessary @@ -75,14 +76,11 @@ // position to the fully retracted position only.) // // - Button input wiring. 24 of the KL25Z's GPIO ports are mapped as digital inputs -// for buttons and switches. The software reports these as joystick buttons when -// it sends reports to the PC. These can be used to wire physical pinball-style -// buttons in the cabinet (e.g., flipper buttons, the Start button) and miscellaneous -// switches (such as a tilt bob) to the PC. Visual Pinball can use joystick buttons -// for input - you just have to assign a VP function to each button using VP's -// keyboard options dialog. To wire a button physically, connect one terminal of -// the button switch to the KL25Z ground, and connect the other terminal to the -// the GPIO port you wish to assign to the button. +// for buttons and switches. You can wire each input to a physical pinball-style +// button or switch, such as flipper buttons, Start buttons, coin chute switches, +// tilt bobs, and service buttons. Each button can be configured to be reported +// to the PC as a joystick button or as a keyboard key (you can select which key +// is used for each button). // // - LedWiz emulation. The KL25Z can appear to the PC as an LedWiz device, and will // accept and process LedWiz commands from the host. The software can turn digital @@ -134,6 +132,20 @@ // 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. // +// - Night Mode control for output devices. You can connect a switch or button +// to the controller to activate "Night Mode", which disables feedback devices +// that you designate as noisy. You can designate outputs individually as being +// included in this set or not. This is useful if you want to play a game on +// your cabinet late at night without waking the kids and annoying the neighbors. +// +// - TV ON switch. The controller can pulse a relay to turn on your TVs after +// power to the cabinet comes on, with a configurable delay timer. This feature +// is for TVs that don't turn themselves on automatically when first plugged in. +// To use this feature, you have to build some external circuitry to allow the +// software to sense the power supply status, and you have to run wires to your +// TV's on/off button, which requires opening the case on your TV. The Build +// Guide has details on the necessary circuitry and connections to the TV. +// // // // STATUS LIGHTS: The on-board LED on the KL25Z flashes to indicate the current @@ -146,16 +158,20 @@ // // short red flash = the host computer is in sleep/suspend mode // +// long red/yellow = USB connection problem. The device still has a USB +// connection to the host, but data transmissions are failing. This +// condition shouldn't ever occur; if it does, it probably indicates +// a bug in the device's USB software. This display is provided to +// flag any occurrences for investigation. You'll probably need to +// manually reset the device if this occurs. +// // long yellow/green = everything's working, but the plunger hasn't -// been calibrated; follow the calibration procedure described above. -// This flash mode won't appear if the CCD has been disabled. Note -// that the device can't tell whether a CCD is physically attached; -// if you don't have a CCD attached, you can set the appropriate option -// in config.h or use the Windows config tool to disable the CCD -// software features. +// been calibrated. Follow the calibration procedure described in +// the project documentation. This flash mode won't appear if there's +// no plunger sensor configured. // -// alternating blue/green = everything's working, and the plunger has -// been calibrated +// alternating blue/green = everything's working normally, and plunger +// calibration has been completed (or there's no plunger attached) // // // USB PROTOCOL: please refer to USBProtocol.h for details on the USB @@ -182,6 +198,13 @@ // --------------------------------------------------------------------------- +// +// Forward declarations +// +void setNightMode(bool on); +void toggleNightMode(); + +// --------------------------------------------------------------------------- // utilities // number of elements in an array @@ -209,20 +232,6 @@ // --------------------------------------------------------------------------- // -// 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); - - -// --------------------------------------------------------------------------- -// // Wire protocol value translations. These translate byte values from // the USB protocol to local native format. // @@ -251,12 +260,12 @@ inline PinName wirePinName(int c) { static const PinName p[] = { - NC, PTA1, PTA2, PTA4, PTA5, PTA12, PTA13, PTA16, PTA17, PTB0, // 0-9 - PTB1, PTB2, PTB3, PTB8, PTB9, PTB10, PTB11, PTC0, PTC1, PTC2, // 10-19 - PTC3, PTC4, PTC5, PTC6, PTC7, PTC8, PTC9, PTC10, PTC11, PTC12, // 20-29 - PTC13, PTC16, PTC17, PTD0, PTD1, PTD2, PTD3, PTD4, PTD5, PTD6, // 30-39 - PTD7, PTE0, PTE1, PTE2, PTE3, PTE4, PTE5, PTE20, PTE21, PTE22, // 40-49 - PTE23, PTE29, PTE30, PTE31 // 50-53 + NC, PTA1, PTA2, PTA4, PTA5, PTA12, PTA13, PTA16, PTA17, PTB0, // 0-9 + PTB1, PTB2, PTB3, PTB8, PTB9, PTB10, PTB11, PTB18, PTB19, PTC0, // 10-19 + PTC1, PTC2, PTC3, PTC4, PTC5, PTC6, PTC7, PTC8, PTC9, PTC10, // 20-29 + PTC11, PTC12, PTC13, PTC16, PTC17, PTD0, PTD1, PTD2, PTD3, PTD4, // 30-39 + PTD5, PTD6, PTD7, PTE0, PTE1, PTE2, PTE3, PTE4, PTE5, PTE20, // 40-49 + PTE21, PTE22, PTE23, PTE29, PTE30, PTE31 // 50-55 }; return (c < countof(p) ? p[c] : NC); } @@ -264,6 +273,81 @@ // --------------------------------------------------------------------------- // +// 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, *ledG, *ledB; + +// Show the indicated pattern on the diagnostic LEDs. 0 is off, 1 is +// on, and -1 is no change (leaves the current setting intact). +void diagLED(int r, int g, int b) +{ + if (ledR != 0 && r != -1) ledR->write(!r); + if (ledG != 0 && g != -1) ledG->write(!g); + if (ledB != 0 && b != -1) ledB->write(!b); +} + +// check an output port assignment to see if it conflicts with +// an on-board LED segment +struct LedSeg +{ + bool r, g, b; + LedSeg() { r = g = b = false; } + + void check(LedWizPortCfg &pc) + { + // if it's a GPIO, check to see if it's assigned to one of + // our on-board LED segments + int t = pc.typ; + if (t == PortTypeGPIOPWM || t == PortTypeGPIODig) + { + // it's a GPIO port - check for a matching pin assignment + PinName pin = wirePinName(pc.pin); + if (pin == LED1) + r = true; + else if (pin == LED2) + g = true; + else if (pin == LED3) + b = true; + } + } +}; + +// Initialize the diagnostic LEDs. By default, we use the on-board +// RGB LED to display the microcontroller status. However, we allow +// the user to commandeer the on-board LED as an LedWiz output device, +// which can be useful for testing a new installation. So we'll check +// for LedWiz outputs assigned to the on-board LED segments, and turn +// off the diagnostic use for any so assigned. +void initDiagLEDs(Config &cfg) +{ + // run through the configuration list and cross off any of the + // LED segments assigned to LedWiz ports + LedSeg l; + for (int i = 0 ; i < MAX_OUT_PORTS && cfg.outPort[i].typ != PortTypeDisabled ; ++i) + l.check(cfg.outPort[i]); + + // check the special ports + for (int i = 0 ; i < countof(cfg.specialPort) ; ++i) + l.check(cfg.specialPort[i]); + + // We now know which segments are taken for LedWiz use and which + // are free. Create diagnostic ports for the ones not claimed for + // LedWiz use. + if (!l.r) ledR = new DigitalOut(LED1, 1); + if (!l.g) ledG = new DigitalOut(LED2, 1); + if (!l.b) ledB = new DigitalOut(LED3, 1); +} + + +// --------------------------------------------------------------------------- +// // LedWiz emulation, and enhanced TLC5940 output controller // // There are two modes for this feature. The default mode uses the on-board @@ -354,7 +438,7 @@ virtual void set(float val) { if (val != prv) - tlc5940->set(idx, (int)((prv = val) * 4095)); + tlc5940->set(idx, (int)((prv = val) * 4095.0f)); } int idx; float prv; @@ -443,6 +527,13 @@ static int numOutputs; static LwOut **lwPin; +// Special output ports: +// +// [0] = Night Mode indicator light +// +static LwOut *specialPin[1]; + + // Number of LedWiz emulation outputs. This is the number of ports // accessible through the standard (non-extended) LedWiz protocol // messages. The protocol has a fixed set of 32 outputs, but we @@ -451,13 +542,79 @@ static int numLwOutputs; // 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. +// value from 0.0 for fully off to 1.0 for fully on. This is used +// for all extended ports (33 and above), and for any LedWiz port +// with wizVal == 255. static float *outLevel; +// Day/night mode override for an output. For each output, this is +// set to 1 if the output is enabled and 0 if the output is disabled +// by a global mode control, such as Night Mode (currently Night Mode +// is the only such global mode, but the idea could be extended to +// other similar controls if other needs emerge). To get the final +// output level for each output, we simply multiply the outLevel value +// for the port by this override vlaue. +static uint8_t *modeLevel; + +// create a single output pin +LwOut *createLwPin(LedWizPortCfg &pc, Config &cfg) +{ + // get this item's values + int typ = pc.typ; + int pin = pc.pin; + int flags = pc.flags; + int activeLow = flags & PortFlagActiveLow; + + // create the pin interface object according to the port type + LwOut *lwp; + switch (typ) + { + case PortTypeGPIOPWM: + // PWM GPIO port + lwp = new LwPwmOut(wirePinName(pin)); + break; + + case PortTypeGPIODig: + // Digital GPIO port + lwp = new LwDigOut(wirePinName(pin)); + break; + + case PortTypeTLC5940: + // TLC5940 port (if we don't have a TLC controller object, or it's not a valid + // output port number on the chips we have, create a virtual port) + if (tlc5940 != 0 && pin < cfg.tlc5940.nchips*16) + lwp = new Lw5940Out(pin); + else + lwp = new LwVirtualOut(); + break; + + case PortType74HC595: + // 74HC595 port (if we don't have an HC595 controller object, or it's not a valid + // output number, create a virtual port) + if (hc595 != 0 && pin < cfg.hc595.nchips*8) + lwp = new Lw595Out(pin); + else + lwp = new LwVirtualOut(); + break; + + case PortTypeVirtual: + default: + // virtual or unknown + lwp = new LwVirtualOut(); + break; + } + + // if it's Active Low, layer on an inverter + if (activeLow) + lwp = new LwInvertedOut(lwp); + + // turn it off initially + lwp->set(0); + + // return the pin + return lwp; +} + // initialize the output pin array void initLwOut(Config &cfg) { @@ -481,55 +638,26 @@ // allocate the pin array lwPin = new LwOut*[numOutputs]; - // Allocate the current brightness array. - outLevel = new float[numOutputs < 32 ? 32 : numOutputs]; + // Allocate the current brightness array. For these, allocate at + // least 32, so that we have enough for all LedWiz messages, but + // allocate the full set of actual ports if we have more than the + // LedWiz complement. + int minOuts = numOutputs < 32 ? 32 : numOutputs; + outLevel = new float[minOuts]; + + // Allocate the mode override array + modeLevel = new uint8_t[minOuts]; + + // start with all modeLevel values set to ON + memset(modeLevel, 1, minOuts); // create the pin interface object for each port for (i = 0 ; i < numOutputs ; ++i) - { - // get this item's values - int typ = cfg.outPort[i].typ; - int pin = cfg.outPort[i].pin; - int flags = cfg.outPort[i].flags; - int activeLow = flags & PortFlagActiveLow; - - // create the pin interface object according to the port type - switch (typ) - { - case PortTypeGPIOPWM: - // PWM GPIO port - lwPin[i] = new LwPwmOut(wirePinName(pin)); - break; - - case PortTypeGPIODig: - // Digital GPIO port - lwPin[i] = new LwDigOut(wirePinName(pin)); - break; + lwPin[i] = createLwPin(cfg.outPort[i], cfg); - case PortTypeTLC5940: - // TLC5940 port - lwPin[i] = new Lw5940Out(pin); - break; - - case PortType74HC595: - // 74HC595 port - lwPin[i] = new Lw595Out(pin); - break; - - case PortTypeVirtual: - default: - // virtual or unknown - lwPin[i] = new LwVirtualOut(); - break; - } - - // if it's Active Low, layer an inverter - if (activeLow) - lwPin[i] = new LwInvertedOut(lwPin[i]); - - // turn it off initially - lwPin[i]->set(0); - } + // create the pin interface for each special port + for (i = 0 ; i < countof(cfg.specialPort) ; ++i) + specialPin[i] = createLwPin(cfg.specialPort[i], cfg); } // LedWiz output states. @@ -612,7 +740,7 @@ // makes us work properly with software that's expecting the // documented LedWiz behavior and therefore uses level 48 to // turn a contactor or relay fully on. - return val/48.0; + return val/48.0f; } else if (val == 49) { @@ -623,29 +751,29 @@ // the PC side (notably DOF) is aware of this and uses level 49 // to mean "100% on". To ensure compatibility with existing // PC-side software, we need to recognize level 49. - return 1.0; + return 1.0f; } else if (val == 129) { // 129 = ramp up / ramp down return wizFlashCounter < 128 - ? wizFlashCounter/128.0 - : (256 - wizFlashCounter)/128.0; + ? wizFlashCounter/128.0f + : (256 - wizFlashCounter)/128.0f; } else if (val == 130) { // 130 = flash on / off - return wizFlashCounter < 128 ? 1.0 : 0.0; + return wizFlashCounter < 128 ? 1.0f : 0.0f; } else if (val == 131) { // 131 = on / ramp down - return wizFlashCounter < 128 ? 1.0 : (255 - wizFlashCounter)/128.0; + return wizFlashCounter < 128 ? 1.0f : (255 - wizFlashCounter)/128.0f; } else if (val == 132) { // 132 = ramp up / on - return wizFlashCounter < 128 ? wizFlashCounter/128.0 : 1.0; + return wizFlashCounter < 128 ? wizFlashCounter/128.0f : 1.0f; } else { @@ -654,7 +782,7 @@ // LedWiz unit exhibits in response is accidental and could change // in a future version. We'll treat all undefined values as equivalent // to 48 (fully on). - return 1.0; + return 1.0f; } } @@ -668,7 +796,7 @@ // larger steps through the cycle on each interrupt. Running // every 1/127 of a second = 8ms seems to be a pretty light load. Timeout wizPulseTimer; -#define WIZ_PULSE_TIME_BASE (1.0/127.0) +#define WIZ_PULSE_TIME_BASE (1.0f/127.0f) static void wizPulse() { // increase the counter by the speed increment, and wrap at 256 @@ -684,7 +812,7 @@ uint8_t s = wizVal[i]; if (s >= 129 && s <= 132) { - lwPin[i]->set(wizState(i)); + lwPin[i]->set(wizState(i) * modeLevel[i]); ena = true; } } @@ -709,7 +837,7 @@ for (int i = 0 ; i < numLwOutputs ; ++i) { pulse |= (wizVal[i] >= 129 && wizVal[i] <= 132); - lwPin[i]->set(wizState(i)); + lwPin[i]->set(wizState(i) * modeLevel[i]); } // if any outputs are set to flashing mode, and the pulse timer @@ -721,7 +849,24 @@ if (hc595 != 0) hc595->update(); } + +// Update all physical outputs. This is called after a change to a global +// setting that affects all outputs, such as engaging or canceling Night Mode. +static void updateAllOuts() +{ + // uddate each LedWiz output + for (int i = 0 ; i < numLwOutputs ; ++i) + lwPin[i]->set(wizState(i) * modeLevel[i]); + // update each extended output + for (int i = 33 ; i < numOutputs ; ++i) + lwPin[i]->set(outLevel[i] * modeLevel[i]); + + // flush 74HC595 changes, if necessary + if (hc595 != 0) + hc595->update(); +} + // --------------------------------------------------------------------------- // // Button input @@ -730,19 +875,39 @@ // button state struct ButtonState { - ButtonState() : di(NULL), pressed(0), t(0), js(0), keymod(0), keycode(0) { } + ButtonState() + { + di = NULL; + on = 0; + pressed = prev = 0; + dbstate = 0; + js = 0; + keymod = 0; + keycode = 0; + special = 0; + pulseState = 0; + pulseTime = 0.0f; + } // DigitalIn for the button DigitalIn *di; - - // current on/off state - int pressed; + + // current PHYSICAL on/off state, after debouncing + uint8_t on; - // Sticky time remaining for current state. When a - // state transition occurs, we set this to a debounce - // period. Future state transitions will be ignored - // until the debounce time elapses. - float t; + // current LOGICAL on/off state as reported to the host. + uint8_t pressed; + + // previous logical on/off state, when keys were last processed for USB + // reports and local effects + uint8_t prev; + + // Debounce history. On each scan, we shift in a 1 bit to the lsb if + // the physical key is reporting ON, and shift in a 0 bit if the physical + // key is reporting OFF. We consider the key to have a new stable state + // if we have N consecutive 0's or 1's in the low N bits (where N is + // a parameter that determines how long we wait for transients to settle). + uint8_t dbstate; // joystick button mask for the button, if mapped as a joystick button uint32_t js; @@ -754,11 +919,103 @@ // media control key code uint8_t mediakey; - + // special key code + uint8_t special; + + // Pulse mode: a button in pulse mode transmits a brief logical button press and + // release each time the attached physical switch changes state. This is useful + // for cases where the host expects a key press for each change in the state of + // the physical switch. The canonical example is the Coin Door switch in VPinMAME, + // which requires pressing the END key to toggle the open/closed state. This + // software design isn't easily implemented in a physical coin door, though - + // the easiest way to sense a physical coin door's state is with a simple on/off + // switch. Pulse mode bridges that divide by converting a physical switch state + // to on/off toggle key reports to the host. + // + // Pulse state: + // 0 -> not a pulse switch - logical key state equals physical switch state + // 1 -> off + // 2 -> transitioning off-on + // 3 -> on + // 4 -> transitioning on-off + // + // Each state change sticks for a minimum period; when the timer expires, + // if the underlying physical switch is in a different state, we switch + // to the next state and restart the timer. pulseTime is the amount of + // time remaining before we can make another state transition. The state + // transitions require a complete cycle, 1 -> 2 -> 3 -> 4 -> 1...; this + // guarantees that the parity of the pulse count always matches the + // current physical switch state when the latter is stable, which makes + // it impossible to "trick" the host by rapidly toggling the switch state. + // (On my original Pinscape cabinet, I had a hardware pulse generator + // for coin door, and that *was* possible to trick by rapid toggling. + // This software system can't be fooled that way.) + uint8_t pulseState; + float pulseTime; + } buttonState[MAX_BUTTONS]; -// timer for button reports -static Timer buttonTimer; + +// Button data +uint32_t jsButtons = 0; + +// Keyboard report state. This tracks the USB keyboard state. We can +// report at most 6 simultaneous non-modifier keys here, plus the 8 +// modifier keys. +struct +{ + bool changed; // flag: changed since last report sent + int nkeys; // number of active keys in the list + uint8_t data[8]; // key state, in USB report format: byte 0 is the modifier key mask, + // byte 1 is reserved, and bytes 2-7 are the currently pressed key codes +} kbState = { false, 0, { 0, 0, 0, 0, 0, 0, 0, 0 } }; + +// Media key state +struct +{ + bool changed; // flag: changed since last report sent + uint8_t data; // key state byte for USB reports +} mediaState = { false, 0 }; + +// button scan interrupt ticker +Ticker buttonTicker; + +// Button scan interrupt handler. We call this periodically via +// a timer interrupt to scan the physical button states. +void scanButtons() +{ + // scan all button input pins + ButtonState *bs = buttonState; + for (int i = 0 ; i < MAX_BUTTONS ; ++i, ++bs) + { + // if it's connected, check its physical state + if (bs->di != NULL) + { + // Shift the new state into the debounce history. Note that + // the physical pin inputs are active low (0V/GND = ON), so invert + // the reading by XOR'ing the low bit with 1. And of course we + // only want the low bit (since the history is effectively a bit + // vector), so mask the whole thing with 0x01 as well. + uint8_t db = bs->dbstate; + db <<= 1; + db |= (bs->di->read() & 0x01) ^ 0x01; + bs->dbstate = db; + + // if we have all 0's or 1's in the history for the required + // debounce period, the key state is stable - check for a change + // to the last stable state + const uint8_t stable = 0x1F; // 00011111b -> 5 stable readings + db &= stable; + if (db == 0 || db == stable) + bs->on = db; + } + } +} + +// Button state transition timer. This is used for pulse buttons, to +// control the timing of the logical key presses generated by transitions +// in the physical button state. +Timer buttonTimer; // initialize the button inputs void initButtons(Config &cfg, bool &kbKeys) @@ -776,6 +1033,10 @@ // set up the GPIO input pin for this button bs->di = new DigitalIn(pin); + // if it's a pulse mode button, set the initial pulse state to Off + if (cfg.button[i].flags & BtnFlagPulse) + bs->pulseState = 1; + // note if it's a keyboard key of some kind (including media keys) uint8_t val = cfg.button[i].val; switch (cfg.button[i].typ) @@ -806,37 +1067,19 @@ } } - // start the button timer - buttonTimer.reset(); + // start the button scan thread + buttonTicker.attach_us(scanButtons, 1000); + + // start the button state transition timer buttonTimer.start(); } -// Button data -uint32_t jsButtons = 0; - -// Keyboard state -struct +// Process the button state. This sets up the joystick, keyboard, and +// media control descriptors with the current state of keys mapped to +// those HID interfaces, and executes the local effects for any keys +// mapped to special device functions (e.g., Night Mode). +void processButtons() { - bool changed; // flag: changed since last report sent - int nkeys; // number of active keys in the list - uint8_t data[8]; // key state, in USB report format: byte 0 is the modifier key mask, - // byte 1 is reserved, and bytes 2-7 are the currently pressed key codes -} kbState = { false, 0, { 0, 0, 0, 0, 0, 0, 0, 0 } }; - -// Media key state -struct -{ - bool changed; // flag: changed since last report sent - uint8_t data; // key state byte for USB reports -} mediaState = { false, 0 }; - -// read the button input state; returns true if there are any button -// state changes to report, false if not -bool readButtons(Config &cfg) -{ - // no changes detected yet - bool changes = false; - // start with an empty list of USB key codes uint8_t modkeys = 0; uint8_t keys[7] = { 0, 0, 0, 0, 0, 0, 0 }; @@ -847,70 +1090,112 @@ // start with no media keys pressed uint8_t mediakeys = 0; - - // figure the time elapsed since the last scan + + // calculate the time since the last run float dt = buttonTimer.read(); - - // reset the time for the next scan buttonTimer.reset(); - + // scan the button list ButtonState *bs = buttonState; for (int i = 0 ; i < MAX_BUTTONS ; ++i, ++bs) { - // read this button - if (bs->di != 0) + // if it's a pulse-mode switch, get the virtual pressed state + if (bs->pulseState != 0) { - // deduct the elapsed time since the last update - // from the button's remaining sticky time - bs->t -= dt; - if (bs->t < 0) - bs->t = 0; - - // If the sticky time has elapsed, note the new physical - // state of the button. If we still have sticky time - // remaining, ignore the physical state; the last state - // change persists until the sticky time elapses so that - // we smooth out any "bounce" (electrical transients that - // occur when the switch contact is opened or closed). - if (bs->t == 0) + // deduct the time to the next state change + bs->pulseTime -= dt; + if (bs->pulseTime < 0) + bs->pulseTime = 0; + + // if the timer has expired, check for state changes + if (bs->pulseTime == 0) { - // get the new physical state - int pressed = !bs->di->read(); - - // update the button's logical state if this is a change - if (pressed != bs->pressed) + const float pulseLength = 0.2; + switch (bs->pulseState) { - // store the new state - bs->pressed = pressed; + case 1: + // off - if the physical switch is now on, start a button pulse + if (bs->on) { + bs->pulseTime = pulseLength; + bs->pulseState = 2; + bs->pressed = 1; + } + break; - // start a new sticky period for debouncing this - // state change - bs->t = 0.075; + case 2: + // transitioning off to on - end the pulse, and start a gap + // equal to the pulse time so that the host can observe the + // change in state in the logical button + bs->pulseState = 3; + bs->pulseTime = pulseLength; + bs->pressed = 0; + break; + + case 3: + // on - if the physical switch is now off, start a button pulse + if (!bs->on) { + bs->pulseTime = pulseLength; + bs->pulseState = 4; + bs->pressed = 1; + } + break; + + case 4: + // transitioning on to off - end the pulse, and start a gap + bs->pulseState = 1; + bs->pulseTime = pulseLength; + bs->pressed = 0; + break; } } + } + else + { + // not a pulse switch - the logical state is the same as the physical state + bs->pressed = bs->on; + } - // if it's pressed, add it to the appropriate key state list - if (bs->pressed) + // carry out any edge effects from buttons changing states + if (bs->pressed != bs->prev) + { + // check for special key transitions + switch (bs->special) { - // OR in the joystick button bit, mod key bits, and media key bits - newjs |= bs->js; - modkeys |= bs->keymod; - mediakeys |= bs->mediakey; + case 1: + // night mode momentary switch - when the button transitions from + // OFF to ON, invert night mode + if (bs->pressed) + toggleNightMode(); + break; - // if it has a keyboard key, add the scan code to the active list - if (bs->keycode != 0 && nkeys < 7) - keys[nkeys++] = bs->keycode; + case 2: + // night mode toggle switch - when the button changes state, change + // night mode to match the new state + setNightMode(bs->pressed); + break; } + + // remember the new state for comparison on the next run + bs->prev = bs->pressed; + } + + // if it's pressed, add it to the appropriate key state list + if (bs->pressed) + { + // OR in the joystick button bit, mod key bits, and media key bits + newjs |= bs->js; + modkeys |= bs->keymod; + mediakeys |= bs->mediakey; + + // if it has a keyboard key, add the scan code to the active list + if (bs->keycode != 0 && nkeys < 7) + keys[nkeys++] = bs->keycode; } } // check for joystick button changes if (jsButtons != newjs) - { - changes = true; jsButtons = newjs; - } // Check for changes to the keyboard keys if (kbState.data[0] != modkeys @@ -919,7 +1204,6 @@ { // we have changes - set the change flag and store the new key data kbState.changed = true; - changes = true; kbState.data[0] = modkeys; if (nkeys <= 6) { // 6 or fewer simultaneous keys - report the key codes @@ -938,11 +1222,7 @@ { mediaState.changed = true; mediaState.data = mediakeys; - changes = true; } - - // return the change indicator - return changes; } // --------------------------------------------------------------------------- @@ -1106,7 +1386,7 @@ vx_ = vy_ = 0; // get the time since the last get() sample - float dt = tGet_.read_us()/1.0e6; + float dt = tGet_.read_us()/1.0e6f; tGet_.reset(); // done manipulating the shared data @@ -1277,7 +1557,7 @@ // void clear_i2c() { - // assume a general-purpose output pin to the I2C clock + // set up general-purpose output pins to the I2C lines DigitalOut scl(MMA8451_SCL_PIN); DigitalIn sda(MMA8451_SDA_PIN); @@ -1652,6 +1932,48 @@ // --------------------------------------------------------------------------- // +// NIGHT MODE flag. When night mode is on, we disable all outputs +// marked as "noisemakers" in the output configuration flags. +int nightMode; + +// Update the global output mode settings +static void globalOutputModeChange() +{ + // set the global modeLevel[] + for (int i = 0 ; i < numOutputs ; ++i) + { + // assume the port will be on + uint8_t f = 1; + + // if night mode is in effect, and this is a noisemaker, disable it + if (nightMode && (cfg.outPort[i].flags & PortFlagNoisemaker) != 0) + f = 0; + + // set the final output port override value + modeLevel[i] = f; + } + + // update all outputs for the mode change + updateAllOuts(); +} + +// Turn night mode on or off +static void setNightMode(bool on) +{ + nightMode = on; + globalOutputModeChange(); + specialPin[0]->set(on ? 255.0 : 0.0); +} + +// Toggle night mode +static void toggleNightMode() +{ + setNightMode(!nightMode); +} + + +// --------------------------------------------------------------------------- +// // Plunger Sensor // @@ -1883,6 +2205,7 @@ cfg.button[idx].pin = data[3]; cfg.button[idx].typ = data[4]; cfg.button[idx].val = data[5]; + cfg.button[idx].flags = data[6]; } } break; @@ -1904,8 +2227,21 @@ cfg.outPort[idx].pin = data[4]; cfg.outPort[idx].flags = data[5]; } + else if (idx == 254) + { + // special ports + idx -= 254; + cfg.specialPort[idx].typ = data[3]; + cfg.specialPort[idx].pin = data[4]; + cfg.specialPort[idx].flags = data[5]; + } } break; + + case 14: + // engage/cancel Night Mode + setNightMode(data[2]); + break; } } @@ -1914,256 +2250,261 @@ // Handle an input report from the USB host. Input reports use our extended // LedWiz protocol. // -void handleInputMsg(HID_REPORT &report, USBJoystick &js, int &z) +void handleInputMsg(uint8_t data[8], USBJoystick &js, int &z) { - // all Led-Wiz reports are exactly 8 bytes - 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-228 -> extended bank brightness set for outputs N to N+6, where + // N is (first byte - 200)*7 + // other -> reserved for future use + // + if (data[0] == 64) { - // 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-228 -> 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) + // LWZ-SBA - first four bytes are bit-packed on/off flags + // for the outputs; 5th byte is the pulse speed (1-7) + //printf("LWZ-SBA %02x %02x %02x %02x ; %02x\r\n", + // data[1], data[2], data[3], data[4], data[5]); + + // update all on/off states + for (int i = 0, bit = 1, ri = 1 ; i < numLwOutputs ; ++i, bit <<= 1) { - // LWZ-SBA - first four bytes are bit-packed on/off flags - // for the outputs; 5th byte is the pulse speed (1-7) - //printf("LWZ-SBA %02x %02x %02x %02x ; %02x\r\n", - // data[1], data[2], data[3], data[4], data[5]); - - // update all on/off states - for (int i = 0, bit = 1, ri = 1 ; i < numLwOutputs ; ++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); + // figure the on/off state bit for this output + if (bit == 0x100) { + bit = 1; + ++ri; } - // set the flash speed - enforce the value range 1-7 - wizSpeed = data[5]; - if (wizSpeed < 1) - wizSpeed = 1; - else if (wizSpeed > 7) - wizSpeed = 7; + // 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 + wizSpeed = data[5]; + if (wizSpeed < 1) + wizSpeed = 1; + else if (wizSpeed > 7) + wizSpeed = 7; + + // update the physical outputs + updateWizOuts(); + if (hc595 != 0) + hc595->update(); + + // reset the PBA counter + pbaIdx = 0; + } + else if (data[0] == 65) + { + // Private control message. This isn't an LedWiz message - it's + // an extension for this device. 65 is an invalid PBA setting, + // and isn't used for any other LedWiz message, so we appropriate + // it for our own private use. The first byte specifies the + // message type. + if (data[1] == 1) + { + // 1 = Old Set Configuration: + // data[2] = LedWiz unit number (0x00 to 0x0f) + // data[3] = feature enable bit mask: + // 0x01 = enable plunger sensor + + // get the new LedWiz unit number - this is 0-15, whereas we + // we save the *nominal* unit number 1-16 in the config + uint8_t newUnitNo = (data[2] & 0x0f) + 1; - // update the physical outputs + // we'll need a reset if the LedWiz unit number is changing + bool needReset = (newUnitNo != cfg.psUnitNo); + + // set the configuration parameters from the message + cfg.psUnitNo = newUnitNo; + cfg.plunger.enabled = data[3] & 0x01; + + // update the status flags + statusFlags = (statusFlags & ~0x01) | (data[3] & 0x01); + + // if the plunger is no longer enabled, use 0 for z reports + if (!cfg.plunger.enabled) + z = 0; + + // save the configuration + saveConfigToFlash(); + + // reboot if necessary + if (needReset) + reboot(js); + } + else if (data[1] == 2) + { + // 2 = Calibrate plunger + // (No parameters) + + // enter calibration mode + calBtnState = 3; + calBtnTimer.reset(); + cfg.plunger.cal.reset(plungerSensor->npix); + } + else if (data[1] == 3) + { + // 3 = pixel dump + // (No parameters) + reportPix = true; + + // show purple until we finish sending the report + diagLED(1, 0, 1); + } + else if (data[1] == 4) + { + // 4 = hardware configuration query + // (No parameters) + wait_ms(1); + js.reportConfig( + numOutputs, + cfg.psUnitNo - 1, // report 0-15 range for unit number (we store 1-16 internally) + cfg.plunger.cal.zero, cfg.plunger.cal.max); + } + else if (data[1] == 5) + { + // 5 = all outputs off, reset to LedWiz defaults + allOutputsOff(); + } + else if (data[1] == 6) + { + // 6 = Save configuration to flash. + saveConfigToFlash(); + + // Reboot the microcontroller. Nearly all config changes + // require a reset, and a reset only takes a few seconds, + // so we don't bother tracking whether or not a reboot is + // really needed. + reboot(js); + } + } + else if (data[0] == 66) + { + // Extended protocol - Set configuration variable. + // The second byte of the message is the ID of the variable + // to update, and the remaining bytes give the new value, + // in a variable-dependent format. + configVarMsg(data); + } + else if (data[0] >= 200 && data[0] <= 228) + { + // Extended protocol - Extended output port 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 * modeLevel[i]); + } + + // update 74HC595 outputs, if attached + if (hc595 != 0) + hc595->update(); + } + else + { + // 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 + for (int i = 0 ; i < 8 ; ++i) + wizVal[pbaIdx + i] = data[i]; + + // 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(); if (hc595 != 0) hc595->update(); - - // reset the PBA counter pbaIdx = 0; } - else if (data[0] == 65) - { - // Private control message. This isn't an LedWiz message - it's - // an extension for this device. 65 is an invalid PBA setting, - // and isn't used for any other LedWiz message, so we appropriate - // it for our own private use. The first byte specifies the - // message type. - if (data[1] == 1) - { - // 1 = Old Set Configuration: - // data[2] = LedWiz unit number (0x00 to 0x0f) - // data[3] = feature enable bit mask: - // 0x01 = enable plunger sensor + else + pbaIdx += 8; + } +} - // get the new LedWiz unit number - this is 0-15, whereas we - // we save the *nominal* unit number 1-16 in the config - uint8_t newUnitNo = (data[2] & 0x0f) + 1; - // we'll need a reset if the LedWiz unit number is changing - bool needReset = (newUnitNo != cfg.psUnitNo); - - // set the configuration parameters from the message - cfg.psUnitNo = newUnitNo; - cfg.plunger.enabled = data[3] & 0x01; - - // update the status flags - statusFlags = (statusFlags & ~0x01) | (data[3] & 0x01); - - // if the plunger is no longer enabled, use 0 for z reports - if (!cfg.plunger.enabled) - z = 0; - - // save the configuration - saveConfigToFlash(); - - // reboot if necessary - if (needReset) - reboot(js); - } - else if (data[1] == 2) - { - // 2 = Calibrate plunger - // (No parameters) - - // enter calibration mode - calBtnState = 3; - calBtnTimer.reset(); - cfg.plunger.cal.reset(plungerSensor->npix); - } - else if (data[1] == 3) - { - // 3 = pixel dump - // (No parameters) - reportPix = true; - - // show purple until we finish sending the report - ledR = 0; - ledB = 0; - ledG = 1; - } - else if (data[1] == 4) - { - // 4 = hardware configuration query - // (No parameters) - wait_ms(1); - js.reportConfig( - numOutputs, - cfg.psUnitNo - 1, // report 0-15 range for unit number (we store 1-16 internally) - cfg.plunger.cal.zero, cfg.plunger.cal.max); - } - else if (data[1] == 5) - { - // 5 = all outputs off, reset to LedWiz defaults - allOutputsOff(); - } - else if (data[1] == 6) - { - // 6 = Save configuration to flash. - saveConfigToFlash(); - - // Reboot the microcontroller. Nearly all config changes - // require a reset, and a reset only takes a few seconds, - // so we don't bother tracking whether or not a reboot is - // really needed. - reboot(js); - } - } - else if (data[0] == 66) - { - // Extended protocol - Set configuration variable. - // The second byte of the message is the ID of the variable - // to update, and the remaining bytes give the new value, - // in a variable-dependent format. - configVarMsg(data); - } - else if (data[0] >= 200 && data[0] <= 228) - { - // Extended protocol - Extended output port 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); - } - - // update 74HC595 outputs, if attached - if (hc595 != 0) - hc595->update(); - } - else - { - // 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 - for (int i = 0 ; i < 8 ; ++i) - wizVal[pbaIdx + i] = data[i]; - - // 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(); - if (hc595 != 0) - hc595->update(); - pbaIdx = 0; - } - else - pbaIdx += 8; - } - } +// --------------------------------------------------------------------------- +// +// Pre-connection diagnostic flasher +// +void preConnectFlasher() +{ + diagLED(1, 0, 0); + wait(0.05); + diagLED(0, 0, 0); } // --------------------------------------------------------------------------- @@ -2177,17 +2518,21 @@ // int main(void) { - // turn off our on-board indicator LED - ledR = 1; - ledG = 1; - ledB = 1; + printf("\r\nPinscape Controller starting\r\n"); // $$$ debug // clear the I2C bus for the accelerometer clear_i2c(); - + // load the saved configuration loadConfigFromFlash(); + // initialize the diagnostic LEDs + initDiagLEDs(cfg); + + // set up the pre-connected ticker + Ticker preConnectTicker; + preConnectTicker.attach(preConnectFlasher, 3); + // start the TV timer, if applicable startTVTimer(cfg); @@ -2204,7 +2549,11 @@ // enable the 74HC595 chips, if present init_hc595(cfg); - // initialize the LedWiz ports + // Initialize the LedWiz ports. Note that it's important to wait until + // after initializing the various off-board output port controller chip + // sybsystems (TLC5940, 74HC595), since pins attached to peripheral + // controllers will need to address their respective controller objects, + // which don't exit until we initialize those subsystems. initLwOut(cfg); // start the TLC5940 clock @@ -2214,15 +2563,26 @@ // initialize the button input ports bool kbKeys = false; initButtons(cfg, kbKeys); - + // Create the joystick USB client. Note that we use the LedWiz unit // number from the saved configuration. MyUSBJoystick js(cfg.usbVendorID, cfg.usbProductID, USB_VERSION_NO, true, cfg.joystickEnabled, kbKeys); + + // we're now connected - kill the pre-connect ticker + preConnectTicker.detach(); - // last report timer - we use this to throttle reports, since VP - // doesn't want to hear from us more than about every 10ms - Timer reportTimer; - reportTimer.start(); + // Last report timer for the joytick interface. We use the joystick timer + // to throttle the report rate, because VP doesn't benefit from reports any + // faster than about every 10ms. + Timer jsReportTimer; + jsReportTimer.start(); + + // Time since we successfully sent a USB report. This is a hacky workaround + // for sporadic problems in the USB stack that I haven't been able to figure + // out. If we go too long without successfully sending a USB report, we'll + // try resetting the connection. + Timer jsOKTimer; + jsOKTimer.start(); // set the initial status flags statusFlags = (cfg.plunger.enabled ? 0x01 : 0x00); @@ -2355,18 +2715,12 @@ // host requests for (;;) { - // Look for an incoming report. Process a few input reports in - // a row, but stop after a few so that a barrage of inputs won't - // starve our output event processing. Also, pause briefly between - // reads; allowing reads to occur back-to-back seems to occasionally - // stall the USB pipeline (for reasons unknown; I'd fix the underlying - // problem if I knew what it was). - HID_REPORT report; - for (int rr = 0 ; rr < 4 && js.readNB(&report) ; ++rr, wait_ms(1)) - { - handleInputMsg(report, js, z); - } + // Process incoming reports + LedWizMsg lwmsg; + for (int rr = 0 ; rr < 64 && js.readLedWizMsg(lwmsg) ; ++rr) + handleInputMsg(lwmsg.data, js, z); + // check for plunger calibration if (calBtn != 0 && !calBtn->read()) { @@ -2460,19 +2814,15 @@ if (calBtnLit) { if (calBtnLed != 0) calBtnLed->write(1); - ledR = 1; - ledG = 1; - ledB = 0; + diagLED(0, 0, 1); // blue } else { if (calBtnLed != 0) calBtnLed->write(0); - ledR = 1; - ledG = 1; - ledB = 1; + diagLED(0, 0, 0); // off } } - + // If the plunger is enabled, and we're not already in a firing event, // and the last plunger reading had the plunger pulled back at least // a bit, watch for plunger release events until it's time for our next @@ -2480,7 +2830,7 @@ if (!firing && cfg.plunger.enabled && z >= JOYMAX/6) { // monitor the plunger until it's time for our next report - while (reportTimer.read_ms() < 15) + while (jsReportTimer.read_ms() < 15) { // do a fast low-res scan; if it's at or past the zero point, // start a firing event @@ -2817,22 +3167,27 @@ z0 = znew; } - // update the buttons - bool buttonsChanged = readButtons(cfg); + // process button updates + processButtons(); - // send a keyboard report if we have new data to report + // send a keyboard report if we have new data if (kbState.changed) { + // send a keyboard report js.kbUpdate(kbState.data); kbState.changed = false; } - - // send the media control report, if applicable + + // likewise for the media controller if (mediaState.changed) { + // send a media report js.mediaUpdate(mediaState.data); mediaState.changed = false; } + + // flag: did we successfully send a joystick report on this round? + bool jsOK = false; // If it's been long enough since our last USB status report, // send the new report. We throttle the report rate because @@ -2840,7 +3195,7 @@ // VP only wants to sync with the real world in 10ms intervals, // so reporting more frequently creates I/O overhead without // doing anything to improve the simulation. - if (cfg.joystickEnabled && reportTimer.read_ms() > 10) + if (cfg.joystickEnabled && jsReportTimer.read_ms() > 10) { // read the accelerometer int xa, ya; @@ -2867,10 +3222,10 @@ accelRotate(x, y); // send the joystick report - js.update(x, y, zrep, jsButtons | simButtons, statusFlags); + jsOK = js.update(x, y, zrep, jsButtons | simButtons, statusFlags); // we've just started a new report interval, so reset the timer - reportTimer.reset(); + jsReportTimer.reset(); } // If we're in pixel dump mode, report all pixel exposure values @@ -2885,9 +3240,17 @@ // If joystick reports are turned off, send a generic status report // periodically for the sake of the Windows config tool. - if (!cfg.joystickEnabled && reportTimer.read_ms() > 200) + if (!cfg.joystickEnabled && jsReportTimer.read_ms() > 200) { - js.updateStatus(0); + jsOK = js.updateStatus(0); + jsReportTimer.reset(); + } + + // if we successfully sent a joystick report, reset the watchdog timer + if (jsOK) + { + jsOKTimer.reset(); + jsOKTimer.start(); } #ifdef DEBUG_PRINTF @@ -2912,45 +3275,60 @@ allOutputsOff(); } } - + // provide a visual status indication on the on-board LED if (calBtnState < 2 && hbTimer.read_ms() > 1000) { if (!newConnected) { // suspended - turn off the LED - ledR = 1; - ledG = 1; - ledB = 1; + diagLED(0, 0, 0); // show a status flash every so often if (hbcnt % 3 == 0) { - // disconnected = red/red flash; suspended = red + // disconnected = short red/red flash + // suspended = short red flash for (int n = js.isConnected() ? 1 : 2 ; n > 0 ; --n) { - ledR = 0; + diagLED(1, 0, 0); wait(0.05); - ledR = 1; + diagLED(0, 0, 0); wait(0.25); } } } + else if (jsOKTimer.read() > 5) + { + // too long without a USB report - show red/yellow + static bool dumped; + if (!dumped) { + extern void USBDeviceStatusDump(void); + USBDeviceStatusDump(); + dumped = true; + } + extern bool USB_DMAERR; + if (USB_DMAERR) { + printf("USB DMAERR DETECTED!\r\n"); + // js.disconnect(); + // js.connect(); + // USB_DMAERR = false; + } + jsOKTimer.stop(); + hb = !hb; + diagLED(1, hb, 0); + } else if (cfg.plunger.enabled && !cfg.plunger.cal.calibrated) { // connected, plunger calibration needed - flash yellow/green hb = !hb; - ledR = (hb ? 0 : 1); - ledG = 0; - ledB = 1; + diagLED(hb, 1, 0); } else { // connected - flash blue/green hb = !hb; - ledR = 1; - ledG = (hb ? 0 : 1); - ledB = (hb ? 1 : 0); + diagLED(0, hb, !hb); } // reset the heartbeat timer