Heart Rate Click: On EasyPIC v7 (with MikroC code)

15

The Heart Rate click from MikroElektronika is a pulse-oximeter featuring one MAX30100 sensor, plus all the required additional circuits, including the 1.8V power supply implemented with an AP7331 low dropout regulator. The click boards requires only a 3.3V power supply (sorry Arduino users, there’s no 5V version of this click board). Communication with the host is done via I2C interface, plus one INT pin that can be configured in software.

This article is more than two years old and might contain obsolete information; it is still kept here for informational purposes.

The MAX30100 is an optical, reflective sensor that combines one IR and one red LED, a photodetector, plus a low noise analog processing stage, which among other things performs ambient light cancellation. The analog signal is fed into an A/D converter with programmable resolution and integration time. A digital filter follows, then the signal is sent to the host microcontroller via I2C. A temperature sensor is also provided, as temperature compensation is needed in precise SpO2 determination.

With this being said, at the first glance things should be nice – just let the sensor do the hard work. In practice things are not that bright – the sensor does return only the IR and Red reflectance, leaving the tasks to determine the heart rate and SpO2 to the user. Moreover, there’s a complicated procedure to read the acquired data, involving FIFO register as circular buffer, with read and write pointers stored in separate registers. Another register stores the number of FIFO overflows (the number of lost samples). This overflow register tops sat 0x0F, after that we don’t know how many samples we have lost.

Advertisements
EasyPIC v7 with Heart Rate Click

EasyPIC v7 with Heart Rate Click

I have spent over one week to try to understand how this sensor works, and all I could do so far is to get a steady data flow, and to show the red and IR reflectance data on an LCD. The development board used in this blog post is an EasyPIC v7, with PIC18F45K22 microcontroller, at a clock frequency of 8MHz. The reason for using this clock rate is that the LCD refuses to work on higher frequencies when powered from 3.3V.

Only I2C communication is used. The interrupt pin of the MAX30100 is not used, as the INT pin of the click board is shared with the LCD data lines, and is set as output by the LCD routine.

I’ve split the code in three parts, so one can reuse the MAX30100 functions with ease. So, we start with the max30100.h file, which contains definitions of MAX30100 registers and register values, and declarations of functions in MAX30100.c:

/******************************************************************************/
/*********** PULSE OXIMETER AND HEART RATE REGISTER MAPPING  **************/
/******************************************************************************/

// status registers
#define MAX30100_INT_STATUS          0x00
#define MAX30100_INT_ENABLE          0x01

// FIFO registers
#define MAX30100_FIFO_W_POINTER      0x02
#define MAX30100_OVF_COUNTER         0x03
#define MAX30100_FIFO_R_POINTER      0x04
#define MAX30100_FIFO_DATA_REG       0x05

// configuration registers
#define MAX30100_MODE_CONFIG         0x06
#define MAX30100_SPO2_CONFIG         0x07
#define MAX30100_LED_CONFIG          0x09

// temperature registers
#define MAX30100_TEMP_INTEGER        0x16
#define MAX30100_TEMP_FRACTION       0x17

// PART ID registers
#define MAX30100_REVISION_ID         0xFE
#define MAX30100_PART_ID             0xFF

#define I_AM_MAX30100                0x11

/************************************** REGISTERS VALUE *******************************************/

// MAX30100 I2C addresses
#define MAX30100_WADDRESS        0xAE  // 8bit address converted to 7bit + W
#define MAX30100_RADDRESS        0xAF  // 8bit address converted to 7bit + R

//Enable interrupts
#define MAX30100_INT_ENB_A_FULL      0x80
#define MAX30100_INT_ENB_TEMP_RDY    0x40
#define MAX30100_INT_ENB_HR_RDY      0x20
#define MAX30100_INT_ENB_SO2_RDY     0x10

#define MAX30100_MODE_HR             0x02
#define MAX30100_MODE_SPO2           0x03

//SPO2 configuration
#define MAX30100_SPO2_HI_RES_EN      0x40

