自之前出了一系列FreeRTOS的学习笔记系列还没有应用于实战。本期介绍使用FreeRTOS实现运动状态与心率状态的检测。 看展示视频可以直接拉到最后~
源码可以加QQ群获取~~ 656210280
本来这期内容上周就会发出来了,但是因为我的代码开始是在F4上写的,然后移植到F1上面的时候却因为芯片的问题导致FreeRTOS一开启调度就会死机,所以导致耽搁了几天。
本期所用的材料为:
-
STM32F103C8T6
-
MAX30100心率血氧检测模块
-
MPU6050姿态检测模块
-
0.96寸OLED显示模块
旨在制作一款:检测心率,检测运动姿态,当运动加速度太大的时候(加速度超过跌倒阈值的时候发送警报)
- ** 程序大纲 **
- ** CubeMX配置 **
- ** 时钟配置 **
由于是借来的核心板,因此高速时钟选择石英晶体振荡。
- ** 时钟源 **
时钟源选择一个定时器,这里FreeRTOS不推荐使用系统滴答定时器,因为虽然系统滴答定时器的精度比较高,但是不方便RTOS进行任务调度,并且
软件定时器可以根据实际需求进行裁剪和配置所以这里推荐使用一个定时器来替代系统滴答定时器。
- 外设配置
外设选择一个串口和两个硬件I2C,硬件I2C可以用于OLED,MAX30100以及MPU6050的通讯。
这里我将OLED挂在到I2C1上,MAX30100和MPU6050挂在到I2C2上,这里大家可以挂载到同一根I2C总线上,I2C总线上可以挂载多个设备。我使用两个是开始测试的时候OLED是单独测试的。
- FreeRTOS设置
修改一下堆栈大小,防止等会分配任务的时候堆栈溢出。
创建一个任务作为MPU6050的数据采集,将这个任务的优先级设置为高优先级,因为检测跌倒的优先级应该是最高的。
此外,还有MAX30100的数据处理任务,MPU状态异常处理任务,OLED刷新任务。
这里由于我决定使用全局缓存区来存放和处理数据(而不是在任务内利用临时变量来处理任务)
设置一个定时器用来定时读取MAX30100代码,设置三个信号量分别用来用来通知MAX30100数据处理,MPU6050异常处理以及OLED刷新显示。
用一个事件组来标记各个状态,例如各模块的初始化状态,MPU6050异常状态。
- ** KEIL代码 **
在Keil中FreeRTOS中,帮我们实现了初始化,但是我们需要自己开启定时器,并且设置定时时间为15ms。
float x[3];//存放int stepSum;/* USER CODE END Header_MPURuning */void MPURuning(void *argument){ /* USER CODE BEGIN MPURuning */ /* Infinite loop */ for(;;) { MPU6050_ReadAccel(&mpu6050, x, x+1, x+2); //这里的x[3]是三个方向的加速度,按理说是要计算合加速度 //printf("B:%.2frn",x[2]); if(x[1]>1.5&&x[1]!=1.9) { stepNow = stepSum; MpuErrorFlag = 1; xSemaphoreGive(MPU6050EventHandle); } if(x[1]>0.7) { stepSum++; } osDelay(10); } /* USER CODE END MPURuning */}
这里MPU6050不断获取加速度,通过调整阈值来判断是否跌倒。
void MAXData(void *argument){ /* USER CODE BEGIN MAXData */ static int i = 0; uint32_t DCRED; update(); DCRED = -removeRedDcComponent(rawIRValue)*2; Data[i++] = DCRED; if(i==300) { xSemaphoreGive(MAXStartHandle); i = 0; } /* USER CODE END MAXData */}
MAX30100通过一个软件定时器,15ms采样率定时读取数据,将数据写入一个缓存区,当采集到300时,发送一个二进制信号量。
void MAXUsing(void *argument){ /* USER CODE BEGIN MAXUsing */ /* Infinite loop */ for(;;) { if (xSemaphoreTake(MAXStartHandle,portMAX_DELAY)) { xTimerStop(MAXDataHandleHandle,0); smoothFilter(Data,smooth,BuffLenth,3); int index; float cot; float sum; float Heart;// for(int i = 0;i<300;i++)// {// printf("A:%drn",smooth[i]);// } for(int i = 0;i<300;i++) { if(smooth[i]>70&&smooth[i]<300) { index = i; while(smooth[i]>0) { i++; if(i==BuffLenth-1) { break; } } while(smooth[i]<70) { i++; if(i==BuffLenth-1) { break; } }
index = i-index; if(index>23)//23对应心率173认为误差 { sum+= index; cot++; //printf("A:%drn",index); } } } Heart = (float)(sum/cot)*0.015; Heart = 60/Heart; HeartFre= Heart; sum = 0; cot = 0; if((HeartFre>160&&HeartFre<250)||HeartFre<30) { xEventGroupSetBits(TaskStauteHandle,1<<0);//第0位置1 } xSemaphoreGive(OLEDShowSemHandle); xTimerStart(MAXDataHandleHandle,0); } } /* USER CODE END MAXUsing */}
MAX30100数据处理任务等待信号量的释放,当等待到信号量释放即关闭定时器,并且对数据进行处理。
在数据中提取中心率信息(还可以提取血氧信息,但是要校准,我没做校准,就不展示了)
假如统计到了异常心率,将事件组的第0位置1.
之后通知OLED函数刷新。
void MPUERROR(void *argument){ /* USER CODE BEGIN MPUERROR */ /* Infinite loop */ for(;;) { int flag = 1; if (xSemaphoreTake(MPU6050EventHandle, portMAX_DELAY) == pdPASS) { for(int i = 0;i<200;i++) { //printf("A:%drn",stepSum); if((stepNow-stepSum)>2) { flag = 0; } osDelay(10); } if(flag) { xEventGroupSetBits(TaskStauteHandle,1<<1);//第1位置1 xSemaphoreGive(OLEDShowSemHandle); } } /* USER CODE END MPUERROR */ }}
在MPU6050数据异常处理任务中,如果两秒内(这里实际情况应该设置更长时间)没有步数增加,就标志异常位置,之后通知OLED刷新。
void OLEDShow(void *argument){ /* USER CODE BEGIN OLEDShow */ char s[50]; /* Infinite loop */ for(;;) { if (xSemaphoreTake(OLEDShowSemHandle,portMAX_DELAY)) { sprintf(s,"Heart:%.2f",HeartFre); OLED_Clear(); OLED_ShowString(0,1,(unsigned char *)s,2); EventBits_t Bits = xEventGroupGetBits(TaskStauteHandle); if(Bits&1<<0) { //第0位异常,心率异常 sprintf(s,"Heart is Error"); OLED_ShowString(0,2,(unsigned char *)s,2); } if(Bits&1<<1) { //第1位异常,姿态异常 sprintf(s,"MPU Error"); OLED_ShowString(0,3,(unsigned char *)s,2); } } } /* USER CODE END OLEDShow */}
OLED的刷新任务中实时显示心率,并且根据错误选择显示错误信息。
效果展示
由于做的比较仓促,因此使用杜邦线+核心板的形式来展示