基于STM32设计的实时心率检测仪

818 阅读12分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第29天,点击查看活动详情

PulseSensor 是一款用于脉搏心率测量的光电反射式模拟传感器。可以将其佩戴于手指、耳垂、手腕等处,通过杜邦线--导线将引脚连接到单片机,可将采集到的模拟信号传输给单片机,单片机配置ADC用来转换为数字信号,再通过单片机简单计算后就可以得到心率数值。

一、开发环境介绍

主控芯片: STM32F103ZET6

代码编程软件: keil5

心率检测模块: PulseSensor

WIFI模块: ESP8266 --可选的。直接使用串口有线传输给上位机也可以。

上位机: C++(QT) 设计的。 支持PC机电脑、Android手机显示。

与上位机的传输协议: 支持串口传输、WIFI网络传输两种。 如果是PC就可以直接连接串口传输数据,如果不方便可以直接通过WIFI---TCP协议传输。

二、PulseSensor心率模块介绍

PulseSensor 是一款用于脉搏心率测量的光电反射式模拟传感器。

可以将其佩戴于手指、耳垂、手腕等处,通过杜邦线--导线将引脚连接到单片机,可将采集到的模拟信号传输给单片机,单片机配置ADC用来转换为数字信号,再通过单片机简单计算后就可以得到心率数值;为了方便联动健康管理系统,也方便自己了解自己的心率,可将脉搏波形通过串口、WIFI等方式上传到电脑、手机显示波形,然后根据提前配置的参数,结合算法确定是否正常。

PulseSensor 是一款开源硬件, 目前国外官网上已有其对应的单片机程序,也附带有对应的上位机Processing 程序, 比较适用于心率方面的科学研究和教学演示,也非常适合用于二次开发;上位机也可以自己开发,根据自己的需求定制,达到自己想要的功能。

image.png

传感器的接口一共 3 个, 其中标有S的为模拟信号输出线 标有+的为电源输入线(中间); 标有-的为地线。

总结一下:

  S → 脉搏信号输出(要接单片机 AD 接口)
  + → 5v(或 3.3v)电源输入
  - → GND 地

传感器的硬件参数介绍:

电路板直径: 16mm 电路板厚度: 1.2mm LED 峰值波长: 515nm 供电电压: 3.35v 检测信号类型:光反射信号(PPG) 输出信号类型:模拟信号 信号放大倍数: 330 倍 输出信号大小: 0VCC 电流大小: ~4ma(5v 下)

传统的测量方法介绍:

传统的脉搏测量方法主要有三种:

一是从心电信号中提取;

二是从测量血压时压力传感器测到的波动来计算脉率;

三是光电容积法。前两种方法提取信号都会限制病人的活动,如果长时间使用会增加病人生理和心理上的不舒适感。而光电容积法脉搏测量作为监护测量中最普遍的方法之一,其具有方法简单、佩戴方便、可靠性高等特点。

光电容积法的基本原理是利用人体组织在血管搏动时造成透光率不同来进行脉搏测量的。其使用的传感器由光源和光电变换器两部分组成,通过绑带或夹子固定在病人的手指或耳垂上。光源一般采用对动脉血中氧和血红蛋白有选择性的一定波长(500nm~700nm)的发光二极管。当光束透过人体外周血管,由于动脉搏动充血容积变化导致这束光的透光率发生改变,此时由光电变换器接收经人体组织反射的光线,转变为电信号并将其放大和输出。由于脉搏是随心脏的搏动而周期性变化的信号,动脉血管容积也周期性变化,因此光电变换器的电信号变化周期就是脉搏率。

根据相关文献和实验结果, 560nm 波长左右的波可以反映皮肤浅部微动脉信息,适合用来提取脉搏信号。

本传感器采用了峰值波长为 515nm 的绿光 LED,型号为 AM2520,而光接收器采用了 APDS-9008, 这是一款环境光感受器,感受峰值波长为 565nm,两者的峰值波长相近,灵敏度较高。此外,由于脉搏信号的频带一般在 0.05~200Hz 之间, 信号幅度均很小,一般在毫伏级水平,容易受到各种信号干扰。在传感器后面使用了低通滤波器和由运放 MCP6001 构成的放大器,将信号放大了 330 倍,同时采用分压电阻设置直流偏置电压为电源电压的 1/2,使放大后的信号可以很好地被单片机的 AD 采集到。

整个心率传感器的结构如下图:

image.png

由于传感器使用的是固定倍数的放大器, 而人体生理信号是微弱信号,细微的差异会导致放大后的信号产生巨大的差别。 所以下图的示波器显示的波形只是理想情况下的波形,每个人的实际效果会略有区别。

image.png

三、STM32的控制代码

STM32的采集代码比较简单,因为就只需要配置对应引脚的ADC功能采集即可。

可以采集10次,去掉最大值最小值取平均值,拿到最终结果再传递给上位机显示。

3.1 ADC的配置代码

 /*
 函数功能: 初始化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设计的上位机代码

4.1 软件运行效果图

软件有两个版本: 1. 网络版本 2. 串口版本

网络版本主要是通过TCP协议传输数据显示,串口版本直接通过串口传输。

image.png

image.png

4.2 widget.cpp代码

image.png

代码较多,这里就主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"));
 }