Important changes to repositories hosted on mbed.com
Mbed hosted mercurial repositories are deprecated and are due to be permanently deleted in July 2026.
To keep a copy of this software download the repository Zip archive or clone locally using Mercurial.
It is also possible to export all your personal repositories from the account settings page.
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
