Mirror with some correction

Dependencies:   mbed FastIO FastPWM USBDevice

Revision:
112:8ed709f455c0
Parent:
111:42dc75fbe623
Child:
113:7330439f2ffc
--- a/main.cpp	Mon Feb 22 06:57:59 2021 +0000
+++ b/main.cpp	Thu Apr 29 19:56:49 2021 +0000
@@ -306,6 +306,13 @@
 
 // --------------------------------------------------------------------------
 // 
+// placement new
+//
+void* operator new (size_t, void *p) { return p; }
+
+
+// --------------------------------------------------------------------------
+// 
 // OpenSDA module identifier.  This is for the benefit of the Windows
 // configuration tool.  When the config tool installs a .bin file onto
 // the KL25Z, it will first find the sentinel string within the .bin file,
@@ -4119,9 +4126,9 @@
 // I2C address of the accelerometer (this is a constant of the KL25Z)
 const int MMA8451_I2C_ADDRESS = (0x1d<<1);
 
-// SCL and SDA pins for the accelerometer (constant for the KL25Z)
-#define MMA8451_SCL_PIN   PTE25
-#define MMA8451_SDA_PIN   PTE24
+// I2C pins for the accelerometer (constant for the KL25Z)
+#define MMA8451_SDA_PIN   PTE25
+#define MMA8451_SCL_PIN   PTE24
 
 // Digital in pin to use for the accelerometer interrupt.  For the KL25Z,
 // this can be either PTA14 or PTA15, since those are the pins physically
@@ -4164,18 +4171,13 @@
 class Accel
 {
 public:
-    Accel(PinName sda, PinName scl, int i2cAddr, PinName irqPin, 
-        int range, int autoCenterMode)
-        : mma_(sda, scl, i2cAddr)        
+    Accel(const Config &cfg) : mma_(MMA8451_SDA_PIN, MMA8451_SCL_PIN, MMA8451_I2C_ADDRESS)        
     {
-        // remember the interrupt pin assignment
-        irqPin_ = irqPin;
-        
         // remember the range
-        range_ = range;
+        range_ = cfg.accel.range;
         
         // set the auto-centering mode
-        setAutoCenterMode(autoCenterMode);
+        setAutoCenterMode(cfg.accel.autoCenterTime);
         
         // no manual centering request has been received
         manualCenterRequest_ = false;
@@ -4184,6 +4186,62 @@
         reset();
     }
     
+    // Do a full reset of the object.  This tries to clear the I2C
+    // bus, and then re-creates the Accel object in place, running
+    // through all of the constructors again.  This is only a "soft"
+    // reset, since the KL25Z doesn't give us any way to do a power
+    // cycle on the MMA8451Q from software - its power connection is
+    // hardwired to the KL25Z's main board power connection, so the
+    // only way to power cycle the accelerometer is to power cycle
+    // the whole board.
+    //
+    // We use this to try to reset the accelerometer if it stops
+    // sending us new samples.  I've received a few reports from
+    // people who say their accelerometers seem to stop working even
+    // though the rest of the firmware is still functioning normally,
+    // which suggests that there's either a problem in the Accel class
+    // itself, or that the MMA8451Q can get into a non-responsive state
+    // under some circumstances.  Since the reports have been extremely
+    // rare and isolated, and since I've never myself seen this happen
+    // on any of the multiple KL25Z boards I've tested with (even after
+    // leaving them running for days at a time), my best guess is that
+    // it's actually a fault in the MMA8451Q.  The fact that everyone
+    // who's experienced the accelerometer freeze says that the rest of
+    // the firwmare is still working supports this hypothesis - given
+    // that the firmware is single-threaded, it seems unlikely that a
+    // "crash" of some kind in the accelerometer code wouldn't crash
+    // the firmware as a whole.  This soft reset code is an attempt to
+    // recover from a scenario where the MMA8451Q hardware is still
+    // functioning properly, but its internal state machine is somehow
+    // out of sync with the host in such a way that it can no longer
+    // send us samples - either its I2C state machine is stuck in the
+    // middle of a transaction, or its sample processing state machine
+    // is no longer taking samples.  The soft reset doesn't have any
+    // hope of rebooting the chip if the freeze is due to some kind
+    // of hardware fault, because our only connection to the chip is
+    // the I2C bus, and there's no reason to think its I2C state
+    // machine would even be running in the event of a hardware fault.
+    // Hopefully we can find out which it is by testing this fix on
+    // boards where the problem is known to have occurred, since it
+    // seems to be readily repeatable for the people who experience
+    // it at all.
+    static void softReset(Accel *accel, const Config &config)
+    {
+        // save the current centering position, so that the user
+        // doesn't see a jump across the reset
+        int cx = accel->cx_, cy = accel->cy_;
+        
+        // try to reset the I2C bus, in case that's 
+        accel->clear_i2c();
+        
+        // re-construct the Accel object
+        new (accel) Accel(config);
+
+        // restore the center point
+        accel->cx_ = cx;
+        accel->cy_ = cy; 
+    }
+    
     // Request manual centering.  This applies the trailing average
     // of recent measurements and applies it as the new center point
     // as soon as we have enough data.
@@ -4220,6 +4278,81 @@
         }
     }
     