typedef enum{ // This is the same for both LEDs
    pw200,    // 200us pulse
    pw400,    // 400us pulse
    pw800,    // 800us pulse
    pw1600    // 1600us pulse
}pulseWidth;

typedef enum{
    sr50,    // 50 samples per second
    sr100,   // 100 samples per second
    sr167,   // 167 samples per second
    sr200,   // 200 samples per second
    sr400,   // 400 samples per second
    sr600,   // 600 samples per second
    sr800,   // 800 samples per second
    sr1000   // 1000 samples per second
}sampleRate;

typedef enum{
    i0,    // No current
    i4,    // 4.4mA
    i8,    // 7.6mA
    i11,   // 11.0mA
    i14,   // 14.2mA
    i17,   // 17.4mA
    i21,   // 20.8mA
    i24,   // 24.0mA
    i27,   // 27.1mA
    i31,   // 30.6mA
    i34,   // 33.8mA
    i37,   // 37.0mA
    i40,   // 40.2mA
    i44,   // 43.6mA
    i47,   // 46.8mA
    i50    // 50.0mA
}ledCurrent;

typedef enum{
    low,    // low resolution SPO2
    high    // high resolution SPO2 (16 bit with 1.6ms LED pulse width)
}high_resolution;

// Functions

// Reads from register 0xFE
// returns version ID
unsigned short MAX30100_getRevID(void);

// Reads from register 0xFF
// should return 0x11
unsigned short MAX30100_getPartID(void);

// Resets the MAX30100 IC
void MAX30100_reset(void);

// Wakes up the MAX30100
void MAX30100_wakeup(void);

// Shuits down the MAX30100
void MAX30100_shutdown(void);

// Sets Heart rate mode
void MAX30100_SetHR (void);

// Sets SPO2 rate mode
void MAX30100_SetSPO2 (void);

// Initializes FIFO
// Sets RD and WR pointers to 0
// Clears OVF
void MAX30100_InitFIFO (void);

// Sets LED currents
void MAX30100_setLEDs(ledCurrent red, ledCurrent ir);

// Used to set sample rate
void MAX30100_setSR(sampleRate sr);

// Sets pulse width
// sample rate is bits 1:0 of register MAX30100_SPO2_CONFIG
void MAX30100_setPW (pulseWidth pw);

// Gets number of samples in FIFO
int MAX30100_getNumSamp(void);

// Reads FIFO data
void MAX30100_readFIFO(unsigned long *data1, unsigned long *data2);


// aditional I2C functions
unsigned short MAX30100_read (unsigned short device_register);
void MAX30100_write (unsigned short device_register, unsigned short reg_data);

The MAX30100.c file contains all the required functions to set the MAX30100 and to read and write data to its registers:

#include "max30100.h"

// Reads from register 0xFE
// returns version ID
unsigned short MAX30100_getRevID(void){
   unsigned short reg;
   reg =  MAX30100_read (MAX30100_REVISION_ID);
   return reg;
}

// Reads from register 0xFF
// should return 0x11
unsigned short MAX30100_getPartID(void){
   unsigned short reg;
   reg =  MAX30100_read (MAX30100_PART_ID);
   return reg;
}

// Resets the MAX30100 IC
void MAX30100_reset(void){
   unsigned short reg;
   reg =  MAX30100_read (MAX30100_MODE_CONFIG);
   // RESET bit is B6
   // 0x40 = 01000 0000
   reg = reg | 0x40; // Set reset bit to 1
   MAX30100_write (MAX30100_MODE_CONFIG, reg);
}

// Wakes up the MAX30100
// Clears bit 7 of MODE CONFIG register
void MAX30100_wakeup(void){
   unsigned short reg;
   reg =  MAX30100_read (MAX30100_MODE_CONFIG);
   // RESET bit is B7
   // 0x7F = 0111 1111
   reg = reg & 0x7F; // Set SHDN bit to 0
   MAX30100_write (MAX30100_MODE_CONFIG, reg);
}

