lugl 发表于 2024-3-25 18:17:05

基于【STM32H735-DK】指尖血氧心率监护仪

指尖血氧动态监护仪

【实验器材】

STM32H735-DK开发板

MAX30100血氧传感器

【开发软件】

TouchGFX 4.23.2 Designer

STM32CubeMAX

STM32CubeIDE

【MAX30100简介】

MAX30100是一款集成有脉搏血氧仪和心率监测传感器的模块。该器件集成有两个LED、一个光电探测器, 经过优化的光学器件和低噪声模拟信号处理器,可检测脉搏血氧及心率信号。MAX30100采用1.8V和3.3V的电源电压。可通过软件来关断电源,待机模式下的电流消耗量可忽略不计,因而可以始终保持电源连接。并且 MAX30100采用iic通信方式

【开发步骤】

1、使用TouchGFX 4.23.2 Designer创建一个基于STM32H735-DK的空白工程。

2、添加一个标签textState用于显示检测状态

![图片1.png](data/attachment/forum/202403/25/181543ljpbkav6r3a0r4yr.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "图片1.png")

3、再创建两个texArea,用于显示心率与血氧浓度:

![图片3.png](data/attachment/forum/202403/25/181557kp3hsk6xisk436s0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "图片3.png")![图片2.png](data/attachment/forum/202403/25/181557x8s8gtgt9tuat26y.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "图片2.png")

4、放置两个图表控件,用于显示血氧与心跳的图形。

![图片4.png](data/attachment/forum/202403/25/181611a41s54cw9emq2s27.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "图片4.png")

5、生成工程后使用stm32cubIDE打开工程,添加max30100的驱动max30100.c以及血氧数据采集的blood.c与FFT分析algorithm.c三个文件。由于开发板给用户使用的i2c4触摸屏也在使用,由于他们的初始化的问题,我这次使用软件i2c来实现对max30100的驱动,所以添加了IICcom.c的驱动。添加好驱动后的工程如下图所示:

![图片5.png](data/attachment/forum/202403/25/181632crmdqqm3napb1paq.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "图片5.png")

6、程序组织实现的流程图如下

![图片6.png](data/attachment/forum/202403/25/181643mjercp97eeusghpg.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "图片6.png")

大的流程为在freertos的任务中,周期的检测传感器的数据,如果达到显示的次数,测置显示标志为1,如果达到fft的采样次数,测进行fft数据处理后,更新状态标志与血氧、心率的数据。

在TouchGFX的model中,使用tick来检测两个标志,分别向persenter、view传递需要的显示的数据,并同时更新显示。

【主要代码分析】

1、max30100初始化:

**void** **max30100_init** (**void** )

{

GPIO_InitTypeDef GPIO_InitStruct = {0};

GPIO_InitStruct.Pin = GPIO_PIN_7;

GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;

GPIO_InitStruct.Pull = GPIO_NOPULL;

GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;

**HAL_GPIO_Init** (GPIOB, &GPIO_InitStruct);

GPIO_InitStruct.Pull = GPIO_NOPULL;

GPIO_InitStruct.Pin = GPIO_PIN_3;

**HAL_GPIO_Init** (GPIOE, &GPIO_InitStruct);

PY_usDelayTest();

max30100_Bus_Write(0x06, 0x0b); //mode configuration : temp_en MODE=010 HR only enabled 011 SP02 enabled

//max30100_Bus_Write(0x06, 0x0a); //MODE=010 HR only enabled when used is mode ,the red led is not used.

max30100_Bus_Write(0x01, 0xF0); //open all of interrupt

max30100_Bus_Write(INTERRUPT_REG, 0x00); //all interrupt clear

max30100_Bus_Write(0x09, 0x33); //r_pa=3,ir_pa=3

**#if** (SAMPLES_PER_SECOND == 50)

max30100_Bus_Write(0x07, 0x43); //SPO2_SR=000 50 per second LED_PW=11 16BITS

**#elif** (SAMPLES_PER_SECOND == 100)

max30100_Bus_Write(0x07, 0x47); //SPO2_SR=001 100 per second LED_PW=11 16BITS

**#elif** (SAMPLES_PER_SECOND == 200)

max30100_Bus_Write(0x07, 0x4F);

**#elif** (SAMPLES_PER_SECOND == 400)

max30100_Bus_Write(0x07, 0x53);

**#endif**

max30100_Bus_Write(0x02, 0x00); //set FIFO write Pointer reg = 0x00 for clear it

max30100_Bus_Write(0x03, 0x00); //set Over Flow Counter reg = 0x00 for clear it

max30100_Bus_Write(0x04, 0x0F); //set FIFO Read Pointer reg = 0x0f for

//waitting write pointer eq read pointer to interrupts INTERRUPT_REG_A_FULL

}

我这次使用的IO为开发板上的CN4的D8、D9为模拟II2C的接口。在初始化时初始为普通IO输出。

![图片7.png](data/attachment/forum/202403/25/181658k5olitm25bnzm7t2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "图片7.png")

