一、开发环境介绍
主控芯片: STM32F103ZET6
代码编程软件: keil5
心率检测模块: PulseSensor
WIFI模块: ESP8266 --可选的。直接使用串口有线传输给上位机也可以。
上位机: C++(QT) 设计的。 支持PC机电脑、Android手机显示。
与上位机的传输协议: 支持串口传输、WIFI网络传输两种。 如果是PC就可以直接连接串口传输数据,如果不方便可以直接通过WIFI---TCP协议传输。
代码下载地址: 心率检测
二、PulseSensor心率模块介绍
PulseSensor 是一款用于脉搏心率测量的光电反射式模拟传感器。
可以将其佩戴于手指、耳垂、手腕等处,通过杜邦线--导线将引脚连接到单片机,可将采集到的模拟信号传输给单片机,单片机配置ADC用来转换为数字信号,再通过单片机简单计算后就可以得到心率数值;为了方便联动健康管理系统,也方便自己了解自己的心率,可将脉搏波形通过串口、WIFI等方式上传到电脑、手机显示波形,然后根据提前配置的参数,结合算法确定是否正常。
PulseSensor 是一款开源硬件, 目前国外官网上已有其对应的单片机程序,也附带有对应的上位机Processing 程序, 比较适用于心率方面的科学研究和教学演示,也非常适合用于二次开发;上位机也可以自己开发,根据自己的需求定制,达到自己想要的功能。
传感器的接口一共 3 个,
其中标有S的为模拟信号输出线
标有+的为电源输入线(中间);
标有-的为地线。
总结一下:
S → 脉搏信号输出(要接单片机 AD 接口)
+ → 5v(或 3.3v)电源输入
- → GND 地
传感器的硬件参数介绍:
传统的测量方法介绍:
整个心率传感器的结构如下图:
三、STM32的控制代码
STM32的采集代码比较简单,因为就只需要配置对应引脚的ADC功能采集即可。
可以采集10次,去掉最大值最小值取平均值,拿到最终结果再传递给上位机显示。
/* 函数功能: 初始化ADC1 硬件连接: PA1 --ADC1的通道1 配置的模式:模拟输入 */ void ADC1_Init(void) { /*1. 配置GPIO口*/ RCC->APB2ENR|=1<<2; //开启PA时钟 GPIOA->CRL&=0xFFFFFF0F; GPIOA->CRL|=0x00000000; /*2. 配置ADC相关寄存器*/ RCC->APB2ENR|=1<<9;//开启ADC1时钟 RCC->APB2RSTR|=1<<9; //开启ADC1复位时钟 RCC->APB2RSTR&=~(1<<9);//关闭ADC1复位时钟 RCC->CFGR&=~(0x3<<14); //清除ADC的时钟配置 RCC->CFGR|=0x2<<14; //配置6分频 ADC1->CR2|=1<<20; //开启外部事件转换 ADC1->CR2|=0x7<<17; //SW开关方式控制ADC转换(作为外部事件) ADC1->SMPR2|=0x7<<3; //配置通道1的采样时间 ADC1->CR2|=1<<0;//开启ADC并启动转换 ADC1->CR2|=1<<3;//开启ADC校准初始化 while(ADC1->CR2&1<<3){}//等待初始化完成 ADC1->CR2|=1<<2;//开启ADC校准 while(ADC1->CR2&1<<2){} //等待ADC校准完成 } /* 函数功能: 根据传入的通道编号获取一次该通道的ADC值 */ u16 ADC1_GetData(u8 ch) { ADC1->SQR3&=0xFFFFFFE0; //0xE0-->11100000 //清除原来的通道编号 ADC1->SQR3|=ch<<0; //配置现在即将转换的通道号 ADC1->CR2|=1<<22; //开启一次ADC规则通道转换 while(!(ADC1->SR&1<<1)){} //等待转换完成 return ADC1->DR; //读出ADC的结果值 }
3.2 ESP8266 WIFI 配置代码
#include "esp8266.h" /* 函数功能:向ESP82668266发送命令 函数参数: cmd:发送的命令字符串 ack:期待的应答结果,如果为空,则表示不需要等待应答 waittime:等待时间(单位:10ms) 返 回 值: 0,发送成功(得到了期待的应答结果) 1,发送失败 */ u8 ESP8266_SendCmd(u8 *cmd,u8 *ack,u16 waittime) { u8 res=0; USART3_RX_STA=0; USART3_RX_CNT=0; UsartStringSend(USART3,cmd);//发送命令 if(ack&&waittime) //需要等待应答 { while(--waittime) //等待倒计时 { DelayMs(10); if(USART3_RX_STA)//接收到期待的应答结果 { if(ESP8266_CheckCmd(ack)) { res=0; //printf("cmd->ack:%s,%s\r\n",cmd,(u8*)ack); break;//得到有效数据 } USART3_RX_STA=0; USART3_RX_CNT=0; } } if(waittime==0)res=1; } return res; } /* 函数功能:ESP8266发送命令后,检测接收到的应答 函数参数:str:期待的应答结果 返 回 值:0,没有得到期待的应答结果 其他,期待应答结果的位置(str的位置) */ u8* ESP8266_CheckCmd(u8 *str) { char *strx=0; if(USART3_RX_STA) //接收到一次数据了 { USART3_RX_BUF[USART3_RX_CNT]=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)//接收到期待的应答结果 { if(ESP8266_CheckCmd(ack))break;//得到有效数据 USART3_RX_STA=0; USART3_RX_CNT=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\r\n","OK",20);//退出透传判断. } /* 函数功能:获取ESP8266模块的连接状态 返 回 值:0,未连接;1,连接成功. */ u8 ESP8266_ConstaCheck(void) { u8 *p; u8 res; if(ESP8266_QuitTrans())return 0; //退出透传 ESP8266_SendCmd("AT+CIPSTATUS\r\n",":",50); //发送AT+CIPSTATUS指令,查询连接状态 p=ESP8266_CheckCmd("+CIPSTATUS\r\n:"); 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); }
四、QT设计的上位机代码
软件有两个版本: 1. 网络版本 2. 串口版本
网络版本主要是通过TCP协议传输数据显示,串口版本直接通过串口传输。
4.2 widget.cpp代码
代码较多,这里就主UI的部分代码。
#include "widget.h" #include "ui_widget.h" #define AppFontName "Microsoft YaHei" #define AppFontSize 9 #define TextColor QColor(255,255,255) #define Plot_NoColor QColor(0,0,0,0) //曲线1的颜色 #define HeartRate_Plot_DotColor QColor(236,110,0) #define HeartRate_Plot_LineColor QColor(246,98,0) #define HeartRate_Plot_BGColor QColor(246,98,0,80) //曲线2的颜色 #define HeartRate_Plot_DotColor_2 Qt::blue #define HeartRate_Plot_LineColor_2 Qt::blue #define HeartRate_Plot_BGColor_2 Qt::blue #define TextWidth 1 #define LineWidth 2 #define DotWidth 5 //一个刻度里的小刻度数量--太小的话显示的时间会重叠 #define HeartRate_Plot_Count 5 //Y轴最大范围值 #define HeartRate_Plot_MaxY 3000 /* * 设置QT界面的样式 */ void Widget::SetStyle(const QString &qssFile) { QFile file(qssFile); if (file.open(QFile::ReadOnly)) { QString qss = QLatin1String(file.readAll()); qApp->setStyleSheet(qss); QString PaletteColor = qss.mid(20,7); qApp->setPalette(QPalette(QColor(PaletteColor))); file.close(); } else { qApp->setStyleSheet(""); } } Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui->setupUi(this); /*服务器线程*/ //开始信号 connect(this,SIGNAL(StartServerThread()),&tcp_server_class,SLOT(run())); //日志信号 connect(&tcp_server_class,SIGNAL(LogSend(QString)),this,SLOT(Log_Display(QString))); //移动到线程 tcp_server_class.moveToThread(&tcp_server_thread); tcp_server_thread.start(); //启动线程 StartServerThread(); //创建服务器 this->setWindowTitle("万邦易嵌-健康监控管家"); //波形图界面初始化 InitForm(); InitPlot(); HeartRate_InitPlot(); HeartRate_LoadPlot(); SetStyle(":/blue.css"); //开始加载数据 plot_timer->start(100); } Widget::~Widget() { delete ui; } //日志显示 void Widget::Log_Display(QString text) { QPlainTextEdit *plainTextEdit_log=ui->plainTextEdit_log; //设置光标到文本末尾 plainTextEdit_log->moveCursor(QTextCursor::End, QTextCursor::MoveAnchor); //当文本数量超出一定范围就清除 if(plainTextEdit_log->toPlainText().size()>1024*4) { plainTextEdit_log->clear(); } plainTextEdit_log->insertPlainText(text); //移动滚动条到底部 QScrollBar *scrollbar = plainTextEdit_log->verticalScrollBar(); if(scrollbar) { scrollbar->setSliderPosition(scrollbar->maximum()); } } //查看服务器状态 void Widget::on_toolButton_server_stat_clicked() { QString text="TCP服务器IP地址列表:\n"; QList<QHostAddress> list = QNetworkInterface::allAddresses(); for(int i=0;i<list.count();i++) { QHostAddress addr=list.at(i); if(addr.protocol() == QAbstractSocket::IPv4Protocol) { text+=addr.toString()+"\n"; } } text+="TCP服务器端口号:8888\n"; if(ClientSocket) { if(ClientSocket->socketDescriptor()==-1) { text+="设备未连接\n"; } else { text+="设备连接成功\n"; } } else { text+="设备未连接\n"; } text+="数据协议:\n"; text+="A:心电数据1,B:新电数据2,C:运动步数,D:运动距离,E:体表温度\n"; text+="例如: \"A:1633215,B:1833215,C:45,D:28,E:66.55\""; QMessageBox::about(this,"状态信息",text); } //窗口关闭事件 void Widget::closeEvent(QCloseEvent *event) { tcp_server_thread.quit(); tcp_server_thread.wait(); } void Widget::InitForm() { //初始化随机数种子 QTime time = QTime::currentTime(); qsrand(time.msec() + time.second() * 1000); //初始化动态曲线定时器 plot_timer = new QTimer(this); connect(plot_timer, SIGNAL(timeout()), this, SLOT(HeartRate_LoadPlot())); plots.append(ui->plot2); } void Widget::InitPlot() { //设置纵坐标名称 plots.at(0)->yAxis->setLabel("心电数据(单位:%)"); //设置纵坐标范围 plots.at(0)->yAxis->setRange(0, HeartRate_Plot_MaxY); //设置支持鼠标移动缩放波形界面 plots.at(0)->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom); //设置背景颜色 #if 1 foreach (QCustomPlot *plot, plots) { //设置字体大小 QFont font = QFont(AppFontName, AppFontSize - 2); plot->legend->setFont(font); plot->xAxis->setLabelFont(font); plot->yAxis->setLabelFont(font); plot->xAxis->setTickLabelFont(font); plot->yAxis->setTickLabelFont(font); //设置坐标颜色/坐标名称颜色 plot->yAxis->setLabelColor(TextColor); plot->xAxis->setTickLabelColor(TextColor); plot->yAxis->setTickLabelColor(TextColor); plot->xAxis->setBasePen(QPen(TextColor, TextWidth)); plot->yAxis->setBasePen(QPen(TextColor, TextWidth)); plot->xAxis->setTickPen(QPen(TextColor, TextWidth)); plot->yAxis->setTickPen(QPen(TextColor, TextWidth)); plot->xAxis->setSubTickPen(QPen(TextColor, TextWidth)); plot->yAxis->setSubTickPen(QPen(TextColor, TextWidth)); //设置画布背景色 QLinearGradient plotGradient; plotGradient.setStart(0, 0); plotGradient.setFinalStop(0, 350); plotGradient.setColorAt(0, QColor(80, 80, 80)); plotGradient.setColorAt(1, QColor(50, 50, 50)); plot->setBackground(plotGradient); //设置坐标背景色 QLinearGradient axisRectGradient; axisRectGradient.setStart(0, 0); axisRectGradient.setFinalStop(0, 350); axisRectGradient.setColorAt(0, QColor(80, 80, 80)); axisRectGradient.setColorAt(1, QColor(30, 30, 30)); plot->axisRect()->setBackground(axisRectGradient); //设置图例提示位置及背景色 plot->axisRect()->insetLayout()->setInsetAlignment(0, Qt::AlignTop | Qt::AlignRight); plot->legend->setBrush(QColor(255, 255, 255, 200)); plot->replot(); } #endif } void Widget::HeartRate_InitPlot() { plots.at(0)->addGraph(); plots.at(0)->graph(0)->setName("心电数据1"); plots.at(0)->graph(0)->setPen(QPen(HeartRate_Plot_LineColor, LineWidth)); plots.at(0)->graph(0)->setScatterStyle( QCPScatterStyle(QCPScatterStyle::ssCircle, QPen(HeartRate_Plot_DotColor, LineWidth), QBrush(HeartRate_Plot_DotColor), DotWidth)); //设置动态曲线的横坐标格式及范围 plots.at(0)->xAxis->setTickLabelType(QCPAxis::ltDateTime); plots.at(0)->xAxis->setDateTimeFormat("HH:mm:ss"); plots.at(0)->xAxis->setAutoTickStep(true); plots.at(0)->xAxis->setTickStep(0.5); plots.at(0)->xAxis->setRange(0, HeartRate_Plot_Count, Qt::AlignRight); plots.at(0)->addGraph();//相当于添加一条新的曲线 plots.at(0)->graph(1)->setName("心电数据2"); plots.at(0)->graph(1)->setPen(QPen(HeartRate_Plot_LineColor_2, LineWidth)); plots.at(0)->graph(1)->setScatterStyle( QCPScatterStyle(QCPScatterStyle::ssCircle, QPen(HeartRate_Plot_DotColor_2, LineWidth), QBrush(HeartRate_Plot_DotColor_2), DotWidth)); //设置是否需要显示曲线的图例说明 foreach (QCustomPlot *plot, plots) { plot->legend->setVisible(true); plot->replot(); } //得到数据指针 mData_0 = plots.at(0)->graph(0)->data(); mData_1 = plots.at(0)->graph(1)->data(); } void addToDataBuffer(QCPDataMap *mData,double x, double y) { QCPData newData; newData.key = x; newData.value = y; mData->insert(x, newData); } //加载曲线数据 void Widget::HeartRate_LoadPlot() { int i; bool flag=false; for(i=0;i<5;i++) { //得到秒单位的时间 HeartRate_plot_key = QDateTime::currentDateTime().toMSecsSinceEpoch() / 1000.0; //心电数据1 HeartRate_plot_value=uart_queue_data.read_queueA(); if(HeartRate_plot_value>0) { flag=true; addToDataBuffer(mData_0,HeartRate_plot_key,HeartRate_plot_value); } //心电数据2 HeartRate_plot_value=uart_queue_data.read_queueB(); if(HeartRate_plot_value>0) { flag=true; addToDataBuffer(mData_1,HeartRate_plot_key,HeartRate_plot_value); } } if(flag) { plots.at(0)->xAxis->setRange(HeartRate_plot_key, HeartRate_Plot_Count , Qt::AlignRight); plots.at(0)->rescaleAxes(false); //设置图表完全可见 plots.at(0)->replot(); } /* A:心电数据1,B:新电数据2,C:运动步数,D:运动距离,E:体表温度 例如: "A:1633215,B:1833215,C:45,D:28,E:66.55" */ int val=uart_queue_data.read_queueC(); if(val>0) { ui->lcdNumber_bumber->display(val); } val=uart_queue_data.read_queueD(); if(val>0) { ui->lcdNumber_len->display(val); } double tmp_val=uart_queue_data.read_queueE(); if(tmp_val>0) { ui->lcdNumber_temp->display(tmp_val); } } void Widget::on_toolButton_src_data_clicked() { ui->stackedWidget->setCurrentIndex(0); } void Widget::on_toolButton_image_data_clicked() { ui->stackedWidget->setCurrentIndex(1); } void Widget::on_toolButton_clear_clicked() { mData_0->clear(); mData_1->clear(); } void Widget::on_commandLinkButton_clicked() { QDesktopServices::openUrl(QUrl("https://blog.csdn.net/xiaolong1126626497/article/details/116694318")); }