+    // Clear the I2C bus for the MMA8451Q.  This seems necessary some of the time
+    // for reasons that aren't clear to me.  Doing a hard power cycle has the same
+    // effect, but when we do a soft reset, the hardware sometimes seems to leave
+    // the MMA's SDA line stuck low.  Presumably, the MMA8451Q's internal state
+    // machine is still in the middle of an I2C transaction, and it expects the
+    // host to clock in/out the rest of the bits for the transaction.  Forcing a
+    // series of clock pulses through SCL is the standard remedy for this type
+    // of situation, since it should force the state machine to the end of the
+    // I2C state it's stuck in so that it's ready to start a new transaction.
+    // This really shouldn't be necessary, because the mbed library I2C code that
+    // we're using in the MMA8451Q driver appears to do the same thing when it
+    // sets up the I2C pins, but it should at least be harmless.  What we really
+    // need is a way to power-cycle the MMA8451Q, but the KL25Z simply isn't
+    // wired to do that from software; the only way is to power-cycle the whole
+    // board.
+    // 
+    // If the accelerometer does get stuck, and a software reboot doesn't reset
+    // it, the only workaround is to manually power cycle the whole KL25Z by 
+    // unplugging both of its USB connections.
+    //
+    // The entire Accel object must be re-constructed after calling this,
+    // because this reconfigures the I2C SDA/SCL pins as plain digital in/out
+    // pins.  They have to be reconfigured as I2C pins again by the I2C
+    // constructor after this is called.
+    static bool clear_i2c()
+    {
+        // set up both pints as input pins
+        DigitalInOut pin_sda(MMA8451_SDA_PIN, PIN_INPUT, PullNone, 1);
+        DigitalInOut pin_scl(MMA8451_SCL_PIN, PIN_INPUT, PullNone, 1);
+
+        // if SCL is being held low, the bus is locked by another device;
+        // wait a couple of milliseconds and then give up
+        Timer t;
+        t.start();
+        while (pin_scl == 0 && t.read_us() < 2000) { }
+        if (pin_scl == 0)
+            return false;
+
+        // if SDA and SCL are both high, the bus is free
+        if (pin_sda == 1)
+            return true;
+
+        // Send a series of clock pulses to try to knock the device out
+        // of whatever I2C transaction it thinks it's in the middle of.
+        // 9 pulses should be sufficient for a device with byte commands,
+        // but do some extra for good measure, in case it's in some kind
+        // of multi-byte transaction.
+        pin_scl.mode(PullNone);
+        pin_scl.output();
+        for (int count = 0; count < 35; count++) 
+        {
+            pin_scl.mode(PullNone);
+            pin_scl = 0;
+            wait_us(5);
+            pin_scl.mode(PullUp);
+            pin_scl = 1;
+            wait_us(5);
+        }
+
+        // Send Stop
+        pin_sda.output();
+        pin_sda = 0;
+        wait_us(5);
+        pin_scl = 1;
+        wait_us(5);
+        pin_sda = 1;
+        wait_us(5);
+
+        // confirm that both SDA and SCL are now high, indicating that
+        // the bus is free
+        pin_sda.input();
+        pin_scl.input();
+        return (pin_scl != 0 && pin_sda != 0);
+    }
+
     void reset()
     {
         // clear the center point
@@ -4244,16 +4377,34 @@
         
         // read the current registers to clear the data ready flag
         mma_.getAccXYZ(ax_, ay_, az_);
+        
+        // start the FIFO timer
+        fifoTimer.reset();
+        fifoTimer.start();
+        tLastSample = tLastChangedSample = fifoTimer.read_us();
     }
     
