这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 嵌入式开发 » MCU » 【MAX32625PICO开发板】基于freertos的心率/血氧分析

共1条 1/1 1 跳转至

【MAX32625PICO开发板】基于freertos的心率/血氧分析

工程师
2025-06-02 16:09:39     打赏

【前言】

在前面我发表了【MAX32625PICO开发板】驱动MAX30102-电子产品世界论坛 关于max30102的驱动。这一篇将分享如何正确的分析数据。

同时分享了如何移植freeRTOS到MAX32625的文章:【MAX32625PICO开发板】移植FreeRTOS   该篇详细的分享发移植的过程。

【缘由】

在前一篇帖子里,我是使用阻塞式的方法,先采集500个数据,然后再分析,再采集,这样的话,采集不连续,再有就是如果还需要处理其它任务,比如实时刷新日期时间,那莫就没有实时性,因此引入freertos的多任务系统来处理数据,是非常有必要的。我结合上面两篇帖子,创建了多任务,来实现高效的数据分析功能。

【工程设计】

我创建三个任务,一个任务负责显示,每一秒刷新一次数据,显示RTC时间、心率、血氧值。另一个任务负责采集MAX30102的数据。第三个任务负责将采集的任务数据进行滤波、FFT后,得出心率与血氧指数。

1、显示任务代码如下:

//OLED显示任务函数   
void show_task(void *pvParameters)  
{  
    uint32_t timesecond;
    uint16_t year;
    uint8_t mon, day, hour, min, sec;
    char buff[40];
    OLED_Init();
    OLED_Clear(0);
    RTC_Setup();
    while(1)  
    {  
      timesecond = RTC_GetCount();
      OLED_Clear(0);
      RTC_GetDate(timesecond, &year, &mon, &day, &hour, &min, &sec);
      sprintf(buff, "Time:%02d:%02d:%02d\n", hour, min, sec);
      GUI_ShowString(0,0,(uint8_t *)buff,16,0); 
      sprintf(buff, "Heart_Rate:%d", g_blooddata.heart);
      GUI_ShowString(0,16,(uint8_t *)buff,16,0); 
      sprintf(buff, "sp02_num:%.2f", g_blooddata.SpO2);
      GUI_ShowString(0,32,(uint8_t *)buff,16,0);
      OLED_Display();
      vTaskDelay(1000);  
    }  
}

由于获取RTC时间的非常短,因此我把他的整合在这个任务中。

2、MAX30102数据采集任务,代码如下:

// 数据处理函数声明
void process_buffer(uint32_t *red_data, uint32_t *ir_data, uint16_t length);

// =============== 数据采集任务 ===============
void vTaskDataCollection(void *pvParameters)
{
    uint32_t fifo_red, fifo_ir;
    uint8_t write_flag = 0;
    uint8_t read_flag = 0;
    uint8_t full_flag = 0;
    uint8_t reg_int_status_1;
    int num_samples = 0;
    max30102_reset();
    max30102_init();
     for (;;)
    {
			  
//        write_flag = max30102_Bus_Read(REG_FIFO_WR_PTR) & 0x0F;
//        read_flag = max30102_Bus_Read(REG_FIFO_RD_PTR)& 0x0F;
	num_samples = max30102_Bus_Read(REG_OVF_COUNTER)& 0x0F;
        if (num_samples == 0)
        {
            vTaskDelay(pdMS_TO_TICKS(160)); // 控制采样率
        }
        else
        {
					
//					  reg_int_status_1 = max30102_Bus_Read(REG_INTR_STATUS_1);
//					//打印中断寄存器的值:
//						printf("REG_INTR_STATUS_1:%X\r\n",reg_int_status_1);
//					
//					//打印三个寄存器的值
//        printf("write_flag:%d\r\n",write_flag);
//        printf("read_flag:%d\r\n",read_flag);
//         printf("full_flag:%d\r\n",full_flag);
					
        printf("num_samples:%d\r\n",num_samples);	
        for(int i = 0; i < num_samples; i++)
        {
	   maxim_max30102_read_fifo(&fifo_red, &fifo_ir);
        // 写入当前缓冲区
           buffer_red[buffer_index][sample_index] = fifo_red;
	   buffer_ir[buffer_index][sample_index] = fifo_ir;
	   sample_index++;

    	 // 缓冲满时切换并通知
    	   if (sample_index >= FFT_N)
    	   {
	 // 发送任务通知(直接使用预先保存的句柄)	
	   sample_index = 0;
	   uint8_t full_buffer_index = buffer_index;
	   xTaskNotifyGive(xDataProcessingHandle);
	   printf("Buffer %d ready\n", full_buffer_index);
	   buffer_index = (buffer_index + 1) % BUFFER_COUNT;	
	   }
       }

   }
 }
}

