work in progress

Dependencies:   FastAnalogIn FastIO USBDevice mbed FastPWM SimpleDMA

Fork of Pinscape_Controller by Mike R

Revision:
2:c174f9ee414a
Parent:
1:d913e0afb2ac
Child:
3:3514575d4f86
--- a/main.cpp	Wed Jul 16 23:33:12 2014 +0000
+++ b/main.cpp	Tue Jul 22 04:33:47 2014 +0000
@@ -3,6 +3,31 @@
 #include "MMA8451Q.h"
 #include "tsl1410r.h"
 #include "FreescaleIAP.h"
+#include "crc32.h"
+
+// customization of the joystick class to expose connect/suspend status
+class MyUSBJoystick: public USBJoystick
+{
+public:
+    MyUSBJoystick(uint16_t vendor_id, uint16_t product_id, uint16_t product_release) 
+        : USBJoystick(vendor_id, product_id, product_release)
+    {
+        connected_ = false;
+        suspended_ = false;
+    }
+    
+    int isConnected() const { return connected_; }
+    int isSuspended() const { return suspended_; }
+    
+protected:
+    virtual void connectStateChanged(unsigned int connected) 
+        { connected_ = connected; }
+    virtual void suspendStateChanged(unsigned int suspended)
+        { suspended_ = suspended; }
+
+    int connected_;
+    int suspended_; 
+};
 
 // on-board RGB LED elements - we use these for diagnostics
 PwmOut led1(LED1), led2(LED2), led3(LED3);
@@ -62,6 +87,36 @@
     }
 };
 
+// Non-volatile memory structure.  We store persistent a small
+// amount of persistent data in flash memory to retain calibration
+// data between sessions.
+struct NVM
+{
+    // checksum - we use this to determine if the flash record
+    // has been initialized
+    uint32_t checksum;
+
+    // signature value
+    static const uint32_t SIGNATURE = 0x4D4A522A;
+    static const uint16_t VERSION = 0x0002;
+    
+    // stored data (excluding the checksum)
+    struct
+    {
+        // signature and version - further verification that we have valid 
+        // initialized data
+        uint32_t sig;
+        uint16_t vsn;
+        
+        // direction - 0 means unknown, 1 means bright end is pixel 0, 2 means reversed
+        uint8_t dir;
+
+        // plunger calibration min and max
+        int plungerMin;
+        int plungerMax;
+    } d;
+};
+
 int main(void)
 {
     // turn off our on-board indicator LED
@@ -69,9 +124,42 @@
     led2 = 1;
     led3 = 1;
     
-    // plunger calibration data
-    const int npix = 320;
-    int plungerMin = 0, plungerMax = npix;
+    // set up a flash memory controller
+    FreescaleIAP iap;
+    
+    // use the last sector of flash for our non-volatile memory structure
+    int flash_addr = (iap.flash_size() - SECTOR_SIZE);
+    NVM *flash = (NVM *)flash_addr;
+    NVM cfg;
+    
+    // check for valid flash
+    bool flash_valid = (flash->d.sig == flash->SIGNATURE 
+                        && flash->d.vsn == flash->VERSION
+                        && flash->checksum == CRC32(&flash->d, sizeof(flash->d)));
+                      
+    // Number of pixels we read from the sensor on each frame.  This can be
+    // less than the physical pixel count if desired; we'll read every nth
+    // piexl if so.  E.g., with a 1280-pixel physical sensor, if npix is 320,
+    // we'll read every 4th pixel.  VP doesn't seem to have very high
+    // resolution internally for the plunger, so it's probably not necessary
+    // to use the full resolution of the sensor - about 160 pixels seems
+    // perfectly adequate.  We can read the sensor faster (and thus provide
+    // a higher refresh rate) if we read fewer pixels in each frame.
+    const int npix = 160;
+
+    // if the flash is valid, load it; otherwise initialize to defaults
+    if (flash_valid) {
+        memcpy(&cfg, flash, sizeof(cfg));
+        printf("Flash restored: plunger min=%d, max=%d\r\n", 
+            cfg.d.plungerMin, cfg.d.plungerMax);
+    }
+    else {
+        printf("Factory reset\r\n");
+        cfg.d.sig = cfg.SIGNATURE;
+        cfg.d.vsn = cfg.VERSION;
+        cfg.d.plungerMin = 0;
+        cfg.d.plungerMax = npix;
+    }
     
     // plunger calibration button debounce timer
     Timer calBtnTimer;
@@ -97,32 +185,20 @@
     acTimer.start();
     int t0ac = acTimer.read_ms();
     
-    // set up a timer for reading the plunger sensor
-    Timer ccdTimer;
-    ccdTimer.start();
-    int t0ccd = ccdTimer.read_ms();
-    
-#if 0
-    // DEBUG
-    Timer ccdDbgTimer;
-    ccdDbgTimer.start();
-    int t0ccdDbg = ccdDbgTimer.read_ms();
-#endif
-
     // Create the joystick USB client.  Light the on-board indicator LED
     // red while connecting, and change to green after we connect.
-    led1 = 0.75;
-    USBJoystick js(0xFAFA, 0x00F7, 0x0001);
+    led1 = 0;
+    MyUSBJoystick js(0xFAFA, 0x00F7, 0x0001);
     led1 = 1;
-    led2 = 0.75;
-    
+    led2 = 0;
+
     // create the accelerometer object
     const int MMA8451_I2C_ADDRESS = (0x1d<<1);
     MMA8451Q accel(PTE25, PTE24, MMA8451_I2C_ADDRESS);
     
     // create the CCD array object
     TSL1410R ccd(PTE20, PTE21, PTB0);
-
+    
     // recent accelerometer readings, for auto centering
     int iAccPrv = 0, nAccPrv = 0;
     const int maxAccPrv = 5;
@@ -130,9 +206,12 @@
 
     // last accelerometer report, in mouse coordinates
     int x = 127, y = 127, z = 0;
-    
+
     // raw accelerator centerpoint, on the unit interval (-1.0 .. +1.0)
     float xCenter = 0.0, yCenter = 0.0;    
+    
+    // start the first CCD integration cycle
+    ccd.clear();
 
     // we're all set up - now just loop, processing sensor reports and 
     // host requests
@@ -219,21 +298,49 @@
                     calBtnState = 3;
                     
                     // reset the calibration limits
-                    plungerMax = 0;
-                    plungerMin = npix;
+                    cfg.d.plungerMax = 0;
+                    cfg.d.plungerMin = npix;
                 }
                 break;
