TA的每日心情  | 开心 2025-10-22 07:09 | 
|---|
 
  签到天数: 590 天 连续签到: 1 天 [LV.9]以坛为家II 
状元 
 
 
	- 积分
 - 8788
 
 
 
 
 | 
 
 
 本帖最后由 eefocus_3914144 于 2024-3-25 18:08 编辑  
 
【实验器材】 
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用于显示检测状态 
 
3、再创建两个texArea,用于显示心率与血氧浓度: 
 
 
4、放置两个图表控件,用于显示血氧与心跳的图形。 
 
5、生成工程后使用stm32cubIDE打开工程,添加max30100的驱动max30100.c以及血氧数据采集的blood.c与FFT分析algorithm.c三个文件。由于开发板给用户使用的i2c4触摸屏也在使用,由于他们的初始化的问题,我这次使用软件i2c来实现对max30100的驱动,所以添加了IICcom.c的驱动。添加好驱动后的工程如下图所示: 
 
6、程序组织实现的流程图如下 
 
大的流程为在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[3] MODE[2:0]=010 HR only enabled 011 SP02 enabled 
//max30100_Bus_Write(0x06, 0x0a); //MODE[2:0]=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[4:2]=000 50 per second LED_PW[1:0]=11 16BITS 
#elif (SAMPLES_PER_SECOND == 100) 
max30100_Bus_Write(0x07, 0x47); //SPO2_SR[4:2]=001 100 per second LED_PW[1:0]=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输出。 
 
同时使用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[][2],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[0] = (((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[1] = (((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[1][2]; 
  
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[g_fft_index].real = fifo_word_buff[0]; 
s1[g_fft_index].imag= 0; 
s2[g_fft_index].real = fifo_word_buff[1]; 
s2[g_fft_index].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[s1_max_index].real * s1[0].real) 
/(s1[s1_max_index].real * s2[0].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给了强大的图形实现工程,只需要通过简单的设计,就可以实现复杂的功能。 
【视频分享】 
 
 
 |   
 
  
  
  
 
 
 |