一、 引言
在上一篇中,我们完成了 FPB-RA6E2 开发板的开箱和基础开发环境的搭建。本篇将进入项目的核心硬件环节:接入 MAX30102 血氧传感器,并在 RA6E2 上实现稳定的高采样率数据采集,为后续上位机 AI 分析做准备。
终端设备 (RA6E2): 专注于低功耗、高实时性的原始数据采集。
二、 硬件连接:RA6E2 与 MAX30102
MAX30102 是一款集成的脉搏血氧仪和心率监测模块。选用 Maxim Integrated 的 MAX30102 作为 PPG 信号采集前端。该传感器集成了红光和红外光 LED、光电探测器和低噪声模拟前端,非常适合可穿戴应用。它使用 I2C 接口进行通信,逻辑电平通常为 3.3V。FPB-RA6E2 的 Arduino 接口和 Pmod 接口都支持 3.3V 逻辑,非常适合直接连接。
我们将 MAX30102 模块与 FPB-RA6E2 开发板进行硬件连接,确保信号完整性和稳定性:





三、 嵌入式固件开发:FSP I2C 中断驱动与 FIFO 管理
利用瑞萨 FSP (Flexible Software Package) 可以方便地配置 I2C 主机驱动:
在 e2 studio 的 FSP Configuration 界面,切换到 Stacks 标签页。
点击 New Stack -> Driver -> Connectivity -> I2C Master Driver on r_iic_master。
配置 I2C 通道(例如通道 0),并确保 SDA/SCL 引脚配置正确(对应你实际连接的引脚)。
配置一个定时器或利用中断 (INT pin),以固定的采样率(例如 100 Hz 或更高)触发数据读取。MAX30102 的 I2C 从机地址是 0x57(7位地址)。
1. FSP I2C 配置
在 e2 studio 的 FSP Configuration 中,我们配置 I2C Master 通道,并启用回调函数(Callback Function)和中断。当传感器数据准备好(FIFO Almost Full)时,MAX30102 会触发中断引脚,MCU 立即响应并读取 FIFO 缓冲区,避免轮询(Polling)等待,从而降低整体功耗。

