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: mbed FastIO FastPWM USBDevice
Revision 87:8d35c74403af, committed 2017-05-09
- Comitter:
- mjr
- Date:
- Tue May 09 05:48:37 2017 +0000
- Parent:
- 86:e30a1f60f783
- Child:
- 88:98bce687e6c0
- Commit message:
- AEDR-8300, VL6180X, TLC59116; new plunger firing detection
Changed in this revision
--- a/BitBangI2C/BitBangI2C.cpp Fri Apr 21 18:50:37 2017 +0000
+++ b/BitBangI2C/BitBangI2C.cpp Tue May 09 05:48:37 2017 +0000
@@ -16,7 +16,7 @@
// dprintf() = general debug diagnostics (printed only in case 2)
// eprintf() = error diagnostics (printed in case 1 and above)
//
-#define BBI2C_DEBUG 1
+#define BBI2C_DEBUG 0
#if BBI2C_DEBUG
# define eprintf(...) printf(__VA_ARGS__)
# if BBI2C_DEBUG >= 2
@@ -46,11 +46,14 @@
//
// Bit-bang I2C implementation
//
-BitBangI2C::BitBangI2C(PinName sda, PinName scl) :
- sclPin(scl), sdaPin(sda)
+BitBangI2C::BitBangI2C(PinName sda, PinName scl, bool internalPullup) :
+ sdaPin(sda, internalPullup), sclPin(scl, internalPullup)
{
// set the default frequency to 100kHz
frequency(100000);
+
+ // we're initially in a stop
+ inStop = true;
}
void BitBangI2C::frequency(uint32_t freq)
@@ -62,72 +65,101 @@
if (freq <= 100000)
{
// standard mode I2C bus - up to 100kHz
+
+ // nanosecond parameters
tLow = calcHiResWaitTime(4700);
tHigh = calcHiResWaitTime(4000);
- tBuf = calcHiResWaitTime(4700);
tHdSta = calcHiResWaitTime(4000);
tSuSta = calcHiResWaitTime(4700);
tSuSto = calcHiResWaitTime(4000);
tAck = calcHiResWaitTime(300);
- tData = calcHiResWaitTime(300);
tSuDat = calcHiResWaitTime(250);
+ tBuf = calcHiResWaitTime(4700);
}
else if (freq <= 400000)
{
// fast mode I2C - up to 400kHz
+
+ // nanosecond parameters
tLow = calcHiResWaitTime(1300);
tHigh = calcHiResWaitTime(600);
- tBuf = calcHiResWaitTime(1300);
tHdSta = calcHiResWaitTime(600);
tSuSta = calcHiResWaitTime(600);
tSuSto = calcHiResWaitTime(600);
tAck = calcHiResWaitTime(100);
- tData = calcHiResWaitTime(100);
tSuDat = calcHiResWaitTime(100);
+ tBuf = calcHiResWaitTime(1300);
}
else
{
// fast mode plus - up to 1MHz
+
+ // nanosecond parameters
tLow = calcHiResWaitTime(500);
tHigh = calcHiResWaitTime(260);
- tBuf = calcHiResWaitTime(500);
tHdSta = calcHiResWaitTime(260);
tSuSta = calcHiResWaitTime(260);
tSuSto = calcHiResWaitTime(260);
tAck = calcHiResWaitTime(50);
- tData = calcHiResWaitTime(50);
tSuDat = calcHiResWaitTime(50);
+ tBuf = calcHiResWaitTime(500);
}
}
void BitBangI2C::start()
{
- // take clock and data high
- sclHi();
- sdaHi();
- hiResWait(tBuf);
+ // check to see if we're starting after a stop, or if this is a
+ // repeated start
+ if (inStop)
+ {
+ // in a stop - make sure we waited for the minimum hold time
+ hiResWait(tBuf);
+ }
+ else
+ {
+ // repeated start - take data high
+ sdaHi();
+ hiResWait(tSuDat);
+
+ // take clock high
+ sclHi();
+
+ // wait for the minimum setup period
+ hiResWait(tSuSta);
+ }
// take data low
sdaLo();
- hiResWait(tHdSta);
- // take clock low
+ // wait for the setup period and take clock low
+ hiResWait(tHdSta);
sclLo();
- hiResWait(tLow);
+
+ // wait for the low period
+ hiResWait(tLow);
+
+ // no longer in a stop
+ inStop = false;
}
void BitBangI2C::stop()
{
- // take SDA low
- sdaLo();
+ // if we're not in a stop, enter one
+ if (!inStop)
+ {
+ // take SDA low
+ sdaLo();
- // take SCL high
- sclHi();
- hiResWait(tSuSto);
-
- // take SDA high
- sdaHi();
- hiResWait(tBuf);
+ // take SCL high
+ sclHi();
+ hiResWait(tSuSto);
+
+ // take SDA high
+ sdaHi();
+
+ // we're in a stop
+ inStop = true;
+ }
}
bool BitBangI2C::wait(uint32_t timeout_us)
@@ -233,13 +265,25 @@
// write the bits, most significant first
for (int i = 0 ; i < 8 ; ++i, data <<= 1)
writeBit(data & 0x80);
-
- // read and return the ACK bit
- return readBit();
+
+ // release SDA so the device can control it
+ sdaHi();
+
+ // read the ACK bit
+ int ack = readBit();
+
+ // take SDA low again
+ sdaLo();
+
+ // return success if ACK was 0
+ return ack;
}
int BitBangI2C::read(bool ack)
{
+ // take SDA high before reading
+ sdaHi();
+
// read 8 bits, most significant first
uint8_t data = 0;
for (int i = 0 ; i < 8 ; ++i)
@@ -248,6 +292,9 @@
// switch to output mode and send the ACK bit
writeBit(!ack);
+ // release SDA
+ sdaHi();
+
// return the data byte we read
return data;
}
@@ -260,27 +307,29 @@
// Wait (within reason) for it to actually read as high. The device
// can intentionally pull the clock line low to tell us to wait while
// it's working on preparing the data for us.
- Timer t;
- t.start();
- while (sclPin.read() == 0 && t.read_us() < 500000) ;
-
- // if the clock isn't high, we timed out
- if (sclPin.read() == 0)
+ int t = 0;
+ do
{
- eprintf("i2c.readBit, clock stretching timeout\r\n");
- return 0;
+ // if the clock is high, we're ready to go
+ if (sclPin.read())
+ {
+ // wait for the data setup time
+ hiResWait(tSuDat);
+
+ // read the bit
+ bool bit = sdaPin.read();
+
+ // take the clock low again
+ sclLo();
+ hiResWait(tLow);
+
+ // return the bit
+ return bit;
+ }
}
-
- // wait until the clock interval is up
- while (t.read_us() < clkPeriod_us);
-
- // read the bit
- bool bit = sdaPin.read();
-
- // take the clock low again
- sclLo();
- hiResWait(tLow);
-
- // return the bit
- return bit;
+ while (t++ < 100000);
+
+ // we timed out
+ eprintf("i2c.readBit, clock stretching timeout\r\n");
+ return 0;
}
--- a/BitBangI2C/BitBangI2C.h Fri Apr 21 18:50:37 2017 +0000
+++ b/BitBangI2C/BitBangI2C.h Tue May 09 05:48:37 2017 +0000
@@ -34,13 +34,17 @@
// the nominal address from the data sheet left one bit in each call
// to a routine here.
//
-// Electrically, the I2C bus is designed as a a pair of open-collector
-// lines with pull-up resistors. Any device can pull a line low by
-// shorting it to ground, but no one can pull a line high: instead, you
-// *allow* a line to go high by releasing it, which is to say putting
-// your connection to it in a high-Z state. On an MCU, we put a GPIO
-// pin in high-Z state by setting its direction to INPUT mode. So our
-// GPIO write strategy is like this:
+// Electrically, the I2C bus consists of two lines, SDA (data) and SCL
+// (clock). Multiple devices can connect to the bus by connecting to
+// these two lines; the lines are shared among all of the devices. Each
+// line has a pull-up resistor that pulls it to logic '1' voltage. Each
+// device connects with an open-collector circuit that can short the line
+// to ground (logic '0'). This means that any device can assert a 'low'
+// but no one can actually assert a 'high'; the pull-up makes it so that
+// a 'high' occurs when no one is asserting a 'low'. On an MCU, we release
+// a line by putting the GPIO pin in high-Z state, which we can do on the
+// KL25Z by setting its direction to INPUT mode. So our GPIO write strategy
+// is like this:
//
// - take a pin low (0):
// pin.input();
@@ -54,6 +58,12 @@
// the direction to output is enough to assert the low level, since the
// hardware asserts the level that was previously stored in the output
// register whenever the direction is changed from input to output.
+//
+// The KL25Z by default provides a built-in pull-up resistor on each GPIO
+// set to input mode. This can optionally be used as the bus-wide pull-up
+// for each line. Standard practice is to use external pull-up resistors
+// rather than MCU pull-ups, but the internal pull-ups are fine for ad hoc
+// setups where there's only one external device connected to a GPIO pair.
#ifndef _BITBANGI2C_H_
@@ -64,6 +74,27 @@
#include "pinmap.h"
+// For testing purposes: a cover class for the mbed library I2C bridging
+// the minor differences in our interface. This allows switching between
+// BitBangI2C and the mbed library I2C via a macro of the like.
+class MbedI2C: public I2C
+{
+public:
+ MbedI2C(PinName sda, PinName scl, bool internalPullups) : I2C(sda, scl) { }
+
+ int write(int addr, const uint8_t *data, size_t len, bool repeated = false)
+ {
+ return I2C::write(addr, (const char *)data, len, repeated);
+ }
+ int read(int addr, uint8_t *data, size_t len, bool repeated = false)
+ {
+ return I2C::read(addr, (char *)data, len, repeated);
+ }
+
+ void reset() { }
+};
+
+
// DigitalInOut replacmement class for I2C use. I2C uses pins a little
// differently from other use cases. I2C is a bus, where many devices can
// be attached to each line. To allow this shared access, devices can
@@ -81,19 +112,23 @@
class I2CInOut
{
public:
- I2CInOut(PinName pin)
+ I2CInOut(PinName pin, bool internalPullup)
{
// initialize the pin
gpio_t g;
gpio_init(&g, pin);
// get the registers
- unsigned int port = (unsigned int)pin >> PORT_SHIFT;
- FGPIO_Type *r = (FGPIO_Type *)(FPTA_BASE + port*0x40);
- __IO uint32_t *pin_pcr = (__IO uint32_t*)(PORTA_BASE + pin);
+ unsigned int portno = (unsigned int)pin >> PORT_SHIFT;
+ uint32_t pinno = (uint32_t)(pin & 0x7C) >> 2;
+ FGPIO_Type *r = (FGPIO_Type *)(FPTA_BASE + portno*0x40);
+ __IO uint32_t *pin_pcr = &(((PORT_Type *)(PORTA_BASE + 0x1000*portno)))->PCR[pinno];
- // set no-pull-up mode (clear PE bit = Pull Enable)
- *pin_pcr &= ~0x02;
+ // set the desired internal pull-up mode
+ if (internalPullup)
+ *pin_pcr |= 0x02;
+ else
+ *pin_pcr &= ~0x02;
// save the register information we'll need later
this->mask = g.mask;
@@ -148,7 +183,7 @@
{
public:
// create the interface
- BitBangI2C(PinName sda, PinName scl);
+ BitBangI2C(PinName sda, PinName scl, bool internalPullups);
// set the bus frequency in Hz
void frequency(uint32_t freq);
@@ -192,7 +227,7 @@
// clock it
sclPin.hi();
- hiResWait(tData);
+ hiResWait(tHigh);
// drop the clock
sclPin.lo();
@@ -208,9 +243,9 @@
inline void sdaHi() { sdaPin.hi(); }
inline void sdaLo() { sdaPin.lo(); }
- // SCL and SDA pins
+ // SDA and SCL pins
+ I2CInOut sdaPin;
I2CInOut sclPin;
- I2CInOut sdaPin;
// inverse of frequency = clock period in microseconds
uint32_t clkPeriod_us;
@@ -263,13 +298,15 @@
//
int tLow; // SCL low period
int tHigh; // SCL high period
- int tBuf; // bus free time between start and stop conditions
int tHdSta; // hold time for start condition
int tSuSta; // setup time for repeated start condition
int tSuSto; // setup time for stop condition
int tSuDat; // data setup time
int tAck; // ACK time
- int tData; // data valid time
+ int tBuf; // bus free time between start and stop conditions
+
+ // are we in a Stop condition?
+ bool inStop;
};
#endif /* _BITBANGI2C_H_ */
--- a/Plunger/barCodeSensor.h Fri Apr 21 18:50:37 2017 +0000
+++ b/Plunger/barCodeSensor.h Tue May 09 05:48:37 2017 +0000
@@ -5,10 +5,19 @@
// by reading the bar code and decoding it into a position figure.
//
// The bar code has to be encoded in a specific format that we recognize.
-// We use a 10-bit reflected Gray code, optically encoded using a Manchester-
-// type of coding. Each bit is represented as a fixed-width area on the
-// bar, half white and half black. The bit value is encoded in the order
-// of the colors: Black/White is '0', and White/Black is '1'.
+// We use a reflected Gray code, optically encoded in black/white pixel
+// patterns. Each bit is represented by a fixed-width area. Half the
+// pixels in every bit are white, and half are black. A '0' bit is
+// represented by black pixels in the left half and white pixels in the
+// right half, and a '1' bit is white on the left and black on the right.
+// To read a bit, we identify the set of pixels covering the bit's fixed
+// area in the code, then we see if the left or right half is brighter.
+//
+// (This optical encoding scheme is based on Manchester coding, which is
+// normally used in the context of serial protocols, but translates to
+// bar codes straightforwardly. Replace the serial protocol's time
+// dimension with the spatial dimension across the bar, and replace the
+// high/low wire voltage levels with white/black pixels.)
//
// Gray codes are ideal for this type of application. Gray codes are
// defined such that each code point differs in exactly one bit from each
@@ -20,21 +29,73 @@
// the reading will come out as one of points on either side of the true
// position. Finally, motion blur will have the same effect, of creating
// ambiguity in the least significant bits, and thus giving us a reading
-// that's correct to as many bits as we can read with teh blur.
+// that's correct to as many bits as we can make out.
//
-// We use the Manchester-type optical coding because it has good properties
-// for low-contrast images, and doesn't require uniform lighting. Each bit's
-// pixel span contains equal numbers of light and dark pixels, so each bit
-// provides its own local level reference. This means we don't care about
-// lighting uniformity over the whole image, because we don't need a global
-// notion of light and dark, just a local one over a single bit at a time.
+// The half-and-half optical coding also has good properties for our
+// purposes. The fixed-width bit regions require essentially no CPU work
+// to find the bits, which is good because we're using a fairly slow CPU.
+// The half white/half black coding of each pixel makes every pixel
+// self-relative in terms of brightness, so we don't need to figure the
+// black and white thresholds globally for the whole image. That makes
+// the physical device engineering and installation easier because the
+// software can tolerate a fairly wide range of lighting conditions.
//
#ifndef _BARCODESENSOR_H_
#define _BARCODESENSOR_H_
#include "plunger.h"
-#include "tsl14xxSensor.h"
+
+// Gray code to binary mapping for our special coding. This is a custom
+// 7-bit code, minimum run length 6, 110 positions populated. The minimum
+// run length is the minimum number of consecutive code points where each
+// bit must remain fixed. For out optical coding, this defines the smallest
+// "island" size for a black or white bar horizontally. Small features are
+// prone to light scattering that makes them appear gray on the sensor.
+// Larger features are less subject to scatter, making them easier to
+// distinguish by brightness level.
+static const uint8_t grayToBin[] = {
+ 0, 1, 83, 2, 71, 100, 84, 3, 69, 102, 82, 128, 70, 101, 57, 4, // 0-15
+ 35, 50, 36, 37, 86, 87, 85, 128, 34, 103, 21, 104, 128, 128, 20, 5, // 16-31
+ 11, 128, 24, 25, 98, 99, 97, 40, 68, 67, 81, 80, 55, 54, 56, 41, // 32-47
+ 10, 51, 23, 38, 128, 52, 128, 39, 9, 66, 22, 128, 8, 53, 7, 6, // 48-63
+ 47, 14, 60, 128, 72, 15, 59, 16, 46, 91, 93, 92, 45, 128, 58, 17, // 64-79
+ 48, 49, 61, 62, 73, 88, 74, 75, 33, 90, 106, 105, 32, 89, 19, 18, // 80-95
+ 12, 13, 95, 26, 128, 28, 96, 27, 128, 128, 94, 79, 44, 29, 43, 42, // 96-111
+ 128, 64, 128, 63, 110, 128, 109, 76, 128, 65, 107, 78, 31, 30, 108, 77 // 112-127
+};
+
+
+// Auto-exposure counter
+class BarCodeExposureCounter
+{
+public:
+ BarCodeExposureCounter()
+ {
+ nDark = 0;
+ nBright = 0;
+ nZero = 0;
+ nSat = 0;
+ }
+
+ inline void count(int pix)
+ {
+ if (pix <= 2)
+ ++nZero;
+ else if (pix < 12)
+ ++nDark;
+ else if (pix >= 253)
+ ++nSat;
+ else if (pix > 200)
+ ++nBright;
+ }
+
+ int nDark; // dark pixels
+ int nBright; // bright pixels
+ int nZero; // pixels at zero brightness
+ int nSat; // pixels at full saturation
+};
+
// Base class for bar-code sensors
//
@@ -58,13 +119,51 @@
// full bit, including both "half bits" - it's the full white/black or
// black/white pattern area.
+struct BarCodeProcessResult
+{
+ int pixofs;
+ int raw;
+ int mask;
+};
+
template <int nBits, int leftBarWidth, int leftBarMaxOfs, int bitWidth>
-class PlungerSensorBarCode
+class PlungerSensorBarCode: public PlungerSensorImage<BarCodeProcessResult>
{
public:
+ PlungerSensorBarCode(PlungerSensorImageInterface &sensor, int npix)
+ : PlungerSensorImage(sensor, npix, (1 << nBits) - 1)
+ {
+ startOfs = 0;
+ }
+
+ // process a configuration change
+ virtual void onConfigChange(int varno, Config &cfg)
+ {
+ // check for bar-code variables
+ switch (varno)
+ {
+ case 20:
+ // bar code offset
+ startOfs = cfg.plunger.barCode.startPix;
+ break;
+ }
+
+ // do the generic work
+ PlungerSensorImage::onConfigChange(varno, cfg);
+ }
+
+protected:
// process the image
- bool process(const uint8_t *pix, int npix, int &pos)
+ virtual bool process(const uint8_t *pix, int npix, int &pos, BarCodeProcessResult &res)
{
+ // adjust auto-exposure
+ adjustExposure(pix, npix);
+
+ // clear the result descriptor
+ res.pixofs = 0;
+ res.raw = 0;
+ res.mask = 0;
+
#if 0 // $$$
// scan from the left edge until we find the fixed '0' start bit
@@ -147,25 +246,46 @@
}
else
{
- barStart = 4; // $$$ should be configurable via config tool
+ // start at the fixed pixel offset
+ barStart = startOfs;
}
+ // Start with zero in the barcode and success mask. The mask
+ // indicates which bits we were able to read successfully: a
+ // '1' bit in the mask indicates that the corresponding bit
+ // position in 'barcode' was successfully read, a '0' bit means
+ // that the image was too fuzzy to read.
+ int barcode = 0, mask = 0;
+
// Scan the bits
- int barcode = 0;
for (int bit = 0, x0 = barStart; bit < nBits ; ++bit, x0 += bitWidth)
{
- // figure the extent of this bit
- int x1 = x0 + bitWidth / 2;
- int x2 = x0 + bitWidth;
+#if 0
+ // Figure the extent of this bit. The last bit is double
+ // the width of the other bits, to give us a better chance
+ // of making out the small features of the last bit.
+ int w = bitWidth;
+ if (bit == nBits - 1) w *= 2;
+#else
+ // width of the bit
+ const int w = bitWidth;
+#endif
+
+ // figure the bit's internal pixel layout
+ int halfBitWidth = w / 2;
+ int x1 = x0 + halfBitWidth; // midpoint
+ int x2 = x0 + w; // right edge
+
+ // make sure we didn't go out of bounds
if (x1 > npix) x1 = npix;
if (x2 > npix) x2 = npix;
+#if 0
// get the average of the pixels over the bit
int sum = 0;
for (int x = x0 ; x < x2 ; ++x)
sum += pix[x];
- int avg = sum / bitWidth;
-
+ int avg = sum / w;
// Scan the left and right sections. Classify each
// section according to whether the majority of its
// pixels are above or below the local average.
@@ -174,20 +294,56 @@
lsum += (pix[x] < avg ? 0 : 1);
for (int x = x1 + 1 ; x < x2 - 1 ; ++x)
rsum += (pix[x] < avg ? 0 : 1);
+#else
+ // Sum the pixel readings in each half-bit. Ignore
+ // the first and last bit of each section, since these
+ // could be contaminated with scattered light from the
+ // adjacent half-bit. On the right half, hew to the
+ // right side if the overall pixel width is odd.
+ int lsum = 0, rsum = 0;
+ for (int x = x0 + 1 ; x < x1 - 1 ; ++x)
+ lsum += pix[x];
+ for (int x = x2 - halfBitWidth + 1 ; x < x2 - 1 ; ++x)
+ rsum += pix[x];
+#endif
- // if we don't have a winner, fail
- if (lsum == rsum)
- return false;
+ // shift a zero bit into the code and success mask
+ barcode <<= 1;
+ mask <<= 1;
- // black/white = 0, white/black = 1
- barcode = (barcode << 1) | (lsum < rsum ? 0 : 1);
+ // Brightness difference required per pixel. Higher values
+ // require greater contrast to make a reading, which reduces
+ // spurious readings at the cost of reducing the overall
+ // success rate. The right level depends on the quality of
+ // the optical system. Setting this to zero makes us maximally
+ // tolerant of low-contrast images, allowing for the simplest
+ // optical system. Our simple optical system suffers from
+ // poor focus, which in turn causes poor contrast in small
+ // features.
+ const int minDelta = 2;
+
+ // see if we could tell the difference in brightness
+ int delta = lsum - rsum;
+ if (delta < 0) delta = -delta;
+ if (delta > minDelta * w/2)
+ {
+ // got it - black/white = 0, white/black = 1
+ if (lsum > rsum) barcode |= 1;
+ mask |= 1;
+ }
}
// decode the Gray code value to binary
- pos = grayToBin(barcode);
+ pos = grayToBin[barcode];
- // success
- return true;
+ // set the results descriptor structure
+ res.pixofs = barStart;
+ res.raw = barcode;
+ res.mask = mask;
+
+ // return success if we decoded all bits, and the Gray-to-binary
+ // mapping was populated
+ return pos != (1 << nBits) && mask == ((1 << nBits) - 1);
#endif
}
@@ -255,83 +411,17 @@
}
}
- // convert a reflected Gray code value (up to 16 bits) to binary
- int grayToBin(int grayval)
- {
- int temp = grayval ^ (grayval >> 8);
- temp ^= (temp >> 4);
- temp ^= (temp >> 2);
- temp ^= (temp >> 1);
- return temp;
- }
-};
-
-// Auto-exposure counter
-class BarCodeExposureCounter
-{
-public:
- BarCodeExposureCounter()
- {
- nDark = 0;
- nBright = 0;
- nZero = 0;
- nSat = 0;
- }
-
- inline void count(int pix)
- {
- if (pix <= 2)
- ++nZero;
- else if (pix < 12)
- ++nDark;
- else if (pix >= 253)
- ++nSat;
- else if (pix > 200)
- ++nBright;
- }
-
- int nDark; // dark pixels
- int nBright; // bright pixels
- int nZero; // pixels at zero brightness
- int nSat; // pixels at full saturation
-};
-
-// PlungerSensor interface implementation for bar code readers.
-//
-// Bar code readers are image sensors, so we have a pixel size for
-// the sensor. However, this isn't the scale for the readings. The
-// scale for the readings is determined by the number of bits in the
-// bar code, since an n-bit bar code can encode 2^n distinct positions.
-//
-template <int nBits, int leftBarWidth, int leftBarMaxOfs, int bitWidth>
-class PlungerSensorBarCodeTSL14xx: public PlungerSensorTSL14xxSmall,
- PlungerSensorBarCode<nBits, leftBarWidth, leftBarMaxOfs, bitWidth>
-{
-public:
- PlungerSensorBarCodeTSL14xx(int nativePix, PinName si, PinName clock, PinName ao)
- : PlungerSensorTSL14xxSmall(nativePix, (1 << nBits) - 1, si, clock, ao)
- {
- // the native scale is the number of positions we can
- // encode in the bar code
- nativeScale = 1023;
- }
-
-protected:
-
- // process the image through the bar code reader
- virtual bool process(const uint8_t *pix, int npix, int &pos)
- {
- // adjust the exposure
- adjustExposure(pix, npix);
-
- // do the standard bar code processing
- return PlungerSensorBarCode<nBits, leftBarWidth, leftBarMaxOfs, bitWidth>
- ::process(pix, npix, pos);
- }
-
// bar code sensor orientation is fixed
virtual int getOrientation() const { return 1; }
+ // send extra status report headers
+ virtual void extraStatusHeaders(USBJoystick &js, BarCodeProcessResult &res)
+ {
+ // Send the bar code status report. We use coding type 1 (Gray code,
+ // Manchester pixel coding).
+ js.sendPlungerStatusBarcode(nBits, 1, res.pixofs, bitWidth, res.raw, res.mask);
+ }
+
// adjust the exposure
void adjustExposure(const uint8_t *pix, int npix)
{
@@ -342,23 +432,24 @@
// pixels has to look: the bit area will be 50% black and 50%
// white, and the margins will be all white. For maximum
// contrast, target an exposure level where the black pixels
- // are all below the middle brightness level and the white
+ // are all below a certain brightness level and the white
// pixels are all above. Start by figuring the number of
// pixels above and below.
- int nDark = 0;
+ const int medianTarget = 160;
+ int nBelow = 0;
for (int i = 0 ; i < npix ; ++i)
{
- if (pix[i] < 200)
- ++nDark;
+ if (pix[i] < medianTarget)
+ ++nBelow;
}
- // Figure the percentage of black pixels: the left bar is
+ // Figure the desired number of black pixels: the left bar is
// all black pixels, and 50% of each bit is black pixels.
- int targetDark = leftBarWidth + (nBits * bitWidth)/2;
+ int targetBelow = leftBarWidth + (nBits * bitWidth)/2;
// Increase exposure time if too many pixels are below the
// halfway point; decrease it if too many pixels are above.
- int d = nDark - targetDark;
+ int d = nBelow - targetBelow;
if (d > 5 || d < -5)
{
axcTime += d;
@@ -475,20 +566,21 @@
if (axcTime > 2500)
axcTime = 2500;
}
-};
-// TSL1401CL - 128-bit image sensor, used as a bar code reader
-class PlungerSensorTSL1401CL: public PlungerSensorBarCodeTSL14xx<
- 10, // number of bits in code
- 0, // left delimiter bar width in pixels (0 for none)
- 24, // maximum left margin width in pixels
- 12> // pixel width of each bit
-{
-public:
- PlungerSensorTSL1401CL(PinName si, PinName clock, PinName a0)
- : PlungerSensorBarCodeTSL14xx(128, si, clock, a0)
+#if 0
+ // convert a reflected Gray code value (up to 16 bits) to binary
+ static inline int grayToBin(int grayval)
{
+ int temp = grayval ^ (grayval >> 8);
+ temp ^= (temp >> 4);
+ temp ^= (temp >> 2);
+ temp ^= (temp >> 1);
+ return temp;
}
+#endif
+
+ // bar code starting pixel offset
+ int startOfs;
};
#endif
--- a/Plunger/distanceSensor.h Fri Apr 21 18:50:37 2017 +0000
+++ b/Plunger/distanceSensor.h Tue May 09 05:48:37 2017 +0000
@@ -43,8 +43,8 @@
public:
PlungerSensorDistance(int nativeScale) : PlungerSensor(nativeScale)
{
- // start the sample timer
- t.start();
+ totalTime = 0;
+ nRuns = 0;
}
// get the average scan time
@@ -58,11 +58,7 @@
nRuns += 1;
}
- // sample timer
- Timer t;
-
// scan time statistics
- uint32_t tStart; // time (on this->t) of start of current scan
uint64_t totalTime; // total time consumed by all reads so far
uint32_t nRuns; // number of runs so far
};
@@ -73,66 +69,79 @@
// sensor units are millimeters. A physical plunger has about 3" of
// total travel, but leave a little extra padding for measurement
// inaccuracies and other unusual situations, so'll use an actual
-// native scale of 5" = 127mm.
+// native scale of 150mm.
class PlungerSensorVL6180X: public PlungerSensorDistance
{
public:
PlungerSensorVL6180X(PinName sda, PinName scl, PinName gpio0)
- : PlungerSensorDistance(127),
- sensor(sda, scl, I2C_ADDRESS, gpio0)
+ : PlungerSensorDistance(150),
+ sensor(sda, scl, I2C_ADDRESS, gpio0, true)
{
}
- static const int I2C_ADDRESS = 0x28;
+ // fixed I2C bus address for the VL6180X
+ static const int I2C_ADDRESS = 0x29;
virtual void init()
{
- // reboot and initialize the sensor
+ // initialize the sensor and set the default configuration
sensor.init();
-
- // set the default configuration
sensor.setDefaults();
- // start the first reading
- tStart = t.read_us();
+ // start a reading
sensor.startRangeReading();
}
virtual bool ready()
{
+ // make sure a reading has been initiated
+ sensor.startRangeReading();
+
+ // check if a reading is ready
return sensor.rangeReady();
}
virtual bool readRaw(PlungerReading &r)
{
- // get the range reading
- uint8_t d;
- int err = sensor.getRange(d, 25000);
-
- // start a new reading
- sensor.startRangeReading();
- tStart = t.read_us();
-
- // use the current timestamp
- r.t = t.read_us();
+ // if we have a new reading ready, collect it
+ if (sensor.rangeReady())
+ {
+ // Get the range reading. Note that we already know that the
+ // sensor has a reading ready, so it shouldn't be possible to
+ // time out on the read. (The sensor could have timed out on
+ // convergence, but if it did, that's in the past already so
+ // it's not something we have to wait for now.)
+ uint8_t d;
+ uint32_t t, dt;
+ lastErr = sensor.getRange(d, t, dt, 100);
+
+ // if we got a reading, update the last reading
+ if (lastErr == 0)
+ {
+ // save the new reading
+ last.pos = d;
+ last.t = t;
+
+ // collect scan time statistics
+ collectScanTimeStats(dt);
+ }
+
+ // start a new reading
+ sensor.startRangeReading();
+ }
- // The sensor measures distance from the front of the cabinet
- // (in our standard setup). For reporting purposes, we want
- // the position reading to increase as the plunger is retracted,
- // so we want to reverse the scale.
- r.pos = nativeScale - d;
-
- // collect scan time statistics
- if (err == 0)
- collectScanTimeStats(uint32_t(r.t - tStart));
-
- // return the status ('err' is zero on success)
- return err == 0;
+ // return the most recent reading
+ r = last;
+ return lastErr == 0;
}
protected:
// underlying sensor interface
VL6180X sensor;
+
+ // last reading and error status
+ PlungerReading last;
+ int lastErr;
};
--- a/Plunger/edgeSensor.h Fri Apr 21 18:50:37 2017 +0000
+++ b/Plunger/edgeSensor.h Tue May 09 05:48:37 2017 +0000
@@ -18,7 +18,6 @@
#define _EDGESENSOR_H_
#include "plunger.h"
-#include "tsl14xxSensor.h"
// Scan method - select a method listed below. Method 2 (find the point
// with maximum brightness slop) seems to work the best so far.
@@ -101,12 +100,15 @@
// This is a generic base class for image-based sensors where we detect
// the plunger position by finding the edge of the shadow it casts on
// the detector.
-class PlungerSensorEdgePos
+//
+// Edge sensors use the image pixel span as the native position scale,
+// since a position reading is the pixel offset of the shadow edge.
+class PlungerSensorEdgePos: public PlungerSensorImage<int>
{
public:
- PlungerSensorEdgePos(int npix)
+ PlungerSensorEdgePos(PlungerSensorImageInterface &sensor, int npix)
+ : PlungerSensorImage<int>(sensor, npix, npix - 1)
{
- native_npix = npix;
}
// Process an image - scan for the shadow edge to determine the plunger
@@ -121,7 +123,7 @@
#if SCAN_METHOD == 0
// Scan method 0: one-way scan; original method used in v1 firmware.
- bool process(const uint8_t *pix, int n, int &pos)
+ bool process(const uint8_t *pix, int n, int &pos, int& /*processResult*/)
{
// Get the levels at each end
int a = (int(pix[0]) + pix[1] + pix[2] + pix[3] + pix[4])/5;
@@ -241,7 +243,7 @@
#if SCAN_METHOD == 1
// Scan method 1: meet in the middle.
- bool process(const uint8_t *pix, int n, int &pos)
+ bool process(const uint8_t *pix, int n, int &pos, int& /*processResult*/)
{
// Get the levels at each end
int a = (int(pix[0]) + pix[1] + pix[2] + pix[3] + pix[4])/5;
@@ -361,7 +363,7 @@
#if SCAN_METHOD == 2
// Scan method 2: scan for steepest brightness slope.
- bool process(const uint8_t *pix, int n, int &pos)
+ virtual bool process(const uint8_t *pix, int n, int &pos, int& /*processResult*/)
{
// Get the levels at each end by averaging across several pixels.
// Compute just the sums: don't bother dividing by the count, since
@@ -419,7 +421,7 @@
#if SCAN_METHOD == 3
// Scan method 0: one-way scan; original method used in v1 firmware.
- bool process(const uint8_t *pix, int n, int &pos)
+ bool process(const uint8_t *pix, int n, int &pos, int& /*processResult*/)
{
// Get the levels at each end
int a = (int(pix[0]) + pix[1] + pix[2] + pix[3] + pix[4])/5;
@@ -497,9 +499,6 @@
virtual int getOrientation() const { return dir; }
int dir;
- // number of pixels
- int native_npix;
-
// History of midpoint brightness levels for the last few successful
// scans. This is a circular buffer that we write on each scan where
// we successfully detect a shadow edge. (It's circular, so we
@@ -531,62 +530,4 @@
};
-// -------------------------------------------------------------------------
-//
-// Edge position plunger sensor for TSL14xx-based sensors. An edge
-// detection setup requires one of the large sensors, 1410R or 1412S,
-// since we need the sensor to cover the whole extent of the physical
-// plunger's travel, which is about 3".
-//
-// The native scale for image edge detectors is sensor pixels, since
-// we read the plunger position as the pixel location of the shadow
-// edge on the image.
-//
-class PlungerSensorEdgePosTSL14xx: public PlungerSensorTSL14xxLarge, public PlungerSensorEdgePos
-{
-public:
- PlungerSensorEdgePosTSL14xx(int nativePix, PinName si, PinName clock, PinName ao)
- : PlungerSensorTSL14xxLarge(nativePix, nativePix - 1, si, clock, ao),
- PlungerSensorEdgePos(nativePix)
- {
- // we don't know the direction yet
- dir = 0;
-
- // set the midpoint history arbitrarily to the absolute halfway point
- memset(midpt, 127, sizeof(midpt));
- midptIdx = 0;
-
- // the native reporting scale is the pixel size of the sensor, since
- // the position is figured as the shadow location in the image
- nativeScale = nativePix;
- }
-
-protected:
- // process the image through the edge detector
- virtual bool process(const uint8_t *pix, int npix, int &pixpos)
- {
- return PlungerSensorEdgePos::process(pix, npix, pixpos);
- }
-};
-
-// TSL1410R sensor
-class PlungerSensorTSL1410R: public PlungerSensorEdgePosTSL14xx
-{
-public:
- PlungerSensorTSL1410R(PinName si, PinName clock, PinName ao)
- : PlungerSensorEdgePosTSL14xx(1280, si, clock, ao)
- {
- }
-};
-
-// TSL1412R
-class PlungerSensorTSL1412R: public PlungerSensorEdgePosTSL14xx
-{
-public:
- PlungerSensorTSL1412R(PinName si, PinName clock, PinName ao)
- : PlungerSensorEdgePosTSL14xx(1536, si, clock, ao)
- {
- }
-};
-
#endif /* _EDGESENSOR_H_ */
--- a/Plunger/plunger.h Fri Apr 21 18:50:37 2017 +0000
+++ b/Plunger/plunger.h Tue May 09 05:48:37 2017 +0000
@@ -13,6 +13,8 @@
#ifndef PLUNGER_H
#define PLUNGER_H
+#include "config.h"
+
// Plunger reading with timestamp
struct PlungerReading
{
@@ -73,6 +75,9 @@
// working on the current reading's data transfer.
virtual bool ready() { return true; }
+ // Is a plunger DMA operation in progress?
+ virtual bool dmaBusy() { return false; }
+
// Read the sensor position, if possible. Returns true on success,
// false if it wasn't possible to take a reading. On success, fills
// in 'r' with the current reading and timestamp and returns true.
@@ -103,7 +108,7 @@
if (readRaw(r))
{
// process it through the jitter filter
- //$$$ r.pos = jitterFilter(r.pos);
+ r.pos = jitterFilter(r.pos);
// adjust to the abstract scale via the scaling factor
r.pos = uint16_t(uint32_t((scalingFactor * r.pos) + 32768) >> 16);
@@ -195,7 +200,9 @@
// of the window
jfLo = pos;
jfHi = pos + jfWindow;
- jfLast = pos;
+
+ // figure the new position as the centerpoint of the new window
+ jfLast = pos = (jfHi + jfLo)/2;
return pos;
}
else if (pos > jfHi)
@@ -205,7 +212,9 @@
// the window
jfHi = pos;
jfLo = pos - jfWindow;
- jfLast = pos;
+
+ // figure the new position as the centerpoint of the new window
+ jfLast = pos = (jfHi + jfLo)/2;
return pos;
}
else
@@ -216,6 +225,20 @@
}
}
+ // Process a configuration variable change. 'varno' is the
+ // USB protocol variable number being updated; 'cfg' is the
+ // updated configuration.
+ virtual void onConfigChange(int varno, Config &cfg)
+ {
+ switch (varno)
+ {
+ case 19:
+ // jitter window
+ setJitterWindow(cfg.plunger.jitterWindow);
+ break;
+ }
+ }
+
// Set the jitter filter window size. This is specified in native
// sensor units.
void setJitterWindow(int w)
@@ -264,4 +287,217 @@
int jfLast; // last filtered reading
};
+
+// --------------------------------------------------------------------------
+//
+// Generic image sensor interface for image-based plungers
+//
+class PlungerSensorImageInterface
+{
+public:
+ PlungerSensorImageInterface(int npix)
+ {
+ native_npix = npix;
+ }
+
+ // initialize the sensor
+ virtual void init() = 0;
+
+ // is the sensor ready?
+ virtual bool ready() = 0;
+
+ // is a DMA transfer in progress?
+ virtual bool dmaBusy() = 0;
+
+ // read the image
+ virtual void readPix(uint8_t* &pix, uint32_t &t, int axcTime) = 0;
+
+ // Get an image for a pixel status report. 't' is the timestamp of
+ // the image. 'extraTime' is extra exposure time for the image, in
+ // 0.1ms increments.
+ virtual void getStatusReportPixels(
+ uint8_t* &pix, uint32_t &t, int axcTime, int extraTime) = 0;
+
+ // Reset the sensor after a status report. Status reports take a long
+ // time to send, so sensors that use continuous integration cycling may
+ // need to reset after a status report so that they aren't overexposed
+ // by the long delay of sending the status report.
+ virtual void resetAfterStatusReport(int axcTime) = 0;
+
+ // get the average sensor pixel scan time (the time it takes on average
+ // to read one image frame from the sensor)
+ virtual uint32_t getAvgScanTime() = 0;
+
+protected:
+ // number of pixels on sensor
+ int native_npix;
+};
+
+
+// ----------------------------------------------------------------------------
+//
+// Plunger base class for image-based sensors
+//
+template<class ProcessResult>
+class PlungerSensorImage: public PlungerSensor
+{
+public:
+ PlungerSensorImage(PlungerSensorImageInterface &sensor, int npix, int nativeScale)
+ : PlungerSensor(nativeScale), sensor(sensor)
+ {
+ axcTime = 0;
+ native_npix = npix;
+ }
+
+ // initialize the sensor
+ virtual void init() { sensor.init(); }
+
+ // is the sensor ready?
+ virtual bool ready() { return sensor.ready(); }
+
+ // is a DMA transfer in progress?
+ virtual bool dmaBusy() { return sensor.dmaBusy(); }
+
+ // get the pixel transfer time
+ virtual uint32_t getAvgScanTime() { return sensor.getAvgScanTime(); }
+
+ // read the plunger position
+ virtual bool readRaw(PlungerReading &r)
+ {
+ // read pixels from the sensor
+ uint8_t *pix;
+ uint32_t tpix;
+ sensor.readPix(pix, tpix, axcTime);
+
+ // process the pixels
+ int pixpos;
+ ProcessResult res;
+ if (process(pix, native_npix, pixpos, res))
+ {
+ r.pos = pixpos;
+ r.t = tpix;
+
+ // success
+ return true;
+ }
+ else
+ {
+ // no position found
+ return false;
+ }
+ }
+
+ // Send a status report to the joystick interface.
+ // See plunger.h for details on the arguments.
+ virtual void sendStatusReport(USBJoystick &js, uint8_t flags, uint8_t extraTime)
+ {
+ // get pixels
+ uint8_t *pix;
+ uint32_t t;
+ sensor.getStatusReportPixels(pix, t, axcTime, extraTime);
+
+ // start a timer to measure the processing time
+ Timer pt;
+ pt.start();
+
+ // process the pixels and read the position
+ int pos, rawPos;
+ int n = native_npix;
+ ProcessResult res;
+ if (process(pix, n, rawPos, res))
+ {
+ // success - apply the jitter filter
+ pos = jitterFilter(rawPos);
+ }
+ else
+ {
+ // report 0xFFFF to indicate that the position wasn't read
+ pos = 0xFFFF;
+ rawPos = 0xFFFF;
+ }
+
+ // note the processing time
+ uint32_t processTime = pt.read_us();
+
+ // If a low-res scan is desired, reduce to a subset of pixels. Ignore
+ // this for smaller sensors (below 512 pixels)
+ if ((flags & 0x01) && n >= 512)
+ {
+ // figure how many sensor pixels we combine into each low-res pixel
+ const int group = 8;
+ int lowResPix = n / group;
+
+ // combine the pixels
+ int src, dst;
+ for (src = dst = 0 ; dst < lowResPix ; ++dst)
+ {
+ // average this block of pixels
+ int a = 0;
+ for (int j = 0 ; j < group ; ++j)
+ a += pix[src++];
+
+ // we have the sum, so get the average
+ a /= group;
+
+ // store the down-res'd pixel in the array
+ pix[dst] = uint8_t(a);
+ }
+
+ // update the pixel count to the reduced array size
+ n = lowResPix;
+ }
+
+ // figure the report flags
+ int jsflags = 0;
+
+ // add flags for the detected orientation: 0x01 for normal orientation,
+ // 0x02 for reversed orientation; no flags if orientation is unknown
+ int dir = getOrientation();
+ if (dir == 1)
+ jsflags |= 0x01;
+ else if (dir == -1)
+ jsflags |= 0x02;
+
+ // send the sensor status report headers
+ js.sendPlungerStatus(n, pos, jsflags, sensor.getAvgScanTime(), processTime);
+ js.sendPlungerStatus2(nativeScale, jfLo, jfHi, rawPos, axcTime);
+
+ // send any extra status headers for subclasses
+ extraStatusHeaders(js, res);
+
+ // If we're not in calibration mode, send the pixels
+ extern bool plungerCalMode;
+ if (!plungerCalMode)
+ {
+ // send the pixels in report-sized chunks until we get them all
+ int idx = 0;
+ while (idx < n)
+ js.sendPlungerPix(idx, n, pix);
+ }
+
+ // reset the sensor, if necessary
+ sensor.resetAfterStatusReport(axcTime);
+ }
+
+protected:
+ // process an image to read the plunger position
+ virtual bool process(const uint8_t *pix, int npix, int &rawPos, ProcessResult &res) = 0;
+
+ // send extra status headers, following the standard headers (types 0 and 1)
+ virtual void extraStatusHeaders(USBJoystick &js, ProcessResult &res) { }
+
+ // get the detected orientation
+ virtual int getOrientation() const { return 0; }
+
+ // underlying hardware sensor interface
+ PlungerSensorImageInterface &sensor;
+
+ // number of pixels
+ int native_npix;
+
+ // auto-exposure time
+ uint32_t axcTime;
+};
+
+
#endif /* PLUNGER_H */
--- a/Plunger/tsl14xxSensor.h Fri Apr 21 18:50:37 2017 +0000
+++ b/Plunger/tsl14xxSensor.h Tue May 09 05:48:37 2017 +0000
@@ -15,35 +15,35 @@
#define _TSL14XXSENSOR_H_
#include "plunger.h"
+#include "edgeSensor.h"
+#include "barCodeSensor.h"
#include "TSL14xx.h"
-class PlungerSensorTSL14xx: public PlungerSensor
+class PlungerSensorImageInterfaceTSL14xx: public PlungerSensorImageInterface
{
public:
- PlungerSensorTSL14xx(int nativePix, int nativeScale,
- PinName si, PinName clock, PinName ao)
- : PlungerSensor(nativeScale),
- sensor(nativePix, si, clock, ao)
+ PlungerSensorImageInterfaceTSL14xx(int nativePix, PinName si, PinName clock, PinName ao)
+ : PlungerSensorImageInterface(nativePix), sensor(nativePix, si, clock, ao)
{
- // remember the native pixel size
- native_npix = nativePix;
-
- // start with no additional integration time for automatic
- // exposure control
- axcTime = 0;
}
// is the sensor ready?
virtual bool ready() { return sensor.ready(); }
+ // is a DMA transfer in progress?
+ virtual bool dmaBusy() { return sensor.dmaBusy(); }
+
virtual void init()
{
sensor.clear();
}
- // Send a status report to the joystick interface.
- // See plunger.h for details on the arguments.
- virtual void sendStatusReport(USBJoystick &js, uint8_t flags, uint8_t extraTime)
+ // get the average sensor scan time
+ virtual uint32_t getAvgScanTime() { return sensor.getAvgScanTime(); }
+
+protected:
+ virtual void getStatusReportPixels(
+ uint8_t* &pix, uint32_t &t, int axcTime, int extraTime)
{
// The sensor's internal buffering scheme makes it a little tricky
// to get the requested timing, and our own double-buffering adds a
@@ -90,85 +90,12 @@
// wait for the DMA transfer of period B to finish, and get the
// period B pixels
- uint8_t *pix;
- uint32_t t;
sensor.waitPix(pix, t);
-
- // start a timer to measure the processing time
- Timer pt;
- pt.start();
-
- // process the pixels and read the position
- int pos, rawPos;
- int n = native_npix;
- if (process(pix, n, rawPos))
- {
- // success - apply the jitter filter
- pos = jitterFilter(rawPos);
- }
- else
- {
- // report 0xFFFF to indicate that the position wasn't read
- pos = 0xFFFF;
- rawPos = 0xFFFF;
- }
-
- // note the processing time
- uint32_t processTime = pt.read_us();
-
- // If a low-res scan is desired, reduce to a subset of pixels. Ignore
- // this for smaller sensors (below 512 pixels)
- if ((flags & 0x01) && n >= 512)
- {
- // figure how many sensor pixels we combine into each low-res pixel
- const int group = 8;
- int lowResPix = n / group;
-
- // combine the pixels
- int src, dst;
- for (src = dst = 0 ; dst < lowResPix ; ++dst)
- {
- // average this block of pixels
- int a = 0;
- for (int j = 0 ; j < group ; ++j)
- a += pix[src++];
-
- // we have the sum, so get the average
- a /= group;
-
- // store the down-res'd pixel in the array
- pix[dst] = uint8_t(a);
- }
-
- // update the pixel count to the reduced array size
- n = lowResPix;
- }
-
- // figure the report flags
- int jsflags = 0;
-
- // add flags for the detected orientation: 0x01 for normal orientation,
- // 0x02 for reversed orientation; no flags if orientation is unknown
- int dir = getOrientation();
- if (dir == 1)
- jsflags |= 0x01;
- else if (dir == -1)
- jsflags |= 0x02;
-
- // send the sensor status report headers
- js.sendPlungerStatus(n, pos, jsflags, sensor.getAvgScanTime(), processTime);
- js.sendPlungerStatus2(nativeScale, jfLo, jfHi, rawPos, axcTime);
-
- // If we're not in calibration mode, send the pixels
- extern bool plungerCalMode;
- if (!plungerCalMode)
- {
- // send the pixels in report-sized chunks until we get them all
- int idx = 0;
- while (idx < n)
- js.sendPlungerPix(idx, n, pix);
- }
-
+ }
+
+ // reset after a status report
+ virtual void resetAfterStatusReport(int axcTime)
+ {
// It takes us a while to send all of the pixels, since we have
// to break them up into many USB reports. This delay means that
// the sensor has been sitting there integrating for much longer
@@ -179,39 +106,9 @@
sensor.clear();
sensor.startCapture(axcTime);
}
-
- // get the average sensor scan time
- virtual uint32_t getAvgScanTime() { return sensor.getAvgScanTime(); }
-
-protected:
- // Analyze the image and find the plunger position. If successful,
- // fills in 'pixpos' with the plunger position using the 0..65535
- // scale and returns true. If no position can be detected from the
- // image data, returns false.
- virtual bool process(const uint8_t *pix, int npix, int &pixpos) = 0;
-
- // Get the currently detected sensor orientation, if applicable.
- // Returns 1 for standard orientation, -1 for reversed orientation,
- // or 0 for orientation unknown or not applicable. Edge sensors can
- // automatically detect orientation by observing which side of the
- // image is in shadow. Bar code sensors generally can't detect
- // orientation.
- virtual int getOrientation() const { return 0; }
-
+
// the low-level interface to the TSL14xx sensor
TSL14xx sensor;
-
- // number of pixels
- int native_npix;
-
- // Automatic exposure control time, in microseconds. This is an amount
- // of time we add to each integration cycle to compensate for low light
- // levels. By default, this is always zero; the base class doesn't have
- // any logic for determining proper exposure, because that's a function
- // of the type of image we're looking for. Subclasses can add logic in
- // the process() function to check exposure level and adjust this value
- // if the image looks over- or under-exposed.
- uint32_t axcTime;
};
// ---------------------------------------------------------------------
@@ -245,42 +142,23 @@
// but that would complicate things considerably since our image
// analysis is too time-consuming to do in interrupt context.
//
-class PlungerSensorTSL14xxLarge: public PlungerSensorTSL14xx
+class PlungerSensorTSL14xxLarge: public PlungerSensorImageInterfaceTSL14xx
{
public:
- PlungerSensorTSL14xxLarge(int nativePix, int nativeScale,
- PinName si, PinName clock, PinName ao)
- : PlungerSensorTSL14xx(nativePix, nativeScale, si, clock, ao)
+ PlungerSensorTSL14xxLarge(int nativePix, PinName si, PinName clock, PinName ao)
+ : PlungerSensorImageInterfaceTSL14xx(nativePix, si, clock, ao)
{
}
- // read the plunger position
- virtual bool readRaw(PlungerReading &r)
- {
+ virtual void readPix(uint8_t* &pix, uint32_t &t, int axcTime)
+ {
// start reading the next pixel array (this waits for any DMA
// transfer in progress to finish, ensuring a stable pixel buffer)
sensor.startCapture(axcTime);
// get the image array from the last capture
- uint8_t *pix;
- uint32_t tpix;
- sensor.getPix(pix, tpix);
+ sensor.getPix(pix, t);
- // process the pixels
- int pixpos;
- if (process(pix, native_npix, pixpos))
- {
- r.pos = pixpos;
- r.t = tpix;
-
- // success
- return true;
- }
- else
- {
- // no position found
- return false;
- }
}
};
@@ -307,17 +185,16 @@
// transferring period A's pixels into a DMA buffer. We want
// those period A pixels, so we wait for this transfer to finish.
//
-class PlungerSensorTSL14xxSmall: public PlungerSensorTSL14xx
+class PlungerSensorTSL14xxSmall: public PlungerSensorImageInterfaceTSL14xx
{
public:
- PlungerSensorTSL14xxSmall(int nativePix, int nativeScale,
- PinName si, PinName clock, PinName ao)
- : PlungerSensorTSL14xx(nativePix, nativeScale, si, clock, ao)
+ PlungerSensorTSL14xxSmall(int nativePix, PinName si, PinName clock, PinName ao)
+ : PlungerSensorImageInterfaceTSL14xx(nativePix, si, clock, ao)
{
}
- // read the plunger position
- virtual bool readRaw(PlungerReading &r)
+ // read the image
+ virtual void readPix(uint8_t* &pix, uint32_t &t, int axcTime)
{
// Clear the sensor. This sends a HOLD/SI pulse to the sensor,
// which ends the current integration period, starts a new one
@@ -339,27 +216,55 @@
// wait for the period A pixel transfer to finish, and grab
// its pixels
- uint8_t *pix;
- uint32_t tpix;
- sensor.waitPix(pix, tpix);
-
- // process the pixels
- int pixpos;
- if (process(pix, native_npix, pixpos))
- {
- r.pos = pixpos;
- r.t = tpix;
-
- // success
- return true;
- }
- else
- {
- // no position found
- return false;
- }
+ sensor.waitPix(pix, t);
}
};
+// -------------------------------------------------------------------------
+//
+// Concrete TSL14xx sensor types
+//
+
+
+// TSL1410R sensor - edge detection sensor
+class PlungerSensorTSL1410R: public PlungerSensorEdgePos
+{
+public:
+ PlungerSensorTSL1410R(PinName si, PinName clock, PinName ao)
+ : PlungerSensorEdgePos(sensor, 1280), sensor(1280, si, clock, ao)
+ {
+ }
+
+protected:
+ PlungerSensorTSL14xxLarge sensor;
+};
+
+// TSL1412R - edge detection sensor
+class PlungerSensorTSL1412R: public PlungerSensorEdgePos
+{
+public:
+ PlungerSensorTSL1412R(PinName si, PinName clock, PinName ao)
+ : PlungerSensorEdgePos(sensor, 1536), sensor(1536, si, clock, ao)
+ {
+ }
+
+protected:
+ PlungerSensorTSL14xxLarge sensor;
+};
+
+// TSL1401CL - bar code sensor
+class PlungerSensorTSL1401CL: public PlungerSensorBarCode<7, 0, 1, 16>
+{
+public:
+ PlungerSensorTSL1401CL(PinName si, PinName clock, PinName ao)
+ : PlungerSensorBarCode(sensor, 128), sensor(128, si, clock, ao)
+ {
+ }
+
+protected:
+ PlungerSensorTSL14xxSmall sensor;
+};
+
+
#endif
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/TLC59116/TLC59116.h Tue May 09 05:48:37 2017 +0000
@@ -0,0 +1,473 @@
+// TLC59116 interface
+//
+// The TLC59116 is a 16-channel constant-current PWM controller chip with
+// an I2C interface.
+//
+// Up to 14 of these chips can be connected to a single bus. Each chip needs
+// a unique address, configured via four pin inputs. (The I2C address is 7
+// bits, but the high-order 3 bits are fixed in the hardware, leaving 4 bits
+// to configure per chip. Two of the possible 16 addresses are reserved by
+// the chip hardware as broadcast addresses, leaving room for 14 unique chip
+// addresses per bus.)
+//
+// EXTERNAL PULL-UP RESISTORS ARE REQUIRED ON SDA AND SCL. The internal
+// pull-ups in the KL25Z GPIO ports will only work if the bus speed is
+// limited to 100kHz. Higher speeds require external pull-ups. Because
+// of the relatively high data rate required, we use the maximum 1MHz bus
+// speed, requiring external pull-ups. These are typically 2.2K.
+//
+// This chip is similar to the TLC5940, but has a more modern design with
+// several advantages, including a standardized and much more robust data
+// interface (I2C) and glitch-free startup. The only downside vs the TLC5940
+// is that it's only available in an SMD package, whereas the TLC5940 is
+// available in easy-to-solder DIP format. The DIP 5940 is longer being
+// manufactured, but it's still easy to find old stock; when those run out,
+// though, and the choice is between SMD 5940 and 59116, the 59116 will be
+// the clear winner.
+//
+
+#ifndef _TLC59116_H_
+#define _TLC59116_H_
+
+#include "mbed.h"
+#include "BitBangI2C.h"
+
+// Which I2C class are we using? We use this to switch between
+// BitBangI2C and MbedI2C for testing and debugging.
+#define I2C_Type BitBangI2C
+
+// register constants
+struct TLC59116R
+{
+ // control register bits
+ static const uint8_t CTL_AIALL = 0x80; // auto-increment mode, all registers
+ static const uint8_t CTL_AIPWM = 0xA0; // auto-increment mode, PWM registers only
+ static const uint8_t CTL_AICTL = 0xC0; // auto-increment mode, control registers only
+ static const uint8_t CTL_AIPWMCTL = 0xE0; // auto-increment mode, PWM + control registers only
+
+ // register addresses
+ static const uint8_t REG_MODE1 = 0x00; // MODE1
+ static const uint8_t REG_MODE2 = 0x01; // MODE2
+ static const uint8_t REG_PWM0 = 0x02; // PWM 0
+ static const uint8_t REG_PWM1 = 0x03; // PWM 1
+ static const uint8_t REG_PWM2 = 0x04; // PWM 2
+ static const uint8_t REG_PWM3 = 0x05; // PWM 3
+ static const uint8_t REG_PWM4 = 0x06; // PWM 4
+ static const uint8_t REG_PWM5 = 0x07; // PWM 5
+ static const uint8_t REG_PWM6 = 0x08; // PWM 6
+ static const uint8_t REG_PWM7 = 0x09; // PWM 7
+ static const uint8_t REG_PWM8 = 0x0A; // PWM 8
+ static const uint8_t REG_PWM9 = 0x0B; // PWM 9
+ static const uint8_t REG_PWM10 = 0x0C; // PWM 10
+ static const uint8_t REG_PWM11 = 0x0D; // PWM 11
+ static const uint8_t REG_PWM12 = 0x0E; // PWM 12
+ static const uint8_t REG_PWM13 = 0x0F; // PWM 13
+ static const uint8_t REG_PWM14 = 0x10; // PWM 14
+ static const uint8_t REG_PWM15 = 0x11; // PWM 15
+ static const uint8_t REG_GRPPWM = 0x12; // Group PWM duty cycle
+ static const uint8_t REG_GRPFREQ = 0x13; // Group frequency register
+ static const uint8_t REG_LEDOUT0 = 0x14; // LED driver output status register 0
+ static const uint8_t REG_LEDOUT1 = 0x15; // LED driver output status register 1
+ static const uint8_t REG_LEDOUT2 = 0x16; // LED driver output status register 2
+ static const uint8_t REG_LEDOUT3 = 0x17; // LED driver output status register 3
+
+ // MODE1 bits
+ static const uint8_t MODE1_AI2 = 0x80; // auto-increment mode enable
+ static const uint8_t MODE1_AI1 = 0x40; // auto-increment bit 1
+ static const uint8_t MODE1_AI0 = 0x20; // auto-increment bit 0
+ static const uint8_t MODE1_OSCOFF = 0x10; // oscillator off
+ static const uint8_t MODE1_SUB1 = 0x08; // subaddress 1 enable
+ static const uint8_t MODE1_SUB2 = 0x04; // subaddress 2 enable
+ static const uint8_t MODE1_SUB3 = 0x02; // subaddress 3 enable
+ static const uint8_t MODE1_ALLCALL = 0x01; // all-call enable
+
+ // MODE2 bits
+ static const uint8_t MODE2_EFCLR = 0x80; // clear error status flag
+ static const uint8_t MODE2_DMBLNK = 0x20; // group blinking mode
+ static const uint8_t MODE2_OCH = 0x08; // outputs change on ACK (vs Stop command)
+
+ // LEDOUTn states
+ static const uint8_t LEDOUT_OFF = 0x00; // driver is off
+ static const uint8_t LEDOUT_ON = 0x01; // fully on
+ static const uint8_t LEDOUT_PWM = 0x02; // individual PWM control via PWMn register
+ static const uint8_t LEDOUT_GROUP = 0x03; // PWM control + group dimming/blinking via PWMn + GRPPWM
+};
+
+
+// Individual unit object. We create one of these for each unit we
+// find on the bus. This keeps track of the state of each output on
+// a unit so that we can update outputs in batches, to reduce the
+// amount of time we spend in I2C communications during rapid updates.
+struct TLC59116Unit
+{
+ TLC59116Unit()
+ {
+ // start inactive, since we haven't been initialized yet
+ active = false;
+
+ // set all brightness levels to 0 intially
+ memset(bri, 0, sizeof(bri));
+
+ // mark all outputs as dirty to force an update after initializing
+ dirty = 0xFFFF;
+ }
+
+ // initialize
+ void init(int addr, I2C_Type &i2c)
+ {
+ // set all output drivers to individual PWM control
+ const uint8_t all_pwm =
+ TLC59116R::LEDOUT_PWM
+ | (TLC59116R::LEDOUT_PWM << 2)
+ | (TLC59116R::LEDOUT_PWM << 4)
+ | (TLC59116R::LEDOUT_PWM << 6);
+ static const uint8_t buf[] = {
+ TLC59116R::REG_LEDOUT0 | TLC59116R::CTL_AIALL,
+ all_pwm,
+ all_pwm,
+ all_pwm,
+ all_pwm
+ };
+ int err = i2c.write(addr << 1, buf, sizeof(buf));
+
+ // turn on the oscillator
+ static const uint8_t buf2[] = {
+ TLC59116R::REG_MODE1,
+ TLC59116R::MODE1_AI2 | TLC59116R::MODE1_ALLCALL
+ };
+ err |= i2c.write(addr << 1, buf2, sizeof(buf));
+
+ // mark the unit as active if the writes succeeded
+ active = !err;
+ }
+
+ // Set an output
+ void set(int idx, int val)
+ {
+ // validate the index
+ if (idx >= 0 && idx <= 15)
+ {
+ // record the new brightness
+ bri[idx] = val;
+
+ // set the dirty bit
+ dirty |= 1 << idx;
+ }
+ }
+
+ // Get an output's current value
+ int get(int idx) const
+ {
+ return idx >= 0 && idx <= 15 ? bri[idx] : -1;
+ }
+
+ // Send I2C updates
+ void send(int addr, I2C_Type &i2c)
+ {
+ // Scan all outputs. I2C sends are fairly expensive, so we
+ // minimize the send time by using the auto-increment mode.
+ // Optimizing this is a bit tricky. Suppose that the outputs
+ // are in this state, where c represents a clean output and D
+ // represents a dirty output:
+ //
+ // cccDcDccc...
+ //
+ // Clearly we want to start sending at the first dirty output
+ // so that we don't waste time sending the three clean bytes
+ // ahead of it. However, do we send output[3] as one chunk
+ // and then send output[5] as a separate chunk, or do we send
+ // outputs [3],[4],[5] as a single block to take advantage of
+ // the auto-increment mode? Based on I2C bus timing parameters,
+ // the answer is that it's cheaper to send this as a single
+ // contiguous block [3],[4],[5]. The reason is that the cost
+ // of starting a new block is a Stop/Start sequence plus another
+ // register address byte; the register address byte costs the
+ // same as a data byte, so the extra Stop/Start of the separate
+ // chunk approach makes the single continguous send cheaper.
+ // But how about this one?:
+ //
+ // cccDccDccc...
+ //
+ // This one is cheaper to send as two separate blocks. The
+ // break costs us a Start/Stop plus a register address byte,
+ // but the Start/Stop is only about 25% of the cost of a data
+ // byte, so Start/Stop+Register Address is cheaper than sending
+ // the two clean data bytes sandwiched between the dirty bytes.
+ //
+ // So: we want to look for sequences of contiguous dirty bytes
+ // and send those as a chunk. We furthermore will allow up to
+ // one clean byte in the midst of the dirty bytes.
+ uint8_t buf[17];
+ int n = 0;
+ for (int i = 0, bit = 1 ; i < 16 ; ++i, bit <<= 1)
+ {
+ // If this one is dirty, include it in the set of outputs to
+ // send to the chip. Also include this one if it's clean
+ // and the outputs on both sides are dirty - see the notes
+ // above about optimizing for the case where we have one clean
+ // output surrounded by dirty outputs.
+ if ((dirty & bit) != 0)
+ {
+ // it's dirty - add it to the dirty set under construction
+ buf[++n] = bri[i];
+ }
+ else if (n != 0 && n < 15 && (dirty & (bit << 1)) != 0)
+ {
+ // this one is clean, but the one before and the one after
+ // are both dirty, so keep it in the set anyway to take
+ // advantage of the auto-increment mode for faster sends
+ buf[++n] = bri[i];
+ }
+ else
+ {
+ // This one is clean, and it's not surrounded by dirty
+ // outputs. If the set of dirty outputs so far has any
+ // members, send them now.
+ if (n != 0)
+ {
+ // set the starting register address, including the
+ // auto-increment flag, and write the block
+ buf[0] = (TLC59116R::REG_PWM0 + i - n) | TLC59116R::CTL_AIALL;
+ i2c.write(addr << 1, buf, n + 1);
+
+ // empty the set
+ n = 0;
+ }
+ }
+ }
+
+ // if we finished the loop with dirty outputs to send, send them
+ if (n != 0)
+ {
+ // fill in the starting register address, and write the block
+ buf[0] = (TLC59116R::REG_PWM15 + 1 - n) | TLC59116R::CTL_AIALL;
+ i2c.write(addr << 1, buf, n + 1);
+ }
+
+ // all outputs are now clean
+ dirty = 0;
+ }
+
+ // Is the unit active? If we have trouble writing a unit,
+ // we can mark it inactive so that we know to stop wasting
+ // time writing to it, and so that we can re-initialize it
+ // if it comes back on later bus scans.
+ bool active;
+
+ // Output states. This records the latest brightness level
+ // for each output as set by the client. We don't actually
+ // send these values to the physical unit until the client
+ // tells us to do an I2C update.
+ uint8_t bri[16];
+
+ // Dirty output mask. Whenever the client changes an output,
+ // we record the new brightness in bri[] and set the
+ // corresponding bit here to 1. We use these bits to determine
+ // which outputs to send during each I2C update.
+ uint16_t dirty;
+};
+
+// TLC59116 public interface. This provides control over a collection
+// of units connected on a common I2C bus.
+class TLC59116
+{
+public:
+ // Initialize. The address given is the configurable part
+ // of the address, 0x0000 to 0x000F.
+ TLC59116(PinName sda, PinName scl, PinName reset)
+ : i2c(sda, scl, true), reset(reset)
+ {
+ // Use the fastest I2C speed possible, since we want to be able
+ // to rapidly update many outputs at once. The TLC59116 can run
+ // I2C at up to 1MHz.
+ i2c.frequency(1000000);
+
+ // assert !RESET until we're ready to go
+ this->reset.write(0);
+
+ // there are no units yet
+ memset(units, 0, sizeof(units));
+ nextUpdate = 0;
+ }
+
+ void init()
+ {
+ // un-assert reset
+ reset.write(1);
+ wait_us(10000);
+
+ // scan the bus for new units
+ scanBus();
+ }
+
+ // scan the bus
+ void scanBus()
+ {
+ // scan each possible address
+ for (int i = 0 ; i < 16 ; ++i)
+ {
+ // Address 8 and 11 are reserved - skip them
+ if (i == 8 || i == 11)
+ continue;
+
+ // Try reading register REG_MODE1
+ int addr = I2C_BASE_ADDR | i;
+ TLC59116Unit *u = units[i];
+ if (readReg8(addr, TLC59116R::REG_MODE1) >= 0)
+ {
+ // success - if the slot wasn't already populated, allocate
+ // a unit entry for it
+ if (u == 0)
+ units[i] = u = new TLC59116Unit();
+
+ // if the unit isn't already marked active, initialize it
+ if (!u->active)
+ u->init(addr, i2c);
+ }
+ else
+ {
+ // failed - if the unit was previously active, mark it
+ // as inactive now
+ if (u != 0)
+ u->active = false;
+ }
+ }
+ }
+
+ // set an output
+ void set(int unit, int output, int val)
+ {
+ if (unit >= 0 && unit <= 15)
+ {
+ TLC59116Unit *u = units[unit];
+ if (u != 0)
+ u->set(output, val);
+ }
+ }
+
+ // get an output's current value
+ int get(int unit, int output)
+ {
+ if (unit >= 0 && unit <= 15)
+ {
+ TLC59116Unit *u = units[unit];
+ if (u != 0)
+ return u->get(output);
+ }
+
+ return -1;
+ }
+
+ // Send I2C updates to the next unit. The client must call this
+ // periodically to send pending updates. We only update one unit on
+ // each call to ensure that the time per cycle is relatively constant
+ // (rather than scaling with the number of chips).
+ void send()
+ {
+ // look for a dirty unit
+ for (int i = 0, n = nextUpdate ; i < 16 ; ++i, ++n)
+ {
+ // wrap the unit number
+ n &= 0x0F;
+
+ // if this unit is populated and dirty, it's the one to update
+ TLC59116Unit *u = units[n];
+ if (u != 0 && u->dirty != 0)
+ {
+ // it's dirty - update it
+ u->send(I2C_BASE_ADDR | n, i2c);
+
+ // We only update one on each call, so we're done.
+ // Remember where to pick up again on the next update()
+ // call, and return.
+ nextUpdate = n + 1;
+ return;
+ }
+ }
+ }
+
+ // Enable/disable all outputs
+ void enable(bool f)
+ {
+ // visit each populated unit
+ for (int i = 0 ; i < 16 ; ++i)
+ {
+ // if this unit is populated, enable/disable it
+ TLC59116Unit *u = units[i];
+ if (u != 0)
+ {
+ // read the current MODE1 register
+ int m = readReg8(I2C_BASE_ADDR | i, TLC59116R::REG_MODE1);
+ if (m >= 0)
+ {
+ // Turn the oscillator off to disable, on to enable.
+ // Note that the bit is kind of backwards: SETTING the
+ // OSC bit turns the oscillator OFF.
+ if (f)
+ m &= ~TLC59116R::MODE1_OSCOFF; // enable - clear the OSC bit
+ else
+ m |= TLC59116R::MODE1_OSCOFF; // disable - set the OSC bit
+
+ // update MODE1
+ writeReg8(I2C_BASE_ADDR | i, TLC59116R::REG_MODE1, m);
+ }
+ }
+ }
+ }
+
+protected:
+ // TLC59116 base I2C address. These chips use an address of
+ // the form 110xxxx, where the the low four bits are set by
+ // external pins on the chip. The top three bits are always
+ // the same, so we construct the full address by combining
+ // the upper three fixed bits with the four-bit unit number.
+ //
+ // Note that addresses 1101011 (0x6B) and 1101000 (0x68) are
+ // reserved (for SWRSTT and ALLCALL, respectively), and can't
+ // be used for configured device addresses.
+ static const uint8_t I2C_BASE_ADDR = 0x60;
+
+ // Units. We populate this with active units we find in
+ // bus scans. Note that units 8 and 11 can't be used because
+ // of the reserved ALLCALL and SWRST addresses, but we allocate
+ // the slots anyway to keep indexing simple.
+ TLC59116Unit *units[16];
+
+ // next unit to update
+ int nextUpdate;
+
+ // read 8-bit register; returns the value read on success, -1 on failure
+ int readReg8(int addr, uint16_t registerAddr)
+ {
+ // write the request - register address + auto-inc mode
+ uint8_t data_write[1];
+ data_write[0] = registerAddr | TLC59116R::CTL_AIALL;
+ if (i2c.write(addr << 1, data_write, 1, true))
+ return -1;
+
+ // read the result
+ uint8_t data_read[1];
+ if (i2c.read(addr << 1, data_read, 1))
+ return -1;
+
+ // return the result
+ return data_read[0];
+ }
+
+ // write 8-bit register; returns true on success, false on failure
+ bool writeReg8(int addr, uint16_t registerAddr, uint8_t data)
+ {
+ uint8_t data_write[2];
+ data_write[0] = registerAddr | TLC59116R::CTL_AIALL;
+ data_write[1] = data;
+ return !i2c.write(addr << 1, data_write, 2);
+ }
+
+ // I2C bus interface
+ I2C_Type i2c;
+
+ // reset pin (active low)
+ DigitalOut reset;
+};
+
+#endif
--- a/TLC5940/TLC5940.h Fri Apr 21 18:50:37 2017 +0000
+++ b/TLC5940/TLC5940.h Tue May 09 05:48:37 2017 +0000
@@ -392,16 +392,6 @@
}
}
- // Update the outputs. In our current implementation, this doesn't do
- // anything, since we send the current state to the chips on every grayscale
- // cycle, whether or not there are updates. We provide the interface for
- // consistency with other peripheral device interfaces in the main loop,
- // and in case we make any future implementation changes that require some
- // action to carry out an explicit update.
- void update(bool force = false)
- {
- }
-
// Send updates if ready. Our top-level program's main loop calls this on
// every iteration. This lets us send grayscale updates to the chips in
// regular application context (rather than in interrupt context), to keep
--- a/TSL14xx/TSL14xx.h Fri Apr 21 18:50:37 2017 +0000
+++ b/TSL14xx/TSL14xx.h Tue May 09 05:48:37 2017 +0000
@@ -480,6 +480,9 @@
// Is the latest reading ready?
bool ready() const { return !running; }
+
+ // Is a DMA transfer in progress?
+ bool dmaBusy() const { return running; }
// Clock through all pixels to clear the array. Pulses SI at the
// beginning of the operation, which starts a new integration cycle.
--- a/USBJoystick/USBJoystick.cpp Fri Apr 21 18:50:37 2017 +0000
+++ b/USBJoystick/USBJoystick.cpp Tue May 09 05:48:37 2017 +0000
@@ -94,7 +94,7 @@
}
bool USBJoystick::sendPlungerStatus(
- int npix, int edgePos, int flags, uint32_t avgScanTime, uint32_t processingTime)
+ int npix, int plungerPos, int flags, uint32_t avgScanTime, uint32_t processingTime)
{
HID_REPORT report;
@@ -112,8 +112,8 @@
put(ofs, uint16_t(npix));
ofs += 2;
- // write the shadow edge position to bytes 5-6
- put(ofs, uint16_t(edgePos));
+ // write the detected plunger position to bytes 5-6
+ put(ofs, uint16_t(plungerPos));
ofs += 2;
// Add the calibration mode flag if applicable
@@ -191,6 +191,45 @@
return sendTO(&report, 100);
}
+bool USBJoystick::sendPlungerStatusBarcode(
+ int nbits, int codetype, int startOfs, int pixPerBit, int raw, int mask)
+{
+ HID_REPORT report;
+ memset(report.data, 0, sizeof(report.data));
+
+ // Set the special status bits to indicate it's an extended
+ // exposure report.
+ put(0, 0x87FF);
+
+ // start at the second byte
+ int ofs = 2;
+
+ // write the report subtype (2) to byte 2
+ report.data[ofs++] = 2;
+
+ // write the bit count and code type
+ report.data[ofs++] = nbits;
+ report.data[ofs++] = codetype;
+
+ // write the bar code starting pixel offset
+ put(ofs, uint16_t(startOfs));
+ ofs += 2;
+
+ // write the pixel width per bit
+ report.data[ofs++] = pixPerBit;
+
+ // write the raw bar code and success bit mask
+ put(ofs, uint16_t(raw));
+ ofs += 2;
+ put(ofs, uint16_t(mask));
+ ofs += 2;
+
+ // send the report
+ report.length = reportLen;
+ return sendTO(&report, 100);
+}
+
+
bool USBJoystick::sendPlungerPix(int &idx, int npix, const uint8_t *pix)
{
HID_REPORT report;
--- a/USBJoystick/USBJoystick.h Fri Apr 21 18:50:37 2017 +0000
+++ b/USBJoystick/USBJoystick.h Tue May 09 05:48:37 2017 +0000
@@ -176,6 +176,18 @@
*/
bool sendPlungerStatus2(
int nativeScale, int jitterLo, int jitterHi, int rawPos, int axcTime);
+
+ /**
+ * Send a barcode plunger status report header.
+ *
+ * @param nbits number of bits in bar code
+ * @param codetype bar code type (1=Gray code/Manchester bit coding)
+ * @param pixofs pixel offset of first bit
+ * @param raw raw bar code bits
+ * @param mask mask of successfully read bar code bits
+ */
+ bool sendPlungerStatusBarcode(
+ int nbits, int codetype, int startOfs, int pixPerBit, int raw, int mask);
/**
* Write an exposure report. We'll fill out a report with as many pixels as
--- a/USBProtocol.h Fri Apr 21 18:50:37 2017 +0000 +++ b/USBProtocol.h Tue May 09 05:48:37 2017 +0000 @@ -172,6 +172,21 @@ // status as of this reading. // bytes 9:10 = Raw sensor reading before jitter filter was applied. // bytes 11:12 = Auto-exposure time in microseconds +// +// An optional third message provides additional information specifically +// for bar-code sensors: +// +// bytes 0:1 = 0x87FF +// byte 2 = 2 -> bar code status report +// byte 3 = number of bits in bar code +// byte 4 = bar code type: +// 1 = Gray code/Manchester bit coding +// bytes 5:6 = pixel offset of first bit +// byte 7 = width in pixels of each bit +// bytes 8:9 = raw bar code bits +// bytes 10:11 = mask of successfully read bar code bits; a '1' bit means +// that the bit was read successfully, '0' means the bit was +// unreadable // // // If the sensor is an imaging sensor type, this will be followed by a @@ -182,7 +197,7 @@ // zero, so obviously no pixel packets will follow. If the "calibration // active" bit in the flags byte is set, no pixel packets are sent even // if the sensor is an imaging type, since the transmission time for the -// pixels would intefere with the calibration process. If pixels are sent, +// pixels would interfere with the calibration process. If pixels are sent, // they're sent in order starting at the first pixel. The format of each // pixel packet is: // @@ -898,7 +913,7 @@ // finishes. This allows TVs to be turned on via IR remotes codes rather than // hard-wiring them through the relay. The relay can be omitted in this case. // -// 10 -> TLC5940NT setup. This chip is an external PWM controller, with 32 outputs +// 10 -> TLC5940NT setup. This chip is an external PWM controller, with 16 outputs // per chip and a serial data interface that allows the chips to be daisy- // chained. We can use these chips to add an arbitrary number of PWM output // ports for the LedWiz emulation. @@ -1072,6 +1087,44 @@ // // byte 3:4 = window size in joystick units, little-endian // +// 20 -> Plunger bar code setup. Sets parameters applicable only to bar code +// sensor types. +// +// bytes 3:4 = Starting pixel offset of bar code (margin width) +// +// 21 -> TLC59116 setup. This chip is an external PWM controller with 16 +// outputs per chip and an I2C bus interface. Up to 14 of the chips +// can be connected to a single bus. This chip is a successor to the +// TLC5940 with a more modern design and some nice improvements, such +// as glitch-free startup and a standard (I2C) physical interface. +// +// Each chip has a 7-bit I2C address. The top three bits of the +// address are fixed in the chip itself and can't be configured, but +// the low four bits are configurable via the address line pins on +// the chip, A3 A2 A1 A0. Our convention here is to ignore the fixed +// three bits and refer to the chip address as just the A3 A2 A1 A0 +// bits. This gives each chip an address from 0 to 15. +// +// I2C allows us to discover the attached chips automatically, so in +// principle we don't need to know which chips will be present. +// However, it's useful for the config tool to know which chips are +// expected so that it can offer them in the output port setup UI. +// We therefore provide a bit mask specifying the enabled chips. Each +// bit specifies whether the chip at the corresponding address is +// present: 0x0001 is the chip at address 0, 0x0002 is the chip at +// address 1, etc. This is mostly for the config tool's use; we only +// use it to determine if TLC59116 support should be enabled at all, +// by checking if it's non-zero. +// +// To disable support, set the populated chip mask to 0. The pin +// assignments are all ignored in this case. +// +// bytes 3:4 = populated chips, as a bit mask (OR in 1<<address +// each populated address) +// byte 5 = SDA (any GPIO pin) +// byte 6 = SCL (any GPIO pin) +// byte 7 = RESET (any GPIO pin) +// // // SPECIAL DIAGNOSTICS VARIABLES: These work like the array variables below, // the only difference being that we don't report these in the number of array @@ -1225,42 +1278,63 @@ // regardless of the settings for post 34 and higher. // // The bytes of the message are: +// // byte 3 = LedWiz port number (1 to MAX_OUT_PORTS) +// // byte 4 = physical output type: +// // 0 = Disabled. This output isn't used, and isn't visible to the // LedWiz/DOF software on the host. The FIRST disabled port // determines the number of ports visible to the host - ALL ports // after the first disabled port are also implicitly disabled. +// // 1 = GPIO PWM output: connected to GPIO pin specified in byte 5, // operating in PWM mode. Note that only a subset of KL25Z GPIO // ports are PWM-capable. +// // 2 = GPIO Digital output: connected to GPIO pin specified in byte 5, // operating in digital mode. Digital ports can only be set ON // or OFF, with no brightness/intensity control. All pins can be // used in this mode. +// // 3 = TLC5940 port: connected to TLC5940 output port number specified // in byte 5. Ports are numbered sequentially starting from port 0 // for the first output (OUT0) on the first chip in the daisy chain. +// // 4 = 74HC595 port: connected to 74HC595 output port specified in byte 5. // As with the TLC5940 outputs, ports are numbered sequentially from 0 // for the first output on the first chip in the daisy chain. +// // 5 = Virtual output: this output port exists for the purposes of the // LedWiz/DOF software on the host, but isn't physically connected // to any output device. This can be used to create a virtual output // for the DOF ZB Launch Ball signal, for example, or simply as a // placeholder in the LedWiz port numbering. The physical output ID // (byte 5) is ignored for this port type. +// +// 6 = TLC59116 output: connected to the TLC59116 output port specified +// in byte 5. The high four bits of this value give the chip's +// I2C address, specifically the A3 A2 A1 A0 bits configured in +// the hardware. (A chip's I2C address is actually 7 bits, but +// the three high-order bits are fixed, so we don't bother including +// those in the byte 5 value). The low four bits of this value +// give the output port number on the chip. For example, 0x37 +// specifies chip 3 (the one with A3 A2 A1 A0 wired as 0 0 1 1), +// output #7 on that chip. Note that outputs are numbered from 0 +// to 15 (0xF) on each chip. +// // byte 5 = physical output port, interpreted according to the value in byte 4 +// // byte 6 = flags: a combination of these bit values: // 0x01 = active-high output (0V on output turns attached device ON) // 0x02 = noisemaker device: disable this output when "night mode" is engaged // 0x04 = apply gamma correction to this output // -// Note that the on-board LED segments can be used as LedWiz output ports. This +// Note that the KL25Z's on-board LEDs 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. +// having to connect any external devices. Assigning the on-board LEDs as output +// ports overrides their normal status/diagnostic display use, so the normal status +// flash pattern won't appear when they're used this way. //
--- a/VL6180X/VL6180X.cpp Fri Apr 21 18:50:37 2017 +0000
+++ b/VL6180X/VL6180X.cpp Tue May 09 05:48:37 2017 +0000
@@ -3,16 +3,19 @@
#include "mbed.h"
#include "VL6180X.h"
-VL6180X::VL6180X(PinName sda, PinName scl, uint8_t addr, PinName gpio0)
- : i2c(sda, scl), gpio0Pin(gpio0)
+VL6180X::VL6180X(PinName sda, PinName scl, uint8_t addr, PinName gpio0,
+ bool internalPullups)
+ : i2c(sda, scl, internalPullups), gpio0Pin(gpio0)
{
// remember the address
this->addr = addr;
// start in single-shot distance mode
distMode = 0;
+ rangeStarted = false;
- // initially reset the sensor
+ // initially reset the sensor by holding GPIO0/CE low
+ gpio0Pin.mode(PullNone);
gpio0Pin.output();
gpio0Pin.write(0);
}
@@ -28,10 +31,10 @@
gpio0Pin.write(0);
wait_us(10000);
- // release reset to allow the sensor to reboot
+ // release reset and allow 10ms for the sensor to reboot
gpio0Pin.input();
wait_us(10000);
-
+
// reset the I2C bus
i2c.reset();
@@ -40,7 +43,7 @@
t.start();
while (readReg8(VL6180X_SYSTEM_FRESH_OUT_OF_RESET) != 1)
{
- if (t.read_us() > 10000000)
+ if (t.read_us() > 1000000)
return false;
}
@@ -85,7 +88,10 @@
// allow time to settle
wait_us(1000);
-
+
+ // start the sample timer
+ sampleTimer.start();
+
// success
return true;
}
@@ -94,20 +100,16 @@
{
writeReg8(VL6180X_SYSTEM_GROUPED_PARAMETER_HOLD, 0x01); // set parameter hold while updating settings
- writeReg8(VL6180X_SYSTEM_INTERRUPT_CONFIG_GPIO, (4<<3) | 4); // Enable interrupts from range and ambient integrator
- writeReg8(VL6180X_SYSTEM_MODE_GPIO1, 0x10); // Set GPIO1 low when sample complete
+ writeReg8(VL6180X_SYSTEM_INTERRUPT_CONFIG_GPIO, 4); // Enable interrupts from range only
+ writeReg8(VL6180X_SYSTEM_MODE_GPIO1, 0x00); // Disable GPIO1
writeReg8(VL6180X_SYSRANGE_VHV_REPEAT_RATE, 0xFF); // Set auto calibration period (Max = 255)/(OFF = 0)
writeReg8(VL6180X_SYSRANGE_INTERMEASUREMENT_PERIOD, 0x09); // Set default ranging inter-measurement period to 100ms
- writeReg8(VL6180X_SYSRANGE_MAX_CONVERGENCE_TIME, 0x32); // Max range convergence time 48ms
- writeReg8(VL6180X_SYSRANGE_RANGE_CHECK_ENABLES, 0x11); // S/N enable, ignore disable, early convergence test enable
- writeReg16(VL6180X_SYSRANGE_EARLY_CONVERGENCE_ESTIMATE, 0x7B); // abort range measurement if convergence rate below this value
-
- writeReg8(VL6180X_SYSALS_INTERMEASUREMENT_PERIOD, 0x0A); // Set default ALS inter-measurement period to 100ms
- writeReg8(VL6180X_SYSALS_ANALOGUE_GAIN, 0x46); // Set the ALS gain
- writeReg16(VL6180X_SYSALS_INTEGRATION_PERIOD, 0x63); // ALS integration time 100ms
-
- writeReg8(VL6180X_READOUT_AVERAGING_SAMPLE_PERIOD, 0x30); // Sample averaging period (1.3ms + N*64.5us)
- writeReg8(VL6180X_FIRMWARE_RESULT_SCALER, 0x01);
+ writeReg8(VL6180X_SYSRANGE_MAX_CONVERGENCE_TIME, 63); // Max range convergence time 63ms
+ writeReg8(VL6180X_SYSRANGE_RANGE_CHECK_ENABLES, 0x00); // S/N disable, ignore disable, early convergence test disable
+ writeReg16(VL6180X_SYSRANGE_EARLY_CONVERGENCE_ESTIMATE, 0x00); // abort range measurement if convergence rate below this value
+ writeReg8(VL6180X_READOUT_AVERAGING_SAMPLE_PERIOD, averagingSamplePeriod); // Sample averaging period (1.3ms + N*64.5us)
+ writeReg8(VL6180X_SYSRANGE_THRESH_LOW, 0x00); // low threshold
+ writeReg8(VL6180X_SYSRANGE_THRESH_HIGH, 0xff); // high threshold
writeReg8(VL6180X_SYSTEM_GROUPED_PARAMETER_HOLD, 0x00); // end parameter hold
@@ -118,7 +120,7 @@
while (readReg8(VL6180X_SYSRANGE_VHV_RECALIBRATE) != 0)
{
// if we've been waiting too long, abort
- if (t.read_us() > 1000000)
+ if (t.read_us() > 100000)
break;
}
}
@@ -141,25 +143,7 @@
id.manufDate.mm = (time % 3600) / 60;
id.manufDate.ss = time % 60;
}
-
-
-uint8_t VL6180X::changeAddress(uint8_t newAddress)
-{
- // do nothing if the address is the same or it's out of range
- if (newAddress == addr || newAddress > 127)
- return addr;
- // set the new address
- writeReg8(VL6180X_I2C_SLAVE_DEVICE_ADDRESS, newAddress);
-
- // read it back and store it
- addr = readReg8(VL6180X_I2C_SLAVE_DEVICE_ADDRESS);
-
- // return the new address
- return addr;
-}
-
-
void VL6180X::continuousDistanceMode(bool on)
{
if (distMode != on)
@@ -184,31 +168,48 @@
bool VL6180X::rangeReady()
{
- return (readReg8(VL6180X_RESULT_INTERRUPT_STATUS_GPIO) & 0x07) == 4;
+ // check if the status register says a sample is ready (bits 0-2/0x07)
+ // or an error has occurred (bits 6-7/0xC0)
+ return ((readReg8(VL6180X_RESULT_INTERRUPT_STATUS_GPIO) & 0xC7) != 0);
}
void VL6180X::startRangeReading()
{
- writeReg8(VL6180X_SYSRANGE_START, 0x01);
+ // start a new range reading if one isn't already in progress
+ if (!rangeStarted)
+ {
+ tSampleStart = sampleTimer.read_us();
+ writeReg8(VL6180X_SYSTEM_INTERRUPT_CLEAR, 0x07);
+ writeReg8(VL6180X_SYSRANGE_START, 0x00);
+ writeReg8(VL6180X_SYSRANGE_START, 0x01);
+ rangeStarted = true;
+ }
}
-int VL6180X::getRange(uint8_t &distance, uint32_t timeout_us)
+int VL6180X::getRange(uint8_t &distance, uint32_t &tMid, uint32_t &dt, uint32_t timeout_us)
{
- if (!rangeReady())
- writeReg8(VL6180X_SYSRANGE_START, 0x01);
+ // start a reading if one isn't already in progress
+ startRangeReading();
+
+ // we're going to wait until this reading ends, so consider the
+ // 'start' command consumed, no matter what happens next
+ rangeStarted = false;
// wait for the sample
Timer t;
t.start();
for (;;)
{
- // if the GPIO pin is high, the sample is ready
+ // check for a sample
if (rangeReady())
break;
// if we've exceeded the timeout, return failure
if (t.read_us() > timeout_us)
+ {
+ writeReg8(VL6180X_SYSRANGE_START, 0x00);
return -1;
+ }
}
// check for errors
@@ -217,6 +218,22 @@
// read the distance
distance = readReg8(VL6180X_RESULT_RANGE_VAL);
+ // Read the convergence time, and compute the overall sample time.
+ // Per the data sheet, the total execution time is the sum of the
+ // fixed 3.2ms pre-calculation time, the convergence time, and the
+ // readout averaging time. We can query the convergence time for
+ // each reading from the sensor. The averaging time is a controlled
+ // by the READOUT_AVERAGING_SAMPLE_PERIOD setting, which we set to
+ // our constant value averagingSamplePeriod.
+ dt =
+ 3200 // fixed 3.2ms pre-calculation period
+ + readReg32(VL6180X_RESULT_RANGE_RETURN_CONV_TIME) // convergence time
+ + (1300 + 48*averagingSamplePeriod); // readout averaging period
+
+ // figure the midpoint of the sample time - the starting time
+ // plus half the collection time
+ tMid = tSampleStart + dt/2;
+
// clear the data-ready interrupt
writeReg8(VL6180X_SYSTEM_INTERRUPT_CLEAR, 0x07);
@@ -236,53 +253,13 @@
stats.refConvTime = readReg32(VL6180X_RESULT_RANGE_REFERENCE_CONV_TIME);
}
-float VL6180X::getAmbientLight(VL6180X_ALS_Gain gain)
-{
- // set the desired gain
- writeReg8(VL6180X_SYSALS_ANALOGUE_GAIN, (0x40 | gain));
-
- // start the integration
- writeReg8(VL6180X_SYSALS_START, 0x01);
-
- // give it time to integrate
- wait_ms(100);
-
- // clear the data-ready interrupt
- writeReg8(VL6180X_SYSTEM_INTERRUPT_CLEAR, 0x07);
-
- // retrieve the raw sensor reading om the sensoe
- unsigned int alsRaw = readReg16(VL6180X_RESULT_ALS_VAL);
-
- // get the integration period
- unsigned int tIntRaw = readReg16(VL6180X_SYSALS_INTEGRATION_PERIOD);
- float alsIntegrationPeriod = 100.0 / tIntRaw ;
-
- // get the actual gain at the user's gain setting
- float trueGain = 0.0;
- switch (gain)
- {
- case GAIN_20: trueGain = 20.0; break;
- case GAIN_10: trueGain = 10.32; break;
- case GAIN_5: trueGain = 5.21; break;
- case GAIN_2_5: trueGain = 2.60; break;
- case GAIN_1_67: trueGain = 1.72; break;
- case GAIN_1_25: trueGain = 1.28; break;
- case GAIN_1: trueGain = 1.01; break;
- case GAIN_40: trueGain = 40.0; break;
- default: trueGain = 1.0; break;
- }
-
- // calculate the lux (see the manufacturer's app notes)
- return alsRaw * 0.32f / trueGain * alsIntegrationPeriod;
-}
-
uint8_t VL6180X::readReg8(uint16_t registerAddr)
{
// write the request - MSB+LSB of register address
uint8_t data_write[2];
data_write[0] = (registerAddr >> 8) & 0xFF;
data_write[1] = registerAddr & 0xFF;
- if (i2c.write(addr << 1, data_write, 2, true))
+ if (i2c.write(addr << 1, data_write, 2, false))
return 0x00;
// read the result
@@ -300,7 +277,7 @@
uint8_t data_write[2];
data_write[0] = (registerAddr >> 8) & 0xFF;
data_write[1] = registerAddr & 0xFF;
- if (i2c.write(addr << 1, data_write, 2, true))
+ if (i2c.write(addr << 1, data_write, 2, false))
return 0;
// read the result
--- a/VL6180X/VL6180X.h Fri Apr 21 18:50:37 2017 +0000
+++ b/VL6180X/VL6180X.h Tue May 09 05:48:37 2017 +0000
@@ -7,23 +7,6 @@
#include "BitBangI2C.h"
-class MyI2C: public I2C
-{
-public:
- MyI2C(PinName sda, PinName scl) : I2C(sda, scl) { }
-
- int write(int addr, const uint8_t *data, size_t len, bool repeated = false)
- {
- return I2C::write(addr, (const char *)data, len, repeated);
- }
- int read(int addr, uint8_t *data, size_t len, bool repeated = false)
- {
- return I2C::read(addr, (char *)data, len, repeated);
- }
-
- void reset() { }
-};
-
#define VL6180X_IDENTIFICATION_MODEL_ID 0x0000
#define VL6180X_IDENTIFICATION_MODEL_REV_MAJOR 0x0001
#define VL6180X_IDENTIFICATION_MODEL_REV_MINOR 0x0002
@@ -135,12 +118,19 @@
{
public:
// Set up the interface with the given I2C pins, I2C address, and
- // the GPIO0 pin (for resetting the sensor at startup).
+ // the GPIO0 pin (for resetting the sensor at startup).
+ //
+ // If 'internalPullups' is true, we'll set the I2C SDA/SCL pins to
+ // enable the internal pullup resistors. Set this to false if you're
+ // using your own external pullup resistors on the lines. External
+ // pullups are better if you're attaching more than one device to the
+ // same I2C bus; the internal pullups are fine for a single device.
//
// Note that the power-on default I2C address is always 0x29. The
// address can be changed during a session, but it's not saved
// persistently; it always resets to 0x29 on the next power cycle.
- VL6180X(PinName sda, PinName scl, uint8_t addr, PinName gpio0);
+ VL6180X(PinName sda, PinName scl, uint8_t addr, PinName gpio0,
+ bool internalPullups);
// destruction
~VL6180X();
@@ -159,7 +149,15 @@
// Get TOF range distance in mm. Returns 0 on success, a device
// "range error code" (>0) on failure, or -1 on timeout.
- int getRange(uint8_t &distance, uint32_t timeout_us);
+ //
+ // 'tMid' is the timestamp in microseconds of the midpoint of the
+ // sample, relative to an arbitrary zero point. This can be used
+ // to construct a timeline of successive readings, such as for
+ // velocity calculations. 'dt' is the time the sensor took to
+ // collect the sample.
+ int getRange(
+ uint8_t &distance, uint32_t &tMid, uint32_t &dt,
+ uint32_t timeout_us);
// get range statistics
void getRangeStats(VL6180X_RangeStats &stats);
@@ -170,17 +168,15 @@
// is a sample ready?
bool rangeReady();
- // get ambient light level in lux
- float getAmbientLight(VL6180X_ALS_Gain gain);
-
// get identification data
void getID(VL6180X_ID &id);
- // Change the address of the device. Returns the new address.
- uint8_t changeAddress(uint8_t newAddress);
-
+protected:
+ // READOUT_AVERAGING_SAMPLE_PERIOD setting. Each unit represents
+ // 64.5us of added time beyond the 1.3ms fixed base period. The
+ // default is 48 units.
+ static const int averagingSamplePeriod = 48;
-protected:
// I2C interface to device
BitBangI2C i2c;
@@ -192,6 +188,15 @@
// current distance mode: 0=single shot, 1=continuous
bool distMode;
+
+ // range reading is in progress
+ bool rangeStarted;
+
+ // sample timer
+ Timer sampleTimer;
+
+ // time (from Timer t) of start of last range sample
+ uint32_t tSampleStart;
// read registers
uint8_t readReg8(uint16_t regAddr);
--- a/cfgVarMsgMap.h Fri Apr 21 18:50:37 2017 +0000
+++ b/cfgVarMsgMap.h Tue May 09 05:48:37 2017 +0000
@@ -48,7 +48,7 @@
// ********** DESCRIBE CONFIGURATION VARIABLES **********
case 0:
- v_byte_ro(19, 2); // number of SCALAR variables
+ v_byte_ro(21, 2); // number of SCALAR variables
v_byte_ro(6, 3); // number of ARRAY variables
break;
@@ -185,6 +185,18 @@
v_ui16(plunger.jitterWindow, 2);
break;
+ case 20:
+ // bar-code plunger setup
+ v_ui16(plunger.barCode.startPix, 2);
+ break;
+
+ case 21:
+ v_ui16(tlc59116.chipMask, 2);
+ v_byte(tlc59116.sda, 4);
+ v_byte(tlc59116.scl, 5);
+ v_byte(tlc59116.reset, 6);
+ break;
+
// case N: // new scalar variable
//
// !!! ATTENTION !!!
--- a/config.h Fri Apr 21 18:50:37 2017 +0000
+++ b/config.h Tue May 09 05:48:37 2017 +0000
@@ -1,49 +1,44 @@
// Pinscape Controller Configuration
//
-// New for 2016: dynamic configuration! To configure the controller,
-// connect the KL25Z to your PC, install the STANDARD pre-compiled .bin
-// file, and run the Windows config tool. There's no need (as there was in
-// the past) to edit the source code or to compile a custom version of the
-// binary just to customize setup options.
+// !!! ATTENTION !!!
+// If you've come here on advice in a forum to change a GPIO setting or
+// to #define a macro to enable the expansion boards, >>>STOP NOW<<<. The
+// advice you found is out of date and no longer applies. You don't need
+// to edit this file or recompile the firmware, and you shouldn't. Instead,
+// use the standard firmware, and set options using the Pinscape Config Tool
+// on your Windows PC. All options that were formerly configurable by
+// editing this file can be selected with the Config Tool. That's much
+// cleaner and easier than editing the source code, and it eliminates the
+// problem of re-synchronizing a private copy of the source code with future
+// updates. With the config tool, you only need the standard firmware build,
+// so future updates are a simple matter of downloading the latest version.
//
-// In earlier versions, configuration was handled mostly with #ifdef and
-// similar constructs. To customize the setup, you had to create a private
-// forked copy of the source code, edit the constants defined in config.h,
-// and compile a custom binary. That's no longer necessary!
//
-// The new approach is to do everything (or as much as possible, anyway)
-// via the Windows config tool. You shouldn't have to recompile a custom
-// version just to make a configurable change. Of course, you're still free
-// to create a custom version if you want to add entirely new features or
-// make changes that go beyond what the setup tool exposes.
+// IN THE PAST (but NOT NOW - see above), configuration was handled mostly
+// with #defines and #ifdefs. To customize the setup, you had to create a
+// private forked copy of the source code, edit the constants defined in
+// config.h, and compile a custom binary. That's no longer necessary because
+// the config tool lets you set all configurable options dynamically. Of
+// course, you're still free to create a custom version if you want to add
+// entirely new features or make changes that go beyond the configurable
+// options.
//
+#ifndef CONFIG_H
+#define CONFIG_H
-// Pre-packaged configuration selection.
+
+// TEST SETTINGS - FOR DEBUGGING PURPOSES ONLY. The macros below select
+// special option combinations for debugging purposes.
//
-// IMPORTANT! If you just want to create a custom configuration, DON'T
-// modify this file, DON'T use these macros, and DON'T compiler on mbed.
-// Instead, use the unmodified standard build and configure your system
-// using the Pinscape Config Tool on Windows. That's easier and better
-// because the config tool will be able to back up your settings to a
-// local file on your PC, and will automatically preserve your settings
-// across upgrades. You won't have to worry about merging your changes
-// into every update of the repository source code, since you'll never
-// have to change the source code.
-//
-// The different configurations here are purely for testing purposes.
-// The standard build uses the STANDARD_CONFIG settings, which are the
-// same as the original version where you had to modify config.h by hand
-// to customize your system.
-//
+// IMPORTANT! If you're trying to create a custom configuration because
+// you have a pin conflict or because you're using the expansion boards,
+// DON'T modify this file, DON'T use these macros, and DON'T recompile
+// the firmware. Use the Config Tool on your Windows PC instead.
#define STANDARD_CONFIG 1 // standard settings, based on v1 base settings
#define TEST_CONFIG_EXPAN 0 // configuration for the expansion boards
#define TEST_KEEP_PRINTF 0 // for debugging purposes, keep printf() enabled
// by leaving the SDA UART GPIO pins unallocated
-
-#ifndef CONFIG_H
-#define CONFIG_H
-
// Plunger type codes
// NOTE! These values are part of the external USB interface. New
// values can be added, but the meaning of an existing assigned number
@@ -143,6 +138,7 @@
const int PortType74HC595 = 4; // 74HC595 port
const int PortTypeVirtual = 5; // Virtual port - visible to host software, but not connected
// to a physical output
+const int PortTypeTLC59116 = 6; // TLC59116 port
// LedWiz output port flag bits
const uint8_t PortFlagActiveLow = 0x01; // physical output is active-low
@@ -156,10 +152,22 @@
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 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 in the overall daisy chain, starting
+ // from 0 for OUT0 on the first chip in the chain
+ //
+ // - for a TLC59116, the high 4 bits are the chip
+ // address (the low 4 bits of the address only),
+ // and the low 4 bits are the output number on
+ // the chip
+ //
+ // - for inactive and virtual ports, this is unused
+ //
uint8_t flags; // flags: a combination of PortFlagXxx values
void set(uint8_t typ, uint8_t pin, uint8_t flags = 0)
@@ -331,6 +339,13 @@
hc595.latch = PINNAME_TO_WIRE(PTA12);
hc595.ena = PINNAME_TO_WIRE(PTD4);
+ // disable all TLC59116 chips by default
+ tlc59116.chipMask = 0;
+
+ // Default TLC59116 pin assignments
+ tlc59116.sda = PINNAME_TO_WIRE(PTC6);
+ tlc59116.scl = PINNAME_TO_WIRE(PTC5);
+ tlc59116.reset = PINNAME_TO_WIRE(PTC10);
// Default IR hardware pin assignments. On the expansion boards,
// the sensor is connected to PTA13, and the emitter LED is on PTC9.
@@ -618,6 +633,12 @@
// physical travel. Zero disables the jitter filter.
uint16_t jitterWindow;
+ // bar code sensor parameters
+ struct
+ {
+ uint16_t startPix; // starting pixel offset
+ } barCode;
+
// ZB LAUNCH BALL button setup.
//
// This configures the "ZB Launch Ball" feature in DOF, based on Zeb's (of
@@ -757,7 +778,7 @@
struct
{
// number of TLC5940NT chips connected in daisy chain
- int nchips;
+ uint8_t nchips;
// pin connections (wire pin IDs)
uint8_t sin; // Serial data - must connect to SPIO MOSI -> PTC6 or PTD2
@@ -774,7 +795,7 @@
struct
{
// number of 74HC595 chips attached in daisy chain
- int nchips;
+ uint8_t nchips;
// pin connections
uint8_t sin; // Serial data - use any GPIO pin
@@ -784,6 +805,21 @@
} hc595;
+ // --- TLC59116 PWM Controller Chip Setup --
+ struct
+ {
+ // Chip mask. Each bit represents an enabled chip at the
+ // corresponding 4-bit address (i.e., bit 1<<addr represents
+ // the chip at 'addr').
+ uint16_t chipMask;
+
+ // pin connections
+ uint8_t sda; // I2C SDA
+ uint8_t scl; // I2C SCL
+ uint8_t reset; // !RESET (hardware reset line, active low)
+
+ } tlc59116;
+
// --- IR Remote Control Hardware Setup ---
struct
--- a/main.cpp Fri Apr 21 18:50:37 2017 +0000
+++ b/main.cpp Tue May 09 05:48:37 2017 +0000
@@ -47,34 +47,48 @@
// 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.
+// We support several sensor types:
//
-// Note that 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.
+// - AEDR-8300-1K2 optical encoders. These are quadrature encoders with
+// reflective optical sensing and built-in lighting and optics. The sensor
+// is attached to the plunger so that it moves with the plunger, and slides
+// along a guide rail with a reflective pattern of regularly spaces bars
+// for the encoder to read. We read the plunger position by counting the
+// bars the sensor passes as it moves across the rail. This is the newest
+// option, and it's my current favorite because it's highly accurate,
+// precise, and fast, plus it's relatively inexpensive.
+//
+// - Slide potentiometers. There are slide potentioneters available with a
+// long enough travel distance (at least 85mm) to cover the plunger travel.
+// Attach the plunger to the potentiometer knob so that the moving the
+// plunger moves the pot knob. We sense the position by simply reading
+// the analog voltage on the pot brush. A pot with a "linear taper" (that
+// is, the resistance varies linearly with the position) is required.
+// This option is cheap, easy to set up, and works well.
//
-// 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
-// to do the calibration once, when you first install everything. (You might
-// also want to re-calibrate if you physically remove and reinstall the CCD
-// sensor or the mechanical plunger, since their alignment shift change slightly
-// when you put everything back together.) You can optionally install a
-// dedicated momentary switch or pushbutton to activate the calibration mode;
-// this is describe in the project documentation. If you don't want to bother
-// with the extra button, you can also trigger calibration using the Windows
-// setup software, which you can find on the Pinscape project page.
+// - VL6108X time-of-flight distance sensor. This is an optical distance
+// sensor that measures the distance to a nearby object (within about 10cm)
+// by measuring the travel time for reflected pulses of light. It's fairly
+// cheap and easy to set up, but I don't recommend it because it has very
+// low precision.
//
-// The calibration procedure is described in the project documentation. Briefly,
-// when you trigger calibration mode, the software will scan the CCD for about
-// 15 seconds, during which you should simply pull the physical plunger back
-// all the way, hold it for a moment, and then slowly return it to the rest
-// position. (DON'T just release it from the retracted position, since that
-// let it shoot forward too far. We want to measure the range from the park
-// position to the fully retracted position only.)
+// - TSL1410R/TSL1412R linear array optical sensors. These are large optical
+// sensors with the pixels arranged in a single row. The pixel arrays are
+// large enough on these to cover the travel distance of the plunger, so we
+// can set up the sensor near the plunger in such a way that the plunger
+// casts a shadow on the sensor. We detect the plunger position by finding
+// the edge of the sahdow in the image. The optics for this setup are very
+// simple since we don't need any lenses. This was the first sensor we
+// supported, and works very well, but unfortunately the sensor is difficult
+// to find now since it's been discontinued by the manufacturer.
+//
+// The v2 Build Guide has details on how to build and configure all of the
+// sensor options.
+//
+// Visual Pinball has built-in support for plunger devices like this one, but
+// some older VP tables (particularly for VP 9) can't use it without some
+// modifications to their scripting. The Build Guide has advice on how to
+// fix up VP tables to add plunger support when necessary.
//
// - Button input wiring. You can assign GPIO ports as inputs for physical
// pinball-style buttons, such as flipper buttons, a Start button, coin
@@ -107,28 +121,34 @@
// current handing. The Build Guide has a reference circuit design for this
// purpose that's simple and inexpensive to build.
//
-// - Enhanced LedWiz emulation with TLC5940 PWM controller chips. You can attach
-// external PWM controller chips for controlling device outputs, instead of using
-// the on-board GPIO ports as described above. The software can control a set of
-// daisy-chained TLC5940 chips. Each chip provides 16 PWM outputs, so you just
-// need two of them to get the full complement of 32 output ports of a real LedWiz.
-// You can hook up even more, though. Four chips gives you 64 ports, which should
-// be plenty for nearly any virtual pinball project. To accommodate the larger
-// supply of ports possible with the PWM chips, the controller software provides
-// a custom, extended version of the LedWiz protocol that can handle up to 128
-// ports. PC software designed only for the real LedWiz obviously won't know
-// about the extended protocol and won't be able to take advantage of its extra
-// capabilities, but the latest version of DOF (DirectOutput Framework) *does*
-// know the new language and can take full advantage. Older software will still
-// work, though - the new extensions are all backward compatible, so old software
-// that only knows about the original LedWiz protocol will still work, with the
-// obvious limitation that it can only access the first 32 ports.
+// - Enhanced LedWiz emulation with TLC5940 and/or TLC59116 PWM controller chips.
+// You can attach external PWM chips for controlling device outputs, instead of
+// using (or in addition to) the on-board GPIO ports as described above. The
+// software can control a set of daisy-chained TLC5940 or TLC59116 chips. Each
+// chip provides 16 PWM outputs, so you just need two of them to get the full
+// complement of 32 output ports of a real LedWiz. You can hook up even more,
+// though. Four chips gives you 64 ports, which should be plenty for nearly any
+// virtual pinball project.
//
// The Pinscape Expansion Board project (which appeared in early 2016) provides
// a reference hardware design, with EAGLE circuit board layouts, that takes full
// advantage of the TLC5940 capability. It lets you create a customized set of
// outputs with full PWM control and power handling for high-current devices
-// built in to the boards.
+// built in to the boards.
+//
+// To accommodate the larger supply of ports possible with the external chips,
+// the controller software provides a custom, extended version of the LedWiz
+// protocol that can handle up to 128 ports. Legacy PC software designed only
+// for the original LedWiz obviously can't use the extended protocol, and thus
+// can't take advantage of its extra capabilities, but the latest version of
+// DOF (DirectOutput Framework) *does* know the new language and can take full
+// advantage. Older software will still work, though - the new extensions are
+// all backwards compatible, so old software that only knows about the original
+// LedWiz protocol will still work, with the limitation that it can only access
+// the first 32 ports. In addition, we provide a replacement LEDWIZ.DLL that
+// creates virtual LedWiz units representing additional ports beyond the first
+// 32. This allows legacy LedWiz client software to address all ports by
+// making them think that you have several physical LedWiz units installed.
//
// - Night Mode control for output devices. You can connect a switch or button
// to the controller to activate "Night Mode", which disables feedback devices
@@ -222,6 +242,7 @@
#include "FreescaleIAP.h"
#include "crc32.h"
#include "TLC5940.h"
+#include "TLC59116.h"
#include "74HC595.h"
#include "nvm.h"
#include "TinyDigitalIn.h"
@@ -237,6 +258,7 @@
#include "nullSensor.h"
#include "barCodeSensor.h"
#include "distanceSensor.h"
+#include "tsl14xxSensor.h"
#define DECL_EXTERNS
@@ -646,17 +668,26 @@
// about 50 GPIO pins. So if you want to do everything with GPIO ports,
// you have to ration pins among features.
//
-// To overcome some of these limitations, we also provide two types of
+// To overcome some of these limitations, we also support several external
// peripheral controllers that allow adding many more outputs, using only
-// a small number of GPIO pins to interface with the peripherals. First,
-// we support TLC5940 PWM controller chips. Each TLC5940 provides 16 ports
-// with full PWM, and multiple TLC5940 chips can be daisy-chained. The
-// chip only requires 5 GPIO pins for the interface, no matter how many
-// chips are in the chain, so it effectively converts 5 GPIO pins into
-// almost any number of PWM outputs. Second, we support 74HC595 chips.
-// These provide only digital outputs, but like the TLC5940 they can be
-// daisy-chained to provide almost unlimited outputs with a few GPIO pins
-// to control the whole chain.
+// a small number of GPIO pins to interface with the peripherals:
+//
+// - TLC5940 PWM controller chips. Each TLC5940 provides 16 ports with
+// 12-bit PWM, and multiple TLC5940 chips can be daisy-chained. The
+// chips connect via 5 GPIO pins, and since they're daisy-chainable,
+// one set of 5 pins can control any number of the chips. So this chip
+// effectively converts 5 GPIO pins into almost any number of PWM outputs.
+//
+// - TLC59116 PWM controller chips. These are similar to the TLC5940 but
+// a newer generation with an improved design. These use an I2C bus,
+// allowing up to 14 chips to be connected via 3 GPIO pins.
+//
+// - 74HC595 shift register chips. These provide 8 digital (on/off only)
+// outputs per chip. These need 4 GPIO pins, and like the other can be
+// daisy chained to add more outputs without using more GPIO pins. These
+// are advantageous for outputs that don't require PWM, since the data
+// transfer sizes are so much smaller. The expansion boards use these
+// for the chime board outputs.
//
// Direct GPIO output ports and peripheral controllers can be mixed and
// matched in one system. The assignment of pins to ports and the
@@ -735,7 +766,7 @@
// Gamma correction table for 8-bit input values
-static const uint8_t gamma[] = {
+static const uint8_t dof_to_gamma_8bit[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2,
@@ -761,7 +792,7 @@
{
public:
LwGammaOut(LwOut *o) : out(o) { }
- virtual void set(uint8_t val) { out->set(gamma[val]); }
+ virtual void set(uint8_t val) { out->set(dof_to_gamma_8bit[val]); }
private:
LwOut *out;
@@ -903,10 +934,55 @@
uint8_t prv;
};
-
-
+//
+// TLC59116 interface object
+//
+TLC59116 *tlc59116 = 0;
+void init_tlc59116(Config &cfg)
+{
+ // Create the interface if any chips are enabled
+ if (cfg.tlc59116.chipMask != 0)
+ {
+ // set up the interface
+ tlc59116 = new TLC59116(
+ wirePinName(cfg.tlc59116.sda),
+ wirePinName(cfg.tlc59116.scl),
+ wirePinName(cfg.tlc59116.reset));
+
+ // initialize the chips
+ tlc59116->init();
+ }
+}
+
+// LwOut class for TLC59116 outputs. The 'addr' value in the constructor
+// is low 4 bits of the chip's I2C address; this is the part of the address
+// that's configurable per chip. 'port' is the output number on the chip
+// (0-15).
+//
+// Note that we don't need a separate gamma-corrected subclass for this
+// output type, since there's no loss of precision with the standard layered
+// gamma (it emits 8-bit values, and we take 8-bit inputs).
+class Lw59116Out: public LwOut
+{
+public:
+ Lw59116Out(uint8_t addr, uint8_t port) : addr(addr), port(port) { prv = 0; }
+ virtual void set(uint8_t val)
+ {
+ if (val != prv)
+ tlc59116->set(addr, port, prv = val);
+ }
+
+protected:
+ uint8_t addr;
+ uint8_t port;
+ uint8_t prv;
+};
+
+
+//
// 74HC595 interface object. Set this up with the port assignments in
// config.h.
+//
HC595 *hc595 = 0;
// initialize the 74HC595 interface
@@ -1276,13 +1352,22 @@
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)
+ // 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 PortTypeTLC59116:
+ // TLC59116 port. The pin number in the config encodes the chip address
+ // in the high 4 bits and the output number on the chip in the low 4 bits.
+ // There's no gamma-corrected version of this output handler, so we don't
+ // need to worry about that here; just use the layered gamma as needed.
+ if (tlc59116 != 0)
+ lwp = new Lw59116Out((pin >> 4) & 0x0F, pin & 0x0F);
+ break;
case PortTypeVirtual:
case PortTypeDisabled:
@@ -3907,10 +3992,8 @@
else
{
// The latch didn't stick, so PSU2 was still off at
- // our last check. Try pulsing it again in case PSU2
- // was turned on since the last check.
- psu2_status_set->write(1);
- psu2_state = 2;
+ // our last check. Return to idle state.
+ psu2_state = 1;
}
break;
@@ -4197,6 +4280,8 @@
saveConfigSucceededFlag = 0x40;
// start the followup timer
+ saveConfigFollowupTime = tFollowup;
+ saveConfigFollowupTimer.reset();
saveConfigFollowupTimer.start();
// if a reboot is pending, flag it
@@ -4336,10 +4421,10 @@
// the plunger sensor interface object
PlungerSensor *plungerSensor = 0;
-// wait for the plunger sensor to complete any outstanding read
+// wait for the plunger sensor to complete any outstanding DMA transfer
static void waitPlungerIdle(void)
{
- while (!plungerSensor->ready()) { }
+ while (plungerSensor->dmaBusy()) { }
}
// Create the plunger sensor based on the current configuration. If
@@ -4409,8 +4494,9 @@
break;
}
- // set the jitter filter
- plungerSensor->setJitterWindow(cfg.plunger.jitterWindow);
+ // initialize the config variables affecting the plunger
+ plungerSensor->onConfigChange(19, cfg);
+ plungerSensor->onConfigChange(20, cfg);
}
// Global plunger calibration mode flag
@@ -4473,9 +4559,6 @@
{
// not in a firing event yet
firing = 0;
-
- // no history yet
- histIdx = 0;
}
// Collect a reading from the plunger sensor. The main loop calls
@@ -4492,25 +4575,6 @@
PlungerReading r;
if (plungerSensor->read(r))
{
- // Pull the previous reading from the history
- const PlungerReading &prv = nthHist(0);
-
- // If the new reading is within 1ms of the previous reading,
- // ignore it. We require a minimum time between samples to
- // ensure that we have a usable amount of precision in the
- // denominator (the time interval) for calculating the plunger
- // velocity. The CCD sensor hardware takes about 2.5ms to
- // read, so it will never be affected by this, but other sensor
- // types don't all have the same hardware cycle time, so we need
- // to throttle them artificially. E.g., the potentiometer only
- // needs one ADC sample per reading, which only takes about 15us;
- // the quadrature sensor needs no time at all since it keeps
- // track of the position continuously via interrupts. We don't
- // need to check which sensor type we have here; we just ignore
- // readings until the minimum interval has passed.
- if (uint32_t(r.t - prv.t) < 1000UL)
- return;
-
// check for calibration mode
if (plungerCalMode)
{
@@ -4575,98 +4639,130 @@
r.pos = -JOYMAX;
}
- // Calculate the velocity from the second-to-last reading
- // to here, in joystick distance units per microsecond.
- // Note that we use the second-to-last reading rather than
- // the very last reading to give ourselves a little longer
- // time base. The time base is so short between consecutive
- // readings that the error bars in the position would be too
- // large.
+ // Look for a firing event - the user releasing the plunger and
+ // allowing it to shoot forward at full speed. Wait at least 5ms
+ // between samples for this, to help distinguish random motion
+ // from the rapid motion of a firing event.
//
- // For reference, the physical plunger velocity ranges up
- // to about 100,000 joystick distance units/sec. This is
- // based on empirical measurements. The typical time for
- // a real plunger to travel the full distance when released
- // from full retraction is about 85ms, so the average velocity
- // covering this distance is about 56,000 units/sec. The
- // peak is probably about twice that. In real-world units,
- // this translates to an average speed of about .75 m/s and
- // a peak of about 1.5 m/s.
+ // There's a trade-off in the choice of minimum sampling interval.
+ // The longer we wait, the more certain we can be of the trend.
+ // But if we wait too long, the user will perceive a delay. We
+ // also want to sample frequently enough to see the release motion
+ // at intermediate steps along the way, so the sampling has to be
+ // considerably faster than the whole travel time, which is about
+ // 25-50ms.
+ if (uint32_t(r.t - prv.t) < 5000UL)
+ return;
+
+ // assume that we'll report this reading as-is
+ z = r.pos;
+
+ // Firing event detection.
+ //
+ // A "firing event" is when the player releases the plunger from
+ // a retracted position, allowing it to shoot forward under the
+ // spring tension.
//
- // Note that we actually calculate the value here in units
- // per *microsecond* - the discussion above is in terms of
- // units/sec because that's more on a human scale. Our
- // choice of internal units here really isn't important,
- // since we only use the velocity for comparison purposes,
- // to detect acceleration trends. We therefore save ourselves
- // a little CPU time by using the natural units of our inputs.
+ // We monitor the plunger motion for these events, and when they
+ // occur, we report an "idealized" version of the motion to the
+ // PC. The idealized version consists of a series of readings
+ // frozen at the fully retracted position for the whole duration
+ // of the forward travel, followed by a series of readings at the
+ // fully forward position for long enough for the plunger to come
+ // mostly to rest. The series of frozen readings aren't meant to
+ // be perceptible to the player - we try to keep them short enough
+ // that they're not apparent as delay. Instead, they're for the
+ // PC client software's benefit. PC joystick clients use polling,
+ // so they only see an unpredictable subset of the readings we
+ // send. The only way to be sure that the client sees a particular
+ // reading is to hold it for long enough that the client is sure to
+ // poll within the hold interval. In the case of the plunger
+ // firing motion, it's important that the client sees the *ends*
+ // of the travel - the fully retracted starting position in
+ // particular. If the PC client only polls for a sample while the
+ // plunger is somewhere in the middle of the travel, the PC will
+ // think that the firing motion *started* in that middle position,
+ // so it won't be able to model the right amount of momentum when
+ // the plunger hits the ball. We try to ensure that the PC sees
+ // the right starting point by reporting the starting point for
+ // extra time during the forward motion. By the same token, we
+ // want the PC to know that the plunger has moved all the way
+ // forward, rather than mistakenly thinking that it stopped
+ // somewhere in the middle of the travel, so we freeze at the
+ // forward position for a short time.
//
- // We don't care about the absolute velocity; this is a purely
- // relative calculation. So to speed things up, calculate it
- // in the integer domain, using a fixed-point representation
- // with a 64K scale. In other words, with the stored values
- // shifted left 16 bits from the actual values: the value 1
- // is stored as 1<<16. The position readings are in the range
- // -JOYMAX..JOYMAX, which fits in 16 bits, and the time
- // differences will generally be on the scale of a few
- // milliseconds = thousands of microseconds. So the velocity
- // figures will fit nicely into a 32-bit fixed point value with
- // a 64K scale factor.
- const PlungerReading &prv2 = nthHist(1);
- int v = ((r.pos - prv2.pos) * 65536L)/int(r.t - prv2.t);
-
- // presume we'll report the latest instantaneous reading
- z = r.pos;
-
- // Check firing events
+ // To detect a firing event, we look for forward motion that's
+ // fast enough to be a firing event. To determine how fast is
+ // fast enough, we use a simple model of the plunger motion where
+ // the acceleration is constant. This is only an approximation,
+ // as the spring force actually varies with spring's compression,
+ // but it's close enough for our purposes here.
+ //
+ // Do calculations in fixed-point 2^48 scale with 64-bit ints.
+ // acc2 = acceleration/2 for 50ms release time, units of unit
+ // distances per microsecond squared, where the unit distance
+ // is the overall travel from the starting retracted position
+ // to the park position.
+ const int32_t acc2 = 112590; // 2^48 scale
switch (firing)
{
case 0:
- // Default state - not in a firing event.
-
- // If we have forward motion from a position that's retracted
- // beyond a threshold, enter phase 1. If we're not pulled back
- // far enough, don't bother with this, as a release wouldn't
- // be strong enough to require the synthetic firing treatment.
- if (v < 0 && r.pos > JOYMAX/6)
+ // Not in firing mode. If we're retracted a bit, and the
+ // motion is forward at a fast enough rate to look like a
+ // release, enter firing mode.
+ if (r.pos > JOYMAX/6)
{
- // enter firing phase 1
- firingMode(1);
-
- // if in calibration state 1 (at rest), switch to state 2 (not
- // at rest)
- if (calState == 1)
- calState = 2;
-
- // we don't have a freeze position yet, but note the start time
- f1.pos = 0;
- f1.t = r.t;
-
- // Figure the barrel spring "bounce" position in case we complete
- // the firing event. This is the amount that the forward momentum
- // of the plunger will compress the barrel spring at the peak of
- // the forward travel during the release. Assume that this is
- // linearly proportional to the starting retraction distance.
- // The barrel spring is about 1/6 the length of the main spring,
- // so figure it compresses by 1/6 the distance. (This is overly
- // simplistic and not very accurate, but it seems to give good
- // visual results, and that's all it's for.)
- f2.pos = -r.pos/6;
+ const uint32_t dt = uint32_t(r.t - prv.t);
+ const uint32_t dt2 = dt*dt; // dt^2
+ if (r.pos < prv.pos - int((prv.pos*acc2*uint64_t(dt2)) >> 48))
+ {
+ // Tentatively enter firing mode. Use the prior reading
+ // as the starting point, and freeze reports for now.
+ firingMode(1);
+ f0 = prv;
+ z = f0.pos;
+
+ // if in calibration state 1 (at rest), switch to
+ // state 2 (not at rest)
+ if (calState == 1)
+ calState = 2;
+ }
}
break;
case 1:
- // Phase 1 - acceleration. If we cross the zero point, trigger
- // the firing event. Otherwise, continue monitoring as long as we
- // see acceleration in the forward direction.
+ // Tentative firing mode: the plunger was moving forward
+ // at last check. To stay in firing mode, the plunger has
+ // to keep moving forward fast enough to look like it's
+ // moving under spring force. To figure out how fast is
+ // fast enough, we use a simple model where the acceleration
+ // is constant over the whole travel distance and the total
+ // travel time is 50ms. The acceleration actually varies
+ // slightly since it comes from the spring force, which
+ // is linear in the displacement; but the plunger spring is
+ // fairly compressed even when the plunger is all the way
+ // forward, so the difference in tension from one end of
+ // the travel to the other is fairly small, so it's not too
+ // far off to model it as constant. And the real travel
+ // time obviously isn't a constant, but all we need for
+ // that is an upper bound. So: we'll figure the time since
+ // we entered firing mode, and figure the distance we should
+ // have traveled to complete the trip within the maximum
+ // time allowed. If we've moved far enough, we'll stay
+ // in firing mode; if not, we'll exit firing mode. And if
+ // we cross the finish line while still in firing mode,
+ // we'll switch to the next phase of the firing event.
if (r.pos <= 0)
{
- // switch to the synthetic firing mode
+ // We crossed the park position. Switch to the second
+ // phase of the firing event, where we hold the reported
+ // position at the "bounce" position (where the plunger
+ // is all the way forward, compressing the barrel spring).
+ // We'll stick here long enough to ensure that the PC
+ // client (Visual Pinball or whatever) sees the reading
+ // and processes the release motion via the simulated
+ // physics.
firingMode(2);
- z = f2.pos;
-
- // note the start time for the firing phase
- f2.t = r.t;
// if in calibration mode, and we're in state 2 (moving),
// collect firing statistics for calibration purposes
@@ -4676,142 +4772,96 @@
// come to rest
calState = 0;
- // collect average firing time statistics in millseconds, if
- // it's in range (20 to 255 ms)
- int dt = uint32_t(r.t - f1.t)/1000UL;
- if (dt >= 20 && dt <= 255)
+ // collect average firing time statistics in millseconds,
+ // if it's in range (20 to 255 ms)
+ const int dt = uint32_t(r.t - f0.t)/1000UL;
+ if (dt >= 15 && dt <= 255)
{
calRlsTimeSum += dt;
calRlsTimeN += 1;
cfg.plunger.cal.tRelease = uint8_t(calRlsTimeSum / calRlsTimeN);
}
}
- }
- else if (v < vprv2)
- {
- // We're still accelerating, and we haven't crossed the zero
- // point yet - stay in phase 1. (Note that forward motion is
- // negative velocity, so accelerating means that the new
- // velocity is more negative than the previous one, which
- // is to say numerically less than - that's why the test
- // for acceleration is the seemingly backwards 'v < vprv'.)
-
- // If we've been accelerating for at least 20ms, we're probably
- // really doing a release. Jump back to the recent local
- // maximum where the release *really* started. This is always
- // a bit before we started seeing sustained accleration, because
- // the plunger motion for the first few milliseconds is too slow
- // for our sensor precision to reliably detect acceleration.
- if (f1.pos != 0)
- {
- // we have a reset point - freeze there
- z = f1.pos;
- }
- else if (uint32_t(r.t - f1.t) >= 20000UL)
- {
- // it's been long enough - set a reset point.
- f1.pos = z = histLocalMax(r.t, 50000UL);
- }
+
+ // Figure the "bounce" position as forward of the park
+ // position by 1/6 of the starting retraction distance.
+ // This simulates the momentum of the plunger compressing
+ // the barrel spring on the rebound. The barrel spring
+ // can compress by about 1/6 of the maximum retraction
+ // distance, so we'll simply treat its compression as
+ // proportional to the retraction. (It might be more
+ // realistic to use a slightly higher value here, maybe
+ // 1/4 or 1/3 or the retraction distance, capping it at
+ // a maximum of 1/6, because the real plunger probably
+ // compresses the barrel spring by 100% with less than
+ // 100% retraction. But that won't affect the physics
+ // meaningfully, just the animation, and the effect is
+ // small in any case.)
+ z = f0.pos = -f0.pos / 6;
+
+ // reset the starting time for this phase
+ f0.t = r.t;
}
else
{
- // We're not accelerating. Cancel the firing event.
- firingMode(0);
- calState = 1;
+ // check for motion since the start of the firing event
+ const uint32_t dt = uint32_t(r.t - f0.t);
+ const uint32_t dt2 = dt*dt; // dt^2
+ if (dt < 50000
+ && r.pos < f0.pos - int((f0.pos*acc2*uint64_t(dt2)) >> 48))
+ {
+ // It's moving fast enough to still be in a release
+ // motion. Continue reporting the start position, and
+ // stay in the first release phase.
+ z = f0.pos;
+ }
+ else
+ {
+ // It's not moving fast enough to be a release
+ // motion. Return to the default state.
+ firingMode(0);
+ calState = 1;
+ }
}
break;
case 2:
- // Phase 2 - start of synthetic firing event. Report the fake
- // bounce for 25ms. VP polls the joystick about every 10ms, so
- // this should be enough time to guarantee that VP sees this
- // report at least once.
- if (uint32_t(r.t - f2.t) < 25000UL)
+ // Firing mode, holding at forward compression position.
+ // Hold here for 25ms.
+ if (uint32_t(r.t - f0.t) < 25000)
{
- // report the bounce position
- z = f2.pos;
+ // stay here for now
+ z = f0.pos;
}
else
{
- // it's been long enough - switch to phase 3, where we
- // report the park position until the real plunger comes
- // to rest
+ // advance to the next phase, where we report the park
+ // position until the plunger comes to rest
firingMode(3);
z = 0;
-
- // set the start of the "stability window" to the rest position
- f3s.t = r.t;
- f3s.pos = 0;
-
- // set the start of the "retraction window" to the actual position
- f3r = r;
+
+ // remember when we started
+ f0.t = r.t;
}
break;
case 3:
- // Phase 3 - in synthetic firing event. Report the park position
- // until the plunger position stabilizes. Left to its own devices,
- // the plunger will usualy bounce off the barrel spring several
- // times before coming to rest, so we'll see oscillating motion
- // for a second or two. In the simplest case, we can aimply wait
- // for the plunger to stop moving for a short time. However, the
- // player might intervene by pulling the plunger back again, so
- // watch for that motion as well. If we're just bouncing freely,
- // we'll see the direction change frequently. If the player is
- // moving the plunger manually, the direction will be constant
- // for longer.
- if (v >= 0)
+ // Firing event, holding at park position. Stay here for
+ // a few moments so that the PC client can simulate the
+ // full release motion, then return to real readings.
+ if (uint32_t(r.t - f0.t) < 250000)
{
- // We're moving back (or standing still). If this has been
- // going on for a while, the user must have taken control.
- if (uint32_t(r.t - f3r.t) > 65000UL)
- {
- // user has taken control - cancel firing mode
- firingMode(0);
- break;
- }
+ // stay here a while longer
+ z = 0;
}
else
{
- // forward motion - reset retraction window
- f3r.t = r.t;
- }
-
- // Check if we're close to the last starting point. The joystick
- // positive axis range (0..4096) covers the retraction distance of
- // about 2.5", so 1" is about 1638 joystick units, hence 1/16" is
- // about 100 units.
- if (abs(r.pos - f3s.pos) < 100)
- {
- // It's at roughly the same position as the starting point.
- // Consider it stable if this has been true for 300ms.
- if (uint32_t(r.t - f3s.t) > 300000UL)
- {
- // we're done with the firing event
- firingMode(0);
- }
- else
- {
- // it's close to the last position but hasn't been
- // here long enough; stay in firing mode and continue
- // to report the park position
- z = 0;
- }
- }
- else
- {
- // It's not close enough to the last starting point, so use
- // this as a new starting point, and stay in firing mode.
- f3s = r;
- z = 0;
+ // it's been long enough - return to normal mode
+ firingMode(0);
}
break;
}
- // save the velocity reading for next time
- vprv2 = vprv;
- vprv = v;
-
// Check for auto-zeroing, if enabled
if ((cfg.plunger.autoZero.flags & PlungerAutoZeroEnabled) != 0)
{
@@ -4837,10 +4887,8 @@
}
}
- // add the new reading to the history
- hist[histIdx] = r;
- if (++histIdx >= countof(hist))
- histIdx = 0;
+ // this new reading becomes the previous reading for next time
+ prv = r;
}
}
@@ -4851,9 +4899,6 @@
return z;
}
- // get the timestamp of the current joystick report (microseconds)
- uint32_t getTimestamp() const { return nthHist(0).t; }
-
// Set calibration mode on or off
void setCalMode(bool f)
{
@@ -4942,6 +4987,12 @@
bool isFiring() { return firing == 3; }
private:
+ // current reported joystick reading
+ int z;
+
+ // previous reading
+ PlungerReading prv;
+
// Calibration state. During calibration mode, we watch for release
// events, to measure the time it takes to complete the release
// motion; and we watch for the plunger to come to reset after a
@@ -4978,105 +5029,21 @@
firing = m;
}
- // Find the most recent local maximum in the history data, up to
- // the given time limit.
- int histLocalMax(uint32_t tcur, uint32_t dt)
- {
- // start with the prior entry
- int idx = (histIdx == 0 ? countof(hist) : histIdx) - 1;
- int hi = hist[idx].pos;
-
- // scan backwards for a local maximum
- for (int n = countof(hist) - 1 ; n > 0 ; idx = (idx == 0 ? countof(hist) : idx) - 1)
- {
- // if this isn't within the time window, stop
- if (uint32_t(tcur - hist[idx].t) > dt)
- break;
-
- // if this isn't above the current hith, stop
- if (hist[idx].pos < hi)
- break;
-
- // this is the new high
- hi = hist[idx].pos;
- }
-
- // return the local maximum
- return hi;
- }
-
- // velocity at previous reading, and the one before that
- int vprv, vprv2;
-
- // Circular buffer of recent readings. We keep a short history
- // of readings to analyze during firing events. We can only identify
- // a firing event once it's somewhat under way, so we need a little
- // retrospective information to accurately determine after the fact
- // exactly when it started. We throttle our readings to no more
- // than one every 1ms, so we have at least N*1ms of history in this
- // array.
- PlungerReading hist[32];
- int histIdx;
-
- // get the nth history item (0=last, 1=2nd to last, etc)
- inline const PlungerReading &nthHist(int n) const
- {
- // histIdx-1 is the last written; go from there
- n = histIdx - 1 - n;
-
- // adjust for wrapping
- if (n < 0)
- n += countof(hist);
-
- // return the item
- return hist[n];
- }
-
// Firing event state.
//
- // 0 - Default state. We report the real instantaneous plunger
- // position to the joystick interface.
- //
- // 1 - Moving forward
+ // 0 - Default state: not in firing event. We report the true
+ // instantaneous plunger position to the joystick interface.
//
- // 2 - Accelerating
+ // 1 - Moving forward at release speed
//
- // 3 - Firing. We report the rest position for a minimum interval,
- // or until the real plunger comes to rest somewhere.
+ // 2 - Firing - reporting the bounce position
+ //
+ // 3 - Firing - reporting the park position
//
int firing;
- // Position/timestamp at start of firing phase 1. When we see a
- // sustained forward acceleration, we freeze joystick reports at
- // the recent local maximum, on the assumption that this was the
- // start of the release. If this is zero, it means that we're
- // monitoring accelerating motion but haven't seen it for long
- // enough yet to be confident that a release is in progress.
- PlungerReading f1;
-
- // Position/timestamp at start of firing phase 2. The position is
- // the fake "bounce" position we report during this phase, and the
- // timestamp tells us when the phase began so that we can end it
- // after enough time elapses.
- PlungerReading f2;
-
- // Position/timestamp of start of stability window during phase 3.
- // We use this to determine when the plunger comes to rest. We set
- // this at the beginning of phase 3, and then reset it when the
- // plunger moves too far from the last position.
- PlungerReading f3s;
-
- // Position/timestamp of start of retraction window during phase 3.
- // We use this to determine if the user is drawing the plunger back.
- // If we see retraction motion for more than about 65ms, we assume
- // that the user has taken over, because we should see forward
- // motion within this timeframe if the plunger is just bouncing
- // freely.
- PlungerReading f3r;
-
- // next Z value to report to the joystick interface (in joystick
- // distance units)
- int z;
+ // Starting position for current firing mode phase
+ PlungerReading f0;
};
// plunger reader singleton
@@ -5574,10 +5541,9 @@
// in a variable-dependent format.
configVarSet(data);
- // If updating the jitter window (variable 19), apply it immediately
- // to the plunger sensor object
- if (data[1] == 19)
- plungerSensor->setJitterWindow(cfg.plunger.jitterWindow);
+ // notify the plunger, so that it can update relevant variables
+ // dynamically
+ plungerSensor->onConfigChange(data[1], cfg);
}
else if (data[0] == 67)
{
@@ -5775,6 +5741,9 @@
// set up the TLC5940 interface, if these chips are present
init_tlc5940(cfg);
+ // initialize the TLC5916 interface, if these chips are present
+ init_tlc59116(cfg);
+
// set up 74HC595 interface, if these chips are present
init_hc595(cfg);
@@ -5787,7 +5756,7 @@
// start the TLC5940 refresh cycle clock
if (tlc5940 != 0)
tlc5940->start();
-
+
// Assume that nothing uses keyboard keys. We'll check for keyboard
// usage when initializing the various subsystems that can send keys
// (buttons, IR). If we find anything that does, we'll create the
@@ -5927,6 +5896,8 @@
tlc5940->enable(true);
if (hc595 != 0)
hc595->enable(true);
+ if (tlc59116 != 0)
+ tlc59116->enable(true);
// start the LedWiz flash cycle timer
wizCycleTimer.start();
@@ -5985,6 +5956,10 @@
// send TLC5940 data updates if applicable
if (tlc5940 != 0)
tlc5940->send();
+
+ // send TLC59116 data updates
+ if (tlc59116 != 0)
+ tlc59116->send();
// collect diagnostic statistics, checkpoint 1
IF_DIAG(mainLoopIterCheckpt[1] += mainLoopTimer.read_us();)
@@ -6255,6 +6230,8 @@
// the power first comes on.
if (tlc5940 != 0)
tlc5940->enable(false);
+ if (tlc59116 != 0)
+ tlc59116->enable(false);
if (hc595 != 0)
hc595->enable(false);
}
@@ -6263,7 +6240,7 @@
// if we have a reboot timer pending, check for completion
if (saveConfigFollowupTimer.isRunning()
- && saveConfigFollowupTimer.read() > saveConfigFollowupTime)
+ && saveConfigFollowupTimer.read_us() > saveConfigFollowupTime*1000000UL)
{
// if a reboot is pending, execute it now
if (saveConfigRebootPending)
@@ -6323,6 +6300,10 @@
// send TLC5940 data if necessary
if (tlc5940 != 0)
tlc5940->send();
+
+ // update TLC59116 outputs
+ if (tlc59116 != 0)
+ tlc59116->send();
// show a diagnostic flash every couple of seconds
if (diagTimer.read_us() > 2000000)
@@ -6364,10 +6345,9 @@
// Enable peripheral chips and update them with current output data
if (tlc5940 != 0)
- {
tlc5940->enable(true);
- tlc5940->update(true);
- }
+ if (tlc59116 != 0)
+ tlc59116->enable(true);
if (hc595 != 0)
{
hc595->enable(true);