一、硬件介绍
1、产品特点
MAX78000FTHR 开发板集成卷积神经网络加速器,将ARM Cortex-M4处理器与浮点单元 (FPU)、卷积神经网络 (CNN) 加速器和 RISC-V 内核组合在一起,包括MAX20303 PMIC,用于电池和电源管理,兼容Adafruit Feather Wing外设扩展板。评估板包括各种外设,例如CMOS VGA图像传感器、数字麦克风、低功耗立体声音频CODEC、1MB QSPI SRAM、micro SD存储卡连接器、RGB指示LED和按键。
板载的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图像传感器
低功耗、立体声音频编解码器
数字麦克风
硬件框图
系统框图
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 控制
引脚图
脚序 | 名称 | 功能 |
1 | RST | 主复位引脚 |
2 | 3V3 | 3.3V 输出为外设提供 3.3V 电压 |
3 | 1V8 | 1.8V 输出为外设提供 1.8V 电压 |
4 | GND | 地 |
5 | P2_3 | GPIO 或 模拟输入(AIN3 通道) |
6 | P2_4 | GPIO 或 模拟输入(AIN4 通道) |
7 | P1_1 | GPIO 或 UART2_Tx |
8 | P1_0 | GPIO 或 UART2_Rx |
9 | MPC1 | GPIO 由 PMIC 通过 I2C 接口控制开漏 或 推挽输出 |
10 | MPC2 | GPIO 由 PMIC 通过 I2C 接口控制开漏 或 推挽输出 |
11 | P0_7 | GPIO 或 QSPI0 时钟SD卡 和 板载 QSPI SRAM 共享 |
12 | P0_5 | GPIO 或 QSPI0 MOSISD卡 和 板载 QSPI SRAM 共享 |
13 | P0_6 | GPIO 或 QSPI0 MISOSD卡 和 板载 QSPI SRAM 共享 |
14 | P2_6 | GPIO 或 LPUART_Rx |
15 | P2_7 | GPIO 或 LPUART_Tx |
16 | GND | 地 |
17 | SYS | 这是主系统电源,可在电池电压 和 USB电源 之间自动切换 (5V) |
18 | PWR | 强制关机按钮对地短路 13 秒,则关闭 PMIC |
19 | VBUS | USB_VBUS当连接到 USB 时,为外设提供 5V 电压在不使用 USB 连接时,也可以用作为电路的供电输入(最好不要,因为没有电路防止电流回流至USB) |
20 | P1_6 | GPIO |
21 | MPC3 | GPIO 由 PMIC 通过 I2C 接口控制开漏 或 推挽输出 |
22 | P0_9 | GPIO 或 QSPI0 SDIO3SD 卡和板载 QSPI SRAM 共享 |
23 | P0_8 | GPIO 或 QSPI0 SDIO2SD卡和板载 QSPI SRAM 共享 |
24 | P0_11 | GPIO 或 QSPI0_Slave |
25 | P0_19 | GPIO |
26 | P3_1 | GPIO 或 Wake-up该引脚为 3.3V |
27 | P0_16 | GPIO 或 I2C1_SCL板载电平转换器允许通过 R15 或 R20 电阻器选择 1.8V 或 3.3V (详见原理图) |
28 | P0_17 | GPIO 或 I2C1_SDA |
3、血氧心率模块 (MAX30102 )
MAX30102是一个集成的脉搏血氧仪和心率监测仪生物传感器的模块。它集成了一个红光LED和一个红外光LED、光电检测器、光器件,以及带环境光抑制的低噪声电子电路,采用I2C协议进行通信;
基本原理
当光线照射到皮肤组织(通常是指尖、耳垂或手腕)时,一部分光会被组织、骨骼、静脉血等非脉动成分吸收,这部分吸收是恒定不变的。另一部分光则会穿透组织,被动脉血吸收。而动脉血会因为心脏的泵血活动而发生周期性的脉动(血液容积变化)。
主要参数:
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 | 中断引脚:低电平有效 |
系统框图
原理图
二、硬件连接
开发板通过硬件I2C的方式连接MAX30102;
开发板 模块
3V3 VIN
GND GND
P0_16(SCL) SCL
P0_17(SDA) SDA
P0_19 INT
实物效果
三、代码编写
读取心率和血氧饱和度(SPO2)数据
测量算法核心思路:上电后首先进行器件的初始化,然后通过IIC读取500组传感器数据,将其中的最大值和最小值筛选出来,调用函数计算500组数据的心率值有效性、血氧值有效性、心率值和血氧值,起到一个滤除前期杂波数据的作用,最后在while(1)中循环处理以下逻辑:舍弃前100组数据,将后400组数据移植前方,即将100-500缓存数据移位到0-400,并寻找其中的最大值和最小值,通过IIC读取新的100组数据放置在401-500缓存数组中,用新获取的数据与上次数据做比较,两者取绝对值,除以最大值和最小值的差值,再乘255计算出心率曲线,调用函数计算500组数据的心率值有效性、血氧值有效性、心率值和血氧值,如果测量值有效则读出心率值和血氧值,如此反复。
主要相关代码
max30102.h
#ifndef __MAX30102_H
#define __MAX30102_H
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include "board.h"
#include "mxc_device.h"
#include "mxc_delay.h"
#include "nvic_table.h"
#include "i2c.h"
#define MAX_BRIGHTNESS 255
#define BUFFER_LENTH 500
/* 算法部分 */
#define true 1
#define false 0
#define FS 100
#define BUFFER_SIZE (FS * 5)
#define HR_FIFO_SIZE 7
#define MA4_SIZE 4 // DO NOT CHANGE
#define HAMMING_SIZE 5 // DO NOT CHANGE
#define min(x, y) ((x) < (y) ? (x) : (y))
/* end */
#define I2C_MASTER MXC_I2C1
#define MAX30102_INT_PORT MXC_GPIO0
#define MAX30102_INT_PIN MXC_GPIO_PIN_19
#define MAX30102_INT MXC_GPIO_InGet(MAX30102_INT_PORT, MAX30102_INT_PIN)
#define I2C_FREQ 400000 // 400kHZ
#define MAX30102_I2C_ADDR 0x57
// register addresses
#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_OVF_COUNTER 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_PILOT_PA 0x10
#define REG_MULTI_LED_CTRL1 0x11
#define REG_MULTI_LED_CTRL2 0x12
#define REG_TEMP_INTR 0x1F
#define REG_TEMP_FRAC 0x20
#define REG_TEMP_CONFIG 0x21
#define REG_PROX_INT_THRESH 0x30
#define REG_REV_ID 0xFE
#define REG_PART_ID 0xFF
int max30102_init(void);
void max30102_getFirst(void);
int max30102_get(uint8_t *HeartRate, uint8_t *spo2Data);
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);
void maxim_find_peaks(int32_t *pn_locs, int32_t *pn_npks, int32_t *pn_x, int32_t n_size, int32_t n_min_height, int32_t n_min_distance, int32_t n_max_num);
void maxim_peaks_above_min_height(int32_t *pn_locs, int32_t *pn_npks, int32_t *pn_x, int32_t n_size, int32_t n_min_height);
void maxim_remove_close_peaks(int32_t *pn_locs, int32_t *pn_npks, int32_t *pn_x, int32_t n_min_distance);
void maxim_sort_ascend(int32_t *pn_x, int32_t n_size);
void maxim_sort_indices_descend(int32_t *pn_x, int32_t *pn_indx, int32_t n_size);
#endif
max30102.c
#include "max30102.h"
// I2C 写
static int i2c_write_reg(uint8_t reg, uint8_t val)
{
uint8_t buf[2] = {reg, val};
mxc_i2c_req_t req = {
.i2c = I2C_MASTER,
.addr = MAX30102_I2C_ADDR,
.tx_buf = buf,
.tx_len = 2,
.rx_buf = NULL,
.rx_len = 0,
.restart = 0,
.callback = NULL};
return MXC_I2C_MasterTransaction(&req);
}
// I2C 读取
static int i2c_read_reg(uint8_t reg, uint8_t *val)
{
mxc_i2c_req_t req = {
.i2c = I2C_MASTER,
.addr = MAX30102_I2C_ADDR,
.tx_buf = ®,
.tx_len = 1,
.rx_buf = val,
.rx_len = 1,
.restart = 1,
.callback = NULL};
return MXC_I2C_MasterTransaction(&req);
}
static int max30102_reset(void)
{
i2c_write_reg(REG_MODE_CONFIG, 0x40);
//printf("max30102 reset sucess\n");
return E_NO_ERROR;
}
// max30102 初始化
int max30102_init(void)
{
mxc_gpio_cfg_t gpio_out;
/* INT中断引脚初始化 上拉 */
gpio_out.port = MAX30102_INT_PORT;
gpio_out.mask = MAX30102_INT_PIN;
gpio_out.pad = MXC_GPIO_PAD_PULL_UP;
gpio_out.func = MXC_GPIO_FUNC_IN;
gpio_out.vssel = MXC_GPIO_VSSEL_VDDIO;
gpio_out.drvstr = MXC_GPIO_DRVSTR_0;
MXC_GPIO_Config(&gpio_out);
max30102_reset();
// 清空 FIFO 指针
i2c_write_reg(REG_FIFO_WR_PTR, 0x00);
i2c_write_reg(REG_OVF_COUNTER, 0x00);
i2c_write_reg(REG_FIFO_RD_PTR, 0x00);
i2c_write_reg(REG_FIFO_CONFIG, 0x0F);
i2c_write_reg(REG_SPO2_CONFIG, 0x27); // SPO2_ADC
i2c_write_reg(REG_LED1_PA, 0x24); // RED
i2c_write_reg(REG_LED2_PA, 0x24); // IR
i2c_write_reg(REG_PILOT_PA, 0x7F);
// 模式: SpO2 模式 = 0x03
i2c_write_reg(REG_MODE_CONFIG, 0x03);
i2c_write_reg(REG_INTR_ENABLE_1, 0xc0); // INTR setting
i2c_write_reg(REG_INTR_ENABLE_2, 0x00);
return E_NO_ERROR;
}
// 读取6个字节 数据
static int max30102_read_fifo(uint32_t *red, uint32_t *ir)
{
uint8_t reg = REG_FIFO_DATA;
uint8_t data[6];
mxc_i2c_req_t req = {
.i2c = I2C_MASTER,
.addr = MAX30102_I2C_ADDR,
.tx_buf = ®,
.tx_len = 1,
.rx_buf = data,
.rx_len = 6,
.restart = 1,
.callback = NULL};
int ret = MXC_I2C_MasterTransaction(&req);
if (ret != E_NO_ERROR)
return ret;
*red = ((uint32_t)data[0] << 16) | ((uint32_t)data[1] << 8) | data[2];
*red &= 0x3FFFF; // 18 bit
*ir = ((uint32_t)data[3] << 16) | ((uint32_t)data[4] << 8) | data[5];
*ir &= 0x3FFFF;
// printf("red:%d , ir:%d \n", *red, *ir);
// 过滤异常值(例:无手指 / 未贴合检测时)
if (*ir <= 50000 || *red <= 50000)
{
*ir = 0;
*red = 0;
}
return E_NO_ERROR;
}
uint32_t aun_ir_buffer[BUFFER_LENTH];
// IR LED sensor data 红外数据,用于计算血氧
uint32_t aun_red_buffer[BUFFER_LENTH];
// Red LED sensor data 红光数据,用于计算心率曲线以及计算心率
int32_t n_sp02; // SPO2 value
int8_t ch_spo2_valid; // indicator to show if the SP02 calculation is valid
int32_t n_heart_rate; // heart rate value
int8_t ch_hr_valid; // indicator to show if the heart rate calculation is valid
uint32_t un_min = 0x3FFFF, un_max = 0, un_prev_data;
int32_t n_brightness = 0;
void max30102_getFirst(void)
{
int i;
// 读取前500个样本,并确定信号范围
for (i = 0; i < BUFFER_LENTH; i++)
{
while (MAX30102_INT == 1) // 等待中断引脚
;
max30102_read_fifo(&aun_red_buffer[i], &aun_ir_buffer[i]);
if (un_min > aun_red_buffer[i])
un_min = aun_red_buffer[i]; // 更新信号最小值
if (un_max < aun_red_buffer[i])
un_max = aun_red_buffer[i]; // 更新信号最大值
}
// 计算前500个样本后的心率和SpO2(样本的前5秒)
maxim_heart_rate_and_oxygen_saturation(aun_ir_buffer, BUFFER_LENTH, aun_red_buffer, &n_sp02, &ch_spo2_valid, &n_heart_rate, &ch_hr_valid);
}
int max30102_get(uint8_t *HeartRate, uint8_t *spo2Data)
{
float f_temp;
uint8_t temp[6];
// 读取和计算max30102数据,总体用缓存的500组数据分析,实际每读取100组新数据分析一次
un_min = 0x3FFFF;
un_max = 0;
// 将前100组样本转储到内存中(实际没有),并将后400组样本移到顶部,将100-500缓存数据移位到0-400
for (int i = 100; i < 500; i++)
{
aun_red_buffer[i - 100] = aun_red_buffer[i]; // 将100-500缓存数据移位到0-400
aun_ir_buffer[i - 100] = aun_ir_buffer[i]; // 将100-500缓存数据移位到0-400
// 更新信号的最小值和最大值
if (un_min > aun_red_buffer[i]) // 寻找移位后0-400中的最小值
un_min = aun_red_buffer[i];
if (un_max < aun_red_buffer[i]) // 寻找移位后0-400中的最大值
un_max = aun_red_buffer[i];
}
// 在计算心率前取100组样本,取的数据放在400-500缓存数组中
for (int i = 400; i < 500; i++)
{
un_prev_data = aun_red_buffer[i - 1]; // 临时记录上一次读取数据
while (MAX30102_INT == 1) // 等待中断引脚
;
max30102_read_fifo(&aun_red_buffer[i], &aun_ir_buffer[i]);
if (aun_red_buffer[i] > un_prev_data)
{ // 用新获取的一个数值与上一个数值对比
f_temp = aun_red_buffer[i] - un_prev_data;
f_temp /= (un_max - un_min);
f_temp *= MAX_BRIGHTNESS; // 公式(心率曲线)=(新数值-旧数值)/(最大值-最小值)*255
n_brightness -= (int)f_temp;
if (n_brightness < 0)
n_brightness = 0;
}
else
{
f_temp = un_prev_data - aun_red_buffer[i];
f_temp /= (un_max - un_min);
f_temp *= MAX_BRIGHTNESS; // 公式(心率曲线)=(旧数值-新数值)/(最大值-最小值)*255
n_brightness += (int)f_temp;
if (n_brightness > MAX_BRIGHTNESS)
n_brightness = MAX_BRIGHTNESS;
}
}
maxim_heart_rate_and_oxygen_saturation(aun_ir_buffer, BUFFER_LENTH, aun_red_buffer, &n_sp02, &ch_spo2_valid, &n_heart_rate, &ch_hr_valid); // 传入500个心率和血氧数据计算传感器检测结论,反馈心率和血氧测试结果
if ((1 == ch_hr_valid) && (1 == ch_spo2_valid) && (n_heart_rate < 120) && (n_sp02 < 101))
{
*HeartRate = n_heart_rate;
*spo2Data = n_sp02;
return 1; // 有效数据
}
return 0;
}
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"
#define COUNT 30
uint8_t spo2Data; // 血氧数据
uint8_t heartData; // 心率数据
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);
}
else
{
MXC_I2C_SetFrequency(I2C_MASTER, I2C_FREQ);
printf("I2C init success\n");
}
}
int main(void)
{
i2c_init();
if (max30102_init() != E_NO_ERROR)
{
printf("MAX30102 init failed\n");
}
else
printf("MAX30102 init OK\n");
max30102_getFirst();
while (1)
{
uint32_t hr_sum = 0, spo2_sum = 0;
int valid_cnt = 0;
// 有效数值 平均
while (valid_cnt < COUNT)
{
if (max30102_get(&heartData, &spo2Data))
{
hr_sum += heartData;
spo2_sum += spo2Data;
valid_cnt++;
}
}
printf("Heart Rate:%d bpm, SpO2:%d%% \n", hr_sum / valid_cnt, spo2_sum / valid_cnt);
}
}
编译代码
使用CTRL + SHIFT + B 选择 Build 编译项目;
或使用终端 make -j16 PROJECT=GPIO,编译成 .elf 程序二进制文件;
四、程序烧录
1、用数据线连接开发板至电脑上;
2、程序烧录
使用CTRL + SHIFT + B 选择 flash & run 烧录并运行程序;
五、演示效果
串口打印 心率 / 血氧的相关信息;