硬件介绍:MAX78000FTHR为快速开发平台,MAX78000 M4F处理器能够快速实施超低功耗、人工智能(AI)方案,器件集成卷积神经网络加速器。评估板包括MAX20303 PMIC,用于电池和电源管理。评估板规格为0.9in x 2.6in、双排连接器,兼容Adafruit Feather Wing外设扩展板。评估板包括各种外设,例如CMOS VGA图像传感器、数字麦克风、低功耗立体声音频CODEC、1MB QSPI SRAM、micro SD存储卡连接器、RGB指示LED和按键。
1、使用 eclipse maximsdk 的固件,学会点亮 RGB 灯
这个开发板上集成了一课3色LED,由图可知,使用了P2_0、P2_1、P2_2三个管脚,所以本任务转换为控制这三个GPIO管脚电平控制。打开官方的例程,找到GPIO例程。
/* Setup output pin. */
gpio_out.port = MXC_GPIO_PORT_OUT;
gpio_out.mask = MXC_GPIO_PIN_OUT;
gpio_out.pad = MXC_GPIO_PAD_NONE;
gpio_out.func = MXC_GPIO_FUNC_OUT;
MXC_GPIO_Config(&gpio_out);
while (1) {
/* Read state of the input pin. */
if (MXC_GPIO_InGet(gpio_in.port, gpio_in.mask)) {
/* Input pin was high, set the output pin. */
MXC_GPIO_OutSet(gpio_out.port, gpio_out.mask); //高电平
} else {
/* Input pin was low, clear the output pin. */
MXC_GPIO_OutClr(gpio_out.port, gpio_out.mask); //低电平
}
}关注这段代码,这是一段控制GPIO输出的范例代码,参考这这个写法,将P2_0、P2_1、P2_2设置为输出,即可驱动OLED灯的亮灭。