// Wakes up the MAX30100
// Sets bit 7 of MODE CONFIG register
void MAX30100_shutdown (void){
   unsigned short reg;
   reg =  MAX30100_read (MAX30100_MODE_CONFIG);
   // RESET bit is B7
   // 0x80 = 1000 0000
   reg = reg | 0x80; // Set SHDN bit to 1
   MAX30100_write (MAX30100_MODE_CONFIG, reg);
}

// Sets Heart rate mode
// This means MODE{2:0} = 0b010 (or 0x02 in hexadecimal)
void MAX30100_SetHR (void){
   unsigned short reg;
   reg =  MAX30100_read (MAX30100_MODE_CONFIG);
   // RESET bit is B7
   // First we clear bits 2:0
   reg = reg & 0xF8;
   // Then we set bits 2:0 to 0x02
   reg = reg | 0x02;
   MAX30100_write (MAX30100_MODE_CONFIG, reg);
}

// Sets SPO2 rate mode
// This means MODE{2:0} = 0b011 (or 0x03 in hexadecimal)
void MAX30100_SetSPO2 (void){
   unsigned short reg;
   reg =  MAX30100_read (MAX30100_MODE_CONFIG);
   // RESET bit is B7
   // First we clear bits 2:0
   reg = reg & 0xF8;
   // Then we set bits 2:0 to 0x03
   reg = reg | 0x03;
   MAX30100_write (MAX30100_MODE_CONFIG, reg);
}

// Initializes FIFO
// Sets RD and WR pointers to 0
// Clears OVF
void MAX30100_InitFIFO (void){
   MAX30100_write (MAX30100_FIFO_W_POINTER, 0x00);
   MAX30100_write (MAX30100_FIFO_R_POINTER, 0x00);
   MAX30100_write (MAX30100_OVF_COUNTER, 0x00);
}

// Sets LED currents
void MAX30100_setLEDs(ledCurrent red, ledCurrent ir){
   unsigned short reg;
   reg = ( red << 4 ) | ir;
   MAX30100_write (MAX30100_LED_CONFIG, reg);
}

// Sets sample rate
// sample rate is bits 4:2 of register MAX30100_SPO2_CONFIG
// bitmask is 0xE3
void MAX30100_setSR (sampleRate sr){
   unsigned short reg;
   reg =  MAX30100_read (MAX30100_SPO2_CONFIG);
   reg = reg & 0xE3;
   reg = reg | (sr << 2);
   MAX30100_write (MAX30100_SPO2_CONFIG, reg);
}

// Sets pulse width
// sample rate is bits 1:0 of register MAX30100_SPO2_CONFIG
void MAX30100_setPW (pulseWidth pw){
   unsigned short reg;
   reg =  MAX30100_read (MAX30100_SPO2_CONFIG);
   reg = reg & 0xFC;
   reg = reg | pw;
   MAX30100_write (MAX30100_SPO2_CONFIG, reg);
}

// now we write functions to work with fifo

// Gets number of samples in FIFO
int MAX30100_getNumSamp(void){
    unsigned short wreg;
    unsigned short rreg;
    wreg = MAX30100_read (MAX30100_FIFO_W_POINTER);
    rreg = MAX30100_read (MAX30100_FIFO_R_POINTER);
    return (abs( 16 + (int)wreg - (int)rreg ) % 16);
}

// In each sample we have to read four bytes
void MAX30100_readFIFO(unsigned int *data1, unsigned int *data2){
   unsigned int reg;
   unsigned int readout;
   I2C1_Start();
   I2C1_Wr(MAX30100_WADDRESS);        // send byte via I2C  (device address + W)
   I2C1_Wr(MAX30100_FIFO_DATA_REG);          // send byte (data address)
   I2C1_Repeated_Start();             // issue I2C signal repeated start
   I2C1_Wr(MAX30100_RADDRESS);        // send byte (device address + R)
   readout= I2C1_Rd(1u);			  // I2C read with ACK
   reg = I2C1_Rd(1u);
   *data2 = (readout << 8) | reg;
   readout = I2C1_Rd(1u);
   reg = I2C1_Rd(0u);                 // I2C read with NACK (tells slave to let go)
   *data1 = (readout << 8) | reg;
   I2C1_Stop();
}


