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.