1、硬件连接
硬件连接很简单,因为只有一个传感器一个oled的屏幕并且都是IIC通信因此直接通过IIC总线连接至主控上

2、软件
软件上主要是驱动心率传感器以及OLED屏幕,软件的流程图如下

3、数据处理的问题
首先感谢公开自己代码的朋友,让我可以跳过驱动开发的过程。在初次调试时发现这个传感器比较灵敏,数据的跳动较大。因此在数据的处理中加入了一个简单的指数型低通滤波器,从头到尾遍历数据,每个采样点用前一个滤波结果加上当期样本的一小部分的方式递推,从而抑制高频噪声并平滑波形,再按 指定的次数重复这一过程,以进一步增强平滑效果。
#define LPF_ALPHA 8
#define LPF_PASSES 2
static void maxim_low_pass_filter_pass(int32_t *pn_data, int32_t n_length)
{
if (!pn_data || n_length <= 0)
{
return;
}
int32_t filtered = pn_data[0];
pn_data[0] = filtered;
for (int32_t i = 1; i < n_length; ++i)
{
filtered += (pn_data[i] - filtered) / LPF_ALPHA;
pn_data[i] = filtered;
}
}
static void maxim_apply_low_pass_filter(int32_t *pn_data, int32_t n_length)
{
for (int32_t pass = 0; pass < LPF_PASSES; ++pass)
{
maxim_low_pass_filter_pass(pn_data, n_length);
}
}4、主要代码
心率血氧采集和显示
#include "mxc_device.h"
#include "mxc_delay.h"
#include "i2c.h"
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include "algorithm.h"
#include "ssd1306.h"
// ---------- MAX30102 寄存器 ----------
#define MAX30102_ADDR 0x57 // 7-bit I2C 地址
#define REG_INT_STATUS_1 0x00
#define REG_INT_ENABLE_1 0x02
#define REG_FIFO_WR_PTR 0x04
#define REG_OVF_COUNTER 0x05
#define REG_FIFO_RD_PTR 0x06
#define REG_FIFO_DATA 0x07
#define REG_MODE_CONFIG 0x09
#define REG_SPO2_CONFIG 0x0A
#define REG_LED1_PA 0x0C // RED LED
#define REG_LED2_PA 0x0D // IR LED
#define REG_TEMP_INTEGER 0x1F
#define REG_TEMP_FRACTION 0x20
#define REG_PART_ID 0xFF
#define MA4_SIZE 4
#define HAMMING_SIZE 5
#define HR_MA_WINDOW 5
uint32_t red_buffer[BUFFER_SIZE];
uint32_t ir_buffer[BUFFER_SIZE];
int buffer_index = 0;
typedef struct
{
int buffer[HR_MA_WINDOW];
int current_size;
int sum;
int front;
} HeartRateMovingAverage;
static void hr_ma_init(HeartRateMovingAverage *ma)
{
ma->current_size = 0;
ma->sum = 0;
ma->front = 0;
for (int i = 0; i < HR_MA_WINDOW; ++i)
{
ma->buffer[i] = 0;
}
}
static int32_t hr_ma_add(HeartRateMovingAverage *ma, int32_t data)
{
if (ma->current_size == HR_MA_WINDOW)
{
ma->sum -= ma->buffer[ma->front];
ma->buffer[ma->front] = data;
ma->front = (ma->front + 1) % HR_MA_WINDOW;
}
else
{
ma->buffer[(ma->front + ma->current_size) % HR_MA_WINDOW] = data;
ma->current_size++;
}
ma->sum += data;
return (int32_t)((ma->sum + ma->current_size / 2) / ma->current_size);
}
// I2C 使用 I2C1
#define I2C_INST MXC_I2C1
static int32_t sanitize_heart_rate(int32_t heart_rate, int8_t valid)
{
if (valid && heart_rate > 0 && heart_rate < 18000)
{
return heart_rate;
}
return -1;
}
static int32_t sanitize_spo2(int32_t spo2, int8_t valid)
{
if (valid && spo2 > 0 && spo2 <= 10000)
{
return spo2;
}
return -1;
}
static uint8_t choose_scale_for_text(const char *text, uint8_t max_scale)
{
uint8_t scale = max_scale;
const uint8_t min_margin = 12;
while (scale > 1)
{
uint8_t width = SSD1306_ScaledTextWidth(text, scale);
if (width <= (uint8_t)(SSD1306_WIDTH - 2 * min_margin))
{
break;
}
scale--;
}
if (scale == 0)
{
scale = 1;
}
return scale;
}
static void display_init_screen(void)
{
SSD1306_Clear();
}
static HeartRateMovingAverage g_hr_ma;
static bool g_hr_ma_initialized = false;
static bool display_update_metrics(int32_t heart_rate, int32_t spo2)
{
static int32_t last_hr_display = -2;
static int32_t last_spo2_display = -2;
bool updated = false;
const uint8_t max_scale = 3;
if (heart_rate != last_hr_display)
{
char hr_text[8];
if (heart_rate >= 0)
{
snprintf(hr_text, sizeof(hr_text), "%ld", (long)heart_rate);
}
else
{
strcpy(hr_text, "---");
}
uint8_t hr_scale = choose_scale_for_text(hr_text, max_scale);
uint8_t hr_width = SSD1306_ScaledTextWidth(hr_text, hr_scale);
uint8_t hr_x = (SSD1306_WIDTH > hr_width) ? (uint8_t)((SSD1306_WIDTH - hr_width) / 2) : 0;
SSD1306_ClearArea(0, SSD1306_WIDTH, 0, 3);
SSD1306_ClearArea(0, SSD1306_WIDTH, 3, 1);
SSD1306_PrintScaledString(hr_x, 0, hr_text, hr_scale, hr_scale);
SSD1306_DrawString(4, 3, "HR");
SSD1306_DrawString((uint8_t)(SSD1306_WIDTH - 3U * 6U), 3, "BPM");
last_hr_display = heart_rate;
updated = true;
}
if (spo2 != last_spo2_display)
{
char spo2_text[8];
if (spo2 >= 0)
{
snprintf(spo2_text, sizeof(spo2_text), "%ld", (long)spo2);
}
else
{
strcpy(spo2_text, "---");
}
uint8_t spo2_scale = choose_scale_for_text(spo2_text, max_scale);
uint8_t spo2_width = SSD1306_ScaledTextWidth(spo2_text, spo2_scale);
uint8_t spo2_x = (SSD1306_WIDTH > spo2_width) ? (uint8_t)((SSD1306_WIDTH - spo2_width) / 2) : 0;
SSD1306_ClearArea(0, SSD1306_WIDTH, 4, 3);
SSD1306_ClearArea(0, SSD1306_WIDTH, 7, 1);
SSD1306_PrintScaledString(spo2_x, 4, spo2_text, spo2_scale, spo2_scale);
SSD1306_DrawString(4, 7, "SpO2");
SSD1306_DrawString((uint8_t)(SSD1306_WIDTH - 6U), 7, "%");
last_spo2_display = spo2;
updated = true;
}
return updated;
}
static void log_measurements(int32_t heart_rate, int32_t spo2)
{
if (heart_rate >= 0)
{
printf("Heart Rate = %d BPM, ", heart_rate);
}
else
{
printf("Heart Rate = --- , ");
}
if (spo2 >= 0)
{
printf("SpO2 = %d%%\n", spo2);
}
else
{
printf("SpO2 = ---\n");
}
}
// ---------------- I2C 基础读写函数 ----------------
int max30102_write_reg(uint8_t reg, uint8_t value)
{
uint8_t data[2] = {reg, value};
mxc_i2c_req_t req;
req.i2c = I2C_INST;
req.addr = MAX30102_ADDR;
req.tx_buf = data;
req.tx_len = 2;
req.rx_buf = NULL;
req.rx_len = 0;
req.restart = 0;
req.callback = NULL;
return MXC_I2C_MasterTransaction(&req);
}
int max30102_read_reg(uint8_t reg, uint8_t *value)
{
mxc_i2c_req_t req;
int err;
req.i2c = I2C_INST;
req.addr = MAX30102_ADDR;
req.tx_buf = ®
req.tx_len = 1;
req.rx_buf = NULL;
req.rx_len = 0;
req.restart = 1; // Repeated START
req.callback = NULL;
err = MXC_I2C_MasterTransaction(&req);
if (err != E_NO_ERROR)
return err;
req.tx_buf = NULL;
req.tx_len = 0;
req.rx_buf = value;
req.rx_len = 1;
req.restart = 0;
req.callback = NULL;
return MXC_I2C_MasterTransaction(&req);
}
int max30102_read_multi(uint8_t reg, uint8_t *buf, int len)
{
mxc_i2c_req_t req;
int err;
req.i2c = I2C_INST;
req.addr = MAX30102_ADDR;
req.tx_buf = ®
req.tx_len = 1;
req.rx_buf = NULL;
req.rx_len = 0;
req.restart = 1;
req.callback = NULL;
err = MXC_I2C_MasterTransaction(&req);
if (err != E_NO_ERROR)
return err;
req.tx_buf = NULL;
req.tx_len = 0;
req.rx_buf = buf;
req.rx_len = len;
req.restart = 0;
req.callback = NULL;
return MXC_I2C_MasterTransaction(&req);
}
// ---------------- MAX30102 初始化 ----------------
int max30102_init(void)
{
int ret;
uint8_t part_id;
MXC_I2C_Shutdown(I2C_INST);
MXC_I2C_Init(I2C_INST, 1, 0); // master mode
MXC_I2C_SetFrequency(I2C_INST, 400000); // 400kHz
ret = max30102_read_reg(REG_PART_ID, &part_id);
if (ret != E_NO_ERROR)
{
printf("I2C read failed\n");
return ret;
}
if (part_id != 0x15)
{
printf("MAX30102 not found! Part ID: 0x%02X\n", part_id);
return -1;
}
printf("MAX30102 detected, Part ID: 0x%02X\n", part_id);
max30102_write_reg(REG_MODE_CONFIG, 0x40);
MXC_Delay(MXC_DELAY_MSEC(10));
max30102_write_reg(REG_MODE_CONFIG, 0x03); // SpO2 模式
max30102_write_reg(REG_SPO2_CONFIG, 0x27); // 18-bit, 100Hz
max30102_write_reg(REG_LED1_PA, 0x24); // 红光 ~7mA
max30102_write_reg(REG_LED2_PA, 0x24); // IR ~7mA
return 0;
}
// ---------------- 读取 FIFO 数据 ----------------
int max30102_read_fifo(uint32_t *red, uint32_t *ir)
{
uint8_t buf[6];
int ret = max30102_read_multi(REG_FIFO_DATA, buf, 6);
if (ret != E_NO_ERROR)
return ret;
*red = ((uint32_t)buf[0] << 16) | ((uint32_t)buf[1] << 8) | buf[2];
*ir = ((uint32_t)buf[3] << 16) | ((uint32_t)buf[4] << 8) | buf[5];
*red &= 0x3FFFF; // 18-bit
*ir &= 0x3FFFF;
return 0;
}
// ---------------- 主程序 ----------------
int main(void)
{
uint32_t red, ir;
if (max30102_init() != 0)
{
printf("MAX30102 init failed!\n");
return 0;
}
SSD1306_Init();
display_init_screen();
if (!g_hr_ma_initialized)
{
hr_ma_init(&g_hr_ma);
g_hr_ma_initialized = true;
}
while (1)
{
if (max30102_read_fifo(&red, &ir) == 0)
{
red_buffer[buffer_index] = red;
ir_buffer[buffer_index] = ir;
buffer_index++;
if (buffer_index >= BUFFER_SIZE)
{
int32_t spo2, heart_rate;
int8_t spo2_valid, hr_valid;
// 调用算法
maxim_heart_rate_and_oxygen_saturation(
ir_buffer, BUFFER_SIZE,
red_buffer,
&spo2, &spo2_valid,
&heart_rate, &hr_valid);
int32_t filtered_hr = heart_rate;
if (hr_valid && heart_rate > 0 && heart_rate < 18000)
{
filtered_hr = hr_ma_add(&g_hr_ma, heart_rate);
}
else
{
hr_ma_init(&g_hr_ma);
}
int32_t display_hr = sanitize_heart_rate(filtered_hr, hr_valid);
int32_t display_spo2 = sanitize_spo2(spo2, spo2_valid);
log_measurements(display_hr, display_spo2);
if (display_update_metrics(display_hr, display_spo2))
{
SSD1306_DisplayFrame(SSD1306_Buffer);
}
// 重置 buffer
buffer_index = 0;
}
}
// 100Hz 采样
MXC_Delay(MXC_DELAY_MSEC(10));
}
}RGB渐变功能
#include "mxc_device.h"
#include "mxc_delay.h"
#include "gpio.h"
#include "board.h"
#include <stdint.h>
#include <stddef.h>
static const mxc_gpio_cfg_t rgb_red_pin = {
MXC_GPIO2, MXC_GPIO_PIN_0, MXC_GPIO_FUNC_OUT,
MXC_GPIO_PAD_NONE, MXC_GPIO_VSSEL_VDDIOH, MXC_GPIO_DRVSTR_0};
static const mxc_gpio_cfg_t rgb_green_pin = {
MXC_GPIO2, MXC_GPIO_PIN_1, MXC_GPIO_FUNC_OUT,
MXC_GPIO_PAD_NONE, MXC_GPIO_VSSEL_VDDIOH, MXC_GPIO_DRVSTR_0};
static const mxc_gpio_cfg_t rgb_blue_pin = {
MXC_GPIO2, MXC_GPIO_PIN_2, MXC_GPIO_FUNC_OUT,
MXC_GPIO_PAD_NONE, MXC_GPIO_VSSEL_VDDIOH, MXC_GPIO_DRVSTR_0};
static void rgb_apply_pwm(uint8_t r, uint8_t g, uint8_t b, uint32_t duration_ms)
{
const uint32_t frame_us = 1000;
uint32_t cycles = duration_ms;
if (cycles == 0)
{
cycles = 1;
}
for (uint32_t c = 0; c < cycles; ++c)
{
uint32_t r_on = ((uint32_t)r * frame_us) / 255U;
uint32_t g_on = ((uint32_t)g * frame_us) / 255U;
uint32_t b_on = ((uint32_t)b * frame_us) / 255U;
if (r_on)
{
MXC_GPIO_OutClr(rgb_red_pin.port, rgb_red_pin.mask);
MXC_Delay(MXC_DELAY_USEC(r_on));
MXC_GPIO_OutSet(rgb_red_pin.port, rgb_red_pin.mask);
}
if (g_on)
{
MXC_GPIO_OutClr(rgb_green_pin.port, rgb_green_pin.mask);
MXC_Delay(MXC_DELAY_USEC(g_on));
MXC_GPIO_OutSet(rgb_green_pin.port, rgb_green_pin.mask);
}
if (b_on)
{
MXC_GPIO_OutClr(rgb_blue_pin.port, rgb_blue_pin.mask);
MXC_Delay(MXC_DELAY_USEC(b_on));
MXC_GPIO_OutSet(rgb_blue_pin.port, rgb_blue_pin.mask);
}
uint32_t used = r_on + g_on + b_on;
if (used < frame_us)
{
MXC_Delay(MXC_DELAY_USEC(frame_us - used));
}
}
}
static void hsv_to_rgb(uint16_t hue, uint8_t sat, uint8_t val,
uint8_t *r, uint8_t *g, uint8_t *b)
{
if (sat == 0U)
{
*r = *g = *b = val;
return;
}
hue %= 360U;
uint16_t region = hue / 60U;
uint16_t remainder = (hue % 60U) * 255U / 60U;
uint32_t p = ((uint32_t)val * (255U - sat)) / 255U;
uint32_t q = ((uint32_t)val * (255U - ((uint32_t)sat * remainder) / 255U)) / 255U;
uint32_t t = ((uint32_t)val * (255U - ((uint32_t)sat * (255U - remainder)) / 255U)) / 255U;
switch (region)
{
case 0:
*r = val;
*g = (uint8_t)t;
*b = (uint8_t)p;
break;
case 1:
*r = (uint8_t)q;
*g = val;
*b = (uint8_t)p;
break;
case 2:
*r = (uint8_t)p;
*g = val;
*b = (uint8_t)t;
break;
case 3:
*r = (uint8_t)p;
*g = (uint8_t)q;
*b = val;
break;
case 4:
*r = (uint8_t)t;
*g = (uint8_t)p;
*b = val;
break;
default:
*r = val;
*g = (uint8_t)p;
*b = (uint8_t)q;
break;
}
}
static void RGB_Init(void)
{
MXC_GPIO_Config(&rgb_red_pin);
MXC_GPIO_Config(&rgb_green_pin);
MXC_GPIO_Config(&rgb_blue_pin);
MXC_GPIO_OutSet(rgb_red_pin.port, rgb_red_pin.mask);
MXC_GPIO_OutSet(rgb_green_pin.port, rgb_green_pin.mask);
MXC_GPIO_OutSet(rgb_blue_pin.port, rgb_blue_pin.mask);
}
int main(void)
{
if (Board_Init() != E_NO_ERROR)
{
return -1;
}
RGB_Init();
uint16_t hue = 0;
while (1)
{
uint8_t r, g, b;
hsv_to_rgb(hue, 255U, 255U, &r, &g, &b);
rgb_apply_pwm(r, g, b, 18U); // 55hz
hue = (uint16_t)((hue + 2U) % 360U);
}
return 0;
}5、总结
这次DIY活动提供的主控让我体验到了低功耗的边缘计算体验,同时在DIY的任务中,通过对传感器驱动中滤波功能函数的添加,对这款传感器的原理有了了解,总的来说是一次非常有意义的DIY活动
我要赚赏金
