这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » 智能手环DIY活动成果帖【基础任务-心率/血氧检测显示】

共1条 1/1 1 跳转至

智能手环DIY活动成果帖【基础任务-心率/血氧检测显示】

菜鸟
2025-10-07 21:16:33     打赏

一、硬件介绍

1、产品特点

MAX78000FTHR 开发板集成卷积神经网络加速器,将ARM Cortex-M4处理器与浮点单元 (FPU)、卷积神经网络 (CNN) 加速器和 RISC-V 内核组合在一起,包括MAX20303 PMIC,用于电池和电源管理,兼容Adafruit Feather Wing外设扩展板。评估板包括各种外设,例如CMOS VGA图像传感器、数字麦克风、低功耗立体声音频CODEC、1MB QSPI SRAM、micro SD存储卡连接器、RGB指示LED和按键;

max78000fthr.jpg


板载的MAX32625微控制器已预先编程有 DAPLink 固件,可通过 USB 对 MAX78000 Arm 内核进行调试和编程

标准10pin引脚JTAG接口,可调试和编程MAX78000的RISC-V内核


特性

  • 双核:Arm Cortex-M4 FPU处理器,100MHz;RISC-V协处理器,60MHz

  • 512KB闪存

  • 128KB SRAM

  • 16KB 缓存

  • 卷积神经网络加速器

  • 12位并行摄像头接口

  • MAX20303可穿戴PMIC,带电量计

  • Micro SD卡连接器

  • CMOS VGA图像传感器

  • 低功耗、立体声音频编解码器

  • 数字麦克风


硬件框图

硬件框图.jpg


系统框图

image-20250924223301534.png


2、功能引脚示意图 / 原理图

原理图


板载按钮

SW1:用户可编程功能按钮;

连接到 P0_2

SW2:用户可编程功能按钮;

连接到 P1_7

SW3:PMIC 电源按钮;

Power_Button:当电路板处于通电状态时,按住此按钮 12 秒将执行硬断电; 当电路板处于断电状态时,按下此按钮可重新打开电路板电源。

该按钮可以被MAX78000读取,当按下按钮时,PMIC_PFN2(P3_0)信号进入逻辑低电平状态。

SW4:连接到 RSTN 的输入;

用于复位 MCU

SW5:DAPLink 固件更新按钮;

用于 DAPLINK 固件的更新; 当按下按钮上电时,将进入固件更新模式;


板载LED

D1: 该LED可由用户控制,连接到对应的GPIO端口;

LED_R:P2_0 LED_G:P2_1 LED_B:P2_2

D2:连接到MAX20303的PMIC_LEDx输出;

这些LED可以通过I2C命令进行控制; 还可以通过 I2C 命令配置为充电状态指示灯;

PMIC_LED1(Red) PMIC_LED2(Green) PMIC_LED0(Blue)

D3:DAPLink(MAX32625)状态指示灯;

由 DAPLink 控制


引脚图

image-20250923220205380.png

image-20250923220205380

image-20250923220052647

image-20250923220052647.png



