【前言】
在前面我发表了【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的任务初始化中调用。
【实现效果】
【总结】
在前的max30102的简单数据采集中,心率与血氧指数是非常不稳定的,经过多任务实时的处理后,得到的数据经过10次更新后,数据就非常稳定且与智能手表进对比,也是非常准确的。
基中,这次数据采集并没有采集中断来读取,经过调试,如果中断设置为满时产生中断,那么就会有数据溢出,而我采用如果没有缓存时采集任务暂停160ms左右,再次采集时,刚好在10组缓存左右,刚好不会产生溢出。
【注意事项】
1、在获取缓存数据时,由于他的寄存器只有4位有效数据,因此读取后,需要把高4位进行置零即&0x0F,要不会出现数据不对。
2、在进行数据分析时需要与max30102的配置采集频率与是否进行平均对应。