+                
+            case 3:
+                // Already in calibration mode - pushing the button in this
+                // state doesn't change the current state, but we won't leave
+                // this state as long as it's held down.  We can simply do
+                // nothing here.
+                break;
             }
         }
         else
         {
-            // Button released.  If we're not already in calibration mode,
-            // reset the button state.  Once calibration mode starts, it sticks
-            // until the calibration time elapses.
-            if (calBtnState != 3)
+            // Button released.  If we're in calibration mode, and
+            // the calibration time has elapsed, end the calibration
+            // and save the results to flash.
+            //
+            // Otherwise, return to the base state without saving anything.
+            // If the button is released before we make it to calibration
+            // mode, it simply cancels the attempt.
+            if (calBtnState == 3
+                && calBtnTimer.read_ms() - calBtnDownTime > 17500)
+            {
+                // exit calibration mode
                 calBtnState = 0;
-            else if (calBtnTimer.read_ms() - calBtnDownTime > 32500)
+                
+                // Save the current configuration state to flash, so that it
+                // will be preserved through power off.  Update the checksum
+                // first so that we recognize the flash record as valid.
+                cfg.checksum = CRC32(&cfg.d, sizeof(cfg.d));
+                iap.erase_sector(flash_addr);
+                iap.program_flash(flash_addr, &cfg, sizeof(cfg));
+                
+                // the flash state is now valid
+                flash_valid = true;
+            }
+            else if (calBtnState != 3)
+            {
+                // didn't make it to calibration mode - cancel the operation
                 calBtnState = 0;
+            }
         }       
         
         // light/flash the calibration button light, if applicable
@@ -258,89 +365,86 @@
         if (calBtnLit != newCalBtnLit)
         {
             calBtnLit = newCalBtnLit;
-            calBtnLed = (calBtnLit ? 1 : 0);
+            if (calBtnLit) {
+                calBtnLed = 1;
+                led1 = 0;
+                led2 = 0;
+                led3 = 1;
+            }
+            else {
+                calBtnLed = 0;
+                led1 = 1;
+                led2 = 1;
+                led3 = 0;
+            }
         }
         
         // read the plunger sensor
         int znew = z;