在这个任务函数,首先对max30102进行复位、初始化。并声明了两个缓存数组。在大循环中,先读取REG_OVF_COUNTER寄存器的值,即缓冲区已读取的数据,如果没有数据则这个任务暂停执行160ms。如果数据缓存区有数据,则读取完毕,并添加到数据缓存数组中,如果缓存数组达到FFT_N(512个数据)则更换缓存数组,同时通xTaskNotifyGive(xDataProcessingHandle),将数据索引发送给数据处理任务。

3、数据处理任务:

// =============== 数据处理任务 ===============
void vTaskDataProcessing(void *pvParameters)
{
    for (;;)
    {
        // 等待数据采集任务的通知
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
	printf("buffer_index:%d\r\n",buffer_index);
        process_buffer(buffer_red[buffer_index], buffer_ir[buffer_index], FFT_N);
    }
}

在这个任务中,如果没有接收到数据采集任务的通知,将阻塞任务,再收到数据后执行数据处理任务。

4、数据分析函数:

// 数据处理函数 - 适配原有血液信息转换逻辑
void process_buffer(uint32_t *red_data, uint32_t *ir_data, uint16_t length)
{
    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 < length; i++) {
        s1[i].real = (float)red_data[i];
        s1[i].imag = 0;
        s2[i].real = (float)ir_data[i];
        s2[i].imag = 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);
    // 解平方
    // UsartPrintf(USART_DEBUG,"开始FFT算法****************************************************************************************************\r\n");
    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;
    }

    // 读取峰值点的横坐标  结果的物理意义为
    int s1_max_index = find_max_num_index(s1, 30);
    int s2_max_index = find_max_num_index(s2, 30);

    float Heart_Rate = 60.00 * ((50.0 * s1_max_index) / 512.00);

    g_blooddata.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;
    g_blooddata.SpO2 = sp02_num;

}

在这个处理函数中,什么FFT分析,计算出心率与血氧值。

5、任务创建函数:

// =============== 初始化函数(需在main中调用) ===============
void Blood_Init(void)
{
    // 创建互斥锁
    xBufferMutex = xSemaphoreCreateMutex();

    // 创建数据处理任务(保存句柄)
    xTaskCreate(
        vTaskDataProcessing,
        "DataProcessing",
        2048,
        NULL,
        tskIDLE_PRIORITY + 2, // 优先级高于采集任务
        &xDataProcessingHandle);

    // 创建数据采集任务(传递句柄)
    xTaskCreate(
        vTaskDataCollection,
        "DataCollection",
        2048,
        xDataProcessingHandle, // 直接传递句柄
        tskIDLE_PRIORITY + 1,
        NULL);
}

这个任务函数开放出来后,在main的任务初始化中调用。

【实现效果】

8e64030d0496fed5aa7d5812cc6e456.jpg

【总结】

在前的max30102的简单数据采集中,心率与血氧指数是非常不稳定的,经过多任务实时的处理后,得到的数据经过10次更新后,数据就非常稳定且与智能手表进对比,也是非常准确的。

基中,这次数据采集并没有采集中断来读取,经过调试,如果中断设置为满时产生中断,那么就会有数据溢出,而我采用如果没有缓存时采集任务暂停160ms左右,再次采集时,刚好在10组缓存左右,刚好不会产生溢出。

【注意事项】

1、在获取缓存数据时,由于他的寄存器只有4位有效数据,因此读取后,需要把高4位进行置零即&0x0F,要不会出现数据不对。

2、在进行数据分析时需要与max30102的配置采集频率与是否进行平均对应。




关键词: MAX32625PICO     MAX30102     fre    

共1条 1/1 1 跳转至

回复

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