SolderBit
Categories: Embedded Systems PCB Design RF & LoRa Semiconductors Simulation
Porting the MAX30102 Pulse Sensor from Arduino to TivaWare C

The MAX30102 is a combined pulse oximeter and heart rate sensor from Maxim Integrated. It is popular on Arduino because there are good libraries for it. Porting it to TivaWare C on the TM4C123 means writing the I2C communication from scratch and understanding the sensor registers directly.

This guide documents the full porting process including the build errors and runtime problems that came up along the way.

How the MAX30102 Works

The MAX30102 uses two LEDs — red and infrared — pointed at your skin. A photodetector measures how much light passes through. When your heart beats, more blood flows through the capillaries and absorbs more light. The sensor captures this variation as a waveform. Your heart rate comes from measuring the time between peaks in that waveform.

The sensor communicates over I2C at address 0x57 and outputs raw ADC values that you process in software to get BPM.

I2C Setup on TM4C123

The TM4C123 has four I2C modules. I2C1 on PB2 (SCL) and PB3 (SDA) is a convenient choice that does not conflict with other common peripherals.

#include "driverlib/i2c.h"
#include "driverlib/sysctl.h"
#include "driverlib/gpio.h"
#include "inc/hw_memmap.h"

void I2C1_Init(void) {
    SysCtlPeripheralEnable(SYSCTL_PERIPH_I2C1);
    SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOB);

    GPIOPinConfigure(GPIO_PB2_I2C1SCL);
    GPIOPinConfigure(GPIO_PB3_I2C1SDA);
    GPIOPinTypeI2CSCL(GPIO_PORTB_BASE, GPIO_PIN_2);
    GPIOPinTypeI2C(GPIO_PORTB_BASE, GPIO_PIN_3);

    I2CMasterInitExpClk(I2C1_BASE, SysCtlClockGet(), false); // false = 100kHz
}

Writing and Reading MAX30102 Registers

All communication with the MAX30102 goes through register reads and writes.

#define MAX30102_ADDR 0x57

void MAX30102_WriteReg(uint8_t reg, uint8_t value) {
    I2CMasterSlaveAddrSet(I2C1_BASE, MAX30102_ADDR, false); // write mode
    I2CMasterDataPut(I2C1_BASE, reg);
    I2CMasterControl(I2C1_BASE, I2C_MASTER_CMD_BURST_SEND_START);
    while(I2CMasterBusy(I2C1_BASE));

    I2CMasterDataPut(I2C1_BASE, value);
    I2CMasterControl(I2C1_BASE, I2C_MASTER_CMD_BURST_SEND_FINISH);
    while(I2CMasterBusy(I2C1_BASE));
}

uint8_t MAX30102_ReadReg(uint8_t reg) {
    // write register address
    I2CMasterSlaveAddrSet(I2C1_BASE, MAX30102_ADDR, false);
    I2CMasterDataPut(I2C1_BASE, reg);
    I2CMasterControl(I2C1_BASE, I2C_MASTER_CMD_SINGLE_SEND);
    while(I2CMasterBusy(I2C1_BASE));

    // read data
    I2CMasterSlaveAddrSet(I2C1_BASE, MAX30102_ADDR, true); // read mode
    I2CMasterControl(I2C1_BASE, I2C_MASTER_CMD_SINGLE_RECEIVE);
    while(I2CMasterBusy(I2C1_BASE));

    return I2CMasterDataGet(I2C1_BASE);
}

Initializing the MAX30102

The sensor needs to be configured before it starts sampling. These are the register values that work reliably for heart rate measurement:

#define REG_INTR_ENABLE_1   0x02
#define REG_FIFO_WR_PTR     0x04
#define REG_OVF_COUNTER     0x05
#define REG_FIFO_RD_PTR     0x06
#define REG_FIFO_CONFIG     0x08
#define REG_MODE_CONFIG     0x09
#define REG_SPO2_CONFIG     0x0A
#define REG_LED1_PA         0x0C
#define REG_LED2_PA         0x0D

void MAX30102_Init(void) {
    // reset the device
    MAX30102_WriteReg(REG_MODE_CONFIG, 0x40);
    SysCtlDelay(SysCtlClockGet() / 100); // 10ms delay

    // enable FIFO almost full interrupt
    MAX30102_WriteReg(REG_INTR_ENABLE_1, 0xC0);

    // FIFO config: 4 samples averaged, FIFO rollover enabled
    MAX30102_WriteReg(REG_FIFO_CONFIG, 0x4F);

    // SpO2 mode (both LEDs active)
    MAX30102_WriteReg(REG_MODE_CONFIG, 0x03);

    // SpO2 config: 100Hz sample rate, 411us pulse width, 4096nA range
    MAX30102_WriteReg(REG_SPO2_CONFIG, 0x27);

    // LED pulse amplitude — 6.4mA
    MAX30102_WriteReg(REG_LED1_PA, 0x24);
    MAX30102_WriteReg(REG_LED2_PA, 0x24);

    // clear FIFO pointers
    MAX30102_WriteReg(REG_FIFO_WR_PTR, 0x00);
    MAX30102_WriteReg(REG_OVF_COUNTER, 0x00);
    MAX30102_WriteReg(REG_FIFO_RD_PTR, 0x00);
}

