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活动