Bicycle Computer #6 – More Complex Sensor Integration

This is the sixth in a series of articles demonstrating how someone with modest .NET programming capabilities can now write applications for embedded devices using the .NET Micro Framework and Visual Studio.  If you would like to start from the beginning, the first article can be found here.  This particular article is a little long but we get a lot done here in terms of defining our product. 

Before we start, I took the devices for a ride (in the car) while testing the code for this article and found the interface to be almost unusable in sunlight. I have redone the colors for the mainWindow background and the buttons.  I’ll see if this works well enough or whether I need to step up to a higher quality (more expensive) display. 

Ok – it’s time for the bake-off between using a pressure sensor and or an inclinometer for feedback on how hard we are working during our ride (as if you didn’t know already). If you recall, the pressure sensor option is less expensive but there were questions about the accuracy of the feedback that it could give. Pressure sensors provide measurements in Pascals (Pa) with .3 kiloPascals roughly the equivalent to one foot in elevation at sea level. Inclinometers are basically two axis accelerometers that can resolve the angle of the sensor relative to some zero point.  There are several ways that this information can be returned to you but the most useful is just the positive or negative angle. 

The real point of this article is to show that you can build drivers for sophisticated peripherals in managed code – we’ll do that for both of these sensors to facilitate a decision about which one to use in the product. To remind you – the pressure sensor is the  VTI SCP1000 D01 and the inclinometer is the Analog Devices ADIS16203. Both of these sensors use the SPI interface. We could have put both sensors on one SPI port but there were some warnings on one of the discussion groups that this could cause problems for the SCP1000 so we will connect them with different SPI ports.

If you are not familiar with the SPI interface, don't worry – it is a pretty simple serial interface when the runtime is handling all the details.  It requires 4 wires - a clock to synchronize the communications, input and output wires, and a chip selection wire so that you can have multiple peripherals on the bus and select which one you are talking to. (In that case, of course, your processor board would have an additional chip select wire for each peripheral.)

SPI works like a shift register, so remember that when you are sending a bit to an SPI device, the device is sending one to you on the same clock cycle.  The runtime and the SPI type hide this details and all you have to do is coming up with the right configuration for you SPI instance.  You can derive that from the timing diagrams of the device data sheet.

In terms of using the bus, you need to configure each connection as you will see below and then invoke Write and WriteRead methods.  In most cases, you write an address/command and read the return in the same method call.  Wikipedia has a pretty clear explanation of the protocols here.  It includes the kind of timing diagrams that you will find in most specs that you need to understand to configure the SPI port (eg whether the bits are ‘good’ on the rising edge of the clock cycle or the falling edge).  If you get the configuration correct, the runtime will make the rest work.

Pressure Sensor

The pressure sensor that I started with is the SCP1000 from VTI that I ordered from Sparkfun. The specification can be found here if you want to check it out. When writing this driver, the critical things were to carefully read the diagram on the communications so that the SPI port is set up correctly and reading the startup descriptions to understand how the sensor starts up and what opportunities we have for checking for errors. For example, the clock line is low when the chip is not selected or communicating and the data is sampled on the rising edge of the clock cycle as can be seen in the diagram in the specification.

The VTI SCP1000 Product Specification includes the following SPI frame description:

image

From this, the SPI Port is configured as follows

// set up the SPI port
           SPI.Configuration pressureSensorConfiguration = new SPI.Configuration(
            GHIElectronics.NETMF.Hardware.EMX.Pin.IO11,        // use GPIO pin IO11 for the chip select (in case there are multiple SPI devices on this bus
            false,                                             // set the pin low to access the sensor
            0,                                                 // setup time set to 0
            0,                                                 // set delay after transfer to 0
            false,                                             // clock line is low when chip is not selected/communicating
            true,                                              // data is sampled on the rising edge of the clock cycle
            500,                                               // 500 is the max clock rate that this sensor supports
            SPI.SPI_module.SPI2                  // the device will be connected to SPI port #2
            );

          pressureSensor = new SPI( pressureSensorConfiguration);

The sensor supports multiple modes of operation. To keep the power consumption to a minimum, I selected the triggered mode. In this mode, the device stays in standby until I ask for data (either through a hardware pin or by writing to a register). I tried it both ways and found no differences in operation so I used the register to trigger data collection. Once triggered, the sensor makes a measurement and then sets a ‘data ready’ GPIO pin high. I set up an InterruptPort so that an interrupt routine would gather the data once it is available.

// Set up the DRDY InterruptPort on IO26
_DRDY = new InterruptPort(GHIElectronics.NETMF.Hardware.EMX.Pin.IO26, true, Port.ResistorMode.PullDown, Port.InterruptMode.InterruptEdgeHigh);

Setting up the triggered mode required several SPI writes since this particular register can only be addressed indirectly.

