本系统设计了一个可以进行心率、血氧实时监测、GPS经纬度数据获取的装置,并能够通过无线通信模块将监测数据发送给岸上救生人员配备的无线救生装置中,救生人员能够根据接收到的信息迅速启动应急措施。本章节用于完成救生装置硬件模块的选型和电路的设计。
设计要求如下:
(1)采用心率血氧监测模块:通过MAX30102检测使用者的心率和血氧数据,采集的心率血氧信息通过串口传输到单片机中。该传感器输入电压支持3.3~5.0V,默认上拉电压3.3V,内置ADC精度:16bit。
(2)当佩戴该装置下水时,在水中若检测到用户心率血氧处于不正常水平,可以控制二氧化碳充气救护衣(采用继电器模块控制开启和关闭,继电器模块工作电压为3.3V,高电平关闭,低电平开启),实现漂浮功能。
(3)当佩戴该系统下水时,正常情况下救护系统是没有充气的(继电器关闭),方便用户自由泳。
(5)实现用户自救功能:当用户自我感觉无法自由泳时,且检测系统没有打开充气功能(继电器开启),此时用户可通过按钮手动打开充气功能实现自救。
(6)实现远程控制功能:当用户处于溺水时,岸上救生员可以通过远程控制功能实现充气(远程按键按下,继电器打开,远程传输使用GT-38模块,433M无线通信频率,可传输距离理论为1200米,串口通信),实现及时救生。
(7)当心率和血氧监测异常时,系统配置多颗LED灯发光(心率指示一个红色LED,血氧一个红色LED,报警指示红色LED,正常指示绿色LED,LED灯工作电压3.3V,高电平关闭,低电平点亮),做到提示用户所在位置的功能,方便施救人员及时发现并救助。
(8)当心率和血氧监测异常时,实现蜂鸣器报警(对应LED亮起,同时报警指示红色LED亮,正常指示绿色LED灭,蜂鸣器模块工作电压3.3V,低电平工作,高电平停止)。
(9)实现GPS定位功能,岸上救生员可以通过液晶屏显示的用户GPS坐标寻找水中用户位置(GPS模块工作电压3.3V,串口通信,NMEA协议,定位精度2.5米内,初次上电寻星时间小于40秒,具备记忆功能)。
(10)屏幕采用LCD显示GPS坐标和救生系统状态信息。
本系统设计要求心率监测的误差在±2次/分,当心率发生急速突变超过设定的心率血氧阈值时系统能够自动启动报警功能;血氧饱和度精度为±0.2%,当血氧含量低于设定的阈值时,则启动系统报警功能;本系统设置了远程可控功能用于接收游泳者的身体信息以供水边救生人员人为检测以及远程模块具备按键输入控制游泳者救生系统的功能,当游泳者身体参数突发异常时,救生人员可以通过远程按键及时手动触发救生系统。
本设备配备了LCD屏幕心率和GPS坐标显示,当本设计的报警系统被触发时,远程设备能够接收到从游泳者佩戴的救生系统发出的警报信息、身体血氧、心率指标信息以及GPS坐标信息,并能够在远程设备的LCD显示屏上进行显示。故本系统采集信息需要使用到GPS模块获取GPS坐标,心率血氧模块获取佩戴者心率血氧数据,无线通信模块进行数据通信,LCD显示模块进行数据的可视化显示,按键模块进行配置设置。本系统设计框架如下图所示。
本系统采集心率血氧传感器数据,采集GPS经纬度数据,将采集的数据显示到OLED屏幕上,当将手指放入心率传感器采集的位置后,传感器采集对应的心率血氧值,通过串口传输给单片机,单片机通过接收和解析程序,成功解析后,将数据显示到屏幕上,GPS采集方法与血氧传感器类似,都是串口方式,不同的就是协议以及
内容有所差别。
LCD屏幕显示使用的是串口屏幕,通过指令发送对应信息就可以完成显示功能,如字体大小,颜色等。如图5.4所示为本次通过单片机向液晶屏发的指令,格式为字符串形式。内容为DC16即发送的显示内容字体大小为16 * 16,往后的数字如0,11为显示的位置,0代表X轴方向第0个像素点,11代表Y轴方向第11个像素点,位置是从0开始,屏幕像素大小为128 * 64即宽度为128像素点,高度为64像素点,屏幕支持旋转,本次设置为竖向,即X轴取值范围为0-63,Y轴取值范围为0-123。再往后的单引号括起来的为显示内容,最后放的是显示字体的颜色。本系统通过GT38无线传输模块将主从机信息进行同步,当处于接收范围之内,主机采集的信息会被同步显示到从机,如下图所示为本系统硬件整体显示情况,将手指放在传感器上进行采集后,可以看到采集信息是同步的(为了增加对比度,本次采用了黑色背景,白色字体显示)。
MAX30102是一个集成了脉搏血氧仪和心率仪的生物感器模块[3]。本次采用的心率血氧模块为MAX30102[12],该模块采用了一个STM32F07单片机,该单片机是STM32系列的低功耗产品,芯片封装小,性能高,使用该单片机用于读取心率血氧值,然后通过算法处理后,通过模块引出的串口接口传输出去,该模块的串口通信波特率可调,且该模块使用了AT指令的通讯协议。该模块的心率测量范围宽,为20-200次/分钟,血氧的测量范围:50%-100%,该模块使用串口输出,因此无需操心MAX30102的数据读取和处理,主控单片机只需要通过串口发送AT指令来获取心率血氧值即可,这样极大的方便了心率血氧程序的设计。该模块的基本信息如下图所示,从原理图可以看出,该模块是MAX30102传感器和STM32F070F6P6单片机组成,由STM32F070F6P6单片机采集MAX30102的数据,经处理后通过串口使用AT协议进行输出。由此,本系统使用单片机的串口3接口PB10和PB11连接到该传感器模块对应的串口引脚,以获得通信数据,解析其中的心率血氧内容即可。
本设计需要对游泳者的位置数据进行采集,当游泳者出现身体异常时,系统能够及时上报用户位置供救生员准确救援。本次使用的GPS定位模块为ATGM336H-5N。ATGM336H-5N模块具有非常高的灵敏度,功耗很低,其运行时电流仅仅为25mA[4],该模块上电首次定位时间相对来说还是比较短的,在空旷的地方32秒能即可完成首次定位。模块内置了天线检测和天线短路保护的功能。ATGM336H-5N模块输出方式为串口传输。 ATGM336H-5N模块基本情况如下图所示。本系统使用单片机的串口1的接收引脚RXD对应的PA10口和该模块相连。
本系统使用GT38作为无线通信模块。该模块在空旷地无遮挡的情况下最大的通信距离为1200米,模块的功耗比较低,最大功率为100mW。 如图所示,GT38无线通信模块通过串口线进行交叉连接,即串口的TX连接到其它模块串口的RX,相应的,RX连接到其它模块串口的TX,模块不支持同时收发数据,故仅能工作在半双工状态,收完数据再进行发送即可。本系统使用该模块将主控板上测得的使用者的心率、血氧以及GPS经纬度数据传输给从机,采用此无线传输模块传输距离远。模块通信采用串口,故本次设计使用主控单片机的串口2,从控单片机的串口1来发送或接收数据,连接原理图如图所示。
/*******************************************************************************
\* 文件名称:基于STM32单片机的心率血氧检测与远程定位报警装置
\* 实验目的:1.
\* 2.
\* 程序说明:完整程序Q:277 227 2579;@: itworkstation@ hotmail.com
\* 日期版本:本项目分享关键细节,熟悉使用单片机的可做参考代码。完整讲解+源代码工程可联系获取,可定制。
*******************************************************************************/
#ifndef __gps_h
#define __gps_h
#include "stm32f10x.h"
#define GPS_USART_REC_LEN 200 //定义最大接收字节数 200
extern char GPS_USART_RX_BUF[GPS_USART_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节.末字节为换行符
//定义数组长度
#define GPS_Buffer_Length 80
#define UTCTime_Length 11
#define latitude_Length 11
#define N_S_Length 2
#define longitude_Length 12
#define E_W_Length 2
#define false 0
#define true 1
typedef struct SaveData
{
char GPS_Buffer[GPS_Buffer_Length];
char isGetData; //是否获取到GPS数据
char isParseData; //是否解析完成
char UTCTime[UTCTime_Length]; //UTC时间
char latitude[latitude_Length]; //纬度
char N_S[N_S_Length]; //N/S
char longitude[longitude_Length]; //经度
char E_W[E_W_Length]; //E/W
char isUsefull; //定位信息是否有效
} _SaveData;
extern _SaveData Save_Data;
void CLR_Buf(void);
void clrStruct(void);
void GPS_RecHandle(u8 res);
void parseGpsBuffer(void); //parse:分析GPS数据
#endif
#include "gps.h"
#include
#include
#include "led.h"
u16 point1 = 0; //接收数组定位
char GPS_USART_RX_BUF[GPS_USART_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节.
_SaveData Save_Data;
void CLR_Buf(void) // 串口缓存清理
{
memset(GPS_USART_RX_BUF, 0, GPS_USART_REC_LEN); //清空
point1 = 0;
}
void clrStruct(void)
{
Save_Data.isGetData = false;
Save_Data.isParseData = false;
Save_Data.isUsefull = false;
memset(Save_Data.GPS_Buffer, 0, GPS_Buffer_Length); //清空
memset(Save_Data.UTCTime, 0, UTCTime_Length);
memset(Save_Data.latitude, 0, latitude_Length);
memset(Save_Data.N_S, 0, N_S_Length);
memset(Save_Data.longitude, 0, longitude_Length);
memset(Save_Data.E_W, 0, E_W_Length);
}
void GPS_RecHandle(u8 Res)
{
if(Res == '$')
{
point1 = 0;
}
GPS_USART_RX_BUF[point1++] = Res;
if(GPS_USART_RX_BUF[0] == '$' && GPS_USART_RX_BUF[4] == 'M' && GPS_USART_RX_BUF[5] == 'C') //确定是否收到"GPRMC/GNRMC"这一帧数据
{
if(Res == '\n')
{
memset(Save_Data.GPS_Buffer, 0, GPS_Buffer_Length); //清空
memcpy(Save_Data.GPS_Buffer, GPS_USART_RX_BUF, point1); //保存数据
Save_Data.isGetData = true;
point1 = 0;
memset(GPS_USART_RX_BUF, 0, GPS_USART_REC_LEN); //清空
}
}
if(point1 >= GPS_USART_REC_LEN)
{
point1 = GPS_USART_REC_LEN;
}
}
void errorLog(int num)
{
// while (1)
// {
// printf("ERROR%d\r\n",num);
// }
LED_Control(ON);
}
void parseGpsBuffer(void) //parse:分析GPS数据
{
char *subString;
char *subStringNext;
char i = 0;
if (Save_Data.isGetData)
{
Save_Data.isGetData = false;
// printf("**************\r\n");
// printf(Save_Data.GPS_Buffer);
for (i = 0 ; i <= 6 ; i++)
{
if (i == 0)
{
if ((subString = strstr(Save_Data.GPS_Buffer, ",")) == NULL)
errorLog(1); //解析错误
}
else
{
subString++;
if ((subStringNext = strstr(subString, ",")) != NULL)
{
char usefullBuffer[2];
switch(i)
{
case 1:memcpy(Save_Data.UTCTime, subString, subStringNext - subString);break; //获取UTC时间
case 2:memcpy(usefullBuffer, subString, subStringNext - subString);break; //获取UTC时间
case 3:memcpy(Save_Data.latitude, subString, subStringNext - subString);break; //获取纬度信息
case 4:memcpy(Save_Data.N_S, subString, subStringNext - subString);break; //获取N/S
case 5:memcpy(Save_Data.longitude, subString, subStringNext - subString);break; //获取经度信息
case 6:memcpy(Save_Data.E_W, subString, subStringNext - subString);break; //获取E/W
default:break;
}
subString = subStringNext;
Save_Data.isParseData = true;
if(usefullBuffer[0] == 'A')
Save_Data.isUsefull = true;
else if(usefullBuffer[0] == 'V')
Save_Data.isUsefull = false;
}
else
{
errorLog(2); //解析错误
}
}
}
}
}
#ifndef __WUXIAN_H
#define __WUXIAN_H
#include "Def_config.h"
#include "stm32f10x.h"
#define XinLv_Length 5
#define XueYang_Length 5
#define latitude_Length 11
#define N_S_Length 2
#define longitude_Length 12
#define E_W_Length 2
#define WuXian_RX_BUF_Length 100
#define JUDGE_Length 2
typedef struct
{
char WuXian_Buffer[WuXian_RX_BUF_Length];
char isGetData; //是否获取到 数据
char isParseData; //是否解析完成
char XinLv[XinLv_Length]; //心率值,字符串形式存储
char XueYang[XueYang_Length]; //血氧数据
char latitude[latitude_Length]; //纬度
char N_S[N_S_Length]; //N/S
char longitude[longitude_Length]; //经度
char E_W[E_W_Length]; //E/W
char is_XinLvWarn[JUDGE_Length];
char is_XueYangWarn[JUDGE_Length];
char is_HandWarn[JUDGE_Length];
} _SendData;
extern _SendData Send_Data;
#define false 0
#define true 1
void parseWuXianBuffer(void);
void WuXian_Rec(u8 Res);
void WuXian_Clear(void);
#endif
#include "wuxian.h"
#include
#include
#include "lcdUart.h"
#include "usart1.h"
u8 point1 = 0;
u8 WuXian_RX_BUF[WuXian_RX_BUF_Length];
_SendData Send_Data;
void WuXian_Clear(void)
{
memset(Send_Data.WuXian_Buffer,'\0', WuXian_RX_BUF_Length);
memset(Send_Data.XinLv,'\0', WuXian_RX_BUF_Length);
memset(Send_Data.XueYang,'\0', WuXian_RX_BUF_Length);
memset(Send_Data.latitude,'\0', WuXian_RX_BUF_Length);
memset(Send_Data.N_S,'\0', WuXian_RX_BUF_Length);
memset(Send_Data.longitude,'\0', WuXian_RX_BUF_Length);
memset(Send_Data.E_W,'\0', WuXian_RX_BUF_Length);
memset(Send_Data.is_XinLvWarn,'\0', WuXian_RX_BUF_Length);
memset(Send_Data.is_XueYangWarn,'\0', WuXian_RX_BUF_Length);
memset(Send_Data.is_HandWarn,'\0', WuXian_RX_BUF_Length);
Send_Data.isGetData = FALSE;
Send_Data.isParseData = FALSE;
}
void WuXian_Rec(u8 Res)
{
if(Res == '$')
{
point1 = 0;
}
WuXian_RX_BUF[point1++] = Res;
if(Res == '\n')
{
memset(Send_Data.WuXian_Buffer,'\0', WuXian_RX_BUF_Length); //清空
memcpy(Send_Data.WuXian_Buffer, WuXian_RX_BUF, point1); //保存数据
Send_Data.isGetData = true;
point1 = 0;
USART1_SendString((u8 *)Send_Data.WuXian_Buffer);
// printf("DCV16(0,7,'%s',%d);\r\n",WuXian_RX_BUF,1);
// LCD_CheckBusy();
memset(WuXian_RX_BUF,'\0', WuXian_RX_BUF_Length); //清空
}
if(point1 >= WuXian_RX_BUF_Length)
{
point1 = WuXian_RX_BUF_Length;
}
}
void parseWuXianBuffer(void) //parse:分析数据
{
char *subString;
char *subStringNext;
char i = 0;
if (Send_Data.isGetData)
{
Send_Data.isGetData = false;
for(i=0;i<9;i++)
{
if (i == 0)
{
if ((subString = strstr(Send_Data.WuXian_Buffer, "$")) != NULL)
{
subString++;
if ((subStringNext = strstr(subString, ",")) != NULL)
{
memcpy(Send_Data.XinLv, subString, subStringNext - subString);
subString = subStringNext;
}
}
}
else if(i<8)
{
subString++;
if((subStringNext = strstr(subString, ",")) != NULL)
{
switch(i)
{
case 1:
memcpy(Send_Data.XueYang, subString, subStringNext - subString);
break;
case 2:
memcpy(Send_Data.latitude, subString, subStringNext - subString);
break;
case 3:
memcpy(Send_Data.N_S, subString, subStringNext - subString);
break;
case 4:
memcpy(Send_Data.longitude, subString, subStringNext - subString);
break;
case 5:
memcpy(Send_Data.E_W, subString, subStringNext - subString);
break;
case 6:
memcpy(Send_Data.is_XinLvWarn, subString, subStringNext - subString);
break;
case 7:
memcpy(Send_Data.is_XueYangWarn, subString, subStringNext - subString);
break;
default:break;
}
subString = subStringNext;
}
}
else
{
subString++;
if((subStringNext = strstr(subString, "\r")) != NULL)
{
memcpy(Send_Data.is_HandWarn, subString, subStringNext - subString);
}
}
}
Send_Data.isParseData = true;
}
}
#ifndef __XINLV_H
#define __XINLV_H
#include "stm32f10x.h"
#define XinLv_Buffer_Length 200
#define XinLv_Length 5
#define XueYang_Length 5
#define false 0
#define true 1
typedef struct
{
char XinLv_Rec_Buffer[XinLv_Buffer_Length];
char isGetData; //是否获取到数据
char isParseData; //是否解析完成
char XinLv[XinLv_Length]; //心率值,字符串形式存储
char XueYang[XueYang_Length]; //血氧数据
char isUsefull; //信息是否有效
int count_erroTime;
} _XinLvData;
extern _XinLvData XinLv_Data;
void XinLv_RecHandle(u8 Res);
void parseXinLvBuffer(void);
#endif
#include "xinLv.h"
#include
#include
#include "usart3.h"
u8 point2 = 0;
char XinLv_RX_BUF[XinLv_Buffer_Length]; //接收缓冲,最大XinLv_Buffer_Length个字节.末字节为换行符
_XinLvData XinLv_Data;
u8 XinLv_Find(char *a) // 串口命令识别函数
{
if(strstr(XinLv_Data.XinLv_Rec_Buffer,a)!=NULL)
return 1;
else
return 0;
}
void XinLv_Clear_Data(void)
{
XinLv_Data.isGetData = false;
XinLv_Data.isParseData = false;
XinLv_Data.isUsefull = false;
memset(XinLv_Data.XinLv, 0, XinLv_Length); //清空
memset(XinLv_Data.XueYang, 0, XueYang_Length); //清空
memset(XinLv_Data.XinLv_Rec_Buffer, 0, XinLv_Buffer_Length); //清空
}
void XinLv_RecHandle(u8 Res)
{
if(Res == '+')
{
point2 = 0;
}
XinLv_RX_BUF[point2++] = Res;
if(Res == 'K')
{
memset(XinLv_Data.XinLv_Rec_Buffer, 0, XinLv_Buffer_Length); //清空
memcpy(XinLv_Data.XinLv_Rec_Buffer, XinLv_RX_BUF, point2); //保存数据
XinLv_Data.isGetData = true;
point2 = 0;
memset(XinLv_RX_BUF, 0, XinLv_Buffer_Length); //清空
if(XinLv_Find("NULL"))
{
XinLv_Clear_Data();
}
}
if(point2 >= XinLv_Buffer_Length)
{
point2 = XinLv_Buffer_Length;
}
}
void parseXinLvBuffer(void)
{
char *subString;
char *subStringNext;
if (XinLv_Data.isGetData)
{
XinLv_Data.isGetData = false;
if(XinLv_Find("HEART"))
{
subString = strstr(XinLv_Data.XinLv_Rec_Buffer, "=")+1;
subStringNext = strstr(XinLv_Data.XinLv_Rec_Buffer, "\r");
memset(XinLv_Data.XinLv,'\0', XinLv_Length); //清空
memset(XinLv_Data.XinLv,' ', 3); //清空
memcpy(XinLv_Data.XinLv, subString, subStringNext - subString);
XinLv_Data.isParseData = true;
XinLv_Data.isUsefull = true;
XinLv_Data.count_erroTime = 0;
}
if(XinLv_Find("SPO2"))
{
subString = strstr(XinLv_Data.XinLv_Rec_Buffer, "=")+1;
subStringNext = strstr(XinLv_Data.XinLv_Rec_Buffer, "\r");
memset(XinLv_Data.XueYang,'\0', XueYang_Length); //清空
memset(XinLv_Data.XueYang,' ', 3); //清空
memcpy(XinLv_Data.XueYang, subString, subStringNext - subString);
// USART3_SendString((char *)XinLv_Data.XueYang);
XinLv_Data.isParseData = true;
XinLv_Data.isUsefull = true;
XinLv_Data.count_erroTime = 0;
}
}
}