同时使用PY_usDelayTest();初始化一个微秒的延时函数。经过初化后,MAX30100的LED灯才能工作。

2、同时实现三个写入与读取寄存器数据函数如下:

uint8_t **max30100_Bus_Write** (uint8_t Register_Address, uint8_t Word_Data)

{

/* 采用串行EEPROM随即读取指令序列,连续读取若干字节 */

I2C_Start();

/* 第1步:发起I2C总线启动信号 */

/* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */

I2C_SendByte(max30100_WR_address | I2C_WR); /* 此处是写指令 */

/* 第3步:发送ACK */

**if** (I2C_WaitAck() != 0)

{

**goto** cmd_fail; /* EEPROM器件无应答 */

}

/* 第4步:发送字节地址 */

I2C_SendByte(Register_Address);

**if** (I2C_WaitAck() != 0)

{

**goto** cmd_fail; /* EEPROM器件无应答 */

}

/* 第5步:开始写入数据 */

I2C_SendByte(Word_Data);

/* 第6步:发送ACK */

**if** (I2C_WaitAck() != 0)

{

**goto** cmd_fail; /* EEPROM器件无应答 */

}

/* 发送I2C总线停止信号 */

I2C_Stop();

**return** 1; /* 执行成功 */

cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */

/* 发送I2C总线停止信号 */

I2C_Stop();

**return** 0;

}

uint8_t **max30100_Bus_Read** (uint8_t Register_Address)

{

uint8_t data;

/* 第1步:发起I2C总线启动信号 */

I2C_Start();

/* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */

I2C_SendByte(max30100_WR_address | I2C_WR); /* 此处是写指令 */

/* 第3步:发送ACK */

**if** (I2C_WaitAck() != 0)

{

**goto** cmd_fail; /* EEPROM器件无应答 */

}

/* 第4步:发送字节地址, */

I2C_SendByte((uint8_t)Register_Address);

**if** (I2C_WaitAck() != 0)

{

**goto** cmd_fail; /* EEPROM器件无应答 */

}

/* 第6步:重新启动I2C总线。下面开始读取数据 */

I2C_Start();

/* 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */

I2C_SendByte(max30100_WR_address | I2C_RD); /* 此处是读指令 */

/* 第8步:发送ACK */

**if** (I2C_WaitAck() != 0)

{

**goto** cmd_fail; /* EEPROM器件无应答 */

}

/* 第9步:读取数据 */

{

data = I2C_RadeByte(); /* 读1个字节 */

I2C_NoAck(); /* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */

}

/* 发送I2C总线停止信号 */

I2C_Stop();

**return** data; /* 执行成功 返回data值 */

cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */

/* 发送I2C总线停止信号 */

I2C_Stop();

**return** 0;

}

**void** **max30100_FIFO_Read** (uint8_t Register_Address,uint16_t Word_Data[],uint8_t count)

{

uint8_t i=0;

uint8_t no = count;

uint8_t data1, data2;

/* 第1步:发起I2C总线启动信号 */

I2C_Start();

/* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */

I2C_SendByte(max30100_WR_address | I2C_WR); /* 此处是写指令 */

/* 第3步:发送ACK */

**if** (I2C_WaitAck() != 0)

{

**goto** cmd_fail; /* EEPROM器件无应答 */

}

/* 第4步:发送字节地址, */

I2C_SendByte((uint8_t)Register_Address);

**if** (I2C_WaitAck() != 0)

{

**goto** cmd_fail; /* EEPROM器件无应答 */

}

/* 第6步:重新启动I2C总线。下面开始读取数据 */

I2C_Start();

/* 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */

I2C_SendByte(max30100_WR_address | I2C_RD); /* 此处是读指令 */

/* 第8步:发送ACK */

**if** (I2C_WaitAck() != 0)

{

**goto** cmd_fail; /* EEPROM器件无应答 */

}

/* 第9步:读取数据 */

**while** (no)

{

data1 = I2C_RadeByte();

I2C_Ack();

data2 = I2C_RadeByte();

I2C_Ack();

Word_Data = (((uint16_t)data1 << 8) | data2); //

data1 = I2C_RadeByte();

I2C_Ack();

data2 = I2C_RadeByte();

**if** (1==no)

I2C_NoAck(); /* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */

**else**

I2C_Ack();

Word_Data = (((uint16_t)data1 << 8) | data2);

no--;

i++;

}

/* 发送I2C总线停止信号 */

I2C_Stop();

cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */

/* 发送I2C总线停止信号 */

I2C_Stop();

}

**#endif**

3、血液检测信息更新

**void** **blood_data_update** (**void** )