// *****************************************************************************
// My I2C read and write functions
unsigned short MAX30100_read ( unsigned short device_register ){
   unsigned short read_data;
   I2C1_Start();
   I2C1_Wr(MAX30100_WADDRESS);        // send byte via I2C  (device address + W)
   I2C1_Wr(device_register);          // send byte (data address)
   I2C1_Repeated_Start();             // issue I2C signal repeated start
   I2C1_Wr(MAX30100_RADDRESS);        // send byte (device address + R)
   read_data = I2C1_Rd(0u);           // read the data (NO acknowledge)
   //while (!I2C1_Is_Idle())
   //     asm nop;                      // read the data (NO acknowledge)
   I2C1_Stop();
   return read_data;
}

void MAX30100_write (unsigned short device_register, unsigned short reg_data){
   I2C1_Start();
   I2C1_Wr(MAX30100_WADDRESS);            // send byte via I2C  (device address + W)
   I2C1_Wr(device_register);           // send byte (data address)
   I2C1_Wr(reg_data);
   while (!I2C1_Is_Idle())
     asm nop;                          // read the data (NO acknowledge)
   I2C1_Stop();
}

The MAX3100 features 8-bit registers for most functions, with the exception of the FIFO register which stores four bytes (two for IR reflectance and two for red reflectance). Even when working in heart rate mode, without data from the red LED, we still have to read four bytes from the FIFO register, with the first two bytes containing red reflectance and the last two bytes being zero. My approach in HR mode is to use *data1 to update heart rate (IR reflectance), and I assign *data2 to a dummy variable which is left unused. In SpO2 mode I use *data1 to update IR reflectance and *data2 to update red reflectance. The main code is as follows:

// *****************************************************************************
//
// HEART RATE CLICK / MAX30100 demo code in MikroC Pro for PIC
//
// Place Heart rate click in socket #1 of EasyPIC v7
// Microcontroller is PIC18F45K22 @ 8MHz
// Uses the LCD onbloaord EasyPIC v7
//
// For explanations about code visit
// https://electronza.com/heart-rate-easy-pic
//
// *****************************************************************************


#include "max30100.h"

// Initialize LCD on EasyPIC v7

// Lcd pinout settings
sbit LCD_RS at RB4_bit;
sbit LCD_EN at RB5_bit;
sbit LCD_D7 at RB3_bit;
sbit LCD_D6 at RB2_bit;
sbit LCD_D5 at RB1_bit;
sbit LCD_D4 at RB0_bit;

// Pin direction
sbit LCD_RS_Direction at TRISB4_bit;
sbit LCD_EN_Direction at TRISB5_bit;
sbit LCD_D7_Direction at TRISB3_bit;
sbit LCD_D6_Direction at TRISB2_bit;
sbit LCD_D5_Direction at TRISB1_bit;
sbit LCD_D4_Direction at TRISB0_bit;

// Used to show data on LCD
char txt[8];

// Device ID
unsigned short id;

// Used to read samples
unsigned int i;
int samples;
unsigned short wrp;

// Used to store HR and SPO2 reflectance
unsigned int HR;
unsigned int SPO2;