核心代码实现:
#include "hal_data.h"
#include <stdio.h>
#include "debug/SEGGER_RTT.h"
#include "r_ioport.h"
#include "drivers/max30102.h"
#include "algorithm/algorithm.h"
#define MAX30102_INT_PIN BSP_IO_PORT_05_PIN_00
#define BUFFER_SIZE 500
#define CALC_THRESHOLD_SAMPLES 100
static uint32_t aun_ir_buffer[BUFFER_SIZE];
static uint32_t aun_red_buffer[BUFFER_SIZE];
static uint32_t aligned_ir[BUFFER_SIZE];
static uint32_t aligned_red[BUFFER_SIZE];
static int32_t n_spo2, n_heart_rate;
static int8_t ch_spo2_valid, ch_hr_valid;
static int32_t smooth_hr = 0;
static int32_t smooth_spo2 = 0;
static void align_circular_buffer(int32_t head) {
for (int32_t i = 0; i < BUFFER_SIZE; i++) {
int32_t source_idx = (head + i) % BUFFER_SIZE;
aligned_ir[i] = aun_ir_buffer[source_idx];
aligned_red[i] = aun_red_buffer[source_idx];
}
}
void hal_entry(void) {
SEGGER_RTT_Init();
setvbuf(stdout, NULL, _IONBF, 0);
printf("System Boot: RA6E2 MAX30102 SPO2 Project\n");
R_IOPORT_PinCfg(&g_ioport_ctrl, MAX30102_INT_PIN,
IOPORT_CFG_PORT_DIRECTION_INPUT | IOPORT_CFG_PULLUP_ENABLE);
MAX30102_Init();
int32_t head = 0;
int32_t new_samples = 0;
printf("Status: Filling initial buffer (5 seconds)...\n");
while (head < BUFFER_SIZE) {
bsp_io_level_t level;
R_IOPORT_PinRead(&g_ioport_ctrl, MAX30102_INT_PIN, &level);
if (level == BSP_IO_LEVEL_LOW) {
if (MAX30102_ReadFifo(&aun_red_buffer[head], &aun_ir_buffer[head])) {
head++;
if (head % 100 == 0) printf("Progress: %ld/500\n", head);
}
}
}
head = 0;
printf("Status: Measuring Ready!\n");
while (1) {
bsp_io_level_t int_level;
R_IOPORT_PinRead(&g_ioport_ctrl, MAX30102_INT_PIN, &int_level);
if (int_level == BSP_IO_LEVEL_LOW) {
if (MAX30102_ReadFifo(&aun_red_buffer[head], &aun_ir_buffer[head])) {
head = (head + 1) % BUFFER_SIZE;
new_samples++;
}
}
if (new_samples >= CALC_THRESHOLD_SAMPLES) {
uint32_t current_ir = aun_ir_buffer[(head + BUFFER_SIZE - 1) % BUFFER_SIZE];
if (current_ir < 30000) {
smooth_hr = 0;
smooth_spo2 = 0;
printf("Status: No Finger [IR: %lu]\n", current_ir);
}
else {
align_circular_buffer(head);
maxim_heart_rate_and_oxygen_saturation(aligned_ir, BUFFER_SIZE, aligned_red,
&n_spo2, &ch_spo2_valid,
&n_heart_rate, &ch_hr_valid);
if (ch_hr_valid && n_heart_rate >= 40 && n_heart_rate <= 160) {
if (smooth_hr == 0) smooth_hr = n_heart_rate;
else smooth_hr = (int32_t)(smooth_hr * 0.7f + n_heart_rate * 0.3f);
if (ch_spo2_valid && n_spo2 >= 80 && n_spo2 <= 100) {
if (smooth_spo2 == 0) smooth_spo2 = n_spo2;
else smooth_spo2 = (int32_t)(smooth_spo2 * 0.9f + n_spo2 * 0.1f);
}
if (smooth_spo2 > 0) {
printf(">> HR: %ld bpm | SpO2: %ld%% | IR: %lu\n",
smooth_hr, smooth_spo2, current_ir);
} else {
printf(">> HR: %ld bpm | SpO2: Calibrating... | IR: %lu\n",
smooth_hr, current_ir);
}
} else {
printf("Status: Analyzing... (Keep finger steady)\n");
}
}
new_samples = 0;
}
R_BSP_SoftwareDelay(500, BSP_DELAY_UNITS_MICROSECONDS);
}
}
#ifndef MAX30102_H
#define MAX30102_H
#include<stdint.h>
#include<stdbool.h>
#include"hal_data.h"
#define MAX30102_ADDR (0x57)
#define REG_INTR_STATUS_1 0x00
#define REG_INTR_STATUS_2 0x01
#define REG_INTR_ENABLE_1 0x02
#define REG_INTR_ENABLE_2 0x03
#define REG_FIFO_WR_PTR 0x04
#define REG_FIFO_OVERFLOW 0x05
#define REG_FIFO_RD_PTR 0x06
#define REG_FIFO_DATA 0x07
#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
#define REG_PART_ID 0xFF
#define I2C_OK 0
#define I2C_NACK_ADDR -1
#define I2C_NACK_REG -2
#define I2C_NACK_DATA -3
#define I2C_ERR_OTHER -4
voidMAX30102_Init(void);
voidMAX30102_ScanBus(void);
boolMAX30102_ReadFifo(uint32_t *red, uint32_t *ir);
#endif
#include"max30102.h"
#include<stdio.h>
#include"r_ioport.h"
#include"common_data.h"
staticvolatile bool g_i2c_complete = false;
staticvolatilei2c_master_event_t g_i2c_event = I2C_MASTER_EVENT_ABORTED;
voidsci_i2c_master_callback(i2c_master_callback_args_t *p_args) {
g_i2c_event = p_args->event;
g_i2c_complete = true;
}
staticfsp_err_thwWait(void) {
uint32_t timeout = 20000;
while (!g_i2c_complete && timeout > 0) {
timeout--;
R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_MICROSECONDS);
}
if (timeout == 0) returnFSP_ERR_TIMEOUT;
g_i2c_complete = false;
return (I2C_MASTER_EVENT_TX_COMPLETE == g_i2c_event || I2C_MASTER_EVENT_RX_COMPLETE == g_i2c_event) ? FSP_SUCCESS : FSP_ERR_ABORTED;
}
staticvoidhwInit(void) {
uint32_t pin_cfg = IOPORT_CFG_DRIVE_HIGH | IOPORT_CFG_NMOS_ENABLE | IOPORT_CFG_PULLUP_ENABLE | IOPORT_CFG_PERIPHERAL_PIN | IOPORT_PERIPHERAL_SCI0_2_4_6_8;
R_IOPORT_PinCfg(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_10, pin_cfg);
R_IOPORT_PinCfg(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_11, pin_cfg);
g_i2c0.p_api->open(g_i2c0.p_ctrl, g_i2c0.p_cfg);
}
staticintwriteReg(uint8_t reg, uint8_t data) {
uint8_t buf[2] = {reg, data};
g_i2c0.p_api->slaveAddressSet(g_i2c0.p_ctrl, MAX30102_ADDR, I2C_MASTER_ADDR_MODE_7BIT);
g_i2c_complete = false;
if (g_i2c0.p_api->write(g_i2c0.p_ctrl, buf, 2, false) != FSP_SUCCESS) return I2C_ERR_OTHER;
return (hwWait() == FSP_SUCCESS) ? I2C_OK : I2C_ERR_OTHER;
}
staticintreadReg(uint8_t reg, uint8_t * buf, uint32_t bytes) {
g_i2c0.p_api->slaveAddressSet(g_i2c0.p_ctrl, MAX30102_ADDR, I2C_MASTER_ADDR_MODE_7BIT);
g_i2c_complete = false;
if (g_i2c0.p_api->write(g_i2c0.p_ctrl, ®, 1, true) != FSP_SUCCESS) return I2C_ERR_OTHER;
if (hwWait() != FSP_SUCCESS) return I2C_ERR_OTHER;
g_i2c_complete = false;
if (g_i2c0.p_api->read(g_i2c0.p_ctrl, buf, bytes, false) != FSP_SUCCESS) return I2C_ERR_OTHER;
return (hwWait() == FSP_SUCCESS) ? I2C_OK : I2C_ERR_OTHER;
}
voidMAX30102_Init(void) {
hwInit();
writeReg(REG_MODE_CONFIG, 0x40);
R_BSP_SoftwareDelay(100, BSP_DELAY_UNITS_MILLISECONDS);
writeReg(REG_INTR_ENABLE_1, 0xC0);
writeReg(REG_FIFO_WR_PTR, 0x00);
writeReg(REG_FIFO_OVERFLOW, 0x00);
writeReg(REG_FIFO_RD_PTR, 0x00);
writeReg(REG_FIFO_CONFIG, 0x1F);
writeReg(REG_MODE_CONFIG, 0x03);
writeReg(REG_SPO2_CONFIG, 0x27);
writeReg(REG_LED1_PA, 0x1A);
writeReg(REG_LED2_PA, 0x1A);
uint8_t dummy;
readReg(REG_INTR_STATUS_1, &dummy, 1);
printf("MAX30102 Ready: 100Hz Native Mode\n");
}
bool MAX30102_ReadFifo(uint32_t *red, uint32_t *ir) {
uint8_t data[6];
uint8_t status;
readReg(REG_INTR_STATUS_1, &status, 1);
if (readReg(REG_FIFO_DATA, data, 6) == I2C_OK) {
*red = ((uint32_t)data[0] << 16 | (uint32_t)data[1] << 8 | data[2]) & 0x3FFFF;
*ir = ((uint32_t)data[3] << 16 | (uint32_t)data[4] << 8 | data[5]) & 0x3FFFF;
return true;
}
return false;
}
#ifndef ALGORITHM_H_
#define ALGORITHM_H_
#include <stdint.h>
#include <stdbool.h>
#define FT_W_HR 4
#define FT_W_SPO2 4
#define TEST_MAX_BRIGHTNESS 255
void maxim_heart_rate_and_oxygen_saturation(uint32_t *pun_ir_buffer, int32_t n_ir_buffer_length, uint32_t *pun_red_buffer, int32_t *pn_spo2, int8_t *pch_spo2_valid, int32_t *pn_heart_rate, int8_t *pch_hr_valid);
#endif
#include"algorithm.h"
#include<math.h>
#include<string.h>
#define SPO2_A 112.0f
#define SPO2_B 25.0f
#define MA_SIZE 4
#define MAX_SAMPLES 500
#define MIN_PEAK_DIST 40
staticfloat af_x[MAX_SAMPLES];
staticfloat af_diff[MAX_SAMPLES];
voidmaxim_heart_rate_and_oxygen_saturation(uint32_t *pun_ir_buffer, int32_t n_ir_buffer_length,
uint32_t *pun_red_buffer, int32_t *pn_spo2,
int8_t *pch_spo2_valid, int32_t *pn_heart_rate,
int8_t *pch_hr_valid)
{
int32_t k, i;
int32_t an_peak_locs[15];
int32_t n_npks = 0;
*pn_heart_rate = -999; *pch_hr_valid = 0;
*pn_spo2 = -999; *pch_spo2_valid = 0;
if (n_ir_buffer_length > MAX_SAMPLES) n_ir_buffer_length = MAX_SAMPLES;
if (n_ir_buffer_length < 150) return;
float f_ir_mean = 0.0f;
for (k = 0; k < n_ir_buffer_length; k++) f_ir_mean += (float)pun_ir_buffer[k];
f_ir_mean /= (float)n_ir_buffer_length;
for (k = 0; k < n_ir_buffer_length; k++) af_x[k] = (float)pun_ir_buffer[k] - f_ir_mean;
for (k = 0; k < n_ir_buffer_length - MA_SIZE; k++) {
af_x[k] = (af_x[k] + af_x[k+1] + af_x[k+2] + af_x[k+3]) * 0.25f;
}
for (k = 2; k < n_ir_buffer_length - MA_SIZE; k++) {
af_diff[k] = af_x[k] - af_x[k-2];
}
float f_max_diff = 0.0f;
for (k = 2; k < n_ir_buffer_length - MA_SIZE; k++) {
if (af_diff[k] > f_max_diff) f_max_diff = af_diff[k];
}
float f_thresh = f_max_diff * 0.25f;
if (f_thresh < 8.0f) f_thresh = 8.0f;
for (k = 3; k < n_ir_buffer_length - MA_SIZE - 1; k++) {
if (af_diff[k] > f_thresh && af_diff[k] > af_diff[k-1] && af_diff[k] >= af_diff[k+1]) {
if (n_npks == 0 || (k - an_peak_locs[n_npks-1]) > MIN_PEAK_DIST) {
an_peak_locs[n_npks++] = k;
if (n_npks >= 15) break;
}
}
}
if (n_npks >= 3) {
float f_interval_avg = 0;
for (k = 1; k < n_npks; k++) f_interval_avg += (float)(an_peak_locs[k] - an_peak_locs[k-1]);
f_interval_avg /= (float)(n_npks - 1);
int32_t hr_tmp = (int32_t)(6000.0f / f_interval_avg);
if (hr_tmp >= 40 && hr_tmp <= 160) {
*pn_heart_rate = hr_tmp;
*pch_hr_valid = 1;
}
}
if (*pch_hr_valid) {
float f_ratio_sum = 0.0f;
int32_tn_ratio_count = 0;
for (k = 0; k < n_npks - 1; k++) {
float max_red = -1e9f, min_red = 1e9f, max_ir = -1e9f, min_ir = 1e9f;
float sum_red = 0, sum_ir = 0;
for (i = an_peak_locs[k]; i < an_peak_locs[k+1]; i++) {
float r = (float)pun_red_buffer[i], ir = (float)pun_ir_buffer[i];
if (r > max_red) max_red = r; if (r < min_red) min_red = r;
if (ir > max_ir) max_ir = ir; if (ir < min_ir) min_ir = ir;
sum_red += r; sum_ir += ir;
}
float dc_red = sum_red / (float)(an_peak_locs[k+1] - an_peak_locs[k]);
float dc_ir = sum_ir / (float)(an_peak_locs[k+1] - an_peak_locs[k]);
float ac_red = max_red - min_red;
float ac_ir = max_ir - min_ir;
if (ac_ir > 10.0f && dc_ir > 100.0f) {
float f_r = (ac_red / dc_red) / (ac_ir / dc_ir);
if (f_r > 0.3f && f_r < 1.8f) {
f_ratio_sum += f_r;
n_ratio_count++;
}
}
}
if (n_ratio_count > 0) {
*pn_spo2 = (int32_t)(SPO2_A - (SPO2_B * (f_ratio_sum / (float)n_ratio_count)));
if (*pn_spo2 > 100) *pn_spo2 = 100; if (*pn_spo2 < 70) *pn_spo2 = 70;
*pch_spo2_valid = 1;
}
}
}
三、 数据输出
采用J-Link RTT Viewer查看输出,具体如下

四、 总结与展望
本篇我们成功将 MAX30102 硬件集成到 FPB-RA6E2 平台,并通过 FSP 配置了 I2C 通信,完成了MAX30102的数据获取,并编写了算法计算血样和心率的计算,通过J-Link RTT Viewer进行了输出,FPB-RA6E2 的实时性和 FSP 工具链的高效性保证了原始数据的准确捕获与稳定传输。下一阶段,我们将重点介绍桌面端临床分析系统的构建,实现数据的可视化和 Reality AI 特征分析的前端集成。