这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » Let'sdo2025年第2期—智能手环DIY活动成果帖(基础任务)

共1条 1/1 1 跳转至

Let'sdo2025年第2期—智能手环DIY活动成果帖(基础任务)

助工
2025-10-12 22:20:12     打赏

1、硬件连接

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

3a07d38d305e0143179954c1e1907cdf.png

2、软件

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

245473b4a42549b5b2796ecc09480c0e.png


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 = &reg;
    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 = &reg;
    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活动


共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]