void main() {

ANSELB = 0x00;                             // Configure PORTB pins as digital
ANSELC = 0x00;
ANSELE = 0x00;

TRISE = 0x00;                              // Configure PORTE pins as outputs
LATE = 0x00;

// Initialize LCD and print some text
Lcd_Init();
Delay_ms(100);
Lcd_Cmd(_LCD_CLEAR);
Lcd_Cmd(_LCD_CURSOR_OFF);          // Cursor off
Lcd_Out(1,1,"Heart Rate Click");   // Write text in first row
Lcd_Out(2,1,"Demo code");          // Write text in second row
Delay_ms(2000);

// Initialize I2C1 (RC3 is SCL, RC4 is SDA)
I2C1_Init(400000);
Delay_ms(10);

// First we reset the MAX30100
MAX30100_reset();
Delay_ms(100);        // Allow for some time to pass

// Is the heart rate click connected?
// We read from register 0xFF
// If the value is 0x11 then the sensor is connected
id = MAX30100_getPartID();
if (id = 0x11) {
   Lcd_Cmd(_LCD_CLEAR);
   Lcd_Out(1,1,"MAX30100 found");
   id = MAX30100_getRevID();
   ByteToHex(id, txt);
   Lcd_Out(2,1,"Rev");
   Lcd_Out(2,5,txt);
   Delay_ms(2000);
}

// Set LED current
MAX30100_setLEDs(i11, i8);

// Set sample rate
MAX30100_setSR(sr50);

// Set pulse width
MAX30100_setPW(pw1600);

MAX30100_InitFIFO();
wrp = 0; // used to check FIFO overflow

// Wake up
MAX30100_wakeup();

// We set SpO2 mode and start measuring
MAX30100_SetSPO2 ();

// Or we can go in heart rate mode
//MAX30100_SetHR ();

Lcd_Cmd(_LCD_CLEAR);

  while(1){
    // We first check for overflows
    wrp = MAX30100_read ( MAX30100_OVF_COUNTER );
      if (wrp == 0x0F){ // signal overflow
        LATE=0x01;      // light LED on pin RE0
        Lcd_Cmd(_LCD_CLEAR);
        Lcd_Out(1,1,"FIFO Overflow");
        Lcd_Out(2,1,"Press RESET");
        while(1); // blocking
      }
      // Gets the number  of samples in the FIFO
      samples = MAX30100_getNumSamp();
      // Then reads the samples
      if (samples > 0 ){ // we have data in FIFO
         for (i = 0; i < samples; i++){
             // read one sample from the sensor
             MAX30100_readFIFO(&HR, &SPO2);

             // Process the samples
             // One has about 14ms to do the processing
             // before the next data sample is ready

             // Here we show the gathered data on LCD
             IntToStr(HR, txt);
             Lcd_Out(1,1,txt);
             IntToStr(SPO2, txt);
             Lcd_Out(2,1,txt);

         } // end of sample reading loop
      }    // end if statement that checks if we have data
      else { // There's no data in FIFO
             Delay_ms(20); // Wait for samples to be aquired
      }

   } // end of while(1)

} // end of void main()

All source files are available for download here.

In the above code, I first check if the MAX30100 is present by reading from register 0xFF. If the returned data is 0x11 then I know that MAX30100 is connected correctly. Then I read from register 0xFE which stores the revision version and I print that data on LCD.

Then I set the IR current to 8mA and the Red LED current to 11mA. The currents can be the same, this is just to show how to set them. In practice, one should adjust the LED currents to get the best readings (make the most of the A/D resolution).

Sampling rate is set at 50 samples per second. Anything more than this and the PIC won’t be able to handle the data flow.

Pulse width is set to 1600μs, thus A/D resolution is 16 bit. A short comment here: the A/D data is left-justified, so if you use a lower A/D resolution you’ll have to perform some byte-shifting to get rid of the extra zeroes.

Now I can wake up the sensor and initialize the FIFO by setting the read, write and overflow registers to 0x00.

Finally, writing the bits MODE[2:0] of the CONFIG register starts the data acquisition process. If you have chosen SpO2 mode you’ll notice the Red LED going on. In heart rate mode only the IR led works.

Now we go to the main loop. We first read the value in the OVF (overflow) register. If the value is 0x0F this means we have lost too much data – how much is unknown, as the OVF tops at 0x0F – and the program stops.