-        /* if (ccdTimer.read_ms() - t0ccd > 33) */
+        uint16_t pix[npix];
+        ccd.read(pix, npix);
+
+        // get the average brightness at each end of the sensor
+        long avg1 = (long(pix[0]) + long(pix[1]) + long(pix[2]) + long(pix[3]) + long(pix[4]))/5;
+        long avg2 = (long(pix[npix-1]) + long(pix[npix-2]) + long(pix[npix-3]) + long(pix[npix-4]) + long(pix[npix-5]))/5;
+        
+        // figure the midpoint in the brightness; multiply by 3 so that we can
+        // compare sums of three pixels at a time to smooth out noise
+        long midpt = (avg1 + avg2)/2 * 3;
+        
+        // Work from the bright end to the dark end.  VP interprets the
+        // Z axis value as the amount the plunger is pulled: the minimum
+        // is the rest position, the maximum is fully pulled.  So we 
+        // essentially want to report how much of the sensor is lit,
+        // since this increases as the plunger is pulled back.
+        int si = 1, di = 1;
+        if (avg1 < avg2)
+            si = npix - 2, di = -1;
+
+        // scan for the midpoint     
+        uint16_t *pixp = pix + si;           
+        for (int n = 1 ; n < npix - 1 ; ++n, pixp += di)
         {
-            // read the sensor at reduced resolution
-            uint16_t pix[npix];
-            ccd.read(pix, npix, 0);
-            
-#if 0
-            // debug - send samples every 5 seconds
-            if (ccdDbgTimer.read_ms() - t0ccdDbg > 5000)
-            {
-                for (int i = 0 ; i < npix ; ++i)
-                    printf("%x ", pix[i]);
-                printf("\r\n\r\n");
-            
-                ccdDbgTimer.reset();
-                t0ccdDbg = ccdDbgTimer.read_ms();
-            }
-#endif
-    
-            // check which end is the brighter - this is the "tip" end
-            // of the plunger
-            long avg1 = (long(pix[0]) + long(pix[1]) + long(pix[2]) + long(pix[3]) + long(pix[4]))/5;
-            long avg2 = (long(pix[npix-1]) + long(pix[npix-2]) + long(pix[npix-3]) + long(pix[npix-4]) + long(pix[npix-5]))/5;
-            
-            // figure the midpoint in the brightness
-            long midpt = (avg1 + avg2)/2 * 3;
-            
-            // Work from the bright end to the dark end.  VP interprets the
-            // Z axis value as the amount the plunger is pulled: the minimum
-            // is the rest position, the maximum is fully pulled.  So we 
-            // essentially want to report how much of the sensor is lit,
-            // since this increases as the plunger is pulled back.
-            int si = 1, di = 1;
-            if (avg1 < avg2)
-                si = npix - 1, di = -1;
-
-            // scan for the midpoint                
-            for (int n = 1, i = si ; n < npix - 1 ; ++n, i += di)
+            // if we've crossed the midpoint, report this position
+            if (long(pixp[-1]) + long(pixp[0]) + long(pixp[1]) < midpt)
             {
-                // if we've crossed the midpoint, report this position
-                if (long(pix[i-1]) + long(pix[i]) + long(pix[i+1]) < midpt)
+                // note the new position
+                int pos = n;
+                
+                // if the bright end and dark end don't differ by enough, skip this
+                // reading entirely - we must have an overexposed or underexposed frame
+                if (labs(avg1 - avg2) < 0x3333)
+                    break; 
+                
+                // Calibrate, or apply calibration, depending on the mode.
+                // In either case, normalize to a 0-127 range.  VP appears to
+                // ignore negative Z axis values.
+                if (calBtnState == 3)
                 {
-                    // note the new position
-                    int pos = abs(i - si);
-                    
-                    // Calibrate, or apply calibration, depending on the mode.
-                    // In either case, normalize to a 0-127 range.  VP appears to
-                    // ignore negative Z axis values.
-                    if (calBtnState == 3)
-                    {
-                        // calibrating - note if we're expanding the calibration envelope
-                        if (pos < plungerMin)
-                            plungerMin = pos;   
-                        if (pos > plungerMax)
-                            plungerMax = pos;
-                            
-                        // normalize to the full physical range while calibrating
-                        znew = int(float(pos)/npix * 127);
-                    }
-                    else
-                    {
-                        // running normally - normalize to the calibration range
-                        if (pos < plungerMin)
-                            pos = plungerMin;
-                        if (pos > plungerMax)
-                            pos = plungerMax;
-                        znew = int(float(pos - plungerMin)/(plungerMax - plungerMin + 1) * 127);
-                    }
-                    
-                    // done
-                    break;
+                    // calibrating - note if we're expanding the calibration envelope
+                    if (pos < cfg.d.plungerMin)
+                        cfg.d.plungerMin = pos;   
+                    if (pos > cfg.d.plungerMax)
+                        cfg.d.plungerMax = pos;
+                        
+                    // normalize to the full physical range while calibrating
+                    znew = int(float(pos)/npix * 127);
                 }
+                else
+                {
+                    // running normally - normalize to the calibration range
+                    if (pos < cfg.d.plungerMin)
+                        pos = cfg.d.plungerMin;
+                    if (pos > cfg.d.plungerMax)
+                        pos = cfg.d.plungerMax;
+                    znew = int(float(pos - cfg.d.plungerMin)
+                        / (cfg.d.plungerMax - cfg.d.plungerMin + 1) * 127);
+                }
+                
+                // done
+                break;
             }
-            
-            // reset the timer
-            ccdTimer.reset();
-            t0ccd = ccdTimer.read_ms();
-        } 
+        }
         
         // read the accelerometer
         float xa, ya;