脚序名称功能
1RST主复位引脚
23V33.3V 输出为外设提供 3.3V 电压
31V81.8V 输出为外设提供 1.8V 电压
4GND
5P2_3GPIO 或 模拟输入(AIN3 通道)
6P2_4GPIO 或 模拟输入(AIN4 通道)
7P1_1GPIO 或 UART2_Tx
8P1_0GPIO 或 UART2_Rx
9MPC1GPIO 由 PMIC 通过 I2C 接口控制开漏 或 推挽输出
10MPC2GPIO 由 PMIC 通过 I2C 接口控制开漏 或 推挽输出
11P0_7GPIO 或 QSPI0 时钟SD卡 和 板载 QSPI SRAM 共享
12P0_5GPIO 或 QSPI0 MOSISD卡 和 板载 QSPI SRAM 共享
13P0_6GPIO 或 QSPI0 MISOSD卡 和 板载 QSPI SRAM 共享
14P2_6GPIO 或 LPUART_Rx
15P2_7GPIO 或 LPUART_Tx
16GND
17SYS这是主系统电源,可在电池电压 和 USB电源 之间自动切换 (5V)
18PWR强制关机按钮对地短路 13 秒,则关闭 PMIC
19VBUSUSB_VBUS当连接到 USB 时,为外设提供 5V 电压在不使用 USB 连接时,也可以用作为电路的供电输入(最好不要,因为没有电路防止电流回流至USB)
20P1_6GPIO
21MPC3GPIO 由 PMIC 通过 I2C 接口控制开漏 或 推挽输出
22P0_9GPIO 或 QSPI0 SDIO3SD 卡和板载 QSPI SRAM 共享
23P0_8GPIO 或 QSPI0 SDIO2SD卡和板载 QSPI SRAM 共享
24P0_11GPIO 或 QSPI0_Slave
25P0_19GPIO
26P3_1GPIO 或 Wake-up该引脚为 3.3V
27P0_16GPIO 或 I2C1_SCL板载电平转换器允许通过 R15 或 R20 电阻器选择 1.8V 或 3.3V (详见原理图)
28P0_17GPIO 或 I2C1_SDA 板载电平转换器允许通过 R15 或 R20 电阻选择 1.8V 或 3.3V (详见原理图)


3、血氧心率模块 (MAX30102 )

MAX30102是一个集成的脉搏血氧仪和心率监测仪生物传感器的模块; 它集成了一个红光LED和一个红外光LED、光电检测器、光器件,以及带环境光抑制的低噪声电子电路,采用I2C协议进行通信;

芯片手册

image-20250928222026005

image-20250928222026005.png


基本原理

当光线照射到皮肤组织(通常是指尖、耳垂或手腕)时,一部分光会被组织、骨骼、静脉血等非脉动成分吸收,这部分吸收是恒定不变的。另一部分光则会穿透组织,被动脉血吸收。而动脉血会因为心脏的泵血活动而发生周期性的脉动(血液容积变化)。


主要参数:

I2C地址:0x57


名称参数
LED峰值波长器660nm / 880nm
LED供电电压3.3 ~ 5V
检测信号类型光反射信号(PPG)
输出信号接口I2C接口
通信接口电压1.8 ~ 3.3V ~ 5V


引脚说明

MAX30102内置了两个LED光源(红光RD和红外光IRD)它们是IR和RED的驱动,不需要外部连接,内部已经连接好了;


名称                功能

VIN                 电源输入:1.8V - 5V (默认:3V3)

SDA                I2C数据线

SCL                I2C时钟线

GND               地

RD                 红色LED接地端,一般不接

IRD                红外光IR_LED接地端,一般不接

INT                中断引脚:低电平有效



系统框图

image-20250928222407136.png

原理图

image-20250929095558965.png

image-20250929095558965


4、0.96寸OLED 黄蓝显示屏 (SSD1315)

使用文档

数据手册

黄蓝显示屏 (SSD1315) 是一款蓝色和黄色双色显示屏,该显示屏支持 3.3V 和 5V 电源电压。

可以使用 I2CSPI 接口;

若使用 SPI 需要焊接后面;

image-20250927113345290

image-20250927113345290.png


特性

  • 兼容 3.3V / 5V 电源

  • 可更改 I2C 地址

  • 支持 SPI

  • 低功耗

  • 黄色和蓝色双色 128×64 像素

  • 高对比度,高亮度

  • 宽工作温度范围:-40℃ ~ +85 ℃


原理图

image-20250927114811657.png


二、硬件连接

模块通过硬件I2C的方式连接至开发板;


开发板                                MAX30102模块      OLED模块

3V3                                           VIN                        VCC

GND                                         GND                      GND

P0_16(I2C1_SCL)                    SCL                       SCL

