【前言】
在前面我发表了【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的配置采集频率与是否进行平均对应。
我要赚赏金
