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

共2条 1/1 1 跳转至

Let'sdo2025第二期活动[智能手环DIY活动]基础任务

助工
2025-09-11 15:08:00     打赏

硬件介绍: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 灯

FvPqkNj3st3c3OPkKKwZvOkmSlSx

这个开发板上集成了一课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灯的亮灭。

image.png


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;
}

最终的展示效果

image.png


3、驱动 MAX30102 传感器,采集数据并通过分析,生成血氧、心率,显示到 OLED 屏幕中。

FvPqkNj3st3c3OPkKKwZvOkmSlSx

首先看图,这个开发板的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);
	}
}

image.png

2bb034f736ecf1050022c3f3f4df2890.jpg863cb8829ff8c49d8b4d212bde411e50.jpgc8cacb13b98a43570cb4e2486740b8a0.jpg

I2C_SCAN.zip

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


院士
2025-09-14 22:17:05     打赏
2楼

嗯~~

心脏还在跳呢!



共2条 1/1 1 跳转至

回复

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