P0_17(I2C1_SDA)                   SDA                       SDA

P0_19                                       INT                          -


实物效果

image-20251006001751679.png

三、实现思想

实现效果

使用MAX78000FTHR开发板通过硬件I2C方式连接相关模块; 通过MAX30102模块通过手指检测心率 / 血氧数据,并在OLED屏幕上实时显示,实现采集、检测、分析等功能;


UI布局

OLED屏幕上的UI布局,总共由5个部分组成;

1、正中间显示标题【智能检测手环】

2、左上半部分【心形图标】,实现测量过程中的律动效果;

3、左下半部分【显示测量状态(等待测量 / 测量中 / 最后的结果)】

4、右上部分【显示测量过程中,实时的心率 / 血氧数值】

5、右下部分【显示测量过程中,心率实时变化的折线图】


测量前

显示等待测量画面;

image-20251006181804761.png

测量过程中

显示当前测量的进度,以及实时数值、变化趋势图; 同时产生心形律动效果;

image-20251006181929396.png

image-20251006181929396

测量结束

显示1分钟后测得的结果;

image-20251006182440305.png

主要流程图

D1

D1.png



四、代码编写

MAX78000FTHR手册

MAX78000手册

SDK使用文档


主要相关代码

main.c

/***** Includes *****/
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include "board.h"
#include "mxc_device.h"
#include "nvic_table.h"

#include "oled.h"
#include "max30102.h"

static u8g2_t u8g2;

// 左侧宽度
#define LHS_W 64
#define TITLE_H 12
#define REALTIME_LINE_H 12
#define REALTIME_LINES 2

// 心形尺寸
#define HEART_R_BIG 8
#define HEART_R_SMALL 5

// 折线图
#define GRAPH_W 48
#define GRAPH_H 20
#define GRAPH_X (LHS_W + (64 - GRAPH_W)) // 128x64 OLED,右侧 64 区域再向右贴
#define GRAPH_Y (64 - GRAPH_H - 2)

// 心率映射范围
#define HR_MIN 30
#define HR_MAX 130

/***** 数据存储 *****/
static uint8_t hr_graph[GRAPH_W];
static uint8_t graph_count = 0;

static uint8_t last_avg_hr = 0;
static uint8_t last_avg_spo2 = 0;
static uint8_t avg_valid = 0;
static uint8_t measuring = 0; // 1=正在测量

static uint32_t sum_hr = 0, sum_spo2 = 0;
static uint8_t inst_hr = 0, inst_spo2 = 0;

/***** I2C 初始化 *****/
static int i2c_init(void)
{
    int error = MXC_I2C_Init(I2C_MASTER, 1, 0);
    if (error != E_NO_ERROR)
    {
        printf("I2C init fail:%d\n", error);
        return error;
    }
    MXC_I2C_SetFrequency(I2C_MASTER, I2C_FREQ);
    printf("I2C init success\n");
    return E_NO_ERROR;
}

/***** 标题 *****/
static void draw_title(void)
{
    const char *title = "智能检测手环";
    u8g2_SetFont(&u8g2, u8g2_font_wqy12_t_gb2312a);
    int w = u8g2_GetUTF8Width(&u8g2, title);
    int x = (128 - w) / 2;
    if (x < 0)
        x = 0;
    u8g2_DrawUTF8(&u8g2, x, TITLE_H, title);
}

/***** 心形布局辅助 *****/
static int calc_center_start(int total_block_h)
{
    int start = (64 - total_block_h) / 2;
    if (start < 0)
        start = 0;
    return start;
}

static int heart_block_height(void)
{
    int r = HEART_R_BIG;
    return r + r / 2 + r; // ~2.5r
}