// Select the low power triggered mode

           writeBuffer[0] = c_Register__ADDPTR | c_Direction__WRITE;
           writeBuffer[1] = 0x00;
           pressureSensor.Write(writeBuffer);
           writeBuffer[0] = c_Register__DATAWR | c_Direction__WRITE;
           writeBuffer[1] = 0x05;
           pressureSensor.Write(writeBuffer);
           writeBuffer[0] = c_Register__OPERATION | c_Direction__WRITE;
           writeBuffer[1] = 0x02;
           pressureSensor.Write(writeBuffer);
           System.Threading.Thread.Sleep(50);

           _DRDY.OnInterrupt += new NativeEventHandler(drdy_OnInterrupt);

The setup of the sensor continues with several validity checks to make sure that the device comes up correctly. These are listed as optional but it is very helpful to do this so that you know that they sensor is functioning correctly at the start. For now, I just put in exceptions if the results are not what are needed.

// loop testing for successful completion of startup
           byte status = 0xFF;
           for (int i = 0; i < 10; i++)
           {
               status = CheckStatus();
               if(verbose) Debug.Print("Startup -- STATUS: " + status.ToString());
               if ((status & 0x01) == 0) break;
               System.Threading.Thread.Sleep(10);
           }
           if ((status & 0x01)  != 0)
           {
              throw new Exception( " Error in completing startup " );
           }

           // check that startup is completed
           writeBuffer[0] = c_Register__DATARD8 | c_Direction__READ;
           pressureSensor.WriteRead(writeBuffer, readBuffer,1);
           if ((readBuffer[0] & 0x01) != 1)
           {
              throw new Exception( " Error in Checksum after startup " );
           }

This power up process is started when the device first gets power.  That means that if I power it up with board, the startup will have been completed before the application has started running and my ability to confirm that it started up Ok will have been lost.  So, I am powering the sensor from a GPIO pin and I can power the sensor and perform these tests during the sensor constructor.

The last interesting bit of the driver is reading and computing the data. This sensor provides both temperature and pressure so we will read them both. The construction of valid values for these measurements just requires a careful read of what you are getting back.

register[0] = c_Register__TEMPOUT | c_Direction__READ; //  temperature register address
pressureSensor.WriteRead(register, tempBytes,1); 

register[0] = c_Register__DATARD8 | c_Direction__READ;   // DATARD8 register address
pressureSensor.WriteRead(register, pressureByteMSB, 1);// just returning one byte - in which byte?
register[0] = c_Register__DATARD16 | c_Direction__READ;  //DATARD16 register Addresss
pressureSensor.WriteRead(register, pressureByteLSB, 1);

// compute temperature
// temperature is 2's compliment
if (((int) tempBytes[0] & (int) 0x20) > 0)
{
     temperature = (0x1F & ~tempBytes[0]) << 8;
     temperature += ~tempBytes[1];
     temperature++;
     temperature *= -1;
}
else
{
     temperature = (0x1F & tempBytes[0]) << 8;
     temperature += tempBytes[1];
}
temperature /= 20;

// compute pressure
pressure = (0x07 & pressureByteMSB[0]) << 16;
pressure +=        pressureByteLSB[0]  << 8;
pressure +=        pressureByteLSB[1]  << 0;
pressure >>= 2;

//compute height in feet
// source of algorithm: https://www.wrh.noaa.gov/slc/projects/wxcalc/formulas/pressureAltitude.pdf
heightFt = pressure / 101325.0;
heightFt = System.Math.Pow(heightFt, 0.190284);
heightFt = (1 - heightFt) * 145366.45;

There is some further cleanup possible with this code if we move forward with the pressure feedback in the product. The full text of this driver is listed at …..

Incline Sensor

The inclinometer that I use is the ADIS16203/PCBZ.  That last bit means that it comes on a PCB board with exposed connectors.  There is a place for a bypass capacitor and a .1 uF one is specified but not included (what’s with that?).  The common wisdom is to use one with twice the voltage that you are using to power the device (I used one rated at 8 V.)

