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

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

一、开发环境介绍

主控芯片:  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 地

传感器的硬件参数介绍:

image.png

传统的测量方法介绍:

image.png

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

image.png

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"));
}


相关实践学习
容器服务Serverless版ACK Serverless 快速入门:在线魔方应用部署和监控
通过本实验,您将了解到容器服务Serverless版ACK Serverless 的基本产品能力,即可以实现快速部署一个在线魔方应用,并借助阿里云容器服务成熟的产品生态,实现在线应用的企业级监控,提升应用稳定性。
云原生实践公开课
课程大纲 开篇:如何学习并实践云原生技术 基础篇: 5 步上手 Kubernetes 进阶篇:生产环境下的 K8s 实践 相关的阿里云产品:容器服务&nbsp;ACK 容器服务&nbsp;Kubernetes&nbsp;版(简称&nbsp;ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情:&nbsp;https://www.aliyun.com/product/kubernetes
目录
相关文章
|
传感器 数据采集 监控
上千个完整设计的单片机、8086、STM32制作教程和资料-转发分享
在网上收集了接近上千个完整设计的单片机、8086、STM32制作教程和资料-转发分享(涵盖了大部分的毕设课设题目),学习单片机的最好教程,也可以作为帮助大家在做电子课设毕设时有利的帮助,可以从以下百度网盘下载(按照编号下载)。
1927 0
上千个完整设计的单片机、8086、STM32制作教程和资料-转发分享
|
传感器 网络协议 物联网
基于STM32设计的智能家居系统(采用ESP8266+OneNet云平台)
基于STM32设计的智能家居系统(采用ESP8266+OneNet云平台)
1289 1
基于STM32设计的智能家居系统(采用ESP8266+OneNet云平台)
|
存储 JSON 移动开发
基于STM32+ESP8266的奥运会奖牌榜设计之经典
基于STM32+ESP8266的奥运会奖牌榜设计之经典
199 0
基于STM32+ESP8266的奥运会奖牌榜设计之经典
|
传感器 数据采集 编解码
基于STM32设计的环境检测设备
设计以STM32微控制器为平台,采用DHT11温湿度传感器、烟雾传感器MQ-2、易燃气体传感器MQ-4、空气质量检测传感器MQ-135对室内温湿度和危险气体进行采集。通过wifi无线网络将数据传送给微控制器,STM32微控制器处理数据后,由自带oled液晶屏显示。当室内温度达到预警值或有危险气体时,系统将会自动警报并将警报信息通过wifi网络传输给客户手机。且每隔一段时间会通过wifi自动发送监测信息到手机,从而实现对室内环境的监测及报警功能。
699 0
|
算法 芯片
基于STM32设计的计算器(实现基本运算)
当前文章介绍的是STM32+LCD触摸屏设计的一个触摸计算器功能,实现基本的加减乘除,二进制转换显示等功能。LCD屏使用的是3.5寸带触摸屏的显示屏,方便操作屏幕,MCU采用STM32F103ZET6。设计的这个计算器用到的硬件不多,主要是LCD屏和触摸屏,用到了一个W25Q64存储芯片。
403 0
|
传感器 算法
基于STM32设计的健康检测设备(测温心率计步)
本文介绍的项目是基于STM32设计的健康检测设备,支持体温测量,心率检测,支持运动计步(采用MPU6050陀螺仪实现),支持WIFI传输数据到手机APP打印显示。
339 0
|
传感器 数据采集 前端开发
基于STM32设计的数字电子秤
当前项目是采用采用STM32+称重模块+OLED实现了简单的电子秤项目,称重模块上采用24位的ADC芯片,精度较高。实现了称重,校准、去皮等功能。
670 0
|
物联网 数据安全/隐私保护 芯片
基于STM32设计的校园一卡通项目
本文介绍通过STM32 微控制器+RFID RC522设计的一个**校园一卡通消费充值机的项目**,可以模拟实现充值、消费、修改密码、挂失、登录、查询.......等操作。
432 0
|
传感器 算法 芯片
基于STM32设计的指针式电子钟与日历
这是基于STM32设计的一个指针式电子钟+万年历小项目,采用3.5寸的LCD屏显示时钟,日历、温度、天气,支持触摸屏调整设置时间,设置闹钟,查看日历等等。整体项目主要是技术点就是LCD屏的图形绘制。比如: 时钟的时针绘制、分针、秒针、表盘、日历绘制等等。
651 0
|
存储 芯片 内存技术
基于STM32设计的掌上游戏机(运行NES游戏模拟器)详细开发过程
基于STM32设计的掌上游戏机(运行NES游戏模拟器)详细开发过程
908 0
基于STM32设计的掌上游戏机(运行NES游戏模拟器)详细开发过程