/***** 绘制心形 *****/
static void draw_heart_icon_at(int top_y, uint8_t hr)
{
    static uint8_t frame = 0;
    frame++;
    uint8_t big = (frame & 0x08) ? 1 : 0;
    if (hr == 0)
        big = 0;

    uint8_t r = big ? HEART_R_BIG : HEART_R_SMALL;
    int cx = LHS_W / 2 - 8; // 左侧区域X
    int heart_h = r + r / 2 + r + 13;
    int cy = top_y + heart_h / 2;

    u8g2_DrawDisc(&u8g2, cx - r / 2, cy - r / 3, r / 2, U8G2_DRAW_ALL);
    u8g2_DrawDisc(&u8g2, cx + r / 2, cy - r / 3, r / 2, U8G2_DRAW_ALL);
    u8g2_DrawTriangle(&u8g2,
                      cx - r, cy - r / 6,
                      cx + r, cy - r / 6,
                      cx, cy + r);
}

/***** 折线图 *****/
static uint8_t hr_to_level(uint8_t hr)
{
    if (hr < HR_MIN)
        hr = HR_MIN;
    if (hr > HR_MAX)
        hr = HR_MAX;
    if (GRAPH_H <= 1)
        return 0;
    return (uint8_t)((hr - HR_MIN) * (GRAPH_H - 1) / (HR_MAX - HR_MIN));
}

static void graph_push(uint8_t hr)
{
    uint8_t v = hr_to_level(hr);
    if (graph_count < GRAPH_W)
    {
        hr_graph[graph_count++] = v;
    }
    else
    {
        memmove(&hr_graph[0], &hr_graph[1], GRAPH_W - 1);
        hr_graph[GRAPH_W - 1] = v;
    }
}

static void draw_hr_graph(void)
{
    int16_t base_y = GRAPH_Y + GRAPH_H - 1;
    u8g2_DrawFrame(&u8g2, GRAPH_X, GRAPH_Y, GRAPH_W, GRAPH_H);
    for (int x = 1; x < graph_count; x++)
    {
        int y1 = base_y - hr_graph[x - 1];
        int y2 = base_y - hr_graph[x];
        u8g2_DrawLine(&u8g2, GRAPH_X + x - 1, y1, GRAPH_X + x, y2);
    }
}

/***** 左侧文本 *****/
static void draw_left_panel(uint8_t inst_hr, uint8_t progress_percent)
{
    u8g2_SetFont(&u8g2, u8g2_font_wqy12_t_gb2312a);
    int line_h = 12;
    const char *lines[4];
    int line_cnt = 0;

    if (measuring)
    {
        lines[line_cnt++] = "测量中...";
        static char prog[16];
        snprintf(prog, sizeof(prog), "进度:%3u%%", progress_percent);
        lines[line_cnt++] = prog;
    }
    else if (avg_valid)
    {
        lines[line_cnt++] = "1分钟结果";
        static char hr_line[20];
        static char sp_line[20];
        snprintf(hr_line, sizeof(hr_line), "心率:%3u BMP", last_avg_hr);
        snprintf(sp_line, sizeof(sp_line), "血氧:%3u%%", last_avg_spo2);
        lines[line_cnt++] = hr_line;
        lines[line_cnt++] = sp_line;
    }
    else
    {
        lines[line_cnt++] = "等待测量";
    }

    int heart_h = heart_block_height();
    int text_h = line_cnt * line_h;
    int gap = 4;
    int total_h = heart_h + (line_cnt ? gap : 0) + text_h;
    int top = calc_center_start(total_h);

    draw_heart_icon_at(top, inst_hr);

    int text_y_base = top + heart_h + gap + line_h - 2;
    for (int i = 0; i < line_cnt; i++)
    {
        u8g2_DrawUTF8(&u8g2, 2, text_y_base + i * line_h, lines[i]);
    }
}

