Pulse Sensor II: Getting the Lights On

Modern Device >>

Analog vs Digital Sensors

When it comes to sensors I was always an analog guy. Analog sensors put out voltages or currents that are easy to verify with a multimeter. Or if you really need a pretty picture then you can hook up an oscilloscope and see a very complete story. Analog sensors tend to be simple, and have datasheets that tend to be under ten pages long. However, these days, most of the time, analog voltages are going to get run into A/D converters for some further digital domain processing and decision making.

So the chip makers have decided that if the signal needed to be digitized, why bother running the signal into a microcontroller, and perhaps degrading the signal along the way? Why not instead build the A/D converter into the chip and give the customer a signal that has been pre-sliced and pre-diced. And while the chip makers are adding an A/D converter, how about some signal processing and adaptive sensing? Why not build a whole small processor into the sensor to try to give the user options. Perhaps 200 or so options? So simple sensor chips are leveraging their digital interfaces and turning into swiss army knives of options and functionality

And to help the user get acquainted with all those high-powered options, the chips need to have datasheets that explain the options. Sometimes 78 pages long. So you might ask why a 78 page datasheet is necessary  just to read some photodiodes with accompanying LEDs? And I guess the answer would be: Yes, we are going to turn on some LEDs and read some photodiodes, but with lots of options and flexibility for various users and applications.

So the Silicon Labs SI1143 is a wonderful little swiss-army knife of a sensor. It slices, it dices. [A reference to American infomercials – guess I’m severely dating myself.] Rather than try to describe the awesome functionality of this chip, I’ll just post the top of the datasheet.

Pretty sexy eh? (Well sexy to some demograhpic segments I’ll admit.) And the datasheet didn’t even say you could build a pulse oximeter out of it. How narrow of the engineers.

Here is the board we spun up with the sensor on it. Large enough to get a fat finger like mine on the board.

Design Decisions

I wanted to use the sensors with some JeeNode boards in one of my classes so I decided to only include a JeeNode header. Many of the other Modern Device sensors that feature 3.3V chips also include a 5 volt regulator and level shifting circuitry for use with 5 volt Arduinos. Because the JeeNodes run at 3.3 volts, this wouldn’t be necessary. I also figured that a couple of current limiting resistors would be enough to use the chip with a 5 volt ‘duino, as long as the sensor was powered at 3.3 volts. We confirmed that this was indeed the case, it is easy to run the sensors on any Arduino as long as you power them with 3.3 volts (NOT 5 volts). Interfacing the SDA and SCL lines with 10K resistors shouldn’t really be necessary, because the I2C protocol was intended to accommodate daisy-chained chips on the same lines by insisting that the signal lines would never be driven high, only pulled up by a resistor. The resistors are a good idea in case your code accidentally drives the SDA or SCL lines high. This would be in opposition to the I2C protocol, but hey, things happen when coding. At least in my coding.

Code Prove Out

I am mostly a self-taught coder, with a few C-based courses along the way, so after banging my head against the I2C protocol and the JeeLabs implementation of it for a half day or so, I asked for some help from Jean Claude Wippler, my collaborator at JeeLabs. Seemingly within the hour (but probably more like 4 hours later) Jean-Claude had hacked together a very nice framework for experimentation. All the hard parts for writing to the registers and the parameter ram were done, so I could fill in any details that Jean-Claude had not covered.

The 78 page datasheet that I was talking about earlier, actually belongs to the SI1143 so as you can imagine, there are lots of details to take care of (or become aware of before the lights can go on). I’m not sure how thrilling it would be if I talked you through every memory register that has to be addressed, so I’ll just go through somn bullet points of (some of) the chip’s options that we used in the code.

  • The SI1143 has provisions for taking readings with and without the LEDs turned on (ambient light or with the LEDs). This is handy for using the chip as a proximity sensor (as it was intended) so that one may subtract the ambient light hitting the sensor, then take a reading with the lights on. Our default code has an option to read ambient light.
  • The chip can either read in an autonomous loop, or you can ask it to take a measurement. After some experimentation with “manual” operation, autonomous readings seemed to be much faster, so I elected to use that mode.
  • There are registers for selecting which LEDs will be read on which read passes through the chip read loop. You can select any of the LEDs for any of the three reads. I kept it simple and just read one LED per read. (Are you confused yet with the options?)
  • There are two different photo detectors which can be read. The smaller one is good for brighter (outdoor sunlight?) environments. I used the larger, more sensitive setting.
  • The current of each LED can be controlled. It’s easy to experiment by setting a hex digit for each LED.
  • There are a host of options for having the chip issue interrupts under certain circumstances. The chip interrupt is connected to the IRQ pin so that the user may experiment with these options. For example the chip could issue an interrupt at a particular light reading or proximity threshold. This would be good for low-powered operation. An item still on the TODO list is to get interrupts firing on the autonomous reads we are using and to use them to make sure we are not getting data that is been updated across readings, resulting in inacurate hybrid values.
  • There are a lot of options for low-power optimization on the chip, such as the interrupt sensing mentioned above for example, in which a microcontroller could be left in a sleep state. For the SPO2 sensor, I didn’t really experiment with any of these, but for the low-power aficionado (there are lots of them at JeeLabs) there are lots of tricks worth exploring on this chip.