Again, we start with the SPI configuration ( you will find the frame descitpion diagram below..

public InclineSensor()
       {
           SPI.Configuration InclinometerConfig = new SPI.Configuration(
            GHIElectronics.NETMF.Hardware.EMX.Pin.IO11,    //use GPIO pin 11 since it is close to SPI1
            false,                                         //set pin 11 low to access the inclinometer
            0,                                             //setup time required in ms 
            0,                                             //hold chip select low for 0 ms after transfer
            true,                                          //clock line is high when chip not selected/communicating
            true,                                          //data is sampled on the rising edge of clock signal
            800,                                           //clock rate in Hz
            SPI.SPI_module.SPI1                            //select SPI1 channel for communication
                    );

           inclineSensor = new SPI(InclinometerConfig);
       }

There is really only one operational mode in this sensor so I will leave all the default sampling settings for now to see what kind of output this sensor gives me.  Here is how I sample the incline.  The error checking is very rudimentary at this point because I just want to know what kinds of errors and how often for now while we are just selecting the sensors to use.  I am reading the +/- 180 degree register to have positive and negative measures for up and down hills.  This register returns a 2’s complement value.

One interesting nuance that I missed initially, is that writing the register address and reading the contents have to be in two separate SPI transactions.  In the pressure sensor, you saw that I could send a WriteRead transaction and as long as I offset the read by one frame (the time for the device to take the address) I get the contents back immediately.  It took a second set of eyes to point out to me that in this device, the Chip Select line comes up between the two events which means that a separate Write transaction was required.

image

public double GetIncline()
     {
         ushort[] writeBuffer = new ushort[] { 0 };
         ushort[] readBuffer = new ushort[] { 0 };
         double _incline;

         //Get the current incline
         writeBuffer[0] = c_INCL_180_OUT;
         readBuffer[0] = 0;
         inclineSensor.Write(writeBuffer);
         inclineSensor.WriteRead(writeBuffer, readBuffer); 
         if (((int)readBuffer[0] & (int)c_SignBitFilter) > 0)
         {
             _incline = ~readBuffer[0] & c_InclineFilter;
             _incline++;
             _incline *= -1;
         }
         else
         {
             _incline = readBuffer[0] & c_InclineFilter;
         } 
           _incline = _incline * .02778;       //converts to incline in %, * .025 is incline in degrees

         Debug.Print("Current incline is: " + _incline + "%");

         //Read status register - note I did not/could not test these
         writeBuffer[0] = c_STATUS;
         readBuffer[0] = 0;
         inclineSensor.Write(writeBuffer);
         inclineSensor.WriteRead(writeBuffer, readBuffer);

         if (((int)readBuffer[0] & (int)c_StatusOK_Mask) < 1)
         {
             Debug.Print("All Systems Operational");
         }
         else
         {
           if (((int)readBuffer[0] & (int)c_SelfTest_Mask) > 0)
           {
             throw new Exception("Self Test Error");
           }
           if (((int)readBuffer[0] & (int)c_SPIComm_Mask) > 0)
           {
             throw new Exception("SPI Comm Failure");
           }
           if (((int)readBuffer[0] & (int)c_Control_Mask) > 0)
           {
             throw new Exception("Control Register Update Failed");
           }
           if (((int)readBuffer[0] & (int)c_VoltageSupply_Mask) > 0)
           {
             throw new Exception("Voltage Supply over 3.625V or under 2.975");
           }

         }

         return _incline;
     }

The temperature is returned in the same way.

const int c_minRawTemperature = 1065;

const int c_maxRawTemperature = 1416;

public double GetTemperature(string Units)
{
    ushort[] writeBuffer = new ushort[] { 0 };
    ushort[] readBuffer = new ushort[] { 0 };
    double _temperature;

    writeBuffer[0] = c_TEMP_OUT;
    readBuffer[0] = 0;
    inclineSensor.Write(writeBuffer);
    inclineSensor.WriteRead(writeBuffer, readBuffer);
    _temperature = (int)readBuffer[0] & c_TempFilter;
    if (_temperature > c_maxRawTemperature || _temperature < c_minRawTemperature)
    {
        throw new Exception("Error reading temperature");
    }
    else
    {
        _temperature = (((1416 - _temperature) * .47) - 40);

        if (Units == "English")
        {
        _temperature = _temperature * 9 / 5 + 32;
        }

        Debug.Print("Temperature is: " + _temperature.ToString() + " (" + Units + ")" );

    }
    return _temperature;
}

Conclusion

The pressure sensor claimed 9cm accuracy and in fairness, it generated pressure values to hundredths of a millibar (hPa). Unfortunately, the measurements were not stable at that same degree of accuracy and the results drifted around a 5 foot range just sitting on my desk.  I also found that during the duration of a long ride (several hours) ambient barometric pressure could change the readings by several hundred feet.  The result is that I could not get an accuracy that gave me a useful reflection of the slope that I was on. 

If the instability were not enough of an issue, I found out after integrating the sensor that VTI had announced that they were ‘end of life’-ing the product so it is not one that we want to build a product around. There is a good lesson here though. We hear from customers regularly that they used a component that was no longer available. With the hardware virtualization in NETMF, changing a hardware component has no impact on the application itself – just on the driver. This means that even changing an LCD to one with different metrics can be done easily.

The inclinometer on the other hand seems to be very sensitive and stable  and the information is easy to interpret.  This is a significant product decision since this sensor, at ~$34 each) is one of the least expensive inclinometers out there so it will add cost to our end solution (about $30).  I had already moved up to a $24 pressure sensor in the VTI SCP1000 to get the most sensitive one that I could. 

To fully integrate the inclinometer into the solution, there is still lots to do. It has no command driven sleep states so I will power it through a GPIO so that I can turn it on only when needed (ie write the Pause() and Restart() methods).  A quick check of this shows that the GPIO from the board that I am using does not supply enough current for this sensor so rather than powering the sensor directly from the GPIO, I will use the GPIO to run a transistor that will switch power from the 3.3 volt output pin.  Transistors can switch low voltage DC.  For higher voltage or an AC current or current > 5 A, use a relay.  The curcuit looks like:

image

I will also use the sleep timer on this device to reduce the power consumption of the inclinometer itself and to drive the UI updates.  That will cut down processor cycles as well  but requires that I add an InterruptPort from the sensor to the processor to trigger the data read when new data is ready.  I also need to add the ability to zero the inclinometer so that the user is not required to mount the device perfectly level.  I will add that to the settings menu. 

Things are coming together.  I am almost ready to put a rudimentary unit on a bicycle.