{

uint16_t temp_num=0;

uint16_t fifo_word_buff;

temp_num = max30100_Bus_Read(INTERRUPT_REG);

//标志位被使能时 读取FIFO

**if** (INTERRUPT_REG_A_FULL&temp_num)

{

//HAL_GPIO_WritePin(GPIOG,GPIO_PIN_5,1);

//读取FIFO

max30100_FIFO_Read(0x05,fifo_word_buff,1); //read the hr and spo2 data form fifo in reg=0x05

//将数据写入fft输入并清除输出

**for** (**int** i = 0;i < 1;i++)

{

**if** (g_fft_index < FFT_N)

{

s1.real = fifo_word_buff;

s1.imag= 0;

s2.real = fifo_word_buff;

s2.imag= 0;

g_fft_index++;

}

}

//信息更新标志位

g_blooddata.update++;

}

**else**

{

//HAL_GPIO_WritePin(GPIOG,GPIO_PIN_5,0);

}

}

在这个程序中,读取标志位,如果使能则读取转后的数据,并更新到缓冲数组s1、s2中,如果未达FFT的采集数量,测将数组列新到buffk。同时更新检测信息的标志位。

4、血液信息转换

**void** **blood_data_translate** (**void** )

{

//缓冲区写入结束

**if** (g_fft_index>=FFT_N)

{

//开始变换显示

//Gui_DrawFont_GBK16(102,2,BLACK,GREEN,"FFT");

g_fft_index = 0;

//数据更新标志位

g_blooddata.display = 1;

//快速傅里叶变换

FFT(s1);

FFT(s2);

//解平方

**for** (**int** i = 0;i < FFT_N;i++)

{

s1.real=**sqrtf** (s1.real*s1.real+s1.imag*s1.imag);

s2.real=**sqrtf** (s2.real*s2.real+s2.imag*s2.imag);

}

//读取峰值点 结果的物理意义为

uint16_t s1_max_index = find_max_num_index(s1, 60);

uint16_t s2_max_index = find_max_num_index(s2, 60);

//检查HbO2和Hb的变化频率是否一致

**if** (s1_max_index == s2_max_index)

{

//心率计算

uint16_t Heart_Rate = 60 * SAMPLES_PER_SECOND *

s2_max_index / FFT_N;

g_blooddata.heart = Heart_Rate - 10;

//血氧含量计算

**float** sp02_num = (s2.real * s1.real)

/(s1.real * s2.real);

sp02_num = (1 - sp02_num) * SAMPLES_PER_SECOND + CORRECTED_VALUE;

g_blooddata.SpO2 = sp02_num;

//状态正常

g_blooddata.state = *BLD_NORMAL* ;

}

**else** //数据发生异常

{

g_blooddata.heart = 0;

g_blooddata.SpO2 = 0;

g_blooddata.state = *BLD_ERROR* ;

}

//结束变换显示

//Gui_DrawFont_GBK16(102,2,GREEN,BLACK,"FFT");

}

}

如果是达到了FFT的数据采集数量,则进行fft转换,得到血氧、心率的数值,同时更新标志位,通知显示任务进行显示。

5、在TouchGFX的model.c的tick周期函数中,实现图表更新

**if** (g_blooddata.update >= 8)

{

//清除图标更新标志位

g_blooddata.update = 0;

//血液波形数据更新

blood_wave_update();

//绘制波形

//tft_draw_wave();

modelListener->draw_wave( g_BloodWave.HpO2*5,g_BloodWave.Hp*5);

}

//转换后的数据更新

**if** (g_blooddata.display >= 1)

{

//清除更新标志位

g_blooddata.display = 0;

//显示血液状态信息

modelListener->max30100_draw_State(g_blooddata.state,g_blooddata.SpO2,g_blooddata.heart);

//心率血氧数据刷新

//tft_draw_hrsp();

}

7、view的显示函数:

**void** **Screen1View::max30100_draw_State** (**bool** state, **float** SpO2, **int** heart)

{

**if** (!state)

{

Unicode::**snprintf** (textStateBuffer, 20, "Nomarl");

textState.**invalidate** (); //刷新

}

**else**

{

Unicode::**snprintf** (textStateBuffer, 20, "ERROR");

textState.**invalidate** (); //刷新

}

Unicode::**snprintf** (textHRBuffer, *TEXTHR_SIZE* , "%d", heart);

textHR.**invalidate** ();

Unicode::*snprintfFloat* (textSpo2Buffer, *TEXTSPO2_SIZE* , "%.1f",SpO2);

textSpo2.**invalidate** ();

//把数据添加进曲线

}

**void** **Screen1View::draw_wave** (**float** SpO2,**int** heart)

{

dynamicGraHR.addDataPoint(heart);

dynamicGraphSpO2.addDataPoint(SpO2);

}

函数max30100_draw_State,实现了对状态、血氧、心率的更新。函数draw_wave,就是简单的新点新增到图表中,由TouchGFX实现图形的自主显示。

【总结】

**MAX30100的工程实现是学习githug上的基于stm32f103的代码工程进行移植实现的。相比于使用tft的画线提示,TouchGFX给了强大的图形实现工程,只需要通过简单的设计,就可以实现复杂的功能。**
页: [1]
查看完整版本: 基于【STM32H735-DK】指尖血氧心率监护仪