Reading FIFO Data

The MAX30102 stores samples in a FIFO buffer. Each sample is 6 bytes — 3 bytes for the red LED and 3 bytes for the infrared LED. Only the upper 18 bits of each 3-byte value are valid.

#define REG_FIFO_DATA 0x07

void MAX30102_ReadFIFO(uint32_t *red, uint32_t *ir) {
    uint8_t b[6];

    I2CMasterSlaveAddrSet(I2C1_BASE, MAX30102_ADDR, false);
    I2CMasterDataPut(I2C1_BASE, REG_FIFO_DATA);
    I2CMasterControl(I2C1_BASE, I2C_MASTER_CMD_SINGLE_SEND);
    while(I2CMasterBusy(I2C1_BASE));

    I2CMasterSlaveAddrSet(I2C1_BASE, MAX30102_ADDR, true);
    for (int i = 0; i < 6; i++) {
        if (i < 5)
            I2CMasterControl(I2C1_BASE, I2C_MASTER_CMD_BURST_RECEIVE_CONT);
        else
            I2CMasterControl(I2C1_BASE, I2C_MASTER_CMD_BURST_RECEIVE_FINISH);
        while(I2CMasterBusy(I2C1_BASE));
        b[i] = I2CMasterDataGet(I2C1_BASE);
    }

    *red = ((uint32_t)(b[0] & 0x03) << 16) | ((uint32_t)b[1] << 8) | b[2];
    *ir  = ((uint32_t)(b[3] & 0x03) << 16) | ((uint32_t)b[4] << 8) | b[5];
}

BPM Calculation

The raw IR value is a waveform that rises and falls with each heartbeat. To get BPM, find the peaks and measure the time between them.

A simple approach that works well in practice:

#define SAMPLE_RATE   100   // samples per second
#define BUFFER_SIZE   100   // 1 second of data

uint32_t irBuffer[BUFFER_SIZE];
int bufferIndex = 0;

uint32_t lastPeakTime = 0;
uint32_t lastIR = 0;
int bpm = 0;
int rising = 0;

void processSample(uint32_t ir, uint32_t currentTime) {
    if (ir > lastIR && !rising) {
        rising = 1;
    } else if (ir < lastIR && rising) {
        // peak detected
        rising = 0;
        if (lastPeakTime > 0) {
            uint32_t interval = currentTime - lastPeakTime; // in ms
            if (interval > 300 && interval < 2000) { // 30-200 BPM range
                bpm = 60000 / interval;
            }
        }
        lastPeakTime = currentTime;
    }
    lastIR = ir;
}

This is a simplified peak detector. For production use, add a moving average filter to smooth out noise before peak detection.

Build Errors When Porting from Arduino

The most common build error when moving from Arduino to C90-compliant TivaWare:

Variable declarations after statements

Arduino allows this:

int x = 5;
doSomething();
int y = 10; // fine in C++

C90 does not. All variable declarations must be at the top of the block:

int x, y;  // declare first
x = 5;
doSomething();
y = 10;    // now assign

Code Composer Studio will give you a cryptic error about “declaration not allowed here” — this is almost always the cause.

Runtime Problems and Fixes

Sensor always reads zero Check I2C address. The MAX30102 is at 0x57. Verify with an I2C scanner. Also check that your SDA and SCL lines have 4.7kΩ pull-up resistors to 3.3V.

BPM wildly inaccurate The finger placement matters a lot. Press firmly but not hard enough to cut off circulation. The sensor needs a steady signal. Also make sure the room is not too bright — strong ambient light affects the photodetector.

FIFO overflow Read the FIFO fast enough. At 100Hz sample rate, you have 10ms per sample. If your main loop is slower than this, samples pile up and overflow. Reduce the sample rate or read more frequently.

Summary

The MAX30102 port to TivaWare C is mostly an I2C implementation exercise. The sensor itself is straightforward once you understand the FIFO structure and the bit masking needed to extract the 18-bit samples. The trickiest part is getting BPM calculation reliable — invest time in filtering the raw signal before trying to detect peaks.

← Older Ra-01 SX1278 LoRa Getting Started Guide — What Nobody Tells You
Newer → FreeRTOS on TM4C123 — Tasks, Queues, and Semaphores Explained
Discussion