/***** 整体 UI 刷新 *****/
static void oled_draw_ui(uint8_t inst_hr, uint8_t inst_spo2, uint8_t progress_percent)
{
    u8g2_ClearBuffer(&u8g2);

    draw_title();
    draw_left_panel(inst_hr, progress_percent);

    // 右侧实时
    u8g2_SetFont(&u8g2, u8g2_font_wqy12_t_gb2312a);
    int rt_x = LHS_W + 12;
    int base_y = TITLE_H + REALTIME_LINE_H + 2;
    char line[32];
    snprintf(line, sizeof(line), "心率:%3u", inst_hr);
    u8g2_DrawUTF8(&u8g2, rt_x, base_y, line);
    snprintf(line, sizeof(line), "血氧:%3u%%", inst_spo2);
    u8g2_DrawUTF8(&u8g2, rt_x, base_y + REALTIME_LINE_H, line);

    // 右下折线图
    if (inst_hr)
        graph_push(inst_hr);
    draw_hr_graph();

    u8g2_SendBuffer(&u8g2);
}

/***** 1分钟测量 *****/
static int measure_one_minute(uint8_t *avg_hr, uint8_t *avg_spo2)
{
    measuring = 1;
    uint32_t sum_hr = 0, sum_spo2 = 0;
    uint32_t valid = 0;
    uint32_t samples = 0;
    uint8_t inst_hr = 0, inst_spo2 = 0;
    
    const uint16_t TOTAL_LOOPS = 900;
    // 60s 采样数据
    for (uint32_t loop = 0; loop < TOTAL_LOOPS; loop++)
    {
        uint8_t hr, sp;
        if (max30102_get(&hr, &sp))
        {
            inst_hr = hr;
            inst_spo2 = sp;
            sum_hr += hr;
            sum_spo2 += sp;
            valid++;
        }

        samples++;
        uint8_t percent = (uint8_t)((loop * 100) / TOTAL_LOOPS);
        oled_draw_ui(inst_hr, inst_spo2, percent);
    }
    measuring = 0;
    if (valid == 0)
        return 0;
    *avg_hr = (uint8_t)(sum_hr / valid);
    *avg_spo2 = (uint8_t)(sum_spo2 / valid);
    return 1;
}

int main(void)
{
    uint8_t hr, sp;
    i2c_init();
    u8g2Init(&u8g2);
    u8g2_ClearBuffer(&u8g2);
    if (max30102_init() != E_NO_ERROR)
    {
        printf("MAX30102 init failed\n");
    }
    else
    {
        printf("MAX30102 init OK\n");
    }

    max30102_getFirst();
    
    // 等待手指放上
    while (!max30102_get(&hr, &sp))
    {
        oled_draw_ui(0, 0, 0); // 只显示等待与心形动画
    }
    printf("检测到手指,开始测量\n");
    while (1)
    {
        uint8_t avg_hr, avg_spo2;
        if (measure_one_minute(&avg_hr, &avg_spo2))
        {
            last_avg_hr = avg_hr;
            last_avg_spo2 = avg_spo2;
            avg_valid = 1;
            printf("1分钟平均心率:%uBMP, 血氧:%u%%\n", avg_hr, avg_spo2);
        }
        else
        {
            printf("没有有效数据\n");
        }
        // 显示最终结果
        oled_draw_ui(last_avg_hr, last_avg_spo2, 100);
        MXC_Delay(3000000); // 3s后若再次检测到手指时  重新开始测量
        while (!max30102_get(&hr, &sp));
    }
}


编译代码

使用CTRL + SHIFT + B 选择 Build 编译项目;

或使用终端 make -j16 PROJECT=GPIO,编译成 .elf 程序二进制文件;



五、程序烧录

1、用数据线连接开发板至电脑上;

2、程序烧录

使用CTRL + SHIFT + B 选择 flash & run 烧录并运行程序;


六、相关过程帖

环境配置 / RGB呼吸灯

OLED屏幕使用方法

MAX30102模块使用方法



七、演示效果



代码文件

项目代码.zip






关键词: MAX78000FTHR    

共1条 1/1 1 跳转至

回复

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