Proving Out The Boards and Proximity Sensing

We got the boards back and experimented with a few different LEDs. What you see is the “v1” form of the sensor we’re currently marketing. It has two infrared LEDs and one visible LED. The infrared LEDs have a more hefty output because they can get rid of some of their heat as infrared energy. The LED current setting of the infrared LEDs is currently set to almost the lowest level (of a 15 step scale). Higher settings easily overload (max out) the light sensor (with finger sensing). For proximity sensing however, the extra horsepower is really useful. I was able to easily sense a wall (really a floor) at 36 inches. Sensing a hand is more tricky since it has a smaller surface to reflect light and I was only able to get a signal out to about 16 inches from the sensor. Smaller and darker objects will probably be even harder to sense, as you might imagine.

Still as a proximity sensor, it has a very wide cone and could replace PIR sensors, and Sharp proximity sensors in many applications. Experimenting with a small lens might be useful and might extend the sensing range significantly, at the expense of a more narrow sensing cone.

Onward to the code

After cleaning up the code from a lot of previous experiments, I’ve come up with a fairly clean demo of the basic functionality of the sensor as pulse sensor. Along the way though, you can also see the functionality of the chip as a proximity sensor.

Here’s the demo, also posted today at GitHub. Click on the Zip file, download the SI114 library and install it in your Arduino libraries folder. If you don’t know how to install an Arduino library see the Arduino libraries page. Run the example code (shown below) in the Arduino IDE by choosing File->Examples->SI114->Examples->SI114_Pulse_Demo.

/* SI114_Pulse_Demo.ino
 * code for the Modern Device SI1143-based pulse sensor
 * code in the public domain
 */

#include 

const int samples = 5;            // samples for smoothing 1 to 10 seem useful
                                  // increase for smoother waveform (with less resolution) 

// #define POWERLINE_SAMPLING     // samples on an integral of a power line period [eg 1/60 sec]

// #define AMBIENT_LIGHT_SAMPLING // also samples ambient slight (slightly slower)

const int portForSI114 = 1;       // change to the JeeNode port number used

/*
   For Arduino users use the following pins for various ports
   Connect pins with 10k resistors in series
  JeeNode Port  SCL ('duino pin)  SDA ('duino pin)
       1             4             14 (A0)
       2             5             15 (A1)
       3             6             16 (A2)
       4             7             17 (A3)
*/

unsigned long lastMillis, red, IR1, IR2;

PortI2C myBus (portForSI114);
PulsePlug pulse (myBus); 

void setup () {
    Serial.begin(57600);
    Serial.println("n[pulse_demo]");

    if (!pulse.isPresent()) {
        Serial.print("No SI114x found on Port ");
        Serial.println(portForSI114);
    }
    Serial.begin(57600);
    digitalWrite(3, HIGH);

    pulse.setReg(PulsePlug::HW_KEY, 0x17);  
    // pulse.setReg(PulsePlug::COMMAND, PulsePlug::RESET_Cmd);

    Serial.print("PART: "); 
    Serial.print(pulse.getReg(PulsePlug::PART_ID)); 
    Serial.print(" REV: "); 
    Serial.print(pulse.getReg(PulsePlug::REV_ID)); 
    Serial.print(" SEQ: "); 
    Serial.println(pulse.getReg(PulsePlug::SEQ_ID)); 

    pulse.setReg(PulsePlug::INT_CFG, 0x03);       // turn on interrupts
    pulse.setReg(PulsePlug::IRQ_ENABLE, 0x10);    // turn on interrupt on PS3
    pulse.setReg(PulsePlug::IRQ_MODE2, 0x01);     // interrupt on ps3 measurement
    pulse.setReg(PulsePlug::MEAS_RATE, 0x84);     // see datasheet
    pulse.setReg(PulsePlug::ALS_RATE, 0x08);      // see datasheet
    pulse.setReg(PulsePlug::PS_RATE, 0x08);       // see datasheet
    pulse.setReg(PulsePlug::PS_LED21, 0x3A);      // LED current for LEDs 1 (red) & 2 (IR1)
    pulse.setReg(PulsePlug::PS_LED3, 0x02);       // LED current for LED 3 (IR2)

    Serial.print( "PS_LED21 = ");                                         
    Serial.println(pulse.getReg(PulsePlug::PS_LED21), BIN);                                          
    Serial.print("CHLIST = ");
    Serial.println(pulse.readParam(0x01), BIN);

    pulse.writeParam(PulsePlug::PARAM_CH_LIST, 0x77);         // all measurements on

    // increasing PARAM_PS_ADC_GAIN will increase the LED on time and ADC window
    // you will see increase in brightness of visible LED's, ADC output, & noise 
    // datasheet warns not to go beyond 4 because chip or LEDs may be damaged
    pulse.writeParam(PulsePlug::PARAM_PS_ADC_GAIN, 0x00);

    pulse.writeParam(PulsePlug::PARAM_PSLED12_SELECT, 0x21);  // select LEDs on for readings see datasheet
    pulse.writeParam(PulsePlug::PARAM_PSLED3_SELECT, 0x04);   //  3 only
    pulse.writeParam(PulsePlug::PARAM_PS1_ADCMUX, 0x03);      // PS1 photodiode select
    pulse.writeParam(PulsePlug::PARAM_PS2_ADCMUX, 0x03);      // PS2 photodiode select
    pulse.writeParam(PulsePlug::PARAM_PS3_ADCMUX, 0x03);      // PS3 photodiode select  

    pulse.writeParam(PulsePlug::PARAM_PS_ADC_COUNTER, B01110000);    // B01110000 is default                                   
    pulse.setReg(PulsePlug::COMMAND, PulsePlug::PSALS_AUTO_Cmd);     // starts an autonomous read loop
    // Serial.println(pulse.getReg(PulsePlug::CHIP_STAT), HEX);  

}


