硬件介绍: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); } }
准确度呢,刚贴上手指时偏差比较大,过一会就好了,数据还算可以接受吧!