If the value in the OVF register is below 0x0F, even if we have some data lost, at least we can compensate for this. All OK, we can read from FIFO.

To do this we fist have to determine how much data is in the FIFO – we can have from 0 (no samples) up to 16 samples, each sample having four bytes. This is done by the MAX30100_getNumSamp function. If the function returns 0 we have no data, so we wait for 20ms – that is, we take 50 samples per second, so in the next 20ms we will have a sample acquired.

If the number of samples is greater than 0 we enter a loop in which we read the samples, and we perform data processing – in this code example we display the Red and IR reflectances on LCD. One observation here: the whole process of reading one single sample takes a little below 1.3ms, leaving only a bit over 14ms to process the data until the next sample is ready. Not much considering and 8bit PIC and the low clock frequency.

Of course, one can take full advantage of the FIFO architecture and leave the data gather in FIFO until some more complicated computations are performed, followed by a burst read of everything gathered in the FIFO. One should take care not to lose – the OVF register is your friend.

Advertisements
1 2
Share.

15 Comments

  1. Avatar

    Hi TEODOR !
    I’m working on this project, too. All I got is just as you. I still don’t know how to determine the SpO2 and heart hate information.
    Hope you find this soon!

    • Avatar

      Yes, one can use the serial port to send real time data to PC. There’s even an FTDI USB-UART chip on the EasyPIC v7, so sending the data to PC should be easy.

      • Avatar

        But what about if i dont use that EasyPIC v7? I’m trying to run it on MSP430 launchpad and also stucked to the spo2 algorithm.
        Thanks

          • Avatar

            Yeah, but slaa655 isn’t good example for my project… Your is pretty close to mine, however. Did you find some spo2 algorithm wich could be interpreted to EasyPIC v7?

  2. Avatar

    Thank you for this useful post!

    This technique is based on Photoplethysmography PPG that detect blood volume change in the vessels by the mean of non-invasive optical technique.

    The idea behind the HR and SpO2% is quite simple and can be acquired very easily:

    1- For HR you need to detect two sequence peaks or troughs of the signal ( in this case value (two sequence lowest or two highest) and then multiply (sample rate * 60)/difference between two (peaks or trough) to acquire heart beat per mints ( bpm)
    2- For SpO2% you need to do calibration of this particle sensor (MAX10030) to get highly accurate result but for the sake of limit resource and get good results one can use empirical curve or table that you need to save it in the chip and decided which ratio represent oxygen level.
    The algorithm
    ratio of ratio (RR)= (Ratio of Red/Ratio of IR)= [ ( AC amplitude/ DC )of red ] / [ ( AC amplitude/ DC )of IR ]
    and you will get RR and every RR is correspondence to oxygen level %

    I hope this is useful and for more details you can look at my publications

    http://www.mdpi.com/2079-6374/5/2/288

    http://www.mdpi.com/1424-8220/15/10/25681

    http://spie.org/Publications/Proceedings/Paper/10.1117/12.2076582

    http://spie.org/Publications/Proceedings/Paper/10.1117/12.2044640

    • Avatar
      SIMRANPREET KAUR on

      Sir I am working with heart rate 3 click which uses SFH 7050 and AFE 4404. MicroC code is given in its documentations. I am not able to cope up with it and need a arduino code. So please tell me step by step what all i need to do to operate this module.
      relevant data shoul be mailed on ksimranpreet1@gmail.com

      • Avatar

        Hi!

        It just happens that I have one Heart Rate 3 click on its way to me. Next week I will have it, and I will do some testing on Arduino.

  3. Avatar

    Thank you for this amazing code. This code has helped me to learn about MAX30100 in the first step. At least, I have got the raw values.

    Regards,
    Nazmi

    • Teodor Costachioiu
      Teodor Costachioiu on

      I haven’t implemented a heart rate and SpO2 calculation. I realized that the PIC microcontroller is underpowered to run the current algorithms for heart rate detection. Since then, I haven’t revisited the project, and I have no future plans to do so.

Leave A Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

error: