1. 项目介绍
本文介绍的项目是基于STM32设计的健康检测设备,支持体温测量,心率检测,支持运动计步(采用MPU6050陀螺仪实现),支持WIFI传输数据到手机APP打印显示。
硬件环境介绍:
MCU采用STM32F103C8T6
心率传感器采用PulseSensor
体温检测传感器采用红外测温传感器
运动计步功能采用MPU6050陀螺仪实现
OLED显示屏采用0.96寸中景园电子的OLED显示屏-SPI接口
编程软件采用keil5
心率传感器:
MPU6050传感器:
体温测温模块
OLED显示屏:
硬件效果图:
工程源码截图:
项目源码下载地址: https://download.csdn.net/download/xiaolong1126626497/63992839
视频演示地址: https://live.csdn.net/v/182607
2. 项目源码介绍
2.1 计步功能实现代码
计步功能是通过MPU6050陀螺仪测量计算得到,下面贴出计步算法的核心实现代码.
/*******************************************************************************
文件名: stepAlgorithm.c
描述 : 计步算法
*******************************************************************************/
#include "stepAlgorithm.h"
#include "rtc.h"
#include "math.h"
#include "mpu6050.h"
#define TRUE 1
#define FALSE 0
#define VALUE_NUM 4
sportsInfo_t userSportsInfo;
//存放三轴数据
float oriValues[3] = {0};
//用于存放计算阈值的波峰波谷差值
float tempValue[VALUE_NUM] ={0};
int tempCount = 0;
//是否上升的标志位
u8 isDirectionUp = FALSE;
//持续上升次数
int continueUpCount = 0;
//上一点的持续上升的次数,为了记录波峰的上升次数
int continueUpFormerCount = 0;
//上一点的状态,上升还是下降
u8 lastStatus = FALSE;
//波峰值
float peakOfWave = 0;
//波谷值
float valleyOfWave = 0;
//此次波峰的时间
long timeOfThisPeak = 0;
//上次波峰的时间
long timeOfLastPeak = 0;
//当前的时间
long timeOfNow = 0;
//当前传感器的值
float gravityNew = 0;
//上次传感器的值
float gravityOld = 0;
//动态阈值需要动态的数据,这个值用于这些动态数据的阈值
float initialValue = (float) 1.3;
//初始阈值
float ThreadValue = (float) 2.0;
//三轴轴值
accValue_t accValue;
//行走信息:卡路里、里程、步数
static sportsInfo_t sportsInfo;
//计步缓存
static u8 stepTempCount =0;
/*******************************************************************************
* 函数名:onSensorChanged
* 功能描述: G-Sensor工作后会一直调用这个函数对三轴数据进行平方和开根号的处理
* 调用DetectorNewStep检测步子
*
* 参数说明:
* 输入:
* pAccValue:G-sensor的原始数据
* timeStamp_p:动态时间戳
* 返回值说明:
* 修改记录:
*******************************************************************************/
sportsInfo_t *onSensorChanged(accValue_t *pAccValue,timeStamp_t *timeStamp_p,personInfo_t * personInfo)
{
accValue_t *p = pAccValue;
personInfo_t *userInfo = personInfo;
timeStamp_t *time_p = timeStamp_p;
oriValues[0] = p->accX;
oriValues[1] = p->accY;
oriValues[2] = p->accZ;
//对三轴数据进行平方和开根号的处理
gravityNew = (float) sqrt(oriValues[0] * oriValues[0]+ oriValues[1] * oriValues[1] + oriValues[2] * oriValues[2]);
//检测步子
return DetectorNewStep(gravityNew,time_p,userInfo);
}
/*******************************************************************************
* 函数名:DetectorNewStep
* 功能描述:
* 步伐更新:如果检测到了波峰,并且符合时间差以及阈值的条件,则判定为1步
* 阀值更新:符合时间差条件,波峰波谷差值大于initialValue,则将该差值纳入阈值的计算中
* 参数说明:
输入:
values:经过处理的G-sensor数据
timeStamp_p:时间戳
* 返回值说明:
* 修改记录:sportsInfo_t *onSensorChanged(accValue_t *pAccValue,timeStamp_t *timeStamp_p,personInfo_t * personInfo)
*******************************************************************************/
sportsInfo_t *DetectorNewStep(float values,timeStamp_t *timeStamp_p,personInfo_t * personInfo)
{
static u32 time_old;
personInfo_t *userInfo = personInfo;
static u32 step_per_2_second; //每两秒所走的步数
float step_lenth,walk_speed,walk_distance,Calories;//步长
u32 time_now;
timeStamp_t *time_p = timeStamp_p;
if (gravityOld == 0)
{
gravityOld = values;
}
else
{
if (DetectorPeak(values, gravityOld))//检测到波峰
{
timeOfLastPeak = timeOfThisPeak;//更新上次波峰的时间
//将时间戳转换为以毫秒ms为单位
time_now = timeOfNow = ((time_p->hour*60+time_p->minute)*60+time_p->second)*1000+time_p->twentyMsCount*20; //获取时间 ,并转化为毫秒
//如果检测到了波峰,并且符合时间差以及阈值的条件,则判定为1步
if ( (timeOfNow - timeOfLastPeak >= 250 )//Jahol Fan 修改为300,防止轻微动都也会检测步子
//&& (timeOfNow - timeOfLastPeak <= 2000)
&&(peakOfWave - valleyOfWave >= ThreadValue)
)
{
timeOfThisPeak = timeOfNow; //更新此次波峰时间
stepTempCount++;//Jahol:加1为两步
step_per_2_second ++;
//Jahol:这样计算卡路里,不能滤除人为的误操作,导致的结果是:里程和卡路里偏大
if((time_now - time_old) >= 2000 ) //如果时间过了2秒
{
if( 1 == step_per_2_second )
{
step_lenth = userInfo->height/5;
}
else if( 2 == step_per_2_second )
{
step_lenth = userInfo->height/4;
}
else if( 3 == step_per_2_second )
{
step_lenth = userInfo->height/3;
}
else if( 4 == step_per_2_second )
{
step_lenth = userInfo->height/2;
}
else if(5 == step_per_2_second) //Jahol:为了使计步准确,设置上限值为5步,牺牲卡路里准确性
{
step_lenth = userInfo->height/1.2f;
}
else if( 7 == step_per_2_second )
{
step_lenth = userInfo->height;
}
else if(step_per_2_second >= 8) // step_diff>8
{
step_lenth = userInfo->height*1.2f;
}
else
{
step_lenth = 0;
}
walk_speed = step_per_2_second*step_lenth/2; //速度 ,单位:米/秒
walk_distance = step_per_2_second*step_lenth; //行走距离,单位:米
Calories = 4.5f*walk_speed*(userInfo->weight/2)/1800; //Jahol:weight是以kg为单位
sportsInfo.calories += Calories;
sportsInfo.distance += walk_distance;
time_old = time_now; //更新时间
step_per_2_second = 0;
}
else
{
//do nothing
}
/*
* 处理无效运动:
* 1.连续记录5才开始计步
* 2.例如记录的步用户停住超过3秒,则前面的记录失效,下次从头开始
* 3.连续4记录了步用户还在运动,之前的数据才有效
* */
if ((stepTempCount< 5 )&&(timeOfNow - timeOfLastPeak >= 3000))
{
stepTempCount = 0;
}
else if((stepTempCount>= 5)&&(timeOfNow - timeOfLastPeak <= 3000))
{
sportsInfo.stepCount += stepTempCount;
stepTempCount = 0;
}
else
{
//do nothing
}
}
//Jahol:更新阀值,问题:阀值不会一直变大,不能变小?
if (timeOfNow - timeOfLastPeak >= 250
&& (peakOfWave - valleyOfWave >= initialValue))
{
timeOfThisPeak = timeOfNow;
ThreadValue = Peak_Valley_Thread(peakOfWave - valleyOfWave);//更新阀值
}
}
}
gravityOld = values;
return &sportsInfo;
}
/*******************************************************************************
* 函数名:DetectorPeak
* 功能描述:
*检测波峰 。以下四个条件判断为波峰:
*(1)目前点为下降的趋势:isDirectionUp为FALSE
*(2)之前的点为上升的趋势:lastStatus为TRUE
*(3)到波峰为止,持续上升大于等于2次
*(4)波峰值大于20 //Jahol:把这个值修改为15
*记录波谷值 :
*(1)观察波形图,可以发现在出现步子的地方,波谷的下一个就是波峰,有比较明显的特征以及差值
*(2)所以要记录每次的波谷值,为了和下次的波峰做对比
* 参数说明:
* 输入:
* newValue:最新的经过处理的G-sensor数据
* oldValue:前一个处理的G-sensor数据
* 返回值说明:
* 修改记录:
*******************************************************************************/
u8 DetectorPeak(float newValue, float oldValue)
{
lastStatus = isDirectionUp;
if (newValue >= oldValue) //采样数据呈上升趋势
{
isDirectionUp = TRUE;
continueUpCount++;
}
else //数据呈下降趋势
{
continueUpFormerCount = continueUpCount;
continueUpCount = 0;
isDirectionUp = FALSE;
}
if ((!isDirectionUp) && lastStatus
&& (continueUpFormerCount >= 2 || oldValue >= 20))
{
peakOfWave = oldValue;
return TRUE;
}
else if ((!lastStatus) && isDirectionUp)
{
valleyOfWave = oldValue;
return FALSE;
}
else
{
return FALSE;
}
}
/*******************************************************************************
* 函数名:Peak_Valley_Thread
* 功能描述:
* 阈值的计算
* 1.通过波峰波谷的差值计算阈值
* 2.记录4个值,存入tempValue[]数组中
* 3.在将数组传入函数averageValue中计算阈值
*
* 参数说明:
* 返回值说明:
* 修改记录:
*******************************************************************************/
float Peak_Valley_Thread(float value)
{
float tempThread = ThreadValue;
u8 i = 0;
if (tempCount < VALUE_NUM)
{
tempValue[tempCount] = value;
tempCount++;
}
else
{
tempThread = averageValue(tempValue, VALUE_NUM);//计算阀值
for ( i = 1;i < VALUE_NUM;i++)//线性移位更新
{
tempValue[i - 1] = tempValue[i];
}
tempValue[VALUE_NUM - 1] = value;
}
return tempThread;
}
/*******************************************************************************
* 函数名:averageValue
* 功能描述:
* 梯度化阈值
* 1.计算数组的均值
* 2.通过均值将阈值梯度化在一个范围里
*
* 参数说明:
* 返回值说明:
* 修改记录:
*******************************************************************************/
float averageValue(float value[], int n)
{
float ave = 0;
u8 i =0;
for ( i = 0; i < n; i++)
{
ave += value[i];//求和
}
ave = ave / VALUE_NUM;//求平均值
if (ave >= 8)
ave = (float) 4.3; //????
else if (ave >= 7 && ave < 8)
ave = (float) 3.3;
else if (ave >= 4 && ave < 7)
ave = (float) 2.3;
else if (ave >= 3 && ave < 4)
ave = (float) 2.0;
else
{
ave = (float) 1.3;
}
return ave;
}
personInfo_t user_info;
u8 WatchInfo_init(void)
{
WatchInfo_setUserInfo(170,134); //设置身高、体重用于计算卡路里消耗
return 0; //初始化成功返回0
}
/**********************************************************************************************************
* 函数名: WatchInfo_setUserInfo
* 功能描述: 设置手表使用者的个人信息
* 参数说明:
* 返回值说明:
* 修改记录:
**********************************************************************************************************/
u8 WatchInfo_setUserInfo(u8 height,u8 weight)
{
user_info.height = ((float)height)/100;
user_info.weight = ((float)weight)/2;
return 0;//成功 0
}
personInfo_t * WatchInfo_getUserInfo(u8 *error)
{
u8 err;
err = 0;//0表示获取成功
error = &err;
return &user_info;
}
2.2 ESP8266 WIFI模块
设备测量的数据最终通过WIFI传递给手机APP显示,下面列出ESP8266的核心代码。
#include "esp8266.h"
extern u8 USART3_RX_BUF[USART3_MAX_RECV_LEN]; //接收缓冲,最大USART3_MAX_RECV_LEN字节
extern u8 USART3_TX_BUF[USART3_MAX_SEND_LEN]; //发送缓冲,最大USART3_MAX_SEND_LEN字节
extern vu16 USART3_RX_STA; //接收数据状态
///////////////////////////////////////////////////////////////////////////////////////////////////////////
//用户配置区
//连接端口号:8086,可自行修改为其他端口.
const u8 portnum[]="8089";
//WIFI STA模式,设置要去连接的路由器无线参数,请根据你自己的路由器设置,自行修改.
const u8 wifista_ssid[]="wbyq1"; //路由器SSID号
const u8 wifista_encryption[]="wpa2_aes"; //wpa/wpa2 aes加密方式
const u8 wifista_password[]="123456789"; //连接密码
//WIFI AP模式,模块对外的无线参数,可自行修改.
const u8 wifiap_ssid[]="Cortex_M3"; //对外SSID号
const u8 wifiap_encryption[]="wpawpa2_aes"; //wpa/wpa2 aes加密方式
const u8 wifiap_password[]="12345678"; //连接密码
/*
函数功能:向ESP82668266发送命令
函数参数:
cmd:发送的命令字符串
ack:期待的应答结果,如果为空,则表示不需要等待应答
waittime:等待时间(单位:10ms)
返 回 值:
0,发送成功(得到了期待的应答结果)
1,发送失败
*/
u8 ESP8266_SendCmd(u8 *cmd,u8 *ack,u16 waittime)
{
u8 res=0;
USART3_RX_STA=0;
UsartStringSend(USART3,cmd);//发送命令
if(ack&&waittime) //需要等待应答
{
while(--waittime) //等待倒计时
{
DelayMs(10);
if(USART3_RX_STA&0X8000)//接收到期待的应答结果
{
if(ESP8266_CheckCmd(ack))
{
res=0;
//printf("cmd->ack:%s,%s\r\n",cmd,(u8*)ack);
break;//得到有效数据
}
USART3_RX_STA=0;
}
}
if(waittime==0)res=1;
}
return res;
}
/*
函数功能:ESP8266发送命令后,检测接收到的应答
函数参数:str:期待的应答结果
返 回 值:0,没有得到期待的应答结果
其他,期待应答结果的位置(str的位置)
*/
u8* ESP8266_CheckCmd(u8 *str)
{
char *strx=0;
if(USART3_RX_STA&0X8000) //接收到一次数据了
{
USART3_RX_BUF[USART3_RX_STA&0X7FFF]=0;//添加结束符
strx=strstr((const char*)USART3_RX_BUF,(const char*)str); //查找是否应答成功
printf("RX=%s",USART3_RX_BUF);
}
return (u8*)strx;
}
/*
函数功能:向ESP8266发送指定数据
函数参数:
data:发送的数据(不需要添加回车)
ack:期待的应答结果,如果为空,则表示不需要等待应答
waittime:等待时间(单位:10ms)
返 回 值:0,发送成功(得到了期待的应答结果)luojian
*/
u8 ESP8266_SendData(u8 *data,u8 *ack,u16 waittime)
{
u8 res=0;
USART3_RX_STA=0;
UsartStringSend(USART3,data);//发送数据
if(ack&&waittime) //需要等待应答
{
while(--waittime) //等待倒计时
{
DelayMs(10);
if(USART3_RX_STA&0X8000)//接收到期待的应答结果
{
if(ESP8266_CheckCmd(ack))break;//得到有效数据
USART3_RX_STA=0;
}
}
if(waittime==0)res=1;
}
return res;
}
/*
函数功能:ESP8266退出透传模式
返 回 值:0,退出成功;
1,退出失败
*/
u8 ESP8266_QuitTrans(void)
{
while((USART3->SR&0X40)==0); //等待发送空
USART3->DR='+';
DelayMs(15); //大于串口组帧时间(10ms)
while((USART3->SR&0X40)==0); //等待发送空
USART3->DR='+';
DelayMs(15); //大于串口组帧时间(10ms)
while((USART3->SR&0X40)==0); //等待发送空
USART3->DR='+';
DelayMs(500); //等待500ms
return ESP8266_SendCmd("AT","OK",20);//退出透传判断.
}
/*
函数功能:获取ESP82668266模块的AP+STA连接状态
返 回 值:0,未连接;1,连接成功
*/
u8 ESP8266_ApStaCheck(void)
{
if(ESP8266_QuitTrans())return 0; //退出透传
ESP8266_SendCmd("AT+CIPSTATUS",":",50); //发送AT+CIPSTATUS指令,查询连接状态
if(ESP8266_CheckCmd("+CIPSTATUS:0")&&
ESP8266_CheckCmd("+CIPSTATUS:1")&&
ESP8266_CheckCmd("+CIPSTATUS:2")&&
ESP8266_CheckCmd("+CIPSTATUS:4"))
return 0;
else return 1;
}
/*
函数功能:获取ESP8266模块的连接状态
返 回 值:0,未连接;1,连接成功.
*/
u8 ESP8266_ConstaCheck(void)
{
u8 *p;
u8 res;
if(ESP8266_QuitTrans())return 0; //退出透传
ESP8266_SendCmd("AT+CIPSTATUS",":",50); //发送AT+CIPSTATUS指令,查询连接状态
p=ESP8266_CheckCmd("+CIPSTATUS:");
res=*p; //得到连接状态
return res;
}
/*
函数功能:获取ip地址
函数参数:ipbuf:ip地址输出缓存区
*/
void ESP8266_GetWanip(u8* ipbuf)
{
u8 *p,*p1;
if(ESP8266_SendCmd("AT+CIFSR\r\n","OK",50))//获取WAN IP地址失败
{
ipbuf[0]=0;
return;
}
p=ESP8266_CheckCmd("\"");
p1=(u8*)strstr((const char*)(p+1),"\"");
*p1=0;
sprintf((char*)ipbuf,"%s",p+1);
}
/*
函数功能:将收到的AT指令应答数据返回给电脑串口
参 数:mode:0,不清零USART3_RX_STA;
1,清零USART3_RX_STA;
*/
void ESP8266_AtResponse(u8 mode)
{
if(USART3_RX_STA&0X8000) //接收到一次数据了
{
USART3_RX_BUF[USART3_RX_STA&0X7FFF]=0;//添加结束符
printf("%s",USART3_RX_BUF); //发送到串口
if(mode)USART3_RX_STA=0;
}
}
/*
函数功能:ESP8266 AP模式+TCP服务器模式测试
*/
void ESP8266_APorServer(void)
{
u8 p[100],key;
u8 ipbuf[20];
// u32 rlen=0; //接收长度
// u32 constate,t=0;
while(ESP8266_SendCmd("AT\r\n","OK",20))//检查WIFI模块是否在线
{
ESP8266_QuitTrans();//退出透传
ESP8266_SendCmd("AT+CIPMODE=0\r\n","OK",200); //关闭透传模式
printf("未检测到模块,正在尝试连接模块...\r\n");
DelayMs(800);
}
printf("ESP8266模块检测OK!\r\n");
while(ESP8266_SendCmd("ATE0\r\n","OK",20)); //关闭回显
printf("请用设备连接WIFI热点:%s,%s,%ss\r\n",(u8*)wifiap_ssid,(u8*)wifiap_encryption,(u8*)wifiap_password);
/*1. 设置WIFI AP模式 */
ESP8266_SendCmd("AT+CWMODE=2\r\n","OK",50);
/*2. 重启模块 */
ESP8266_SendCmd("AT+RST\r\n","OK",20);
/*3. 延时3S等待重启成功*/
DelayMs(1000);
DelayMs(1000);
DelayMs(1000);
/*5. 配置模块AP模式无线参数*/
sprintf((char*)p,"AT+CWSAP=\"%s\",\"%s\",1,4\r\n",wifiap_ssid,wifiap_password);
ESP8266_SendCmd(p,"OK",1000);
/*4. 设置多连接模式:0单连接,1多连接(服务器模式必须开启)*/
ESP8266_SendCmd("AT+CIPMUX=1\r\n","OK",20);
/*5. 开启Server模式(0,关闭;1,打开),端口号为portnum */
sprintf((char*)p,"AT+CIPSERVER=1,%s\r\n",(u8*)portnum);
ESP8266_SendCmd(p,"OK",50);
/*6. 获取当前模块的IP*/
ESP8266_GetWanip(ipbuf);//
printf("IP地址:%s 端口:%s",ipbuf,(u8*)portnum);
USART3_RX_STA=0; //清空串口的接收标志位
// while(1)
// {
// key=GetKeyVal(1);//退出测试
// if(key==1)
// {
// printf("退出测试!\r\n");
// ESP8266_QuitTrans(); //退出透传
// ESP8266_SendCmd("AT+CIPMODE=0","OK",20); //关闭透传模式
// break;
// }
// else if(key==2) //发送数据
// {
// ESP8266_SendCmd("AT+CIPSEND=0,12\r\n","OK",200); //设置发送数据长度为12个
// ESP8266_SendData("ESP8266测试!","OK",100); //发送指定长度的数据
// DelayMs(200);
// }
// t++;
// DelayMs(10);
// if(USART3_RX_STA&0X8000) //接收到一次数据了
// {
// rlen=USART3_RX_STA&0X7FFF; //得到本次接收到的数据长度
// USART3_RX_BUF[rlen]=0; //添加结束符
// printf("接收的数据: rlen=%d,%s",rlen,USART3_RX_BUF); //发送到串口
// USART3_RX_STA=0;
// if(constate!=3)t=1000; //状态为还未连接,立即更新连接状态
// else t=0; //状态为已经连接了,10秒后再检查
// }
// if(t==1000)//连续10秒钟没有收到任何数据,检查连接是不是还存在.
// {
//// constate=ESP8266_ConstaCheck();//得到连接状态
//// if(!constate)printf("连接失败!\r\n");
// t=0;
// }
// if((t%20)==0)LED2=!LED2;
// ESP8266_AtResponse(1);
// }
}
2.3 体温检测模块
体温检测模块是串口接口,发送指令返回数据,代码如下:
#include "TEMPERATURE.H"
/*
发送一个字节
*/
void UsartSendByte(uint8_t data)
{
USART2->DR=data;
while(!(USART2->SR&(1<<7))){}
}
/*
温度模块检测初始化
*/
void TemPeratureInit(void)
{
UsartInit(USART2,36,9600); //串口初始化
DelayMs(2); //延时启动
UsartSendByte(0xA5);
UsartSendByte(0x45); //发送读方位角指令
UsartSendByte(0xEA);
}
u8 TEMP_data[20]={0},Receive_ok=0;
u8 tem_flag=0;
//读取温度信息
void GetTemInfo(float *buff)
{
u8 sum=0,i;
if(Receive_ok)//串口接收完毕
{
for(sum=0,i=0;i<(TEMP_data[3]+4);i++)//TEMP_data[3]=4
sum+=TEMP_data[i];
if(sum==TEMP_data[i])//校验和判断
{
buff[0]=(float)((TEMP_data[4]<<8)|TEMP_data[5])/100; //得到真实温度
buff[1]=(float)((TEMP_data[6]<<8)|TEMP_data[7])/100; //得到真实温度
tem_flag=1;
}
Receive_ok=0;//处理数据完毕标志
}
}