@@ -391,26 +495,79 @@
         // figure the new mouse report data
         int xnew = (int)(127 * xa);
         int ynew = (int)(127 * ya);
+
+        // store the updated joystick coordinates
+        x = xnew;
+        y = ynew;
+        z = znew;
         
-        // send an update if the position has changed
-        // if (xnew != x || ynew != y || znew != z)
+        // if we're in USB suspend or disconnect mode, spin
+        if (js.isSuspended() || !js.isConnected())
         {
-            x = xnew;
-            y = ynew;
-            z = znew;
+            // go dark (turn off the indicator LEDs)
+            led2 = 1;
+            led3 = 1;
+            led1 = 1;
+            
+            // wait until we're connected and come out of suspend mode
+            while (js.isSuspended() || !js.isConnected())
+            {
+                // spin for a bit
+                wait(1);
+                
+                // if we're not suspended, flash red; otherwise stay dark
+                if (!js.isSuspended())
+                    led1 = !led1;
+            }
+        }
 
-            // Send the status report.  Note that the X axis needs to be
-            // reversed, becasue the native accelerometer reports seem to
-            // assume that the card is component side down.
-            js.update(x, -y, z, 0);
-        }
+        // Send the status report.  Note one of the axes needs to be
+        // reversed, because the native accelerometer reports seem to
+        // assume that the card is component side down; we have to
+        // reverse one or the other axis to account for the reversed
+        // coordinate system.  It doesn't really matter which one,
+        // but reversing Y seems to give intuitive results when viewed
+        // in the Windows joystick control panel.  Note that the 
+        // coordinate system we report is ultimately arbitrary, since
+        // Visual Pinball has preference settings that let us set up
+        // axis reversals and a global rotation for the joystick.
+        js.update(x, -y, z, 0);
         
-        // show a heartbeat flash in blue every so often
-        if (hbTimer.read_ms() - t0Hb > 1000) 
+        // show a heartbeat flash in blue every so often if not in 
+        // calibration mode
+        if (calBtnState < 2 && hbTimer.read_ms() - t0Hb > 1000) 
         {
-            // invert the blue LED state
-            hb = !hb;
-            led3 = (hb ? .5 : 1);
+            if (js.isSuspended())
+            {
+                // suspended - turn off the LEDs entirely
+                led1 = 1;
+                led2 = 1;
+                led3 = 1;
+            }
+            else if (!js.isConnected())
+            {
+                // not connected - flash red
+                hb = !hb;
+                led1 = (hb ? 0 : 1);
+                led2 = 1;
+                led3 = 1;
+            }
+            else if (flash_valid)
+            {
+                // connected, NVM valid - flash blue/green
+                hb = !hb;
+                led1 = 1;
+                led2 = (hb ? 0 : 1);
+                led3 = (hb ? 1 : 0);
+            }
+            else
+            {
+                // connected, factory reset - flash yellow/green
+                hb = !hb;
+                led1 = (hb ? 0 : 1);
+                led2 = 0;
+                led3 = 0;
+            }
             
             // reset the heartbeat timer
             hbTimer.reset();