-    void poll()
+    // Poll the accelerometer.  Returns true on success, false if the
+    // device appears to be wedged (because we haven't received a unique
+    // sample in a long time).  The caller can try re-creating the Accel
+    // object if the device is wedged.
+    bool poll()
     {
         // read samples until we clear the FIFO
         while (mma_.getFIFOCount() != 0)
         {
+            // read the raw data
             int x, y, z;
             mma_.getAccXYZ(x, y, z);
             
+            // note the time
+            tLastSample = fifoTimer.read_us();
+            
+            // note if this sample differs from the last one, to see if
+            // the accelerometer appears to be stuck
+            if (x != ax_ || y != ay_ || z != az_)
+                tLastChangedSample = tLastSample;
+            
             // add the new reading to the running total for averaging
             xSum_ += (x - cx_);
             ySum_ += (y - cy_);
@@ -4264,8 +4415,59 @@
             ay_ = y;
             az_ = z;
         }
+        
+        // If we haven't seen a new sample in a while, the device
+        // might be stuck.  Some people have observed an apparent
+        // freeze in the accelerometer readings even while the
+        // pluger and key inputs continue working, which seems
+        // like it must be due to something stuck on the MMA8451Q.
+        // The caller can try a software reset in that case, by
+        // re-creating the Accel object.  That will go through
+        // all of the I2C and MMA8451Q intialization code again
+        // to try to get things back to a good state.
+        //
+        // We poll about every 2.5ms (or more often, depending on
+        // the plunger sensor type), and we have the accelerometer
+        // set to generate samples at 800 Hz = every 1.25ms, so it
+        // would definitely indicate trouble if the last samples
+        // from the device are older than 5ms.  As for *unique*
+        // samples, that's a harder call, since it depends on how
+        // much background noise there is.  Given the sensitivity
+        // of the device, though, my experience is that nearly
+        // every sample will have at least one bit of difference
+        // from the last, so it's unlikely to see more than a few
+        // identical samples in a row, and extremely unlikely to
+        // see, say, 10 or 20 consecutive identical readings.  To
+        // be conservative, we'll time out the existence of a
+        // reading at 100ms, and unique readings at 2s.  This
+        // should reset a non-responsive device well before the
+        // freeze becomes apparent to the user (unless they're
+        // deliberately looking for it), but should also ensure
+        // that we don't reset unnecessarily - 2s represents 1600
+        // consecutive identical samples, and I think the odds of
+        // that happening for real are practically zero, barring
+        // some kind of test bed with extreme vibration suppression.
+        uint32_t tNow = fifoTimer.read_us();
+        if (static_cast<uint32_t>(tNow - tLastSample) > 100000  // 100 ms
+            || static_cast<uint32_t>(tNow - tLastChangedSample) > 2000000) // 2 seconds
+        {
+            // appears to be wedged
+            return false;
+        }
+        
+        // okay
+        return true;
     }
-
+    
+    // timer, for monitoring incoming FIFO samples
+    Timer fifoTimer;
+    
+    // time of last sample from FIFO
+    uint32_t tLastSample;
+    
+    // time of last *different* sample from FIFO
+    uint32_t tLastChangedSample;
+    
     void get(int &x, int &y) 
     {
         // read the shared data and store locally for calculations
@@ -4407,7 +4609,7 @@
     
     // atuo-centering timer
     Timer tCenter_;
-
+    
     // Auto-centering history.  This is a separate history list that
     // records results spaced out sparsely over time, so that we can
     // watch for long-lasting periods of rest.  When we observe nearly
@@ -4423,43 +4625,8 @@
     uint8_t iAccPrv_, nAccPrv_;
     static const uint8_t maxAccPrv = 5;
     AccHist accPrv_[maxAccPrv];
-    
-    // interurupt pin name
-    PinName irqPin_;
 };
 
-// ---------------------------------------------------------------------------
-//
-// Clear the I2C bus for the MMA8451Q.  This seems necessary some of the time
-// for reasons that aren't clear to me.  Doing a hard power cycle has the same
-// effect, but when we do a soft reset, the hardware sometimes seems to leave
-// the MMA's SDA line stuck low.  Forcing a series of 9 clock pulses through
-// the SCL line is supposed to clear this condition.  I'm not convinced this
-// actually works with the way this component is wired on the KL25Z, but it
-// seems harmless, so we'll do it on reset in case it does some good.  What
-// we really seem to need is a way to power cycle the MMA8451Q if it ever 
-// gets stuck, but this is simply not possible in software on the KL25Z. 
-// 
-// If the accelerometer does get stuck, and a software reboot doesn't reset
-// it, the only workaround is to manually power cycle the whole KL25Z by 
-// unplugging both of its USB connections.
-//
-void clear_i2c()
-{
-    // set up general-purpose output pins to the I2C lines
-    DigitalOut scl(MMA8451_SCL_PIN);
-    DigitalIn sda(MMA8451_SDA_PIN);
-    
-    // clock the SCL 9 times
-    for (int i = 0 ; i < 9 ; ++i)
-    {
-        scl = 1;
-        wait_us(20);
-        scl = 0;
-        wait_us(20);
-    }
-}
-
 
 // ---------------------------------------------------------------------------
 //
@@ -6534,7 +6701,7 @@
     NewPwmUnit::defaultPeriod = 0.0005f;
         
     // clear the I2C connection
-    clear_i2c();
+    Accel::clear_i2c();
     
     // Elevate GPIO pin interrupt priorities, so that they can preempt
     // other interrupts.  This is important for some external peripherals,
@@ -6745,8 +6912,7 @@
     acTimer.start();
     
     // create the accelerometer object
-    Accel accel(MMA8451_SCL_PIN, MMA8451_SDA_PIN, MMA8451_I2C_ADDRESS, 
-        MMA8451_INT_PIN, cfg.accel.range, cfg.accel.autoCenterTime);
+    Accel accel(cfg);
        
     // initialize the plunger sensor
     plungerSensor->init();
@@ -6815,7 +6981,8 @@
         LwChimeLogicOut::poll();
         
         // poll the accelerometer
-        accel.poll();
+        if (!accel.poll())
+            Accel::softReset(&accel, cfg);
             
         // Note the "effective" plunger enabled status.  This has two
         // components: the explicit "enabled" bit, and the plunger sensor