void loop(){

    unsigned long total=0, start;
    int i=0;
    red = 0;
    IR1 = 0;
    IR2 = 0;
    total = 0;
    start = millis();

 #ifdef POWERLINE_SAMPLING

    while (millis() - start < 16){   // 60 hz - or use 33 for two cycles
                                     // 50 hz in Europe use 20, or 40

 #else

        while (i < samples){ 

 #endif


 #ifdef AMBIENT_LIGHT_SAMPLING   

        pulse.fetchData();

 #else
 
        pulse.fetchLedData();

 #endif
        red += pulse.ps1;
        IR1 += pulse.ps2;
        IR2 += pulse.ps3;
        i++;

    }

    red = red / i;  // get averages
    IR1 = IR1 / i;
    IR2 = IR2 / i;
    total = red + IR1 + IR2;

 #ifdef AMBIENT_LIGHT_SAMPLING

    Serial.print(pulse.resp, HEX);     // resp
    Serial.print("t");
    Serial.print(pulse.als_vis);       //  ambient visible
    Serial.print("t");
    Serial.print(pulse.als_ir);        //  ambient IR
    Serial.print("t");
    
 #endif

    Serial.print(red);
    Serial.print("t");
    Serial.print(IR1);
    Serial.print("t");
    Serial.print(IR2);
    Serial.print("t");
    Serial.println((long)total);    // comment out all the bottom print lines
                                    // except this one for Processing heartbeat monitor
}


// simple smoothing function for future heartbeat detection and processing
float smooth(float data, float filterVal, float smoothedVal){

    if (filterVal > 1){      // check to make sure param's are within range
        filterVal = .99;
    }
    else if (filterVal < = 0.0){
        filterVal = 0.01;
    }

    smoothedVal = (data * (1.0 - filterVal)) + (smoothedVal  *  filterVal);
    return smoothedVal;
}

 

A couple of options need explaining.


const int samples = 5; // samples for smoothing 1 to 10 seem useful
// increase for smoother waveform (with less resolution)

// #define POWERLINE_SAMPLING // samples on an integral of a power line period [eg 1/60 sec]

// #define AMBIENT_LIGHT_SAMPLING // also samples ambient slight (slightly slower)

const int samples = 5;

The samples constant is a parameter for how many times the light sensor is read. The readings are then averaged to get rid of some jitter. This also slows things down so limits the resolution, so it represents a tradeoff. Values between 1 and 10 seem useful.

#define POWERLINE_SAMPLING

Powerline sampling samples the sensor on integrals of the power line frequency. This is way to get rid of 60 cycle hum which is omnipresent anywhere where there are power lines. Uncomment this line to use the option. This results in a cleaner but slower refresh rate, so a nice clean heartbeat trace but with fewer datapoints. The JeeLabs bit-banged I2C functions are a bit pokey so the 'duino only succeeds in reading the sensor about 10 times in a 16ms period. I hope to spend some time trying to optimize the JeeLabs I2C routines for speed in the future.

#define AMBIENT_LIGHT_SAMPLING

Ambient light sensing also samples the ambient light (with no LEDs on). This is useful for proximity sensing. By subtracting ambient light, it is possible to examine the added light from the LEDs, even if it has a fairly low level. This will cancel the effect of any interfering ambient light. Because more data is transferred in the I2C read, this option also slows down the sensor read somewhat. Uncomment this line to try out this option.

Displaying the Results

A heartbeat sensor is pretty boring without some visual idea of what the heartbeat looks like. I've included a graphing sketch in Processing in the library to view the heartbeat from the sensor. Below are some videos of the sensor in action.

Video of the Pulse Sensor Output in Action in Processing

The movie is ad-libbed and there are few rough spots - at one point respiration is referred to as heartbeat but hey - they're all part of staying alive right?

The code for the article is up at github.

I've got about two or three more posts in the series but I'm in Maine for a week for some lobster and fishing. The shop is still open and shipping though.

The sensor is here in case you want one..

Paul

The post Pulse Sensor II: Getting the Lights On appeared first on Modern Device.

Back to blog