2、实现 OLED 屏幕显示信息。移植了u8g2到这个开发板上,使用u8g2开源库来驱动OLED,用起来特别的方便。
移植u8g2关键是实现回调函数
#include "mxc_device.h"
#include "mxc_delay.h"
#include "gpio.h"
#include "u8g2.h"
#include "u8x8.h"
#include "oled.h"
#define MXC_GPIO_PORT_SDA MXC_GPIO2
#define MXC_GPIO_PIN_SDA MXC_GPIO_PIN_6
#define MXC_GPIO_PORT_SCL MXC_GPIO2
#define MXC_GPIO_PIN_SCL MXC_GPIO_PIN_7
void oled_init(void) {
mxc_gpio_cfg_t gpio_out;
/* Setup output pin. */
gpio_out.port = MXC_GPIO_PORT_SDA;
gpio_out.mask = MXC_GPIO_PIN_SDA;
gpio_out.pad = MXC_GPIO_PAD_NONE;
gpio_out.func = MXC_GPIO_FUNC_OUT;
MXC_GPIO_Config(&gpio_out);
gpio_out.port = MXC_GPIO_PORT_SCL;
gpio_out.mask = MXC_GPIO_PIN_SCL;
gpio_out.pad = MXC_GPIO_PAD_NONE;
gpio_out.func = MXC_GPIO_FUNC_OUT;
MXC_GPIO_Config(&gpio_out);
}
uint8_t u8x8_gpio_and_delay(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) {
switch (msg) {
case U8X8_MSG_GPIO_AND_DELAY_INIT:
/* only support for software I2C*/
oled_init(); //初始化GPIO
break;
case U8X8_MSG_DELAY_NANO:
/* not required for SW I2C */
break;
case U8X8_MSG_DELAY_10MICRO:
/* not used at the moment */
break;
case U8X8_MSG_DELAY_100NANO:
/* not used at the moment */
break;
case U8X8_MSG_DELAY_MILLI:
MXC_Delay(arg_int * 1000UL); //毫秒延时
break;
case U8X8_MSG_DELAY_I2C:
/* arg_int is 1 or 4: 100KHz (5us) or 400KHz (1.25us) */
MXC_Delay(arg_int <= 2 ? 5 : 1); //微秒延时
break;
case U8X8_MSG_GPIO_I2C_CLOCK:
if (arg_int == 0) {
//MXC_GPIO_OutSet(MXC_GPIO_PORT_SCL, MXC_GPIO_PIN_SCL); // 高电平
MXC_GPIO_OutClr(MXC_GPIO_PORT_SCL, MXC_GPIO_PIN_SCL); // 低电平
} else {
MXC_GPIO_OutSet(MXC_GPIO_PORT_SCL, MXC_GPIO_PIN_SCL); // 高电平
//MXC_GPIO_OutClr(MXC_GPIO_PORT_SCL, MXC_GPIO_PIN_SCL); // 低电平
}
break;
case U8X8_MSG_GPIO_I2C_DATA:
if (arg_int == 0) {
//MXC_GPIO_OutSet(MXC_GPIO_PORT_SDA, MXC_GPIO_PIN_SDA); // 高电平
MXC_GPIO_OutClr(MXC_GPIO_PORT_SDA, MXC_GPIO_PIN_SDA); // 低电平
} else {
MXC_GPIO_OutSet(MXC_GPIO_PORT_SDA, MXC_GPIO_PIN_SDA); // 高电平
//MXC_GPIO_OutClr(MXC_GPIO_PORT_SDA, MXC_GPIO_PIN_SDA); // 低电平
}
break;
/*
case U8X8_MSG_GPIO_MENU_SELECT:
u8x8_SetGPIOResult(u8x8, Chip_GPIO_GetPinState(LPC_GPIO, KEY_SELECT_PORT, KEY_SELECT_PIN));
break;
case U8X8_MSG_GPIO_MENU_NEXT:
u8x8_SetGPIOResult(u8x8, Chip_GPIO_GetPinState(LPC_GPIO, KEY_NEXT_PORT, KEY_NEXT_PIN));
break;
case U8X8_MSG_GPIO_MENU_PREV:
u8x8_SetGPIOResult(u8x8, Chip_GPIO_GetPinState(LPC_GPIO, KEY_PREV_PORT, KEY_PREV_PIN));
break;
case U8X8_MSG_GPIO_MENU_HOME:
u8x8_SetGPIOResult(u8x8, Chip_GPIO_GetPinState(LPC_GPIO, KEY_HOME_PORT, KEY_HOME_PIN));
break;
*/
default:
u8x8_SetGPIOResult(u8x8, 1);
break;
}
return 1;
}
void u8g2_Init(u8g2_t *u8g2) {
u8g2_Setup_ssd1306_i2c_128x64_noname_f(u8g2, U8G2_R0, u8x8_byte_sw_i2c,
u8x8_gpio_and_delay);
u8g2_InitDisplay(u8g2);
u8g2_SetPowerSave(u8g2, 0);
u8g2_ClearBuffer(u8g2);
}u8g2_t u8g2;
int main(void) {
mxc_gpio_cfg_t gpio_out;
u8g2_Init(&u8g2);
/* Setup output pin. */
gpio_out.port = MXC_GPIO_PORT_OUT;
gpio_out.mask = MXC_GPIO_PIN_OUT;
gpio_out.pad = MXC_GPIO_PAD_NONE;
gpio_out.func = MXC_GPIO_FUNC_OUT;
MXC_GPIO_Config(&gpio_out);
u8g2_SetFont(&u8g2, u8g2_font_smart_patrol_nbp_tr);
u8g2_SetFontRefHeightText(&u8g2);
u8g2_SetFontPosTop(&u8g2);
u8g2_DrawStr(&u8g2, 0, 30, "u8g2 Soft I2C");
u8g2_SendBuffer(&u8g2);
while (1) {
MXC_GPIO_OutSet(gpio_out.port, gpio_out.mask);
MXC_Delay(5000000);
MXC_GPIO_OutClr(gpio_out.port, gpio_out.mask);
MXC_Delay(5000000);
}
return 0;
}最终的展示效果

3、驱动 MAX30102 传感器,采集数据并通过分析,生成血氧、心率,显示到 OLED 屏幕中。
首先看图,这个开发板的I2C硬件管脚为P0_16、P0_17两个管脚,OLED屏幕和MAX30102传感器都是走I2C接口。我使用一个I2C扩展板,将两个设备都和MAX78000FTHR的P0_16、P0_17连接起来,这里遇到个问题,单独接任意一个设备,通过I2C_SCAN例程能够很好滴读出设备的I2C地址,但是同时接两个设备后,就不能正常读取到地址了,不知道为啥,按理两个设备地址不同(OLED地址为:0X3C;心率计地址为:0X57)不应该有冲突,最后只能将OLED移到P2_6、P2_7管脚,使用模拟I2C来驱动。OLED的驱动依然使用u8g2来驱动。
在网上找了位老师的帖子,这个帖子有详细介绍心率计工作原理和使用方法,不过开发板使用的是STM32的开发板。MAX30102传感器内部集成了红外LED光源,用于照射到皮肤表面。红外光在血液中的反射特性可用于测量心率和血氧饱和度,通过测量红外光反射后的强度,通过AD获得波动数据,然后通过傅里叶变换,将心率、血氧变化提取出来。这里边傅里叶变换部分看得似懂非懂,感觉还是挺难的。但是傅里叶变换做为宇宙中的真神,决定直接改动代码,适配MAX78000FTHR即可。
/**
* ************************************************************************
*
* @file blood.c
* @author zxr
* @brief
*
* ************************************************************************
* @copyright Copyright (c) 2024 zxr
* ************************************************************************
*/
#include "blood.h"
int heart; //定义心率
float SpO2; //定义血氧饱和度
//调用外部变量
extern uint16_t fifo_red; //定义FIFO中的红光数据
extern uint16_t fifo_ir; //定义FIFO中的红外光数据
uint16_t g_fft_index = 0; //fft输入输出下标
struct compx s1[FFT_N + 16]; //FFT输入和输出:从S[1]开始存放,根据大小自己定义
struct compx s2[FFT_N + 16]; //FFT输入和输出:从S[1]开始存放,根据大小自己定义
#define CORRECTED_VALUE 47 //标定血液氧气含量
/**
* ************************************************************************
* @brief 更新血氧数据
* @note 从 MAX30102 的 FIFO 中读取红光和红外数据,并将它们存储到两个复数数组s1和s2中,
* 这些数据随后可以用于进行傅里叶变换等后续处理
*
* ************************************************************************
*/
void blood_data_update(void) {
//标志位被使能时 读取FIFO
g_fft_index = 0;
while (g_fft_index < FFT_N) {
while (max30102_read_reg(REG_INTR_STATUS_1) & 0x40) {
//读取FIFO
max30102_read_fifo(); //read from MAX30102 FIFO2
//将数据写入fft输入并清除输出
if (g_fft_index < FFT_N) {
//将数据写入fft输入并清除输出
s1[g_fft_index].real = fifo_red;
s1[g_fft_index].imag = 0;
s2[g_fft_index].real = fifo_ir;
s2[g_fft_index].imag = 0;
g_fft_index++;
}
}
}
}
/**
* ************************************************************************
* @brief 血液信息转换
*
*
* ************************************************************************
*/
void blood_data_translate(void) {
float n_denom;
uint16_t i;
//直流滤波
float dc_red = 0;
float dc_ir = 0;
float ac_red = 0;
float ac_ir = 0;
for (i = 0; i < FFT_N; i++) {
dc_red += s1[i].real;
dc_ir += s2[i].real;
}
dc_red = dc_red / FFT_N;
dc_ir = dc_ir / FFT_N;
for (i = 0; i < FFT_N; i++) {
s1[i].real = s1[i].real - dc_red;
s2[i].real = s2[i].real - dc_ir;
}
//移动平均滤波
for (i = 1; i < FFT_N - 1; i++) {
n_denom = (s1[i - 1].real + 2 * s1[i].real + s1[i + 1].real);
s1[i].real = n_denom / 4.00;
n_denom = (s2[i - 1].real + 2 * s2[i].real + s2[i + 1].real);
s2[i].real = n_denom / 4.00;
}
//八点平均滤波
for (i = 0; i < FFT_N - 8; i++) {
n_denom = (s1[i].real + s1[i + 1].real + s1[i + 2].real + s1[i + 3].real
+ s1[i + 4].real + s1[i + 5].real + s1[i + 6].real
+ s1[i + 7].real);
s1[i].real = n_denom / 8.00;
n_denom = (s2[i].real + s2[i + 1].real + s2[i + 2].real + s2[i + 3].real
+ s2[i + 4].real + s2[i + 5].real + s2[i + 6].real
+ s2[i + 7].real);
s2[i].real = n_denom / 8.00;
}
//开始变换显示
g_fft_index = 0;
//快速傅里叶变换
FFT(s1);
FFT(s2);
for (i = 0; i < FFT_N; i++) {
s1[i].real = sqrtf(s1[i].real * s1[i].real + s1[i].imag * s1[i].imag);
s1[i].real = sqrtf(s2[i].real * s2[i].real + s2[i].imag * s2[i].imag);
}
//计算交流分量
for (i = 1; i < FFT_N; i++) {
ac_red += s1[i].real;
ac_ir += s2[i].real;
}
for (i = 0; i < 50; i++) {
if (s1[i].real <= 10)
break;
}
//读取峰值点的横坐标 结果的物理意义为
int s1_max_index = find_max_num_index(s1, 60);
int s2_max_index = find_max_num_index(s2, 60);
//检查HbO2和Hb的变化频率是否一致
if (i >= 45) {
//心率计算
uint16_t Heart_Rate = 60.00 * SAMPLES_PER_SECOND * s1_max_index / FFT_N;
heart = Heart_Rate;
//血氧含量计算
float R = (ac_ir * dc_red) / (ac_red * dc_ir);
float sp02_num = -45.060 * R * R + 30.354 * R + 94.845;
SpO2 = sp02_num;
//状态正常
} else //数据发生异常
{
heart = 0;
SpO2 = 0;
}
//结束变换显示
}
/**
* ************************************************************************
* @brief 心率血氧循环函数
*
*
* ************************************************************************
*/
void blood_Loop(void) {
//血液信息获取
blood_data_update();
//血液信息转换
blood_data_translate();
SpO2 = (SpO2 > 99.99) ? 99.99 : SpO2;
//printf("心率%3d/min; 血氧%2d%%n", heart, (int)SpO2);
}最后在OLED上显示出来。
/***** Includes *****/
#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_regs.h"
#include "i2c.h"
#include "oled.h"
#include "MAX30102.h"
#include "algorithm.h"
#include "blood.h"
/***** Definitions *****/
#define I2C_MASTER MXC_I2C1 // SCL P0_16; SDA P0_17
#define I2C_SCL_PIN 16
#define I2C_SDA_PIN 17
#define I2C_FREQ 100000 // 100kHZ
typedef enum {
FAILED, PASSED
} test_t;
/***** Globals *****/
uint8_t counter = 0;
u8g2_t u8g2;
int main() {
int error;
u8g2_Init(&u8g2);
char buf[30];
//Setup the I2CM
error = MXC_I2C_Init(I2C_MASTER, 1, 0);
if (error != E_NO_ERROR) {
printf("-->Failed mastern");
return FAILED;
}
MXC_I2C_SetFrequency(I2C_MASTER, I2C_FREQ);
Max30102_reset();
MAX30102_Config();
MXC_Delay(MXC_DELAY_MSEC(1000));
while (1) {
blood_Loop();
printf("heart: %d; SpO2: %.2fn", heart, SpO2);
u8g2_ClearBuffer(&u8g2);
u8g2_SetFont(&u8g2, u8g2_font_smart_patrol_nbp_tr);
u8g2_DrawStr(&u8g2, 5, 15, "heart"); //显示心率
sprintf(buf, "%d", heart);
u8g2_SetFont(&u8g2, u8g2_font_inb24_mf);
u8g2_DrawStr(&u8g2, 1, 56, buf);
u8g2_SetFont(&u8g2, u8g2_font_smart_patrol_nbp_tr);
u8g2_DrawStr(&u8g2, 71, 15, "SpO2"); //显示血氧
sprintf(buf, "%.0f", SpO2);
u8g2_SetFont(&u8g2, u8g2_font_inb24_mf);
u8g2_DrawStr(&u8g2, 67, 56, buf);
u8g2_SetFont(&u8g2, u8g2_font_smart_patrol_nbp_tr);
sprintf(buf, ".%d", (int)(10*(SpO2-(int)SpO2)));
u8g2_DrawStr(&u8g2, 110, 40, buf);
u8g2_SendBuffer(&u8g2);
}
}



准确度呢,刚贴上手指时偏差比较大,过一会就好了,数据还算可以接受吧!
视频:

我要赚赏金
